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