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