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