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; 24use Fisharebest\Webtrees\Tree; 25use InvalidArgumentException; 26 27/** 28 * Classes for Gedcom Date/Calendar functionality. 29 * 30 * CalendarDate is a base class for classes such as GregorianDate, etc. 31 * + All supported calendars have non-zero days/months/years. 32 * + We store dates as both Y/M/D and Julian Days. 33 * + For imprecise dates such as "JAN 2000" we store the start/end julian day. 34 * 35 * NOTE: Since different calendars start their days at different times, (civil 36 * midnight, solar midnight, sunset, sunrise, etc.), we convert on the basis of 37 * midday. 38 */ 39abstract class AbstractCalendarDate 40{ 41 // GEDCOM calendar escape 42 const ESCAPE = '@#DUNKNOWN@'; 43 44 // Convert GEDCOM month names to month numbers. 45 const MONTH_ABBREVIATIONS = []; 46 47 /** @var CalendarInterface The calendar system used to represent this date */ 48 protected $calendar; 49 50 /** @var int Year number */ 51 public $year; 52 53 /** @var int Month number */ 54 public $month; 55 56 /** @var int Day number */ 57 public $day; 58 59 /** @var int Earliest Julian day number (start of month/year for imprecise dates) */ 60 private $minimum_julian_day; 61 62 /** @var int Latest Julian day number (end of month/year for imprecise dates) */ 63 private $maximum_julian_day; 64 65 /** 66 * Create a date from either: 67 * a Julian day number 68 * day/month/year strings from a GEDCOM date 69 * another CalendarDate object 70 * 71 * @param array|int|AbstractCalendarDate $date 72 */ 73 protected function __construct($date) 74 { 75 // Construct from an integer (a julian day number) 76 if (is_int($date)) { 77 $this->minimum_julian_day = $date; 78 $this->maximum_julian_day = $date; 79 [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($date); 80 81 return; 82 } 83 84 // Construct from an array (of three gedcom-style strings: "1900", "FEB", "4") 85 if (is_array($date)) { 86 $this->day = (int) $date[2]; 87 if (array_key_exists($date[1], static::MONTH_ABBREVIATIONS)) { 88 $this->month = static::MONTH_ABBREVIATIONS[$date[1]]; 89 } else { 90 $this->month = 0; 91 $this->day = 0; 92 } 93 $this->year = $this->extractYear($date[0]); 94 95 // Our simple lookup table above does not take into account Adar and leap-years. 96 if ($this->month === 6 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) { 97 $this->month = 7; 98 } 99 100 $this->setJdFromYmd(); 101 102 return; 103 } 104 105 // Contruct from a CalendarDate 106 $this->minimum_julian_day = $date->minimum_julian_day; 107 $this->maximum_julian_day = $date->maximum_julian_day; 108 109 // Construct from an equivalent xxxxDate object 110 if (get_class($this) == get_class($date)) { 111 $this->year = $date->year; 112 $this->month = $date->month; 113 $this->day = $date->day; 114 115 return; 116 } 117 118 // Not all dates can be converted 119 if (!$this->inValidRange()) { 120 $this->year = 0; 121 $this->month = 0; 122 $this->day = 0; 123 124 return; 125 } 126 127 // ...else construct an inequivalent xxxxDate object 128 if ($date->year == 0) { 129 // Incomplete date - convert on basis of anniversary in current year 130 $today = $date->calendar->jdToYmd(unixtojd()); 131 $jd = $date->calendar->ymdToJd($today[0], $date->month, $date->day == 0 ? $today[2] : $date->day); 132 } else { 133 // Complete date 134 $jd = intdiv($date->maximum_julian_day + $date->minimum_julian_day, 2); 135 } 136 [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($jd); 137 // New date has same precision as original date 138 if ($date->year == 0) { 139 $this->year = 0; 140 } 141 if ($date->month == 0) { 142 $this->month = 0; 143 } 144 if ($date->day == 0) { 145 $this->day = 0; 146 } 147 $this->setJdFromYmd(); 148 } 149 150 /** 151 * @return CalendarInterface 152 */ 153 public function calendar(): CalendarInterface 154 { 155 return $this->calendar(); 156 } 157 158 /** 159 * @return int 160 */ 161 public function maximumJulianDay(): int 162 { 163 return $this->maximum_julian_day; 164 } 165 166 /** 167 * @return int 168 */ 169 public function year(): int 170 { 171 return $this->year; 172 } 173 174 /** 175 * @return int 176 */ 177 public function month(): int 178 { 179 return $this->month; 180 } 181 182 /** 183 * @return int 184 */ 185 public function day(): int 186 { 187 return $this->day; 188 } 189 190 /** 191 * @return int 192 */ 193 public function minimumJulianDay(): int 194 { 195 return $this->minimum_julian_day; 196 } 197 198 /** 199 * Is the current year a leap year? 200 * 201 * @return bool 202 */ 203 public function isLeapYear(): bool 204 { 205 return $this->calendar->isLeapYear($this->year); 206 } 207 208 /** 209 * Set the object’s Julian day number from a potentially incomplete year/month/day 210 * 211 * @return void 212 */ 213 public function setJdFromYmd() 214 { 215 if ($this->year == 0) { 216 $this->minimum_julian_day = 0; 217 $this->maximum_julian_day = 0; 218 } elseif ($this->month == 0) { 219 $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, 1, 1); 220 $this->maximum_julian_day = $this->calendar->ymdToJd($this->nextYear($this->year), 1, 1) - 1; 221 } elseif ($this->day == 0) { 222 [$ny, $nm] = $this->nextMonth(); 223 $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, 1); 224 $this->maximum_julian_day = $this->calendar->ymdToJd($ny, $nm, 1) - 1; 225 } else { 226 $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, $this->day); 227 $this->maximum_julian_day = $this->minimum_julian_day; 228 } 229 } 230 231 /** 232 * Full day of the week 233 * 234 * @param int $day_number 235 * 236 * @return string 237 */ 238 public function dayNames(int $day_number): string 239 { 240 static $translated_day_names; 241 242 if ($translated_day_names === null) { 243 $translated_day_names = [ 244 0 => I18N::translate('Monday'), 245 1 => I18N::translate('Tuesday'), 246 2 => I18N::translate('Wednesday'), 247 3 => I18N::translate('Thursday'), 248 4 => I18N::translate('Friday'), 249 5 => I18N::translate('Saturday'), 250 6 => I18N::translate('Sunday'), 251 ]; 252 } 253 254 return $translated_day_names[$day_number]; 255 } 256 257 /** 258 * Abbreviated day of the week 259 * 260 * @param int $day_number 261 * 262 * @return string 263 */ 264 protected function dayNamesAbbreviated(int $day_number): string 265 { 266 static $translated_day_names; 267 268 if ($translated_day_names === null) { 269 $translated_day_names = [ 270 /* I18N: abbreviation for Monday */ 271 0 => I18N::translate('Mon'), 272 /* I18N: abbreviation for Tuesday */ 273 1 => I18N::translate('Tue'), 274 /* I18N: abbreviation for Wednesday */ 275 2 => I18N::translate('Wed'), 276 /* I18N: abbreviation for Thursday */ 277 3 => I18N::translate('Thu'), 278 /* I18N: abbreviation for Friday */ 279 4 => I18N::translate('Fri'), 280 /* I18N: abbreviation for Saturday */ 281 5 => I18N::translate('Sat'), 282 /* I18N: abbreviation for Sunday */ 283 6 => I18N::translate('Sun'), 284 ]; 285 } 286 287 return $translated_day_names[$day_number]; 288 } 289 290 /** 291 * Most years are 1 more than the previous, but not always (e.g. 1BC->1AD) 292 * 293 * @param int $year 294 * 295 * @return int 296 */ 297 protected function nextYear(int $year): int 298 { 299 return $year + 1; 300 } 301 302 /** 303 * Calendars that use suffixes, etc. (e.g. “B.C.”) or OS/NS notation should redefine this. 304 * 305 * @param string $year 306 * 307 * @return int 308 */ 309 protected function extractYear(string $year): int 310 { 311 return (int) $year; 312 } 313 314 /** 315 * Compare two dates, for sorting 316 * 317 * @param AbstractCalendarDate $d1 318 * @param AbstractCalendarDate $d2 319 * 320 * @return int 321 */ 322 public static function compare(AbstractCalendarDate $d1, AbstractCalendarDate $d2): int 323 { 324 if ($d1->maximum_julian_day < $d2->minimum_julian_day) { 325 return -1; 326 } 327 328 if ($d2->maximum_julian_day < $d1->minimum_julian_day) { 329 return 1; 330 } 331 332 return 0; 333 } 334 335 /** 336 * Calculate the years/months/days between this date and another date. 337 * Results assume you add the days first, then the months. 338 * 4 February -> 3 July is 27 days (3 March) and 4 months. 339 * It is not 4 months (4 June) and 29 days. 340 * 341 * @param AbstractCalendarDate $date 342 * 343 * @return int[] Age in years/months/days 344 */ 345 public function ageDifference(AbstractCalendarDate $date): array 346 { 347 // Incomplete dates 348 if ($this->year === 0 || $date->year === 0) { 349 return [-1, -1, -1]; 350 } 351 352 // Overlapping dates 353 if (self::compare($this, $date) === 0) { 354 return [0, 0, 0]; 355 } 356 357 // Perform all calculations using the calendar of the first date 358 [$year1, $month1, $day1] = $this->calendar->jdToYmd($this->minimum_julian_day); 359 [$year2, $month2, $day2] = $this->calendar->jdToYmd($date->minimum_julian_day); 360 361 $years = $year2 - $year1; 362 $months = $month2 - $month1; 363 $days = $day2 - $day1; 364 365 if ($days < 0) { 366 $days += $this->calendar->daysInMonth($year1, $month1); 367 $months--; 368 } 369 370 if ($months < 0) { 371 $months += $this->calendar->monthsInYear($year2); 372 $years--; 373 } 374 375 return [$years, $months, $days]; 376 } 377 378 /** 379 * How long between an event and a given julian day 380 * Return result as a number of years. 381 * 382 * @param int $jd date for calculation 383 * 384 * @return int 385 */ 386 public function getAge(int $jd): int 387 { 388 if ($this->year == 0 || $jd == 0) { 389 return 0; 390 } 391 if ($this->minimum_julian_day < $jd && $this->maximum_julian_day > $jd) { 392 return 0; 393 } 394 if ($this->minimum_julian_day == $jd) { 395 return 0; 396 } 397 [$y, $m, $d] = $this->calendar->jdToYmd($jd); 398 $dy = $y - $this->year; 399 $dm = $m - max($this->month, 1); 400 $dd = $d - max($this->day, 1); 401 if ($dd < 0) { 402 $dm--; 403 } 404 if ($dm < 0) { 405 $dy--; 406 } 407 408 // Not a full age? Then just the years 409 return $dy; 410 } 411 412 /** 413 * How long between an event and a given julian day 414 * Return result as a gedcom-style age string. 415 * 416 * @param int $jd date for calculation 417 * 418 * @return string 419 */ 420 public function getAgeFull(int $jd): string 421 { 422 if ($this->year == 0 || $jd == 0) { 423 return ''; 424 } 425 if ($this->minimum_julian_day < $jd && $this->maximum_julian_day > $jd) { 426 return ''; 427 } 428 if ($this->minimum_julian_day == $jd) { 429 return ''; 430 } 431 if ($jd < $this->minimum_julian_day) { 432 return '<i class="icon-warning"></i>'; 433 } 434 [$y, $m, $d] = $this->calendar->jdToYmd($jd); 435 $dy = $y - $this->year; 436 $dm = $m - max($this->month, 1); 437 $dd = $d - max($this->day, 1); 438 if ($dd < 0) { 439 $dm--; 440 } 441 if ($dm < 0) { 442 $dm += $this->calendar->monthsInYear(); 443 $dy--; 444 } 445 // Age in years? 446 if ($dy > 1) { 447 return $dy . 'y'; 448 } 449 $dm += $dy * $this->calendar->monthsInYear(); 450 // Age in months? 451 if ($dm > 1) { 452 return $dm . 'm'; 453 } 454 455 // Age in days? 456 return ($jd - $this->minimum_julian_day) . 'd'; 457 } 458 459 /** 460 * Convert a date from one calendar to another. 461 * 462 * @param string $calendar 463 * 464 * @return AbstractCalendarDate 465 */ 466 public function convertToCalendar(string $calendar): AbstractCalendarDate 467 { 468 switch ($calendar) { 469 case 'gregorian': 470 return new GregorianDate($this); 471 case 'julian': 472 return new JulianDate($this); 473 case 'jewish': 474 return new JewishDate($this); 475 case 'french': 476 return new FrenchDate($this); 477 case 'hijri': 478 return new HijriDate($this); 479 case 'jalali': 480 return new JalaliDate($this); 481 default: 482 return $this; 483 } 484 } 485 486 /** 487 * Is this date within the valid range of the calendar 488 * 489 * @return bool 490 */ 491 public function inValidRange(): bool 492 { 493 return $this->minimum_julian_day >= $this->calendar->jdStart() && $this->maximum_julian_day <= $this->calendar->jdEnd(); 494 } 495 496 /** 497 * How many months in a year 498 * 499 * @return int 500 */ 501 public function monthsInYear(): int 502 { 503 return $this->calendar->monthsInYear(); 504 } 505 506 /** 507 * How many days in the current month 508 * 509 * @return int 510 */ 511 public function daysInMonth(): int 512 { 513 try { 514 return $this->calendar->daysInMonth($this->year, $this->month); 515 } catch (InvalidArgumentException $ex) { 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 * @param Tree $tree 965 * 966 * @return string 967 */ 968 public function calendarUrl(string $date_format, Tree $tree): string 969 { 970 if (strpbrk($date_format, 'dDj') && $this->day) { 971 // If the format includes a day, and the date also includes a day, then use the day view 972 $view = 'day'; 973 } elseif (strpbrk($date_format, 'FMmn') && $this->month) { 974 // If the format includes a month, and the date also includes a month, then use the month view 975 $view = 'month'; 976 } else { 977 // Use the year view 978 $view = 'year'; 979 } 980 981 return route('calendar', [ 982 'cal' => $this->calendar->gedcomCalendarEscape(), 983 'year' => $this->formatGedcomYear(), 984 'month' => $this->formatGedcomMonth(), 985 'day' => $this->formatGedcomDay(), 986 'view' => $view, 987 'ged' => $tree->name(), 988 ]); 989 } 990} 991