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