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