Passed
Push — main ( e317c4...c54022 )
by Greg
06:31
created

IndividualRepository::commonGivenQuery()   F

Complexity

Conditions 16
Paths 375

Size

Total Lines 81
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 54
nc 375
nop 5
dl 0
loc 81
rs 2.5458
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2023 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 Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Gedcom;
24
use Fisharebest\Webtrees\GedcomRecord;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Individual;
27
use Fisharebest\Webtrees\Module\IndividualListModule;
28
use Fisharebest\Webtrees\Module\ModuleInterface;
29
use Fisharebest\Webtrees\Module\ModuleListInterface;
30
use Fisharebest\Webtrees\Registry;
31
use Fisharebest\Webtrees\Services\ModuleService;
32
use Fisharebest\Webtrees\Statistics\Google\ChartAge;
33
use Fisharebest\Webtrees\Statistics\Google\ChartBirth;
34
use Fisharebest\Webtrees\Statistics\Google\ChartCommonGiven;
35
use Fisharebest\Webtrees\Statistics\Google\ChartCommonSurname;
36
use Fisharebest\Webtrees\Statistics\Google\ChartDeath;
37
use Fisharebest\Webtrees\Statistics\Google\ChartFamilyWithSources;
38
use Fisharebest\Webtrees\Statistics\Google\ChartIndividualWithSources;
39
use Fisharebest\Webtrees\Statistics\Google\ChartMortality;
40
use Fisharebest\Webtrees\Statistics\Google\ChartSex;
41
use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface;
42
use Fisharebest\Webtrees\Statistics\Service\CenturyService;
43
use Fisharebest\Webtrees\Statistics\Service\ColorService;
44
use Fisharebest\Webtrees\Tree;
45
use Illuminate\Database\Capsule\Manager as DB;
46
use Illuminate\Database\Query\Builder;
47
use Illuminate\Database\Query\Expression;
48
use Illuminate\Database\Query\JoinClause;
49
use stdClass;
50
51
use function array_key_exists;
52
use function array_keys;
53
use function array_reverse;
54
use function array_shift;
55
use function array_slice;
56
use function array_walk;
57
use function arsort;
58
use function e;
59
use function explode;
60
use function implode;
61
use function preg_match;
62
use function uksort;
63
use function view;
64
65
/**
66
 * A repository providing methods for individual related statistics.
67
 */
