Passed
Push — 2.1 ( 6a6343...26eef6 )
by Greg
16:09 queued 07:00
created

EventRepository::countIndividualsWithEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
nc 1
nop 1
dl 0
loc 11
rs 10
c 1
b 0
f 0
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\Statistics\Repository;
21
22
use Fisharebest\Webtrees\Date;
23
use Fisharebest\Webtrees\Elements\UnknownElement;
24
use Fisharebest\Webtrees\Fact;
25
use Fisharebest\Webtrees\Family;
26
use Fisharebest\Webtrees\Gedcom;
27
use Fisharebest\Webtrees\GedcomRecord;
28
use Fisharebest\Webtrees\Header;
29
use Fisharebest\Webtrees\I18N;
30
use Fisharebest\Webtrees\Individual;
31
use Fisharebest\Webtrees\Registry;
32
use Fisharebest\Webtrees\Tree;
33
use Illuminate\Database\Capsule\Manager as DB;
34
use Illuminate\Database\Query\JoinClause;
35
36
use function abs;
37
use function array_map;
38
use function array_merge;
39
use function e;
40
use function strncmp;
41
use function substr;
42
43
class EventRepository
44
{
45
    /**
46
     * Sorting directions.
47
     */
48
    private const SORT_ASC  = 'ASC';
49
    private const SORT_DESC = 'DESC';
50
51
    /**
52
     * Event facts.
53
     */
54
    private const EVENT_BIRTH    = 'BIRT';
55
    private const EVENT_DEATH    = 'DEAT';
56
    private const EVENT_MARRIAGE = 'MARR';
57
    private const EVENT_DIVORCE  = 'DIV';
58
59
    private Tree $tree;
60
61
    /**
62
     * @param Tree $tree
63
     */
64
    public function __construct(Tree $tree)
65
    {
66
        $this->tree = $tree;
67
    }
68
69
    /**
70
     * Returns the total number of a given list of events (with dates).
71
     *
72
     * @param array<string> $events The list of events to count (e.g. BIRT, DEAT, ...)
73
     *
74
     * @return int
75
     */
76
    private function getEventCount(array $events): int
77
    {
78
        $query = DB::table('dates')
79
            ->where('d_file', '=', $this->tree->id());
80
81
        $no_types = [
82
            'HEAD',
83
            'CHAN',
84
        ];
85
86
        if ($events !== []) {
87
            $types = [];
88
89
            foreach ($events as $type) {
90
                if (strncmp($type, '!', 1) === 0) {
91
                    $no_types[] = substr($type, 1);
92
                } else {
93
                    $types[] = $type;
94
                }
95
            }
96
97
            if ($types !== []) {
98
                $query->whereIn('d_fact', $types);
99
            }
100
        }
101
102
        return $query->whereNotIn('d_fact', $no_types)
103
            ->count();
104
    }
105
106
    /**
107
     * @param array<string> $events
108
     *
109
     * @return string
110
     */
111
    public function totalEvents(array $events = []): string
112
    {
113
        return I18N::number($this->getEventCount($events));
114
    }
115
116
    /**
117
     * @return string
118
     */
119
    public function totalEventsBirth(): string
120
    {
121
        return $this->totalEvents(Gedcom::BIRTH_EVENTS);
122
    }
123
124
    /**
125
     * @return string
126
     */
127
    public function totalBirths(): string
128
    {
129
        return I18N::number($this->countIndividualsWithEvents([self::EVENT_BIRTH]));
130
    }
131
132
    /**
133
     * @return string
134
     */
135
    public function totalEventsDeath(): string
136
    {
137
        return $this->totalEvents(Gedcom::DEATH_EVENTS);
138
    }
139
140
    /**
141
     * @return string
142
     */
143
    public function totalDeaths(): string
144
    {
145
        return I18N::number($this->countIndividualsWithEvents([self::EVENT_DEATH]));
146
    }
147
148
    /**
149
     * @return string
150
     */
151
    public function totalEventsMarriage(): string
152
    {
153
        return $this->totalEvents(Gedcom::MARRIAGE_EVENTS);
154
    }
155
156
    /**
157
     * @return string
158
     */
159
    public function totalMarriages(): string
160
    {
161
        return I18N::number($this->countFamiliesWithEvents([self::EVENT_MARRIAGE]));
162
    }
163
164
    /**
165
     * @return string
166
     */
167
    public function totalEventsDivorce(): string
168
    {
169
        return $this->totalEvents(Gedcom::DIVORCE_EVENTS);
170
    }
171
172
    /**
173
     * @return string
174
     */
175
    public function totalDivorces(): string
176
    {
177
        return I18N::number($this->countFamiliesWithEvents([self::EVENT_DIVORCE]));
178
    }
179
180
    /**
181
     * Returns the list of common facts used query the data.
182
     *
183
     * @return array<string>
184
     */
185
    private function getCommonFacts(): array
186
    {
187
        // The list of facts used to limit the query result
188
        return array_merge(
189
            Gedcom::BIRTH_EVENTS,
190
            Gedcom::MARRIAGE_EVENTS,
191
            Gedcom::DIVORCE_EVENTS,
192
            Gedcom::DEATH_EVENTS
193
        );
194
    }
195
196
    /**
197
     * @return string
198
     */
199
    public function totalEventsOther(): string
200
    {
201
        $no_facts = array_map(
202
            static function (string $fact): string {
203
                return '!' . $fact;
204
            },
205
            $this->getCommonFacts()
206
        );
207
208
        return $this->totalEvents($no_facts);
209
    }
210
211
    /**
212
     * Returns the first/last event record from the given list of event facts.
213
     *
214
     * @param string $direction The sorting direction of the query (To return first or last record)
215
     *
216
     * @return object{id:string,year:int,fact:string,type:string}|null
217
     */
218
    private function eventQuery(string $direction): ?object
219
    {
220
        return DB::table('dates')
221
            ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type'])
222
            ->where('d_file', '=', $this->tree->id())
223
            ->where('d_gid', '<>', Header::RECORD_TYPE)
224
            ->whereIn('d_fact', $this->getCommonFacts())
225
            ->where('d_julianday1', '<>', 0)
226
            ->orderBy('d_julianday1', $direction)
227
            ->orderBy('d_type')
228
            ->limit(1)
229
            ->get()
230
            ->map(static fn (object $row): object => (object) [
231
                'id'   => $row->id,
232
                'year' => (int) $row->year,
233
                'fact' => $row->fact,
234
                'type' => $row->type,
235
            ])
236
            ->first();
237
    }
238
239
    /**
240
     * Returns the formatted first/last occurring event.
241
     *
242
     * @param string $direction The sorting direction
243
     *
244
     * @return string
245
     */
246
    private function getFirstLastEvent(string $direction): string
247
    {
248
        $row    = $this->eventQuery($direction);
249
        $result = I18N::translate('This information is not available.');
250
251
        if ($row !== null) {
252
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
253
254
            if ($record instanceof GedcomRecord && $record->canShow()) {
255
                $result = $record->formatList();
256
            } else {
257
                $result = I18N::translate('This information is private and cannot be shown.');
258
            }
259
        }
260
261
        return $result;
262
    }
263
264
    /**
265
     * @return string
266
     */
267
    public function firstEvent(): string
268
    {
269
        return $this->getFirstLastEvent(self::SORT_ASC);
270
    }
271
272
    /**
273
     * @return string
274
     */
275
    public function lastEvent(): string
276
    {
277
        return $this->getFirstLastEvent(self::SORT_DESC);
278
    }
279
280
    /**
281
     * Returns the formatted year of the first/last occurring event.
282
     *
283
     * @param string $direction The sorting direction
284
     *
285
     * @return string
286
     */
287
    private function getFirstLastEventYear(string $direction): string
288
    {
289
        $row = $this->eventQuery($direction);
290
291
        if ($row === null) {
292
            return '';
293
        }
294
295
        if ($row->year < 0) {
296
            $row->year = abs($row->year) . ' B.C.';
297
        }
298
299
        return (new Date($row->type . ' ' . $row->year))
300
            ->display();
301
    }
302
303
    /**
304
     * @return string
305
     */
306
    public function firstEventYear(): string
307
    {
308
        return $this->getFirstLastEventYear(self::SORT_ASC);
309
    }
310
311
    /**
312
     * @return string
313
     */
314
    public function lastEventYear(): string
315
    {
316
        return $this->getFirstLastEventYear(self::SORT_DESC);
317
    }
318
319
    /**
320
     * Returns the formatted type of the first/last occurring event.
321
     *
322
     * @param string $direction The sorting direction
323
     *
324
     * @return string
325
     */
326
    private function getFirstLastEventType(string $direction): string
327
    {
328
        $row = $this->eventQuery($direction);
329
330
        if ($row === null) {
331
            return '';
332
        }
333
334
        foreach ([Individual::RECORD_TYPE, Family::RECORD_TYPE] as $record_type) {
335
            $element = Registry::elementFactory()->make($record_type . ':' . $row->fact);
336
337
            if (!$element instanceof UnknownElement) {
338
                return $element->label();
339
            }
340
        }
341
342
        return $row->fact;
343
    }
344
345
    /**
346
     * @return string
347
     */
348
    public function firstEventType(): string
349
    {
350
        return $this->getFirstLastEventType(self::SORT_ASC);
351
    }
352
353
    /**
354
     * @return string
355
     */
356
    public function lastEventType(): string
357
    {
358
        return $this->getFirstLastEventType(self::SORT_DESC);
359
    }
360
361
    /**
362
     * Returns the formatted name of the first/last occurring event.
363
     *
364
     * @param string $direction The sorting direction
365
     *
366
     * @return string
367
     */
368
    private function getFirstLastEventName(string $direction): string
369
    {
370
        $row = $this->eventQuery($direction);
371
372
        if ($row !== null) {
373
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
374
375
            if ($record instanceof GedcomRecord) {
376
                return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>';
377
            }
378
        }
379
380
        return '';
381
    }
382
383
    /**
384
     * @return string
385
     */
386
    public function firstEventName(): string
387
    {
388
        return $this->getFirstLastEventName(self::SORT_ASC);
389
    }
390
391
    /**
392
     * @return string
393
     */
394
    public function lastEventName(): string
395
    {
396
        return $this->getFirstLastEventName(self::SORT_DESC);
397
    }
398
399
    /**
400
     * Returns the formatted place of the first/last occurring event.
401
     *
402
     * @param string $direction The sorting direction
403
     *
404
     * @return string
405
     */
406
    private function getFirstLastEventPlace(string $direction): string
407
    {
408
        $row = $this->eventQuery($direction);
409
410
        if ($row !== null) {
411
            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
412
            $fact   = null;
413
414
            if ($record instanceof GedcomRecord) {
415
                $fact = $record->facts([$row->fact])->first();
416
            }
417
418
            if ($fact instanceof Fact) {
419
                return $fact->place()->shortName();
420
            }
421
        }
422
423
        return I18N::translate('Private');
424
    }
425
426
    /**
427
     * @return string
428
     */
429
    public function firstEventPlace(): string
430
    {
431
        return $this->getFirstLastEventPlace(self::SORT_ASC);
432
    }
433
434
    /**
435
     * @return string
436
     */
437
    public function lastEventPlace(): string
438
    {
439
        return $this->getFirstLastEventPlace(self::SORT_DESC);
440
    }
441
442
    /**
443
     * @param array<string> $events
444
     */
445
    private function countFamiliesWithEvents(array $events): int
446
    {
447
        return DB::table('dates')
448
            ->join('families', static function (JoinClause $join): void {
449
                $join
450
                    ->on('f_id', '=', 'd_gid')
451
                    ->on('f_file', '=', 'd_file');
452
            })
453
            ->where('d_file', '=', $this->tree->id())
454
            ->whereIn('d_fact', $events)
455
            ->count();
456
    }
457
458
    /**
459
     * @param array<string> $events
460
     */
461
    private function countIndividualsWithEvents(array $events): int
462
    {
463
        return DB::table('dates')
464
            ->join('individuals', static function (JoinClause $join): void {
465
                $join
466
                    ->on('i_id', '=', 'd_gid')
467
                    ->on('i_file', '=', 'd_file');
468
            })
469
            ->where('d_file', '=', $this->tree->id())
470
            ->whereIn('d_fact', $events)
471
            ->count();
472
    }
473
}
474