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