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