xref: /webtrees/app/Relationship.php (revision e873f434551745f888937263ff89e80db3b0f785)
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 string SISTER   = 'sis';
41    public const string BROTHER  = 'bro';
42    public const string SIBLING  = 'sib';
43    public const string MOTHER   = 'mot';
44    public const string FATHER   = 'fat';
45    public const string PARENT   = 'par';
46    public const string DAUGHTER = 'dau';
47    public const string SON      = 'son';
48    public const string CHILD    = 'chi';
49    public const string WIFE     = 'wif';
50    public const string HUSBAND  = 'hus';
51    public const string SPOUSE   = 'spo';
52
53    public const array SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING];
54    public const array PARENTS  = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT];
55    public const array CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD];
56    public const array 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|null
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 fn (array $nodes): bool => $nodes[0]->sex() === $sex;
337
338        return $this;
339    }
340
341    /**
342     * @return Relationship
343     */
344    public function fostered(): Relationship
345    {
346        $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
347                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
348                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER);
349
350        return $this;
351    }
352
353    /**
354     * @return Relationship
355     */
356    public function fostering(): Relationship
357    {
358        $this->matchers[] = static fn (array $nodes): bool => $nodes[0]
359            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
360            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER);
361
362        return $this;
363    }
364
365    /**
366     * @return Relationship
367     */
368    public function husband(): Relationship
369    {
370        return $this->married()->relation([self::HUSBAND]);
371    }
372
373    /**
374     * @return Relationship
375     */
376    public function married(): Relationship
377    {
378        return $this->marriageStatus('MARR');
379    }
380
381    /**
382     * @return Relationship
383     */
384    public function male(): Relationship
385    {
386        return $this->sex('M');
387    }
388
389    /**
390     * @return Relationship
391     */
392    public function mother(): Relationship
393    {
394        return $this->relation([self::MOTHER]);
395    }
396
397    /**
398     * @return Relationship
399     */
400    public function older(): Relationship
401    {
402        $this->matchers[] = static function (array $nodes): bool {
403            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
404            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
405
406            return Date::compare($date1, $date2) > 0;
407        };
408
409        return $this;
410    }
411
412    /**
413     * @return Relationship
414     */
415    public function parent(): Relationship
416    {
417        return $this->relation(self::PARENTS);
418    }
419
420    /**
421     * @return Relationship
422     */
423    public function sister(): Relationship
424    {
425        return $this->relation([self::SISTER]);
426    }
427
428    /**
429     * @return Relationship
430     */
431    public function son(): Relationship
432    {
433        return $this->relation([self::SON]);
434    }
435
436    /**
437     * @return Relationship
438     */
439    public function spouse(): Relationship
440    {
441        return $this->married()->partner();
442    }
443
444    /**
445     * @return Relationship
446     */
447    public function partner(): Relationship
448    {
449        return $this->relation(self::SPOUSES);
450    }
451
452    /**
453     * The number of ancestors must be the same as the number of descendants
454     *
455     * @return Relationship
456     */
457    public function symmetricCousin(): Relationship
458    {
459        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool {
460            $count = count($patterns);
461
462            $n = 0;
463
464            // Ancestors
465            while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) {
466                $n++;
467            }
468
469            // No ancestors?  Not enough path left for descendants?
470            if ($n === 0 || $n * 2 + 1 !== $count) {
471                return false;
472            }
473
474            // Siblings
475            if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) {
476                return false;
477            }
478
479            // Descendants
480            for ($descendants = $n + 1; $descendants < $count; ++$descendants) {
481                if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) {
482                    return false;
483                }
484            }
485
486            $nodes      = array_slice($nodes, 2 * (2 * $n + 1));
487            $patterns   = [];
488            $captures[] = $n;
489
490            return true;
491        };
492
493        return $this;
494    }
495
496    /**
497     * @return Relationship
498     */
499    public function twin(): Relationship
500    {
501        $this->matchers[] = static function (array $nodes): bool {
502            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
503            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
504
505            return
506                $date1->isOK() &&
507                $date2->isOK() &&
508                abs($date1->julianDay() - $date2->julianDay()) < 2 &&
509                $date1->minimumDate()->day > 0 &&
510                $date2->minimumDate()->day > 0;
511        };
512
513        return $this;
514    }
515
516    /**
517     * @return Relationship
518     */
519    public function wife(): Relationship
520    {
521        return $this->married()->relation([self::WIFE]);
522    }
523
524    /**
525     * @return Relationship
526     */
527    public function younger(): Relationship
528    {
529        $this->matchers[] = static function (array $nodes): bool {
530            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
531            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
532
533            return Date::compare($date1, $date2) < 0;
534        };
535
536        return $this;
537    }
538}
539