xref: /webtrees/app/Relationship.php (revision 6fcafd028e511367c3fcdbfaa31aa05a85bf85c0)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees;
21
22use Closure;
23
24use function abs;
25use function array_slice;
26use function count;
27use function in_array;
28use function intdiv;
29use function min;
30
31/**
32 * Class Relationship - define a relationship for a language.
33 */
34class Relationship
35{
36    // The basic components of a relationship.
37    // These strings are needed for compatibility with the legacy algorithm.
38    // Once that has been replaced, it may be more efficient to use integers here.
39    public const SISTER   = 'sis';
40    public const BROTHER  = 'bro';
41    public const SIBLING  = 'sib';
42    public const MOTHER   = 'mot';
43    public const FATHER   = 'fat';
44    public const PARENT   = 'par';
45    public const DAUGHTER = 'dau';
46    public const SON      = 'son';
47    public const CHILD    = 'chi';
48    public const WIFE     = 'wif';
49    public const HUSBAND  = 'hus';
50    public const SPOUSE   = 'spo';
51
52    public const SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING];
53    public const PARENTS  = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT];
54    public const CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD];
55    public const SPOUSES  = ['F' => self::WIFE, 'M' => self::HUSBAND, 'U' => self::SPOUSE];
56
57    // Generates a name from the matched relationship.
58    private Closure $callback;
59
60    // List of rules that need to match.
61    private array $matchers;
62
63    /**
64     * Relationship constructor.
65     *
66     * @param Closure $callback
67     */
68    private function __construct(Closure $callback)
69    {
70        $this->callback = $callback;
71        $this->matchers = [];
72    }
73
74    /**
75     * Allow fluent constructor.
76     *
77     * @param string $nominative
78     * @param string $genitive
79     *
80     * @return Relationship
81     */
82    public static function fixed(string $nominative, string $genitive): Relationship
83    {
84        return new static(fn () => [$nominative, $genitive]);
85    }
86
87    /**
88     * Allow fluent constructor.
89     *
90     * @param Closure $callback
91     *
92     * @return Relationship
93     */
94    public static function dynamic(Closure $callback): Relationship
95    {
96        return new static($callback);
97    }
98
99    /**
100     * Does this relationship match the pattern?
101     *
102     * @param array<Individual|Family> $nodes
103     * @param array<string>            $patterns
104     *
105     * @return array [nominative, genitive] or null
106     */
107    public function match(array $nodes, array $patterns): ?array
108    {
109        $captures = [];
110
111        foreach ($this->matchers as $matcher) {
112            if (!$matcher($nodes, $patterns, $captures)) {
113                return null;
114            }
115        }
116
117        if ($patterns === []) {
118            return ($this->callback)(...$captures);
119        }
120
121        return null;
122    }
123
124    /**
125     * @return Relationship
126     */
127    public function adopted(): Relationship
128    {
129        $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
130                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
131                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted');
132
133        return $this;
134    }
135
136    /**
137     * @return Relationship
138     */
139    public function adoptive(): Relationship
140    {
141        $this->matchers[] = fn (array $nodes): bool => $nodes[0]
142            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
143            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted');
144
145        return $this;
146    }
147
148    /**
149     * @return Relationship
150     */
151    public function brother(): Relationship
152    {
153        return $this->relation([self::BROTHER]);
154    }
155
156    /**
157     * Match the next relationship in the path.
158     *
159     * @param array<string> $relationships
160     *
161     * @return Relationship
162     */
163    protected function relation(array $relationships): Relationship
164    {
165        $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool {
166            if (in_array($patterns[0] ?? '', $relationships, true)) {
167                $nodes    = array_slice($nodes, 2);
168                $patterns = array_slice($patterns, 1);
169
170                return true;
171            }
172
173            return false;
174        };
175
176        return $this;
177    }
178
179    /**
180     * The number of ancestors may be different to the number of descendants
181     *
182     * @return Relationship
183     */
184    public function asymmetricCousin(): Relationship
185    {
186        return $this->ancestor()->sibling()->descendant();
187    }
188
189    /**
190     * @return Relationship
191     */
192    public function descendant(): Relationship
193    {
194        return $this->repeatedRelationship(self::CHILDREN);
195    }
196
197    /**
198     * Match a repeated number of the same type of component
199     *
200     * @param array<string> $relationships
201     *
202     * @return Relationship
203     */
204    protected function repeatedRelationship(array $relationships): Relationship
205    {
206        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool {
207            $limit = min(intdiv(count($nodes), 2), count($patterns));
208
209            for ($generations = 0; $generations < $limit; ++$generations) {
210                if (!in_array($patterns[$generations], $relationships, true)) {
211                    break;
212                }
213            }
214
215            if ($generations > 0) {
216                $nodes      = array_slice($nodes, 2 * $generations);
217                $patterns   = array_slice($patterns, $generations);
218                $captures[] = $generations;
219
220                return true;
221            }
222
223            return false;
224        };
225
226        return $this;
227    }
228
229    /**
230     * @return Relationship
231     */
232    public function sibling(): Relationship
233    {
234        return $this->relation(self::SIBLINGS);
235    }
236
237    /**
238     * @return Relationship
239     */
240    public function ancestor(): Relationship
241    {
242        return $this->repeatedRelationship(self::PARENTS);
243    }
244
245    /**
246     * @return Relationship
247     */
248    public function child(): Relationship
249    {
250        return $this->relation(self::CHILDREN);
251    }
252
253    /**
254     * @return Relationship
255     */
256    public function daughter(): Relationship
257    {
258        return $this->relation([self::DAUGHTER]);
259    }
260
261    /**
262     * @return Relationship
263     */
264    public function divorced(): Relationship
265    {
266        return $this->marriageStatus('DIV');
267    }
268
269    /**
270     * Match a marriage status
271     *
272     * @param string $status
273     *
274     * @return Relationship
275     */
276    protected function marriageStatus(string $status): Relationship
277    {
278        $this->matchers[] = static function (array $nodes) use ($status): bool {
279            $family = $nodes[1] ?? null;
280
281            if ($family instanceof Family) {
282                $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last();
283
284                if ($fact instanceof Fact) {
285                    switch ($status) {
286                        case 'MARR':
287                            return $fact->tag() === 'FAM:MARR';
288
289                        case 'DIV':
290                            return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL';
291
292                        case 'ENGA':
293                            return $fact->tag() === 'FAM:ENGA';
294                    }
295                }
296            }
297
298            return false;
299        };
300
301        return $this;
302    }
303
304    /**
305     * @return Relationship
306     */
307    public function engaged(): Relationship
308    {
309        return $this->marriageStatus('ENGA');
310    }
311
312    /**
313     * @return Relationship
314     */
315    public function father(): Relationship
316    {
317        return $this->relation([self::FATHER]);
318    }
319
320    /**
321     * @return Relationship
322     */
323    public function female(): Relationship
324    {
325        return $this->sex('F');
326    }
327
328    /**
329     * Match the sex of the current individual
330     *
331     * @param string $sex
332     *
333     * @return Relationship
334     */
335    protected function sex(string $sex): Relationship
336    {
337        $this->matchers[] = static function (array $nodes) use ($sex): bool {
338            return $nodes[0]->sex() === $sex;
339        };
340
341        return $this;
342    }
343
344    /**
345     * @return Relationship
346     */
347    public function fostered(): Relationship
348    {
349        $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
350                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
351                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster');
352
353        return $this;
354    }
355
356    /**
357     * @return Relationship
358     */
359    public function fostering(): Relationship
360    {
361        $this->matchers[] = fn (array $nodes): bool => $nodes[0]
362            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
363            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster');
364
365        return $this;
366    }
367
368    /**
369     * @return Relationship
370     */
371    public function husband(): Relationship
372    {
373        return $this->married()->relation([self::HUSBAND]);
374    }
375
376    /**
377     * @return Relationship
378     */
379    public function married(): Relationship
380    {
381        return $this->marriageStatus('MARR');
382    }
383
384    /**
385     * @return Relationship
386     */
387    public function male(): Relationship
388    {
389        return $this->sex('M');
390    }
391
392    /**
393     * @return Relationship
394     */
395    public function mother(): Relationship
396    {
397        return $this->relation([self::MOTHER]);
398    }
399
400    /**
401     * @return Relationship
402     */
403    public function older(): Relationship
404    {
405        $this->matchers[] = static function (array $nodes): bool {
406            if (count($nodes) > 2) {
407                $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
408                $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
409
410                return Date::compare($date1, $date2) < 0;
411            }
412
413            return false;
414        };
415
416        return $this;
417    }
418
419    /**
420     * @return Relationship
421     */
422    public function parent(): Relationship
423    {
424        return $this->relation(self::PARENTS);
425    }
426
427    /**
428     * @return Relationship
429     */
430    public function sister(): Relationship
431    {
432        return $this->relation([self::SISTER]);
433    }
434
435    /**
436     * @return Relationship
437     */
438    public function son(): Relationship
439    {
440        return $this->relation([self::SON]);
441    }
442
443    /**
444     * @return Relationship
445     */
446    public function spouse(): Relationship
447    {
448        return $this->married()->partner();
449    }
450
451    /**
452     * @return Relationship
453     */
454    public function partner(): Relationship
455    {
456        return $this->relation(self::SPOUSES);
457    }
458
459    /**
460     * The number of ancestors must be the same as the number of descendants
461     *
462     * @return Relationship
463     */
464    public function symmetricCousin(): Relationship
465    {
466        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool {
467            $count = count($patterns);
468
469            $n = 0;
470
471            // Ancestors
472            while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) {
473                $n++;
474            }
475
476            // No ancestors?  Not enough path left for descendants?
477            if ($n === 0 || $n * 2 + 1 !== $count) {
478                return false;
479            }
480
481            // Siblings
482            if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) {
483                return false;
484            }
485
486            // Descendants
487            for ($descendants = $n + 1; $descendants < $count; ++$descendants) {
488                if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) {
489                    return false;
490                }
491            }
492
493
494            $nodes      = array_slice($nodes, 2 * $n);
495            $patterns   = [];
496            $captures[] = $n;
497
498            return true;
499        };
500
501        return $this;
502    }
503
504    /**
505     * @return Relationship
506     */
507    public function twin(): Relationship
508    {
509        $this->matchers[] = static function (array $nodes): bool {
510            if (count($nodes) > 2) {
511                $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
512                $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
513
514                return
515                    $date1->isOK() &&
516                    $date2->isOK() &&
517                    abs($date1->julianDay() - $date2->julianDay()) < 2 &&
518                    $date1->minimumDate()->day > 0 &&
519                    $date2->minimumDate()->day > 0;
520            }
521
522            return false;
523        };
524
525        return $this;
526    }
527
528    /**
529     * @return Relationship
530     */
531    public function wife(): Relationship
532    {
533        return $this->married()->relation([self::WIFE]);
534    }
535
536    /**
537     * @return Relationship
538     */
539    public function younger(): Relationship
540    {
541        $this->matchers[] = static function (array $nodes): bool {
542            if (count($nodes) > 2) {
543                $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
544                $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
545
546                return Date::compare($date1, $date2) > 0;
547            }
548
549            return false;
550        };
551
552        return $this;
553    }
554}
555