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