Passed
Push — 2.1 ( 438dd8...4d644f )
by Greg
15:45 queued 07:28
created

StatisticsData::marriageQuery()   B

Complexity

Conditions 10
Paths 32

Size

Total Lines 76
Code Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 54
c 1
b 0
f 0
nc 32
nop 4
dl 0
loc 76
rs 7.1369

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) 2025 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;
21
22
use Fisharebest\Webtrees\Contracts\UserInterface;
23
use Fisharebest\Webtrees\Elements\UnknownElement;
24
use Fisharebest\Webtrees\Http\RequestHandlers\MessagePage;
25
use Fisharebest\Webtrees\Module\IndividualListModule;
26
use Fisharebest\Webtrees\Module\ModuleInterface;
27
use Fisharebest\Webtrees\Module\ModuleListInterface;
28
use Fisharebest\Webtrees\Services\MessageService;
29
use Fisharebest\Webtrees\Services\ModuleService;
30
use Fisharebest\Webtrees\Services\UserService;
31
use Illuminate\Database\Capsule\Manager as DB;
32
use Illuminate\Database\Query\Builder;
33
use Illuminate\Database\Query\Expression;
34
use Illuminate\Database\Query\JoinClause;
35
use Illuminate\Support\Collection;
36
37
use function abs;
38
use function app;
39
use function array_keys;
40
use function array_reverse;
41
use function array_search;
42
use function array_shift;
43
use function array_slice;
44
use function arsort;
45
use function asort;
46
use function count;
47
use function e;
48
use function explode;
49
use function floor;
50
use function implode;
51
use function in_array;
52
use function preg_match;
53
use function preg_quote;
54
use function route;
55
use function str_replace;
56
use function strip_tags;
57
use function uksort;
58
use function view;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Fisharebest\Webtrees\view. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
59
60
class StatisticsData
61
{
62
    private Tree $tree;
63
    private UserService $user_service;
64
65
    public function __construct(Tree $tree, UserService $user_service)
66
    {
67
        $this->tree         = $tree;
68
        $this->user_service = $user_service;
69
    }
70
71
    public function averageChildrenPerFamily(): float
72
    {
73
        return (float) DB::table('families')
74
            ->where('f_file', '=', $this->tree->id())
75
            ->avg('f_numchil');
76
    }
77
78
    public function averageLifespanDays(string $sex): int
79
    {
80
        $prefix = DB::connection()->getTablePrefix();
81
82
        return (int) $this->birthAndDeathQuery($sex)
83
            ->select([new Expression('AVG(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')])
84
            ->value('days');
85
    }
86
87
    /**
88
     * @return Collection<string,int>
89
     */
90
    public function commonGivenNames(string $sex, int $threshold, int $limit): Collection
91
    {
92
        $query = DB::table('name')
93
            ->where('n_file', '=', $this->tree->id())
94
            ->where('n_type', '<>', '_MARNM')
95
            ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
96
            ->where(new Expression('LENGTH(n_givn)'), '>', 1);
97
98
        if ($sex !== 'ALL') {
99
            $query
100
                ->join('individuals', static function (JoinClause $join): void {
101
                    $join
102
                        ->on('i_file', '=', 'n_file')
103
                        ->on('i_id', '=', 'n_id');
104
                })
105
                ->where('i_sex', '=', $sex);
106
        }
107
108
        $rows = $query
109
            ->groupBy(['n_givn'])
110
            ->pluck(new Expression('COUNT(DISTINCT n_id)'), 'n_givn')
111
            ->map(static fn (mixed $count): int => (int) $count);
112
113
114
        $given_names = [];
115
116
        foreach ($rows as $n_givn => $count) {
117
            // Split “John Thomas” into “John” and “Thomas” and count against both totals
118
            foreach (explode(' ', (string) $n_givn) as $given) {
119
                // Exclude initials and particles.
120
                if (preg_match('/^([A-Z]|[a-z]{1,3})$/', $given) !== 1) {
121
                    $given_names[$given] ??= 0;
122
                    $given_names[$given] += (int) $count;
123
                }
124
            }
125
        }
126
127
        return (new Collection($given_names))
128
            ->sortDesc()
129
            ->slice(0, $limit)
130
            ->filter(static fn (int $count): bool => $count >= $threshold);
131
    }
132
133
    /**
134
     * @return array<array<int>>
135
     */
136
    public function commonSurnames(int $limit, int $threshold, string $sort): array
137
    {
138
        // Use the count of base surnames.
139
        $top_surnames = DB::table('name')
140
            ->where('n_file', '=', $this->tree->id())
141
            ->where('n_type', '<>', '_MARNM')
142
            ->whereNotIn('n_surn', ['', Individual::NOMEN_NESCIO])
143
            ->select(['n_surn'])
144
            ->groupBy(['n_surn'])
145
            ->orderByRaw('COUNT(n_surn) DESC')
146
            ->orderBy(new Expression('COUNT(n_surn)'), 'DESC')
147
            ->having(new Expression('COUNT(n_surn)'), '>=', $threshold)
148
            ->take($limit)
149
            ->pluck('n_surn')
150
            ->all();
151
152
        $surnames = [];
153
154
        foreach ($top_surnames as $top_surname) {
155
            $surnames[$top_surname] = DB::table('name')
156
                ->where('n_file', '=', $this->tree->id())
157
                ->where('n_type', '<>', '_MARNM')
158
                ->where('n_surn', '=', $top_surname)
159
                ->select(['n_surn', new Expression('COUNT(n_surn) AS count')])
160
                ->groupBy(['n_surn'])
161
                ->orderBy('n_surn')
162
                ->pluck('count', 'n_surn')
163
                ->map(static fn (string $count): int => (int) $count)
164
                ->all();
165
        }
166
167
        switch ($sort) {
168
            default:
169
            case 'alpha':
170
                uksort($surnames, I18N::comparator());
171
                break;
172
            case 'count':
173
                break;
174
            case 'rcount':
175
                $surnames = array_reverse($surnames, true);
176
                break;
177
        }
178
179
        return $surnames;
180
    }
181
182
    /**
183
     * @param array<string> $events
184
     */
185
    public function countAllEvents(array $events): int
186
    {
187
        return DB::table('dates')
188
            ->where('d_file', '=', $this->tree->id())
189
            ->whereIn('d_fact', $events)
190
            ->count();
191
    }
192
193
    public function countAllPlaces(): int
194
    {
195
        return DB::table('places')
196
            ->where('p_file', '=', $this->tree->id())
197
            ->count();
198
    }
199
200
    public function countAllRecords(): int
201
    {
202
        return
203
            $this->countIndividuals() +
204
            $this->countFamilies() +
205
            $this->countMedia() +
206
            $this->countNotes() +
207
            $this->countRepositories() +
208
            $this->countSources();
209
    }
210
211
    public function countChildren(): int
212
    {
213
        return (int) DB::table('families')
214
            ->where('f_file', '=', $this->tree->id())
215
            ->sum('f_numchil');
216
    }
217
218
    /**
219
     * @return array<array{place:Place,count:int}>
220
     */
221
    public function countCountries(int $limit): array
222
    {
223
        return DB::table('places')
224
            ->join('placelinks', static function (JoinClause $join): void {
225
                $join
226
                    ->on('pl_file', '=', 'p_file')
227
                    ->on('pl_p_id', '=', 'p_id');
228
            })
229
            ->where('p_file', '=', $this->tree->id())
230
            ->where('p_parent_id', '=', 0)
231
            ->groupBy(['p_place'])
232
            ->orderByDesc(new Expression('COUNT(*)'))
233
            ->orderBy('p_place')
234
            ->take($limit)
235
            ->select([new Expression('COUNT(*) AS total'), 'p_place AS place'])
236
            ->get()
237
            ->map(fn (object $row): array => [
238
                'place' => new Place($row->place, $this->tree),
239
                'count' => (int) $row->total,
240
            ])
241
            ->all();
242
    }
243
244
    private function countEventQuery(string $event, int $year1 = 0, int $year2 = 0): Builder
245
    {
246
        $query = DB::table('dates')
247
            ->where('d_file', '=', $this->tree->id())
248
            ->where('d_fact', '=', $event)
249
            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
250
251
        if ($year1 !== 0 && $year2 !== 0) {
252
            $query->whereBetween('d_year', [$year1, $year2]);
253
        } else {
254
            $query->where('d_year', '<>', 0);
255
        }
256
257
        return $query;
258
    }
259
260
    /**
261
     * @return array<string,int>
262
     */
263
    public function countEventsByMonth(string $event, int $year1, int $year2): array
264
    {
265
        return $this->countEventQuery($event, $year1, $year2)
266
            ->groupBy(['d_month'])
267
            ->pluck(new Expression('COUNT(*)'), 'd_month')
268
            ->map(static fn (string $total): int => (int) $total)
269
            ->all();
270
    }
271
272
    /**
273
     * @return array<object{month:string,sex:string,total:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{month:string,sex:string,total:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
274
     */
275
    public function countEventsByMonthAndSex(string $event, int $year1, int $year2): array
276
    {
277
        return $this->countEventQuery($event, $year1, $year2)
278
            ->join('individuals', static function (JoinClause $join): void {
279
                $join
280
                    ->on('i_id', '=', 'd_gid')
281
                    ->on('i_file', '=', 'd_file');
282
            })
283
            ->groupBy(['i_sex', 'd_month'])
284
            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
285
            ->get()
286
            ->map(static fn (object $row): object => (object) [
287
                'month' => $row->d_month,
288
                'sex'   => $row->i_sex,
289
                'total' => (int) $row->total,
290
            ])
291
            ->all();
292
    }
293
294
    /**
295
     * @return array<int,array{0:string,1:int}>
296
     */
297
    public function countEventsByCentury(string $event): array
298
    {
299
        return $this->countEventQuery($event, 0, 0)
300
            ->select([new Expression('ROUND((d_year + 49) / 100, 0) AS century'), new Expression('COUNT(*) AS total')])
301
            ->groupBy(['century'])
302
            ->orderBy('century')
303
            ->get()
304
            ->map(fn (object $row): array => [$this->centuryName((int) $row->century), (int) $row->total])
305
            ->all();
306
    }
307
308
    public function countFamilies(): int
309
    {
310
        return DB::table('families')
311
            ->where('f_file', '=', $this->tree->id())
312
            ->count();
313
    }
314
315
    /**
316
     * @param array<string> $events
317
     */
318
    public function countFamiliesWithEvents(array $events): int
319
    {
320
        return DB::table('dates')
321
            ->join('families', static function (JoinClause $join): void {
322
                $join
323
                    ->on('f_id', '=', 'd_gid')
324
                    ->on('f_file', '=', 'd_file');
325
            })
326
            ->where('d_file', '=', $this->tree->id())
327
            ->whereIn('d_fact', $events)
328
            ->count();
329
    }
330
331
    public function countFamiliesWithNoChildren(): int
332
    {
333
        return DB::table('families')
334
            ->where('f_file', '=', $this->tree->id())
335
            ->where('f_numchil', '=', 0)
336
            ->count();
337
    }
338
339
    public function countFamiliesWithSources(): int
340
    {
341
        return DB::table('families')
342
            ->select(['f_id'])
343
            ->distinct()
344
            ->join('link', static function (JoinClause $join): void {
345
                $join->on('f_id', '=', 'l_from')
346
                    ->on('f_file', '=', 'l_file');
347
            })
348
            ->where('l_file', '=', $this->tree->id())
349
            ->where('l_type', '=', 'SOUR')
350
            ->count('f_id');
351
    }
352
353
    /**
354
     * @return array<string,int>
355
     */
356
    public function countFirstChildrenByMonth(int $year1, int $year2): array
357
    {
358
        return $this->countFirstChildrenQuery($year1, $year2)
359
            ->groupBy(['d_month'])
360
            ->pluck(new Expression('COUNT(*)'), 'd_month')
361
            ->map(static fn (string $total): int => (int) $total)
362
            ->all();
363
    }
364
365
    /**
366
     * @return array<object{month:string,sex:string,total:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{month:string,sex:string,total:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
367
     */
368
    public function countFirstChildrenByMonthAndSex(int $year1, int $year2): array
369
    {
370
        return $this->countFirstChildrenQuery($year1, $year2)
371
            ->join('individuals', static function (JoinClause $join): void {
372
                $join
373
                    ->on('i_file', '=', 'l_file')
374
                    ->on('i_id', '=', 'l_to');
375
            })
376
            ->groupBy(['d_month', 'i_sex'])
377
            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
378
            ->get()
379
            ->map(static fn (object $row): object => (object) [
380
                'month' => $row->d_month,
381
                'sex'   => $row->i_sex,
382
                'total' => (int) $row->total,
383
            ])
384
            ->all();
385
    }
386
387
    private function countFirstChildrenQuery(int $year1, int $year2): Builder
388
    {
389
        $first_child_subquery = DB::table('link')
390
            ->join('dates', static function (JoinClause $join): void {
391
                $join
392
                    ->on('d_gid', '=', 'l_to')
393
                    ->on('d_file', '=', 'l_file')
394
                    ->where('d_julianday1', '<>', 0)
395
                    ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']);
396
            })
397
            ->where('l_file', '=', $this->tree->id())
398
            ->where('l_type', '=', 'CHIL')
399
            ->select(['l_from AS family_id', new Expression('MIN(d_julianday1) AS min_birth_jd')])
400
            ->groupBy(['family_id']);
401
402
        $query = DB::table('link')
403
            ->join('dates', static function (JoinClause $join): void {
404
                $join
405
                    ->on('d_gid', '=', 'l_to')
406
                    ->on('d_file', '=', 'l_file');
407
            })
408
            ->joinSub($first_child_subquery, 'subquery', static function (JoinClause $join): void {
409
                $join
410
                    ->on('family_id', '=', 'l_from')
411
                    ->on('min_birth_jd', '=', 'd_julianday1');
412
            })
413
            ->where('link.l_file', '=', $this->tree->id())
414
            ->where('link.l_type', '=', 'CHIL');
415
416
        if ($year1 !== 0 && $year2 !== 0) {
417
            $query->whereBetween('d_year', [$year1, $year2]);
418
        }
419
420
        return $query;
421
    }
422
423
    /**
424
     * @return array<string,int>
425
     */
426
    public function countFirstMarriagesByMonth(Tree $tree, int $year1, int $year2): array
427
    {
428
        $query = DB::table('families')
429
            ->join('dates', static function (JoinClause $join): void {
430
                $join
431
                    ->on('d_gid', '=', 'f_id')
432
                    ->on('d_file', '=', 'f_file')
433
                    ->where('d_fact', '=', 'MARR')
434
                    ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC'])
435
                    ->where('d_julianday2', '<>', 0);
436
            })
437
            ->where('f_file', '=', $tree->id());
438
439
        if ($year1 !== 0 && $year2 !== 0) {
440
            $query->whereBetween('d_year', [$year1, $year2]);
441
        }
442
443
        $rows = $query
444
            ->orderBy('d_julianday2')
445
            ->select(['f_husb AS husb', 'f_wife AS wife', 'd_month AS month'])
446
            ->get();
447
448
        $months = [
449
            'JAN' => 0,
450
            'FEB' => 0,
451
            'MAR' => 0,
452
            'APR' => 0,
453
            'MAY' => 0,
454
            'JUN' => 0,
455
            'JUL' => 0,
456
            'AUG' => 0,
457
            'SEP' => 0,
458
            'OCT' => 0,
459
            'NOV' => 0,
460
            'DEC' => 0,
461
        ];
462
        $seen = [];
463
464
        foreach ($rows as $row) {
465
            if (!in_array($row->husb, $seen, true) && !in_array($row->wife, $seen, true)) {
466
                $months[$row->month]++;
467
                $seen[] = $row->husb;
468
                $seen[] = $row->wife;
469
            }
470
        }
471
472
        return $months;
473
    }
474
475
    /**
476
     * @param array<string> $names
477
     */
478
    public function countGivenNames(array $names): int
479
    {
480
        if ($names === []) {
481
            // Count number of distinct given names.
482
            return DB::table('name')
483
                ->where('n_file', '=', $this->tree->id())
484
                ->distinct()
485
                ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
486
                ->whereNotNull('n_givn')
487
                ->count('n_givn');
488
        }
489
490
        // Count number of occurrences of specific given names.
491
        return DB::table('name')
492
            ->where('n_file', '=', $this->tree->id())
493
            ->whereIn('n_givn', $names)
494
            ->count('n_givn');
495
    }
496
497
    public function countHits(string $page_name, string $page_parameter): int
498
    {
499
        return DB::table('hit_counter')
500
            ->where('gedcom_id', '=', $this->tree->id())
501
            ->where('page_name', '=', $page_name)
502
            ->where('page_parameter', '=', $page_parameter)
503
            ->sum('page_count');
504
    }
505
506
    public function countIndividuals(): int
507
    {
508
        return DB::table('individuals')
509
            ->where('i_file', '=', $this->tree->id())
510
            ->count();
511
    }
512
513
    public function countIndividualsBySex(string $sex): int
514
    {
515
        return DB::table('individuals')
516
            ->where('i_file', '=', $this->tree->id())
517
            ->where('i_sex', '=', $sex)
518
            ->count();
519
    }
520
521
    public function countIndividualsDeceased(): int
522
    {
523
        return DB::table('individuals')
524
            ->where('i_file', '=', $this->tree->id())
525
            ->where(static function (Builder $query): void {
526
                foreach (Gedcom::DEATH_EVENTS as $death_event) {
527
                    $query->orWhere('i_gedcom', 'LIKE', "%\n1 " . $death_event . '%');
528
                }
529
            })
530
            ->count();
531
    }
532
533
    public function countIndividualsLiving(): int
534
    {
535
        $query = DB::table('individuals')
536
            ->where('i_file', '=', $this->tree->id());
537
538
        foreach (Gedcom::DEATH_EVENTS as $death_event) {
539
            $query->where('i_gedcom', 'NOT LIKE', "%\n1 " . $death_event . '%');
540
        }
541
542
        return $query->count();
543
    }
544
545
    /**
546
     * @param array<string> $events
547
     */
548
    public function countIndividualsWithEvents(array $events): int
549
    {
550
        return DB::table('dates')
551
            ->join('individuals', static function (JoinClause $join): void {
552
                $join
553
                    ->on('i_id', '=', 'd_gid')
554
                    ->on('i_file', '=', 'd_file');
555
            })
556
            ->where('d_file', '=', $this->tree->id())
557
            ->whereIn('d_fact', $events)
558
            ->count();
559
    }
560
561
    public function countIndividualsWithSources(): int
562
    {
563
        return DB::table('individuals')
564
            ->select(['i_id'])
565
            ->distinct()
566
            ->join('link', static function (JoinClause $join): void {
567
                $join->on('i_id', '=', 'l_from')
568
                    ->on('i_file', '=', 'l_file');
569
            })
570
            ->where('l_file', '=', $this->tree->id())
571
            ->where('l_type', '=', 'SOUR')
572
            ->count('i_id');
573
    }
574
575
    public function countMarriedFemales(): int
576
    {
577
        return DB::table('families')
578
            ->where('f_file', '=', $this->tree->id())
579
            ->where('f_gedcom', 'LIKE', "%\n1 MARR%")
580
            ->distinct()
581
            ->count('f_wife');
582
    }
583
584
    public function countMarriedMales(): int
585
    {
586
        return DB::table('families')
587
            ->where('f_file', '=', $this->tree->id())
588
            ->where('f_gedcom', 'LIKE', "%\n1 MARR%")
589
            ->distinct()
590
            ->count('f_husb');
591
    }
592
593
    public function countMedia(string $type = 'all'): int
594
    {
595
        $query = DB::table('media_file')
596
            ->where('m_file', '=', $this->tree->id());
597
598
        if ($type !== 'all') {
599
            $query->where('source_media_type', '=', $type);
600
        }
601
602
        return $query->count();
603
    }
604
605
    /**
606
     * @return array<array{0:string,1:int}>
607
     */
608
    public function countMediaByType(): array
609
    {
610
        $element = Registry::elementFactory()->make('OBJE:FILE:FORM:TYPE');
611
        $values  = $element->values();
612
613
        return DB::table('media_file')
614
            ->where('m_file', '=', $this->tree->id())
615
            ->groupBy('source_media_type')
616
            ->select([new Expression('COUNT(*) AS total'), 'source_media_type'])
617
            ->get()
618
            ->map(static fn (object $row): array => [
619
                $values[$element->canonical($row->source_media_type)] ?? I18N::translate('Other'),
620
                (int) $row->total,
621
            ])
622
            ->all();
623
    }
624
625
    public function countNotes(): int
626
    {
627
        return DB::table('other')
628
            ->where('o_file', '=', $this->tree->id())
629
            ->where('o_type', '=', 'NOTE')
630
            ->count();
631
    }
632
633
    /**
634
     * @param array<string> $events
635
     */
636
    public function countOtherEvents(array $events): int
637
    {
638
        return DB::table('dates')
639
            ->where('d_file', '=', $this->tree->id())
640
            ->whereNotIn('d_fact', $events)
641
            ->count();
642
    }
643
644
    /**
645
     * @param array<string> $rows
646
     *
647
     * @return array<array{place:Place,count:int}>
648
     */
649
    private function countPlaces(array $rows, string $event, int $limit): array
650
    {
651
        $places = [];
652
653
        foreach ($rows as $gedcom) {
654
            if (preg_match('/\n1 ' . $event . '(?:\n[2-9].*)*\n2 PLAC (.+)/', $gedcom, $match) === 1) {
655
                $places[$match[1]] ??= 0;
656
                $places[$match[1]]++;
657
            }
658
        }
659
660
        arsort($places);
661
662
        $records = [];
663
664
        foreach (array_slice($places, 0, $limit) as $place => $count) {
665
            $records[] = [
666
                'place' => new Place((string) $place, $this->tree),
667
                'count' => $count,
668
            ];
669
        }
670
671
        return $records;
672
    }
673
674
    /**
675
     * @return array<array{place:Place,count:int}>
676
     */
677
    public function countPlacesForFamilies(string $event, int $limit): array
678
    {
679
        $rows = DB::table('families')
680
            ->where('f_file', '=', $this->tree->id())
681
            ->where('f_gedcom', 'LIKE', "%\n2 PLAC %")
682
            ->pluck('f_gedcom')
683
            ->all();
684
685
        return $this->countPlaces($rows, $event, $limit);
686
    }
687
688
    /**
689
     * @return array<array{place:Place,count:int}>
690
     */
691
    public function countPlacesForIndividuals(string $event, int $limit): array
692
    {
693
        $rows = DB::table('individuals')
694
            ->where('i_file', '=', $this->tree->id())
695
            ->where('i_gedcom', 'LIKE', "%\n2 PLAC %")
696
            ->pluck('i_gedcom')
697
            ->all();
698
699
        return $this->countPlaces($rows, $event, $limit);
700
    }
701
702
    public function countRepositories(): int
703
    {
704
        return DB::table('other')
705
            ->where('o_file', '=', $this->tree->id())
706
            ->where('o_type', '=', 'REPO')
707
            ->count();
708
    }
709
710
    public function countSources(): int
711
    {
712
        return DB::table('sources')
713
            ->where('s_file', '=', $this->tree->id())
714
            ->count();
715
    }
716
717
    /**
718
     * @param array<string> $names
719
     */
720
    public function countSurnames(array $names): int
721
    {
722
        if ($names === []) {
723
            // Count number of distinct surnames
724
            return DB::table('name')
725
                ->where('n_file', '=', $this->tree->id())->distinct()
726
                ->whereNotNull('n_surn')
727
                ->count('n_surn');
728
        }
729
730
        // Count number of occurrences of specific surnames.
731
        return DB::table('name')
732
            ->where('n_file', '=', $this->tree->id())
733
            ->whereIn('n_surn', $names)
734
            ->count('n_surn');
735
    }
736
737
    public function countTreeFavorites(): int
738
    {
739
        return DB::table('favorite')
740
            ->where('gedcom_id', '=', $this->tree->id())
741
            ->count();
742
    }
743
744
    public function countTreeNews(): int
745
    {
746
        return DB::table('news')
747
            ->where('gedcom_id', '=', $this->tree->id())
748
            ->count();
749
    }
750
751
    public function countUserfavorites(): int
752
    {
753
        return DB::table('favorite')
754
            ->where('user_id', '=', Auth::id())
755
            ->count();
756
    }
757
758
    public function countUserJournal(): int
759
    {
760
        return DB::table('news')
761
            ->where('user_id', '=', Auth::id())
762
            ->count();
763
    }
764
765
    public function countUserMessages(): int
766
    {
767
        return DB::table('message')
768
            ->where('user_id', '=', Auth::id())
769
            ->count();
770
    }
771
772
    /**
773
     * @return array<int,object{family:Family,children:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<int,object{family:Family,children:int}> at position 4 could not be parsed: Expected '>' at position 4, but found 'object'.
Loading history...
774
     */
775
    public function familiesWithTheMostChildren(int $limit): array
776
    {
777
        return DB::table('families')
778
            ->where('f_file', '=', $this->tree->id())
779
            ->orderByDesc('f_numchil')
780
            ->limit($limit)
781
            ->get()
782
            ->map(fn (object $row): object => (object) [
783
                'family'   => Registry::familyFactory()->make($row->f_id, $this->tree, $row->f_gedcom),
784
                'children' => (int) $row->f_numchil,
785
            ])
786
            ->all();
787
    }
788
789
    /**
790
     * @param array<string> $events
791
     *
792
     * @return object{id:string,year:int,fact:string,type:string}|null
793
     */
794
    private function firstEvent(array $events, bool $ascending): ?object
795
    {
796
        if ($events === []) {
797
            $events = [
798
                ...Gedcom::BIRTH_EVENTS,
799
                ...Gedcom::DEATH_EVENTS,
800
                ...Gedcom::MARRIAGE_EVENTS,
801
                ...Gedcom::DIVORCE_EVENTS,
802
            ];
803
        }
804
805
        return DB::table('dates')
806
            ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type'])
807
            ->where('d_file', '=', $this->tree->id())
808
            ->whereIn('d_fact', $events)
809
            ->where('d_julianday1', '<>', 0)
810
            ->orderBy('d_julianday1', $ascending ? 'ASC' : 'DESC')
811
            ->limit(1)
812
            ->get()
813
            ->map(static fn (object $row): object => (object) [
814
                'id'   => $row->id,
815
                'year' => (int) $row->year,
816
                'fact' => $row->fact,
817
                'type' => $row->type,
818
            ])
819
            ->first();
820
    }
821
822
    /**
823
     * @param array<string> $events
824
     */
825
    public function firstEventName(array $events, bool $ascending): string
826
    {
827
        $row = $this->firstEvent($events, $ascending);
828
829
        if ($row !== null) {
830
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
831
832
            if ($record instanceof GedcomRecord) {
833
                return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>';
834
            }
835
        }
836
837
        return '';
838
    }
839
840
    /**
841
     * @param array<string> $events
842
     */
843
    public function firstEventPlace(array $events, bool $ascending): string
844
    {
845
        $row = $this->firstEvent($events, $ascending);
846
847
        if ($row !== null) {
848
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
849
            $fact   = null;
850
851
            if ($record instanceof GedcomRecord) {
852
                $fact = $record->facts([$row->fact])->first();
853
            }
854
855
            if ($fact instanceof Fact) {
856
                return $fact->place()->shortName();
857
            }
858
        }
859
860
        return I18N::translate('Private');
861
    }
862
863
    /**
864
     * @param array<string> $events
865
     */
866
    public function firstEventRecord(array $events, bool $ascending): string
867
    {
868
        $row = $this->firstEvent($events, $ascending);
869
        $result = I18N::translate('This information is not available.');
870
871
        if ($row !== null) {
872
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
873
874
            if ($record instanceof GedcomRecord && $record->canShow()) {
875
                $result = $record->formatList();
876
            } else {
877
                $result = I18N::translate('This information is private and cannot be shown.');
878
            }
879
        }
880
881
        return $result;
882
    }
883
884
    /**
885
     * @param array<string> $events
886
     */
887
    public function firstEventType(array $events, bool $ascending): string
888
    {
889
        $row = $this->firstEvent($events, $ascending);
890
891
        if ($row === null) {
892
            return '';
893
        }
894
895
        foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
896
            $element = Registry::elementFactory()->make($record_type . ':' . $row->fact);
897
898
            if (!$element instanceof UnknownElement) {
899
                return $element->label();
900
            }
901
        }
902
903
        return $row->fact;
904
    }
