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