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