905
906
    /**
907
     * @param array<string> $events
908
     */
909
    public function firstEventYear(array $events, bool $ascending): string
910
    {
911
        $row = $this->firstEvent($events, $ascending);
912
913
        if ($row === null) {
914
            return '-';
915
        }
916
917
        if ($row->year < 0) {
918
            $date = new Date($row->type . ' ' . abs($row->year) . ' B.C.');
919
        } else {
920
            $date = new Date($row->type . ' ' . $row->year);
921
        }
922
923
        return $date->display();
924
    }
925
926
    public function isUserLoggedIn(?int $user_id): bool
927
    {
928
        return $user_id !== null && DB::table('session')
929
            ->where('user_id', '=', $user_id)
930
            ->exists();
931
    }
932
933
    public function latestUserId(): ?int
934
    {
935
        $user_id = DB::table('user')
936
            ->select(['user.user_id'])
937
            ->leftJoin('user_setting', 'user.user_id', '=', 'user_setting.user_id')
938
            ->where('setting_name', '=', UserInterface::PREF_TIMESTAMP_REGISTERED)
939
            ->orderByDesc('setting_value')
940
            ->value('user_id');
941
942
        if ($user_id === null) {
943
            return null;
944
        }
945
946
        return (int) $user_id;
947
    }
