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