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