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