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