xref: /webtrees/app/Statistics/Repository/FamilyDatesRepository.php (revision 7dcc0dd1f98a779ae8ed3c9c0b53d14ead31a778)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2023 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Statistics\Repository;
21
22use Fisharebest\Webtrees\Date;
23use Fisharebest\Webtrees\DB;
24use Fisharebest\Webtrees\Fact;
25use Fisharebest\Webtrees\GedcomRecord;
26use Fisharebest\Webtrees\I18N;
27use Fisharebest\Webtrees\Registry;
28use Fisharebest\Webtrees\Statistics\Repository\Interfaces\FamilyDatesRepositoryInterface;
29use Fisharebest\Webtrees\Tree;
30use Illuminate\Database\Query\Builder;
31
32use function abs;
33use function e;
34
35/**
36 * A repository providing methods for family dates related statistics (birth, death, marriage, divorce).
37 */
38class FamilyDatesRepository implements FamilyDatesRepositoryInterface
39{
40    /**
41     * Sorting directions.
42     */
43    private const SORT_MIN = 'MIN';
44    private const SORT_MAX = 'MAX';
45
46    /**
47     * Event facts.
48     */
49    private const EVENT_BIRTH    = 'BIRT';
50    private const EVENT_DEATH    = 'DEAT';
51    private const EVENT_MARRIAGE = 'MARR';
52    private const EVENT_DIVORCE  = 'DIV';
53
54    private Tree $tree;
55
56    /**
57     * @param Tree $tree
58     */
59    public function __construct(Tree $tree)
60    {
61        $this->tree = $tree;
62    }
63
64    /**
65     * Returns the first/last event record for the given event fact.
66     *
67     * @param string $fact
68     * @param string $operation
69     *
70     * @return object{id:string,year:int,fact:string,type:string}|null
71     */
72    private function eventQuery(string $fact, string $operation): ?object
73    {
74        return DB::table('dates')
75            ->select(['d_gid as id', 'd_year as year', 'd_fact AS fact', 'd_type AS type'])
76            ->where('d_file', '=', $this->tree->id())
77            ->where('d_fact', '=', $fact)
78            ->where('d_julianday1', '=', function (Builder $query) use ($operation, $fact): void {
79                $query->selectRaw($operation . '(d_julianday1)')
80                    ->from('dates')
81                    ->where('d_file', '=', $this->tree->id())
82                    ->where('d_fact', '=', $fact)
83                    ->where('d_julianday1', '<>', 0);
84            })
85            ->limit(1)
86            ->get()
87            ->map(static fn (object $row): object => (object) [
88                'id'   => $row->id,
89                'year' => (int) $row->year,
90                'fact' => $row->fact,
91                'type' => $row->type,
92            ])
93            ->first();
94    }
95
96    /**
97     * Returns the formatted year of the first/last occurring event.
98     *
99     * @param string $type      The fact to query
100     * @param string $operation The sorting operation
101     *
102     * @return string
103     */
104    private function getFirstLastEvent(string $type, string $operation): string
105    {
106        $row    = $this->eventQuery($type, $operation);
107        $result = I18N::translate('This information is not available.');
108
109        if ($row !== null) {
110            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
111
112            if ($record instanceof GedcomRecord && $record->canShow()) {
113                $result = $record->formatList();
114            } else {
115                $result = I18N::translate('This information is private and cannot be shown.');
116            }
117        }
118
119        return $result;
120    }
121
122    /**
123     * @return string
124     */
125    public function firstBirth(): string
126    {
127        return $this->getFirstLastEvent(self::EVENT_BIRTH, self::SORT_MIN);
128    }
129
130    /**
131     * @return string
132     */
133    public function lastBirth(): string
134    {
135        return $this->getFirstLastEvent(self::EVENT_BIRTH, self::SORT_MAX);
136    }
137
138    /**
139     * @return string
140     */
141    public function firstDeath(): string
142    {
143        return $this->getFirstLastEvent(self::EVENT_DEATH, self::SORT_MIN);
144    }
145
146    /**
147     * @return string
148     */
149    public function lastDeath(): string
150    {
151        return $this->getFirstLastEvent(self::EVENT_DEATH, self::SORT_MAX);
152    }
153
154    /**
155     * @return string
156     */
157    public function firstMarriage(): string
158    {
159        return $this->getFirstLastEvent(self::EVENT_MARRIAGE, self::SORT_MIN);
160    }
161
162    /**
163     * @return string
164     */
165    public function lastMarriage(): string
166    {
167        return $this->getFirstLastEvent(self::EVENT_MARRIAGE, self::SORT_MAX);
168    }
169
170    /**
171     * @return string
172     */
173    public function firstDivorce(): string
174    {
175        return $this->getFirstLastEvent(self::EVENT_DIVORCE, self::SORT_MIN);
176    }
177
178    /**
179     * @return string
180     */
181    public function lastDivorce(): string
182    {
183        return $this->getFirstLastEvent(self::EVENT_DIVORCE, self::SORT_MAX);
184    }
185
186    /**
187     * Returns the formatted year of the first/last occurring event.
188     *
189     * @param string $type      The fact to query
190     * @param string $operation The sorting operation
191     *
192     * @return string
193     */
194    private function getFirstLastEventYear(string $type, string $operation): string
195    {
196        $row = $this->eventQuery($type, $operation);
197
198        if ($row === null) {
199            return '';
200        }
201
202        if ($row->year < 0) {
203            $row->year = abs($row->year) . ' B.C.';
204        }
205
206        return (new Date($row->type . ' ' . $row->year))
207            ->display();
208    }
209
210    /**
211     * @return string
212     */
213    public function firstBirthYear(): string
214    {
215        return $this->getFirstLastEventYear(self::EVENT_BIRTH, self::SORT_MIN);
216    }
217
218    /**
219     * @return string
220     */
221    public function lastBirthYear(): string
222    {
223        return $this->getFirstLastEventYear(self::EVENT_BIRTH, self::SORT_MAX);
224    }
225
226    /**
227     * @return string
228     */
229    public function firstDeathYear(): string
230    {
231        return $this->getFirstLastEventYear(self::EVENT_DEATH, self::SORT_MIN);
232    }
233
234    /**
235     * @return string
236     */
237    public function lastDeathYear(): string
238    {
239        return $this->getFirstLastEventYear(self::EVENT_DEATH, self::SORT_MAX);
240    }
241
242    /**
243     * @return string
244     */
245    public function firstMarriageYear(): string
246    {
247        return $this->getFirstLastEventYear(self::EVENT_MARRIAGE, self::SORT_MIN);
248    }
249
250    /**
251     * @return string
252     */
253    public function lastMarriageYear(): string
254    {
255        return $this->getFirstLastEventYear(self::EVENT_MARRIAGE, self::SORT_MAX);
256    }
257
258    /**
259     * @return string
260     */
261    public function firstDivorceYear(): string
262    {
263        return $this->getFirstLastEventYear(self::EVENT_DIVORCE, self::SORT_MIN);
264    }
265
266    /**
267     * @return string
268     */
269    public function lastDivorceYear(): string
270    {
271        return $this->getFirstLastEventYear(self::EVENT_DIVORCE, self::SORT_MAX);
272    }
273
274    /**
275     * Returns the formatted name of the first/last occurring event.
276     *
277     * @param string $type      The fact to query
278     * @param string $operation The sorting operation
279     *
280     * @return string
281     */
282    private function getFirstLastEventName(string $type, string $operation): string
283    {
284        $row = $this->eventQuery($type, $operation);
285
286        if ($row !== null) {
287            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
288
289            if ($record instanceof GedcomRecord) {
290                return '<a href="' . e($record->url()) . '">' . $record->fullName() . '</a>';
291            }
292        }
293
294        return '';
295    }
296
297    /**
298     * @return string
299     */
300    public function firstBirthName(): string
301    {
302        return $this->getFirstLastEventName(self::EVENT_BIRTH, self::SORT_MIN);
303    }
304
305    /**
306     * @return string
307     */
308    public function lastBirthName(): string
309    {
310        return $this->getFirstLastEventName(self::EVENT_BIRTH, self::SORT_MAX);
311    }
312
313    /**
314     * @return string
315     */
316    public function firstDeathName(): string
317    {
318        return $this->getFirstLastEventName(self::EVENT_DEATH, self::SORT_MIN);
319    }
320
321    /**
322     * @return string
323     */
324    public function lastDeathName(): string
325    {
326        return $this->getFirstLastEventName(self::EVENT_DEATH, self::SORT_MAX);
327    }
328
329    /**
330     * @return string
331     */
332    public function firstMarriageName(): string
333    {
334        return $this->getFirstLastEventName(self::EVENT_MARRIAGE, self::SORT_MIN);
335    }
336
337    /**
338     * @return string
339     */
340    public function lastMarriageName(): string
341    {
342        return $this->getFirstLastEventName(self::EVENT_MARRIAGE, self::SORT_MAX);
343    }
344
345    /**
346     * @return string
347     */
348    public function firstDivorceName(): string
349    {
350        return $this->getFirstLastEventName(self::EVENT_DIVORCE, self::SORT_MIN);
351    }
352
353    /**
354     * @return string
355     */
356    public function lastDivorceName(): string
357    {
358        return $this->getFirstLastEventName(self::EVENT_DIVORCE, self::SORT_MAX);
359    }
360
361    /**
362     * Returns the formatted place of the first/last occurring event.
363     *
364     * @param string $type      The fact to query
365     * @param string $operation The sorting operation
366     *
367     * @return string
368     */
369    private function getFirstLastEventPlace(string $type, string $operation): string
370    {
371        $row = $this->eventQuery($type, $operation);
372
373        if ($row != null) {
374            $record = Registry::gedcomRecordFactory()->make($row->id, $this->tree);
375            $fact   = null;
376
377            if ($record instanceof GedcomRecord) {
378                $fact = $record->facts([$row->fact])->first();
379            }
380
381            if ($fact instanceof Fact) {
382                return $fact->place()->shortName();
383            }
384        }
385
386        return I18N::translate('This information is private and cannot be shown.');
387    }
388
389    /**
390     * @return string
391     */
392    public function firstBirthPlace(): string
393    {
394        return $this->getFirstLastEventPlace(self::EVENT_BIRTH, self::SORT_MIN);
395    }
396
397    /**
398     * @return string
399     */
400    public function lastBirthPlace(): string
401    {
402        return $this->getFirstLastEventPlace(self::EVENT_BIRTH, self::SORT_MAX);
403    }
404
405    /**
406     * @return string
407     */
408    public function firstDeathPlace(): string
409    {
410        return $this->getFirstLastEventPlace(self::EVENT_DEATH, self::SORT_MIN);
411    }
412
413    /**
414     * @return string
415     */
416    public function lastDeathPlace(): string
417    {
418        return $this->getFirstLastEventPlace(self::EVENT_DEATH, self::SORT_MAX);
419    }
420
421    /**
422     * @return string
423     */
424    public function firstMarriagePlace(): string
425    {
426        return $this->getFirstLastEventPlace(self::EVENT_MARRIAGE, self::SORT_MIN);
427    }
428
429    /**
430     * @return string
431     */
432    public function lastMarriagePlace(): string
433    {
434        return $this->getFirstLastEventPlace(self::EVENT_MARRIAGE, self::SORT_MAX);
435    }
436
437    /**
438     * @return string
439     */
440    public function firstDivorcePlace(): string
441    {
442        return $this->getFirstLastEventPlace(self::EVENT_DIVORCE, self::SORT_MIN);
443    }
444
445    /**
446     * @return string
447     */
448    public function lastDivorcePlace(): string
449    {
450        return $this->getFirstLastEventPlace(self::EVENT_DIVORCE, self::SORT_MAX);
451    }
452}
453