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\Statistics\Repository; 21 22use Exception; 23use Fisharebest\Webtrees\Registry; 24use Fisharebest\Webtrees\Family; 25use Fisharebest\Webtrees\GedcomRecord; 26use Fisharebest\Webtrees\I18N; 27use Fisharebest\Webtrees\Statistics\Google\ChartChildren; 28use Fisharebest\Webtrees\Statistics\Google\ChartDivorce; 29use Fisharebest\Webtrees\Statistics\Google\ChartFamilyLargest; 30use Fisharebest\Webtrees\Statistics\Google\ChartMarriage; 31use Fisharebest\Webtrees\Statistics\Google\ChartMarriageAge; 32use Fisharebest\Webtrees\Statistics\Google\ChartNoChildrenFamilies; 33use Fisharebest\Webtrees\Tree; 34use Illuminate\Database\Capsule\Manager as DB; 35use Illuminate\Database\Query\Builder; 36use Illuminate\Database\Query\Expression; 37use Illuminate\Database\Query\JoinClause; 38use stdClass; 39 40use function arsort; 41use function asort; 42use function e; 43use function floor; 44use function implode; 45use function in_array; 46use function str_replace; 47use function view; 48 49/** 50 * A repository providing methods for family related statistics. 51 */ 52class FamilyRepository 53{ 54 private Tree $tree; 55 56 /** 57 * @param Tree $tree 58 */ 59 public function __construct(Tree $tree) 60 { 61 $this->tree = $tree; 62 } 63 64 /** 65 * General query on family. 66 * 67 * @param string $type 68 * 69 * @return string 70 */ 71 private function familyQuery(string $type): string 72 { 73 $row = DB::table('families') 74 ->where('f_file', '=', $this->tree->id()) 75 ->orderBy('f_numchil', 'desc') 76 ->first(); 77 78 if ($row === null) { 79 return ''; 80 } 81 82 /** @var Family $family */ 83 $family = Registry::familyFactory()->mapper($this->tree)($row); 84 85 if (!$family->canShow()) { 86 return I18N::translate('This information is private and cannot be shown.'); 87 } 88 89 switch ($type) { 90 default: 91 case 'full': 92 return $family->formatList(); 93 94 case 'size': 95 return I18N::number((int) $row->f_numchil); 96 97 case 'name': 98 return '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 99 } 100 } 101 102 /** 103 * Find the family with the most children. 104 * 105 * @return string 106 */ 107 public function largestFamily(): string 108 { 109 return $this->familyQuery('full'); 110 } 111 112 /** 113 * Find the number of children in the largest family. 114 * 115 * @return string 116 */ 117 public function largestFamilySize(): string 118 { 119 return $this->familyQuery('size'); 120 } 121 122 /** 123 * Find the family with the most children. 124 * 125 * @return string 126 */ 127 public function largestFamilyName(): string 128 { 129 return $this->familyQuery('name'); 130 } 131 132 /** 133 * Find the couple with the most grandchildren. 134 * 135 * @param int $total 136 * 137 * @return array<stdClass> 138 */ 139 private function topTenGrandFamilyQuery(int $total): array 140 { 141 return DB::table('families') 142 ->join('link AS children', static function (JoinClause $join): void { 143 $join 144 ->on('children.l_from', '=', 'f_id') 145 ->on('children.l_file', '=', 'f_file') 146 ->where('children.l_type', '=', 'CHIL'); 147 })->join('link AS mchildren', static function (JoinClause $join): void { 148 $join 149 ->on('mchildren.l_file', '=', 'children.l_file') 150 ->on('mchildren.l_from', '=', 'children.l_to') 151 ->where('mchildren.l_type', '=', 'FAMS'); 152 })->join('link AS gchildren', static function (JoinClause $join): void { 153 $join 154 ->on('gchildren.l_file', '=', 'mchildren.l_file') 155 ->on('gchildren.l_from', '=', 'mchildren.l_to') 156 ->where('gchildren.l_type', '=', 'CHIL'); 157 }) 158 ->where('f_file', '=', $this->tree->id()) 159 ->groupBy(['f_id', 'f_file']) 160 ->orderBy(new Expression('COUNT(*)'), 'DESC') 161 ->select(['families.*']) 162 ->limit($total) 163 ->get() 164 ->map(Registry::familyFactory()->mapper($this->tree)) 165 ->filter(GedcomRecord::accessFilter()) 166 ->map(static function (Family $family): array { 167 $count = 0; 168 foreach ($family->children() as $child) { 169 foreach ($child->spouseFamilies() as $spouse_family) { 170 $count += $spouse_family->children()->count(); 171 } 172 } 173 174 return [ 175 'family' => $family, 176 'count' => $count, 177 ]; 178 }) 179 ->all(); 180 } 181 182 /** 183 * Find the couple with the most grandchildren. 184 * 185 * @param int $total 186 * 187 * @return string 188 */ 189 public function topTenLargestGrandFamily(int $total = 10): string 190 { 191 return view('statistics/families/top10-nolist-grand', [ 192 'records' => $this->topTenGrandFamilyQuery($total), 193 ]); 194 } 195 196 /** 197 * Find the couple with the most grandchildren. 198 * 199 * @param int $total 200 * 201 * @return string 202 */ 203 public function topTenLargestGrandFamilyList(int $total = 10): string 204 { 205 return view('statistics/families/top10-list-grand', [ 206 'records' => $this->topTenGrandFamilyQuery($total), 207 ]); 208 } 209 210 /** 211 * Find the families with no children. 212 * 213 * @return int 214 */ 215 private function noChildrenFamiliesQuery(): int 216 { 217 return DB::table('families') 218 ->where('f_file', '=', $this->tree->id()) 219 ->where('f_numchil', '=', 0) 220 ->count(); 221 } 222 223 /** 224 * Find the families with no children. 225 * 226 * @return string 227 */ 228 public function noChildrenFamilies(): string 229 { 230 return I18N::number($this->noChildrenFamiliesQuery()); 231 } 232 233 /** 234 * Find the families with no children. 235 * 236 * @param string $type 237 * 238 * @return string 239 */ 240 public function noChildrenFamiliesList(string $type = 'list'): string 241 { 242 $families = DB::table('families') 243 ->where('f_file', '=', $this->tree->id()) 244 ->where('f_numchil', '=', 0) 245 ->get() 246 ->map(Registry::familyFactory()->mapper($this->tree)) 247 ->filter(GedcomRecord::accessFilter()); 248 249 $top10 = []; 250 251 /** @var Family $family */ 252 foreach ($families as $family) { 253 if ($type === 'list') { 254 $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a></li>'; 255 } else { 256 $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a>'; 257 } 258 } 259 260 if ($type === 'list') { 261 $top10 = implode('', $top10); 262 } else { 263 $top10 = implode('; ', $top10); 264 } 265 266 267 if ($type === 'list') { 268 return '<ul>' . $top10 . '</ul>'; 269 } 270 271 return $top10; 272 } 273 274 /** 275 * Create a chart of children with no families. 276 * 277 * @param int $year1 278 * @param int $year2 279 * 280 * @return string 281 */ 282 public function chartNoChildrenFamilies(int $year1 = -1, int $year2 = -1): string 283 { 284 $no_child_fam = $this->noChildrenFamiliesQuery(); 285 286 return (new ChartNoChildrenFamilies($this->tree)) 287 ->chartNoChildrenFamilies($no_child_fam, $year1, $year2); 288 } 289 290 /** 291 * Returns the ages between siblings. 292 * 293 * @param int $total The total number of records to query 294 * 295 * @return array<stdClass> 296 */ 297 private function ageBetweenSiblingsQuery(int $total): array 298 { 299 $prefix = DB::connection()->getTablePrefix(); 300 301 return DB::table('link AS link1') 302 ->join('link AS link2', static function (JoinClause $join): void { 303 $join 304 ->on('link2.l_from', '=', 'link1.l_from') 305 ->on('link2.l_type', '=', 'link1.l_type') 306 ->on('link2.l_file', '=', 'link1.l_file'); 307 }) 308 ->join('dates AS child1', static function (JoinClause $join): void { 309 $join 310 ->on('child1.d_gid', '=', 'link1.l_to') 311 ->on('child1.d_file', '=', 'link1.l_file') 312 ->where('child1.d_fact', '=', 'BIRT') 313 ->where('child1.d_julianday1', '<>', 0); 314 }) 315 ->join('dates AS child2', static function (JoinClause $join): void { 316 $join 317 ->on('child2.d_gid', '=', 'link2.l_to') 318 ->on('child2.d_file', '=', 'link2.l_file') 319 ->where('child2.d_fact', '=', 'BIRT') 320 ->whereColumn('child2.d_julianday2', '>', 'child1.d_julianday1'); 321 }) 322 ->where('link1.l_type', '=', 'CHIL') 323 ->where('link1.l_file', '=', $this->tree->id()) 324 ->distinct() 325 ->select(['link1.l_from AS family', 'link1.l_to AS ch1', 'link2.l_to AS ch2', new Expression($prefix . 'child2.d_julianday2 - ' . $prefix . 'child1.d_julianday1 AS age')]) 326 ->orderBy('age', 'DESC') 327 ->take($total) 328 ->get() 329 ->all(); 330 } 331 332 /** 333 * Returns the calculated age the time of event. 334 * 335 * @param int $age The age from the database record 336 * 337 * @return string 338 */ 339 private function calculateAge(int $age): string 340 { 341 if ($age < 31) { 342 return I18N::plural('%s day', '%s days', $age, I18N::number($age)); 343 } 344 345 if ($age < 365) { 346 $months = (int) ($age / 30.5); 347 348 return I18N::plural('%s month', '%s months', $months, I18N::number($months)); 349 } 350 351 $years = (int) ($age / 365.25); 352 353 return I18N::plural('%s year', '%s years', $years, I18N::number($years)); 354 } 355 356 /** 357 * Find the ages between siblings. 358 * 359 * @param int $total The total number of records to query 360 * 361 * @return array<mixed> 362 * @throws Exception 363 */ 364 private function ageBetweenSiblingsNoList(int $total): array 365 { 366 $rows = $this->ageBetweenSiblingsQuery($total); 367 368 foreach ($rows as $fam) { 369 $family = Registry::familyFactory()->make($fam->family, $this->tree); 370 $child1 = Registry::individualFactory()->make($fam->ch1, $this->tree); 371 $child2 = Registry::individualFactory()->make($fam->ch2, $this->tree); 372 373 if ($child1->canShow() && $child2->canShow()) { 374 // ! Single array (no list) 375 return [ 376 'child1' => $child1, 377 'child2' => $child2, 378 'family' => $family, 379 'age' => $this->calculateAge((int) $fam->age), 380 ]; 381 } 382 } 383 384 return []; 385 } 386 387 /** 388 * Find the ages between siblings. 389 * 390 * @param int $total The total number of records to query 391 * @param bool $one Include each family only once if true 392 * 393 * @return array<string,array> 394 * @throws Exception 395 */ 396 private function ageBetweenSiblingsList(int $total, bool $one): array 397 { 398 $rows = $this->ageBetweenSiblingsQuery($total); 399 $top10 = []; 400 $dist = []; 401 402 foreach ($rows as $fam) { 403 $family = Registry::familyFactory()->make($fam->family, $this->tree); 404 $child1 = Registry::individualFactory()->make($fam->ch1, $this->tree); 405 $child2 = Registry::individualFactory()->make($fam->ch2, $this->tree); 406 407 $age = $this->calculateAge((int) $fam->age); 408 409 if ($one && !in_array($fam->family, $dist, true)) { 410 if ($child1->canShow() && $child2->canShow()) { 411 $top10[] = [ 412 'child1' => $child1, 413 'child2' => $child2, 414 'family' => $family, 415 'age' => $age, 416 ]; 417 418 $dist[] = $fam->family; 419 } 420 } elseif (!$one && $child1->canShow() && $child2->canShow()) { 421 $top10[] = [ 422 'child1' => $child1, 423 'child2' => $child2, 424 'family' => $family, 425 'age' => $age, 426 ]; 427 } 428 } 429 430 return $top10; 431 } 432 433 /** 434 * Find the ages between siblings. 435 * 436 * @param int $total The total number of records to query 437 * 438 * @return string 439 */ 440 private function ageBetweenSiblingsAge(int $total): string 441 { 442 $rows = $this->ageBetweenSiblingsQuery($total); 443 444 foreach ($rows as $fam) { 445 return $this->calculateAge((int) $fam->age); 446 } 447 448 return ''; 449 } 450 451 /** 452 * Find the ages between siblings. 453 * 454 * @param int $total The total number of records to query 455 * 456 * @return string 457 * @throws Exception 458 */ 459 private function ageBetweenSiblingsName(int $total): string 460 { 461 $rows = $this->ageBetweenSiblingsQuery($total); 462 463 foreach ($rows as $fam) { 464 $family = Registry::familyFactory()->make($fam->family, $this->tree); 465 $child1 = Registry::individualFactory()->make($fam->ch1, $this->tree); 466 $child2 = Registry::individualFactory()->make($fam->ch2, $this->tree); 467 468 if ($child1->canShow() && $child2->canShow()) { 469 $return = '<a href="' . e($child2->url()) . '">' . $child2->fullName() . '</a> '; 470 $return .= I18N::translate('and') . ' '; 471 $return .= '<a href="' . e($child1->url()) . '">' . $child1->fullName() . '</a>'; 472 $return .= ' <a href="' . e($family->url()) . '">[' . I18N::translate('View this family') . ']</a>'; 473 } else { 474 $return = I18N::translate('This information is private and cannot be shown.'); 475 } 476 477 return $return; 478 } 479 480 return ''; 481 } 482 483 /** 484 * Find the names of siblings with the widest age gap. 485 * 486 * @param int $total 487 * 488 * @return string 489 */ 490 public function topAgeBetweenSiblingsName(int $total = 10): string 491 { 492 return $this->ageBetweenSiblingsName($total); 493 } 494 495 /** 496 * Find the widest age gap between siblings. 497 * 498 * @param int $total 499 * 500 * @return string 501 */ 502 public function topAgeBetweenSiblings(int $total = 10): string 503 { 504 return $this->ageBetweenSiblingsAge($total); 505 } 506 507 /** 508 * Find the name of siblings with the widest age gap. 509 * 510 * @param int $total 511 * 512 * @return string 513 */ 514 public function topAgeBetweenSiblingsFullName(int $total = 10): string 515 { 516 $record = $this->ageBetweenSiblingsNoList($total); 517 518 if ($record === []) { 519 return I18N::translate('This information is not available.'); 520 } 521 522 return view('statistics/families/top10-nolist-age', [ 523 'record' => $record, 524 ]); 525 } 526 527 /** 528 * Find the siblings with the widest age gaps. 529 * 530 * @param int $total 531 * @param string $one 532 * 533 * @return string 534 */ 535 public function topAgeBetweenSiblingsList(int $total = 10, string $one = ''): string 536 { 537 $records = $this->ageBetweenSiblingsList($total, (bool) $one); 538 539 return view('statistics/families/top10-list-age', [ 540 'records' => $records, 541 ]); 542 } 543 544 /** 545 * General query on familes/children. 546 * 547 * @param int $year1 548 * @param int $year2 549 * 550 * @return array<stdClass> 551 */ 552 public function statsChildrenQuery(int $year1 = -1, int $year2 = -1): array 553 { 554 $query = DB::table('families') 555 ->where('f_file', '=', $this->tree->id()) 556 ->groupBy(['f_numchil']) 557 ->select(['f_numchil', new Expression('COUNT(*) AS total')]); 558 559 if ($year1 >= 0 && $year2 >= 0) { 560 $query 561 ->join('dates', static function (JoinClause $join): void { 562 $join 563 ->on('d_file', '=', 'f_file') 564 ->on('d_gid', '=', 'f_id'); 565 }) 566 ->where('d_fact', '=', 'MARR') 567 ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 568 ->whereBetween('d_year', [$year1, $year2]); 569 } 570 571 return $query->get()->all(); 572 } 573 574 /** 575 * Genearl query on families/children. 576 * 577 * @return string 578 */ 579 public function statsChildren(): string 580 { 581 return (new ChartChildren($this->tree)) 582 ->chartChildren(); 583 } 584 585 /** 586 * Count the total children. 587 * 588 * @return string 589 */ 590 public function totalChildren(): string 591 { 592 $total = (int) DB::table('families') 593 ->where('f_file', '=', $this->tree->id()) 594 ->sum('f_numchil'); 595 596 return I18N::number($total); 597 } 598 599 /** 600 * Find the average number of children in families. 601 * 602 * @return string 603 */ 604 public function averageChildren(): string 605 { 606 $average = (float) DB::table('families') 607 ->where('f_file', '=', $this->tree->id()) 608 ->avg('f_numchil'); 609 610 return I18N::number($average, 2); 611 } 612 613 /** 614 * General query on families. 615 * 616 * @param int $total 617 * 618 * @return array<array<string,mixed>> 619 */ 620 private function topTenFamilyQuery(int $total): array 621 { 622 return DB::table('families') 623 ->where('f_file', '=', $this->tree->id()) 624 ->orderBy('f_numchil', 'DESC') 625 ->limit($total) 626 ->get() 627 ->map(Registry::familyFactory()->mapper($this->tree)) 628 ->filter(GedcomRecord::accessFilter()) 629 ->map(static function (Family $family): array { 630 return [ 631 'family' => $family, 632 'count' => $family->numberOfChildren(), 633 ]; 634 }) 635 ->all(); 636 } 637 638 /** 639 * The the families with the most children. 640 * 641 * @param int $total 642 * 643 * @return string 644 */ 645 public function topTenLargestFamily(int $total = 10): string 646 { 647 $records = $this->topTenFamilyQuery($total); 648 649 return view('statistics/families/top10-nolist', [ 650 'records' => $records, 651 ]); 652 } 653 654 /** 655 * Find the families with the most children. 656 * 657 * @param int $total 658 * 659 * @return string 660 */ 661 public function topTenLargestFamilyList(int $total = 10): string 662 { 663 $records = $this->topTenFamilyQuery($total); 664 665 return view('statistics/families/top10-list', [ 666 'records' => $records, 667 ]); 668 } 669 670 /** 671 * Create a chart of the largest families. 672 * 673 * @param string|null $color_from 674 * @param string|null $color_to 675 * @param int $total 676 * 677 * @return string 678 */ 679 public function chartLargestFamilies( 680 string $color_from = null, 681 string $color_to = null, 682 int $total = 10 683 ): string { 684 return (new ChartFamilyLargest($this->tree)) 685 ->chartLargestFamilies($color_from, $color_to, $total); 686 } 687 688 /** 689 * Find the month in the year of the birth of the first child. 690 * 691 * @param int $year1 692 * @param int $year2 693 * 694 * @return Builder 695 */ 696 public function monthFirstChildQuery(int $year1 = -1, int $year2 = -1): Builder 697 { 698 $first_child_subquery = DB::table('link') 699 ->join('dates', static function (JoinClause $join): void { 700 $join 701 ->on('d_gid', '=', 'l_to') 702 ->on('d_file', '=', 'l_file') 703 ->where('d_julianday1', '<>', 0) 704 ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']); 705 }) 706 ->where('l_file', '=', $this->tree->id()) 707 ->where('l_type', '=', 'CHIL') 708 ->select(['l_from AS family_id', new Expression('MIN(d_julianday1) AS min_birth_jd')]) 709 ->groupBy(['family_id']); 710 711 $query = DB::table('link') 712 ->join('dates', static function (JoinClause $join): void { 713 $join 714 ->on('d_gid', '=', 'l_to') 715 ->on('d_file', '=', 'l_file'); 716 }) 717 ->joinSub($first_child_subquery, 'subquery', static function (JoinClause $join): void { 718 $join 719 ->on('family_id', '=', 'l_from') 720 ->on('min_birth_jd', '=', 'd_julianday1'); 721 }) 722 ->where('link.l_file', '=', $this->tree->id()) 723 ->where('link.l_type', '=', 'CHIL') 724 ->select(['d_month', new Expression('COUNT(*) AS total')]) 725 ->groupBy(['d_month']); 726 727 if ($year1 >= 0 && $year2 >= 0) { 728 $query->whereBetween('d_year', [$year1, $year2]); 729 } 730 731 return $query; 732 } 733 734 /** 735 * Find the month in the year of the birth of the first child. 736 * 737 * @param int $year1 738 * @param int $year2 739 * 740 * @return Builder 741 */ 742 public function monthFirstChildBySexQuery(int $year1 = -1, int $year2 = -1): Builder 743 { 744 return $this->monthFirstChildQuery($year1, $year2) 745 ->join('individuals', static function (JoinClause $join): void { 746 $join 747 ->on('i_file', '=', 'l_file') 748 ->on('i_id', '=', 'l_to'); 749 }) 750 ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')]) 751 ->groupBy(['d_month', 'i_sex']); 752 } 753 754 /** 755 * Number of husbands. 756 * 757 * @return string 758 */ 759 public function totalMarriedMales(): string 760 { 761 $n = DB::table('families') 762 ->where('f_file', '=', $this->tree->id()) 763 ->where('f_gedcom', 'LIKE', "%\n1 MARR%") 764 ->distinct() 765 ->count('f_husb'); 766 767 return I18N::number($n); 768 } 769 770 /** 771 * Number of wives. 772 * 773 * @return string 774 */ 775 public function totalMarriedFemales(): string 776 { 777 $n = DB::table('families') 778 ->where('f_file', '=', $this->tree->id()) 779 ->where('f_gedcom', 'LIKE', "%\n1 MARR%") 780 ->distinct() 781 ->count('f_wife'); 782 783 return I18N::number($n); 784 } 785 786 /** 787 * General query on parents. 788 * 789 * @param string $type 790 * @param string $age_dir 791 * @param string $sex 792 * @param bool $show_years 793 * 794 * @return string 795 */ 796 private function parentsQuery(string $type, string $age_dir, string $sex, bool $show_years): string 797 { 798 if ($sex === 'F') { 799 $sex_field = 'WIFE'; 800 } else { 801 $sex_field = 'HUSB'; 802 } 803 804 if ($age_dir !== 'ASC') { 805 $age_dir = 'DESC'; 806 } 807 808 $prefix = DB::connection()->getTablePrefix(); 809 810 $row = DB::table('link AS parentfamily') 811 ->join('link AS childfamily', static function (JoinClause $join): void { 812 $join 813 ->on('childfamily.l_file', '=', 'parentfamily.l_file') 814 ->on('childfamily.l_from', '=', 'parentfamily.l_from') 815 ->where('childfamily.l_type', '=', 'CHIL'); 816 }) 817 ->join('dates AS birth', static function (JoinClause $join): void { 818 $join 819 ->on('birth.d_file', '=', 'parentfamily.l_file') 820 ->on('birth.d_gid', '=', 'parentfamily.l_to') 821 ->where('birth.d_fact', '=', 'BIRT') 822 ->where('birth.d_julianday1', '<>', 0); 823 }) 824 ->join('dates AS childbirth', static function (JoinClause $join): void { 825 $join 826 ->on('childbirth.d_file', '=', 'parentfamily.l_file') 827 ->on('childbirth.d_gid', '=', 'childfamily.l_to') 828 ->where('childbirth.d_fact', '=', 'BIRT'); 829 }) 830 ->where('childfamily.l_file', '=', $this->tree->id()) 831 ->where('parentfamily.l_type', '=', $sex_field) 832 ->where('childbirth.d_julianday2', '>', 'birth.d_julianday1') 833 ->select(['parentfamily.l_to AS id', new Expression($prefix . 'childbirth.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]) 834 ->take(1) 835 ->orderBy('age', $age_dir) 836 ->get() 837 ->first(); 838 839 if ($row === null) { 840 return ''; 841 } 842 843 $person = Registry::individualFactory()->make($row->id, $this->tree); 844 845 switch ($type) { 846 default: 847 case 'full': 848 if ($person && $person->canShow()) { 849 $result = $person->formatList(); 850 } else { 851 $result = I18N::translate('This information is private and cannot be shown.'); 852 } 853 break; 854 855 case 'name': 856 $result = '<a href="' . e($person->url()) . '">' . $person->fullName() . '</a>'; 857 break; 858 859 case 'age': 860 $age = $row->age; 861 862 if ($show_years) { 863 $result = $this->calculateAge((int) $row->age); 864 } else { 865 $result = (string) floor($age / 365.25); 866 } 867 868 break; 869 } 870 871 return $result; 872 } 873 874 /** 875 * Find the youngest mother 876 * 877 * @return string 878 */ 879 public function youngestMother(): string 880 { 881 return $this->parentsQuery('full', 'ASC', 'F', false); 882 } 883 884 /** 885 * Find the name of the youngest mother. 886 * 887 * @return string 888 */ 889 public function youngestMotherName(): string 890 { 891 return $this->parentsQuery('name', 'ASC', 'F', false); 892 } 893 894 /** 895 * Find the age of the youngest mother. 896 * 897 * @param string $show_years 898 * 899 * @return string 900 */ 901 public function youngestMotherAge(string $show_years = ''): string 902 { 903 return $this->parentsQuery('age', 'ASC', 'F', (bool) $show_years); 904 } 905 906 /** 907 * Find the oldest mother. 908 * 909 * @return string 910 */ 911 public function oldestMother(): string 912 { 913 return $this->parentsQuery('full', 'DESC', 'F', false); 914 } 915 916 /** 917 * Find the name of the oldest mother. 918 * 919 * @return string 920 */ 921 public function oldestMotherName(): string 922 { 923 return $this->parentsQuery('name', 'DESC', 'F', false); 924 } 925 926 /** 927 * Find the age of the oldest mother. 928 * 929 * @param string $show_years 930 * 931 * @return string 932 */ 933 public function oldestMotherAge(string $show_years = ''): string 934 { 935 return $this->parentsQuery('age', 'DESC', 'F', (bool) $show_years); 936 } 937 938 /** 939 * Find the youngest father. 940 * 941 * @return string 942 */ 943 public function youngestFather(): string 944 { 945 return $this->parentsQuery('full', 'ASC', 'M', false); 946 } 947 948 /** 949 * Find the name of the youngest father. 950 * 951 * @return string 952 */ 953 public function youngestFatherName(): string 954 { 955 return $this->parentsQuery('name', 'ASC', 'M', false); 956 } 957 958 /** 959 * Find the age of the youngest father. 960 * 961 * @param string $show_years 962 * 963 * @return string 964 */ 965 public function youngestFatherAge(string $show_years = ''): string 966 { 967 return $this->parentsQuery('age', 'ASC', 'M', (bool) $show_years); 968 } 969 970 /** 971 * Find the oldest father. 972 * 973 * @return string 974 */ 975 public function oldestFather(): string 976 { 977 return $this->parentsQuery('full', 'DESC', 'M', false); 978 } 979 980 /** 981 * Find the name of the oldest father. 982 * 983 * @return string 984 */ 985 public function oldestFatherName(): string 986 { 987 return $this->parentsQuery('name', 'DESC', 'M', false); 988 } 989 990 /** 991 * Find the age of the oldest father. 992 * 993 * @param string $show_years 994 * 995 * @return string 996 */ 997 public function oldestFatherAge(string $show_years = ''): string 998 { 999 return $this->parentsQuery('age', 'DESC', 'M', (bool) $show_years); 1000 } 1001 1002 /** 1003 * General query on age at marriage. 1004 * 1005 * @param string $type 1006 * @param string $age_dir "ASC" or "DESC" 1007 * @param int $total 1008 * 1009 * @return string 1010 */ 1011 private function ageOfMarriageQuery(string $type, string $age_dir, int $total): string 1012 { 1013 $prefix = DB::connection()->getTablePrefix(); 1014 1015 $hrows = DB::table('families') 1016 ->where('f_file', '=', $this->tree->id()) 1017 ->join('dates AS married', static function (JoinClause $join): void { 1018 $join 1019 ->on('married.d_file', '=', 'f_file') 1020 ->on('married.d_gid', '=', 'f_id') 1021 ->where('married.d_fact', '=', 'MARR') 1022 ->where('married.d_julianday1', '<>', 0); 1023 }) 1024 ->join('dates AS husbdeath', static function (JoinClause $join): void { 1025 $join 1026 ->on('husbdeath.d_gid', '=', 'f_husb') 1027 ->on('husbdeath.d_file', '=', 'f_file') 1028 ->where('husbdeath.d_fact', '=', 'DEAT'); 1029 }) 1030 ->whereColumn('married.d_julianday1', '<', 'husbdeath.d_julianday2') 1031 ->groupBy(['f_id']) 1032 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'husbdeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1033 ->get() 1034 ->all(); 1035 1036 $wrows = DB::table('families') 1037 ->where('f_file', '=', $this->tree->id()) 1038 ->join('dates AS married', static function (JoinClause $join): void { 1039 $join 1040 ->on('married.d_file', '=', 'f_file') 1041 ->on('married.d_gid', '=', 'f_id') 1042 ->where('married.d_fact', '=', 'MARR') 1043 ->where('married.d_julianday1', '<>', 0); 1044 }) 1045 ->join('dates AS wifedeath', static function (JoinClause $join): void { 1046 $join 1047 ->on('wifedeath.d_gid', '=', 'f_wife') 1048 ->on('wifedeath.d_file', '=', 'f_file') 1049 ->where('wifedeath.d_fact', '=', 'DEAT'); 1050 }) 1051 ->whereColumn('married.d_julianday1', '<', 'wifedeath.d_julianday2') 1052 ->groupBy(['f_id']) 1053 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'wifedeath.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1054 ->get() 1055 ->all(); 1056 1057 $drows = DB::table('families') 1058 ->where('f_file', '=', $this->tree->id()) 1059 ->join('dates AS married', static function (JoinClause $join): void { 1060 $join 1061 ->on('married.d_file', '=', 'f_file') 1062 ->on('married.d_gid', '=', 'f_id') 1063 ->where('married.d_fact', '=', 'MARR') 1064 ->where('married.d_julianday1', '<>', 0); 1065 }) 1066 ->join('dates AS divorced', static function (JoinClause $join): void { 1067 $join 1068 ->on('divorced.d_gid', '=', 'f_id') 1069 ->on('divorced.d_file', '=', 'f_file') 1070 ->whereIn('divorced.d_fact', ['DIV', 'ANUL', '_SEPR']); 1071 }) 1072 ->whereColumn('married.d_julianday1', '<', 'divorced.d_julianday2') 1073 ->groupBy(['f_id']) 1074 ->select(['f_id AS family', new Expression('MIN(' . $prefix . 'divorced.d_julianday2 - ' . $prefix . 'married.d_julianday1) AS age')]) 1075 ->get() 1076 ->all(); 1077 1078 $rows = []; 1079 foreach ($drows as $family) { 1080 $rows[$family->family] = $family->age; 1081 } 1082 1083 foreach ($hrows as $family) { 1084 if (!isset($rows[$family->family])) { 1085 $rows[$family->family] = $family->age; 1086 } 1087 } 1088 1089 foreach ($wrows as $family) { 1090 if (!isset($rows[$family->family])) { 1091 $rows[$family->family] = $family->age; 1092 } elseif ($rows[$family->family] > $family->age) { 1093 $rows[$family->family] = $family->age; 1094 } 1095 } 1096 1097 if ($age_dir === 'DESC') { 1098 arsort($rows); 1099 } else { 1100 asort($rows); 1101 } 1102 1103 $top10 = []; 1104 $i = 0; 1105 foreach ($rows as $xref => $age) { 1106 $family = Registry::familyFactory()->make((string) $xref, $this->tree); 1107 if ($type === 'name') { 1108 return $family->formatList(); 1109 } 1110 1111 $age = $this->calculateAge((int) $age); 1112 1113 if ($type === 'age') { 1114 return $age; 1115 } 1116 1117 $husb = $family->husband(); 1118 $wife = $family->wife(); 1119 1120 if (($husb && ($husb->getAllDeathDates() || !$husb->isDead())) && ($wife && ($wife->getAllDeathDates() || !$wife->isDead()))) { 1121 if ($family && $family->canShow()) { 1122 if ($type === 'list') { 1123 $top10[] = '<li><a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')' . '</li>'; 1124 } else { 1125 $top10[] = '<a href="' . e($family->url()) . '">' . $family->fullName() . '</a> (' . $age . ')'; 1126 } 1127 } 1128 if (++$i === $total) { 1129 break; 1130 } 1131 } 1132 } 1133 1134 if ($type === 'list') { 1135 $top10 = implode('', $top10); 1136 } else { 1137 $top10 = implode('; ', $top10); 1138 } 1139 1140 if (I18N::direction() === 'rtl') { 1141 $top10 = str_replace([ 1142 '[', 1143 ']', 1144 '(', 1145 ')', 1146 '+', 1147 ], [ 1148 '‏[', 1149 '‏]', 1150 '‏(', 1151 '‏)', 1152 '‏+', 1153 ], $top10); 1154 } 1155 1156 if ($type === 'list') { 1157 return '<ul>' . $top10 . '</ul>'; 1158 } 1159 1160 return $top10; 1161 } 1162 1163 /** 1164 * General query on marriage ages. 1165 * 1166 * @return string 1167 */ 1168 public function topAgeOfMarriageFamily(): string 1169 { 1170 return $this->ageOfMarriageQuery('name', 'DESC', 1); 1171 } 1172 1173 /** 1174 * General query on marriage ages. 1175 * 1176 * @return string 1177 */ 1178 public function topAgeOfMarriage(): string 1179 { 1180 return $this->ageOfMarriageQuery('age', 'DESC', 1); 1181 } 1182 1183 /** 1184 * General query on marriage ages. 1185 * 1186 * @param int $total 1187 * 1188 * @return string 1189 */ 1190 public function topAgeOfMarriageFamilies(int $total = 10): string 1191 { 1192 return $this->ageOfMarriageQuery('nolist', 'DESC', $total); 1193 } 1194 1195 /** 1196 * General query on marriage ages. 1197 * 1198 * @param int $total 1199 * 1200 * @return string 1201 */ 1202 public function topAgeOfMarriageFamiliesList(int $total = 10): string 1203 { 1204 return $this->ageOfMarriageQuery('list', 'DESC', $total); 1205 } 1206 1207 /** 1208 * General query on marriage ages. 1209 * 1210 * @return string 1211 */ 1212 public function minAgeOfMarriageFamily(): string 1213 { 1214 return $this->ageOfMarriageQuery('name', 'ASC', 1); 1215 } 1216 1217 /** 1218 * General query on marriage ages. 1219 * 1220 * @return string 1221 */ 1222 public function minAgeOfMarriage(): string 1223 { 1224 return $this->ageOfMarriageQuery('age', 'ASC', 1); 1225 } 1226 1227 /** 1228 * General query on marriage ages. 1229 * 1230 * @param int $total 1231 * 1232 * @return string 1233 */ 1234 public function minAgeOfMarriageFamilies(int $total = 10): string 1235 { 1236 return $this->ageOfMarriageQuery('nolist', 'ASC', $total); 1237 } 1238 1239 /** 1240 * General query on marriage ages. 1241 * 1242 * @param int $total 1243 * 1244 * @return string 1245 */ 1246 public function minAgeOfMarriageFamiliesList(int $total = 10): string 1247 { 1248 return $this->ageOfMarriageQuery('list', 'ASC', $total); 1249 } 1250 1251 /** 1252 * Find the ages between spouses. 1253 * 1254 * @param string $age_dir 1255 * @param int $total 1256 * 1257 * @return array<array<string,mixed>> 1258 */ 1259 private function ageBetweenSpousesQuery(string $age_dir, int $total): array 1260 { 1261 $prefix = DB::connection()->getTablePrefix(); 1262 1263 $query = DB::table('families') 1264 ->where('f_file', '=', $this->tree->id()) 1265 ->join('dates AS wife', static function (JoinClause $join): void { 1266 $join 1267 ->on('wife.d_gid', '=', 'f_wife') 1268 ->on('wife.d_file', '=', 'f_file') 1269 ->where('wife.d_fact', '=', 'BIRT') 1270 ->where('wife.d_julianday1', '<>', 0); 1271 }) 1272 ->join('dates AS husb', static function (JoinClause $join): void { 1273 $join 1274 ->on('husb.d_gid', '=', 'f_husb') 1275 ->on('husb.d_file', '=', 'f_file') 1276 ->where('husb.d_fact', '=', 'BIRT') 1277 ->where('husb.d_julianday1', '<>', 0); 1278 }); 1279 1280 if ($age_dir === 'DESC') { 1281 $query 1282 ->whereColumn('wife.d_julianday1', '>=', 'husb.d_julianday1') 1283 ->orderBy(new Expression('MIN(' . $prefix . 'wife.d_julianday1) - MIN(' . $prefix . 'husb.d_julianday1)'), 'DESC'); 1284 } else { 1285 $query 1286 ->whereColumn('husb.d_julianday1', '>=', 'wife.d_julianday1') 1287 ->orderBy(new Expression('MIN(' . $prefix . 'husb.d_julianday1) - MIN(' . $prefix . 'wife.d_julianday1)'), 'DESC'); 1288 } 1289 1290 return $query 1291 ->groupBy(['f_id', 'f_file']) 1292 ->select(['families.*']) 1293 ->take($total) 1294 ->get() 1295 ->map(Registry::familyFactory()->mapper($this->tree)) 1296 ->filter(GedcomRecord::accessFilter()) 1297 ->map(function (Family $family) use ($age_dir): array { 1298 $husb_birt_jd = $family->husband()->getBirthDate()->minimumJulianDay(); 1299 $wife_birt_jd = $family->wife()->getBirthDate()->minimumJulianDay(); 1300 1301 if ($age_dir === 'DESC') { 1302 $diff = $wife_birt_jd - $husb_birt_jd; 1303 } else { 1304 $diff = $husb_birt_jd - $wife_birt_jd; 1305 } 1306 1307 return [ 1308 'family' => $family, 1309 'age' => $this->calculateAge($diff), 1310 ]; 1311 }) 1312 ->all(); 1313 } 1314 1315 /** 1316 * Find the age between husband and wife. 1317 * 1318 * @param int $total 1319 * 1320 * @return string 1321 */ 1322 public function ageBetweenSpousesMF(int $total = 10): string 1323 { 1324 $records = $this->ageBetweenSpousesQuery('DESC', $total); 1325 1326 return view('statistics/families/top10-nolist-spouses', [ 1327 'records' => $records, 1328 ]); 1329 } 1330 1331 /** 1332 * Find the age between husband and wife. 1333 * 1334 * @param int $total 1335 * 1336 * @return string 1337 */ 1338 public function ageBetweenSpousesMFList(int $total = 10): string 1339 { 1340 $records = $this->ageBetweenSpousesQuery('DESC', $total); 1341 1342 return view('statistics/families/top10-list-spouses', [ 1343 'records' => $records, 1344 ]); 1345 } 1346 1347 /** 1348 * Find the age between wife and husband.. 1349 * 1350 * @param int $total 1351 * 1352 * @return string 1353 */ 1354 public function ageBetweenSpousesFM(int $total = 10): string 1355 { 1356 $records = $this->ageBetweenSpousesQuery('ASC', $total); 1357 1358 return view('statistics/families/top10-nolist-spouses', [ 1359 'records' => $records, 1360 ]); 1361 } 1362 1363 /** 1364 * Find the age between wife and husband.. 1365 * 1366 * @param int $total 1367 * 1368 * @return string 1369 */ 1370 public function ageBetweenSpousesFMList(int $total = 10): string 1371 { 1372 $records = $this->ageBetweenSpousesQuery('ASC', $total); 1373 1374 return view('statistics/families/top10-list-spouses', [ 1375 'records' => $records, 1376 ]); 1377 } 1378 1379 /** 1380 * General query on ages at marriage. 1381 * 1382 * @param string $sex "M" or "F" 1383 * @param int $year1 1384 * @param int $year2 1385 * 1386 * @return array<stdClass> 1387 */ 1388 public function statsMarrAgeQuery(string $sex, int $year1 = -1, int $year2 = -1): array 1389 { 1390 $prefix = DB::connection()->getTablePrefix(); 1391 1392 $query = DB::table('dates AS married') 1393 ->join('families', static function (JoinClause $join): void { 1394 $join 1395 ->on('f_file', '=', 'married.d_file') 1396 ->on('f_id', '=', 'married.d_gid'); 1397 }) 1398 ->join('dates AS birth', static function (JoinClause $join) use ($sex): void { 1399 $join 1400 ->on('birth.d_file', '=', 'married.d_file') 1401 ->on('birth.d_gid', '=', $sex === 'M' ? 'f_husb' : 'f_wife') 1402 ->where('birth.d_julianday1', '<>', 0) 1403 ->where('birth.d_fact', '=', 'BIRT') 1404 ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']); 1405 }) 1406 ->where('married.d_file', '=', $this->tree->id()) 1407 ->where('married.d_fact', '=', 'MARR') 1408 ->whereIn('married.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']) 1409 ->whereColumn('married.d_julianday1', '>', 'birth.d_julianday1') 1410 ->select(['f_id', 'birth.d_gid', new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age')]); 1411 1412 if ($year1 >= 0 && $year2 >= 0) { 1413 $query->whereBetween('married.d_year', [$year1, $year2]); 1414 } 1415 1416 return $query 1417 ->get() 1418 ->map(static function (stdClass $row): stdClass { 1419 $row->age = (int) $row->age; 1420 1421 return $row; 1422 }) 1423 ->all(); 1424 } 1425 1426 /** 1427 * General query on marriage ages. 1428 * 1429 * @return string 1430 */ 1431 public function statsMarrAge(): string 1432 { 1433 return (new ChartMarriageAge($this->tree)) 1434 ->chartMarriageAge(); 1435 } 1436 1437 /** 1438 * Query the database for marriage tags. 1439 * 1440 * @param string $type "full", "name" or "age" 1441 * @param string $age_dir "ASC" or "DESC" 1442 * @param string $sex "F" or "M" 1443 * @param bool $show_years 1444 * 1445 * @return string 1446 */ 1447 private function marriageQuery(string $type, string $age_dir, string $sex, bool $show_years): string 1448 { 1449 if ($sex === 'F') { 1450 $sex_field = 'f_wife'; 1451 } else { 1452 $sex_field = 'f_husb'; 1453 } 1454 1455 if ($age_dir !== 'ASC') { 1456 $age_dir = 'DESC'; 1457 } 1458 1459 $prefix = DB::connection()->getTablePrefix(); 1460 1461 $row = DB::table('families') 1462 ->join('dates AS married', static function (JoinClause $join): void { 1463 $join 1464 ->on('married.d_file', '=', 'f_file') 1465 ->on('married.d_gid', '=', 'f_id') 1466 ->where('married.d_fact', '=', 'MARR'); 1467 }) 1468 ->join('individuals', static function (JoinClause $join) use ($sex, $sex_field): void { 1469 $join 1470 ->on('i_file', '=', 'f_file') 1471 ->on('i_id', '=', $sex_field) 1472 ->where('i_sex', '=', $sex); 1473 }) 1474 ->join('dates AS birth', static function (JoinClause $join): void { 1475 $join 1476 ->on('birth.d_file', '=', 'i_file') 1477 ->on('birth.d_gid', '=', 'i_id') 1478 ->where('birth.d_fact', '=', 'BIRT') 1479 ->where('birth.d_julianday1', '<>', 0); 1480 }) 1481 ->where('f_file', '=', $this->tree->id()) 1482 ->where('married.d_julianday2', '>', 'birth.d_julianday1') 1483 ->orderBy(new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1'), $age_dir) 1484 ->select(['f_id AS famid', $sex_field, new Expression($prefix . 'married.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS age'), 'i_id']) 1485 ->take(1) 1486 ->get() 1487 ->first(); 1488 1489 if ($row === null) { 1490 return ''; 1491 } 1492 1493 $family = Registry::familyFactory()->make($row->famid, $this->tree); 1494 $person = Registry::individualFactory()->make($row->i_id, $this->tree); 1495 1496 switch ($type) { 1497 default: 1498 case 'full': 1499 if ($family && $family->canShow()) { 1500 $result = $family->formatList(); 1501 } else { 1502 $result = I18N::translate('This information is private and cannot be shown.'); 1503 } 1504 break; 1505 1506 case 'name': 1507 $result = '<a href="' . e($family->url()) . '">' . $person->fullName() . '</a>'; 1508 break; 1509 1510 case 'age': 1511 $age = $row->age; 1512 1513 if ($show_years) { 1514 $result = $this->calculateAge((int) $row->age); 1515 } else { 1516 $result = I18N::number((int) ($age / 365.25)); 1517 } 1518 1519 break; 1520 } 1521 1522 return $result; 1523 } 1524 1525 /** 1526 * Find the youngest wife. 1527 * 1528 * @return string 1529 */ 1530 public function youngestMarriageFemale(): string 1531 { 1532 return $this->marriageQuery('full', 'ASC', 'F', false); 1533 } 1534 1535 /** 1536 * Find the name of the youngest wife. 1537 * 1538 * @return string 1539 */ 1540 public function youngestMarriageFemaleName(): string 1541 { 1542 return $this->marriageQuery('name', 'ASC', 'F', false); 1543 } 1544 1545 /** 1546 * Find the age of the youngest wife. 1547 * 1548 * @param string $show_years 1549 * 1550 * @return string 1551 */ 1552 public function youngestMarriageFemaleAge(string $show_years = ''): string 1553 { 1554 return $this->marriageQuery('age', 'ASC', 'F', (bool) $show_years); 1555 } 1556 1557 /** 1558 * Find the oldest wife. 1559 * 1560 * @return string 1561 */ 1562 public function oldestMarriageFemale(): string 1563 { 1564 return $this->marriageQuery('full', 'DESC', 'F', false); 1565 } 1566 1567 /** 1568 * Find the name of the oldest wife. 1569 * 1570 * @return string 1571 */ 1572 public function oldestMarriageFemaleName(): string 1573 { 1574 return $this->marriageQuery('name', 'DESC', 'F', false); 1575 } 1576 1577 /** 1578 * Find the age of the oldest wife. 1579 * 1580 * @param string $show_years 1581 * 1582 * @return string 1583 */ 1584 public function oldestMarriageFemaleAge(string $show_years = ''): string 1585 { 1586 return $this->marriageQuery('age', 'DESC', 'F', (bool) $show_years); 1587 } 1588 1589 /** 1590 * Find the youngest husband. 1591 * 1592 * @return string 1593 */ 1594 public function youngestMarriageMale(): string 1595 { 1596 return $this->marriageQuery('full', 'ASC', 'M', false); 1597 } 1598 1599 /** 1600 * Find the name of the youngest husband. 1601 * 1602 * @return string 1603 */ 1604 public function youngestMarriageMaleName(): string 1605 { 1606 return $this->marriageQuery('name', 'ASC', 'M', false); 1607 } 1608 1609 /** 1610 * Find the age of the youngest husband. 1611 * 1612 * @param string $show_years 1613 * 1614 * @return string 1615 */ 1616 public function youngestMarriageMaleAge(string $show_years = ''): string 1617 { 1618 return $this->marriageQuery('age', 'ASC', 'M', (bool) $show_years); 1619 } 1620 1621 /** 1622 * Find the oldest husband. 1623 * 1624 * @return string 1625 */ 1626 public function oldestMarriageMale(): string 1627 { 1628 return $this->marriageQuery('full', 'DESC', 'M', false); 1629 } 1630 1631 /** 1632 * Find the name of the oldest husband. 1633 * 1634 * @return string 1635 */ 1636 public function oldestMarriageMaleName(): string 1637 { 1638 return $this->marriageQuery('name', 'DESC', 'M', false); 1639 } 1640 1641 /** 1642 * Find the age of the oldest husband. 1643 * 1644 * @param string $show_years 1645 * 1646 * @return string 1647 */ 1648 public function oldestMarriageMaleAge(string $show_years = ''): string 1649 { 1650 return $this->marriageQuery('age', 'DESC', 'M', (bool) $show_years); 1651 } 1652 1653 /** 1654 * General query on marriages. 1655 * 1656 * @param int $year1 1657 * @param int $year2 1658 * 1659 * @return Builder 1660 */ 1661 public function statsMarriageQuery(int $year1 = -1, int $year2 = -1): Builder 1662 { 1663 $query = DB::table('dates') 1664 ->where('d_file', '=', $this->tree->id()) 1665 ->where('d_fact', '=', 'MARR') 1666 ->select(['d_month', new Expression('COUNT(*) AS total')]) 1667 ->groupBy(['d_month']); 1668 1669 if ($year1 >= 0 && $year2 >= 0) { 1670 $query->whereBetween('d_year', [$year1, $year2]); 1671 } 1672 1673 return $query; 1674 } 1675 1676 /** 1677 * General query on marriages. 1678 * 1679 * @param int $year1 1680 * @param int $year2 1681 * 1682 * @return Builder 1683 */ 1684 public function statsFirstMarriageQuery(int $year1 = -1, int $year2 = -1): Builder 1685 { 1686 $query = DB::table('families') 1687 ->join('dates', static function (JoinClause $join): void { 1688 $join 1689 ->on('d_gid', '=', 'f_id') 1690 ->on('d_file', '=', 'f_file') 1691 ->where('d_fact', '=', 'MARR') 1692 ->whereIn('d_month', ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']) 1693 ->where('d_julianday2', '<>', 0); 1694 }) 1695 ->where('f_file', '=', $this->tree->id()); 1696 1697 if ($year1 >= 0 && $year2 >= 0) { 1698 $query->whereBetween('d_year', [$year1, $year2]); 1699 } 1700 1701 return $query 1702 ->select(['f_husb', 'f_wife', 'd_month AS month']) 1703 ->orderBy('d_julianday2'); 1704 } 1705 1706 /** 1707 * General query on marriages. 1708 * 1709 * @param string|null $color_from 1710 * @param string|null $color_to 1711 * 1712 * @return string 1713 */ 1714 public function statsMarr(string $color_from = null, string $color_to = null): string 1715 { 1716 return (new ChartMarriage($this->tree)) 1717 ->chartMarriage($color_from, $color_to); 1718 } 1719 1720 /** 1721 * General divorce query. 1722 * 1723 * @param string|null $color_from 1724 * @param string|null $color_to 1725 * 1726 * @return string 1727 */ 1728 public function statsDiv(string $color_from = null, string $color_to = null): string 1729 { 1730 return (new ChartDivorce($this->tree)) 1731 ->chartDivorce($color_from, $color_to); 1732 } 1733} 1734