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|null 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 fn(array $nodes): bool => $nodes[0]->sex() === $sex; 337 338 return $this; 339 } 340 341 /** 342 * @return Relationship 343 */ 344 public function fostered(): Relationship 345 { 346 $this->matchers[] = static fn (array $nodes): bool => count($nodes) > 2 && $nodes[2] 347 ->facts(['FAMC'], false, Auth::PRIV_HIDE) 348 ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER); 349 350 return $this; 351 } 352 353 /** 354 * @return Relationship 355 */ 356 public function fostering(): Relationship 357 { 358 $this->matchers[] = static fn (array $nodes): bool => $nodes[0] 359 ->facts(['FAMC'], false, Auth::PRIV_HIDE) 360 ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === PedigreeLinkageType::VALUE_FOSTER); 361 362 return $this; 363 } 364 365 /** 366 * @return Relationship 367 */ 368 public function husband(): Relationship 369 { 370 return $this->married()->relation([self::HUSBAND]); 371 } 372 373 /** 374 * @return Relationship 375 */ 376 public function married(): Relationship 377 { 378 return $this->marriageStatus('MARR'); 379 } 380 381 /** 382 * @return Relationship 383 */ 384 public function male(): Relationship 385 { 386 return $this->sex('M'); 387 } 388 389 /** 390 * @return Relationship 391 */ 392 public function mother(): Relationship 393 { 394 return $this->relation([self::MOTHER]); 395 } 396 397 /** 398 * @return Relationship 399 */ 400 public function older(): Relationship 401 { 402 $this->matchers[] = static function (array $nodes): bool { 403 $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 404 $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 405 406 return Date::compare($date1, $date2) > 0; 407 }; 408 409 return $this; 410 } 411 412 /** 413 * @return Relationship 414 */ 415 public function parent(): Relationship 416 { 417 return $this->relation(self::PARENTS); 418 } 419 420 /** 421 * @return Relationship 422 */ 423 public function sister(): Relationship 424 { 425 return $this->relation([self::SISTER]); 426 } 427 428 /** 429 * @return Relationship 430 */ 431 public function son(): Relationship 432 { 433 return $this->relation([self::SON]); 434 } 435 436 /** 437 * @return Relationship 438 */ 439 public function spouse(): Relationship 440 { 441 return $this->married()->partner(); 442 } 443 444 /** 445 * @return Relationship 446 */ 447 public function partner(): Relationship 448 { 449 return $this->relation(self::SPOUSES); 450 } 451 452 /** 453 * The number of ancestors must be the same as the number of descendants 454 * 455 * @return Relationship 456 */ 457 public function symmetricCousin(): Relationship 458 { 459 $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool { 460 $count = count($patterns); 461 462 $n = 0; 463 464 // Ancestors 465 while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) { 466 $n++; 467 } 468 469 // No ancestors? Not enough path left for descendants? 470 if ($n === 0 || $n * 2 + 1 !== $count) { 471 return false; 472 } 473 474 // Siblings 475 if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) { 476 return false; 477 } 478 479 // Descendants 480 for ($descendants = $n + 1; $descendants < $count; ++$descendants) { 481 if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) { 482 return false; 483 } 484 } 485 486 $nodes = array_slice($nodes, 2 * (2 * $n + 1)); 487 $patterns = []; 488 $captures[] = $n; 489 490 return true; 491 }; 492 493 return $this; 494 } 495 496 /** 497 * @return Relationship 498 */ 499 public function twin(): Relationship 500 { 501 $this->matchers[] = static function (array $nodes): bool { 502 $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 503 $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 504 505 return 506 $date1->isOK() && 507 $date2->isOK() && 508 abs($date1->julianDay() - $date2->julianDay()) < 2 && 509 $date1->minimumDate()->day > 0 && 510 $date2->minimumDate()->day > 0; 511 }; 512 513 return $this; 514 } 515 516 /** 517 * @return Relationship 518 */ 519 public function wife(): Relationship 520 { 521 return $this->married()->relation([self::WIFE]); 522 } 523 524 /** 525 * @return Relationship 526 */ 527 public function younger(): Relationship 528 { 529 $this->matchers[] = static function (array $nodes): bool { 530 $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 531 $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date(''); 532 533 return Date::compare($date1, $date2) < 0; 534 }; 535 536 return $this; 537 } 538} 539