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