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