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