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