948
949
    /**
950
     * @return array<object{family:Family,child1:Individual,child2:Individual,age:string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{family:Fami...Individual,age:string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
951
     */
952
    public function maximumAgeBetweenSiblings(int $limit): array
953
    {
954
        $prefix = DB::connection()->getTablePrefix();
955
956
        return DB::table('link AS link1')
957
            ->join('link AS link2', static function (JoinClause $join): void {
958
                $join
959
                    ->on('link2.l_from', '=', 'link1.l_from')
960
                    ->on('link2.l_type', '=', 'link1.l_type')
961
                    ->on('link2.l_file', '=', 'link1.l_file');
962
            })
963
            ->join('dates AS child1', static function (JoinClause $join): void {
964
                $join
965
                    ->on('child1.d_gid', '=', 'link1.l_to')
966
                    ->on('child1.d_file', '=', 'link1.l_file')
967
                    ->where('child1.d_fact', '=', 'BIRT')
968
                    ->where('child1.d_julianday1', '<>', 0);
969
            })
970
            ->join('dates AS child2', static function (JoinClause $join): void {
971
                $join
972
                    ->on('child2.d_gid', '=', 'link2.l_to')
973
                    ->on('child2.d_file', '=', 'link2.l_file')
974
                    ->where('child2.d_fact', '=', 'BIRT')
975
                    ->whereColumn('child2.d_julianday2', '>', 'child1.d_julianday1');
976
            })
977
            ->where('link1.l_type', '=', 'CHIL')
978
            ->where('link1.l_file', '=', $this->tree->id())
979
            ->distinct()
980
            ->select(['link1.l_from AS family', 'link1.l_to AS child1', 'link2.l_to AS child2', new Expression($prefix . 'child2.d_julianday2 - ' . $prefix . 'child1.d_julianday1 AS age')])
981
            ->orderBy('age', 'DESC')
982
            ->take($limit)
983
            ->get()
984
            ->map(fn (object $row): object => (object) [
985
                'family' => Registry::familyFactory()->make($row->family, $this->tree),
986
                'child1' => Registry::individualFactory()->make($row->child1, $this->tree),
987
                'child2' => Registry::individualFactory()->make($row->child2, $this->tree),
988
                'age'    => $this->calculateAge((int) $row->age),
989
            ])
990
            ->filter(static fn (object $row): bool => $row->family !== null)
991
            ->filter(static fn (object $row): bool => $row->child1 !== null)
992
            ->filter(static fn (object $row): bool => $row->child2 !== null)
993
            ->all();
994
    }
995
996
    /**
997
     * @return Collection<int,Individual>
998
     */
999
    public function topTenOldestAliveQuery(string $sex, int $limit): Collection
1000
    {
1001
        $query = DB::table('dates')
1002
            ->join('individuals', static function (JoinClause $join): void {
1003
                $join
1004
                    ->on('i_id', '=', 'd_gid')
1005
                    ->on('i_file', '=', 'd_file');
1006
            })
1007
            ->where('d_file', '=', $this->tree->id())
1008
            ->where('d_julianday1', '<>', 0)
1009
            ->where('d_fact', '=', 'BIRT')
1010
            ->where('i_gedcom', 'NOT LIKE', "%\n1 DEAT%")
1011
            ->where('i_gedcom', 'NOT LIKE', "%\n1 BURI%")
1012
            ->where('i_gedcom', 'NOT LIKE', "%\n1 CREM%");
1013
1014
        if ($sex === 'F' || $sex === 'M' || $sex === 'U' || $sex === 'X') {
1015
            $query->where('i_sex', '=', $sex);
1016
        }
1017
1018
        return $query
1019
            ->groupBy(['i_id', 'i_file'])
1020
            ->orderBy(new Expression('MIN(d_julianday1)'))
1021
            ->select(['individuals.*'])
1022
            ->take($limit)
1023
            ->get()
1024
            ->map(Registry::individualFactory()->mapper($this->tree))
1025
            ->filter(GedcomRecord::accessFilter());
1026
    }
1027
1028
    public function commonSurnamesQuery(string $type, bool $totals, int $threshold, int $limit, string $sort): string
1029
    {
1030
        $surnames = $this->commonSurnames($limit, $threshold, $sort);
1031
1032
        // find a module providing individual lists
1033
        $module = app(ModuleService::class)
1034
            ->findByComponent(ModuleListInterface::class, $this->tree, Auth::user())
1035
            ->first(static fn (ModuleInterface $module): bool => $module instanceof IndividualListModule);
1036
1037
        if ($type === 'list') {
1038
            return view('lists/surnames-bullet-list', [
1039
                'surnames' => $surnames,
1040
                'module'   => $module,
1041
                'totals'   => $totals,
1042
                'tree'     => $this->tree,
1043
            ]);
1044
        }
1045
1046
        return view('lists/surnames-compact-list', [
1047
            'surnames' => $surnames,
1048
            'module'   => $module,
1049
            'totals'   => $totals,
1050
            'tree'     => $this->tree,
1051
        ]);
1052
    }
1053
1054
    /**
1055
     * @return  array<object{age:float,century:int,sex:string}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{age:float,century:int,sex:string}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
1056
     */
1057
    public function statsAge(): array
1058
    {
1059
        $prefix = DB::connection()->getTablePrefix();
1060
1061
        return DB::table('individuals')
1062
            ->select([
1063
                new Expression('AVG(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) / 365.25 AS age'),
1064
                new Expression('ROUND((' . $prefix . 'death.d_year + 49) / 100, 0) AS century'),
1065
                'i_sex AS sex'
1066
            ])
1067
            ->join('dates AS birth', static function (JoinClause $join): void {
1068
                $join
1069
                    ->on('birth.d_file', '=', 'i_file')
1070
                    ->on('birth.d_gid', '=', 'i_id');
1071
            })
1072
            ->join('dates AS death', static function (JoinClause $join): void {
1073
                $join
1074
                    ->on('death.d_file', '=', 'i_file')
1075
                    ->on('death.d_gid', '=', 'i_id');
1076
            })
1077
            ->where('i_file', '=', $this->tree->id())
1078
            ->where('birth.d_fact', '=', 'BIRT')
1079
            ->where('death.d_fact', '=', 'DEAT')
1080
            ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1081
            ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1082
            ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2')
1083
            ->where('birth.d_julianday2', '<>', 0)
1084
            ->groupBy(['century', 'sex'])
1085
            ->orderBy('century')
1086
            ->orderBy('sex')
1087
            ->get()
1088
            ->map(static fn (object $row): object => (object) [
1089
                'age'     => (float) $row->age,
1090
                'century' => (int) $row->century,
1091
                'sex'     => $row->sex,
1092
            ])
1093
            ->all();
1094
    }
1095
1096
    /**
1097
     * General query on ages.
1098
     *
1099
     * @return array<object{days:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{days:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
1100
     */
1101
    public function statsAgeQuery(string $sex, int $year1, int $year2): array