68
class IndividualRepository implements IndividualRepositoryInterface
69
{
70
    private CenturyService $century_service;
71
72
    private ColorService $color_service;
73
74
    private Tree $tree;
75
76
    /**
77
     * @param CenturyService $century_service
78
     * @param ColorService $color_service
79
     * @param Tree         $tree
80
     */
81
    public function __construct(CenturyService $century_service, ColorService $color_service, Tree $tree)
82
    {
83
        $this->century_service = $century_service;
84
        $this->color_service   = $color_service;
85
        $this->tree            = $tree;
86
    }
87
88
    /**
89
     * Find common given names.
90
     *
91
     * @param string $sex
92
     * @param string $type
93
     * @param bool   $show_tot
94
     * @param int    $threshold
95
     * @param int    $maxtoshow
96
     *
97
     * @return string|array<int>
98
     */
99
    private function commonGivenQuery(string $sex, string $type, bool $show_tot, int $threshold, int $maxtoshow)
100
    {
101
        $query = DB::table('name')
102
            ->join('individuals', static function (JoinClause $join): void {
103
                $join
104
                    ->on('i_file', '=', 'n_file')
105
                    ->on('i_id', '=', 'n_id');
106
            })
107
            ->where('n_file', '=', $this->tree->id())
108
            ->where('n_type', '<>', '_MARNM')
109
            ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
110
            ->where(new Expression('LENGTH(n_givn)'), '>', 1);
111
112
        switch ($sex) {
113
            case 'M':
114
            case 'F':
115
            case 'U':
116
                $query->where('i_sex', '=', $sex);
117
                break;
118
119
            case 'B':
120
            default:
121
                $query->where('i_sex', '<>', 'U');
122
                break;
123
        }
124
125
        $rows = $query
126
            ->groupBy(['n_givn'])
127
            ->pluck(new Expression('COUNT(distinct n_id) AS count'), 'n_givn');
128
129
        $nameList = [];
130
131
        foreach ($rows as $n_givn => $count) {
132
            // Split “John Thomas” into “John” and “Thomas” and count against both totals
133
            foreach (explode(' ', (string) $n_givn) as $given) {
134
                // Exclude initials and particles.
135
                if (preg_match('/^([A-Z]|[a-z]{1,3})$/', $given) !== 1) {
136
                    if (array_key_exists($given, $nameList)) {
137
                        $nameList[$given] += (int) $count;
138
                    } else {
139
                        $nameList[$given] = (int) $count;
140
                    }
141
                }
142
            }
143
        }
144
        arsort($nameList);
145
        $nameList = array_slice($nameList, 0, $maxtoshow);
146
147
        foreach ($nameList as $given => $total) {
148
            if ($total < $threshold) {
149
                unset($nameList[$given]);
150
            }
151
        }
152
153
        switch ($type) {
154
            case 'chart':
155
                return $nameList;
156
157
            case 'table':
158
                return view('lists/given-names-table', [
159
                    'given_names' => $nameList,
160
                    'order'       => [[1, 'desc']],
161
                ]);
162
163
            case 'list':
164
                return view('lists/given-names-list', [
165
                    'given_names' => $nameList,
166
                    'show_totals' => $show_tot,
167
                ]);
168
169
            case 'nolist':
170
            default:
171
                array_walk($nameList, static function (string &$value, string $key) use ($show_tot): void {
172
                    if ($show_tot) {
173
                        $value = '<bdi>' . e($key) . '</bdi> (' . I18N::number((int) $value) . ')';
174
                    } else {
175
                        $value = '<bdi>' . e($key) . '</bdi>';
176
                    }
177
                });
178
179
                return implode(I18N::$list_separator, $nameList);
180
        }
181
    }
182
183
    /**
184
     * Find common give names.
185
     *
186
     * @param int $threshold
187
     * @param int $maxtoshow
188
     *
189
     * @return string
190
     */
191
    public function commonGiven(int $threshold = 1, int $maxtoshow = 10): string
192
    {
193
        return $this->commonGivenQuery('B', 'nolist', false, $threshold, $maxtoshow);
194
    }
195
196
    /**
197
     * Find common give names.
198
     *
199
     * @param int $threshold
200
     * @param int $maxtoshow
201
     *
202
     * @return string
203
     */
204
    public function commonGivenTotals(int $threshold = 1, int $maxtoshow = 10): string
205
    {
206
        return $this->commonGivenQuery('B', 'nolist', true, $threshold, $maxtoshow);
207
    }
208
209
    /**
210
     * Find common give names.
211
     *
212
     * @param int $threshold
213
     * @param int $maxtoshow
214
     *
215
     * @return string
216
     */
217
    public function commonGivenList(int $threshold = 1, int $maxtoshow = 10): string
218
    {
219
        return $this->commonGivenQuery('B', 'list', false, $threshold, $maxtoshow);
220
    }
221
222
    /**
223
     * Find common give names.
224
     *
225
     * @param int $threshold
226
     * @param int $maxtoshow
227
     *
228
     * @return string
229
     */
230
    public function commonGivenListTotals(int $threshold = 1, int $maxtoshow = 10): string
231
    {
232
        return $this->commonGivenQuery('B', 'list', true, $threshold, $maxtoshow);
233
    }
234
235
    /**
236
     * Find common give names.
237
     *
238
     * @param int $threshold
239
     * @param int $maxtoshow
240
     *
241
     * @return string
242
     */
243
    public function commonGivenTable(int $threshold = 1, int $maxtoshow = 10): string
244
    {
245
        return $this->commonGivenQuery('B', 'table', false, $threshold, $maxtoshow);
246
    }
247
248
    /**
249
     * Find common give names of females.
250
     *
251
     * @param int $threshold
252
     * @param int $maxtoshow
253
     *
254
     * @return string
255
     */
256
    public function commonGivenFemale(int $threshold = 1, int $maxtoshow = 10): string
257
    {
258
        return $this->commonGivenQuery('F', 'nolist', false, $threshold, $maxtoshow);
259
    }
260
261
    /**
262
     * Find common give names of females.
263
     *
264
     * @param int $threshold
265
     * @param int $maxtoshow
266
     *
267
     * @return string
268
     */
269
    public function commonGivenFemaleTotals(int $threshold = 1, int $maxtoshow = 10): string
270
    {
271
        return $this->commonGivenQuery('F', 'nolist', true, $threshold, $maxtoshow);
272
    }
273
274
    /**
275
     * Find common give names of females.
276
     *
277
     * @param int $threshold
278
     * @param int $maxtoshow
279
     *
280
     * @return string
281
     */
282
    public function commonGivenFemaleList(int $threshold = 1, int $maxtoshow = 10): string
283
    {
284
        return $this->commonGivenQuery('F', 'list', false, $threshold, $maxtoshow);
285
    }
286
287
    /**
288
     * Find common give names of females.
289
     *
290
     * @param int $threshold
291
     * @param int $maxtoshow
292
     *
293
     * @return string
294
     */
295
    public function commonGivenFemaleListTotals(int $threshold = 1, int $maxtoshow = 10): string
296
    {
297
        return $this->commonGivenQuery('F', 'list', true, $threshold, $maxtoshow);
298
    }
299
300
    /**
301
     * Find common give names of females.
302
     *
303
     * @param int $threshold
304
     * @param int $maxtoshow
305
     *
306
     * @return string
307
     */
308
    public function commonGivenFemaleTable(int $threshold = 1, int $maxtoshow = 10): string
309
    {
310
        return $this->commonGivenQuery('F', 'table', false, $threshold, $maxtoshow);
311
    }
312
313
    /**
314
     * Find common give names of males.
315
     *
316
     * @param int $threshold
317
     * @param int $maxtoshow
318
     *
319
     * @return string
320
     */
321
    public function commonGivenMale(int $threshold = 1, int $maxtoshow = 10): string
322
    {
323
        return $this->commonGivenQuery('M', 'nolist', false, $threshold, $maxtoshow);
324
    }
325
326
    /**
327
     * Find common give names of males.
328
     *
329
     * @param int $threshold
330
     * @param int $maxtoshow
331
     *
332
     * @return string
333
     */
334
    public function commonGivenMaleTotals(int $threshold = 1, int $maxtoshow = 10): string
335
    {
336
        return $this->commonGivenQuery('M', 'nolist', true, $threshold, $maxtoshow);
337
    }
338
339
    /**
340
     * Find common give names of males.
341
     *
342
     * @param int $threshold
343
     * @param int $maxtoshow
344
     *
345
     * @return string
346
     */
347
    public function commonGivenMaleList(int $threshold = 1, int $maxtoshow = 10): string
348
    {
349
        return $this->commonGivenQuery('M', 'list', false, $threshold, $maxtoshow);
350
    }
351
352
    /**
353
     * Find common give names of males.
354
     *
355
     * @param int $threshold
356
     * @param int $maxtoshow
357
     *
358
     * @return string
359
     */
360
    public function commonGivenMaleListTotals(int $threshold = 1, int $maxtoshow = 10): string
361
    {
362
        return $this->commonGivenQuery('M', 'list', true, $threshold, $maxtoshow);
363
    }
364
365
    /**
366
     * Find common give names of males.
367
     *
368
     * @param int $threshold
369
     * @param int $maxtoshow
370
     *
371
     * @return string
372
     */
373
    public function commonGivenMaleTable(int $threshold = 1, int $maxtoshow = 10): string
374
    {
375
        return $this->commonGivenQuery('M', 'table', false, $threshold, $maxtoshow);
376
    }
377
378
    /**
379
     * Find common give names of unknown sexes.
380
     *
381
     * @param int $threshold
382
     * @param int $maxtoshow
383
     *
384
     * @return string
385
     */
386
    public function commonGivenUnknown(int $threshold = 1, int $maxtoshow = 10): string
387
    {
388
        return $this->commonGivenQuery('U', 'nolist', false, $threshold, $maxtoshow);
389
    }
390
391
    /**
392
     * Find common give names of unknown sexes.
393
     *
394
     * @param int $threshold
395
     * @param int $maxtoshow
396
     *
397
     * @return string
398
     */
399
    public function commonGivenUnknownTotals(int $threshold = 1, int $maxtoshow = 10): string
400
    {
401
        return $this->commonGivenQuery('U', 'nolist', true, $threshold, $maxtoshow);
402
    }
403
404
    /**
405
     * Find common give names of unknown sexes.
406
     *
407
     * @param int $threshold
408
     * @param int $maxtoshow
409
     *
410
     * @return string
411
     */
412
    public function commonGivenUnknownList(int $threshold = 1, int $maxtoshow = 10): string
413
    {
414
        return $this->commonGivenQuery('U', 'list', false, $threshold, $maxtoshow);
415
    }
416
417
    /**
418
     * Find common give names of unknown sexes.
419
     *
420
     * @param int $threshold
421
     * @param int $maxtoshow
422
     *
423
     * @return string
424
     */
425
    public function commonGivenUnknownListTotals(int $threshold = 1, int $maxtoshow = 10): string
426
    {
427
        return $this->commonGivenQuery('U', 'list', true, $threshold, $maxtoshow);
428
    }
429
430
    /**
431
     * Find common give names of unknown sexes.
432
     *
433
     * @param int $threshold
434
     * @param int $maxtoshow
435
     *
436
     * @return string
437
     */
438
    public function commonGivenUnknownTable(int $threshold = 1, int $maxtoshow = 10): string
439
    {
440
        return $this->commonGivenQuery('U', 'table', false, $threshold, $maxtoshow);
441
    }
442
443
    /**
444
     * Count the number of distinct given names (or the number of occurrences of specific given names).
445
     *
446
     * @param array<string> ...$params
447
     *
448
     * @return string
449
     */
450
    public function totalGivennames(...$params): string
451
    {
452
        $query = DB::table('name')
453
            ->where('n_file', '=', $this->tree->id());
454
455
        if ($params === []) {
456
            // Count number of distinct given names.
457
            $query
458
                ->distinct()
459
                ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
460
                ->whereNotNull('n_givn');
461
        } else {
462
            // Count number of occurrences of specific given names.
463
            $query->whereIn('n_givn', $params);
464
        }
465
466
        $count = $query->count('n_givn');
467
468
        return I18N::number($count);
469
    }
470
471
    /**
472
     * Count the number of distinct surnames (or the number of occurrences of specific surnames).
473
     *
474
     * @param array<string> ...$params
475
     *
476
     * @return string
477
     */
478
    public function totalSurnames(...$params): string
479
    {
480
        $query = DB::table('name')
481
            ->where('n_file', '=', $this->tree->id());
482
483
        if ($params === []) {
484
            // Count number of distinct surnames
485
            $query->distinct()
486
                ->whereNotNull('n_surn');
487
        } else {
488
            // Count number of occurrences of specific surnames.
489
            $query->whereIn('n_surn', $params);
490
        }
491
492
        $count = $query->count('n_surn');
493
494
        return I18N::number($count);
495
    }
496
497
    /**
498
     * @param int $number_of_surnames
499
     * @param int $threshold
500
     *
501
     * @return array<array<int>>
502
     */
503
    private function topSurnames(int $number_of_surnames, int $threshold): array
504
    {
505
        // Use the count of base surnames.
506
        $top_surnames = DB::table('name')
507
            ->where('n_file', '=', $this->tree->id())
508
            ->where('n_type', '<>', '_MARNM')
509
            ->whereNotIn('n_surn', ['', Individual::NOMEN_NESCIO])
510
            ->select(['n_surn'])
511
            ->groupBy(['n_surn'])
512
            ->orderByRaw('COUNT(n_surn) DESC')
513
            ->orderBy(new Expression('COUNT(n_surn)'), 'DESC')
514
            ->having(new Expression('COUNT(n_surn)'), '>=', $threshold)
515
            ->take($number_of_surnames)
516
            ->get()
517
            ->pluck('n_surn')
518
            ->all();
519
520
        $surnames = [];
521
522
        foreach ($top_surnames as $top_surname) {
523
            $surnames[$top_surname] = DB::table('name')
524
                ->where('n_file', '=', $this->tree->id())
525
                ->where('n_type', '<>', '_MARNM')
526
                ->where('n_surn', '=', $top_surname)
527
                ->select(['n_surn', new Expression('COUNT(n_surn) AS count')])
528
                ->groupBy(['n_surn'])
529
                ->orderBy('n_surn')
530
                ->get()
531
                ->pluck('count', 'n_surn')
532
                ->map(static fn (string $count): int => (int) $count)
533
                ->all();
534
        }
535
536
        return $surnames;
537
    }
538
539
    /**
540
     * Find common surnames.
541
     *
542
     * @return string
543
     */
544
    public function getCommonSurname(): string
545
    {
546
        $top_surname = $this->topSurnames(1, 0);
547
548
        return implode(', ', array_keys(array_shift($top_surname) ?? []));
549
    }
550
551
    /**
552
     * Find common surnames.
553
     *
554
     * @param string $type
555
     * @param bool   $show_tot
556
     * @param int    $threshold
557
     * @param int    $number_of_surnames
558
     * @param string $sorting
559
     *
560
     * @return string
561
     */
562
    private function commonSurnamesQuery(
563
        string $type,
564
        bool $show_tot,
565
        int $threshold,
566
        int $number_of_surnames,
567
        string $sorting
568
    ): string {
569
        $surnames = $this->topSurnames($number_of_surnames, $threshold);
570
571
        switch ($sorting) {
572
            default:
573
            case 'alpha':
574
                uksort($surnames, I18N::comparator());
575
                break;
576
            case 'count':
577
                break;
578
            case 'rcount':
579
                $surnames = array_reverse($surnames, true);
580
                break;
581
        }
582
583
        // find a module providing individual lists
584
        $module_service = Registry::container()->get(ModuleService::class);
585
586
        $module = $module_service
587
            ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user())
588
            ->first(static fn (ModuleInterface $module): bool => $module instanceof IndividualListModule);
589
590
        if ($type === 'list') {
591
            return view('lists/surnames-bullet-list', [
592
                'surnames' => $surnames,
593
                'module'   => $module,
594
                'totals'   => $show_tot,
595
                'tree'     => $this->tree,
596
            ]);
597
        }
598
599
        return view('lists/surnames-compact-list', [
600
            'surnames' => $surnames,
601
            'module'   => $module,
602
            'totals'   => $show_tot,
603
            'tree'     => $this->tree,
604
        ]);
605
    }
