xref: /webtrees/app/Relationship.php (revision 88a035604b5083c54cbfbe51294b65fe8cf9a5ed)
16fcafd02SGreg Roach<?php
26fcafd02SGreg Roach
36fcafd02SGreg Roach/**
46fcafd02SGreg Roach * webtrees: online genealogy
55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team
66fcafd02SGreg Roach * This program is free software: you can redistribute it and/or modify
76fcafd02SGreg Roach * it under the terms of the GNU General Public License as published by
86fcafd02SGreg Roach * the Free Software Foundation, either version 3 of the License, or
96fcafd02SGreg Roach * (at your option) any later version.
106fcafd02SGreg Roach * This program is distributed in the hope that it will be useful,
116fcafd02SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
126fcafd02SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
136fcafd02SGreg Roach * GNU General Public License for more details.
146fcafd02SGreg Roach * You should have received a copy of the GNU General Public License
156fcafd02SGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
166fcafd02SGreg Roach */
176fcafd02SGreg Roach
186fcafd02SGreg Roachdeclare(strict_types=1);
196fcafd02SGreg Roach
206fcafd02SGreg Roachnamespace Fisharebest\Webtrees;
216fcafd02SGreg Roach
226fcafd02SGreg Roachuse Closure;
23665e281aSGreg Roachuse Fisharebest\Webtrees\Elements\PedigreeLinkageType;
24665e281aSGreg Roach
256fcafd02SGreg Roachuse function abs;
266fcafd02SGreg Roachuse function array_slice;
276fcafd02SGreg Roachuse function count;
286fcafd02SGreg Roachuse function in_array;
296fcafd02SGreg Roachuse function intdiv;
306fcafd02SGreg Roachuse function min;
317573bc3dSGreg Roachuse function var_dump;
326fcafd02SGreg Roach
336fcafd02SGreg Roach/**
346fcafd02SGreg Roach * Class Relationship - define a relationship for a language.
356fcafd02SGreg Roach */
366fcafd02SGreg Roachclass Relationship
376fcafd02SGreg Roach{
386fcafd02SGreg Roach    // The basic components of a relationship.
396fcafd02SGreg Roach    // These strings are needed for compatibility with the legacy algorithm.
406fcafd02SGreg Roach    // Once that has been replaced, it may be more efficient to use integers here.
416fcafd02SGreg Roach    public const SISTER   = 'sis';
426fcafd02SGreg Roach    public const BROTHER  = 'bro';
436fcafd02SGreg Roach    public const SIBLING  = 'sib';
446fcafd02SGreg Roach    public const MOTHER   = 'mot';
456fcafd02SGreg Roach    public const FATHER   = 'fat';
466fcafd02SGreg Roach    public const PARENT   = 'par';
476fcafd02SGreg Roach    public const DAUGHTER = 'dau';
486fcafd02SGreg Roach    public const SON      = 'son';
496fcafd02SGreg Roach    public const CHILD    = 'chi';
506fcafd02SGreg Roach    public const WIFE     = 'wif';
516fcafd02SGreg Roach    public const HUSBAND  = 'hus';
526fcafd02SGreg Roach    public const SPOUSE   = 'spo';
536fcafd02SGreg Roach
546fcafd02SGreg Roach    public const SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING];
556fcafd02SGreg Roach    public const PARENTS  = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT];
566fcafd02SGreg Roach    public const CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD];
576fcafd02SGreg Roach    public const SPOUSES  = ['F' => self::WIFE, 'M' => self::HUSBAND, 'U' => self::SPOUSE];
586fcafd02SGreg Roach
596fcafd02SGreg Roach    // Generates a name from the matched relationship.
606fcafd02SGreg Roach    private Closure $callback;
616fcafd02SGreg Roach
62696d9d5cSGreg Roach    /** @var array<Closure> List of rules that need to match */
636fcafd02SGreg Roach    private array $matchers;
646fcafd02SGreg Roach
656fcafd02SGreg Roach    /**
666fcafd02SGreg Roach     * Relationship constructor.
676fcafd02SGreg Roach     *
686fcafd02SGreg Roach     * @param Closure $callback
696fcafd02SGreg Roach     */
706fcafd02SGreg Roach    private function __construct(Closure $callback)
716fcafd02SGreg Roach    {
726fcafd02SGreg Roach        $this->callback = $callback;
736fcafd02SGreg Roach        $this->matchers = [];
746fcafd02SGreg Roach    }
756fcafd02SGreg Roach
766fcafd02SGreg Roach    /**
776fcafd02SGreg Roach     * Allow fluent constructor.
786fcafd02SGreg Roach     *
796fcafd02SGreg Roach     * @param string $nominative
806fcafd02SGreg Roach     * @param string $genitive
816fcafd02SGreg Roach     *
826fcafd02SGreg Roach     * @return Relationship
836fcafd02SGreg Roach     */
846fcafd02SGreg Roach    public static function fixed(string $nominative, string $genitive): Relationship
856fcafd02SGreg Roach    {
86696d9d5cSGreg Roach        return new self(fn () => [$nominative, $genitive]);
876fcafd02SGreg Roach    }
886fcafd02SGreg Roach
896fcafd02SGreg Roach    /**
906fcafd02SGreg Roach     * Allow fluent constructor.
916fcafd02SGreg Roach     *
926fcafd02SGreg Roach     * @param Closure $callback
936fcafd02SGreg Roach     *
946fcafd02SGreg Roach     * @return Relationship
956fcafd02SGreg Roach     */
966fcafd02SGreg Roach    public static function dynamic(Closure $callback): Relationship
976fcafd02SGreg Roach    {
98696d9d5cSGreg Roach        return new self($callback);
996fcafd02SGreg Roach    }
1006fcafd02SGreg Roach
1016fcafd02SGreg Roach    /**
1026fcafd02SGreg Roach     * Does this relationship match the pattern?
1036fcafd02SGreg Roach     *
1046fcafd02SGreg Roach     * @param array<Individual|Family> $nodes
1056fcafd02SGreg Roach     * @param array<string>            $patterns
1066fcafd02SGreg Roach     *
107696d9d5cSGreg Roach     * @return array<string>|null [nominative, genitive] or null
1086fcafd02SGreg Roach     */
1096fcafd02SGreg Roach    public function match(array $nodes, array $patterns): ?array
1106fcafd02SGreg Roach    {
1116fcafd02SGreg Roach        $captures = [];
1126fcafd02SGreg Roach
1136fcafd02SGreg Roach        foreach ($this->matchers as $matcher) {
1146fcafd02SGreg Roach            if (!$matcher($nodes, $patterns, $captures)) {
1156fcafd02SGreg Roach                return null;
1166fcafd02SGreg Roach            }
1176fcafd02SGreg Roach        }
1186fcafd02SGreg Roach
1196fcafd02SGreg Roach        if ($patterns === []) {
1206fcafd02SGreg Roach            return ($this->callback)(...$captures);
1216fcafd02SGreg Roach        }
1226fcafd02SGreg Roach
1236fcafd02SGreg Roach        return null;
1246fcafd02SGreg Roach    }
1256fcafd02SGreg Roach
1266fcafd02SGreg Roach    /**
1276fcafd02SGreg Roach     * @return Relationship
1286fcafd02SGreg Roach     */
1296fcafd02SGreg Roach    public function adopted(): Relationship
1306fcafd02SGreg Roach    {
13105babb96SGreg Roach        $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
1326fcafd02SGreg Roach                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
133*88a03560SGreg Roach                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_ADOPTED);
1346fcafd02SGreg Roach
1356fcafd02SGreg Roach        return $this;
1366fcafd02SGreg Roach    }
1376fcafd02SGreg Roach
1386fcafd02SGreg Roach    /**
1396fcafd02SGreg Roach     * @return Relationship
1406fcafd02SGreg Roach     */
1416fcafd02SGreg Roach    public function adoptive(): Relationship
1426fcafd02SGreg Roach    {
14305babb96SGreg Roach        $this->matchers[] = static fn (array $nodes): bool => $nodes[0]
1446fcafd02SGreg Roach            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
145*88a03560SGreg Roach            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_ADOPTED);
1466fcafd02SGreg Roach
1476fcafd02SGreg Roach        return $this;
1486fcafd02SGreg Roach    }
1496fcafd02SGreg Roach
1506fcafd02SGreg Roach    /**
1516fcafd02SGreg Roach     * @return Relationship
1526fcafd02SGreg Roach     */
1536fcafd02SGreg Roach    public function brother(): Relationship
1546fcafd02SGreg Roach    {
1556fcafd02SGreg Roach        return $this->relation([self::BROTHER]);
1566fcafd02SGreg Roach    }
1576fcafd02SGreg Roach
1586fcafd02SGreg Roach    /**
1596fcafd02SGreg Roach     * Match the next relationship in the path.
1606fcafd02SGreg Roach     *
1616fcafd02SGreg Roach     * @param array<string> $relationships
1626fcafd02SGreg Roach     *
1636fcafd02SGreg Roach     * @return Relationship
1646fcafd02SGreg Roach     */
1656fcafd02SGreg Roach    protected function relation(array $relationships): Relationship
1666fcafd02SGreg Roach    {
1676fcafd02SGreg Roach        $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool {
1686fcafd02SGreg Roach            if (in_array($patterns[0] ?? '', $relationships, true)) {
1696fcafd02SGreg Roach                $nodes    = array_slice($nodes, 2);
1706fcafd02SGreg Roach                $patterns = array_slice($patterns, 1);
1716fcafd02SGreg Roach
1726fcafd02SGreg Roach                return true;
1736fcafd02SGreg Roach            }
1746fcafd02SGreg Roach
1756fcafd02SGreg Roach            return false;
1766fcafd02SGreg Roach        };
1776fcafd02SGreg Roach
1786fcafd02SGreg Roach        return $this;
1796fcafd02SGreg Roach    }
1806fcafd02SGreg Roach
1816fcafd02SGreg Roach    /**
1826fcafd02SGreg Roach     * The number of ancestors may be different to the number of descendants
1836fcafd02SGreg Roach     *
1846fcafd02SGreg Roach     * @return Relationship
1856fcafd02SGreg Roach     */
186a004d330SGreg Roach    public function cousin(): Relationship
1876fcafd02SGreg Roach    {
1886fcafd02SGreg Roach        return $this->ancestor()->sibling()->descendant();
1896fcafd02SGreg Roach    }
1906fcafd02SGreg Roach
1916fcafd02SGreg Roach    /**
1926fcafd02SGreg Roach     * @return Relationship
1936fcafd02SGreg Roach     */
1946fcafd02SGreg Roach    public function descendant(): Relationship
1956fcafd02SGreg Roach    {
1966fcafd02SGreg Roach        return $this->repeatedRelationship(self::CHILDREN);
1976fcafd02SGreg Roach    }
1986fcafd02SGreg Roach
1996fcafd02SGreg Roach    /**
2006fcafd02SGreg Roach     * Match a repeated number of the same type of component
2016fcafd02SGreg Roach     *
2026fcafd02SGreg Roach     * @param array<string> $relationships
2036fcafd02SGreg Roach     *
2046fcafd02SGreg Roach     * @return Relationship
2056fcafd02SGreg Roach     */
2066fcafd02SGreg Roach    protected function repeatedRelationship(array $relationships): Relationship
2076fcafd02SGreg Roach    {
2086fcafd02SGreg Roach        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool {
2096fcafd02SGreg Roach            $limit = min(intdiv(count($nodes), 2), count($patterns));
2106fcafd02SGreg Roach
2116fcafd02SGreg Roach            for ($generations = 0; $generations < $limit; ++$generations) {
2126fcafd02SGreg Roach                if (!in_array($patterns[$generations], $relationships, true)) {
2136fcafd02SGreg Roach                    break;
2146fcafd02SGreg Roach                }
2156fcafd02SGreg Roach            }
2166fcafd02SGreg Roach
2176fcafd02SGreg Roach            if ($generations > 0) {
2186fcafd02SGreg Roach                $nodes      = array_slice($nodes, 2 * $generations);
2196fcafd02SGreg Roach                $patterns   = array_slice($patterns, $generations);
2206fcafd02SGreg Roach                $captures[] = $generations;
2216fcafd02SGreg Roach
2226fcafd02SGreg Roach                return true;
2236fcafd02SGreg Roach            }
2246fcafd02SGreg Roach
2256fcafd02SGreg Roach            return false;
2266fcafd02SGreg Roach        };
2276fcafd02SGreg Roach
2286fcafd02SGreg Roach        return $this;
2296fcafd02SGreg Roach    }
2306fcafd02SGreg Roach
2316fcafd02SGreg Roach    /**
2326fcafd02SGreg Roach     * @return Relationship
2336fcafd02SGreg Roach     */
2346fcafd02SGreg Roach    public function sibling(): Relationship
2356fcafd02SGreg Roach    {
2366fcafd02SGreg Roach        return $this->relation(self::SIBLINGS);
2376fcafd02SGreg Roach    }
2386fcafd02SGreg Roach
2396fcafd02SGreg Roach    /**
2406fcafd02SGreg Roach     * @return Relationship
2416fcafd02SGreg Roach     */
2426fcafd02SGreg Roach    public function ancestor(): Relationship
2436fcafd02SGreg Roach    {
2446fcafd02SGreg Roach        return $this->repeatedRelationship(self::PARENTS);
2456fcafd02SGreg Roach    }
2466fcafd02SGreg Roach
2476fcafd02SGreg Roach    /**
2486fcafd02SGreg Roach     * @return Relationship
2496fcafd02SGreg Roach     */
2506fcafd02SGreg Roach    public function child(): Relationship
2516fcafd02SGreg Roach    {
2526fcafd02SGreg Roach        return $this->relation(self::CHILDREN);
2536fcafd02SGreg Roach    }
2546fcafd02SGreg Roach
2556fcafd02SGreg Roach    /**
2566fcafd02SGreg Roach     * @return Relationship
2576fcafd02SGreg Roach     */
2586fcafd02SGreg Roach    public function daughter(): Relationship
2596fcafd02SGreg Roach    {
2606fcafd02SGreg Roach        return $this->relation([self::DAUGHTER]);
2616fcafd02SGreg Roach    }
2626fcafd02SGreg Roach
2636fcafd02SGreg Roach    /**
2646fcafd02SGreg Roach     * @return Relationship
2656fcafd02SGreg Roach     */
2666fcafd02SGreg Roach    public function divorced(): Relationship
2676fcafd02SGreg Roach    {
2686fcafd02SGreg Roach        return $this->marriageStatus('DIV');
2696fcafd02SGreg Roach    }
2706fcafd02SGreg Roach
2716fcafd02SGreg Roach    /**
2726fcafd02SGreg Roach     * Match a marriage status
2736fcafd02SGreg Roach     *
2746fcafd02SGreg Roach     * @param string $status
2756fcafd02SGreg Roach     *
2766fcafd02SGreg Roach     * @return Relationship
2776fcafd02SGreg Roach     */
2786fcafd02SGreg Roach    protected function marriageStatus(string $status): Relationship
2796fcafd02SGreg Roach    {
2806fcafd02SGreg Roach        $this->matchers[] = static function (array $nodes) use ($status): bool {
2816fcafd02SGreg Roach            $family = $nodes[1] ?? null;
2826fcafd02SGreg Roach
2836fcafd02SGreg Roach            if ($family instanceof Family) {
2846fcafd02SGreg Roach                $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last();
2856fcafd02SGreg Roach
2866fcafd02SGreg Roach                if ($fact instanceof Fact) {
2876fcafd02SGreg Roach                    switch ($status) {
2886fcafd02SGreg Roach                        case 'MARR':
2896fcafd02SGreg Roach                            return $fact->tag() === 'FAM:MARR';
2906fcafd02SGreg Roach
2916fcafd02SGreg Roach                        case 'DIV':
2926fcafd02SGreg Roach                            return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL';
2936fcafd02SGreg Roach
2946fcafd02SGreg Roach                        case 'ENGA':
2956fcafd02SGreg Roach                            return $fact->tag() === 'FAM:ENGA';
2966fcafd02SGreg Roach                    }
2976fcafd02SGreg Roach                }
2986fcafd02SGreg Roach            }
2996fcafd02SGreg Roach
3006fcafd02SGreg Roach            return false;
3016fcafd02SGreg Roach        };
3026fcafd02SGreg Roach
3036fcafd02SGreg Roach        return $this;
3046fcafd02SGreg Roach    }
3056fcafd02SGreg Roach
3066fcafd02SGreg Roach    /**
3076fcafd02SGreg Roach     * @return Relationship
3086fcafd02SGreg Roach     */
3096fcafd02SGreg Roach    public function engaged(): Relationship
3106fcafd02SGreg Roach    {
3116fcafd02SGreg Roach        return $this->marriageStatus('ENGA');
3126fcafd02SGreg Roach    }
3136fcafd02SGreg Roach
3146fcafd02SGreg Roach    /**
3156fcafd02SGreg Roach     * @return Relationship
3166fcafd02SGreg Roach     */
3176fcafd02SGreg Roach    public function father(): Relationship
3186fcafd02SGreg Roach    {
3196fcafd02SGreg Roach        return $this->relation([self::FATHER]);
3206fcafd02SGreg Roach    }
3216fcafd02SGreg Roach
3226fcafd02SGreg Roach    /**
3236fcafd02SGreg Roach     * @return Relationship
3246fcafd02SGreg Roach     */
3256fcafd02SGreg Roach    public function female(): Relationship
3266fcafd02SGreg Roach    {
3276fcafd02SGreg Roach        return $this->sex('F');
3286fcafd02SGreg Roach    }
3296fcafd02SGreg Roach
3306fcafd02SGreg Roach    /**
3316fcafd02SGreg Roach     * Match the sex of the current individual
3326fcafd02SGreg Roach     *
3336fcafd02SGreg Roach     * @param string $sex
3346fcafd02SGreg Roach     *
3356fcafd02SGreg Roach     * @return Relationship
3366fcafd02SGreg Roach     */
3376fcafd02SGreg Roach    protected function sex(string $sex): Relationship
3386fcafd02SGreg Roach    {
3396fcafd02SGreg Roach        $this->matchers[] = static function (array $nodes) use ($sex): bool {
3406fcafd02SGreg Roach            return $nodes[0]->sex() === $sex;
3416fcafd02SGreg Roach        };
3426fcafd02SGreg Roach
3436fcafd02SGreg Roach        return $this;
3446fcafd02SGreg Roach    }
3456fcafd02SGreg Roach
3466fcafd02SGreg Roach    /**
3476fcafd02SGreg Roach     * @return Relationship
3486fcafd02SGreg Roach     */
3496fcafd02SGreg Roach    public function fostered(): Relationship
3506fcafd02SGreg Roach    {
35105babb96SGreg Roach        $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
3526fcafd02SGreg Roach                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
353*88a03560SGreg Roach                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER);
3546fcafd02SGreg Roach
3556fcafd02SGreg Roach        return $this;
3566fcafd02SGreg Roach    }
3576fcafd02SGreg Roach
3586fcafd02SGreg Roach    /**
3596fcafd02SGreg Roach     * @return Relationship
3606fcafd02SGreg Roach     */
3616fcafd02SGreg Roach    public function fostering(): Relationship
3626fcafd02SGreg Roach    {
36305babb96SGreg Roach        $this->matchers[] = static fn (array $nodes): bool => $nodes[0]
3646fcafd02SGreg Roach            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
365*88a03560SGreg Roach            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER);
3666fcafd02SGreg Roach
3676fcafd02SGreg Roach        return $this;
3686fcafd02SGreg Roach    }
3696fcafd02SGreg Roach
3706fcafd02SGreg Roach    /**
3716fcafd02SGreg Roach     * @return Relationship
3726fcafd02SGreg Roach     */
3736fcafd02SGreg Roach    public function husband(): Relationship
3746fcafd02SGreg Roach    {
3756fcafd02SGreg Roach        return $this->married()->relation([self::HUSBAND]);
3766fcafd02SGreg Roach    }
3776fcafd02SGreg Roach
3786fcafd02SGreg Roach    /**
3796fcafd02SGreg Roach     * @return Relationship
3806fcafd02SGreg Roach     */
3816fcafd02SGreg Roach    public function married(): Relationship
3826fcafd02SGreg Roach    {
3836fcafd02SGreg Roach        return $this->marriageStatus('MARR');
3846fcafd02SGreg Roach    }
3856fcafd02SGreg Roach
3866fcafd02SGreg Roach    /**
3876fcafd02SGreg Roach     * @return Relationship
3886fcafd02SGreg Roach     */
3896fcafd02SGreg Roach    public function male(): Relationship
3906fcafd02SGreg Roach    {
3916fcafd02SGreg Roach        return $this->sex('M');
3926fcafd02SGreg Roach    }
3936fcafd02SGreg Roach
3946fcafd02SGreg Roach    /**
3956fcafd02SGreg Roach     * @return Relationship
3966fcafd02SGreg Roach     */
3976fcafd02SGreg Roach    public function mother(): Relationship
3986fcafd02SGreg Roach    {
3996fcafd02SGreg Roach        return $this->relation([self::MOTHER]);
4006fcafd02SGreg Roach    }
4016fcafd02SGreg Roach
4026fcafd02SGreg Roach    /**
4036fcafd02SGreg Roach     * @return Relationship
4046fcafd02SGreg Roach     */
4056fcafd02SGreg Roach    public function older(): Relationship
4066fcafd02SGreg Roach    {
4076fcafd02SGreg Roach        $this->matchers[] = static function (array $nodes): bool {
4086fcafd02SGreg Roach            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
4096fcafd02SGreg Roach            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
4106fcafd02SGreg Roach
411d8a5ab6eSJonathan Jaubart            return Date::compare($date1, $date2) > 0;
4126fcafd02SGreg Roach        };
4136fcafd02SGreg Roach
4146fcafd02SGreg Roach        return $this;
4156fcafd02SGreg Roach    }
4166fcafd02SGreg Roach
4176fcafd02SGreg Roach    /**
4186fcafd02SGreg Roach     * @return Relationship
4196fcafd02SGreg Roach     */
4206fcafd02SGreg Roach    public function parent(): Relationship
4216fcafd02SGreg Roach    {
4226fcafd02SGreg Roach        return $this->relation(self::PARENTS);
4236fcafd02SGreg Roach    }
4246fcafd02SGreg Roach
4256fcafd02SGreg Roach    /**
4266fcafd02SGreg Roach     * @return Relationship
4276fcafd02SGreg Roach     */
4286fcafd02SGreg Roach    public function sister(): Relationship
4296fcafd02SGreg Roach    {
4306fcafd02SGreg Roach        return $this->relation([self::SISTER]);
4316fcafd02SGreg Roach    }
4326fcafd02SGreg Roach
4336fcafd02SGreg Roach    /**
4346fcafd02SGreg Roach     * @return Relationship
4356fcafd02SGreg Roach     */
4366fcafd02SGreg Roach    public function son(): Relationship
4376fcafd02SGreg Roach    {
4386fcafd02SGreg Roach        return $this->relation([self::SON]);
4396fcafd02SGreg Roach    }
4406fcafd02SGreg Roach
4416fcafd02SGreg Roach    /**
4426fcafd02SGreg Roach     * @return Relationship
4436fcafd02SGreg Roach     */
4446fcafd02SGreg Roach    public function spouse(): Relationship
4456fcafd02SGreg Roach    {
4466fcafd02SGreg Roach        return $this->married()->partner();
4476fcafd02SGreg Roach    }
4486fcafd02SGreg Roach
4496fcafd02SGreg Roach    /**
4506fcafd02SGreg Roach     * @return Relationship
4516fcafd02SGreg Roach     */
4526fcafd02SGreg Roach    public function partner(): Relationship
4536fcafd02SGreg Roach    {
4546fcafd02SGreg Roach        return $this->relation(self::SPOUSES);
4556fcafd02SGreg Roach    }
4566fcafd02SGreg Roach
4576fcafd02SGreg Roach    /**
4586fcafd02SGreg Roach     * The number of ancestors must be the same as the number of descendants
4596fcafd02SGreg Roach     *
4606fcafd02SGreg Roach     * @return Relationship
4616fcafd02SGreg Roach     */
4626fcafd02SGreg Roach    public function symmetricCousin(): Relationship
4636fcafd02SGreg Roach    {
4646fcafd02SGreg Roach        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool {
4656fcafd02SGreg Roach            $count = count($patterns);
4666fcafd02SGreg Roach
4676fcafd02SGreg Roach            $n = 0;
4686fcafd02SGreg Roach
4696fcafd02SGreg Roach            // Ancestors
4706fcafd02SGreg Roach            while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) {
4716fcafd02SGreg Roach                $n++;
4726fcafd02SGreg Roach            }
4736fcafd02SGreg Roach
4746fcafd02SGreg Roach            // No ancestors?  Not enough path left for descendants?
4756fcafd02SGreg Roach            if ($n === 0 || $n * 2 + 1 !== $count) {
4766fcafd02SGreg Roach                return false;
4776fcafd02SGreg Roach            }
4786fcafd02SGreg Roach
4796fcafd02SGreg Roach            // Siblings
4806fcafd02SGreg Roach            if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) {
4816fcafd02SGreg Roach                return false;
4826fcafd02SGreg Roach            }
4836fcafd02SGreg Roach
4846fcafd02SGreg Roach            // Descendants
4856fcafd02SGreg Roach            for ($descendants = $n + 1; $descendants < $count; ++$descendants) {
4866fcafd02SGreg Roach                if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) {
4876fcafd02SGreg Roach                    return false;
4886fcafd02SGreg Roach                }
4896fcafd02SGreg Roach            }
4906fcafd02SGreg Roach
4916fcafd02SGreg Roach
492d8a5ab6eSJonathan Jaubart            $nodes      = array_slice($nodes, 2 * (2 * $n + 1));
4936fcafd02SGreg Roach            $patterns   = [];
4946fcafd02SGreg Roach            $captures[] = $n;
4956fcafd02SGreg Roach
4966fcafd02SGreg Roach            return true;
4976fcafd02SGreg Roach        };
4986fcafd02SGreg Roach
4996fcafd02SGreg Roach        return $this;
5006fcafd02SGreg Roach    }
5016fcafd02SGreg Roach
5026fcafd02SGreg Roach    /**
5036fcafd02SGreg Roach     * @return Relationship
5046fcafd02SGreg Roach     */
5056fcafd02SGreg Roach    public function twin(): Relationship
5066fcafd02SGreg Roach    {
5076fcafd02SGreg Roach        $this->matchers[] = static function (array $nodes): bool {
5086fcafd02SGreg Roach            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
5096fcafd02SGreg Roach            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
5106fcafd02SGreg Roach
5116fcafd02SGreg Roach            return
5126fcafd02SGreg Roach                $date1->isOK() &&
5136fcafd02SGreg Roach                $date2->isOK() &&
5146fcafd02SGreg Roach                abs($date1->julianDay() - $date2->julianDay()) < 2 &&
5156fcafd02SGreg Roach                $date1->minimumDate()->day > 0 &&
5166fcafd02SGreg Roach                $date2->minimumDate()->day > 0;
5176fcafd02SGreg Roach        };
5186fcafd02SGreg Roach
5196fcafd02SGreg Roach        return $this;
5206fcafd02SGreg Roach    }
5216fcafd02SGreg Roach
5226fcafd02SGreg Roach    /**
5236fcafd02SGreg Roach     * @return Relationship
5246fcafd02SGreg Roach     */
5256fcafd02SGreg Roach    public function wife(): Relationship
5266fcafd02SGreg Roach    {
5276fcafd02SGreg Roach        return $this->married()->relation([self::WIFE]);
5286fcafd02SGreg Roach    }
5296fcafd02SGreg Roach
5306fcafd02SGreg Roach    /**
5316fcafd02SGreg Roach     * @return Relationship
5326fcafd02SGreg Roach     */
5336fcafd02SGreg Roach    public function younger(): Relationship
5346fcafd02SGreg Roach    {
5356fcafd02SGreg Roach        $this->matchers[] = static function (array $nodes): bool {
5366fcafd02SGreg Roach            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
5376fcafd02SGreg Roach            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
5386fcafd02SGreg Roach
539d8a5ab6eSJonathan Jaubart            return Date::compare($date1, $date2) < 0;
5406fcafd02SGreg Roach        };
5416fcafd02SGreg Roach
5426fcafd02SGreg Roach        return $this;
5436fcafd02SGreg Roach    }
5446fcafd02SGreg Roach}
545