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