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