xref: /webtrees/app/Statistics/Repository/FamilyRepository.php (revision 0c0910bf0f275a14f35d2ccdf698f91f79e269d4)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2019 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17declare(strict_types=1);
18
19namespace Fisharebest\Webtrees\Statistics\Repository;
20
21use Exception;
22use Fisharebest\Webtrees\Family;
23use Fisharebest\Webtrees\Functions\FunctionsDate;
24use Fisharebest\Webtrees\GedcomRecord;
25use Fisharebest\Webtrees\I18N;
26use Fisharebest\Webtrees\Individual;
27use Fisharebest\Webtrees\Statistics\Google\ChartChildren;
28use Fisharebest\Webtrees\Statistics\Google\ChartDivorce;
29use Fisharebest\Webtrees\Statistics\Google\ChartFamilyLargest;
30use Fisharebest\Webtrees\Statistics\Google\ChartMarriage;
31use Fisharebest\Webtrees\Statistics\Google\ChartMarriageAge;
32use Fisharebest\Webtrees\Statistics\Google\ChartNoChildrenFamilies;
33use Fisharebest\Webtrees\Tree;
34use Illuminate\Database\Capsule\Manager as DB;
35use Illuminate\Database\Query\Builder;
36use Illuminate\Database\Query\Expression;
37use Illuminate\Database\Query\JoinClause;
38use stdClass;
39
40use function in_array;
41
42/**
43 *
44 */
45class FamilyRepository
46{
47    /**
48     * @var Tree
49     */
50    private $tree;
51
52    /**
53     * Constructor.
54     *
55     * @param Tree $tree
56     */
57    public function __construct(Tree $tree)
58    {
59        $this->tree = $tree;
60    }
61
62    /**
63     * General query on family.
64     *
65     * @param string $type
66     *
67     * @return string
68     */
69    private function familyQuery(string $type): string
70    {
71        $row = DB::table('families')
72            ->where('f_file', '=', $this->tree->id())
73            ->orderBy('f_numchil', 'desc')
74            ->first();
75
76        if ($row === null) {
77            return '';
78        }
79
80        /** @var Family $family */
81        $family = Family::rowMapper()($row);
82
83        if (!$family->canShow()) {
84            return I18N::translate('This information is private and cannot be shown.');
85        }
86
87        switch ($type) {
88            default:
89            case 'full':
90                return $family->formatList();
91
92            case 'size':
93                return I18N::number((int) $row->f_numchil);
94
95            case 'name':
96                return '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>';
97        }
98    }
99
100    /**
101     * Find the family with the most children.
102     *
103     * @return string
104     */
105    public function largestFamily(): string
106    {
107        return $this->familyQuery('full');
108    }
109
110    /**
111     * Find the number of children in the largest family.
112     *
113     * @return string
114     */
115    public function largestFamilySize(): string
116    {
117        return $this->familyQuery('size');
118    }
119
120    /**
121     * Find the family with the most children.
122     *
123     * @return string
124     */
125    public function largestFamilyName(): string
126    {
127        return $this->familyQuery('name');
128    }
129
130    /**
131     * Find the couple with the most grandchildren.
132     *
133     * @param int $total
134     *
135     * @return array
136     */
137    private function topTenGrandFamilyQuery(int $total): array
138    {
139        return DB::table('families')
140            ->join('link AS children', static function (JoinClause $join): void {
141                $join
142                    ->on('children.l_from', '=', 'f_id')
143                    ->on('children.l_file', '=', 'f_file')
144                    ->where('children.l_type', '=', 'CHIL');
145            })->join('link AS mchildren', static function (JoinClause $join): void {
146                $join
147                    ->on('mchildren.l_file', '=', 'children.l_file')
148                    ->on('mchildren.l_from', '=', 'children.l_to')
149                    ->where('mchildren.l_type', '=', 'FAMS');
150            })->join('link AS gchildren', static function (JoinClause $join): void {
151                $join
152                    ->on('gchildren.l_file', '=', 'mchildren.l_file')
153                    ->on('gchildren.l_from', '=', 'mchildren.l_to')
154                    ->where('gchildren.l_type', '=', 'CHIL');
155            })
156            ->where('f_file', '=', $this->tree->id())
157            ->groupBy(['f_id', 'f_file'])
158            ->orderBy(new Expression('COUNT(*)'), 'DESC')
159            ->select(['families.*'])
160            ->limit($total)
161            ->get()
162            ->map(Family::rowMapper())
163            ->filter(GedcomRecord::accessFilter())
164            ->map(static function (Family $family): array {
165                $count = 0;
166                foreach ($family->children() as $child) {
167                    foreach ($child->spouseFamilies() as $spouse_family) {
168                        $count += $spouse_family->children()->count();
169                    }
170                }
171
172                return [
173                    'family' => $family,
174                    'count'  => $count,
175                ];
176            })
177            ->all();
178    }
179
180    /**
181     * Find the couple with the most grandchildren.
182     *
183     * @param int $total
184     *
185     * @return string
186     */
187    public function topTenLargestGrandFamily(int $total = 10): string
188    {
189        return view('statistics/families/top10-nolist-grand', [
190            'records' => $this->topTenGrandFamilyQuery($total),
191        ]);
192    }
193
194    /**
195     * Find the couple with the most grandchildren.
196     *
197     * @param int $total
198     *
199     * @return string
200     */
201    public function topTenLargestGrandFamilyList(int $total = 10): string
202    {
203        return view('statistics/families/top10-list-grand', [
204            'records' => $this->topTenGrandFamilyQuery($total),
205        ]);
206    }
207
208    /**
209     * Find the families with no children.
210     *
211     * @return int
212     */
213    private function noChildrenFamiliesQuery(): int
214    {
215        return DB::table('families')
216            ->where('f_file', '=', $this->tree->id())
217            ->where('f_numchil', '=', 0)
218            ->count();
219    }
220
221    /**
222     * Find the families with no children.
223     *
224     * @return string
225     */
226    public function noChildrenFamilies(): string
227    {
228        return I18N::number($this->noChildrenFamiliesQuery());
229    }
230
231    /**
232     * Find the families with no children.
233     *
234     * @param string $type
235     *
236     * @return string
237     */
238    public function noChildrenFamiliesList($type = 'list'): string
239    {
240        $families = DB::table('families')
241            ->where('f_file', '=', $this->tree->id())
242            ->where('f_numchil', '=', 0)
243            ->get()
244            ->map(Family::rowMapper())
245            ->filter(GedcomRecord::accessFilter());
246
247        $top10 = [];
248
249        /** @var Family $family */
250        foreach ($families as $family) {
251            if ($type === 'list') {
252                $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a></li>';
253            } else {
254                $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>';
255            }
256        }
257
258        if ($type === 'list') {
259            $top10 = implode('', $top10);
260        } else {
261            $top10 = implode('; ', $top10);
262        }
263
264
265        if ($type === 'list') {
266            return '<ul>' . $top10 . '</ul>';
267        }
268
269        return $top10;
270    }
271
272    /**
273     * Create a chart of children with no families.
274     *
275     * @param int $year1
276     * @param int $year2
277     *
278     * @return string
279     */
280    public function chartNoChildrenFamilies(int $year1 = -1, int $year2 = -1): string
281    {
282        $no_child_fam = $this->noChildrenFamiliesQuery();
283
284        return (new ChartNoChildrenFamilies($this->tree))
285            ->chartNoChildrenFamilies($no_child_fam, $year1, $year2);
286    }
287
288    /**
289     * Returns the ages between siblings.
290     *
291     * @param int $total The total number of records to query
292     *
293     * @return array
294     */
295    private function ageBetweenSiblingsQuery(int $total): array
296    {
297        $prefix = DB::connection()->getTablePrefix();
298
299        return DB::table('link AS link1')
300            ->join('link AS link2', static function (JoinClause $join): void {
301                $join
302                    ->on('link2.l_from', '=', 'link1.l_from')
303                    ->on('link2.l_type', '=', 'link1.l_type')
304                    ->on('link2.l_file', '=', 'link1.l_file');
305            })
306            ->join('dates AS child1', static function (JoinClause $join): void {
307                $join
308                    ->on('child1.d_gid', '=', 'link1.l_to')
309                    ->on('child1.d_file', '=', 'link1.l_file')
310                    ->where('child1.d_fact', '=', 'BIRT')
311                    ->where('child1.d_julianday1', '<>', 0);
312            })
313            ->join('dates AS child2', static function (JoinClause $join): void {
314                $join
315                    ->on('child2.d_gid', '=', 'link2.l_to')
316                    ->on('child2.d_file', '=', 'link2.l_file')
317                    ->where('child2.d_fact', '=', 'BIRT')
318                    ->whereColumn('child2.d_julianday2', '>', 'child1.d_julianday1');
319            })
320            ->where('link1.l_type', '=', 'CHIL')
321            ->where('link1.l_file', '=', $this->tree->id())
322            ->distinct()
323            ->select(['link1.l_from AS family', 'link1.l_to AS ch1', 'link2.l_to AS ch2', new Expression($prefix . 'child2.d_julianday2 - ' . $prefix . 'child1.d_julianday1 AS age')])
324            ->orderBy('age', 'DESC')
325            ->take($total)
326            ->get()
327            ->all();
328    }
329
330    /**
331     * Returns the calculated age the time of event.
332     *
333     * @param int $age The age from the database record
334     *
335     * @return string
336     */
337    private function calculateAge(int $age): string
338    {
339        if ((int) ($age / 365.25) > 0) {
340            $result = (int) ($age / 365.25) . 'y';
341        } elseif ((int) ($age / 30.4375) > 0) {
342            $result = (int) ($age / 30.4375) . 'm';
343        } else {
344            $result = $age . 'd';
345        }
346
347        return FunctionsDate::getAgeAtEvent($result);
348    }
349
350    /**
351     * Find the ages between siblings.
352     *
353     * @param int $total The total number of records to query
354     *
355     * @return array
356     * @throws Exception
357     */
358    private function ageBetweenSiblingsNoList(int $total): array
359    {
360        $rows = $this->ageBetweenSiblingsQuery($total);
361
362        foreach ($rows as $fam) {
363            $family = Family::getInstance($fam->family, $this->tree);
364            $child1 = Individual::getInstance($fam->ch1, $this->tree);
365            $child2 = Individual::getInstance($fam->ch2, $this->tree);
366
367            if ($child1->canShow() && $child2->canShow()) {
368                // ! Single array (no list)
369                return [
370                    'child1' => $child1,
371                    'child2' => $child2,
372                    'family' => $family,
373                    'age'    => $this->calculateAge((int) $fam->age),
374                ];
375            }
376        }
377
378        return [];
379    }
380
381    /**
382     * Find the ages between siblings.
383     *
384     * @param int  $total The total number of records to query
385     * @param bool $one   Include each family only once if true
386     *
387     * @return array
388     * @throws Exception
389     */
390    private function ageBetweenSiblingsList(int $total, bool $one): array
391    {
392        $rows  = $this->ageBetweenSiblingsQuery($total);
393        $top10 = [];
394        $dist  = [];
395
396        foreach ($rows as $fam) {
397            $family = Family::getInstance($fam->family, $this->tree);
398            $child1 = Individual::getInstance($fam->ch1, $this->tree);
399            $child2 = Individual::getInstance($fam->ch2, $this->tree);
400
401            $age = $this->calculateAge((int) $fam->age);
402
403            if ($one && !in_array($fam->family, $dist, true)) {
404                if ($child1->canShow() && $child2->canShow()) {
405                    $top10[] = [
406                        'child1' => $child1,
407                        'child2' => $child2,
408                        'family' => $family,
409                        'age'    => $age,
410                    ];
411
412                    $dist[] = $fam->family;
413                }
414            } elseif (!$one && $child1->canShow() && $child2->canShow()) {
415                $top10[] = [
416                    'child1' => $child1,
417                    'child2' => $child2,
418                    'family' => $family,
419                    'age'    => $age,
420                ];
421            }
422        }
423
424        return $top10;
425    }
426
427    /**
428     * Find the ages between siblings.
429     *
430     * @param int $total The total number of records to query
431     *
432     * @return string
433     */
434    private function ageBetweenSiblingsAge(int $total): string
435    {
436        $rows = $this->ageBetweenSiblingsQuery($total);
437
438        foreach ($rows as $fam) {
439            return $this->calculateAge((int) $fam->age);
440        }
441
442        return '';
443    }
444
445    /**
446     * Find the ages between siblings.
447     *
448     * @param int $total The total number of records to query
449     *
450     * @return string
451     * @throws Exception
452     */
453    private function ageBetweenSiblingsName(int $total): string
454    {
455        $rows = $this->ageBetweenSiblingsQuery($total);
456
457        foreach ($rows as $fam) {
458            $family = Family::getInstance($fam->family, $this->tree);
459            $child1 = Individual::getInstance($fam->ch1, $this->tree);
460            $child2 = Individual::getInstance($fam->ch2, $this->tree);
461
462            if ($child1->canShow() && $child2->canShow()) {
463                $return = '<a href="' . e($child2->url()) . '">' . $child2->fullName() . '</a> ';
464                $return .= I18N::translate('and') . ' ';
465                $return .= '<a href="' . e($child1->url()) . '">' . $child1->fullName() . '</a>';
466                $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>';
467            } else {
468                $return = I18N::translate('This information is private and cannot be shown.');
469            }
470
471            return $return;
472        }
473
474        return '';
475    }
476
477    /**
478     * Find the names of siblings with the widest age gap.
479     *
480     * @param int $total
481     *
482     * @return string
483     */
484    public function topAgeBetweenSiblingsName(int $total = 10): string
485    {
486        return $this->ageBetweenSiblingsName($total);
487    }
488
489    /**
490     * Find the widest age gap between siblings.
491     *
492     * @param int $total
493     *
494     * @return string
495     */
496    public function topAgeBetweenSiblings(int $total = 10): string
497    {
498        return $this->ageBetweenSiblingsAge($total);
499    }
500
501    /**
502     * Find the name of siblings with the widest age gap.
503     *
504     * @param int $total
505     *
506     * @return string
507     */
508    public function topAgeBetweenSiblingsFullName(int $total = 10): string
509    {
510        $record = $this->ageBetweenSiblingsNoList($total);
511
512        if (empty($record)) {
513            return I18N::translate('This information is not available.');
514        }
515
516        return view('statistics/families/top10-nolist-age', [
517            'record' => $record,
518        ]);
519    }
520
521    /**
522     * Find the siblings with the widest age gaps.
523     *
524     * @param int    $total
525     * @param string $one
526     *
527     * @return string
528     */
529    public function topAgeBetweenSiblingsList(int $total = 10, string $one = ''): string
530    {
531        $records = $this->ageBetweenSiblingsList($total, (bool) $one);
532
533        return view('statistics/families/top10-list-age', [
534            'records' => $records,
535        ]);
536    }
537
538    /**
539     * General query on familes/children.
540     *
541     * @param int    $year1
542     * @param int    $year2
543     *
544     * @return stdClass[]
545     */
546    public function statsChildrenQuery(int $year1 = -1, int $year2 = -1): array
547    {
548        $query = DB::table('families')
549            ->where('f_file', '=', $this->tree->id())
550            ->groupBy(['f_numchil'])
551            ->select(['f_numchil', new Expression('COUNT(*) AS total')]);
552
553        if ($year1 >= 0 && $year2 >= 0) {
554            $query
555                ->join('dates', static function (JoinClause $join): void {
556                    $join
557                        ->on('d_file', '=', 'f_file')
558                        ->on('d_gid', '=', 'f_id');
559                })
560                ->where('d_fact', '=', 'MARR')
561                ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
562                ->whereBetween('d_year', [$year1, $year2]);
563        }
564
565        return $query->get()->all();
566    }
567
568    /**
569     * Genearl query on families/children.
570     *
571     * @return string
572     */
573    public function statsChildren(): string
574    {
575        return (new ChartChildren($this->tree))
576            ->chartChildren();
577    }
578
579    /**
580     * Count the total children.
581     *
582     * @return string
583     */
584    public function totalChildren(): string
585    {
586        $total = (int) DB::table('families')
587            ->where('f_file', '=', $this->tree->id())
588            ->sum('f_numchil');
589
590        return I18N::number($total);
591    }
592
593    /**
594     * Find the average number of children in families.
595     *
596     * @return string
597     */
598    public function averageChildren(): string
599    {
600        $average = (float) DB::table('families')
601            ->where('f_file', '=', $this->tree->id())
602            ->avg('f_numchil');
603
604        return I18N::number($average, 2);
605    }
606
607    /**
608     * General query on families.
609     *
610     * @param int $total
611     *
612     * @return array
613     */
614    private function topTenFamilyQuery(int $total): array
615    {
616        return DB::table('families')
617            ->where('f_file', '=', $this->tree->id())
618            ->orderBy('f_numchil', 'DESC')
619            ->limit($total)
620            ->get()
621            ->map(Family::rowMapper())
622            ->filter(GedcomRecord::accessFilter())
623            ->map(static function (Family $family): array {
624                return [
625                    'family' => $family,
626                    'count'  => $family->numberOfChildren(),
627                ];
628            })
629            ->all();
630    }
631
632    /**
633     * The the families with the most children.
634     *
635     * @param int $total
636     *
637     * @return string
638     */
639    public function topTenLargestFamily(int $total = 10): string
640    {
641        $records = $this->topTenFamilyQuery($total);
642
643        return view('statistics/families/top10-nolist', [
644            'records' => $records,
645        ]);
646    }
647
648    /**
649     * Find the families with the most children.
650     *
651     * @param int $total
652     *
653     * @return string
654     */
655    public function topTenLargestFamilyList(int $total = 10): string
656    {
657        $records = $this->topTenFamilyQuery($total);
658
659        return view('statistics/families/top10-list', [
660            'records' => $records,
661        ]);
662    }
663
664    /**
665     * Create a chart of the largest families.
666     *
667     * @param string|null $color_from
668     * @param string|null $color_to
669     * @param int         $total
670     *
671     * @return string
672     */
673    public function chartLargestFamilies(
674        string $color_from = null,
675        string $color_to = null,
676        int $total = 10
677    ): string {
678        return (new ChartFamilyLargest($this->tree))
679            ->chartLargestFamilies($color_from, $color_to, $total);
680    }
681
682    /**
683     * Find the month in the year of the birth of the first child.
684     *
685     * @param int $year1
686     * @param int $year2
687     *
688     * @return Builder
689     */
690    public function monthFirstChildQuery(int $year1 = -1, int $year2 = -1): Builder
691    {
692        $first_child_subquery = DB::table('link')
693            ->join('dates', static function (JoinClause $join): void {
694                $join
695                    ->on('d_gid', '=', 'l_to')
696                    ->on('d_file', '=', 'l_file')
697                    ->where('d_julianday1', '<>', 0)
698                    ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']);
699            })
700            ->where('l_file', '=', $this->tree->id())
701            ->where('l_type', '=', 'CHIL')
702            ->select(['l_from AS family_id', new Expression('MIN(d_julianday1) AS min_birth_jd')])
703            ->groupBy(['family_id']);
704
705        $query = DB::table('link')
706            ->join('dates', static function (JoinClause $join): void {
707                $join
708                    ->on('d_gid', '=', 'l_to')
709                    ->on('d_file', '=', 'l_file');
710            })
711            ->joinSub($first_child_subquery, 'subquery', static function (JoinClause $join): void {
712                $join
713                    ->on('family_id', '=', 'l_from')
714                    ->on('min_birth_jd', '=', 'd_julianday1');
715            })
716            ->where('link.l_file', '=', $this->tree->id())
717            ->where('link.l_type', '=', 'CHIL')
718            ->select(['d_month', new Expression('COUNT(*) AS total')])
719            ->groupBy(['d_month']);
720
721        if ($year1 >= 0 && $year2 >= 0) {
722            $query->whereBetween('d_year', [$year1, $year2]);
723        }
724
725        return $query;
726    }
727
728    /**
729     * Find the month in the year of the birth of the first child.
730     *
731     * @param int $year1
732     * @param int $year2
733     *
734     * @return Builder
735     */
736    public function monthFirstChildBySexQuery(int $year1 = -1, int $year2 = -1): Builder
737    {
738        return $this->monthFirstChildQuery($year1, $year2)
739                ->join('individuals', static function (JoinClause $join): void {
740                    $join
741                        ->on('i_file', '=', 'l_file')
742                        ->on('i_id', '=', 'l_to');
743                })
744                ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
745                ->groupBy(['d_month', 'i_sex']);
746    }
747
748    /**
749     * Number of husbands.
750     *
751     * @return string
752     */
753    public function totalMarriedMales(): string
754    {
755        $n = (int) DB::table('families')
756            ->where('f_file', '=', $this->tree->id())
757            ->where('f_gedcom', 'LIKE', "%\n1 MARR%")
758            ->distinct()
759            ->count('f_husb');
760
761        return I18N::number($n);
762    }
763
764    /**
765     * Number of wives.
766     *
767     * @return string
768     */
769    public function totalMarriedFemales(): string
770    {
771        $n = (int) DB::table('families')
772            ->where('f_file', '=', $this->tree->id())
773            ->where('f_gedcom', 'LIKE', "%\n1 MARR%")
774            ->distinct()
775            ->count('f_wife');
776
777        return I18N::number($n);
778    }
779
780    /**
781     * General query on parents.
782     *
783     * @param string $type
784     * @param string $age_dir
785     * @param string $sex
786     * @param bool   $show_years
787     *
788     * @return string
789     */
790    private function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string
791    {
792        if ($sex === 'F') {
793            $sex_field = 'WIFE';
794        } else {
795            $sex_field = 'HUSB';
796        }
797
798        if ($age_dir !== 'ASC') {
799            $age_dir = 'DESC';
800        }
801
802        $prefix = DB::connection()->getTablePrefix();
803
804        $row = DB::table('link AS parentfamily')
805            ->join('link AS childfamily', static function (JoinClause $join): void {
806                $join
807                    ->on('childfamily.l_file', '=', 'parentfamily.l_file')
808                    ->on('childfamily.l_from', '=', 'parentfamily.l_from')
809                    ->where('childfamily.l_type', '=', 'CHIL');
810            })
811            ->join('dates AS birth', static function (JoinClause $join): void {
812                $join
813                    ->on('birth.d_file', '=', 'parentfamily.l_file')
814                    ->on('birth.d_gid', '=', 'parentfamily.l_to')
815                    ->where('birth.d_fact', '=', 'BIRT')
816                    ->where('birth.d_julianday1', '<>', 0);
817            })
818            ->join('dates AS childbirth', static function (JoinClause $join): void {
819                $join
820                    ->on('childbirth.d_file', '=', 'parentfamily.l_file')
821                    ->on('childbirth.d_gid', '=', 'childfamily.l_to')
822                    ->where('childbirth.d_fact', '=', 'BIRT');
823            })
824            ->where('childfamily.l_file', '=', $this->tree->id())
825            ->where('parentfamily.l_type', '=', $sex_field)
826            ->where('childbirth.d_julianday2', '>', 'birth.d_julianday1')
827            ->select(['parentfamily.l_to AS id', new Expression($prefix . 'childbirth.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')])
828            ->take(1)
829            ->orderBy('age', $age_dir)
830            ->get()
831            ->first();
832
833        if ($row === null) {
834            return '';
835        }
836
837        $person = Individual::getInstance($row->id, $this->tree);
838
839        switch ($type) {
840            default:
841            case 'full':
842                if ($person && $person->canShow()) {
843                    $result = $person->formatList();
844                } else {
845                    $result = I18N::translate('This information is private and cannot be shown.');
846                }
847                break;
848
849            case 'name':
850                $result = '<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>';
851                break;
852
853            case 'age':
854                $age = $row->age;
855
856                if ($show_years) {
857                    $result = $this->calculateAge((int) $row->age);
858                } else {
859                    $result = (string) floor($age / 365.25);
860                }
861
862                break;
863        }
864
865        return $result;
866    }
867
868    /**
869     * Find the youngest mother
870     *
871     * @return string
872     */
873    public function youngestMother(): string
874    {
875        return $this->parentsQuery('full', 'ASC', 'F', false);
876    }
877
878    /**
879     * Find the name of the youngest mother.
880     *
881     * @return string
882     */
883    public function youngestMotherName(): string
884    {
885        return $this->parentsQuery('name', 'ASC', 'F', false);
886    }
887
888    /**
889     * Find the age of the youngest mother.
890     *
891     * @param string $show_years
892     *
893     * @return string
894     */
895    public function youngestMotherAge(string $show_years = ''): string
896    {
897        return $this->parentsQuery('age', 'ASC', 'F', (bool) $show_years);
898    }
899
900    /**
901     * Find the oldest mother.
902     *
903     * @return string
904     */
905    public function oldestMother(): string
906    {
907        return $this->parentsQuery('full', 'DESC', 'F', false);
908    }
909
910    /**
911     * Find the name of the oldest mother.
912     *
913     * @return string
914     */
915    public function oldestMotherName(): string
916    {
917        return $this->parentsQuery('name', 'DESC', 'F', false);
918    }
919
920    /**
921     * Find the age of the oldest mother.
922     *
923     * @param string $show_years
924     *
925     * @return string
926     */
927    public function oldestMotherAge(string $show_years = ''): string
928    {
929        return $this->parentsQuery('age', 'DESC', 'F', (bool) $show_years);
930    }
931
932    /**
933     * Find the youngest father.
934     *
935     * @return string
936     */
937    public function youngestFather(): string
938    {
939        return $this->parentsQuery('full', 'ASC', 'M', false);
940    }
941
942    /**
943     * Find the name of the youngest father.
944     *
945     * @return string
946     */
947    public function youngestFatherName(): string
948    {
949        return $this->parentsQuery('name', 'ASC', 'M', false);
950    }
951
952    /**
953     * Find the age of the youngest father.
954     *
955     * @param string $show_years
956     *
957     * @return string
958     */
959    public function youngestFatherAge(string $show_years = ''): string
960    {
961        return $this->parentsQuery('age', 'ASC', 'M', (bool) $show_years);
962    }
963
964    /**
965     * Find the oldest father.
966     *
967     * @return string
968     */
969    public function oldestFather(): string
970    {
971        return $this->parentsQuery('full', 'DESC', 'M', false);
972    }
973
974    /**
975     * Find the name of the oldest father.
976     *
977     * @return string
978     */
979    public function oldestFatherName(): string
980    {
981        return $this->parentsQuery('name', 'DESC', 'M', false);
982    }
983
984    /**
985     * Find the age of the oldest father.
986     *
987     * @param string $show_years
988     *
989     * @return string
990     */
991    public function oldestFatherAge(string $show_years = ''): string
992    {
993        return $this->parentsQuery('age', 'DESC', 'M', (bool) $show_years);
994    }
995
996    /**
997     * General query on age at marriage.
998     *
999     * @param string $type
1000     * @param string $age_dir "ASC" or "DESC"
1001     * @param int    $total
1002     *
1003     * @return string
1004     */
1005    private function ageOfMarriageQuery(string $type, string $age_dir, int $total): string
1006    {
1007        $prefix = DB::connection()->getTablePrefix();
1008
1009        $hrows = DB::table('families')
1010            ->where('f_file', '=', $this->tree->id())
1011            ->join('dates AS married', static function (JoinClause $join): void {
1012                $join
1013                    ->on('married.d_file', '=', 'f_file')
1014                    ->on('married.d_gid', '=', 'f_id')
1015                    ->where('married.d_fact', '=', 'MARR')
1016                    ->where('married.d_julianday1', '<>', 0);
1017            })
1018            ->join('dates AS husbdeath', static function (JoinClause $join): void {
1019                $join
1020                    ->on('husbdeath.d_gid', '=', 'f_husb')
1021                    ->on('husbdeath.d_file', '=', 'f_file')
1022                    ->where('husbdeath.d_fact', '=', 'DEAT');
1023            })
1024            ->whereColumn('married.d_julianday1', '<', 'husbdeath.d_julianday2')
1025            ->groupBy(['f_id'])
1026            ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'husbdeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')])
1027            ->get()
1028            ->all();
1029
1030        $wrows = DB::table('families')
1031            ->where('f_file', '=', $this->tree->id())
1032            ->join('dates AS married', static function (JoinClause $join): void {
1033                $join
1034                    ->on('married.d_file', '=', 'f_file')
1035                    ->on('married.d_gid', '=', 'f_id')
1036                    ->where('married.d_fact', '=', 'MARR')
1037                    ->where('married.d_julianday1', '<>', 0);
1038            })
1039            ->join('dates AS wifedeath', static function (JoinClause $join): void {
1040                $join
1041                    ->on('wifedeath.d_gid', '=', 'f_wife')
1042                    ->on('wifedeath.d_file', '=', 'f_file')
1043                    ->where('wifedeath.d_fact', '=', 'DEAT');
1044            })
1045            ->whereColumn('married.d_julianday1', '<', 'wifedeath.d_julianday2')
1046            ->groupBy(['f_id'])
1047            ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'wifedeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')])
1048            ->get()
1049            ->all();
1050
1051        $drows = DB::table('families')
1052            ->where('f_file', '=', $this->tree->id())
1053            ->join('dates AS married', static function (JoinClause $join): void {
1054                $join
1055                    ->on('married.d_file', '=', 'f_file')
1056                    ->on('married.d_gid', '=', 'f_id')
1057                    ->where('married.d_fact', '=', 'MARR')
1058                    ->where('married.d_julianday1', '<>', 0);
1059            })
1060            ->join('dates AS divorced', static function (JoinClause $join): void {
1061                $join
1062                    ->on('divorced.d_gid', '=', 'f_id')
1063                    ->on('divorced.d_file', '=', 'f_file')
1064                    ->whereIn('divorced.d_fact', ['DIV', 'ANUL', '_SEPR']);
1065            })
1066            ->whereColumn('married.d_julianday1', '<', 'divorced.d_julianday2')
1067            ->groupBy(['f_id'])
1068            ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'divorced.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')])
1069            ->get()
1070            ->all();
1071
1072        $rows = [];
1073        foreach ($drows as $family) {
1074            $rows[$family->family] = $family->age;
1075        }
1076
1077        foreach ($hrows as $family) {
1078            if (!isset($rows[$family->family])) {
1079                $rows[$family->family] = $family->age;
1080            }
1081        }
1082
1083        foreach ($wrows as $family) {
1084            if (!isset($rows[$family->family])) {
1085                $rows[$family->family] = $family->age;
1086            } elseif ($rows[$family->family] > $family->age) {
1087                $rows[$family->family] = $family->age;
1088            }
1089        }
1090
1091        if ($age_dir === 'DESC') {
1092            arsort($rows);
1093        } else {
1094            asort($rows);
1095        }
1096
1097        $top10 = [];
1098        $i     = 0;
1099        foreach ($rows as $xref => $age) {
1100            $family = Family::getInstance((string) $xref, $this->tree);
1101            if ($type === 'name') {
1102                return $family->formatList();
1103            }
1104
1105            $age = $this->calculateAge((int) $age);
1106
1107            if ($type === 'age') {
1108                return $age;
1109            }
1110
1111            $husb = $family->husband();
1112            $wife = $family->wife();
1113
1114            if (($husb && ($husb->getAllDeathDates() || !$husb->isDead())) && ($wife && ($wife->getAllDeathDates() || !$wife->isDead()))) {
1115                if ($family && $family->canShow()) {
1116                    if ($type === 'list') {
1117                        $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')' . '</li>';
1118                    } else {
1119                        $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')';
1120                    }
1121                }
1122                if (++$i === $total) {
1123                    break;
1124                }
1125            }
1126        }
1127
1128        if ($type === 'list') {
1129            $top10 = implode('', $top10);
1130        } else {
1131            $top10 = implode('; ', $top10);
1132        }
1133
1134        if (I18N::direction() === 'rtl') {
1135            $top10 = str_replace([
1136                '[',
1137                ']',
1138                '(',
1139                ')',
1140                '+',
1141            ], [
1142                '&rlm;[',
1143                '&rlm;]',
1144                '&rlm;(',
1145                '&rlm;)',
1146                '&rlm;+',
1147            ], $top10);
1148        }
1149
1150        if ($type === 'list') {
1151            return '<ul>' . $top10 . '</ul>';
1152        }
1153
1154        return $top10;
1155    }
1156
1157    /**
1158     * General query on marriage ages.
1159     *
1160     * @return string
1161     */
1162    public function topAgeOfMarriageFamily(): string
1163    {
1164        return $this->ageOfMarriageQuery('name', 'DESC', 1);
1165    }
1166
1167    /**
1168     * General query on marriage ages.
1169     *
1170     * @return string
1171     */
1172    public function topAgeOfMarriage(): string
1173    {
1174        return $this->ageOfMarriageQuery('age', 'DESC', 1);
1175    }
1176
1177    /**
1178     * General query on marriage ages.
1179     *
1180     * @param int $total
1181     *
1182     * @return string
1183     */
1184    public function topAgeOfMarriageFamilies(int $total = 10): string
1185    {
1186        return $this->ageOfMarriageQuery('nolist', 'DESC', $total);
1187    }
1188
1189    /**
1190     * General query on marriage ages.
1191     *
1192     * @param int $total
1193     *
1194     * @return string
1195     */
1196    public function topAgeOfMarriageFamiliesList(int $total = 10): string
1197    {
1198        return $this->ageOfMarriageQuery('list', 'DESC', $total);
1199    }
1200
1201    /**
1202     * General query on marriage ages.
1203     *
1204     * @return string
1205     */
1206    public function minAgeOfMarriageFamily(): string
1207    {
1208        return $this->ageOfMarriageQuery('name', 'ASC', 1);
1209    }
1210
1211    /**
1212     * General query on marriage ages.
1213     *
1214     * @return string
1215     */
1216    public function minAgeOfMarriage(): string
1217    {
1218        return $this->ageOfMarriageQuery('age', 'ASC', 1);
1219    }
1220
1221    /**
1222     * General query on marriage ages.
1223     *
1224     * @param int $total
1225     *
1226     * @return string
1227     */
1228    public function minAgeOfMarriageFamilies(int $total = 10): string
1229    {
1230        return $this->ageOfMarriageQuery('nolist', 'ASC', $total);
1231    }
1232
1233    /**
1234     * General query on marriage ages.
1235     *
1236     * @param int $total
1237     *
1238     * @return string
1239     */
1240    public function minAgeOfMarriageFamiliesList(int $total = 10): string
1241    {
1242        return $this->ageOfMarriageQuery('list', 'ASC', $total);
1243    }
1244
1245    /**
1246     * Find the ages between spouses.
1247     *
1248     * @param string $age_dir
1249     * @param int    $total
1250     *
1251     * @return array
1252     */
1253    private function ageBetweenSpousesQuery(string $age_dir, int $total): array
1254    {
1255        $prefix = DB::connection()->getTablePrefix();
1256
1257        $query = DB::table('families')
1258            ->where('f_file', '=', $this->tree->id())
1259            ->join('dates AS wife', static function (JoinClause $join): void {
1260                $join
1261                    ->on('wife.d_gid', '=', 'f_wife')
1262                    ->on('wife.d_file', '=', 'f_file')
1263                    ->where('wife.d_fact', '=', 'BIRT')
1264                    ->where('wife.d_julianday1', '<>', 0);
1265            })
1266            ->join('dates AS husb', static function (JoinClause $join): void {
1267                $join
1268                    ->on('husb.d_gid', '=', 'f_husb')
1269                    ->on('husb.d_file', '=', 'f_file')
1270                    ->where('husb.d_fact', '=', 'BIRT')
1271                    ->where('husb.d_julianday1', '<>', 0);
1272            });
1273
1274        if ($age_dir === 'DESC') {
1275            $query
1276                ->whereColumn('wife.d_julianday1', '>=', 'husb.d_julianday1')
1277                ->orderBy(new Expression('MIN(' . $prefix . 'wife.d_julianday1) - MIN(' . $prefix . 'husb.d_julianday1)'), 'DESC');
1278        } else {
1279            $query
1280                ->whereColumn('husb.d_julianday1', '>=', 'wife.d_julianday1')
1281                ->orderBy(new Expression('MIN(' . $prefix . 'husb.d_julianday1) - MIN(' . $prefix . 'wife.d_julianday1)'), 'DESC');
1282        }
1283
1284        return $query
1285            ->groupBy(['f_id', 'f_file'])
1286            ->select(['families.*'])
1287            ->take($total)
1288            ->get()
1289            ->map(Family::rowMapper())
1290            ->filter(GedcomRecord::accessFilter())
1291            ->map(function (Family $family) use ($age_dir): array {
1292                $husb_birt_jd = $family->husband()->getBirthDate()->minimumJulianDay();
1293                $wife_birt_jd = $family->wife()->getBirthDate()->minimumJulianDay();
1294
1295                if ($age_dir === 'DESC') {
1296                    $diff = $wife_birt_jd - $husb_birt_jd;
1297                } else {
1298                    $diff = $husb_birt_jd - $wife_birt_jd;
1299                }
1300
1301                return [
1302                    'family' => $family,
1303                    'age'    => $this->calculateAge($diff),
1304                ];
1305            })
1306            ->all();
1307    }
1308
1309    /**
1310     * Find the age between husband and wife.
1311     *
1312     * @param int $total
1313     *
1314     * @return string
1315     */
1316    public function ageBetweenSpousesMF(int $total = 10): string
1317    {
1318        $records = $this->ageBetweenSpousesQuery('DESC', $total);
1319
1320        return view('statistics/families/top10-nolist-spouses', [
1321            'records' => $records,
1322        ]);
1323    }
1324
1325    /**
1326     * Find the age between husband and wife.
1327     *
1328     * @param int $total
1329     *
1330     * @return string
1331     */
1332    public function ageBetweenSpousesMFList(int $total = 10): string
1333    {
1334        $records = $this->ageBetweenSpousesQuery('DESC', $total);
1335
1336        return view('statistics/families/top10-list-spouses', [
1337            'records' => $records,
1338        ]);
1339    }
1340
1341    /**
1342     * Find the age between wife and husband..
1343     *
1344     * @param int $total
1345     *
1346     * @return string
1347     */
1348    public function ageBetweenSpousesFM(int $total = 10): string
1349    {
1350        $records = $this->ageBetweenSpousesQuery('ASC', $total);
1351
1352        return view('statistics/families/top10-nolist-spouses', [
1353            'records' => $records,
1354        ]);
1355    }
1356
1357    /**
1358     * Find the age between wife and husband..
1359     *
1360     * @param int $total
1361     *
1362     * @return string
1363     */
1364    public function ageBetweenSpousesFMList(int $total = 10): string
1365    {
1366        $records = $this->ageBetweenSpousesQuery('ASC', $total);
1367
1368        return view('statistics/families/top10-list-spouses', [
1369            'records' => $records,
1370        ]);
1371    }
1372
1373    /**
1374     * General query on ages at marriage.
1375     *
1376     * @param string $sex   "M" or "F"
1377     * @param int    $year1
1378     * @param int    $year2
1379     *
1380     * @return array
1381     */
1382    public function statsMarrAgeQuery($sex, $year1 = -1, $year2 = -1): array
1383    {
1384        $prefix = DB::connection()->getTablePrefix();
1385
1386        $query = DB::table('dates AS married')
1387            ->join('families', static function (JoinClause $join): void {
1388                $join
1389                    ->on('f_file', '=', 'married.d_file')
1390                    ->on('f_id', '=', 'married.d_gid');
1391            })
1392            ->join('dates AS birth', static function (JoinClause $join) use ($sex): void {
1393                $join
1394                    ->on('birth.d_file', '=', 'married.d_file')
1395                    ->on('birth.d_gid', '=', $sex === 'M' ? 'f_husb' : 'f_wife')
1396                    ->where('birth.d_julianday1', '<>', 0)
1397                    ->where('birth.d_fact', '=', 'BIRT')
1398                    ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
1399            })
1400            ->where('married.d_file', '=', $this->tree->id())
1401            ->where('married.d_fact', '=', 'MARR')
1402            ->whereIn('married.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1403            ->whereColumn('married.d_julianday1', '>', 'birth.d_julianday1')
1404            ->select(['f_id', 'birth.d_gid', new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]);
1405
1406        if ($year1 >= 0 && $year2 >= 0) {
1407            $query->whereBetween('married.d_year', [$year1, $year2]);
1408        }
1409
1410        return $query
1411            ->get()
1412            ->map(static function (stdClass $row): stdClass {
1413                $row->age = (int) $row->age;
1414
1415                return $row;
1416            })
1417            ->all();
1418    }
1419
1420    /**
1421     * General query on marriage ages.
1422     *
1423     * @return string
1424     */
1425    public function statsMarrAge(): string
1426    {
1427        return (new ChartMarriageAge($this->tree))
1428            ->chartMarriageAge();
1429    }
1430
1431    /**
1432     * Query the database for marriage tags.
1433     *
1434     * @param string $type       "full", "name" or "age"
1435     * @param string $age_dir    "ASC" or "DESC"
1436     * @param string $sex        "F" or "M"
1437     * @param bool   $show_years
1438     *
1439     * @return string
1440     */
1441    private function marriageQuery(string $type, string $age_dir, string $sex, bool $show_years): string
1442    {
1443        if ($sex === 'F') {
1444            $sex_field = 'f_wife';
1445        } else {
1446            $sex_field = 'f_husb';
1447        }
1448
1449        if ($age_dir !== 'ASC') {
1450            $age_dir = 'DESC';
1451        }
1452
1453        $prefix = DB::connection()->getTablePrefix();
1454
1455        $row = DB::table('families')
1456            ->join('dates AS married', static function (JoinClause $join): void {
1457                $join
1458                    ->on('married.d_file', '=', 'f_file')
1459                    ->on('married.d_gid', '=', 'f_id')
1460                    ->where('married.d_fact', '=', 'MARR');
1461            })
1462            ->join('individuals', static function (JoinClause $join) use ($sex, $sex_field): void {
1463                $join
1464                    ->on('i_file', '=', 'f_file')
1465                    ->on('i_id', '=', $sex_field)
1466                    ->where('i_sex', '=', $sex);
1467            })
1468            ->join('dates AS birth', static function (JoinClause $join): void {
1469                $join
1470                    ->on('birth.d_file', '=', 'i_file')
1471                    ->on('birth.d_gid', '=', 'i_id')
1472                    ->where('birth.d_fact', '=', 'BIRT')
1473                    ->where('birth.d_julianday1', '<>', 0);
1474            })
1475            ->where('f_file', '=', $this->tree->id())
1476            ->where('married.d_julianday2', '>', 'birth.d_julianday1')
1477            ->orderBy(new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1'), $age_dir)
1478            ->select(['f_id AS famid', $sex_field, new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age'), 'i_id'])
1479            ->take(1)
1480            ->get()
1481            ->first();
1482
1483        if ($row === null) {
1484            return '';
1485        }
1486
1487        $family = Family::getInstance($row->famid, $this->tree);
1488        $person = Individual::getInstance($row->i_id, $this->tree);
1489
1490        switch ($type) {
1491            default:
1492            case 'full':
1493                if ($family && $family->canShow()) {
1494                    $result = $family->formatList();
1495                } else {
1496                    $result = I18N::translate('This information is private and cannot be shown.');
1497                }
1498                break;
1499
1500            case 'name':
1501                $result = '<a href="' . e($family->url()) . '">' . $person->fullName() . '</a>';
1502                break;
1503
1504            case 'age':
1505                $age = $row->age;
1506
1507                if ($show_years) {
1508                    $result = $this->calculateAge((int) $row->age);
1509                } else {
1510                    $result = I18N::number((int) ($age / 365.25));
1511                }
1512
1513                break;
1514        }
1515
1516        return $result;
1517    }
1518
1519    /**
1520     * Find the youngest wife.
1521     *
1522     * @return string
1523     */
1524    public function youngestMarriageFemale(): string
1525    {
1526        return $this->marriageQuery('full', 'ASC', 'F', false);
1527    }
1528
1529    /**
1530     * Find the name of the youngest wife.
1531     *
1532     * @return string
1533     */
1534    public function youngestMarriageFemaleName(): string
1535    {
1536        return $this->marriageQuery('name', 'ASC', 'F', false);
1537    }
1538
1539    /**
1540     * Find the age of the youngest wife.
1541     *
1542     * @param string $show_years
1543     *
1544     * @return string
1545     */
1546    public function youngestMarriageFemaleAge(string $show_years = ''): string
1547    {
1548        return $this->marriageQuery('age', 'ASC', 'F', (bool) $show_years);
1549    }
1550
1551    /**
1552     * Find the oldest wife.
1553     *
1554     * @return string
1555     */
1556    public function oldestMarriageFemale(): string
1557    {
1558        return $this->marriageQuery('full', 'DESC', 'F', false);
1559    }
1560
1561    /**
1562     * Find the name of the oldest wife.
1563     *
1564     * @return string
1565     */
1566    public function oldestMarriageFemaleName(): string
1567    {
1568        return $this->marriageQuery('name', 'DESC', 'F', false);
1569    }
1570
1571    /**
1572     * Find the age of the oldest wife.
1573     *
1574     * @param string $show_years
1575     *
1576     * @return string
1577     */
1578    public function oldestMarriageFemaleAge(string $show_years = ''): string
1579    {
1580        return $this->marriageQuery('age', 'DESC', 'F', (bool) $show_years);
1581    }
1582
1583    /**
1584     * Find the youngest husband.
1585     *
1586     * @return string
1587     */
1588    public function youngestMarriageMale(): string
1589    {
1590        return $this->marriageQuery('full', 'ASC', 'M', false);
1591    }
1592
1593    /**
1594     * Find the name of the youngest husband.
1595     *
1596     * @return string
1597     */
1598    public function youngestMarriageMaleName(): string
1599    {
1600        return $this->marriageQuery('name', 'ASC', 'M', false);
1601    }
1602
1603    /**
1604     * Find the age of the youngest husband.
1605     *
1606     * @param string $show_years
1607     *
1608     * @return string
1609     */
1610    public function youngestMarriageMaleAge(string $show_years = ''): string
1611    {
1612        return $this->marriageQuery('age', 'ASC', 'M', (bool) $show_years);
1613    }
1614
1615    /**
1616     * Find the oldest husband.
1617     *
1618     * @return string
1619     */
1620    public function oldestMarriageMale(): string
1621    {
1622        return $this->marriageQuery('full', 'DESC', 'M', false);
1623    }
1624
1625    /**
1626     * Find the name of the oldest husband.
1627     *
1628     * @return string
1629     */
1630    public function oldestMarriageMaleName(): string
1631    {
1632        return $this->marriageQuery('name', 'DESC', 'M', false);
1633    }
1634
1635    /**
1636     * Find the age of the oldest husband.
1637     *
1638     * @param string $show_years
1639     *
1640     * @return string
1641     */
1642    public function oldestMarriageMaleAge(string $show_years = ''): string
1643    {
1644        return $this->marriageQuery('age', 'DESC', 'M', (bool) $show_years);
1645    }
1646
1647    /**
1648     * General query on marriages.
1649     *
1650     * @param int  $year1
1651     * @param int  $year2
1652     *
1653     * @return Builder
1654     */
1655    public function statsMarriageQuery(int $year1 = -1, int $year2 = -1): Builder
1656    {
1657        $query = DB::table('dates')
1658            ->where('d_file', '=', $this->tree->id())
1659            ->where('d_fact', '=', 'MARR')
1660            ->select(['d_month', new Expression('COUNT(*) AS total')])
1661            ->groupBy(['d_month']);
1662
1663        if ($year1 >= 0 && $year2 >= 0) {
1664            $query->whereBetween('d_year', [$year1, $year2]);
1665        }
1666
1667        return $query;
1668    }
1669
1670    /**
1671     * General query on marriages.
1672     *
1673     * @param int  $year1
1674     * @param int  $year2
1675     *
1676     * @return Builder
1677     */
1678    public function statsFirstMarriageQuery(int $year1 = -1, int $year2 = -1): Builder
1679    {
1680        $query = DB::table('families')
1681            ->join('dates', static function (JoinClause $join): void {
1682                $join
1683                    ->on('d_gid', '=', 'f_id')
1684                    ->on('d_file', '=', 'f_file')
1685                    ->where('d_fact', '=', 'MARR')
1686                    ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'])
1687                    ->where('d_julianday2', '<>', 0);
1688            })->join('individuals', static function (JoinClause $join): void {
1689                $join
1690                    ->on('i_file', '=', 'f_file');
1691            })
1692            ->where('f_file', '=', $this->tree->id())
1693            ->where(static function (Builder $query): void {
1694                $query
1695                    ->whereColumn('i_id', '=', 'f_husb')
1696                    ->orWhereColumn('i_id', '=', 'f_wife');
1697            });
1698
1699        if ($year1 >= 0 && $year2 >= 0) {
1700            $query->whereBetween('d_year', [$year1, $year2]);
1701        }
1702
1703        return $query
1704            ->select(['f_id AS fams', 'f_husb', 'f_wife', 'd_julianday2 AS age', 'd_month AS month', 'i_id AS indi'])
1705            ->orderBy('f_id')
1706            ->orderBy('i_id')
1707            ->orderBy('d_julianday2');
1708    }
1709
1710    /**
1711     * General query on marriages.
1712     *
1713     * @param string|null $color_from
1714     * @param string|null $color_to
1715     *
1716     * @return string
1717     */
1718    public function statsMarr(string $color_from = null, string $color_to = null): string
1719    {
1720        return (new ChartMarriage($this->tree))
1721            ->chartMarriage($color_from, $color_to);
1722    }
1723
1724    /**
1725     * General divorce query.
1726     *
1727     * @param string|null $color_from
1728     * @param string|null $color_to
1729     *
1730     * @return string
1731     */
1732    public function statsDiv(string $color_from = null, string $color_to = null): string
1733    {
1734        return (new ChartDivorce($this->tree))
1735            ->chartDivorce($color_from, $color_to);
1736    }
1737}
1738