1102
    {
1103
        $prefix = DB::connection()->getTablePrefix();
1104
1105
        $query = $this->birthAndDeathQuery($sex);
1106
1107
        if ($year1 !== 0 && $year2 !== 0) {
1108
            $query
1109
                ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1110
                ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1111
                ->whereBetween('death.d_year', [$year1, $year2]);
1112
        }
1113
1114
        return $query
1115
            ->select([new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')])
1116
            ->orderBy('days', 'desc')
1117
            ->get()
1118
            ->map(static fn (object $row): object => (object) ['days' => (int) $row->days])
1119
            ->all();
1120
    }
1121
1122
    private function birthAndDeathQuery(string $sex): Builder
1123
    {
1124
        $query = DB::table('individuals')
1125
            ->where('i_file', '=', $this->tree->id())
1126
            ->join('dates AS birth', static function (JoinClause $join): void {
1127
                $join
1128
                    ->on('birth.d_file', '=', 'i_file')
1129
                    ->on('birth.d_gid', '=', 'i_id');
1130
            })
1131
            ->join('dates AS death', static function (JoinClause $join): void {
1132
                $join
1133
                    ->on('death.d_file', '=', 'i_file')
1134
                    ->on('death.d_gid', '=', 'i_id');
1135
            })
1136
            ->where('birth.d_fact', '=', 'BIRT')
1137
            ->where('death.d_fact', '=', 'DEAT')
1138
            ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2')
1139
            ->where('birth.d_julianday2', '<>', 0);
1140
1141
        if ($sex !== 'ALL') {
1142
            $query->where('i_sex', '=', $sex);
1143
        }
1144
1145
        return $query;
1146
    }
1147
1148
    /**
1149
     * @return object{individual:Individual,days:int}|null
1150
     */
1151
    public function longlifeQuery(string $sex): ?object
1152
    {
1153
        $prefix = DB::connection()->getTablePrefix();
1154
1155
        return $this->birthAndDeathQuery($sex)
1156
            ->orderBy('days', 'desc')
1157
            ->select(['individuals.*', new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')])
1158
            ->take(1)
1159
            ->get()
1160
            ->map(fn (object $row): object => (object) [
1161
                'individual' => Registry::individualFactory()->mapper($this->tree)($row),
1162
                'days'       => (int) $row->days
1163
            ])
1164
            ->first();
1165
    }
1166
1167
    /**
1168
     * @return Collection<int,object{individual:Individual,days:int}>
1169
     */
1170
    public function topTenOldestQuery(string $sex, int $limit): Collection
1171
    {
1172
        $prefix = DB::connection()->getTablePrefix();
1173
1174
        return $this->birthAndDeathQuery($sex)
1175
            ->groupBy(['i_id', 'i_file'])
1176
            ->orderBy('days', 'desc')
1177
            ->select(['individuals.*', new Expression('MAX(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')])
1178
            ->take($limit)
1179
            ->get()
1180
            ->map(fn (object $row): object => (object) [
1181
                'individual' => Registry::individualFactory()->mapper($this->tree)($row),
1182
                'days'       => (int) $row->days
1183
            ]);
1184
    }
1185
1186
    /**
1187
     * @return array<string>
1188
     */
1189
    private function getIso3166Countries(): array
1190
    {
1191
        // Get the country names for each language
1192
        $country_to_iso3166 = [];
1193
1194
        $current_language = I18N::languageTag();
1195
1196
        foreach (I18N::activeLocales() as $locale) {
1197
            I18N::init($locale->languageTag());
1198
1199
            $countries = $this->getAllCountries();
1200
1201
            foreach ($this->iso3166() as $three => $two) {
1202
                $country_to_iso3166[$three]             = $two;
1203
                $country_to_iso3166[$countries[$three]] = $two;
1204
            }
1205
        }
1206
1207
        I18N::init($current_language);
1208
1209
        return $country_to_iso3166;
1210
    }
1211
1212
    /**
1213
     * Returns the data structure required by google geochart.
1214
     *
1215
     * @param array<int> $places
1216
     *
1217
     * @return array<int,array<int|string|array<string,string>>>
1218
     */
1219
    private function createChartData(array $places): array
1220
    {
1221
        $data = [
1222
            [
1223
                I18N::translate('Country'),
1224
                I18N::translate('Total'),
1225
            ],
1226
        ];
1227
1228
        // webtrees uses 3-letter country codes and localised country names, but google uses 2 letter codes.
1229
        foreach ($places as $country => $count) {
1230
            $data[] = [
1231
                [
1232
                    'v' => $country,
1233
                    'f' => $this->mapTwoLetterToName($country),
1234
                ],
1235
                $count
1236
            ];
1237
        }
1238
1239
        return $data;
1240
    }
1241
1242
    /**
1243
     * @return array<string,int>
1244
     */
1245
    private function countIndividualsByCountry(Tree $tree): array
1246
    {
1247
        $rows = DB::table('places')
1248
            ->where('p_file', '=', $tree->id())
1249
            ->where('p_parent_id', '=', 0)
1250
            ->join('placelinks', static function (JoinClause $join): void {
1251
                $join
1252
                    ->on('pl_file', '=', 'p_file')
1253
                    ->on('pl_p_id', '=', 'p_id');
1254
            })
1255
            ->join('individuals', static function (JoinClause $join): void {
1256
                $join
1257
                    ->on('pl_file', '=', 'i_file')
1258
                    ->on('pl_gid', '=', 'i_id');
1259
            })
1260
            ->groupBy('p_place')
1261
            ->pluck(new Expression('COUNT(*)'), 'p_place')
1262
            ->all();
1263
1264
        $totals = [];
1265
1266
        $country_to_iso3166 = $this->getIso3166Countries();
1267
1268
        foreach ($rows as $country => $count) {
1269
            $country_code = $country_to_iso3166[$country] ?? null;
1270
1271
            if ($country_code !== null) {
1272
                $totals[$country_code] ??= 0;
1273
                $totals[$country_code] += $count;
1274
            }
1275
        }
1276
1277
        return $totals;
1278
    }
1279
1280
    /**
1281
     * @return array<string,int>
1282
     */
1283
    private function countSurnamesByCountry(Tree $tree, string $surname): array
1284
    {
1285
        $rows =
1286
            DB::table('places')
1287
                ->where('p_file', '=', $tree->id())
1288
                ->where('p_parent_id', '=', 0)
1289
                ->join('placelinks', static function (JoinClause $join): void {
1290
                    $join
1291
                        ->on('pl_file', '=', 'p_file')
1292
                        ->on('pl_p_id', '=', 'p_id');
1293
                })
1294
                ->join('name', static function (JoinClause $join): void {
1295
                    $join
1296
                        ->on('n_file', '=', 'pl_file')
1297
                        ->on('n_id', '=', 'pl_gid');
1298
                })
1299
                ->where('n_surn', '=', $surname)
1300
                ->groupBy('p_place')
1301
                ->pluck(new Expression('COUNT(*)'), 'p_place');
1302
1303
        $totals = [];
1304
1305
        $country_to_iso3166 = $this->getIso3166Countries();
1306
1307
        foreach ($rows as $country => $count) {
1308
            $country_code = $country_to_iso3166[$country] ?? null;
1309
1310
            if ($country_code !== null) {
1311
                $totals[$country_code] ??= 0;
1312
                $totals[$country_code] += $count;
1313
            }
1314
        }
1315
1316
        return $totals;
1317
    }
1318
1319
    /**
1320
     * @return array<string,int>
1321
     */
1322
    private function countFamilyEventsByCountry(Tree $tree, string $fact): array
1323
    {
1324
        $query = DB::table('places')
1325
            ->where('p_file', '=', $tree->id())
1326
            ->where('p_parent_id', '=', 0)
1327
            ->join('placelinks', static function (JoinClause $join): void {
1328
                $join
1329
                    ->on('pl_file', '=', 'p_file')
1330
                    ->on('pl_p_id', '=', 'p_id');
1331
            })
1332
            ->join('families', static function (JoinClause $join): void {
1333
                $join
1334
                    ->on('pl_file', '=', 'f_file')
1335
                    ->on('pl_gid', '=', 'f_id');
1336
            })
1337
            ->select(['p_place AS place', 'f_gedcom AS gedcom']);
1338
1339
        return $this->filterEventPlaces($query, $fact);
1340
    }
1341
1342
    /**
1343
     * @return array<string,int>
1344
     */
1345
    private function countIndividualEventsByCountry(Tree $tree, string $fact): array
1346
    {
1347
        $query = DB::table('places')
1348
            ->where('p_file', '=', $tree->id())
1349
            ->where('p_parent_id', '=', 0)
1350
            ->join('placelinks', static function (JoinClause $join): void {
1351
                $join
1352
                    ->on('pl_file', '=', 'p_file')
1353
                    ->on('pl_p_id', '=', 'p_id');
1354
            })
1355
            ->join('individuals', static function (JoinClause $join): void {
1356
                $join
1357
                    ->on('pl_file', '=', 'i_file')
1358
                    ->on('pl_gid', '=', 'i_id');
1359
            })
1360
            ->select(['p_place AS place', 'i_gedcom AS gedcom']);
1361
1362
        return $this->filterEventPlaces($query, $fact);
1363
    }
1364
1365
    /**
1366
     * @return array<string,int>
1367
     */
1368
    private function filterEventPlaces(Builder $query, string $fact): array
1369
    {
1370
        $totals = [];
1371
1372
        $country_to_iso3166 = $this->getIso3166Countries();
1373
1374
        foreach ($query->cursor() as $row) {
1375
            $country_code = $country_to_iso3166[$row->place] ?? null;
1376
1377
            if ($country_code !== null) {
1378
                $place_regex = '/\n1 ' . $fact . '(?:\n[2-9].*)*\n2 PLAC.*[, ]' . preg_quote($row->place, '(?:\n|$)/i') . '\n/';
1379
1380
                if (preg_match($place_regex, $row->gedcom) === 1) {
1381
                    $totals[$country_code] = 1 + ($totals[$country_code] ?? 0);
1382
                }
1383
            }
1384
        }
1385
1386
        return $totals;
1387
    }
1388
1389
    /**
1390
     * Create a chart showing where events occurred.
1391
     *
1392
     * @param string $chart_shows The type of chart map to show
1393
     * @param string $chart_type  The type of chart to show
1394
     * @param string $surname     The surname for surname based distribution chart
1395
     */
1396
    public function chartDistribution(
1397
        string $chart_shows = 'world',
1398
        string $chart_type = '',
1399
        string $surname = ''
1400
    ): string {
1401
        switch ($chart_type) {
1402
            case 'surname_distribution_chart':
1403
                $chart_title = I18N::translate('Surname distribution chart') . ': ' . $surname;
1404
                $surnames    = $this->commonSurnames(1, 0, 'count');
1405
                $surname     = implode(I18N::$list_separator, array_keys(array_shift($surnames) ?? []));
1406
                $data        = $this->createChartData($this->countSurnamesByCountry($this->tree, $surname));
1407
                break;
1408
1409
            case 'birth_distribution_chart':
1410
                $chart_title = I18N::translate('Birth by country');
1411
                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'BIRT'));
1412
                break;
1413
1414
            case 'death_distribution_chart':
1415
                $chart_title = I18N::translate('Death by country');
1416
                $data        = $this->createChartData($this->countIndividualEventsByCountry($this->tree, 'DEAT'));
1417
                break;
1418
1419
            case 'marriage_distribution_chart':
1420
                $chart_title = I18N::translate('Marriage by country');
1421
                $data        = $this->createChartData($this->countFamilyEventsByCountry($this->tree, 'MARR'));
1422
                break;
1423
1424
            case 'indi_distribution_chart':
1425
            default:
1426
                $chart_title = I18N::translate('Individual distribution chart');
1427
                $data        = $this->createChartData($this->countIndividualsByCountry($this->tree));
1428
                break;
1429
        }
1430
1431
        return view('statistics/other/charts/geo', [
1432
            'chart_title'  => $chart_title,
1433
            'chart_color2' => '84beff',
1434
            'chart_color3' => 'c3dfff',
1435
            'region'       => $chart_shows,
1436
            'data'         => $data,
1437
            'language'     => I18N::languageTag(),
1438
        ]);
1439
    }
1440
1441
    /**
1442
     * @return array<array<string,int|Family>>
1443
     */
1444
    private function topTenGrandFamilyQuery(int $limit): array
1445
    {
1446
        return DB::table('families')
1447
            ->join('link AS children', static function (JoinClause $join): void {
1448
                $join
1449
                    ->on('children.l_from', '=', 'f_id')
1450
                    ->on('children.l_file', '=', 'f_file')
1451
                    ->where('children.l_type', '=', 'CHIL');
1452
            })->join('link AS mchildren', static function (JoinClause $join): void {
1453
                $join
1454
                    ->on('mchildren.l_file', '=', 'children.l_file')
1455
                    ->on('mchildren.l_from', '=', 'children.l_to')
1456
                    ->where('mchildren.l_type', '=', 'FAMS');
1457
            })->join('link AS gchildren', static function (JoinClause $join): void {
1458
                $join
1459
                    ->on('gchildren.l_file', '=', 'mchildren.l_file')
1460
                    ->on('gchildren.l_from', '=', 'mchildren.l_to')
1461
                    ->where('gchildren.l_type', '=', 'CHIL');
1462
            })
1463
            ->where('f_file', '=', $this->tree->id())
1464
            ->groupBy(['f_id', 'f_file'])
1465
            ->orderBy(new Expression('COUNT(*)'), 'DESC')
1466
            ->select(['families.*'])
1467
            ->limit($limit)
1468
            ->get()
1469
            ->map(Registry::familyFactory()->mapper($this->tree))
1470
            ->filter(GedcomRecord::accessFilter())
1471
            ->map(static function (Family $family): array {
1472
                $count = 0;
1473
                foreach ($family->children() as $child) {
1474
                    foreach ($child->spouseFamilies() as $spouse_family) {
1475
                        $count += $spouse_family->children()->count();
1476
                    }
1477
                }
1478
1479
                return [
1480
                    'family' => $family,
1481
                    'count'  => $count,
1482
                ];
1483
            })
1484
            ->all();
1485
    }
1486
1487
    public function topTenLargestGrandFamily(int $limit = 10): string
1488
    {
1489
        return view('statistics/families/top10-nolist-grand', [
1490
            'records' => $this->topTenGrandFamilyQuery($limit),
1491
        ]);
1492
    }
1493
1494
    public function topTenLargestGrandFamilyList(int $limit = 10): string
1495
    {
1496
        return view('statistics/families/top10-list-grand', [
1497
            'records' => $this->topTenGrandFamilyQuery($limit),
1498
        ]);
1499
    }
1500
1501
    public function noChildrenFamiliesList(string $type = 'list'): string
1502
    {
1503
        $families = DB::table('families')
1504
            ->where('f_file', '=', $this->tree->id())
1505
            ->where('f_numchil', '=', 0)
1506
            ->get()
1507
            ->map(Registry::familyFactory()->mapper($this->tree))
1508
            ->filter(GedcomRecord::accessFilter());
1509
1510
        $top10 = [];
1511
1512
        foreach ($families as $family) {
1513
            if ($type === 'list') {
1514
                $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a></li>';
1515
            } else {
1516
                $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>';
1517
            }
1518
        }
1519
1520
        if ($type === 'list') {
1521
            $top10 = implode('', $top10);
1522
        } else {
1523
            $top10 = implode('; ', $top10);
1524
        }
1525
1526
        if ($type === 'list') {
1527
            return '<ul>' . $top10 . '</ul>';
1528
        }
1529
1530
        return $top10;
1531
    }
1532
1533
    /**
1534
     * Returns the calculated age the time of event.
1535
     *
1536
     * @param int $age The age from the database record
1537
     */
1538
    private function calculateAge(int $age): string
1539
    {
1540
        if ($age < 31) {
1541
            return I18N::plural('%s day', '%s days', $age, I18N::number($age));
1542
        }
1543
1544
        if ($age < 365) {
1545
            $months = (int) ($age / 30.5);
1546
1547
            return I18N::plural('%s month', '%s months', $months, I18N::number($months));
1548
        }
1549
1550
        $years = (int) ($age / 365.25);
1551
1552
        return I18N::plural('%s year', '%s years', $years, I18N::number($years));
1553
    }
1554
1555
    public function topAgeBetweenSiblings(): string
1556
    {
1557
        $rows = $this->maximumAgeBetweenSiblings(1);
1558
1559
        if ($rows === []) {
1560
            return I18N::translate('This information is not available.');
1561
        }
1562
1563
        return $rows[0]->age;
1564
    }
1565
1566
    public function topAgeBetweenSiblingsFullName(): string
1567
    {
1568
        $rows = $this->maximumAgeBetweenSiblings(1);
1569
1570
        if ($rows === []) {
1571
            return I18N::translate('This information is not available.');
1572
        }
1573
1574
        return view('statistics/families/top10-nolist-age', ['record' => (array) $rows[0]]);
1575
    }
1576
1577
    public function topAgeBetweenSiblingsList(int $limit, bool $unique_families): string
1578
    {
1579
        $rows    = $this->maximumAgeBetweenSiblings($limit);
1580
        $records = [];
1581
        $dist    = [];
1582
1583
        foreach ($rows as $row) {
1584
            if (!$unique_families || !in_array($row->family, $dist, true)) {
1585
                $records[] = [
1586
                    'child1' => $row->child1,
1587
                    'child2' => $row->child2,
1588
                    'family' => $row->family,
1589
                    'age'    => $row->age,
1590
                ];
1591
1592
                $dist[] = $row->family;
1593
            }
1594
        }
1595
1596
        return view('statistics/families/top10-list-age', [
1597
            'records' => $records,
1598
        ]);
1599
    }
1600
1601
    /**
1602
     * @return array<object{f_numchil:int,total:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{f_numchil:int,total:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
1603
     */
1604
    public function statsChildrenQuery(int $year1, int $year2): array
1605
    {
1606
        $query = DB::table('families')
1607
            ->where('f_file', '=', $this->tree->id())
1608
            ->groupBy(['f_numchil'])
1609
            ->select(['f_numchil', new Expression('COUNT(*) AS total')]);
1610
1611
        if ($year1 !== 0 && $year2 !== 0) {
1612
            $query
1613
                ->join('dates', static function (JoinClause $join): void {
1614
                    $join
1615
                        ->on('d_file', '=', 'f_file')
1616
                        ->on('d_gid', '=', 'f_id');
1617
                })
1618
                ->where('d_fact', '=', 'MARR')
1619
                ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
1620
                ->whereBetween('d_year', [$year1, $year2]);
1621
        }
1622
1623
        return $query->get()
1624
            ->map(static fn (object $row): object => (object) [
1625
                'f_numchil' => (int) $row->f_numchil,
1626
                'total'     => (int) $row->total,
1627
            ])
1628
            ->all();
1629
    }
1630
1631
    /**
1632
     * @return array<array<string,mixed>>
1633
     */
1634
    private function topTenFamilyQuery(int $limit): array
1635
    {
1636
        return DB::table('families')
1637
            ->where('f_file', '=', $this->tree->id())
1638
            ->orderBy('f_numchil', 'DESC')
1639
            ->limit($limit)
1640
            ->get()
1641
            ->map(Registry::familyFactory()->mapper($this->tree))
1642
            ->filter(GedcomRecord::accessFilter())
1643
            ->map(static fn (Family $family): array => [
1644
                'family' => $family,
1645
                'count'  => $family->numberOfChildren(),
1646
            ])
1647
            ->all();
1648
    }
1649
1650
    public function topTenLargestFamily(int $limit = 10): string
1651
    {
1652
        $records = $this->topTenFamilyQuery($limit);
1653
1654
        return view('statistics/families/top10-nolist', [
1655
            'records' => $records,
1656
        ]);
1657
    }
1658
1659
    public function topTenLargestFamilyList(int $limit = 10): string
1660
    {
1661
        $records = $this->topTenFamilyQuery($limit);
1662
1663
        return view('statistics/families/top10-list', [
1664
            'records' => $records,
1665
        ]);
1666
    }
1667
1668
    public function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string
1669
    {
1670
        $prefix = DB::connection()->getTablePrefix();
1671
1672
        if ($sex === 'F') {
1673
            $sex_field = 'WIFE';
1674
        } else {
1675
            $sex_field = 'HUSB';
1676
        }
1677
1678
        if ($age_dir !== 'ASC') {
1679
            $age_dir = 'DESC';
1680
        }
1681
1682
        $row = DB::table('link AS parentfamily')
1683
            ->join('link AS childfamily', static function (JoinClause $join): void {
1684
                $join
1685
                    ->on('childfamily.l_file', '=', 'parentfamily.l_file')
1686
                    ->on('childfamily.l_from', '=', 'parentfamily.l_from')
1687
                    ->where('childfamily.l_type', '=', 'CHIL');
1688
            })
1689
            ->join('dates AS birth', static function (JoinClause $join): void {
1690
                $join
1691
                    ->on('birth.d_file', '=', 'parentfamily.l_file')
1692
                    ->on('birth.d_gid', '=', 'parentfamily.l_to')
1693
                    ->where('birth.d_fact', '=', 'BIRT')
1694
                    ->where('birth.d_julianday1', '<>', 0);
1695
            })
1696
            ->join('dates AS childbirth', static function (JoinClause $join): void {
1697
                $join
1698
                    ->on('childbirth.d_file', '=', 'parentfamily.l_file')
1699
                    ->on('childbirth.d_gid', '=', 'childfamily.l_to')
1700
                    ->where('childbirth.d_fact', '=', 'BIRT');
1701
            })
1702
            ->where('childfamily.l_file', '=', $this->tree->id())
1703
            ->where('parentfamily.l_type', '=', $sex_field)
1704
            ->where('childbirth.d_julianday2', '>', new Expression($prefix . 'birth.d_julianday1'))
1705
            ->select(['parentfamily.l_to AS id', new Expression($prefix . 'childbirth.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')])
1706
            ->take(1)
1707
            ->orderBy('age', $age_dir)
1708
            ->get()
1709
            ->first();
1710
1711
        if ($row === null) {
1712
            return I18N::translate('This information is not available.');
1713
        }
1714
1715
        $person = Registry::individualFactory()->make($row->id, $this->tree);
1716
1717
        switch ($type) {
1718
            default:
1719
            case 'full':
1720
                if ($person !== null && $person->canShow()) {
1721
                    $result = $person->formatList();
1722
                } else {
1723
                    $result = I18N::translate('This information is private and cannot be shown.');
1724
                }
1725
                break;
1726
1727
            case 'name':
1728
                $result = '<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>';
1729
                break;
1730
1731
            case 'age':
1732
                $age = $row->age;
1733
1734
                if ($show_years) {
1735
                    $result = $this->calculateAge((int) $row->age);
1736
                } else {
1737
                    $result = (string) floor($age / 365.25);
1738
                }
1739
1740
                break;
1741
        }
1742
1743
        return $result;
1744
    }
1745
1746
    /**
1747
     * General query on age at marriage.
1748
     *
1749
     * @param string $type
1750
     * @param string $age_dir "ASC" or "DESC"
1751
     * @param int    $limit
1752
     */
1753
    public function ageOfMarriageQuery(string $type, string $age_dir, int $limit): string
1754
    {
1755
        $prefix = DB::connection()->getTablePrefix();
1756
1757
        $hrows = DB::table('families')
1758
            ->where('f_file', '=', $this->tree->id())
1759
            ->join('dates AS married', static function (JoinClause $join): void {
1760
                $join
1761
                    ->on('married.d_file', '=', 'f_file')
1762
                    ->on('married.d_gid', '=', 'f_id')
1763
                    ->where('married.d_fact', '=', 'MARR')
1764
                    ->where('married.d_julianday1', '<>', 0);
1765
            })
1766
            ->join('dates AS husbdeath', static function (JoinClause $join): void {
1767
                $join
1768
                    ->on('husbdeath.d_gid', '=', 'f_husb')
1769
                    ->on('husbdeath.d_file', '=', 'f_file')
1770
                    ->where('husbdeath.d_fact', '=', 'DEAT');
1771
            })
1772
            ->whereColumn('married.d_julianday1', '<', 'husbdeath.d_julianday2')
1773
            ->groupBy(['f_id'])
1774
            ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'husbdeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')])
