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