. */ declare(strict_types=1); namespace Fisharebest\Webtrees; use Closure; use function abs; use function array_slice; use function count; use function in_array; use function intdiv; use function min; /** * Class Relationship - define a relationship for a language. */ class Relationship { // The basic components of a relationship. // These strings are needed for compatibility with the legacy algorithm. // Once that has been replaced, it may be more efficient to use integers here. public const SISTER = 'sis'; public const BROTHER = 'bro'; public const SIBLING = 'sib'; public const MOTHER = 'mot'; public const FATHER = 'fat'; public const PARENT = 'par'; public const DAUGHTER = 'dau'; public const SON = 'son'; public const CHILD = 'chi'; public const WIFE = 'wif'; public const HUSBAND = 'hus'; public const SPOUSE = 'spo'; public const SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING]; public const PARENTS = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT]; public const CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD]; public const SPOUSES = ['F' => self::WIFE, 'M' => self::HUSBAND, 'U' => self::SPOUSE]; // Generates a name from the matched relationship. private Closure $callback; // List of rules that need to match. private array $matchers; /** * Relationship constructor. * * @param Closure $callback */ private function __construct(Closure $callback) { $this->callback = $callback; $this->matchers = []; } /** * Allow fluent constructor. * * @param string $nominative * @param string $genitive * * @return Relationship */ public static function fixed(string $nominative, string $genitive): Relationship { return new static(fn () => [$nominative, $genitive]); } /** * Allow fluent constructor. * * @param Closure $callback * * @return Relationship */ public static function dynamic(Closure $callback): Relationship { return new static($callback); } /** * Does this relationship match the pattern? * * @param array $nodes * @param array $patterns * * @return array [nominative, genitive] or null */ public function match(array $nodes, array $patterns): ?array { $captures = []; foreach ($this->matchers as $matcher) { if (!$matcher($nodes, $patterns, $captures)) { return null; } } if ($patterns === []) { return ($this->callback)(...$captures); } return null; } /** * @return Relationship */ public function adopted(): Relationship { $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2] ->facts(['FAMC'], false, Auth::PRIV_HIDE) ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted'); return $this; } /** * @return Relationship */ public function adoptive(): Relationship { $this->matchers[] = fn (array $nodes): bool => $nodes[0] ->facts(['FAMC'], false, Auth::PRIV_HIDE) ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted'); return $this; } /** * @return Relationship */ public function brother(): Relationship { return $this->relation([self::BROTHER]); } /** * Match the next relationship in the path. * * @param array $relationships * * @return Relationship */ protected function relation(array $relationships): Relationship { $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool { if (in_array($patterns[0] ?? '', $relationships, true)) { $nodes = array_slice($nodes, 2); $patterns = array_slice($patterns, 1); return true; } return false; }; return $this; } /** * The number of ancestors may be different to the number of descendants * * @return Relationship */ public function cousin(): Relationship { return $this->ancestor()->sibling()->descendant(); } /** * @return Relationship */ public function descendant(): Relationship { return $this->repeatedRelationship(self::CHILDREN); } /** * Match a repeated number of the same type of component * * @param array $relationships * * @return Relationship */ protected function repeatedRelationship(array $relationships): Relationship { $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool { $limit = min(intdiv(count($nodes), 2), count($patterns)); for ($generations = 0; $generations < $limit; ++$generations) { if (!in_array($patterns[$generations], $relationships, true)) { break; } } if ($generations > 0) { $nodes = array_slice($nodes, 2 * $generations); $patterns = array_slice($patterns, $generations); $captures[] = $generations; return true; } return false; }; return $this; } /** * @return Relationship */ public function sibling(): Relationship { return $this->relation(self::SIBLINGS); } /** * @return Relationship */ public function ancestor(): Relationship { return $this->repeatedRelationship(self::PARENTS); } /** * @return Relationship */ public function child(): Relationship { return $this->relation(self::CHILDREN); } /** * @return Relationship */ public function daughter(): Relationship { return $this->relation([self::DAUGHTER]); } /** * @return Relationship */ public function divorced(): Relationship { return $this->marriageStatus('DIV'); } /** * Match a marriage status * * @param string $status * * @return Relationship */ protected function marriageStatus(string $status): Relationship { $this->matchers[] = static function (array $nodes) use ($status): bool { $family = $nodes[1] ?? null; if ($family instanceof Family) { $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last(); if ($fact instanceof Fact) { switch ($status) { case 'MARR': return $fact->tag() === 'FAM:MARR'; case 'DIV': return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL'; case 'ENGA': return $fact->tag() === 'FAM:ENGA'; } } } return false; }; return $this; } /** * @return Relationship */ public function engaged(): Relationship { return $this->marriageStatus('ENGA'); } /** * @return Relationship */ public function father(): Relationship { return $this->relation([self::FATHER]); } /** * @return Relationship */ public function female(): Relationship { return $this->sex('F'); } /** * Match the sex of the current individual * * @param string $sex * * @return Relationship */ protected function sex(string $sex): Relationship { $this->matchers[] = static function (array $nodes) use ($sex): bool { return $nodes[0]->sex() === $sex; }; return $this; } /** * @return Relationship */ public function fostered(): Relationship { $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2] ->facts(['FAMC'], false, Auth::PRIV_HIDE) ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster'); return $this; } /** * @return Relationship */ public function fostering(): Relationship { $this->matchers[] = fn (array $nodes): bool => $nodes[0] ->facts(['FAMC'], false, Auth::PRIV_HIDE) ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster'); return $this; } /** * @return Relationship */ public function husband(): Relationship { return $this->married()->relation([self::HUSBAND]); } /** * @return Relationship */ public function married(): Relationship { return $this->marriageStatus('MARR'); } /** * @return Relationship */ public function male(): Relationship { return $this->sex('M'); } /** * @return Relationship */ public function mother(): Relationship { return $this->relation([self::MOTHER]); } /** * @return Relationship */ public function older(): Relationship { $this->matchers[] = static function (array $nodes): bool { $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); return Date::compare($date1, $date2) > 0; }; return $this; } /** * @return Relationship */ public function parent(): Relationship { return $this->relation(self::PARENTS); } /** * @return Relationship */ public function sister(): Relationship { return $this->relation([self::SISTER]); } /** * @return Relationship */ public function son(): Relationship { return $this->relation([self::SON]); } /** * @return Relationship */ public function spouse(): Relationship { return $this->married()->partner(); } /** * @return Relationship */ public function partner(): Relationship { return $this->relation(self::SPOUSES); } /** * The number of ancestors must be the same as the number of descendants * * @return Relationship */ public function symmetricCousin(): Relationship { $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool { $count = count($patterns); $n = 0; // Ancestors while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) { $n++; } // No ancestors? Not enough path left for descendants? if ($n === 0 || $n * 2 + 1 !== $count) { return false; } // Siblings if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) { return false; } // Descendants for ($descendants = $n + 1; $descendants < $count; ++$descendants) { if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) { return false; } } $nodes = array_slice($nodes, 2 * (2 * $n + 1)); $patterns = []; $captures[] = $n; return true; }; return $this; } /** * @return Relationship */ public function twin(): Relationship { $this->matchers[] = static function (array $nodes): bool { $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); return $date1->isOK() && $date2->isOK() && abs($date1->julianDay() - $date2->julianDay()) < 2 && $date1->minimumDate()->day > 0 && $date2->minimumDate()->day > 0; }; return $this; } /** * @return Relationship */ public function wife(): Relationship { return $this->married()->relation([self::WIFE]); } /** * @return Relationship */ public function younger(): Relationship { $this->matchers[] = static function (array $nodes): bool { $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); return Date::compare($date1, $date2) < 0; }; return $this; } }