606
607
    /**
608
     * Find common surnames.
609
     *
610
     * @param int    $threshold
611
     * @param int    $number_of_surnames
612
     * @param string $sorting
613
     *
614
     * @return string
615
     */
616
    public function commonSurnames(
617
        int $threshold = 1,
618
        int $number_of_surnames = 10,
619
        string $sorting = 'alpha'
620
    ): string {
621
        return $this->commonSurnamesQuery('nolist', false, $threshold, $number_of_surnames, $sorting);
622
    }
623
624
    /**
625
     * Find common surnames.
626
     *
627
     * @param int    $threshold
628
     * @param int    $number_of_surnames
629
     * @param string $sorting
630
     *
631
     * @return string
632
     */
633
    public function commonSurnamesTotals(
634
        int $threshold = 1,
635
        int $number_of_surnames = 10,
636
        string $sorting = 'count'
637
    ): string {
638
        return $this->commonSurnamesQuery('nolist', true, $threshold, $number_of_surnames, $sorting);
639
    }
640
641
    /**
642
     * Find common surnames.
643
     *
644
     * @param int    $threshold
645
     * @param int    $number_of_surnames
646
     * @param string $sorting
647
     *
648
     * @return string
649
     */
650
    public function commonSurnamesList(
651
        int $threshold = 1,
652
        int $number_of_surnames = 10,
653
        string $sorting = 'alpha'
654
    ): string {
655
        return $this->commonSurnamesQuery('list', false, $threshold, $number_of_surnames, $sorting);
656
    }
657
658
    /**
659
     * Find common surnames.
660
     *
661
     * @param int    $threshold
662
     * @param int    $number_of_surnames
663
     * @param string $sorting
664
     *
665
     * @return string
666
     */
667
    public function commonSurnamesListTotals(
668
        int $threshold = 1,
669
        int $number_of_surnames = 10,
670
        string $sorting = 'count'
671
    ): string {
672
        return $this->commonSurnamesQuery('list', true, $threshold, $number_of_surnames, $sorting);
673
    }
674
675
    /**
676
     * Get a count of births by month.
677
     *
678
     * @param int  $year1
679
     * @param int  $year2
680
     *
681
     * @return Builder
682
     */
683
    public function statsBirthQuery(int $year1 = -1, int $year2 = -1): Builder
684
    {
685
        $query = DB::table('dates')
686
            ->select(['d_month', new Expression('COUNT(*) AS total')])
687
            ->where('d_file', '=', $this->tree->id())
688
            ->where('d_fact', '=', 'BIRT')
689
            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
690
            ->groupBy(['d_month']);
691
692
        if ($year1 >= 0 && $year2 >= 0) {
693
            $query->whereBetween('d_year', [$year1, $year2]);
694
        }
695
696
        return $query;
697
    }
698
699
    /**
700
     * Get a count of births by month.
701
     *
702
     * @param int  $year1
703
     * @param int  $year2
704
     *
705
     * @return Builder
706
     */
707
    public function statsBirthBySexQuery(int $year1 = -1, int $year2 = -1): Builder
708
    {
709
        return $this->statsBirthQuery($year1, $year2)
710
            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
711
            ->join('individuals', static function (JoinClause $join): void {
712
                $join
713
                    ->on('i_id', '=', 'd_gid')
714
                    ->on('i_file', '=', 'd_file');
715
            })
716
            ->groupBy(['i_sex']);
717
    }
