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