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