718
719
    /**
720
     * General query on births.
721
     *
722
     * @param string|null $color_from
723
     * @param string|null $color_to
724
     *
725
     * @return string
726
     */
727
    public function statsBirth(string $color_from = null, string $color_to = null): string
728
    {
729
        return (new ChartBirth($this->century_service, $this->color_service, $this->tree))
730
            ->chartBirth($color_from, $color_to);
731
    }
732
733
    /**
734
     * Get a list of death dates.
735
     *
736
     * @param int  $year1
737
     * @param int  $year2
738
     *
739
     * @return Builder
740
     */
741
    public function statsDeathQuery(int $year1 = -1, int $year2 = -1): Builder
742
    {
743
        $query = DB::table('dates')
744
            ->select(['d_month', new Expression('COUNT(*) AS total')])
745
            ->where('d_file', '=', $this->tree->id())
746
            ->where('d_fact', '=', 'DEAT')
747
            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
748
            ->groupBy(['d_month']);
749
750
        if ($year1 >= 0 && $year2 >= 0) {
751
            $query->whereBetween('d_year', [$year1, $year2]);
752
        }
753
754
        return $query;
755
    }
756
757
    /**
758
     * Get a list of death dates.
759
     *
760
     * @param int  $year1
761
     * @param int  $year2
762
     *
763
     * @return Builder
764
     */
765
    public function statsDeathBySexQuery(int $year1 = -1, int $year2 = -1): Builder
766
    {
767
        return $this->statsDeathQuery($year1, $year2)
768
            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
769
            ->join('individuals', static function (JoinClause $join): void {
770
                $join
771
                    ->on('i_id', '=', 'd_gid')
772
                    ->on('i_file', '=', 'd_file');
773
            })
774
            ->groupBy(['i_sex']);
775
    }
776
777
    /**
778
     * General query on deaths.
779
     *
780
     * @param string|null $color_from
781
     * @param string|null $color_to
782
     *
783
     * @return string
784
     */
785
    public function statsDeath(string $color_from = null, string $color_to = null): string
786
    {
787
        return (new ChartDeath($this->century_service, $this->color_service, $this->tree))
788
            ->chartDeath($color_from, $color_to);
789
    }
790
791
    /**
792
     * General query on ages.
793
     *
794
     * @param string $related
795
     * @param string $sex
796
     * @param int    $year1
797
     * @param int    $year2
798
     *
799
     * @return array<stdClass>
800
     */
801
    public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1): array
