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