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