1775
            ->get()
1776
            ->all();
1777
1778
        $wrows = DB::table('families')
1779
            ->where('f_file', '=', $this->tree->id())
1780
            ->join('dates AS married', static function (JoinClause $join): void {
1781
                $join
1782
                    ->on('married.d_file', '=', 'f_file')
1783
                    ->on('married.d_gid', '=', 'f_id')
1784
                    ->where('married.d_fact', '=', 'MARR')
1785
                    ->where('married.d_julianday1', '<>', 0);
1786
            })
1787
            ->join('dates AS wifedeath', static function (JoinClause $join): void {
1788
                $join
1789
                    ->on('wifedeath.d_gid', '=', 'f_wife')
1790
                    ->on('wifedeath.d_file', '=', 'f_file')
1791
                    ->where('wifedeath.d_fact', '=', 'DEAT');
1792
            })
1793
            ->whereColumn('married.d_julianday1', '<', 'wifedeath.d_julianday2')
1794
            ->groupBy(['f_id'])
1795
            ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'wifedeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')])
1796
            ->get()
1797
            ->all();
1798
1799
        $drows = DB::table('families')
1800
            ->where('f_file', '=', $this->tree->id())
1801
            ->join('dates AS married', static function (JoinClause $join): void {
1802
                $join
1803
                    ->on('married.d_file', '=', 'f_file')
1804
                    ->on('married.d_gid', '=', 'f_id')
1805
                    ->where('married.d_fact', '=', 'MARR')
1806
                    ->where('married.d_julianday1', '<>', 0);
1807
            })
1808
            ->join('dates AS divorced', static function (JoinClause $join): void {
1809
                $join
1810
                    ->on('divorced.d_gid', '=', 'f_id')
1811
                    ->on('divorced.d_file', '=', 'f_file')
1812
                    ->whereIn('divorced.d_fact', ['DIV', 'ANUL', '_SEPR']);
1813
            })
1814
            ->whereColumn('married.d_julianday1', '<', 'divorced.d_julianday2')
1815
            ->groupBy(['f_id'])
1816
            ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'divorced.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')])
1817
            ->get()
1818
            ->all();
1819
1820
        $rows = [];
1821
        foreach ($drows as $family) {
1822
            $rows[$family->family] = $family->age;
1823
        }
1824
1825
        foreach ($hrows as $family) {
1826
            if (!isset($rows[$family->family])) {
1827
                $rows[$family->family] = $family->age;
1828
            }
1829
        }
1830
1831
        foreach ($wrows as $family) {
1832
            if (!isset($rows[$family->family])) {
1833
                $rows[$family->family] = $family->age;
1834
            } elseif ($rows[$family->family] > $family->age) {
1835
                $rows[$family->family] = $family->age;
1836
            }
1837
        }
1838
1839
        if ($age_dir === 'DESC') {
1840
            arsort($rows);
1841
        } else {
1842
            asort($rows);
1843
        }
1844
1845
        $top10 = [];
1846
        $i     = 0;
1847
        foreach ($rows as $xref => $age) {
1848
            $family = Registry::familyFactory()->make((string) $xref, $this->tree);
1849
            if ($type === 'name') {
1850
                return $family->formatList();
1851
            }
1852
1853
            $age = $this->calculateAge((int) $age);
1854
1855
            if ($type === 'age') {
1856
                return $age;
1857
            }
1858
1859
            $husb = $family->husband();
1860
            $wife = $family->wife();
1861
1862
            if (
1863
                $husb instanceof Individual &&
1864
                $wife instanceof Individual &&
1865
                ($husb->getAllDeathDates() || !$husb->isDead()) &&
1866
                ($wife->getAllDeathDates() || !$wife->isDead())
1867
            ) {
1868
                if ($family->canShow()) {
1869
                    if ($type === 'list') {
1870
                        $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')' . '</li>';
1871
                    } else {
1872
                        $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')';
1873
                    }
1874
                }
1875
                if (++$i === $limit) {
1876
                    break;
1877
                }
1878
            }
1879
        }
1880
1881
        if ($type === 'list') {
1882
            $top10 = implode('', $top10);
1883
        } else {
1884
            $top10 = implode('; ', $top10);
1885
        }
1886
1887
        if (I18N::direction() === 'rtl') {
1888
            $top10 = str_replace([
1889
                '[',
1890
                ']',
1891
                '(',
1892
                ')',
1893
                '+',
1894
            ], [
1895
                '&rlm;[',
1896
                '&rlm;]',
1897
                '&rlm;(',
1898
                '&rlm;)',
1899
                '&rlm;+',
1900
            ], $top10);
1901
        }
1902
1903
        if ($type === 'list') {
1904
            return '<ul>' . $top10 . '</ul>';
1905
        }
1906
1907
        return $top10;
1908
    }
1909
1910
    /**
1911
     * @return array<array{family:Family,age:string}>
1912
     */
1913
    private function ageBetweenSpousesQuery(string $age_dir, int $limit): array
1914
    {
1915
        $prefix = DB::connection()->getTablePrefix();
1916
1917
        $query = DB::table('families')
1918
            ->where('f_file', '=', $this->tree->id())
1919
            ->join('dates AS wife', static function (JoinClause $join): void {
1920
                $join
1921
                    ->on('wife.d_gid', '=', 'f_wife')
1922
                    ->on('wife.d_file', '=', 'f_file')
1923
                    ->where('wife.d_fact', '=', 'BIRT')
1924
                    ->where('wife.d_julianday1', '<>', 0);
1925
            })
1926
            ->join('dates AS husb', static function (JoinClause $join): void {
1927
                $join
1928
                    ->on('husb.d_gid', '=', 'f_husb')
1929
                    ->on('husb.d_file', '=', 'f_file')
1930
                    ->where('husb.d_fact', '=', 'BIRT')
1931
                    ->where('husb.d_julianday1', '<>', 0);
1932
            });
1933
1934
        if ($age_dir === 'DESC') {
1935
            $query
1936
                ->whereColumn('wife.d_julianday1', '>=', 'husb.d_julianday1')
1937
                ->orderBy(new Expression('MIN(' . $prefix . 'wife.d_julianday1) - MIN(' . $prefix . 'husb.d_julianday1)'), 'DESC');
1938
        } else {
1939
            $query
1940
                ->whereColumn('husb.d_julianday1', '>=', 'wife.d_julianday1')
1941
                ->orderBy(new Expression('MIN(' . $prefix . 'husb.d_julianday1) - MIN(' . $prefix . 'wife.d_julianday1)'), 'DESC');
1942
        }
1943
1944
        return $query
1945
            ->groupBy(['f_id', 'f_file'])
1946
            ->select(['families.*'])
1947
            ->take($limit)
1948
            ->get()
1949
            ->map(Registry::familyFactory()->mapper($this->tree))
1950
            ->filter(GedcomRecord::accessFilter())
1951
            ->map(function (Family $family) use ($age_dir): array {
1952
                $husb_birt_jd = $family->husband()->getBirthDate()->minimumJulianDay();
1953
                $wife_birt_jd = $family->wife()->getBirthDate()->minimumJulianDay();
1954
1955
                if ($age_dir === 'DESC') {
1956
                    $diff = $wife_birt_jd - $husb_birt_jd;
1957
                } else {
1958
                    $diff = $husb_birt_jd - $wife_birt_jd;
1959
                }
1960
1961
                return [
1962
                    'family' => $family,
1963
                    'age'    => $this->calculateAge($diff),
1964
                ];
1965
            })
1966
            ->all();
1967
    }
1968
1969
    public function ageBetweenSpousesMF(int $limit = 10): string
1970
    {
1971
        $records = $this->ageBetweenSpousesQuery('DESC', $limit);
1972
1973
        return view('statistics/families/top10-nolist-spouses', [
1974
            'records' => $records,
1975
        ]);
1976
    }
1977
1978
    public function ageBetweenSpousesMFList(int $limit = 10): string
1979
    {
1980
        $records = $this->ageBetweenSpousesQuery('DESC', $limit);
1981
1982
        return view('statistics/families/top10-list-spouses', [
1983
            'records' => $records,
1984
        ]);
1985
    }
1986
1987
    public function ageBetweenSpousesFM(int $limit = 10): string
1988
    {
1989
        return view('statistics/families/top10-nolist-spouses', [
1990
            'records' => $this->ageBetweenSpousesQuery('ASC', $limit),
1991
        ]);
1992
    }
1993
1994
    public function ageBetweenSpousesFMList(int $limit = 10): string
1995
    {
1996
        return view('statistics/families/top10-list-spouses', [
1997
            'records' => $this->ageBetweenSpousesQuery('ASC', $limit),
1998
        ]);
1999
    }
2000
2001
    /**
2002
     * @return array<object{f_id:string,d_gid:string,age:int}>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<object{f_id:string,d_gid:string,age:int}> at position 2 could not be parsed: Expected '>' at position 2, but found 'object'.
Loading history...
2003
     */
2004
    public function statsMarrAgeQuery(string $sex, int $year1, int $year2): array
