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