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