802
    {
803
        $prefix = DB::connection()->getTablePrefix();
804
805
        $query = $this->birthAndDeathQuery($sex);
806
807
        if ($year1 >= 0 && $year2 >= 0) {
808
            $query
809
                ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
810
                ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
811
812
            if ($related === 'BIRT') {
813
                $query->whereBetween('birth.d_year', [$year1, $year2]);
814
            } elseif ($related === 'DEAT') {
815
                $query->whereBetween('death.d_year', [$year1, $year2]);
816
            }
817
        }
818
819
        return $query
820
            ->select([new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')])
821
            ->orderBy('days', 'desc')
822
            ->get()
823
            ->all();
824
    }
825
826
    /**
827
     * General query on ages.
828
     *
829
     * @return string
830
     */
831
    public function statsAge(): string
832
    {
833
        return (new ChartAge($this->century_service, $this->tree))->chartAge();
834
    }
835
836
    /**
837
     * Lifespan
838
     *
839
     * @param string $type
840
     * @param string $sex
841
     *
842
     * @return string
843
     */
844
    private function longlifeQuery(string $type, string $sex): string
845
    {
846
        $prefix = DB::connection()->getTablePrefix();
847
848
        $row = $this->birthAndDeathQuery($sex)
849
            ->orderBy('days', 'desc')
850
            ->select(['individuals.*', new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')])
851
            ->first();
852
853
        if ($row === null) {
854
            return '';
855
        }
856
857
        /** @var Individual $individual */
858
        $individual = Registry::individualFactory()->mapper($this->tree)($row);
859
860
        if ($type !== 'age' && !$individual->canShow()) {
861
            return I18N::translate('This information is private and cannot be shown.');
862
        }
863
864
        switch ($type) {
865
            default:
866
            case 'full':
867
                return $individual->formatList();
868
869
            case 'age':
870
                return I18N::number((int) ($row->days / 365.25));
871
872
            case 'name':
873
                return '<a href="' . e($individual->url()) . '">' . $individual->fullName() . '</a>';
874
        }
875
    }
876
877
    /**
878
     * Find the longest lived individual.
879
     *
880
     * @return string
881
     */
882
    public function longestLife(): string
883
    {
884
        return $this->longlifeQuery('full', 'BOTH');
885
    }
886
887
    /**
888
     * Find the age of the longest lived individual.
889
     *
890
     * @return string
891
     */
892
    public function longestLifeAge(): string
893
    {
894
        return $this->longlifeQuery('age', 'BOTH');
895
    }
896
897
    /**
898
     * Find the name of the longest lived individual.
899
     *
900
     * @return string
901
     */
902
    public function longestLifeName(): string
903
    {
904
        return $this->longlifeQuery('name', 'BOTH');
905
    }
906
907
    /**
908
     * Find the longest lived female.
909
     *
910
     * @return string
911
     */
912
    public function longestLifeFemale(): string
913
    {
914
        return $this->longlifeQuery('full', 'F');
915
    }
916
917
    /**
918
     * Find the age of the longest lived female.
919
     *
920
     * @return string
921
     */
922
    public function longestLifeFemaleAge(): string
923
    {
924
        return $this->longlifeQuery('age', 'F');
925
    }
926
927
    /**
928
     * Find the name of the longest lived female.
929
     *
930
     * @return string
931
     */
932
    public function longestLifeFemaleName(): string
933
    {
934
        return $this->longlifeQuery('name', 'F');
935
    }
936
937
    /**
938
     * Find the longest lived male.
939
     *
940
     * @return string
941
     */
942
    public function longestLifeMale(): string
943
    {
944
        return $this->longlifeQuery('full', 'M');
945
    }
946
947
    /**
948
     * Find the age of the longest lived male.
949
     *
950
     * @return string
951
     */
952
    public function longestLifeMaleAge(): string
953
    {
954
        return $this->longlifeQuery('age', 'M');
955
    }
956
957
    /**
958
     * Find the name of the longest lived male.
959
     *
960
     * @return string
961
     */
962
    public function longestLifeMaleName(): string
963
    {
964
        return $this->longlifeQuery('name', 'M');
965
    }
966
967
    /**
968
     * Returns the calculated age the time of event.
969
     *
970
     * @param int $days The age from the database record
971
     *
972
     * @return string
973
     */
974
    private function calculateAge(int $days): string
975
    {
976
        if ($days < 31) {
977
            return I18N::plural('%s day', '%s days', $days, I18N::number($days));
978
        }
979
980
        if ($days < 365) {
981
            $months = (int) ($days / 30.5);
982
            return I18N::plural('%s month', '%s months', $months, I18N::number($months));
983
        }
984
985
        $years = (int) ($days / 365.25);
986
987
        return I18N::plural('%s year', '%s years', $years, I18N::number($years));
988
    }
989
990
    /**
991
     * Find the oldest individuals.
992
     *
993
     * @param string $sex
994
     * @param int    $total
995
     *
996
     * @return array<array<string,mixed>>
997
     */
998
    private function topTenOldestQuery(string $sex, int $total): array
999
    {
1000
        $prefix = DB::connection()->getTablePrefix();
1001
1002
        $rows = $this->birthAndDeathQuery($sex)
1003
            ->groupBy(['i_id', 'i_file'])
1004
            ->orderBy('days', 'desc')
1005
            ->select(['individuals.*', new Expression('MAX(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')])
1006
            ->take($total)
1007
            ->get();
1008
1009
        $top10 = [];
1010
        foreach ($rows as $row) {
1011
            /** @var Individual $individual */
1012
            $individual = Registry::individualFactory()->mapper($this->tree)($row);
1013
1014
            if ($individual->canShow()) {
1015
                $top10[] = [
1016
                    'person' => $individual,
1017
                    'age'    => $this->calculateAge((int) $row->days),
1018
                ];
1019
            }
1020
        }
1021
1022
        return $top10;
1023
    }
1024
1025
    /**
1026
     * Find the oldest individuals.
1027
     *
1028
     * @param int $total
1029
     *
1030
     * @return string
1031
     */
1032
    public function topTenOldest(int $total = 10): string
1033
    {
1034
        $records = $this->topTenOldestQuery('BOTH', $total);
1035
1036
        return view('statistics/individuals/top10-nolist', [
1037
            'records' => $records,
1038
        ]);
1039
    }
1040
1041
    /**
1042
     * Find the oldest living individuals.
1043
     *
1044
     * @param int $total
1045
     *
1046
     * @return string
1047
     */
1048
    public function topTenOldestList(int $total = 10): string
1049
    {
1050
        $records = $this->topTenOldestQuery('BOTH', $total);
1051
1052
        return view('statistics/individuals/top10-list', [
1053
            'records' => $records,
1054
        ]);
1055
    }
1056
1057
    /**
1058
     * Find the oldest females.
1059
     *
1060
     * @param int $total
1061
     *
1062
     * @return string
1063
     */
1064
    public function topTenOldestFemale(int $total = 10): string
1065
    {
1066
        $records = $this->topTenOldestQuery('F', $total);
1067
1068
        return view('statistics/individuals/top10-nolist', [
1069
            'records' => $records,
1070
        ]);
1071
    }
1072
1073
    /**
1074
     * Find the oldest living females.
1075
     *
1076
     * @param int $total
1077
     *
1078
     * @return string
1079
     */
1080
    public function topTenOldestFemaleList(int $total = 10): string
1081
    {
1082
        $records = $this->topTenOldestQuery('F', $total);
1083
1084
        return view('statistics/individuals/top10-list', [
1085
            'records' => $records,
1086
        ]);
1087
    }
1088
1089
    /**
1090
     * Find the longest lived males.
1091
     *
1092
     * @param int $total
1093
     *
1094
     * @return string
1095
     */
1096
    public function topTenOldestMale(int $total = 10): string
1097
    {
1098
        $records = $this->topTenOldestQuery('M', $total);
1099
1100
        return view('statistics/individuals/top10-nolist', [
1101
            'records' => $records,
1102
        ]);
1103
    }
1104
1105
    /**
1106
     * Find the longest lived males.
1107
     *
1108
     * @param int $total
1109
     *
1110
     * @return string
1111
     */
1112
    public function topTenOldestMaleList(int $total = 10): string
1113
    {
1114
        $records = $this->topTenOldestQuery('M', $total);
1115
1116
        return view('statistics/individuals/top10-list', [
1117
            'records' => $records,
1118
        ]);
1119
    }
1120
1121
    /**
1122
     * Find the oldest living individuals.
1123
     *
1124
     * @param string $sex   "M", "F" or "BOTH"
1125
     * @param int    $total
1126
     *
1127
     * @return array<array<string,mixed>>
1128
     */
1129
    private function topTenOldestAliveQuery(string $sex, int $total): array
1130
    {
1131
        $query = DB::table('dates')
1132
            ->join('individuals', static function (JoinClause $join): void {
1133
                $join
1134
                    ->on('i_id', '=', 'd_gid')
1135
                    ->on('i_file', '=', 'd_file');
1136
            })
1137
            ->where('d_file', '=', $this->tree->id())
1138
            ->where('d_julianday1', '<>', 0)
1139
            ->where('d_fact', '=', 'BIRT')
1140
            ->where('i_gedcom', 'NOT LIKE', "%\n1 DEAT%")
1141
            ->where('i_gedcom', 'NOT LIKE', "%\n1 BURI%")
1142
            ->where('i_gedcom', 'NOT LIKE', "%\n1 CREM%");
1143
1144
        if ($sex === 'F' || $sex === 'M') {
1145
            $query->where('i_sex', '=', $sex);
1146
        }
1147
1148
        return $query
1149
            ->groupBy(['i_id', 'i_file'])
1150
            ->orderBy(new Expression('MIN(d_julianday1)'))
1151
            ->select(['individuals.*'])
1152
            ->take($total)
1153
            ->get()
1154
            ->map(Registry::individualFactory()->mapper($this->tree))
1155
            ->filter(GedcomRecord::accessFilter())
1156
            ->map(function (Individual $individual): array {
1157
                return [
1158
                    'person' => $individual,
1159
                    'age'    => $this->calculateAge(Registry::timestampFactory()->now()->julianDay() - $individual->getBirthDate()->minimumJulianDay()),
1160
                ];
1161
            })
1162
            ->all();
1163
    }
1164
1165
    /**
1166
     * Find the oldest living individuals.
1167
     *
1168
     * @param int $total
1169
     *
1170
     * @return string
1171
     */
1172
    public function topTenOldestAlive(int $total = 10): string
1173
    {
1174
        if (!Auth::isMember($this->tree)) {
1175
            return I18N::translate('This information is private and cannot be shown.');
1176
        }
1177
1178
        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1179
1180
        return view('statistics/individuals/top10-nolist', [
1181
            'records' => $records,
1182
        ]);
1183
    }
1184
1185
    /**
1186
     * Find the oldest living individuals.
1187
     *
1188
     * @param int $total
1189
     *
1190
     * @return string
1191
     */
1192
    public function topTenOldestListAlive(int $total = 10): string
1193
    {
1194
        if (!Auth::isMember($this->tree)) {
1195
            return I18N::translate('This information is private and cannot be shown.');
1196
        }
1197
1198
        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1199
1200
        return view('statistics/individuals/top10-list', [
1201
            'records' => $records,
1202
        ]);
1203
    }
1204
1205
    /**
1206
     * Find the oldest living females.
1207
     *
1208
     * @param int $total
1209
     *
1210
     * @return string
1211
     */
1212
    public function topTenOldestFemaleAlive(int $total = 10): string
1213
    {
1214
        if (!Auth::isMember($this->tree)) {
1215
            return I18N::translate('This information is private and cannot be shown.');
1216
        }
1217
1218
        $records = $this->topTenOldestAliveQuery('F', $total);
1219
1220
        return view('statistics/individuals/top10-nolist', [
1221
            'records' => $records,
1222
        ]);
1223
    }
1224
1225
    /**
1226
     * Find the oldest living females.
1227
     *
1228
     * @param int $total
1229
     *
1230
     * @return string
1231
     */
1232
    public function topTenOldestFemaleListAlive(int $total = 10): string
1233
    {
1234
        if (!Auth::isMember($this->tree)) {
1235
            return I18N::translate('This information is private and cannot be shown.');
1236
        }
1237
1238
        $records = $this->topTenOldestAliveQuery('F', $total);
1239
1240
        return view('statistics/individuals/top10-list', [
1241
            'records' => $records,
1242
        ]);
1243
    }
1244
1245
    /**
1246
     * Find the longest lived living males.
1247
     *
1248
     * @param int $total
1249
     *
1250
     * @return string
1251
     */
1252
    public function topTenOldestMaleAlive(int $total = 10): string
1253
    {
1254
        if (!Auth::isMember($this->tree)) {
1255
            return I18N::translate('This information is private and cannot be shown.');
1256
        }
1257
1258
        $records = $this->topTenOldestAliveQuery('M', $total);
1259
1260
        return view('statistics/individuals/top10-nolist', [
1261
            'records' => $records,
1262
        ]);
1263
    }
1264
1265
    /**
1266
     * Find the longest lived living males.
1267
     *
1268
     * @param int $total
1269
     *
1270
     * @return string
1271
     */
1272
    public function topTenOldestMaleListAlive(int $total = 10): string
1273
    {
1274
        if (!Auth::isMember($this->tree)) {
1275
            return I18N::translate('This information is private and cannot be shown.');
1276
        }
1277
1278
        $records = $this->topTenOldestAliveQuery('M', $total);
1279
1280
        return view('statistics/individuals/top10-list', [
1281
            'records' => $records,
1282
        ]);
1283
    }
1284
1285
    /**
1286
     * Find the average lifespan.
1287
     *
1288
     * @param string $sex        "M", "F" or "BOTH"
1289
     * @param bool   $show_years
1290
     *
1291
     * @return string
1292
     */
1293
    private function averageLifespanQuery(string $sex, bool $show_years): string
1294
    {
1295
        $prefix = DB::connection()->getTablePrefix();
1296
1297
        $days = (int) $this->birthAndDeathQuery($sex)
1298
            ->select([new Expression('AVG(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')])
1299
            ->value('days');
1300
1301
        if ($show_years) {
1302
            return $this->calculateAge($days);
1303
        }
1304
1305
        return I18N::number((int) ($days / 365.25));
1306
    }
1307
1308
    /**
1309
     * Find the average lifespan.
1310
     *
1311
     * @param bool $show_years
1312
     *
1313
     * @return string
1314
     */
1315
    public function averageLifespan(bool $show_years): string
1316
    {
1317
        return $this->averageLifespanQuery('BOTH', $show_years);
1318
    }
1319
1320
    /**
1321
     * Find the average lifespan of females.
1322
     *
1323
     * @param bool $show_years
1324
     *
1325
     * @return string
1326
     */
1327
    public function averageLifespanFemale(bool $show_years): string
1328
    {
1329
        return $this->averageLifespanQuery('F', $show_years);
1330
    }
1331
1332
    /**
1333
     * Find the average male lifespan.
1334
     *
1335
     * @param bool $show_years
1336
     *
1337
     * @return string
1338
     */
1339
    public function averageLifespanMale(bool $show_years): string
1340
    {
1341
        return $this->averageLifespanQuery('M', $show_years);
1342
    }
1343
1344
    /**
1345
     * Convert totals into percentages.
1346
     *
1347
     * @param int $count
1348
     * @param int $total
1349
     *
1350
     * @return string
1351
     */
1352
    private function getPercentage(int $count, int $total): string
1353
    {
1354
        return $total !== 0 ? I18N::percentage($count / $total, 1) : '';
1355
    }
1356
1357
    /**
1358
     * Returns how many individuals exist in the tree.
1359
     *
1360
     * @return int
1361
     */
1362
    private function totalIndividualsQuery(): int
1363
    {
1364
        return DB::table('individuals')
1365
            ->where('i_file', '=', $this->tree->id())
1366
            ->count();
1367
    }
1368
1369
    /**
1370
     * Count the number of living individuals.
1371
     *
1372
     * The totalLiving/totalDeceased queries assume that every dead person will
1373
     * have a DEAT record. It will not include individuals who were born more
1374
     * than MAX_ALIVE_AGE years ago, and who have no DEAT record.
1375
     * A good reason to run the “Add missing DEAT records” batch-update!
1376
     *
1377
     * @return int
1378
     */
1379
    private function totalLivingQuery(): int
1380
    {
1381
        $query = DB::table('individuals')
1382
            ->where('i_file', '=', $this->tree->id());
1383
1384
        foreach (Gedcom::DEATH_EVENTS as $death_event) {
1385
            $query->where('i_gedcom', 'NOT LIKE', "%\n1 " . $death_event . '%');
1386
        }
1387
1388
        return $query->count();
1389
    }
1390
1391
    /**
1392
     * Count the number of dead individuals.
1393
     *
1394
     * @return int
1395
     */
1396
    private function totalDeceasedQuery(): int
1397
    {
1398
        return DB::table('individuals')
1399
            ->where('i_file', '=', $this->tree->id())
1400
            ->where(static function (Builder $query): void {
1401
                foreach (Gedcom::DEATH_EVENTS as $death_event) {
1402
                    $query->orWhere('i_gedcom', 'LIKE', "%\n1 " . $death_event . '%');
1403
                }
1404
            })
1405
            ->count();
1406
    }
1407
1408
    /**
1409
     * Returns the total count of a specific sex.
1410
     *
1411
     * @param string $sex The sex to query
1412
     *
1413
     * @return int
1414
     */
1415
    private function getTotalSexQuery(string $sex): int
1416
    {
1417
        return DB::table('individuals')
1418
            ->where('i_file', '=', $this->tree->id())
1419
            ->where('i_sex', '=', $sex)
1420
            ->count();
1421
    }
1422
1423
    /**
1424
     * Returns the total number of males.
1425
     *
1426
     * @return int
1427
     */
1428
    private function totalSexMalesQuery(): int
1429
    {
1430
        return $this->getTotalSexQuery('M');
1431
    }
1432
1433
    /**
1434
     * Returns the total number of females.
1435
     *
1436
     * @return int
1437
     */
1438
    private function totalSexFemalesQuery(): int
1439
    {
1440
        return $this->getTotalSexQuery('F');
1441
    }
1442
1443
    /**
1444
     * Returns the total number of individuals with unknown sex.
1445
     *
1446
     * @return int
1447
     */
1448
    private function totalSexUnknownQuery(): int
1449
    {
1450
        return $this->getTotalSexQuery('U');
1451
    }
1452
1453
    /**
1454
     * Count the total families.
1455
     *
1456
     * @return int
1457
     */
1458
    private function totalFamiliesQuery(): int
1459
    {
1460
        return DB::table('families')
1461
            ->where('f_file', '=', $this->tree->id())
1462
            ->count();
1463
    }
1464
1465
    /**
1466
     * How many individuals have one or more sources.
1467
     *
1468
     * @return int
1469
     */
1470
    private function totalIndisWithSourcesQuery(): int
1471
    {
1472
        return DB::table('individuals')
1473
            ->select(['i_id'])
1474
            ->distinct()
1475
            ->join('link', static function (JoinClause $join): void {
1476
                $join->on('i_id', '=', 'l_from')
1477
                    ->on('i_file', '=', 'l_file');
1478
            })
1479
            ->where('l_file', '=', $this->tree->id())
1480
            ->where('l_type', '=', 'SOUR')
1481
            ->count('i_id');
1482
    }
1483
1484
    /**
1485
     * Count the families with source records.
1486
     *
1487
     * @return int
1488
     */
1489
    private function totalFamsWithSourcesQuery(): int
1490
    {
1491
        return DB::table('families')
1492
            ->select(['f_id'])
1493
            ->distinct()
1494
            ->join('link', static function (JoinClause $join): void {
1495
                $join->on('f_id', '=', 'l_from')
1496
                    ->on('f_file', '=', 'l_file');
1497
            })
1498
            ->where('l_file', '=', $this->tree->id())
1499
            ->where('l_type', '=', 'SOUR')
1500
            ->count('f_id');
1501
    }
1502
1503
    /**
1504
     * Count the number of repositories.
1505
     *
1506
     * @return int
1507
     */
1508
    private function totalRepositoriesQuery(): int
1509
    {
1510
        return DB::table('other')
1511
            ->where('o_file', '=', $this->tree->id())
1512
            ->where('o_type', '=', 'REPO')
1513
            ->count();
1514
    }
1515
1516
    /**
1517
     * Count the total number of sources.
1518
     *
1519
     * @return int
1520
     */
1521
    private function totalSourcesQuery(): int
1522
    {
1523
        return DB::table('sources')
1524
            ->where('s_file', '=', $this->tree->id())
1525
            ->count();
1526
    }
1527
1528
    /**
1529
     * Count the number of notes.
1530
     *
1531
     * @return int
1532
     */
1533
    private function totalNotesQuery(): int
1534
    {
1535
        return DB::table('other')
1536
            ->where('o_file', '=', $this->tree->id())
1537
            ->where('o_type', '=', 'NOTE')
1538
            ->count();
1539
    }
1540
1541
    /**
1542
     * Count the total media.
1543
     *
1544
     * @return int
1545
     */
1546
    private function totalMediaQuery(): int
1547
    {
1548
        return DB::table('media')
1549
            ->where('m_file', '=', $this->tree->id())
1550
            ->count();
1551
    }
1552
1553
    /**
1554
     * Returns the total number of records.
1555
     *
1556
     * @return int
1557
     */
1558
    private function totalRecordsQuery(): int
1559
    {
1560
        return $this->totalIndividualsQuery()
1561
            + $this->totalFamiliesQuery()
1562
            + $this->totalMediaQuery()
1563
            + $this->totalNotesQuery()
1564
            + $this->totalRepositoriesQuery()
1565
            + $this->totalSourcesQuery();
1566
    }
1567
1568
    /**
1569
     * @return string
1570
     */
1571
    public function totalRecords(): string
1572
    {
1573
        return I18N::number($this->totalRecordsQuery());
1574
    }
1575
1576
    /**
1577
     * @return string
1578
     */
1579
    public function totalIndividuals(): string
1580
    {
1581
        return I18N::number($this->totalIndividualsQuery());
1582
    }
1583
1584
    /**
1585
     * Count the number of living individuals.
1586
     *
1587
     * @return string
1588
     */
1589
    public function totalLiving(): string
1590
    {
1591
        return I18N::number($this->totalLivingQuery());
1592
    }
1593
1594
    /**
1595
     * Count the number of dead individuals.
1596
     *
1597
     * @return string
1598
     */
1599
    public function totalDeceased(): string
1600
    {
1601
        return I18N::number($this->totalDeceasedQuery());
1602
    }
1603
1604
    /**
1605
     * @return string
1606
     */
1607
    public function totalSexMales(): string
1608
    {
1609
        return I18N::number($this->totalSexMalesQuery());
1610
    }
1611
1612
    /**
1613
     * @return string
1614
     */
1615
    public function totalSexFemales(): string
1616
    {
1617
        return I18N::number($this->totalSexFemalesQuery());
1618
    }
1619
1620
    /**
1621
     * @return string
1622
     */
1623
    public function totalSexUnknown(): string
1624
    {
1625
        return I18N::number($this->totalSexUnknownQuery());
1626
    }
1627
1628
    /**
1629
     * @return string
1630
     */
1631
    public function totalFamilies(): string
1632
    {
1633
        return I18N::number($this->totalFamiliesQuery());
1634
    }
1635
1636
    /**
1637
     * How many individuals have one or more sources.
1638
     *
1639
     * @return string
1640
     */
1641
    public function totalIndisWithSources(): string
1642
    {
1643
        return I18N::number($this->totalIndisWithSourcesQuery());
1644
    }
1645
1646
    /**
1647
     * Count the families with with source records.
1648
     *
1649
     * @return string
1650
     */
1651
    public function totalFamsWithSources(): string
1652
    {
1653
        return I18N::number($this->totalFamsWithSourcesQuery());
1654
    }
1655
1656
    /**
1657
     * @return string
1658
     */
1659
    public function totalRepositories(): string
1660
    {
1661
        return I18N::number($this->totalRepositoriesQuery());
1662
    }
1663
1664
    /**
1665
     * @return string
1666
     */
1667
    public function totalSources(): string
1668
    {
1669
        return I18N::number($this->totalSourcesQuery());
1670
    }
1671
1672
    /**
1673
     * @return string
1674
     */
1675
    public function totalNotes(): string
1676
    {
1677
        return I18N::number($this->totalNotesQuery());
1678
    }
1679
1680
    /**
1681
     * @return string
1682
     */
1683
    public function totalIndividualsPercentage(): string
1684
    {
1685
        return $this->getPercentage(
1686
            $this->totalIndividualsQuery(),
1687
            $this->totalRecordsQuery()
1688
        );
1689
    }
1690
1691
    /**
1692
     * @return string
1693
     */
1694
    public function totalIndisWithSourcesPercentage(): string
1695
    {
1696
        return $this->getPercentage(
1697
            $this->totalIndisWithSourcesQuery(),
1698
            $this->totalIndividualsQuery()
1699
        );
1700
    }
1701
1702
    /**
1703
     * @return string
1704
     */
1705
    public function totalFamiliesPercentage(): string
1706
    {
1707
        return $this->getPercentage(
1708
            $this->totalFamiliesQuery(),
1709
            $this->totalRecordsQuery()
1710
        );
1711
    }
1712
1713
    /**
1714
     * @return string
1715
     */
1716
    public function totalFamsWithSourcesPercentage(): string
1717
    {
1718
        return $this->getPercentage(
1719
            $this->totalFamsWithSourcesQuery(),
1720
            $this->totalFamiliesQuery()
1721
        );
1722
    }
1723
1724
    /**
1725
     * @return string
1726
     */
1727
    public function totalRepositoriesPercentage(): string
1728
    {
1729
        return $this->getPercentage(
1730
            $this->totalRepositoriesQuery(),
1731
            $this->totalRecordsQuery()
1732
        );
1733
    }
1734
1735
    /**
1736
     * @return string
1737
     */
1738
    public function totalSourcesPercentage(): string
1739
    {
1740
        return $this->getPercentage(
1741
            $this->totalSourcesQuery(),
1742
            $this->totalRecordsQuery()
1743
        );
1744
    }
1745
1746
    /**
1747
     * @return string
1748
     */
1749
    public function totalNotesPercentage(): string
1750
    {
1751
        return $this->getPercentage(
1752
            $this->totalNotesQuery(),
1753
            $this->totalRecordsQuery()
1754
        );
1755
    }
1756
1757
    /**
1758
     * @return string
1759
     */
1760
    public function totalLivingPercentage(): string
1761
    {
1762
        return $this->getPercentage(
1763
            $this->totalLivingQuery(),
1764
            $this->totalIndividualsQuery()
1765
        );
1766
    }
1767
1768
    /**
1769
     * @return string
1770
     */
1771
    public function totalDeceasedPercentage(): string
1772
    {
1773
        return $this->getPercentage(
1774
            $this->totalDeceasedQuery(),
1775
            $this->totalIndividualsQuery()
1776
        );
1777
    }
1778
1779
    /**
1780
     * @return string
1781
     */
1782
    public function totalSexMalesPercentage(): string
1783
    {
1784
        return $this->getPercentage(
1785
            $this->totalSexMalesQuery(),
1786
            $this->totalIndividualsQuery()
1787
        );
1788
    }
1789
1790
    /**
1791
     * @return string
1792
     */
1793
    public function totalSexFemalesPercentage(): string
1794
    {
1795
        return $this->getPercentage(
1796
            $this->totalSexFemalesQuery(),
1797
            $this->totalIndividualsQuery()
1798
        );
1799
    }
1800
1801
    /**
1802
     * @return string
1803
     */
1804
    public function totalSexUnknownPercentage(): string
1805
    {
1806
        return $this->getPercentage(
1807
            $this->totalSexUnknownQuery(),
1808
            $this->totalIndividualsQuery()
1809
        );
1810
    }
1811
1812
    /**
1813
     * Create a chart of common given names.
1814
     *
1815
     * @param string|null $color_from
1816
     * @param string|null $color_to
1817
     * @param int         $maxtoshow
1818
     *
1819
     * @return string
1820
     */
1821
    public function chartCommonGiven(
1822
        string $color_from = null,
1823
        string $color_to = null,
1824
        int $maxtoshow = 7
1825
    ): string {
1826
        $tot_indi = $this->totalIndividualsQuery();
1827
        $given    = $this->commonGivenQuery('B', 'chart', false, 1, $maxtoshow);
1828
1829
        if ($given === []) {
1830
            return I18N::translate('This information is not available.');
1831
        }
1832
1833
        return (new ChartCommonGiven($this->color_service))
1834
            ->chartCommonGiven($tot_indi, $given, $color_from, $color_to);
0 ignored issues
show
Bug introduced by
It seems like $given can also be of type string; however, parameter $given of Fisharebest\Webtrees\Sta...ven::chartCommonGiven() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1834
            ->chartCommonGiven($tot_indi, /** @scrutinizer ignore-type */ $given, $color_from, $color_to);
Loading history...
1835
    }
1836
1837
    /**
1838
     * Create a chart of common surnames.
1839
     *
1840
     * @param string|null $color_from
1841
     * @param string|null $color_to
1842
     * @param int         $number_of_surnames
1843
     *
1844
     * @return string
1845
     */
1846
    public function chartCommonSurnames(
1847
        string $color_from = null,
1848
        string $color_to = null,
1849
        int $number_of_surnames = 10
1850
    ): string {
1851
        $tot_indi     = $this->totalIndividualsQuery();
1852
        $all_surnames = $this->topSurnames($number_of_surnames, 0);
1853
1854
        if ($all_surnames === []) {
1855
            return I18N::translate('This information is not available.');
1856
        }
1857
1858
        $surname_tradition = Registry::surnameTraditionFactory()
1859
            ->make($this->tree->getPreference('SURNAME_TRADITION'));
1860
1861
        return (new ChartCommonSurname($this->color_service, $surname_tradition))
1862
            ->chartCommonSurnames($tot_indi, $all_surnames, $color_from, $color_to);
1863
    }
1864
1865
    /**
1866
     * Create a chart showing mortality.
1867
     *
1868
     * @param string|null $color_living
1869
     * @param string|null $color_dead
1870
     *
1871
     * @return string
1872
     */
1873
    public function chartMortality(string $color_living = null, string $color_dead = null): string
1874
    {
1875
        $tot_l = $this->totalLivingQuery();
1876
        $tot_d = $this->totalDeceasedQuery();
1877
1878
        return (new ChartMortality($this->color_service))
1879
            ->chartMortality($tot_l, $tot_d, $color_living, $color_dead);
1880
    }
1881
1882
    /**
1883
     * Create a chart showing individuals with/without sources.
1884
     *
1885
     * @param string|null $color_from
1886
     * @param string|null $color_to
1887
     *
1888
     * @return string
1889
     */
1890
    public function chartIndisWithSources(
1891
        string $color_from = null,
1892
        string $color_to = null
1893
    ): string {
1894
        $tot_indi        = $this->totalIndividualsQuery();
1895
        $tot_indi_source = $this->totalIndisWithSourcesQuery();
1896
1897
        return (new ChartIndividualWithSources($this->color_service))
1898
            ->chartIndisWithSources($tot_indi, $tot_indi_source, $color_from, $color_to);
1899
    }
1900
1901
    /**
1902
     * Create a chart of individuals with/without sources.
1903
     *
1904
     * @param string|null $color_from
1905
     * @param string|null $color_to
1906
     *
1907
     * @return string
1908
     */
1909
    public function chartFamsWithSources(
1910
        string $color_from = null,
1911
        string $color_to = null
1912
    ): string {
1913
        $tot_fam        = $this->totalFamiliesQuery();
1914
        $tot_fam_source = $this->totalFamsWithSourcesQuery();
1915
1916
        return (new ChartFamilyWithSources($this->color_service))
1917
            ->chartFamsWithSources($tot_fam, $tot_fam_source, $color_from, $color_to);
1918
    }
1919
1920
    /**
1921
     * @param string|null $color_female
1922
     * @param string|null $color_male
1923
     * @param string|null $color_unknown
1924
     *
1925
     * @return string
1926
     */
1927
    public function chartSex(
1928
        string $color_female = null,
1929
        string $color_male = null,
1930
        string $color_unknown = null
1931
    ): string {
1932
        $tot_m = $this->totalSexMalesQuery();
1933
        $tot_f = $this->totalSexFemalesQuery();
1934
        $tot_u = $this->totalSexUnknownQuery();
1935
1936
        return (new ChartSex())
1937
            ->chartSex($tot_m, $tot_f, $tot_u, $color_female, $color_male, $color_unknown);
1938
    }
1939
1940
    /**
1941
     * Query individuals, with their births and deaths.
1942
     *
1943
     * @param string $sex
1944
     *
1945
     * @return Builder
1946
     */
1947
    private function birthAndDeathQuery(string $sex): Builder
1948
    {
1949
        $query = DB::table('individuals')
1950
            ->where('i_file', '=', $this->tree->id())
1951
            ->join('dates AS birth', static function (JoinClause $join): void {
1952
                $join
1953
                    ->on('birth.d_file', '=', 'i_file')
1954
                    ->on('birth.d_gid', '=', 'i_id');
1955
            })
1956
            ->join('dates AS death', static function (JoinClause $join): void {
1957
                $join
1958
                    ->on('death.d_file', '=', 'i_file')
1959
                    ->on('death.d_gid', '=', 'i_id');
1960
            })
1961
            ->where('birth.d_fact', '=', 'BIRT')
1962
            ->where('death.d_fact', '=', 'DEAT')
1963
            ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2')
1964
            ->where('birth.d_julianday2', '<>', 0);
1965
1966
        if ($sex === 'M' || $sex === 'F') {
1967
            $query->where('i_sex', '=', $sex);
1968
        }
1969
1970
        return $query;
1971
    }
1972
}
1973