Passed
Push — 2.0 ( 401112...fa4b31 )
by Greg
13:39
created

FamilyRepository::ageBetweenSiblingsName()   B

Complexity

Conditions 7
Paths 3

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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