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