1<?php 2 3/** 4 * webtrees: online genealogy 5 * Copyright (C) 2023 webtrees development team 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * You should have received a copy of the GNU General Public License 15 * along with this program. If not, see <https://www.gnu.org/licenses/>. 16 */ 17 18declare(strict_types=1); 19 20namespace Fisharebest\Webtrees; 21 22use Closure; 23use Fisharebest\Webtrees\Elements\PedigreeLinkageType; 24 25use function abs; 26use function array_slice; 27use function count; 28use function in_array; 29use function intdiv; 30use function min; 31 32/** 33 * Class Relationship - define a relationship for a language. 34 */ 35class Relationship 36{ 37 // The basic components of a relationship. 38 // These strings are needed for compatibility with the legacy algorithm. 39 // Once that has been replaced, it may be more efficient to use integers here. 40 public const SISTER = 'sis'; 41 public const BROTHER = 'bro'; 42 public const SIBLING = 'sib'; 43 public const MOTHER = 'mot'; 44 public const FATHER = 'fat'; 45 public const PARENT = 'par'; 46 public const DAUGHTER = 'dau'; 47 public const SON = 'son'; 48 public const CHILD = 'chi'; 49 public const WIFE = 'wif'; 50 public const HUSBAND = 'hus'; 51 public const SPOUSE = 'spo'; 52 53 public const SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING]; 54 public const PARENTS = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT]; 55 public const CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD]; 56 public const SPOUSES = ['F' => self::WIFE, 'M' => self::HUSBAND, 'U' => self::SPOUSE]; 57 58 // Generates a name from the matched relationship. 59 private Closure $callback; 60 61 /** @var array<Closure> List of rules that need to match */ 62 private array $matchers; 63 64 /** 65 * Relationship constructor. 66 * 67 * @param Closure $callback 68 */ 69 private function __construct(Closure $callback) 70 { 71 $this->callback = $callback; 72 $this->matchers = []; 73 } 74 75 /** 76 * Allow fluent constructor. 77 * 78 * @param string $nominative 79 * @param string $genitive 80 * 81 * @return Relationship 82 */ 83 public static function fixed(string $nominative, string $genitive): Relationship 84 { 85 return new self(fn () => [$nominative, $genitive]); 86 } 87 88 /** 89 * Allow fluent constructor. 90 * 91 * @param Closure $callback 92 * 93 * @return Relationship 94 */ 95 public static function dynamic(Closure $callback): Relationship 96 { 97 return new self($callback); 98 } 99 100 /** 101 * Does this relationship match the pattern? 102 * 103 * @param array<Individual|Family> $nodes 104 * @param array<string> $patterns 105 * 106 * @return array<string>|null [nominative, genitive] or null 107 */ 108 public function match(array $nodes, array $patterns): ?array 109 { 110 $captures = []; 111 112 foreach ($this->matchers as $matcher) { 113 if (!$matcher($nodes, $patterns, $captures)) { 114 return null; 115 } 116 } 117 118 if ($patterns === []) { 119 return ($this->callback)(...$captures); 120 } 121 122 return null; 123 } 124 125 /** 126 * @return Relationship 127 */ 128 public function adopted(): Relationship 129 { 130 $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2] 131 ->facts(['FAMC'], false, Auth::PRIV_HIDE) 132 ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_ADOPTED); 133 134 return $this; 135 } 136 137 /** 138 * @return Relationship 139 */ 140 public function adoptive(): Relationship 141 { 142 $this->matchers[] = static fn (array $nodes): bool => $nodes[0] 143 ->facts(['FAMC'], false, Auth::PRIV_HIDE) 144 ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_ADOPTED); 145 146 return $this; 147 } 148 149 /** 150 * @return Relationship 151 */ 152 public function brother(): Relationship 153 { 154 return $this->relation([self::BROTHER]); 155 } 156 157 /** 158 * Match the next relationship in the path. 159 * 160 * @param array<string> $relationships 161 * 162 * @return Relationship 163 */ 164 protected function relation(array $relationships): Relationship 165 { 166 $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool { 167 if (in_array($patterns[0] ?? '', $relationships, true)) { 168 $nodes = array_slice($nodes, 2); 169 $patterns = array_slice($patterns, 1); 170 171 return true; 172 } 173 174 return false; 175 }; 176 177 return $this; 178 } 179 180 /** 181 * The number of ancestors may be different to the number of descendants 182 * 183 * @return Relationship 184 */ 185 public function cousin(): Relationship 186 { 187 return $this->ancestor()->sibling()->descendant(); 188 } 189 190 /** 191 * @return Relationship 192 */ 193 public function descendant(): Relationship 194 { 195 return $this->repeatedRelationship(self::CHILDREN); 196 } 197 198 /** 199 * Match a repeated number of the same type of component 200 * 201 * @param array<string> $relationships 202 * 203 * @return Relationship 204 */ 205 protected function repeatedRelationship(array $relationships): Relationship 206 { 207 $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool { 208 $limit = min(intdiv(count($nodes), 2), count($patterns)); 209 210 for ($generations = 0; $generations < $limit; ++$generations) { 211 if (!in_array($patterns[$generations], $relationships, true)) { 212 break; 213 } 214 } 215 216 if ($generations > 0) { 217 $nodes = array_slice($nodes, 2 * $generations); 218 $patterns = array_slice($patterns, $generations); 219 $captures[] = $generations; 220 221 return true; 222 } 223 224 return false; 225 }; 226 227 return $this; 228 } 229 230 /** 231 * @return Relationship 232 */ 233 public function sibling(): Relationship 234 { 235 return $this->relation(self::SIBLINGS); 236 } 237 238 /** 239 * @return Relationship 240 */ 241 public function ancestor(): Relationship 242 { 243 return $this->repeatedRelationship(self::PARENTS); 244 } 245 246 /** 247 * @return Relationship 248 */ 249 public function child(): Relationship 250 { 251 return $this->relation(self::CHILDREN); 252 } 253 254 /** 255 * @return Relationship 256 */ 257 public function daughter(): Relationship 258 { 259 return $this->relation([self::DAUGHTER]); 260 } 261 262 /** 263 * @return Relationship 264 */ 265 public function divorced(): Relationship 266 { 267 return $this->marriageStatus('DIV'); 268 } 269 270 /** 271 * Match a marriage status 272 * 273 * @param string $status 274 * 275 * @return Relationship 276 */ 277 protected function marriageStatus(string $status): Relationship 278 { 279 $this->matchers[] = static function (array $nodes) use ($status): bool { 280 $family = $nodes[1] ?? null; 281 282 if ($family instanceof Family) { 283 $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last(); 284 285 if ($fact instanceof Fact) { 286 switch ($status) { 287 case 'MARR': 288 return $fact->tag() === 'FAM:MARR'; 289 290 case 'DIV': 291 return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL'; 292 293 case 'ENGA': 294 return $fact->tag() === 'FAM:ENGA'; 295 } 296 } 297 } 298 299 return false; 300 }; 301 302 return $this; 303 } 304 305 /** 306 * @return Relationship 307 */ 308 public function engaged(): Relationship 309 { 310 return $this->marriageStatus('ENGA'); 311 } 312 313 /** 314 * @return Relationship 315 */ 316 public function father(): Relationship 317 { 318 return $this->relation([self::FATHER]); 319 } 320 321 /** 322 * @return Relationship 323 */ 324 public function female(): Relationship 325 { 326 return $this->sex('F'); 327 } 328 329 /** 330 * Match the sex of the current individual 331 * 332 * @param string $sex 333 * 334 * @return Relationship 335 */ 336 protected function sex(string $sex): Relationship 337 { 338 $this->matchers[] = static function (array $nodes) use ($sex): bool { 339 return $nodes[0]->sex() === $sex; 340 }; 341 342 return $this; 343 } 344 345 /** 346 * @return Relationship 347 */ 348 public function fostered(): Relationship 349 { 350 $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2] 351 ->facts(['FAMC'], false, Auth::PRIV_HIDE) 352 ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER); 353 354 return $this; 355 } 356 357 /** 358 * @return Relationship 359 */ 360 public function fostering(): Relationship 361 { 362 $this->matchers[] = static fn (array $nodes): bool => $nodes[0] 363 ->facts(['FAMC'], false, Auth::PRIV_HIDE) 364 ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER); 365 366 return $this; 367 } 368 369 /** 370 * @return Relationship 371 */ 372 public function husband(): Relationship 373 { 374 return $this->married()->relation([self::HUSBAND]); 375 } 376 377 /** 378 * @return Relationship 379 */ 380 public function married(): Relationship 381 { 382 return $this->marriageStatus('MARR'); 383 } 384 385 /** 386 * @return Relationship 387 */ 388 public function male(): Relationship 389 { 390 return $this->sex('M'); 391 } 392 393 /** 394 * @return Relationship 395 */ 396 public function mother(): Relationship 397 { 398 return $this->relation([self::MOTHER]); 399 } 400 401 /** 402 * @return Relationship 403 */ 404 public function older(): Relationship 405 { 406 $this->matchers[] = static function (array $nodes): bool { 407 $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 408 $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 409 410 return Date::compare($date1, $date2) > 0; 411 }; 412 413 return $this; 414 } 415 416 /** 417 * @return Relationship 418 */ 419 public function parent(): Relationship 420 { 421 return $this->relation(self::PARENTS); 422 } 423 424 /** 425 * @return Relationship 426 */ 427 public function sister(): Relationship 428 { 429 return $this->relation([self::SISTER]); 430 } 431 432 /** 433 * @return Relationship 434 */ 435 public function son(): Relationship 436 { 437 return $this->relation([self::SON]); 438 } 439 440 /** 441 * @return Relationship 442 */ 443 public function spouse(): Relationship 444 { 445 return $this->married()->partner(); 446 } 447 448 /** 449 * @return Relationship 450 */ 451 public function partner(): Relationship 452 { 453 return $this->relation(self::SPOUSES); 454 } 455 456 /** 457 * The number of ancestors must be the same as the number of descendants 458 * 459 * @return Relationship 460 */ 461 public function symmetricCousin(): Relationship 462 { 463 $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool { 464 $count = count($patterns); 465 466 $n = 0; 467 468 // Ancestors 469 while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) { 470 $n++; 471 } 472 473 // No ancestors? Not enough path left for descendants? 474 if ($n === 0 || $n * 2 + 1 !== $count) { 475 return false; 476 } 477 478 // Siblings 479 if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) { 480 return false; 481 } 482 483 // Descendants 484 for ($descendants = $n + 1; $descendants < $count; ++$descendants) { 485 if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) { 486 return false; 487 } 488 } 489 490 491 $nodes = array_slice($nodes, 2 * (2 * $n + 1)); 492 $patterns = []; 493 $captures[] = $n; 494 495 return true; 496 }; 497 498 return $this; 499 } 500 501 /** 502 * @return Relationship 503 */ 504 public function twin(): Relationship 505 { 506 $this->matchers[] = static function (array $nodes): bool { 507 $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 508 $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 509 510 return 511 $date1->isOK() && 512 $date2->isOK() && 513 abs($date1->julianDay() - $date2->julianDay()) < 2 && 514 $date1->minimumDate()->day > 0 && 515 $date2->minimumDate()->day > 0; 516 }; 517 518 return $this; 519 } 520 521 /** 522 * @return Relationship 523 */ 524 public function wife(): Relationship 525 { 526 return $this->married()->relation([self::WIFE]); 527 } 528 529 /** 530 * @return Relationship 531 */ 532 public function younger(): Relationship 533 { 534 $this->matchers[] = static function (array $nodes): bool { 535 $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 536 $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 537 538 return Date::compare($date1, $date2) < 0; 539 }; 540 541 return $this; 542 } 543} 544