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; 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 const ESCAPE = '@#DUNKNOWN@'; 42 43 // Convert GEDCOM month names to month numbers. 44 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 DebugBar::addThrowable($ex); 516 517 // calendar.php calls this with "DD MMM" dates, for which we cannot calculate 518 // the length of a month. Should we validate this before calling this function? 519 return 0; 520 } 521 } 522 523 /** 524 * How many days in the current week 525 * 526 * @return int 527 */ 528 public function daysInWeek(): int 529 { 530 return $this->calendar->daysInWeek(); 531 } 532 533 /** 534 * Format a date, using similar codes to the PHP date() function. 535 * 536 * @param string $format See http://php.net/date 537 * @param string $qualifier GEDCOM qualifier, so we can choose the right case for the month name. 538 * 539 * @return string 540 */ 541 public function format(string $format, string $qualifier = ''): string 542 { 543 // Don’t show exact details for inexact dates 544 if (!$this->day) { 545 // The comma is for US "M D, Y" dates 546 $format = preg_replace('/%[djlDNSwz][,]?/', '', $format); 547 } 548 if (!$this->month) { 549 $format = str_replace([ 550 '%F', 551 '%m', 552 '%M', 553 '%n', 554 '%t', 555 ], '', $format); 556 } 557 if (!$this->year) { 558 $format = str_replace([ 559 '%t', 560 '%L', 561 '%G', 562 '%y', 563 '%Y', 564 ], '', $format); 565 } 566 // If we’ve trimmed the format, also trim the punctuation 567 if (!$this->day || !$this->month || !$this->year) { 568 $format = trim($format, ',. ;/-'); 569 } 570 if ($this->day && preg_match('/%[djlDNSwz]/', $format)) { 571 // If we have a day-number *and* we are being asked to display it, then genitive 572 $case = 'GENITIVE'; 573 } else { 574 switch ($qualifier) { 575 case 'TO': 576 case 'ABT': 577 case 'FROM': 578 $case = 'GENITIVE'; 579 break; 580 case 'AFT': 581 $case = 'LOCATIVE'; 582 break; 583 case 'BEF': 584 case 'BET': 585 case 'AND': 586 $case = 'INSTRUMENTAL'; 587 break; 588 case '': 589 case 'INT': 590 case 'EST': 591 case 'CAL': 592 default: // There shouldn't be any other options... 593 $case = 'NOMINATIVE'; 594 break; 595 } 596 } 597 // Build up the formatted date, character at a time 598 preg_match_all('/%[^%]/', $format, $matches); 599 foreach ($matches[0] as $match) { 600 switch ($match) { 601 case '%d': 602 $format = str_replace($match, $this->formatDayZeros(), $format); 603 break; 604 case '%j': 605 $format = str_replace($match, $this->formatDay(), $format); 606 break; 607 case '%l': 608 $format = str_replace($match, $this->formatLongWeekday(), $format); 609 break; 610 case '%D': 611 $format = str_replace($match, $this->formatShortWeekday(), $format); 612 break; 613 case '%N': 614 $format = str_replace($match, $this->formatIsoWeekday(), $format); 615 break; 616 case '%w': 617 $format = str_replace($match, $this->formatNumericWeekday(), $format); 618 break; 619 case '%z': 620 $format = str_replace($match, $this->formatDayOfYear(), $format); 621 break; 622 case '%F': 623 $format = str_replace($match, $this->formatLongMonth($case), $format); 624 break; 625 case '%m': 626 $format = str_replace($match, $this->formatMonthZeros(), $format); 627 break; 628 case '%M': 629 $format = str_replace($match, $this->formatShortMonth(), $format); 630 break; 631 case '%n': 632 $format = str_replace($match, $this->formatMonth(), $format); 633 break; 634 case '%t': 635 $format = str_replace($match, (string) $this->daysInMonth(), $format); 636 break; 637 case '%L': 638 $format = str_replace($match, $this->isLeapYear() ? '1' : '0', $format); 639 break; 640 case '%Y': 641 $format = str_replace($match, $this->formatLongYear(), $format); 642 break; 643 case '%y': 644 $format = str_replace($match, $this->formatShortYear(), $format); 645 break; 646 // These 4 extensions are useful for re-formatting gedcom dates. 647 case '%@': 648 $format = str_replace($match, $this->formatGedcomCalendarEscape(), $format); 649 break; 650 case '%A': 651 $format = str_replace($match, $this->formatGedcomDay(), $format); 652 break; 653 case '%O': 654 $format = str_replace($match, $this->formatGedcomMonth(), $format); 655 break; 656 case '%E': 657 $format = str_replace($match, $this->formatGedcomYear(), $format); 658 break; 659 } 660 } 661 662 return $format; 663 } 664 665 /** 666 * Generate the %d format for a date. 667 * 668 * @return string 669 */ 670 protected function formatDayZeros(): string 671 { 672 if ($this->day > 9) { 673 return I18N::digits($this->day); 674 } 675 676 return I18N::digits('0' . $this->day); 677 } 678 679 /** 680 * Generate the %j format for a date. 681 * 682 * @return string 683 */ 684 protected function formatDay(): string 685 { 686 return I18N::digits($this->day); 687 } 688 689 /** 690 * Generate the %l format for a date. 691 * 692 * @return string 693 */ 694 protected function formatLongWeekday(): string 695 { 696 return $this->dayNames($this->minimum_julian_day % $this->calendar->daysInWeek()); 697 } 698 699 /** 700 * Generate the %D format for a date. 701 * 702 * @return string 703 */ 704 protected function formatShortWeekday(): string 705 { 706 return $this->dayNamesAbbreviated($this->minimum_julian_day % $this->calendar->daysInWeek()); 707 } 708 709 /** 710 * Generate the %N format for a date. 711 * 712 * @return string 713 */ 714 protected function formatIsoWeekday(): string 715 { 716 return I18N::digits($this->minimum_julian_day % 7 + 1); 717 } 718 719 /** 720 * Generate the %w format for a date. 721 * 722 * @return string 723 */ 724 protected function formatNumericWeekday(): string 725 { 726 return I18N::digits(($this->minimum_julian_day + 1) % $this->calendar->daysInWeek()); 727 } 728 729 /** 730 * Generate the %z format for a date. 731 * 732 * @return string 733 */ 734 protected function formatDayOfYear(): string 735 { 736 return I18N::digits($this->minimum_julian_day - $this->calendar->ymdToJd($this->year, 1, 1)); 737 } 738 739 /** 740 * Generate the %n format for a date. 741 * 742 * @return string 743 */ 744 protected function formatMonth(): string 745 { 746 return I18N::digits($this->month); 747 } 748 749 /** 750 * Generate the %m format for a date. 751 * 752 * @return string 753 */ 754 protected function formatMonthZeros(): string 755 { 756 if ($this->month > 9) { 757 return I18N::digits($this->month); 758 } 759 760 return I18N::digits('0' . $this->month); 761 } 762 763 /** 764 * Generate the %F format for a date. 765 * 766 * @param string $case Which grammatical case shall we use 767 * 768 * @return string 769 */ 770 protected function formatLongMonth($case = 'NOMINATIVE'): string 771 { 772 switch ($case) { 773 case 'GENITIVE': 774 return $this->monthNameGenitiveCase($this->month, $this->isLeapYear()); 775 case 'NOMINATIVE': 776 return $this->monthNameNominativeCase($this->month, $this->isLeapYear()); 777 case 'LOCATIVE': 778 return $this->monthNameLocativeCase($this->month, $this->isLeapYear()); 779 case 'INSTRUMENTAL': 780 return $this->monthNameInstrumentalCase($this->month, $this->isLeapYear()); 781 default: 782 throw new \InvalidArgumentException($case); 783 } 784 } 785 786 /** 787 * Full month name in genitive case. 788 * 789 * @param int $month 790 * @param bool $leap_year Some calendars use leap months 791 * 792 * @return string 793 */ 794 abstract protected function monthNameGenitiveCase(int $month, bool $leap_year): string; 795 796 /** 797 * Full month name in nominative case. 798 * 799 * @param int $month 800 * @param bool $leap_year Some calendars use leap months 801 * 802 * @return string 803 */ 804 abstract protected function monthNameNominativeCase(int $month, bool $leap_year): string; 805 806 /** 807 * Full month name in locative case. 808 * 809 * @param int $month 810 * @param bool $leap_year Some calendars use leap months 811 * 812 * @return string 813 */ 814 abstract protected function monthNameLocativeCase(int $month, bool $leap_year): string; 815 816 /** 817 * Full month name in instrumental case. 818 * 819 * @param int $month 820 * @param bool $leap_year Some calendars use leap months 821 * 822 * @return string 823 */ 824 abstract protected function monthNameInstrumentalCase(int $month, bool $leap_year): string; 825 826 /** 827 * Abbreviated month name 828 * 829 * @param int $month 830 * @param bool $leap_year Some calendars use leap months 831 * 832 * @return string 833 */ 834 abstract protected function monthNameAbbreviated(int $month, bool $leap_year): string; 835 836 /** 837 * Generate the %M format for a date. 838 * 839 * @return string 840 */ 841 protected function formatShortMonth(): string 842 { 843 return $this->monthNameAbbreviated($this->month, $this->isLeapYear()); 844 } 845 846 /** 847 * Generate the %y format for a date. 848 * NOTE Short year is NOT a 2-digit year. It is for calendars such as hebrew 849 * which have a 3-digit form of 4-digit years. 850 * 851 * @return string 852 */ 853 protected function formatShortYear(): string 854 { 855 return $this->formatLongYear(); 856 } 857 858 /** 859 * Generate the %A format for a date. 860 * 861 * @return string 862 */ 863 protected function formatGedcomDay(): string 864 { 865 if ($this->day == 0) { 866 return ''; 867 } 868 869 return sprintf('%02d', $this->day); 870 } 871 872 /** 873 * Generate the %O format for a date. 874 * 875 * @return string 876 */ 877 protected function formatGedcomMonth(): string 878 { 879 // Our simple lookup table doesn't work correctly for Adar on leap years 880 if ($this->month == 7 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) { 881 return 'ADR'; 882 } 883 884 return array_search($this->month, static::MONTH_ABBREVIATIONS); 885 } 886 887 /** 888 * Generate the %E format for a date. 889 * 890 * @return string 891 */ 892 protected function formatGedcomYear(): string 893 { 894 if ($this->year == 0) { 895 return ''; 896 } 897 898 return sprintf('%04d', $this->year); 899 } 900 901 /** 902 * Generate the %@ format for a calendar escape. 903 * 904 * @return string 905 */ 906 protected function formatGedcomCalendarEscape(): string 907 { 908 return static::ESCAPE; 909 } 910 911 /** 912 * Generate the %Y format for a date. 913 * 914 * @return string 915 */ 916 protected function formatLongYear(): string 917 { 918 return I18N::digits($this->year); 919 } 920 921 /** 922 * Which months follows this one? Calendars with leap-months should provide their own implementation. 923 * 924 * @return int[] 925 */ 926 protected function nextMonth(): array 927 { 928 return [ 929 $this->month === $this->calendar->monthsInYear() ? $this->nextYear($this->year) : $this->year, 930 ($this->month % $this->calendar->monthsInYear()) + 1, 931 ]; 932 } 933 934 /** 935 * Get today’s date in the current calendar. 936 * 937 * @return int[] 938 */ 939 public function todayYmd(): array 940 { 941 return $this->calendar->jdToYmd(unixtojd()); 942 } 943 944 /** 945 * Convert to today’s date. 946 * 947 * @return AbstractCalendarDate 948 */ 949 public function today(): AbstractCalendarDate 950 { 951 $tmp = clone $this; 952 $ymd = $tmp->todayYmd(); 953 $tmp->year = $ymd[0]; 954 $tmp->month = $ymd[1]; 955 $tmp->day = $ymd[2]; 956 $tmp->setJdFromYmd(); 957 958 return $tmp; 959 } 960 961 /** 962 * Create a URL that links this date to the WT calendar 963 * 964 * @param string $date_format 965 * @param Tree $tree 966 * 967 * @return string 968 */ 969 public function calendarUrl(string $date_format, Tree $tree): string 970 { 971 if (strpbrk($date_format, 'dDj') && $this->day) { 972 // If the format includes a day, and the date also includes a day, then use the day view 973 $view = 'day'; 974 } elseif (strpbrk($date_format, 'FMmn') && $this->month) { 975 // If the format includes a month, and the date also includes a month, then use the month view 976 $view = 'month'; 977 } else { 978 // Use the year view 979 $view = 'year'; 980 } 981 982 return route('calendar', [ 983 'cal' => $this->calendar->gedcomCalendarEscape(), 984 'year' => $this->formatGedcomYear(), 985 'month' => $this->formatGedcomMonth(), 986 'day' => $this->formatGedcomDay(), 987 'view' => $view, 988 'ged' => $tree->name(), 989 ]); 990 } 991} 992