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