2005
    {
2006
        $prefix = DB::connection()->getTablePrefix();
2007
2008
        $query = DB::table('dates AS married')
2009
            ->join('families', static function (JoinClause $join): void {
2010
                $join
2011
                    ->on('f_file', '=', 'married.d_file')
2012
                    ->on('f_id', '=', 'married.d_gid');
2013
            })
2014
            ->join('dates AS birth', static function (JoinClause $join) use ($sex): void {
2015
                $join
2016
                    ->on('birth.d_file', '=', 'married.d_file')
2017
                    ->on('birth.d_gid', '=', $sex === 'M' ? 'f_husb' : 'f_wife')
2018
                    ->where('birth.d_julianday1', '<>', 0)
2019
                    ->where('birth.d_fact', '=', 'BIRT')
2020
                    ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
2021
            })
2022
            ->where('married.d_file', '=', $this->tree->id())
2023
            ->where('married.d_fact', '=', 'MARR')
2024
            ->whereIn('married.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
2025
            ->whereColumn('married.d_julianday1', '>', 'birth.d_julianday1')
2026
            ->select(['f_id', 'birth.d_gid', new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]);
2027
2028
        if ($year1 !== 0 && $year2 !== 0) {
2029
            $query->whereBetween('married.d_year', [$year1, $year2]);
2030
        }
2031
2032
        return $query
2033
            ->get()
2034
            ->map(static fn (object $row): object => (object) [
2035
                'f_id'  => $row->f_id,
2036
                'd_gid' => $row->d_gid,
2037
                'age'   => (int) $row->age,
2038
            ])
2039
            ->all();
2040
    }
2041
2042
    /**
2043
     * Query the database for marriage tags.
2044
     *
2045
     * @param string $show    "full", "name" or "age"
2046
     * @param string $age_dir "ASC" or "DESC"
2047
     * @param string $sex     "F" or "M"
2048
     * @param bool   $show_years
2049
     */
2050
    public function marriageQuery(string $show, string $age_dir, string $sex, bool $show_years): string
2051
    {
2052
        $prefix = DB::connection()->getTablePrefix();
2053
2054
        if ($sex === 'F') {
2055
            $sex_field = 'f_wife';
2056
        } else {
2057
            $sex_field = 'f_husb';
2058
        }
2059
2060
        if ($age_dir !== 'ASC') {
2061
            $age_dir = 'DESC';
2062
        }
2063
2064
        $row = DB::table('families')
2065
            ->join('dates AS married', static function (JoinClause $join): void {
2066
                $join
2067
                    ->on('married.d_file', '=', 'f_file')
2068
                    ->on('married.d_gid', '=', 'f_id')
2069
                    ->where('married.d_fact', '=', 'MARR');
2070
            })
2071
            ->join('individuals', static function (JoinClause $join) use ($sex, $sex_field): void {
2072
                $join
2073
                    ->on('i_file', '=', 'f_file')
2074
                    ->on('i_id', '=', $sex_field)
2075
                    ->where('i_sex', '=', $sex);
2076
            })
2077
            ->join('dates AS birth', static function (JoinClause $join): void {
2078
                $join
2079
                    ->on('birth.d_file', '=', 'i_file')
2080
                    ->on('birth.d_gid', '=', 'i_id')
2081
                    ->where('birth.d_fact', '=', 'BIRT')
2082
                    ->where('birth.d_julianday1', '<>', 0);
2083
            })
2084
            ->where('f_file', '=', $this->tree->id())
2085
            ->where('married.d_julianday2', '>', new Expression($prefix . 'birth.d_julianday1'))
2086
            ->orderBy(new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1'), $age_dir)
2087
            ->select(['f_id AS famid', $sex_field, new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age'), 'i_id'])
2088
            ->take(1)
2089
            ->get()
2090
            ->first();
2091
2092
        if ($row === null) {
2093
            return I18N::translate('This information is not available.');
2094
        }
2095
2096
        $family = Registry::familyFactory()->make($row->famid, $this->tree);
2097
        $person = Registry::individualFactory()->make($row->i_id, $this->tree);
2098
2099
        switch ($show) {
2100
            default:
2101
            case 'full':
2102
                if ($family !== null && $family->canShow()) {
2103
                    $result = $family->formatList();
2104
                } else {
2105
                    $result = I18N::translate('This information is private and cannot be shown.');
2106
                }
2107
                break;
2108
2109
            case 'name':
2110
                $result = '<a href="' . e($family->url()) . '">' . $person->fullName() . '</a>';
2111
                break;
2112
2113
            case 'age':
2114
                $age = $row->age;
2115
2116
                if ($show_years) {
2117
                    $result = $this->calculateAge((int) $row->age);
2118
                } else {
2119
                    $result = I18N::number((int) ($age / 365.25));
2120
                }
2121
2122
                break;
2123
        }
2124
2125
        return $result;
2126
    }
2127
2128
    /**
2129
     * Who is currently logged in?
2130
     *
2131
     * @param string $type "list" or "nolist"
2132
     */
2133
    private function usersLoggedInQuery(string $type): string
2134
    {
2135
        $content   = '';
2136
        $anonymous = 0;
2137
        $logged_in = [];
2138
2139
        foreach ($this->user_service->allLoggedIn() as $user) {
2140
            if (Auth::isAdmin() || $user->getPreference(UserInterface::PREF_IS_VISIBLE_ONLINE) === '1') {
2141
                $logged_in[] = $user;
2142
            } else {
2143
                $anonymous++;
2144
            }
2145
        }
2146
2147
        $count_logged_in = count($logged_in);
2148
2149
        if ($count_logged_in === 0 && $anonymous === 0) {
2150
            $content .= I18N::translate('No signed-in and no anonymous users');
2151
        }
2152
2153
        if ($anonymous > 0) {
2154
            $content .= '<b>' . I18N::plural('%s anonymous signed-in user', '%s anonymous signed-in users', $anonymous, I18N::number($anonymous)) . '</b>';
2155
        }
2156
2157
        if ($count_logged_in > 0) {
2158
            if ($anonymous !== 0) {
2159
                if ($type === 'list') {
2160
                    $content .= '<br><br>';
2161
                } else {
2162
                    $content .= ' ' . I18N::translate('and') . ' ';
2163
                }
2164
            }
2165
            $content .= '<b>' . I18N::plural('%s signed-in user', '%s signed-in users', $count_logged_in, I18N::number($count_logged_in)) . '</b>';
2166
            if ($type === 'list') {
2167
                $content .= '<ul>';
2168
            } else {
2169
                $content .= ': ';
2170
            }
2171
        }
2172
2173
        if (Auth::check()) {
2174
            foreach ($logged_in as $user) {
2175
                if ($type === 'list') {
2176
                    $content .= '<li>';
2177
                }
2178
2179
                $individual = Registry::individualFactory()->make($this->tree->getUserPreference($user, UserInterface::PREF_TREE_ACCOUNT_XREF), $this->tree);
2180
2181
                if ($individual instanceof Individual && $individual->canShow()) {
2182
                    $content .= '<a href="' . e($individual->url()) . '">' . e($user->realName()) . '</a>';
2183
                } else {
2184
                    $content .= e($user->realName());
2185
                }
2186
2187
                $content .= ' - ' . e($user->userName());
2188
2189
                if ($user->getPreference(UserInterface::PREF_CONTACT_METHOD) !== MessageService::CONTACT_METHOD_NONE && Auth::id() !== $user->id()) {
2190
                    $content .= '<a href="' . e(route(MessagePage::class, ['to' => $user->userName(), 'tree' => $this->tree->name()])) . '" class="btn btn-link" title="' . I18N::translate('Send a message') . '">' . view('icons/email') . '</a>';
2191
                }
2192
2193
                if ($type === 'list') {
2194
                    $content .= '</li>';
2195
                }
2196
            }
2197
        }
2198
2199
        if ($type === 'list') {
2200
            $content .= '</ul>';
2201
        }
2202
2203
        return $content;
2204
    }
2205
2206
    public function usersLoggedIn(): string
2207
    {
2208
        return $this->usersLoggedInQuery('nolist');
2209
    }
2210
2211
    public function usersLoggedInList(): string
2212
    {
2213
        return $this->usersLoggedInQuery('list');
2214
    }
2215
2216
    /**
2217
     * Century name, English => 21st, Polish => XXI, etc.
2218
     */
2219
    private function centuryName(int $century): string
2220
    {
2221
        if ($century < 0) {
2222
            return I18N::translate('%s BCE', $this->centuryName(-$century));
2223
        }
2224
2225
        // The current chart engine (Google charts) can't handle <sup></sup> markup
2226
        switch ($century) {
2227
            case 21:
2228
                return strip_tags(I18N::translateContext('CENTURY', '21st'));
2229
            case 20:
2230
                return strip_tags(I18N::translateContext('CENTURY', '20th'));
2231
            case 19:
2232
                return strip_tags(I18N::translateContext('CENTURY', '19th'));
2233
            case 18:
2234
                return strip_tags(I18N::translateContext('CENTURY', '18th'));
2235
            case 17:
2236
                return strip_tags(I18N::translateContext('CENTURY', '17th'));
2237
            case 16:
2238
                return strip_tags(I18N::translateContext('CENTURY', '16th'));
2239
            case 15:
2240
                return strip_tags(I18N::translateContext('CENTURY', '15th'));
2241
            case 14:
2242
                return strip_tags(I18N::translateContext('CENTURY', '14th'));
2243
            case 13:
2244
                return strip_tags(I18N::translateContext('CENTURY', '13th'));
2245
            case 12:
2246
                return strip_tags(I18N::translateContext('CENTURY', '12th'));
2247
            case 11:
2248
                return strip_tags(I18N::translateContext('CENTURY', '11th'));
2249
            case 10:
2250
                return strip_tags(I18N::translateContext('CENTURY', '10th'));
2251
            case 9:
2252
                return strip_tags(I18N::translateContext('CENTURY', '9th'));
2253
            case 8:
2254
                return strip_tags(I18N::translateContext('CENTURY', '8th'));
2255
            case 7:
2256
                return strip_tags(I18N::translateContext('CENTURY', '7th'));
2257
            case 6:
2258
                return strip_tags(I18N::translateContext('CENTURY', '6th'));
2259
            case 5:
2260
                return strip_tags(I18N::translateContext('CENTURY', '5th'));
2261
            case 4:
2262
                return strip_tags(I18N::translateContext('CENTURY', '4th'));
2263
            case 3:
2264
                return strip_tags(I18N::translateContext('CENTURY', '3rd'));
2265
            case 2:
2266
                return strip_tags(I18N::translateContext('CENTURY', '2nd'));
2267
            case 1:
2268
                return strip_tags(I18N::translateContext('CENTURY', '1st'));
2269
            default:
2270
                return ($century - 1) . '01-' . $century . '00';
2271
        }
2272
    }
2273
2274
    /**
2275
     * @return array<string>
2276
     */
2277
    private function getAllCountries(): array
2278
    {
2279
        return [
2280
            /* I18N: Name of a country or state */
2281
            '???' => I18N::translate('Unknown'),
2282
            /* I18N: Name of a country or state */
2283
            'ABW' => I18N::translate('Aruba'),
2284
            /* I18N: Name of a country or state */
2285
            'AFG' => I18N::translate('Afghanistan'),
2286
            /* I18N: Name of a country or state */
2287
            'AGO' => I18N::translate('Angola'),
2288
            /* I18N: Name of a country or state */
2289
            'AIA' => I18N::translate('Anguilla'),
2290
            /* I18N: Name of a country or state */
2291
            'ALA' => I18N::translate('Åland Islands'),
2292
            /* I18N: Name of a country or state */
2293
            'ALB' => I18N::translate('Albania'),
2294
            /* I18N: Name of a country or state */
2295
            'AND' => I18N::translate('Andorra'),
2296
            /* I18N: Name of a country or state */
2297
            'ARE' => I18N::translate('United Arab Emirates'),
2298
            /* I18N: Name of a country or state */
2299
            'ARG' => I18N::translate('Argentina'),
2300
            /* I18N: Name of a country or state */
2301
            'ARM' => I18N::translate('Armenia'),
2302
            /* I18N: Name of a country or state */
2303
            'ASM' => I18N::translate('American Samoa'),
2304
            /* I18N: Name of a country or state */
2305
            'ATA' => I18N::translate('Antarctica'),
2306
            /* I18N: Name of a country or state */
2307
            'ATF' => I18N::translate('French Southern Territories'),
2308
            /* I18N: Name of a country or state */
2309
            'ATG' => I18N::translate('Antigua and Barbuda'),
2310
            /* I18N: Name of a country or state */
2311
            'AUS' => I18N::translate('Australia'),
2312
            /* I18N: Name of a country or state */
2313
            'AUT' => I18N::translate('Austria'),
2314
            /* I18N: Name of a country or state */
2315
            'AZE' => I18N::translate('Azerbaijan'),
2316
            /* I18N: Name of a country or state */
2317
            'AZR' => I18N::translate('Azores'),
2318
            /* I18N: Name of a country or state */
2319
            'BDI' => I18N::translate('Burundi'),
2320
            /* I18N: Name of a country or state */
2321
            'BEL' => I18N::translate('Belgium'),
2322
            /* I18N: Name of a country or state */
2323
            'BEN' => I18N::translate('Benin'),
2324
            // BES => Bonaire, Sint Eustatius and Saba
2325
            /* I18N: Name of a country or state */
2326
            'BFA' => I18N::translate('Burkina Faso'),
2327
            /* I18N: Name of a country or state */
2328
            'BGD' => I18N::translate('Bangladesh'),
2329
            /* I18N: Name of a country or state */
2330
            'BGR' => I18N::translate('Bulgaria'),
2331
            /* I18N: Name of a country or state */
2332
            'BHR' => I18N::translate('Bahrain'),
2333
            /* I18N: Name of a country or state */
2334
            'BHS' => I18N::translate('Bahamas'),
2335
            /* I18N: Name of a country or state */
2336
            'BIH' => I18N::translate('Bosnia and Herzegovina'),
2337
            // BLM => Saint Barthélemy
2338
            'BLM' => I18N::translate('Saint Barthélemy'),
2339
            /* I18N: Name of a country or state */
2340
            'BLR' => I18N::translate('Belarus'),
2341
            /* I18N: Name of a country or state */
2342
            'BLZ' => I18N::translate('Belize'),
2343
            /* I18N: Name of a country or state */
2344
            'BMU' => I18N::translate('Bermuda'),
2345
            /* I18N: Name of a country or state */
2346
            'BOL' => I18N::translate('Bolivia'),
2347
            /* I18N: Name of a country or state */
2348
            'BRA' => I18N::translate('Brazil'),
2349
            /* I18N: Name of a country or state */
2350
            'BRB' => I18N::translate('Barbados'),
2351
            /* I18N: Name of a country or state */
2352
            'BRN' => I18N::translate('Brunei Darussalam'),
2353
            /* I18N: Name of a country or state */
2354
            'BTN' => I18N::translate('Bhutan'),
2355
            /* I18N: Name of a country or state */
2356
            'BVT' => I18N::translate('Bouvet Island'),
2357
            /* I18N: Name of a country or state */
2358
            'BWA' => I18N::translate('Botswana'),
2359
            /* I18N: Name of a country or state */
2360
            'CAF' => I18N::translate('Central African Republic'),
2361
            /* I18N: Name of a country or state */
2362
            'CAN' => I18N::translate('Canada'),
2363
            /* I18N: Name of a country or state */
2364
            'CCK' => I18N::translate('Cocos (Keeling) Islands'),
2365
            /* I18N: Name of a country or state */
2366
            'CHE' => I18N::translate('Switzerland'),
2367
            /* I18N: Name of a country or state */
2368
            'CHL' => I18N::translate('Chile'),
2369
            /* I18N: Name of a country or state */
2370
            'CHN' => I18N::translate('China'),
2371
            /* I18N: Name of a country or state */
2372
            'CIV' => I18N::translate('Côte d’Ivoire'),
2373
            /* I18N: Name of a country or state */
2374
            'CMR' => I18N::translate('Cameroon'),
2375
            /* I18N: Name of a country or state */
2376
            'COD' => I18N::translate('Democratic Republic of the Congo'),
2377
            /* I18N: Name of a country or state */
2378
            'COG' => I18N::translate('Republic of the Congo'),
2379
            /* I18N: Name of a country or state */
2380
            'COK' => I18N::translate('Cook Islands'),
2381
            /* I18N: Name of a country or state */
2382
            'COL' => I18N::translate('Colombia'),
2383
            /* I18N: Name of a country or state */
2384
            'COM' => I18N::translate('Comoros'),
2385
            /* I18N: Name of a country or state */
2386
            'CPV' => I18N::translate('Cape Verde'),
2387
            /* I18N: Name of a country or state */
2388
            'CRI' => I18N::translate('Costa Rica'),
2389
            /* I18N: Name of a country or state */
2390
            'CUB' => I18N::translate('Cuba'),
2391
            /* I18N: Name of a country or state */
2392
            'CUW' => I18N::translate('Curaçao'),
2393
            /* I18N: Name of a country or state */
2394
            'CXR' => I18N::translate('Christmas Island'),
2395
            /* I18N: Name of a country or state */
2396
            'CYM' => I18N::translate('Cayman Islands'),
2397
            /* I18N: Name of a country or state */
2398
            'CYP' => I18N::translate('Cyprus'),
2399
            /* I18N: Name of a country or state */
2400
            'CZE' => I18N::translate('Czech Republic'),
2401
            /* I18N: Name of a country or state */
2402
            'DEU' => I18N::translate('Germany'),
2403
            /* I18N: Name of a country or state */
2404
            'DJI' => I18N::translate('Djibouti'),
2405
            /* I18N: Name of a country or state */
2406
            'DMA' => I18N::translate('Dominica'),
2407
            /* I18N: Name of a country or state */
2408
            'DNK' => I18N::translate('Denmark'),
2409
            /* I18N: Name of a country or state */
2410
            'DOM' => I18N::translate('Dominican Republic'),
2411
            /* I18N: Name of a country or state */
2412
            'DZA' => I18N::translate('Algeria'),
2413
            /* I18N: Name of a country or state */
2414
            'ECU' => I18N::translate('Ecuador'),
2415
            /* I18N: Name of a country or state */
2416
            'EGY' => I18N::translate('Egypt'),
2417
            /* I18N: Name of a country or state */
2418
            'ENG' => I18N::translate('England'),
2419
            /* I18N: Name of a country or state */
2420
            'ERI' => I18N::translate('Eritrea'),
2421
            /* I18N: Name of a country or state */
2422
            'ESH' => I18N::translate('Western Sahara'),
2423
            /* I18N: Name of a country or state */
2424
            'ESP' => I18N::translate('Spain'),
2425
            /* I18N: Name of a country or state */
2426
            'EST' => I18N::translate('Estonia'),
2427
            /* I18N: Name of a country or state */
2428
            'ETH' => I18N::translate('Ethiopia'),
2429
            /* I18N: Name of a country or state */
2430
            'FIN' => I18N::translate('Finland'),
2431
            /* I18N: Name of a country or state */
2432
            'FJI' => I18N::translate('Fiji'),
2433
            /* I18N: Name of a country or state */
2434
            'FLD' => I18N::translate('Flanders'),
2435
            /* I18N: Name of a country or state */
2436
            'FLK' => I18N::translate('Falkland Islands'),
2437
            /* I18N: Name of a country or state */
2438
            'FRA' => I18N::translate('France'),
2439
            /* I18N: Name of a country or state */
2440
            'FRO' => I18N::translate('Faroe Islands'),
2441
            /* I18N: Name of a country or state */
2442
            'FSM' => I18N::translate('Micronesia'),
2443
            /* I18N: Name of a country or state */
2444
            'GAB' => I18N::translate('Gabon'),
2445
            /* I18N: Name of a country or state */
2446
            'GBR' => I18N::translate('United Kingdom'),
2447
            /* I18N: Name of a country or state */
2448
            'GEO' => I18N::translate('Georgia'),
2449
            /* I18N: Name of a country or state */
2450
            'GGY' => I18N::translate('Guernsey'),
2451
            /* I18N: Name of a country or state */
2452
            'GHA' => I18N::translate('Ghana'),
2453
            /* I18N: Name of a country or state */
2454
            'GIB' => I18N::translate('Gibraltar'),
2455
            /* I18N: Name of a country or state */
2456
            'GIN' => I18N::translate('Guinea'),
2457
            /* I18N: Name of a country or state */
2458
            'GLP' => I18N::translate('Guadeloupe'),
2459
            /* I18N: Name of a country or state */
2460
            'GMB' => I18N::translate('Gambia'),
2461
            /* I18N: Name of a country or state */
2462
            'GNB' => I18N::translate('Guinea-Bissau'),
2463
            /* I18N: Name of a country or state */
2464
            'GNQ' => I18N::translate('Equatorial Guinea'),
2465
            /* I18N: Name of a country or state */
2466
            'GRC' => I18N::translate('Greece'),
2467
            /* I18N: Name of a country or state */
2468
            'GRD' => I18N::translate('Grenada'),
2469
            /* I18N: Name of a country or state */
2470
            'GRL' => I18N::translate('Greenland'),
2471
            /* I18N: Name of a country or state */
2472
            'GTM' => I18N::translate('Guatemala'),
2473
            /* I18N: Name of a country or state */
2474
            'GUF' => I18N::translate('French Guiana'),
2475
            /* I18N: Name of a country or state */
2476
            'GUM' => I18N::translate('Guam'),
2477
            /* I18N: Name of a country or state */
2478
            'GUY' => I18N::translate('Guyana'),
2479
            /* I18N: Name of a country or state */
2480
            'HKG' => I18N::translate('Hong Kong'),
2481
            /* I18N: Name of a country or state */
2482
            'HMD' => I18N::translate('Heard Island and McDonald Islands'),
2483
            /* I18N: Name of a country or state */
2484
            'HND' => I18N::translate('Honduras'),
2485
            /* I18N: Name of a country or state */
2486
            'HRV' => I18N::translate('Croatia'),
2487
            /* I18N: Name of a country or state */
2488
            'HTI' => I18N::translate('Haiti'),
2489
            /* I18N: Name of a country or state */
2490
            'HUN' => I18N::translate('Hungary'),
2491
            /* I18N: Name of a country or state */
2492
            'IDN' => I18N::translate('Indonesia'),
2493
            /* I18N: Name of a country or state */
2494
            'IND' => I18N::translate('India'),
2495
            /* I18N: Name of a country or state */
2496
            'IOM' => I18N::translate('Isle of Man'),
2497
            /* I18N: Name of a country or state */
2498
            'IOT' => I18N::translate('British Indian Ocean Territory'),
2499
            /* I18N: Name of a country or state */
2500
            'IRL' => I18N::translate('Ireland'),
2501
            /* I18N: Name of a country or state */
2502
            'IRN' => I18N::translate('Iran'),
2503
            /* I18N: Name of a country or state */
2504
            'IRQ' => I18N::translate('Iraq'),
2505
            /* I18N: Name of a country or state */
2506
            'ISL' => I18N::translate('Iceland'),
2507
            /* I18N: Name of a country or state */
2508
            'ISR' => I18N::translate('Israel'),
2509
            /* I18N: Name of a country or state */
2510
            'ITA' => I18N::translate('Italy'),
2511
            /* I18N: Name of a country or state */
2512
            'JAM' => I18N::translate('Jamaica'),
2513
            //'JEY' => Jersey
2514
            /* I18N: Name of a country or state */
2515
            'JOR' => I18N::translate('Jordan'),
2516
            /* I18N: Name of a country or state */
2517
            'JPN' => I18N::translate('Japan'),
2518
            /* I18N: Name of a country or state */
2519
            'KAZ' => I18N::translate('Kazakhstan'),
2520
            /* I18N: Name of a country or state */
2521
            'KEN' => I18N::translate('Kenya'),
2522
            /* I18N: Name of a country or state */
2523
            'KGZ' => I18N::translate('Kyrgyzstan'),
2524
            /* I18N: Name of a country or state */
2525
            'KHM' => I18N::translate('Cambodia'),
2526
            /* I18N: Name of a country or state */
2527
            'KIR' => I18N::translate('Kiribati'),
2528
            /* I18N: Name of a country or state */
2529
            'KNA' => I18N::translate('Saint Kitts and Nevis'),
2530
            /* I18N: Name of a country or state */
2531
            'KOR' => I18N::translate('Korea'),
2532
            /* I18N: Name of a country or state */
2533
            'KWT' => I18N::translate('Kuwait'),
2534
            /* I18N: Name of a country or state */
2535
            'LAO' => I18N::translate('Laos'),
2536
            /* I18N: Name of a country or state */
2537
            'LBN' => I18N::translate('Lebanon'),
2538
            /* I18N: Name of a country or state */
2539
            'LBR' => I18N::translate('Liberia'),
2540
            /* I18N: Name of a country or state */
2541
            'LBY' => I18N::translate('Libya'),
2542
            /* I18N: Name of a country or state */
2543
            'LCA' => I18N::translate('Saint Lucia'),
2544
            /* I18N: Name of a country or state */
2545
            'LIE' => I18N::translate('Liechtenstein'),
2546
            /* I18N: Name of a country or state */
2547
            'LKA' => I18N::translate('Sri Lanka'),
2548
            /* I18N: Name of a country or state */
2549
            'LSO' => I18N::translate('Lesotho'),
2550
            /* I18N: Name of a country or state */
2551
            'LTU' => I18N::translate('Lithuania'),
2552
            /* I18N: Name of a country or state */
2553
            'LUX' => I18N::translate('Luxembourg'),
2554
            /* I18N: Name of a country or state */
2555
            'LVA' => I18N::translate('Latvia'),
2556
            /* I18N: Name of a country or state */
2557
            'MAC' => I18N::translate('Macau'),
2558
            // MAF => Saint Martin
2559
            /* I18N: Name of a country or state */
2560
            'MAR' => I18N::translate('Morocco'),
2561
            /* I18N: Name of a country or state */
2562
            'MCO' => I18N::translate('Monaco'),
2563
            /* I18N: Name of a country or state */
2564
            'MDA' => I18N::translate('Moldova'),
2565
            /* I18N: Name of a country or state */
2566
            'MDG' => I18N::translate('Madagascar'),
2567
            /* I18N: Name of a country or state */
2568
            'MDV' => I18N::translate('Maldives'),
2569
            /* I18N: Name of a country or state */
2570
            'MEX' => I18N::translate('Mexico'),
2571
            /* I18N: Name of a country or state */
2572
            'MHL' => I18N::translate('Marshall Islands'),
2573
            /* I18N: Name of a country or state */
2574
            'MKD' => I18N::translate('Macedonia'),
2575
            /* I18N: Name of a country or state */
2576
            'MLI' => I18N::translate('Mali'),
2577
            /* I18N: Name of a country or state */
2578
            'MLT' => I18N::translate('Malta'),
2579
            /* I18N: Name of a country or state */
2580
            'MMR' => I18N::translate('Myanmar'),
2581
            /* I18N: Name of a country or state */
2582
            'MNG' => I18N::translate('Mongolia'),
2583
            /* I18N: Name of a country or state */
2584
            'MNP' => I18N::translate('Northern Mariana Islands'),
2585
            /* I18N: Name of a country or state */
2586
            'MNT' => I18N::translate('Montenegro'),
2587
            /* I18N: Name of a country or state */
2588
            'MOZ' => I18N::translate('Mozambique'),
2589
            /* I18N: Name of a country or state */
2590
            'MRT' => I18N::translate('Mauritania'),
2591
            /* I18N: Name of a country or state */
2592
            'MSR' => I18N::translate('Montserrat'),
2593
            /* I18N: Name of a country or state */
2594
            'MTQ' => I18N::translate('Martinique'),
2595
            /* I18N: Name of a country or state */
2596
            'MUS' => I18N::translate('Mauritius'),
2597
            /* I18N: Name of a country or state */
2598
            'MWI' => I18N::translate('Malawi'),
2599
            /* I18N: Name of a country or state */
2600
            'MYS' => I18N::translate('Malaysia'),
2601
            /* I18N: Name of a country or state */
2602
            'MYT' => I18N::translate('Mayotte'),
2603
            /* I18N: Name of a country or state */
2604
            'NAM' => I18N::translate('Namibia'),
2605
            /* I18N: Name of a country or state */
2606
            'NCL' => I18N::translate('New Caledonia'),
2607
            /* I18N: Name of a country or state */
2608
            'NER' => I18N::translate('Niger'),
2609
            /* I18N: Name of a country or state */
2610
            'NFK' => I18N::translate('Norfolk Island'),
2611
            /* I18N: Name of a country or state */
2612
            'NGA' => I18N::translate('Nigeria'),
2613
            /* I18N: Name of a country or state */
2614
            'NIC' => I18N::translate('Nicaragua'),
2615
            /* I18N: Name of a country or state */
2616
            'NIR' => I18N::translate('Northern Ireland'),
2617
            /* I18N: Name of a country or state */
2618
            'NIU' => I18N::translate('Niue'),
2619
            /* I18N: Name of a country or state */
2620
            'NLD' => I18N::translate('Netherlands'),
2621
            /* I18N: Name of a country or state */
2622
            'NOR' => I18N::translate('Norway'),
2623
            /* I18N: Name of a country or state */
2624
            'NPL' => I18N::translate('Nepal'),
2625
            /* I18N: Name of a country or state */
2626
            'NRU' => I18N::translate('Nauru'),
2627
            /* I18N: Name of a country or state */
2628
            'NZL' => I18N::translate('New Zealand'),
2629
            /* I18N: Name of a country or state */
2630
            'OMN' => I18N::translate('Oman'),
2631
            /* I18N: Name of a country or state */
2632
            'PAK' => I18N::translate('Pakistan'),
2633
            /* I18N: Name of a country or state */
2634
            'PAN' => I18N::translate('Panama'),
2635
            /* I18N: Name of a country or state */
2636
            'PCN' => I18N::translate('Pitcairn'),
2637
            /* I18N: Name of a country or state */
2638
            'PER' => I18N::translate('Peru'),
2639
            /* I18N: Name of a country or state */
2640
            'PHL' => I18N::translate('Philippines'),
2641
            /* I18N: Name of a country or state */
2642
            'PLW' => I18N::translate('Palau'),
2643
            /* I18N: Name of a country or state */
2644
            'PNG' => I18N::translate('Papua New Guinea'),
2645
            /* I18N: Name of a country or state */
2646
            'POL' => I18N::translate('Poland'),
2647
            /* I18N: Name of a country or state */
2648
            'PRI' => I18N::translate('Puerto Rico'),
2649
            /* I18N: Name of a country or state */
2650
            'PRK' => I18N::translate('North Korea'),
2651
            /* I18N: Name of a country or state */
2652
            'PRT' => I18N::translate('Portugal'),
2653
            /* I18N: Name of a country or state */
2654
            'PRY' => I18N::translate('Paraguay'),
2655
            /* I18N: Name of a country or state */
2656
            'PSE' => I18N::translate('Occupied Palestinian Territory'),
2657
            /* I18N: Name of a country or state */
2658
            'PYF' => I18N::translate('French Polynesia'),
2659
            /* I18N: Name of a country or state */
2660
            'QAT' => I18N::translate('Qatar'),
2661
            /* I18N: Name of a country or state */
2662
            'REU' => I18N::translate('Réunion'),
2663
            /* I18N: Name of a country or state */
2664
            'ROM' => I18N::translate('Romania'),
2665
            /* I18N: Name of a country or state */
2666
            'RUS' => I18N::translate('Russia'),
2667
            /* I18N: Name of a country or state */
2668
            'RWA' => I18N::translate('Rwanda'),
2669
            /* I18N: Name of a country or state */
2670
            'SAU' => I18N::translate('Saudi Arabia'),
2671
            /* I18N: Name of a country or state */
2672
            'SCT' => I18N::translate('Scotland'),
2673
            /* I18N: Name of a country or state */
2674
            'SDN' => I18N::translate('Sudan'),
2675
            /* I18N: Name of a country or state */
2676
            'SEA' => I18N::translate('At sea'),
2677
            /* I18N: Name of a country or state */
2678
            'SEN' => I18N::translate('Senegal'),
2679
            /* I18N: Name of a country or state */
2680
            'SER' => I18N::translate('Serbia'),
2681
            /* I18N: Name of a country or state */
2682
            'SGP' => I18N::translate('Singapore'),
2683
            /* I18N: Name of a country or state */
2684
            'SGS' => I18N::translate('South Georgia and the South Sandwich Islands'),
2685
            /* I18N: Name of a country or state */
2686
            'SHN' => I18N::translate('Saint Helena'),
2687
            /* I18N: Name of a country or state */
2688
            'SJM' => I18N::translate('Svalbard and Jan Mayen'),
2689
            /* I18N: Name of a country or state */
2690
            'SLB' => I18N::translate('Solomon Islands'),
2691
            /* I18N: Name of a country or state */
2692
            'SLE' => I18N::translate('Sierra Leone'),
2693
            /* I18N: Name of a country or state */
2694
            'SLV' => I18N::translate('El Salvador'),
2695
            /* I18N: Name of a country or state */
2696
            'SMR' => I18N::translate('San Marino'),
2697
            /* I18N: Name of a country or state */
2698
            'SOM' => I18N::translate('Somalia'),
2699
            /* I18N: Name of a country or state */
2700
            'SPM' => I18N::translate('Saint Pierre and Miquelon'),
2701
            /* I18N: Name of a country or state */
2702
            'SSD' => I18N::translate('South Sudan'),
2703
            /* I18N: Name of a country or state */
2704
            'STP' => I18N::translate('Sao Tome and Principe'),
2705
            /* I18N: Name of a country or state */
2706
            'SUR' => I18N::translate('Suriname'),
2707
            /* I18N: Name of a country or state */
2708
            'SVK' => I18N::translate('Slovakia'),
2709
            /* I18N: Name of a country or state */
2710
            'SVN' => I18N::translate('Slovenia'),
2711
            /* I18N: Name of a country or state */
2712
            'SWE' => I18N::translate('Sweden'),
2713
            /* I18N: Name of a country or state */
2714
            'SWZ' => I18N::translate('Swaziland'),
2715
            // SXM => Sint Maarten
2716
            /* I18N: Name of a country or state */
2717
            'SYC' => I18N::translate('Seychelles'),
2718
            /* I18N: Name of a country or state */
2719
            'SYR' => I18N::translate('Syria'),
2720
            /* I18N: Name of a country or state */
2721
            'TCA' => I18N::translate('Turks and Caicos Islands'),
2722
            /* I18N: Name of a country or state */
2723
            'TCD' => I18N::translate('Chad'),
2724
            /* I18N: Name of a country or state */
2725
            'TGO' => I18N::translate('Togo'),
2726
            /* I18N: Name of a country or state */
2727
            'THA' => I18N::translate('Thailand'),
2728
            /* I18N: Name of a country or state */
2729
            'TJK' => I18N::translate('Tajikistan'),
2730
            /* I18N: Name of a country or state */
2731
            'TKL' => I18N::translate('Tokelau'),
2732
            /* I18N: Name of a country or state */
2733
            'TKM' => I18N::translate('Turkmenistan'),
2734
            /* I18N: Name of a country or state */
2735
            'TLS' => I18N::translate('Timor-Leste'),
2736
            /* I18N: Name of a country or state */
2737
            'TON' => I18N::translate('Tonga'),
2738
            /* I18N: Name of a country or state */
2739
            'TTO' => I18N::translate('Trinidad and Tobago'),
2740
            /* I18N: Name of a country or state */
2741
            'TUN' => I18N::translate('Tunisia'),
2742
            /* I18N: Name of a country or state */
2743
            'TUR' => I18N::translate('Turkey'),
2744
            /* I18N: Name of a country or state */
2745
            'TUV' => I18N::translate('Tuvalu'),
2746
            /* I18N: Name of a country or state */
2747
            'TWN' => I18N::translate('Taiwan'),
2748
            /* I18N: Name of a country or state */
2749
            'TZA' => I18N::translate('Tanzania'),
2750
            /* I18N: Name of a country or state */
2751
            'UGA' => I18N::translate('Uganda'),
2752
            /* I18N: Name of a country or state */
2753
            'UKR' => I18N::translate('Ukraine'),
2754
            /* I18N: Name of a country or state */
2755
            'UMI' => I18N::translate('US Minor Outlying Islands'),
2756
            /* I18N: Name of a country or state */
2757
            'URY' => I18N::translate('Uruguay'),
2758
            /* I18N: Name of a country or state */
2759
            'USA' => I18N::translate('United States'),
2760
            /* I18N: Name of a country or state */
2761
            'UZB' => I18N::translate('Uzbekistan'),
2762
            /* I18N: Name of a country or state */
2763
            'VAT' => I18N::translate('Vatican City'),
2764
            /* I18N: Name of a country or state */
2765
            'VCT' => I18N::translate('Saint Vincent and the Grenadines'),
2766
            /* I18N: Name of a country or state */
2767
            'VEN' => I18N::translate('Venezuela'),
2768
            /* I18N: Name of a country or state */
2769
            'VGB' => I18N::translate('British Virgin Islands'),
2770
            /* I18N: Name of a country or state */
2771
            'VIR' => I18N::translate('US Virgin Islands'),
2772
            /* I18N: Name of a country or state */
2773
            'VNM' => I18N::translate('Vietnam'),
2774
            /* I18N: Name of a country or state */
2775
            'VUT' => I18N::translate('Vanuatu'),
2776
            /* I18N: Name of a country or state */
2777
            'WLF' => I18N::translate('Wallis and Futuna'),
2778
            /* I18N: Name of a country or state */
2779
            'WLS' => I18N::translate('Wales'),
2780
            /* I18N: Name of a country or state */
2781
            'WSM' => I18N::translate('Samoa'),
2782
            /* I18N: Name of a country or state */
2783
            'YEM' => I18N::translate('Yemen'),
2784
            /* I18N: Name of a country or state */
2785
            'ZAF' => I18N::translate('South Africa'),
2786
            /* I18N: Name of a country or state */
2787
            'ZMB' => I18N::translate('Zambia'),
2788
            /* I18N: Name of a country or state */
2789
            'ZWE' => I18N::translate('Zimbabwe'),
2790
        ];
2791
    }
2792
2793
    /**
2794
     * ISO3166 3 letter codes, with their 2 letter equivalent.
2795
     * NOTE: this is not 1:1. ENG/SCO/WAL/NIR => GB
2796
     * NOTE: this also includes chapman codes and others. Should it?
2797
     *
2798
     * @return array<string>
2799
     */
2800
    private function iso3166(): array
2801
    {
2802
        return [
2803
            'GBR' => 'GB', // Must come before ENG, NIR, SCT and WLS
2804
            'ABW' => 'AW',
2805
            'AFG' => 'AF',
2806
            'AGO' => 'AO',
2807
            'AIA' => 'AI',
2808
            'ALA' => 'AX',
2809
            'ALB' => 'AL',
2810
            'AND' => 'AD',
2811
            'ARE' => 'AE',
2812
            'ARG' => 'AR',
2813
            'ARM' => 'AM',
2814
            'ASM' => 'AS',
2815
            'ATA' => 'AQ',
2816
            'ATF' => 'TF',
2817
            'ATG' => 'AG',
2818
            'AUS' => 'AU',
2819
            'AUT' => 'AT',
2820
            'AZE' => 'AZ',
2821
            'BDI' => 'BI',
2822
            'BEL' => 'BE',
2823
            'BEN' => 'BJ',
2824
            'BFA' => 'BF',
2825
            'BGD' => 'BD',
2826
            'BGR' => 'BG',
2827
            'BHR' => 'BH',
2828
            'BHS' => 'BS',
2829
            'BIH' => 'BA',
2830
            'BLR' => 'BY',
2831
            'BLZ' => 'BZ',
2832
            'BMU' => 'BM',
2833
            'BOL' => 'BO',
2834
            'BRA' => 'BR',
2835
            'BRB' => 'BB',
2836
            'BRN' => 'BN',
2837
            'BTN' => 'BT',
2838
            'BVT' => 'BV',
2839
            'BWA' => 'BW',
2840
            'CAF' => 'CF',
2841
            'CAN' => 'CA',
2842
            'CCK' => 'CC',
2843
            'CHE' => 'CH',
2844
            'CHL' => 'CL',
2845
            'CHN' => 'CN',
2846
            'CIV' => 'CI',
2847
            'CMR' => 'CM',
2848
            'COD' => 'CD',
2849
            'COG' => 'CG',
2850
            'COK' => 'CK',
2851
            'COL' => 'CO',
2852
            'COM' => 'KM',
2853
            'CPV' => 'CV',
2854
            'CRI' => 'CR',
2855
            'CUB' => 'CU',
2856
            'CXR' => 'CX',
2857
            'CYM' => 'KY',
2858
            'CYP' => 'CY',
2859
            'CZE' => 'CZ',
2860
            'DEU' => 'DE',
2861
            'DJI' => 'DJ',
2862
            'DMA' => 'DM',
2863
            'DNK' => 'DK',
2864
            'DOM' => 'DO',
2865
            'DZA' => 'DZ',
2866
            'ECU' => 'EC',
2867
            'EGY' => 'EG',
2868
            'ENG' => 'GB',
2869
            'ERI' => 'ER',
2870
            'ESH' => 'EH',
2871
            'ESP' => 'ES',
2872
            'EST' => 'EE',
2873
            'ETH' => 'ET',
2874
            'FIN' => 'FI',
2875
            'FJI' => 'FJ',
2876
            'FLK' => 'FK',
2877
            'FRA' => 'FR',
2878
            'FRO' => 'FO',
2879
            'FSM' => 'FM',
2880
            'GAB' => 'GA',
2881
            'GEO' => 'GE',
2882
            'GHA' => 'GH',
2883
            'GIB' => 'GI',
2884
            'GIN' => 'GN',
2885
            'GLP' => 'GP',
2886
            'GMB' => 'GM',
2887
            'GNB' => 'GW',
2888
            'GNQ' => 'GQ',
2889
            'GRC' => 'GR',
2890
            'GRD' => 'GD',
2891
            'GRL' => 'GL',
2892
            'GTM' => 'GT',
2893
            'GUF' => 'GF',
2894
            'GUM' => 'GU',
2895
            'GUY' => 'GY',
2896
            'HKG' => 'HK',
2897
            'HMD' => 'HM',
2898
            'HND' => 'HN',
2899
            'HRV' => 'HR',
2900
            'HTI' => 'HT',
2901
            'HUN' => 'HU',
2902
            'IDN' => 'ID',
2903
            'IND' => 'IN',
2904
            'IOT' => 'IO',
2905
            'IRL' => 'IE',
2906
            'IRN' => 'IR',
2907
            'IRQ' => 'IQ',
2908
            'ISL' => 'IS',
2909
            'ISR' => 'IL',
2910
            'ITA' => 'IT',
2911
            'JAM' => 'JM',
2912
            'JOR' => 'JO',
2913
            'JPN' => 'JP',
2914
            'KAZ' => 'KZ',
2915
            'KEN' => 'KE',
2916
            'KGZ' => 'KG',
2917
            'KHM' => 'KH',
2918
            'KIR' => 'KI',
2919
            'KNA' => 'KN',
2920
            'KOR' => 'KO',
2921
            'KWT' => 'KW',
2922
            'LAO' => 'LA',
2923
            'LBN' => 'LB',
2924
            'LBR' => 'LR',
2925
            'LBY' => 'LY',
2926
            'LCA' => 'LC',
2927
            'LIE' => 'LI',
2928
            'LKA' => 'LK',
2929
            'LSO' => 'LS',
2930
            'LTU' => 'LT',
2931
            'LUX' => 'LU',
2932
            'LVA' => 'LV',
2933
            'MAC' => 'MO',
2934
            'MAR' => 'MA',
2935
            'MCO' => 'MC',
2936
            'MDA' => 'MD',
2937
            'MDG' => 'MG',
2938
            'MDV' => 'MV',
2939
            'MEX' => 'MX',
2940
            'MHL' => 'MH',
2941
            'MKD' => 'MK',
2942
            'MLI' => 'ML',
2943
            'MLT' => 'MT',
2944
            'MMR' => 'MM',
2945
            'MNG' => 'MN',
2946
            'MNP' => 'MP',
2947
            'MNT' => 'ME',
2948
            'MOZ' => 'MZ',
2949
            'MRT' => 'MR',
2950
            'MSR' => 'MS',
2951
            'MTQ' => 'MQ',
2952
            'MUS' => 'MU',
2953
            'MWI' => 'MW',
2954
            'MYS' => 'MY',
2955
            'MYT' => 'YT',
2956
            'NAM' => 'NA',
2957
            'NCL' => 'NC',
2958
            'NER' => 'NE',
2959
            'NFK' => 'NF',
2960
            'NGA' => 'NG',
2961
            'NIC' => 'NI',
2962
            'NIR' => 'GB',
2963
            'NIU' => 'NU',
2964
            'NLD' => 'NL',
2965
            'NOR' => 'NO',
2966
            'NPL' => 'NP',
2967
            'NRU' => 'NR',
2968
            'NZL' => 'NZ',
2969
            'OMN' => 'OM',
2970
            'PAK' => 'PK',
2971
            'PAN' => 'PA',
2972
            'PCN' => 'PN',
2973
            'PER' => 'PE',
2974
            'PHL' => 'PH',
2975
            'PLW' => 'PW',
2976
            'PNG' => 'PG',
2977
            'POL' => 'PL',
2978
            'PRI' => 'PR',
2979
            'PRK' => 'KP',
2980
            'PRT' => 'PT',
2981
            'PRY' => 'PY',
2982
            'PSE' => 'PS',
2983
            'PYF' => 'PF',
2984
            'QAT' => 'QA',
2985
            'REU' => 'RE',
2986
            'ROM' => 'RO',
2987
            'RUS' => 'RU',
2988
            'RWA' => 'RW',
2989
            'SAU' => 'SA',
2990
            'SCT' => 'GB',
2991
            'SDN' => 'SD',
2992
            'SEN' => 'SN',
2993
            'SER' => 'RS',
2994
            'SGP' => 'SG',
2995
            'SGS' => 'GS',
2996
            'SHN' => 'SH',
2997
            'SJM' => 'SJ',
2998
            'SLB' => 'SB',
2999
            'SLE' => 'SL',
3000
            'SLV' => 'SV',
3001
            'SMR' => 'SM',
3002
            'SOM' => 'SO',
3003
            'SPM' => 'PM',
3004
            'STP' => 'ST',
3005
            'SUR' => 'SR',
3006
            'SVK' => 'SK',
3007
            'SVN' => 'SI',
3008
            'SWE' => 'SE',
3009
            'SWZ' => 'SZ',
3010
            'SYC' => 'SC',
3011
            'SYR' => 'SY',
3012
            'TCA' => 'TC',
3013
            'TCD' => 'TD',
3014
            'TGO' => 'TG',
3015
            'THA' => 'TH',
3016
            'TJK' => 'TJ',
3017
            'TKL' => 'TK',
3018
            'TKM' => 'TM',
3019
            'TLS' => 'TL',
3020
            'TON' => 'TO',
3021
            'TTO' => 'TT',
3022
            'TUN' => 'TN',
3023
            'TUR' => 'TR',
3024
            'TUV' => 'TV',
3025
            'TWN' => 'TW',
3026
            'TZA' => 'TZ',
3027
            'UGA' => 'UG',
3028
            'UKR' => 'UA',
3029
            'UMI' => 'UM',
3030
            'URY' => 'UY',
3031
            'USA' => 'US',
3032
            'UZB' => 'UZ',
3033
            'VAT' => 'VA',
3034
            'VCT' => 'VC',
3035
            'VEN' => 'VE',
3036
            'VGB' => 'VG',
3037
            'VIR' => 'VI',
3038
            'VNM' => 'VN',
3039
            'VUT' => 'VU',
3040
            'WLF' => 'WF',
3041
            'WLS' => 'GB',
3042
            'WSM' => 'WS',
3043
            'YEM' => 'YE',
3044
            'ZAF' => 'ZA',
3045
            'ZMB' => 'ZM',
3046
            'ZWE' => 'ZW',
3047
        ];
3048
    }
3049
3050
    /**
3051
     * Returns the translated country name based on the given two letter country code.
3052
     */
3053
    private function mapTwoLetterToName(string $twoLetterCode): string
3054
    {
3055
        $threeLetterCode = array_search($twoLetterCode, $this->iso3166(), true);
3056
        $threeLetterCode = $threeLetterCode ?: '???';
3057
3058
        return $this->getAllCountries()[$threeLetterCode];
3059
    }
3060
}
3061