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\Date; 21 22use Fisharebest\ExtCalendar\CalendarInterface; 23use Fisharebest\ExtCalendar\JewishCalendar; 24use Fisharebest\Webtrees\Http\RequestHandlers\CalendarPage; 25use Fisharebest\Webtrees\I18N; 26use Fisharebest\Webtrees\Registry; 27use Fisharebest\Webtrees\Tree; 28use InvalidArgumentException; 29 30use function get_class; 31use function intdiv; 32use function is_array; 33use function is_int; 34use function preg_match; 35use function route; 36use function sprintf; 37use function str_contains; 38use function strpbrk; 39use function strtr; 40use function trim; 41 42/** 43 * Classes for Gedcom Date/Calendar functionality. 44 * 45 * CalendarDate is a base class for classes such as GregorianDate, etc. 46 * + All supported calendars have non-zero days/months/years. 47 * + We store dates as both Y/M/D and Julian Days. 48 * + For imprecise dates such as "JAN 2000" we store the start/end julian day. 49 * 50 * NOTE: Since different calendars start their days at different times, (civil 51 * midnight, solar midnight, sunset, sunrise, etc.), we convert on the basis of 52 * midday. 53 */ 54abstract class AbstractCalendarDate 55{ 56 // GEDCOM calendar escape 57 public const ESCAPE = '@#DUNKNOWN@'; 58 59 // Convert GEDCOM month names to month numbers. 60 protected const MONTH_TO_NUMBER = []; 61 protected const NUMBER_TO_MONTH = []; 62 63 protected CalendarInterface $calendar; 64 65 public int $year; 66 67 public int $month; 68 69 public int $day; 70 71 private int $minimum_julian_day; 72 73 private int $maximum_julian_day; 74 75 /** 76 * Create a date from either: 77 * a Julian day number 78 * day/month/year strings from a GEDCOM date 79 * another CalendarDate object 80 * 81 * @param array<string>|int|AbstractCalendarDate $date 82 */ 83 protected function __construct(array|int|AbstractCalendarDate $date) 84 { 85 // Construct from an integer (a julian day number) 86 if (is_int($date)) { 87 $this->minimum_julian_day = $date; 88 $this->maximum_julian_day = $date; 89 [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($date); 90 91 return; 92 } 93 94 // Construct from an array (of three gedcom-style strings: "1900", "FEB", "4") 95 if (is_array($date)) { 96 $this->day = (int) $date[2]; 97 $this->month = static::MONTH_TO_NUMBER[$date[1]] ?? 0; 98 99 if ($this->month === 0) { 100 $this->day = 0; 101 } 102 103 $this->year = $this->extractYear($date[0]); 104 105 // Our simple lookup table above does not take into account Adar and leap-years. 106 if ($this->month === 6 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) { 107 $this->month = 7; 108 } 109 110 $this->setJdFromYmd(); 111 112 return; 113 } 114 115 // Construct from a CalendarDate 116 $this->minimum_julian_day = $date->minimum_julian_day; 117 $this->maximum_julian_day = $date->maximum_julian_day; 118 119 // Construct from an equivalent xxxxDate object 120 if (get_class($this) === get_class($date)) { 121 $this->year = $date->year; 122 $this->month = $date->month; 123 $this->day = $date->day; 124 125 return; 126 } 127 128 // Not all dates can be converted 129 if (!$this->inValidRange()) { 130 $this->year = 0; 131 $this->month = 0; 132 $this->day = 0; 133 134 return; 135 } 136 137 // ...else construct an inequivalent xxxxDate object 138 if ($date->year === 0) { 139 // Incomplete date - convert on basis of anniversary in current year 140 $today = $date->calendar->jdToYmd(Registry::timestampFactory()->now()->julianDay()); 141 $jd = $date->calendar->ymdToJd($today[0], $date->month, $date->day === 0 ? $today[2] : $date->day); 142 } else { 143 // Complete date 144 $jd = intdiv($date->maximum_julian_day + $date->minimum_julian_day, 2); 145 } 146 [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($jd); 147 // New date has same precision as original date 148 if ($date->year === 0) { 149 $this->year = 0; 150 } 151 if ($date->month === 0) { 152 $this->month = 0; 153 } 154 if ($date->day === 0) { 155 $this->day = 0; 156 } 157 $this->setJdFromYmd(); 158 } 159 160 /** 161 * @return int 162 */ 163 public function maximumJulianDay(): int 164 { 165 return $this->maximum_julian_day; 166 } 167 168 /** 169 * @return int 170 */ 171 public function year(): int 172 { 173 return $this->year; 174 } 175 176 /** 177 * @return int 178 */ 179 public function month(): int 180 { 181 return $this->month; 182 } 183 184 /** 185 * @return int 186 */ 187 public function day(): int 188 { 189 return $this->day; 190 } 191 192 /** 193 * @return int 194 */ 195 public function minimumJulianDay(): int 196 { 197 return $this->minimum_julian_day; 198 } 199 200 /** 201 * Is the current year a leap year? 202 * 203 * @return bool 204 */ 205 public function isLeapYear(): bool 206 { 207 return $this->calendar->isLeapYear($this->year); 208 } 209 210 /** 211 * Set the object’s Julian day number from a potentially incomplete year/month/day 212 * 213 * @return void 214 */ 215 public function setJdFromYmd(): void 216 { 217 if ($this->year === 0) { 218 $this->minimum_julian_day = 0; 219 $this->maximum_julian_day = 0; 220 } elseif ($this->month === 0) { 221 $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, 1, 1); 222 $this->maximum_julian_day = $this->calendar->ymdToJd($this->nextYear($this->year), 1, 1) - 1; 223 } elseif ($this->day === 0) { 224 [$ny, $nm] = $this->nextMonth(); 225 $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, 1); 226 $this->maximum_julian_day = $this->calendar->ymdToJd($ny, $nm, 1) - 1; 227 } else { 228 $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, $this->day); 229 $this->maximum_julian_day = $this->minimum_julian_day; 230 } 231 } 232 233 /** 234 * Full day of the week 235 * 236 * @param int $day_number 237 * 238 * @return string 239 */ 240 public function dayNames(int $day_number): string 241 { 242 static $translated_day_names; 243 244 if ($translated_day_names === null) { 245 $translated_day_names = [ 246 0 => I18N::translate('Monday'), 247 1 => I18N::translate('Tuesday'), 248 2 => I18N::translate('Wednesday'), 249 3 => I18N::translate('Thursday'), 250 4 => I18N::translate('Friday'), 251 5 => I18N::translate('Saturday'), 252 6 => I18N::translate('Sunday'), 253 ]; 254 } 255 256 return $translated_day_names[$day_number]; 257 } 258 259 /** 260 * Abbreviated day of the week 261 * 262 * @param int $day_number 263 * 264 * @return string 265 */ 266 protected function dayNamesAbbreviated(int $day_number): string 267 { 268 static $translated_day_names; 269 270 if ($translated_day_names === null) { 271 $translated_day_names = [ 272 /* I18N: abbreviation for Monday */ 273 0 => I18N::translate('Mon'), 274 /* I18N: abbreviation for Tuesday */ 275 1 => I18N::translate('Tue'), 276 /* I18N: abbreviation for Wednesday */ 277 2 => I18N::translate('Wed'), 278 /* I18N: abbreviation for Thursday */ 279 3 => I18N::translate('Thu'), 280 /* I18N: abbreviation for Friday */ 281 4 => I18N::translate('Fri'), 282 /* I18N: abbreviation for Saturday */ 283 5 => I18N::translate('Sat'), 284 /* I18N: abbreviation for Sunday */ 285 6 => I18N::translate('Sun'), 286 ]; 287 } 288 289 return $translated_day_names[$day_number]; 290 } 291 292 /** 293 * Most years are 1 more than the previous, but not always (e.g. 1BC->1AD) 294 * 295 * @param int $year 296 * 297 * @return int 298 */ 299 protected function nextYear(int $year): int 300 { 301 return $year + 1; 302 } 303 304 /** 305 * Calendars that use suffixes, etc. (e.g. “B.C.”) or OS/NS notation should redefine this. 306 * 307 * @param string $year 308 * 309 * @return int 310 */ 311 protected function extractYear(string $year): int 312 { 313 return (int) $year; 314 } 315 316 /** 317 * Compare two dates, for sorting 318 * 319 * @param AbstractCalendarDate $d1 320 * @param AbstractCalendarDate $d2 321 * 322 * @return int 323 */ 324 public static function compare(AbstractCalendarDate $d1, AbstractCalendarDate $d2): int 325 { 326 if ($d1->maximum_julian_day < $d2->minimum_julian_day) { 327 return -1; 328 } 329 330 if ($d2->maximum_julian_day < $d1->minimum_julian_day) { 331 return 1; 332 } 333 334 return 0; 335 } 336 337 /** 338 * Calculate the years/months/days between this date and another date. 339 * Results assume you add the days first, then the months. 340 * 4 February -> 3 July is 27 days (3 March) and 4 months. 341 * It is not 4 months (4 June) and 29 days. 342 * 343 * @param AbstractCalendarDate $date 344 * 345 * @return array<int> Age in years/months/days 346 */ 347 public function ageDifference(AbstractCalendarDate $date): array 348 { 349 // Incomplete dates 350 if ($this->year === 0 || $date->year === 0) { 351 return [-1, -1, -1]; 352 } 353 354 // Overlapping dates 355 if (self::compare($this, $date) === 0) { 356 return [0, 0, 0]; 357 } 358 359 // Perform all calculations using the calendar of the first date 360 [$year1, $month1, $day1] = $this->calendar->jdToYmd($this->minimum_julian_day); 361 [$year2, $month2, $day2] = $this->calendar->jdToYmd($date->minimum_julian_day); 362 363 $years = $year2 - $year1; 364 $months = $month2 - $month1; 365 $days = $day2 - $day1; 366 367 if ($days < 0) { 368 $days += $this->calendar->daysInMonth($year1, $month1); 369 $months--; 370 } 371 372 if ($months < 0) { 373 $months += $this->calendar->monthsInYear($year2); 374 $years--; 375 } 376 377 return [$years, $months, $days]; 378 } 379 380 /** 381 * Convert a date from one calendar to another. 382 * 383 * @param string $calendar 384 * 385 * @return AbstractCalendarDate 386 */ 387 public function convertToCalendar(string $calendar): AbstractCalendarDate 388 { 389 switch ($calendar) { 390 case 'gregorian': 391 return new GregorianDate($this); 392 case 'julian': 393 return new JulianDate($this); 394 case 'jewish': 395 return new JewishDate($this); 396 case 'french': 397 return new FrenchDate($this); 398 case 'hijri': 399 return new HijriDate($this); 400 case 'jalali': 401 return new JalaliDate($this); 402 default: 403 return $this; 404 } 405 } 406 407 /** 408 * Is this date within the valid range of the calendar 409 * 410 * @return bool 411 */ 412 public function inValidRange(): bool 413 { 414 return $this->minimum_julian_day >= $this->calendar->jdStart() && $this->maximum_julian_day <= $this->calendar->jdEnd(); 415 } 416 417 /** 418 * How many months in a year 419 * 420 * @return int 421 */ 422 public function monthsInYear(): int 423 { 424 return $this->calendar->monthsInYear(); 425 } 426 427 /** 428 * How many days in the current month 429 * 430 * @return int 431 */ 432 public function daysInMonth(): int 433 { 434 try { 435 return $this->calendar->daysInMonth($this->year, $this->month); 436 } catch (InvalidArgumentException) { 437 // calendar.php calls this with "DD MMM" dates, for which we cannot calculate 438 // the length of a month. Should we validate this before calling this function? 439 return 0; 440 } 441 } 442 443 /** 444 * How many days in the current week 445 * 446 * @return int 447 */ 448 public function daysInWeek(): int 449 { 450 return $this->calendar->daysInWeek(); 451 } 452 453 /** 454 * Format a date, using similar codes to the PHP date() function. 455 * 456 * @param string $format See https://php.net/date 457 * @param string $qualifier GEDCOM qualifier, so we can choose the right case for the month name. 458 * 459 * @return string 460 */ 461 public function format(string $format, string $qualifier = ''): string 462 { 463 // Dates can include additional punctuation and symbols. e.g. 464 // %F %j, %Y 465 // %Y. %F %d. 466 // %Y年 %n月 %j日 467 // %j. %F %Y 468 // Don’t show exact details or unnecessary punctuation for inexact dates. 469 if ($this->day === 0) { 470 $format = strtr($format, ['%d' => '', '日' => '', '%j,' => '', '%j' => '', '%l' => '', '%D' => '', '%N' => '', '%S' => '', '%w' => '', '%z' => '']); 471 } 472 if ($this->month === 0) { 473 $format = strtr($format, ['%F' => '', '%m' => '', '%M' => '', '月' => '', '%n' => '', '%t' => '']); 474 } 475 if ($this->year === 0) { 476 $format = strtr($format, ['%t' => '', '%L' => '', '%G' => '', '%y' => '', '年' => '', '%Y' => '']); 477 }// 年 %n月%j日 478 $format = trim($format, ',. /-'); 479 480 if ($this->day !== 0 && preg_match('/%[djlDNSwz]/', $format)) { 481 // If we have a day-number *and* we are being asked to display it, then genitive 482 $case = 'GENITIVE'; 483 } else { 484 switch ($qualifier) { 485 case 'TO': 486 case 'ABT': 487 case 'FROM': 488 $case = 'GENITIVE'; 489 break; 490 case 'AFT': 491 $case = 'LOCATIVE'; 492 break; 493 case 'BEF': 494 case 'BET': 495 case 'AND': 496 $case = 'INSTRUMENTAL'; 497 break; 498 case '': 499 case 'INT': 500 case 'EST': 501 case 'CAL': 502 default: // There shouldn't be any other options... 503 $case = 'NOMINATIVE'; 504 break; 505 } 506 } 507 // Build up the formatted date, character at a time 508 if (str_contains($format, '%d')) { 509 $format = strtr($format, ['%d' => $this->formatDayZeros()]); 510 } 511 if (str_contains($format, '%j')) { 512 $format = strtr($format, ['%j' => $this->formatDay()]); 513 } 514 if (str_contains($format, '%l')) { 515 $format = strtr($format, ['%l' => $this->formatLongWeekday()]); 516 } 517 if (str_contains($format, '%D')) { 518 $format = strtr($format, ['%D' => $this->formatShortWeekday()]); 519 } 520 if (str_contains($format, '%N')) { 521 $format = strtr($format, ['%N' => $this->formatIsoWeekday()]); 522 } 523 if (str_contains($format, '%w')) { 524 $format = strtr($format, ['%w' => $this->formatNumericWeekday()]); 525 } 526 if (str_contains($format, '%z')) { 527 $format = strtr($format, ['%z' => $this->formatDayOfYear()]); 528 } 529 if (str_contains($format, '%F')) { 530 $format = strtr($format, ['%F' => $this->formatLongMonth($case)]); 531 } 532 if (str_contains($format, '%m')) { 533 $format = strtr($format, ['%m' => $this->formatMonthZeros()]); 534 } 535 if (str_contains($format, '%M')) { 536 $format = strtr($format, ['%M' => $this->formatShortMonth()]); 537 } 538 if (str_contains($format, '%n')) { 539 $format = strtr($format, ['%n' => $this->formatMonth()]); 540 } 541 if (str_contains($format, '%t')) { 542 $format = strtr($format, ['%t' => (string) $this->daysInMonth()]); 543 } 544 if (str_contains($format, '%L')) { 545 $format = strtr($format, ['%L' => $this->isLeapYear() ? '1' : '0']); 546 } 547 if (str_contains($format, '%Y')) { 548 $format = strtr($format, ['%Y' => $this->formatLongYear()]); 549 } 550 if (str_contains($format, '%y')) { 551 $format = strtr($format, ['%y' => $this->formatShortYear()]); 552 } 553 // These 4 extensions are useful for re-formatting gedcom dates. 554 if (str_contains($format, '%@')) { 555 $format = strtr($format, ['%@' => $this->formatGedcomCalendarEscape()]); 556 } 557 if (str_contains($format, '%A')) { 558 $format = strtr($format, ['%A' => $this->formatGedcomDay()]); 559 } 560 if (str_contains($format, '%O')) { 561 $format = strtr($format, ['%O' => $this->formatGedcomMonth()]); 562 } 563 if (str_contains($format, '%E')) { 564 $format = strtr($format, ['%E' => $this->formatGedcomYear()]); 565 } 566 567 return $format; 568 } 569 570 /** 571 * Generate the %d format for a date. 572 * 573 * @return string 574 */ 575 protected function formatDayZeros(): string 576 { 577 if ($this->day > 9) { 578 return I18N::digits($this->day); 579 } 580 581 return I18N::digits('0' . $this->day); 582 } 583 584 /** 585 * Generate the %j format for a date. 586 * 587 * @return string 588 */ 589 protected function formatDay(): string 590 { 591 return I18N::digits($this->day); 592 } 593 594 /** 595 * Generate the %l format for a date. 596 * 597 * @return string 598 */ 599 protected function formatLongWeekday(): string 600 { 601 return $this->dayNames($this->minimum_julian_day % $this->calendar->daysInWeek()); 602 } 603 604 /** 605 * Generate the %D format for a date. 606 * 607 * @return string 608 */ 609 protected function formatShortWeekday(): string 610 { 611 return $this->dayNamesAbbreviated($this->minimum_julian_day % $this->calendar->daysInWeek()); 612 } 613 614 /** 615 * Generate the %N format for a date. 616 * 617 * @return string 618 */ 619 protected function formatIsoWeekday(): string 620 { 621 return I18N::digits($this->minimum_julian_day % 7 + 1); 622 } 623 624 /** 625 * Generate the %w format for a date. 626 * 627 * @return string 628 */ 629 protected function formatNumericWeekday(): string 630 { 631 return I18N::digits(($this->minimum_julian_day + 1) % $this->calendar->daysInWeek()); 632 } 633 634 /** 635 * Generate the %z format for a date. 636 * 637 * @return string 638 */ 639 protected function formatDayOfYear(): string 640 { 641 return I18N::digits($this->minimum_julian_day - $this->calendar->ymdToJd($this->year, 1, 1)); 642 } 643 644 /** 645 * Generate the %n format for a date. 646 * 647 * @return string 648 */ 649 protected function formatMonth(): string 650 { 651 return I18N::digits($this->month); 652 } 653 654 /** 655 * Generate the %m format for a date. 656 * 657 * @return string 658 */ 659 protected function formatMonthZeros(): string 660 { 661 if ($this->month > 9) { 662 return I18N::digits($this->month); 663 } 664 665 return I18N::digits('0' . $this->month); 666 } 667 668 /** 669 * Generate the %F format for a date. 670 * 671 * @param string $case Which grammatical case shall we use 672 * 673 * @return string 674 */ 675 protected function formatLongMonth(string $case = 'NOMINATIVE'): string 676 { 677 switch ($case) { 678 case 'GENITIVE': 679 return $this->monthNameGenitiveCase($this->month, $this->isLeapYear()); 680 case 'NOMINATIVE': 681 return $this->monthNameNominativeCase($this->month, $this->isLeapYear()); 682 case 'LOCATIVE': 683 return $this->monthNameLocativeCase($this->month, $this->isLeapYear()); 684 case 'INSTRUMENTAL': 685 return $this->monthNameInstrumentalCase($this->month, $this->isLeapYear()); 686 default: 687 throw new InvalidArgumentException($case); 688 } 689 } 690 691 /** 692 * Full month name in genitive case. 693 * 694 * @param int $month 695 * @param bool $leap_year Some calendars use leap months 696 * 697 * @return string 698 */ 699 abstract protected function monthNameGenitiveCase(int $month, bool $leap_year): string; 700 701 /** 702 * Full month name in nominative case. 703 * 704 * @param int $month 705 * @param bool $leap_year Some calendars use leap months 706 * 707 * @return string 708 */ 709 abstract protected function monthNameNominativeCase(int $month, bool $leap_year): string; 710 711 /** 712 * Full month name in locative case. 713 * 714 * @param int $month 715 * @param bool $leap_year Some calendars use leap months 716 * 717 * @return string 718 */ 719 abstract protected function monthNameLocativeCase(int $month, bool $leap_year): string; 720 721 /** 722 * Full month name in instrumental case. 723 * 724 * @param int $month 725 * @param bool $leap_year Some calendars use leap months 726 * 727 * @return string 728 */ 729 abstract protected function monthNameInstrumentalCase(int $month, bool $leap_year): string; 730 731 /** 732 * Abbreviated month name 733 * 734 * @param int $month 735 * @param bool $leap_year Some calendars use leap months 736 * 737 * @return string 738 */ 739 abstract protected function monthNameAbbreviated(int $month, bool $leap_year): string; 740 741 /** 742 * Generate the %M format for a date. 743 * 744 * @return string 745 */ 746 protected function formatShortMonth(): string 747 { 748 return $this->monthNameAbbreviated($this->month, $this->isLeapYear()); 749 } 750 751 /** 752 * Generate the %y format for a date. 753 * NOTE Short year is NOT a 2-digit year. It is for calendars such as hebrew 754 * which have a 3-digit form of 4-digit years. 755 * 756 * @return string 757 */ 758 protected function formatShortYear(): string 759 { 760 return $this->formatLongYear(); 761 } 762 763 /** 764 * Generate the %A format for a date. 765 * 766 * @return string 767 */ 768 protected function formatGedcomDay(): string 769 { 770 if ($this->day === 0) { 771 return ''; 772 } 773 774 return sprintf('%02d', $this->day); 775 } 776 777 /** 778 * Generate the %O format for a date. 779 * 780 * @return string 781 */ 782 protected function formatGedcomMonth(): string 783 { 784 // Our simple lookup table doesn't work correctly for Adar on leap years 785 if ($this->month === 7 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) { 786 return 'ADR'; 787 } 788 789 return static::NUMBER_TO_MONTH[$this->month] ?? ''; 790 } 791 792 /** 793 * Generate the %E format for a date. 794 * 795 * @return string 796 */ 797 protected function formatGedcomYear(): string 798 { 799 if ($this->year === 0) { 800 return ''; 801 } 802 803 return sprintf('%04d', $this->year); 804 } 805 806 /** 807 * Generate the %@ format for a calendar escape. 808 * 809 * @return string 810 */ 811 protected function formatGedcomCalendarEscape(): string 812 { 813 return static::ESCAPE; 814 } 815 816 /** 817 * Generate the %Y format for a date. 818 * 819 * @return string 820 */ 821 protected function formatLongYear(): string 822 { 823 return I18N::digits($this->year); 824 } 825 826 /** 827 * Which months follows this one? Calendars with leap-months should provide their own implementation. 828 * 829 * @return array<int> 830 */ 831 protected function nextMonth(): array 832 { 833 return [ 834 $this->month === $this->calendar->monthsInYear() ? $this->nextYear($this->year) : $this->year, 835 $this->month % $this->calendar->monthsInYear() + 1, 836 ]; 837 } 838 839 /** 840 * Get today’s date in the current calendar. 841 * 842 * @return array<int> 843 */ 844 public function todayYmd(): array 845 { 846 return $this->calendar->jdToYmd(Registry::timestampFactory()->now()->julianDay()); 847 } 848 849 /** 850 * Convert to today’s date. 851 * 852 * @return AbstractCalendarDate 853 */ 854 public function today(): AbstractCalendarDate 855 { 856 $tmp = clone $this; 857 $ymd = $tmp->todayYmd(); 858 $tmp->year = $ymd[0]; 859 $tmp->month = $ymd[1]; 860 $tmp->day = $ymd[2]; 861 $tmp->setJdFromYmd(); 862 863 return $tmp; 864 } 865 866 /** 867 * Create a URL that links this date to the WT calendar 868 * 869 * @param string $date_format 870 * @param Tree $tree 871 * 872 * @return string 873 */ 874 public function calendarUrl(string $date_format, Tree $tree): string 875 { 876 if ($this->day !== 0 && strpbrk($date_format, 'dDj')) { 877 // If the format includes a day, and the date also includes a day, then use the day view 878 $view = 'day'; 879 } elseif ($this->month !== 0 && strpbrk($date_format, 'FMmn')) { 880 // If the format includes a month, and the date also includes a month, then use the month view 881 $view = 'month'; 882 } else { 883 // Use the year view 884 $view = 'year'; 885 } 886 887 return route(CalendarPage::class, [ 888 'cal' => $this->calendar->gedcomCalendarEscape(), 889 'year' => $this->formatGedcomYear(), 890 'month' => $this->formatGedcomMonth(), 891 'day' => $this->formatGedcomDay(), 892 'view' => $view, 893 'tree' => $tree->name(), 894 ]); 895 } 896} 897