14a83f5d7SGreg Roach<?php 23976b470SGreg Roach 34a83f5d7SGreg Roach/** 44a83f5d7SGreg Roach * webtrees: online genealogy 58fcd0d32SGreg Roach * Copyright (C) 2019 webtrees development team 64a83f5d7SGreg Roach * This program is free software: you can redistribute it and/or modify 74a83f5d7SGreg Roach * it under the terms of the GNU General Public License as published by 84a83f5d7SGreg Roach * the Free Software Foundation, either version 3 of the License, or 94a83f5d7SGreg Roach * (at your option) any later version. 104a83f5d7SGreg Roach * This program is distributed in the hope that it will be useful, 114a83f5d7SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 124a83f5d7SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 134a83f5d7SGreg Roach * GNU General Public License for more details. 144a83f5d7SGreg Roach * You should have received a copy of the GNU General Public License 154a83f5d7SGreg Roach * along with this program. If not, see <http://www.gnu.org/licenses/>. 164a83f5d7SGreg Roach */ 17fcfa147eSGreg Roach 184a83f5d7SGreg Roachdeclare(strict_types=1); 194a83f5d7SGreg Roach 204a83f5d7SGreg Roachnamespace Fisharebest\Webtrees\Date; 214a83f5d7SGreg Roach 224a83f5d7SGreg Roachuse Fisharebest\ExtCalendar\CalendarInterface; 234a83f5d7SGreg Roachuse Fisharebest\ExtCalendar\JewishCalendar; 244459dc9aSGreg Roachuse Fisharebest\Webtrees\Carbon; 254a83f5d7SGreg Roachuse Fisharebest\Webtrees\I18N; 2649d5f1d7SGreg Roachuse Fisharebest\Webtrees\Tree; 2791495569SGreg Roachuse InvalidArgumentException; 284a83f5d7SGreg Roach 291934fe30SGreg Roachuse function array_key_exists; 301934fe30SGreg Roachuse function array_search; 311934fe30SGreg Roachuse function get_class; 321934fe30SGreg Roachuse function intdiv; 331934fe30SGreg Roachuse function is_array; 341934fe30SGreg Roachuse function is_int; 351934fe30SGreg Roachuse function max; 361934fe30SGreg Roachuse function preg_match; 371934fe30SGreg Roachuse function preg_match_all; 381934fe30SGreg Roachuse function preg_replace; 391934fe30SGreg Roachuse function route; 401934fe30SGreg Roachuse function sprintf; 411934fe30SGreg Roachuse function str_replace; 421934fe30SGreg Roachuse function strpbrk; 43054771e9SGreg Roachuse function trigger_error; 441934fe30SGreg Roachuse function trim; 451934fe30SGreg Roachuse function view; 461934fe30SGreg Roach 47054771e9SGreg Roachuse const E_USER_DEPRECATED; 48054771e9SGreg Roach 494a83f5d7SGreg Roach/** 504a83f5d7SGreg Roach * Classes for Gedcom Date/Calendar functionality. 514a83f5d7SGreg Roach * 524a83f5d7SGreg Roach * CalendarDate is a base class for classes such as GregorianDate, etc. 534a83f5d7SGreg Roach * + All supported calendars have non-zero days/months/years. 544a83f5d7SGreg Roach * + We store dates as both Y/M/D and Julian Days. 554a83f5d7SGreg Roach * + For imprecise dates such as "JAN 2000" we store the start/end julian day. 564a83f5d7SGreg Roach * 574a83f5d7SGreg Roach * NOTE: Since different calendars start their days at different times, (civil 584a83f5d7SGreg Roach * midnight, solar midnight, sunset, sunrise, etc.), we convert on the basis of 594a83f5d7SGreg Roach * midday. 604a83f5d7SGreg Roach */ 617bb2eb25SGreg Roachabstract class AbstractCalendarDate 624a83f5d7SGreg Roach{ 634a83f5d7SGreg Roach // GEDCOM calendar escape 6416d6367aSGreg Roach public const ESCAPE = '@#DUNKNOWN@'; 654a83f5d7SGreg Roach 664a83f5d7SGreg Roach // Convert GEDCOM month names to month numbers. 6716d6367aSGreg Roach protected const MONTH_ABBREVIATIONS = []; 684a83f5d7SGreg Roach 694a83f5d7SGreg Roach /** @var CalendarInterface The calendar system used to represent this date */ 704a83f5d7SGreg Roach protected $calendar; 714a83f5d7SGreg Roach 724a83f5d7SGreg Roach /** @var int Year number */ 734a83f5d7SGreg Roach public $year; 744a83f5d7SGreg Roach 754a83f5d7SGreg Roach /** @var int Month number */ 764a83f5d7SGreg Roach public $month; 774a83f5d7SGreg Roach 784a83f5d7SGreg Roach /** @var int Day number */ 794a83f5d7SGreg Roach public $day; 804a83f5d7SGreg Roach 814a83f5d7SGreg Roach /** @var int Earliest Julian day number (start of month/year for imprecise dates) */ 824a83f5d7SGreg Roach private $minimum_julian_day; 834a83f5d7SGreg Roach 844a83f5d7SGreg Roach /** @var int Latest Julian day number (end of month/year for imprecise dates) */ 854a83f5d7SGreg Roach private $maximum_julian_day; 864a83f5d7SGreg Roach 874a83f5d7SGreg Roach /** 884a83f5d7SGreg Roach * Create a date from either: 894a83f5d7SGreg Roach * a Julian day number 904a83f5d7SGreg Roach * day/month/year strings from a GEDCOM date 914a83f5d7SGreg Roach * another CalendarDate object 924a83f5d7SGreg Roach * 93*f4c767fdSGreg Roach * @param array<string>|int|AbstractCalendarDate $date 944a83f5d7SGreg Roach */ 954a83f5d7SGreg Roach protected function __construct($date) 964a83f5d7SGreg Roach { 974a83f5d7SGreg Roach // Construct from an integer (a julian day number) 984a83f5d7SGreg Roach if (is_int($date)) { 994a83f5d7SGreg Roach $this->minimum_julian_day = $date; 1004a83f5d7SGreg Roach $this->maximum_julian_day = $date; 10165e02381SGreg Roach [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($date); 1024a83f5d7SGreg Roach 1034a83f5d7SGreg Roach return; 1044a83f5d7SGreg Roach } 1054a83f5d7SGreg Roach 1064a83f5d7SGreg Roach // Construct from an array (of three gedcom-style strings: "1900", "FEB", "4") 1074a83f5d7SGreg Roach if (is_array($date)) { 1084a83f5d7SGreg Roach $this->day = (int) $date[2]; 1094a83f5d7SGreg Roach if (array_key_exists($date[1], static::MONTH_ABBREVIATIONS)) { 1104a83f5d7SGreg Roach $this->month = static::MONTH_ABBREVIATIONS[$date[1]]; 1114a83f5d7SGreg Roach } else { 1124a83f5d7SGreg Roach $this->month = 0; 1134a83f5d7SGreg Roach $this->day = 0; 1144a83f5d7SGreg Roach } 1154a83f5d7SGreg Roach $this->year = $this->extractYear($date[0]); 1164a83f5d7SGreg Roach 1174a83f5d7SGreg Roach // Our simple lookup table above does not take into account Adar and leap-years. 1184a83f5d7SGreg Roach if ($this->month === 6 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) { 1194a83f5d7SGreg Roach $this->month = 7; 1204a83f5d7SGreg Roach } 1214a83f5d7SGreg Roach 1224a83f5d7SGreg Roach $this->setJdFromYmd(); 1234a83f5d7SGreg Roach 1244a83f5d7SGreg Roach return; 1254a83f5d7SGreg Roach } 1264a83f5d7SGreg Roach 1274a83f5d7SGreg Roach // Contruct from a CalendarDate 1284a83f5d7SGreg Roach $this->minimum_julian_day = $date->minimum_julian_day; 1294a83f5d7SGreg Roach $this->maximum_julian_day = $date->maximum_julian_day; 1304a83f5d7SGreg Roach 1314a83f5d7SGreg Roach // Construct from an equivalent xxxxDate object 132e364afe4SGreg Roach if (get_class($this) === get_class($date)) { 1334a83f5d7SGreg Roach $this->year = $date->year; 1344a83f5d7SGreg Roach $this->month = $date->month; 1354a83f5d7SGreg Roach $this->day = $date->day; 1364a83f5d7SGreg Roach 1374a83f5d7SGreg Roach return; 1384a83f5d7SGreg Roach } 1394a83f5d7SGreg Roach 1404a83f5d7SGreg Roach // Not all dates can be converted 1414a83f5d7SGreg Roach if (!$this->inValidRange()) { 1424a83f5d7SGreg Roach $this->year = 0; 1434a83f5d7SGreg Roach $this->month = 0; 1444a83f5d7SGreg Roach $this->day = 0; 1454a83f5d7SGreg Roach 1464a83f5d7SGreg Roach return; 1474a83f5d7SGreg Roach } 1484a83f5d7SGreg Roach 1494a83f5d7SGreg Roach // ...else construct an inequivalent xxxxDate object 150e364afe4SGreg Roach if ($date->year === 0) { 1514a83f5d7SGreg Roach // Incomplete date - convert on basis of anniversary in current year 1524459dc9aSGreg Roach $today = $date->calendar->jdToYmd(Carbon::now()->julianDay()); 153e364afe4SGreg Roach $jd = $date->calendar->ymdToJd($today[0], $date->month, $date->day === 0 ? $today[2] : $date->day); 1544a83f5d7SGreg Roach } else { 1554a83f5d7SGreg Roach // Complete date 1564a83f5d7SGreg Roach $jd = intdiv($date->maximum_julian_day + $date->minimum_julian_day, 2); 1574a83f5d7SGreg Roach } 15865e02381SGreg Roach [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($jd); 1594a83f5d7SGreg Roach // New date has same precision as original date 160e364afe4SGreg Roach if ($date->year === 0) { 1614a83f5d7SGreg Roach $this->year = 0; 1624a83f5d7SGreg Roach } 163e364afe4SGreg Roach if ($date->month === 0) { 1644a83f5d7SGreg Roach $this->month = 0; 1654a83f5d7SGreg Roach } 166e364afe4SGreg Roach if ($date->day === 0) { 1674a83f5d7SGreg Roach $this->day = 0; 1684a83f5d7SGreg Roach } 1694a83f5d7SGreg Roach $this->setJdFromYmd(); 1704a83f5d7SGreg Roach } 1714a83f5d7SGreg Roach 1724a83f5d7SGreg Roach /** 1734a83f5d7SGreg Roach * @return int 1744a83f5d7SGreg Roach */ 1754a83f5d7SGreg Roach public function maximumJulianDay(): int 1764a83f5d7SGreg Roach { 1774a83f5d7SGreg Roach return $this->maximum_julian_day; 1784a83f5d7SGreg Roach } 1794a83f5d7SGreg Roach 1804a83f5d7SGreg Roach /** 1814a83f5d7SGreg Roach * @return int 1824a83f5d7SGreg Roach */ 1834a83f5d7SGreg Roach public function year(): int 1844a83f5d7SGreg Roach { 1854a83f5d7SGreg Roach return $this->year; 1864a83f5d7SGreg Roach } 1874a83f5d7SGreg Roach 1884a83f5d7SGreg Roach /** 1894a83f5d7SGreg Roach * @return int 1904a83f5d7SGreg Roach */ 1914a83f5d7SGreg Roach public function month(): int 1924a83f5d7SGreg Roach { 1934a83f5d7SGreg Roach return $this->month; 1944a83f5d7SGreg Roach } 1954a83f5d7SGreg Roach 1964a83f5d7SGreg Roach /** 1974a83f5d7SGreg Roach * @return int 1984a83f5d7SGreg Roach */ 1994a83f5d7SGreg Roach public function day(): int 2004a83f5d7SGreg Roach { 2014a83f5d7SGreg Roach return $this->day; 2024a83f5d7SGreg Roach } 2034a83f5d7SGreg Roach 2044a83f5d7SGreg Roach /** 2054a83f5d7SGreg Roach * @return int 2064a83f5d7SGreg Roach */ 2074a83f5d7SGreg Roach public function minimumJulianDay(): int 2084a83f5d7SGreg Roach { 2094a83f5d7SGreg Roach return $this->minimum_julian_day; 2104a83f5d7SGreg Roach } 2114a83f5d7SGreg Roach 2124a83f5d7SGreg Roach /** 2134a83f5d7SGreg Roach * Is the current year a leap year? 2144a83f5d7SGreg Roach * 2154a83f5d7SGreg Roach * @return bool 2164a83f5d7SGreg Roach */ 2174a83f5d7SGreg Roach public function isLeapYear(): bool 2184a83f5d7SGreg Roach { 2194a83f5d7SGreg Roach return $this->calendar->isLeapYear($this->year); 2204a83f5d7SGreg Roach } 2214a83f5d7SGreg Roach 2224a83f5d7SGreg Roach /** 2234a83f5d7SGreg Roach * Set the object’s Julian day number from a potentially incomplete year/month/day 2244a83f5d7SGreg Roach * 2254a83f5d7SGreg Roach * @return void 2264a83f5d7SGreg Roach */ 227e364afe4SGreg Roach public function setJdFromYmd(): void 2284a83f5d7SGreg Roach { 229e364afe4SGreg Roach if ($this->year === 0) { 2304a83f5d7SGreg Roach $this->minimum_julian_day = 0; 2314a83f5d7SGreg Roach $this->maximum_julian_day = 0; 232e364afe4SGreg Roach } elseif ($this->month === 0) { 2334a83f5d7SGreg Roach $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, 1, 1); 2344a83f5d7SGreg Roach $this->maximum_julian_day = $this->calendar->ymdToJd($this->nextYear($this->year), 1, 1) - 1; 235e364afe4SGreg Roach } elseif ($this->day === 0) { 23665e02381SGreg Roach [$ny, $nm] = $this->nextMonth(); 2374a83f5d7SGreg Roach $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, 1); 2384a83f5d7SGreg Roach $this->maximum_julian_day = $this->calendar->ymdToJd($ny, $nm, 1) - 1; 2394a83f5d7SGreg Roach } else { 2404a83f5d7SGreg Roach $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, $this->day); 2414a83f5d7SGreg Roach $this->maximum_julian_day = $this->minimum_julian_day; 2424a83f5d7SGreg Roach } 2434a83f5d7SGreg Roach } 2444a83f5d7SGreg Roach 2454a83f5d7SGreg Roach /** 2464a83f5d7SGreg Roach * Full day of the week 2474a83f5d7SGreg Roach * 2484a83f5d7SGreg Roach * @param int $day_number 2494a83f5d7SGreg Roach * 2504a83f5d7SGreg Roach * @return string 2514a83f5d7SGreg Roach */ 2524a83f5d7SGreg Roach public function dayNames(int $day_number): string 2534a83f5d7SGreg Roach { 2544a83f5d7SGreg Roach static $translated_day_names; 2554a83f5d7SGreg Roach 2564a83f5d7SGreg Roach if ($translated_day_names === null) { 2574a83f5d7SGreg Roach $translated_day_names = [ 2584a83f5d7SGreg Roach 0 => I18N::translate('Monday'), 2594a83f5d7SGreg Roach 1 => I18N::translate('Tuesday'), 2604a83f5d7SGreg Roach 2 => I18N::translate('Wednesday'), 2614a83f5d7SGreg Roach 3 => I18N::translate('Thursday'), 2624a83f5d7SGreg Roach 4 => I18N::translate('Friday'), 2634a83f5d7SGreg Roach 5 => I18N::translate('Saturday'), 2644a83f5d7SGreg Roach 6 => I18N::translate('Sunday'), 2654a83f5d7SGreg Roach ]; 2664a83f5d7SGreg Roach } 2674a83f5d7SGreg Roach 2684a83f5d7SGreg Roach return $translated_day_names[$day_number]; 2694a83f5d7SGreg Roach } 2704a83f5d7SGreg Roach 2714a83f5d7SGreg Roach /** 2724a83f5d7SGreg Roach * Abbreviated day of the week 2734a83f5d7SGreg Roach * 2744a83f5d7SGreg Roach * @param int $day_number 2754a83f5d7SGreg Roach * 2764a83f5d7SGreg Roach * @return string 2774a83f5d7SGreg Roach */ 2784a83f5d7SGreg Roach protected function dayNamesAbbreviated(int $day_number): string 2794a83f5d7SGreg Roach { 2804a83f5d7SGreg Roach static $translated_day_names; 2814a83f5d7SGreg Roach 2824a83f5d7SGreg Roach if ($translated_day_names === null) { 2834a83f5d7SGreg Roach $translated_day_names = [ 2844a83f5d7SGreg Roach /* I18N: abbreviation for Monday */ 2854a83f5d7SGreg Roach 0 => I18N::translate('Mon'), 2864a83f5d7SGreg Roach /* I18N: abbreviation for Tuesday */ 2874a83f5d7SGreg Roach 1 => I18N::translate('Tue'), 2884a83f5d7SGreg Roach /* I18N: abbreviation for Wednesday */ 2894a83f5d7SGreg Roach 2 => I18N::translate('Wed'), 2904a83f5d7SGreg Roach /* I18N: abbreviation for Thursday */ 2914a83f5d7SGreg Roach 3 => I18N::translate('Thu'), 2924a83f5d7SGreg Roach /* I18N: abbreviation for Friday */ 2934a83f5d7SGreg Roach 4 => I18N::translate('Fri'), 2944a83f5d7SGreg Roach /* I18N: abbreviation for Saturday */ 2954a83f5d7SGreg Roach 5 => I18N::translate('Sat'), 2964a83f5d7SGreg Roach /* I18N: abbreviation for Sunday */ 2974a83f5d7SGreg Roach 6 => I18N::translate('Sun'), 2984a83f5d7SGreg Roach ]; 2994a83f5d7SGreg Roach } 3004a83f5d7SGreg Roach 3014a83f5d7SGreg Roach return $translated_day_names[$day_number]; 3024a83f5d7SGreg Roach } 3034a83f5d7SGreg Roach 3044a83f5d7SGreg Roach /** 3054a83f5d7SGreg Roach * Most years are 1 more than the previous, but not always (e.g. 1BC->1AD) 3064a83f5d7SGreg Roach * 3074a83f5d7SGreg Roach * @param int $year 3084a83f5d7SGreg Roach * 3094a83f5d7SGreg Roach * @return int 3104a83f5d7SGreg Roach */ 3114a83f5d7SGreg Roach protected function nextYear(int $year): int 3124a83f5d7SGreg Roach { 3134a83f5d7SGreg Roach return $year + 1; 3144a83f5d7SGreg Roach } 3154a83f5d7SGreg Roach 3164a83f5d7SGreg Roach /** 3174a83f5d7SGreg Roach * Calendars that use suffixes, etc. (e.g. “B.C.”) or OS/NS notation should redefine this. 3184a83f5d7SGreg Roach * 3194a83f5d7SGreg Roach * @param string $year 3204a83f5d7SGreg Roach * 3214a83f5d7SGreg Roach * @return int 3224a83f5d7SGreg Roach */ 3234a83f5d7SGreg Roach protected function extractYear(string $year): int 3244a83f5d7SGreg Roach { 3254a83f5d7SGreg Roach return (int) $year; 3264a83f5d7SGreg Roach } 3274a83f5d7SGreg Roach 3284a83f5d7SGreg Roach /** 3294a83f5d7SGreg Roach * Compare two dates, for sorting 3304a83f5d7SGreg Roach * 3314a83f5d7SGreg Roach * @param AbstractCalendarDate $d1 3324a83f5d7SGreg Roach * @param AbstractCalendarDate $d2 3334a83f5d7SGreg Roach * 3344a83f5d7SGreg Roach * @return int 3354a83f5d7SGreg Roach */ 3364a83f5d7SGreg Roach public static function compare(AbstractCalendarDate $d1, AbstractCalendarDate $d2): int 3374a83f5d7SGreg Roach { 3384a83f5d7SGreg Roach if ($d1->maximum_julian_day < $d2->minimum_julian_day) { 3394a83f5d7SGreg Roach return -1; 3404a83f5d7SGreg Roach } 3414a83f5d7SGreg Roach 3424a83f5d7SGreg Roach if ($d2->maximum_julian_day < $d1->minimum_julian_day) { 3434a83f5d7SGreg Roach return 1; 3444a83f5d7SGreg Roach } 3454a83f5d7SGreg Roach 3464a83f5d7SGreg Roach return 0; 3474a83f5d7SGreg Roach } 3484a83f5d7SGreg Roach 3494a83f5d7SGreg Roach /** 3504a83f5d7SGreg Roach * Calculate the years/months/days between this date and another date. 3514a83f5d7SGreg Roach * Results assume you add the days first, then the months. 3524a83f5d7SGreg Roach * 4 February -> 3 July is 27 days (3 March) and 4 months. 3534a83f5d7SGreg Roach * It is not 4 months (4 June) and 29 days. 3544a83f5d7SGreg Roach * 3554a83f5d7SGreg Roach * @param AbstractCalendarDate $date 3564a83f5d7SGreg Roach * 3574a83f5d7SGreg Roach * @return int[] Age in years/months/days 3584a83f5d7SGreg Roach */ 3594a83f5d7SGreg Roach public function ageDifference(AbstractCalendarDate $date): array 3604a83f5d7SGreg Roach { 3614a83f5d7SGreg Roach // Incomplete dates 3624a83f5d7SGreg Roach if ($this->year === 0 || $date->year === 0) { 3634a83f5d7SGreg Roach return [-1, -1, -1]; 3644a83f5d7SGreg Roach } 3654a83f5d7SGreg Roach 3664a83f5d7SGreg Roach // Overlapping dates 3674a83f5d7SGreg Roach if (self::compare($this, $date) === 0) { 3684a83f5d7SGreg Roach return [0, 0, 0]; 3694a83f5d7SGreg Roach } 3704a83f5d7SGreg Roach 3714a83f5d7SGreg Roach // Perform all calculations using the calendar of the first date 37265e02381SGreg Roach [$year1, $month1, $day1] = $this->calendar->jdToYmd($this->minimum_julian_day); 37365e02381SGreg Roach [$year2, $month2, $day2] = $this->calendar->jdToYmd($date->minimum_julian_day); 3744a83f5d7SGreg Roach 3754a83f5d7SGreg Roach $years = $year2 - $year1; 3764a83f5d7SGreg Roach $months = $month2 - $month1; 3774a83f5d7SGreg Roach $days = $day2 - $day1; 3784a83f5d7SGreg Roach 3794a83f5d7SGreg Roach if ($days < 0) { 3804a83f5d7SGreg Roach $days += $this->calendar->daysInMonth($year1, $month1); 3814a83f5d7SGreg Roach $months--; 3824a83f5d7SGreg Roach } 3834a83f5d7SGreg Roach 3844a83f5d7SGreg Roach if ($months < 0) { 3854a83f5d7SGreg Roach $months += $this->calendar->monthsInYear($year2); 3864a83f5d7SGreg Roach $years--; 3874a83f5d7SGreg Roach } 3884a83f5d7SGreg Roach 3894a83f5d7SGreg Roach return [$years, $months, $days]; 3904a83f5d7SGreg Roach } 3914a83f5d7SGreg Roach 3924a83f5d7SGreg Roach /** 3934a83f5d7SGreg Roach * How long between an event and a given julian day 3944a83f5d7SGreg Roach * Return result as a number of years. 3954a83f5d7SGreg Roach * 3964a83f5d7SGreg Roach * @param int $jd date for calculation 3974a83f5d7SGreg Roach * 3984a83f5d7SGreg Roach * @return int 399054771e9SGreg Roach * 400054771e9SGreg Roach * @deprecated since 2.0.4. Will be removed in 2.1.0 4014a83f5d7SGreg Roach */ 4024a83f5d7SGreg Roach public function getAge(int $jd): int 4034a83f5d7SGreg Roach { 404054771e9SGreg Roach trigger_error('AbstractCalendarDate::getAge() is deprecated. Use class Age instead.', E_USER_DEPRECATED); 405054771e9SGreg Roach 406e364afe4SGreg Roach if ($this->year === 0 || $jd === 0) { 4074a83f5d7SGreg Roach return 0; 4084a83f5d7SGreg Roach } 4094a83f5d7SGreg Roach if ($this->minimum_julian_day < $jd && $this->maximum_julian_day > $jd) { 4104a83f5d7SGreg Roach return 0; 4114a83f5d7SGreg Roach } 412e364afe4SGreg Roach if ($this->minimum_julian_day === $jd) { 4134a83f5d7SGreg Roach return 0; 4144a83f5d7SGreg Roach } 41565e02381SGreg Roach [$y, $m, $d] = $this->calendar->jdToYmd($jd); 4164a83f5d7SGreg Roach $dy = $y - $this->year; 4174a83f5d7SGreg Roach $dm = $m - max($this->month, 1); 4184a83f5d7SGreg Roach $dd = $d - max($this->day, 1); 4194a83f5d7SGreg Roach if ($dd < 0) { 4204a83f5d7SGreg Roach $dm--; 4214a83f5d7SGreg Roach } 4224a83f5d7SGreg Roach if ($dm < 0) { 4234a83f5d7SGreg Roach $dy--; 4244a83f5d7SGreg Roach } 4254a83f5d7SGreg Roach 4264a83f5d7SGreg Roach // Not a full age? Then just the years 4274a83f5d7SGreg Roach return $dy; 4284a83f5d7SGreg Roach } 4294a83f5d7SGreg Roach 4304a83f5d7SGreg Roach /** 4314a83f5d7SGreg Roach * How long between an event and a given julian day 4324a83f5d7SGreg Roach * Return result as a gedcom-style age string. 4334a83f5d7SGreg Roach * 4344a83f5d7SGreg Roach * @param int $jd date for calculation 4354a83f5d7SGreg Roach * 4364a83f5d7SGreg Roach * @return string 437054771e9SGreg Roach * 438054771e9SGreg Roach * @deprecated since 2.0.4. Will be removed in 2.1.0 4394a83f5d7SGreg Roach */ 4404a83f5d7SGreg Roach public function getAgeFull(int $jd): string 4414a83f5d7SGreg Roach { 442054771e9SGreg Roach trigger_error('AbstractCalendarDate::getAge() is deprecated. Use class Age instead.', E_USER_DEPRECATED); 443054771e9SGreg Roach 444e364afe4SGreg Roach if ($this->year === 0 || $jd === 0) { 4454a83f5d7SGreg Roach return ''; 4464a83f5d7SGreg Roach } 4474a83f5d7SGreg Roach if ($this->minimum_julian_day < $jd && $this->maximum_julian_day > $jd) { 4484a83f5d7SGreg Roach return ''; 4494a83f5d7SGreg Roach } 450e364afe4SGreg Roach if ($this->minimum_julian_day === $jd) { 4514a83f5d7SGreg Roach return ''; 4524a83f5d7SGreg Roach } 4534a83f5d7SGreg Roach if ($jd < $this->minimum_julian_day) { 454e39fd5c6SGreg Roach return view('icons/warning'); 4554a83f5d7SGreg Roach } 45665e02381SGreg Roach [$y, $m, $d] = $this->calendar->jdToYmd($jd); 4574a83f5d7SGreg Roach $dy = $y - $this->year; 4584a83f5d7SGreg Roach $dm = $m - max($this->month, 1); 4594a83f5d7SGreg Roach $dd = $d - max($this->day, 1); 4604a83f5d7SGreg Roach if ($dd < 0) { 4614a83f5d7SGreg Roach $dm--; 4624a83f5d7SGreg Roach } 4634a83f5d7SGreg Roach if ($dm < 0) { 4644a83f5d7SGreg Roach $dm += $this->calendar->monthsInYear(); 4654a83f5d7SGreg Roach $dy--; 4664a83f5d7SGreg Roach } 4674a83f5d7SGreg Roach // Age in years? 4684a83f5d7SGreg Roach if ($dy > 1) { 4694a83f5d7SGreg Roach return $dy . 'y'; 4704a83f5d7SGreg Roach } 4714a83f5d7SGreg Roach $dm += $dy * $this->calendar->monthsInYear(); 4724a83f5d7SGreg Roach // Age in months? 4734a83f5d7SGreg Roach if ($dm > 1) { 4744a83f5d7SGreg Roach return $dm . 'm'; 4754a83f5d7SGreg Roach } 4764a83f5d7SGreg Roach 4774a83f5d7SGreg Roach // Age in days? 4784a83f5d7SGreg Roach return ($jd - $this->minimum_julian_day) . 'd'; 4794a83f5d7SGreg Roach } 4804a83f5d7SGreg Roach 4814a83f5d7SGreg Roach /** 4824a83f5d7SGreg Roach * Convert a date from one calendar to another. 4834a83f5d7SGreg Roach * 4844a83f5d7SGreg Roach * @param string $calendar 4854a83f5d7SGreg Roach * 4864a83f5d7SGreg Roach * @return AbstractCalendarDate 4874a83f5d7SGreg Roach */ 4884a83f5d7SGreg Roach public function convertToCalendar(string $calendar): AbstractCalendarDate 4894a83f5d7SGreg Roach { 4904a83f5d7SGreg Roach switch ($calendar) { 4914a83f5d7SGreg Roach case 'gregorian': 4924a83f5d7SGreg Roach return new GregorianDate($this); 4934a83f5d7SGreg Roach case 'julian': 4944a83f5d7SGreg Roach return new JulianDate($this); 4954a83f5d7SGreg Roach case 'jewish': 4964a83f5d7SGreg Roach return new JewishDate($this); 4974a83f5d7SGreg Roach case 'french': 4984a83f5d7SGreg Roach return new FrenchDate($this); 4994a83f5d7SGreg Roach case 'hijri': 5004a83f5d7SGreg Roach return new HijriDate($this); 5014a83f5d7SGreg Roach case 'jalali': 5024a83f5d7SGreg Roach return new JalaliDate($this); 5034a83f5d7SGreg Roach default: 5044a83f5d7SGreg Roach return $this; 5054a83f5d7SGreg Roach } 5064a83f5d7SGreg Roach } 5074a83f5d7SGreg Roach 5084a83f5d7SGreg Roach /** 5094a83f5d7SGreg Roach * Is this date within the valid range of the calendar 5104a83f5d7SGreg Roach * 5114a83f5d7SGreg Roach * @return bool 5124a83f5d7SGreg Roach */ 5134a83f5d7SGreg Roach public function inValidRange(): bool 5144a83f5d7SGreg Roach { 5154a83f5d7SGreg Roach return $this->minimum_julian_day >= $this->calendar->jdStart() && $this->maximum_julian_day <= $this->calendar->jdEnd(); 5164a83f5d7SGreg Roach } 5174a83f5d7SGreg Roach 5184a83f5d7SGreg Roach /** 5194a83f5d7SGreg Roach * How many months in a year 5204a83f5d7SGreg Roach * 5214a83f5d7SGreg Roach * @return int 5224a83f5d7SGreg Roach */ 5234a83f5d7SGreg Roach public function monthsInYear(): int 5244a83f5d7SGreg Roach { 5254a83f5d7SGreg Roach return $this->calendar->monthsInYear(); 5264a83f5d7SGreg Roach } 5274a83f5d7SGreg Roach 5284a83f5d7SGreg Roach /** 5294a83f5d7SGreg Roach * How many days in the current month 5304a83f5d7SGreg Roach * 5314a83f5d7SGreg Roach * @return int 5324a83f5d7SGreg Roach */ 5334a83f5d7SGreg Roach public function daysInMonth(): int 5344a83f5d7SGreg Roach { 5354a83f5d7SGreg Roach try { 5364a83f5d7SGreg Roach return $this->calendar->daysInMonth($this->year, $this->month); 53791495569SGreg Roach } catch (InvalidArgumentException $ex) { 5384a83f5d7SGreg Roach // calendar.php calls this with "DD MMM" dates, for which we cannot calculate 5394a83f5d7SGreg Roach // the length of a month. Should we validate this before calling this function? 5404a83f5d7SGreg Roach return 0; 5414a83f5d7SGreg Roach } 5424a83f5d7SGreg Roach } 5434a83f5d7SGreg Roach 5444a83f5d7SGreg Roach /** 5454a83f5d7SGreg Roach * How many days in the current week 5464a83f5d7SGreg Roach * 5474a83f5d7SGreg Roach * @return int 5484a83f5d7SGreg Roach */ 5494a83f5d7SGreg Roach public function daysInWeek(): int 5504a83f5d7SGreg Roach { 5514a83f5d7SGreg Roach return $this->calendar->daysInWeek(); 5524a83f5d7SGreg Roach } 5534a83f5d7SGreg Roach 5544a83f5d7SGreg Roach /** 5554a83f5d7SGreg Roach * Format a date, using similar codes to the PHP date() function. 5564a83f5d7SGreg Roach * 5574a83f5d7SGreg Roach * @param string $format See http://php.net/date 5584a83f5d7SGreg Roach * @param string $qualifier GEDCOM qualifier, so we can choose the right case for the month name. 5594a83f5d7SGreg Roach * 5604a83f5d7SGreg Roach * @return string 5614a83f5d7SGreg Roach */ 5624a83f5d7SGreg Roach public function format(string $format, string $qualifier = ''): string 5634a83f5d7SGreg Roach { 5641934fe30SGreg Roach // Dates can include additional punctuation and symbols. 5651934fe30SGreg Roach // e.g. "%Y年 %n月 %j日", "Y.M.D" and "M D, Y". 5661934fe30SGreg Roach // The logic here is inflexible, and should be replaced with 5671934fe30SGreg Roach // specific translations for each abbreviated format. 5681934fe30SGreg Roach 5694a83f5d7SGreg Roach // Don’t show exact details for inexact dates 5704a83f5d7SGreg Roach if (!$this->day) { 5711934fe30SGreg Roach $format = preg_replace('/%[djlDNSwz][,日]?/u', '', $format); 5721934fe30SGreg Roach $format = str_replace(['%d,', '%j日', '%j,', '%j', '%l', '%D', '%N', '%S', '%w', '%z'], '', $format); 5734a83f5d7SGreg Roach } 5744a83f5d7SGreg Roach if (!$this->month) { 5751934fe30SGreg Roach $format = str_replace(['%F', '%m', '%M', '年 %n月', '%n', '%t'], '', $format); 5764a83f5d7SGreg Roach } 5774a83f5d7SGreg Roach if (!$this->year) { 5781934fe30SGreg Roach $format = str_replace(['%t', '%L', '%G', '%y', '%Y年', '%Y'], '', $format); 5794a83f5d7SGreg Roach } 5804a83f5d7SGreg Roach // If we’ve trimmed the format, also trim the punctuation 5814a83f5d7SGreg Roach if (!$this->day || !$this->month || !$this->year) { 5824a83f5d7SGreg Roach $format = trim($format, ',. ;/-'); 5834a83f5d7SGreg Roach } 5844a83f5d7SGreg Roach if ($this->day && preg_match('/%[djlDNSwz]/', $format)) { 5854a83f5d7SGreg Roach // If we have a day-number *and* we are being asked to display it, then genitive 5864a83f5d7SGreg Roach $case = 'GENITIVE'; 5874a83f5d7SGreg Roach } else { 5884a83f5d7SGreg Roach switch ($qualifier) { 5894a83f5d7SGreg Roach case 'TO': 5904a83f5d7SGreg Roach case 'ABT': 5914a83f5d7SGreg Roach case 'FROM': 5924a83f5d7SGreg Roach $case = 'GENITIVE'; 5934a83f5d7SGreg Roach break; 5944a83f5d7SGreg Roach case 'AFT': 5954a83f5d7SGreg Roach $case = 'LOCATIVE'; 5964a83f5d7SGreg Roach break; 5974a83f5d7SGreg Roach case 'BEF': 5984a83f5d7SGreg Roach case 'BET': 5994a83f5d7SGreg Roach case 'AND': 6004a83f5d7SGreg Roach $case = 'INSTRUMENTAL'; 6014a83f5d7SGreg Roach break; 6024a83f5d7SGreg Roach case '': 6034a83f5d7SGreg Roach case 'INT': 6044a83f5d7SGreg Roach case 'EST': 6054a83f5d7SGreg Roach case 'CAL': 6064a83f5d7SGreg Roach default: // There shouldn't be any other options... 6074a83f5d7SGreg Roach $case = 'NOMINATIVE'; 6084a83f5d7SGreg Roach break; 6094a83f5d7SGreg Roach } 6104a83f5d7SGreg Roach } 6114a83f5d7SGreg Roach // Build up the formatted date, character at a time 6124a83f5d7SGreg Roach preg_match_all('/%[^%]/', $format, $matches); 6134a83f5d7SGreg Roach foreach ($matches[0] as $match) { 6144a83f5d7SGreg Roach switch ($match) { 6154a83f5d7SGreg Roach case '%d': 6164a83f5d7SGreg Roach $format = str_replace($match, $this->formatDayZeros(), $format); 6174a83f5d7SGreg Roach break; 6184a83f5d7SGreg Roach case '%j': 6194a83f5d7SGreg Roach $format = str_replace($match, $this->formatDay(), $format); 6204a83f5d7SGreg Roach break; 6214a83f5d7SGreg Roach case '%l': 6224a83f5d7SGreg Roach $format = str_replace($match, $this->formatLongWeekday(), $format); 6234a83f5d7SGreg Roach break; 6244a83f5d7SGreg Roach case '%D': 6254a83f5d7SGreg Roach $format = str_replace($match, $this->formatShortWeekday(), $format); 6264a83f5d7SGreg Roach break; 6274a83f5d7SGreg Roach case '%N': 6284a83f5d7SGreg Roach $format = str_replace($match, $this->formatIsoWeekday(), $format); 6294a83f5d7SGreg Roach break; 6304a83f5d7SGreg Roach case '%w': 6314a83f5d7SGreg Roach $format = str_replace($match, $this->formatNumericWeekday(), $format); 6324a83f5d7SGreg Roach break; 6334a83f5d7SGreg Roach case '%z': 6344a83f5d7SGreg Roach $format = str_replace($match, $this->formatDayOfYear(), $format); 6354a83f5d7SGreg Roach break; 6364a83f5d7SGreg Roach case '%F': 6374a83f5d7SGreg Roach $format = str_replace($match, $this->formatLongMonth($case), $format); 6384a83f5d7SGreg Roach break; 6394a83f5d7SGreg Roach case '%m': 6404a83f5d7SGreg Roach $format = str_replace($match, $this->formatMonthZeros(), $format); 6414a83f5d7SGreg Roach break; 6424a83f5d7SGreg Roach case '%M': 6434a83f5d7SGreg Roach $format = str_replace($match, $this->formatShortMonth(), $format); 6444a83f5d7SGreg Roach break; 6454a83f5d7SGreg Roach case '%n': 6464a83f5d7SGreg Roach $format = str_replace($match, $this->formatMonth(), $format); 6474a83f5d7SGreg Roach break; 6484a83f5d7SGreg Roach case '%t': 6494a83f5d7SGreg Roach $format = str_replace($match, (string) $this->daysInMonth(), $format); 6504a83f5d7SGreg Roach break; 6514a83f5d7SGreg Roach case '%L': 6524a83f5d7SGreg Roach $format = str_replace($match, $this->isLeapYear() ? '1' : '0', $format); 6534a83f5d7SGreg Roach break; 6544a83f5d7SGreg Roach case '%Y': 6554a83f5d7SGreg Roach $format = str_replace($match, $this->formatLongYear(), $format); 6564a83f5d7SGreg Roach break; 6574a83f5d7SGreg Roach case '%y': 6584a83f5d7SGreg Roach $format = str_replace($match, $this->formatShortYear(), $format); 6594a83f5d7SGreg Roach break; 6604a83f5d7SGreg Roach // These 4 extensions are useful for re-formatting gedcom dates. 6614a83f5d7SGreg Roach case '%@': 6624a83f5d7SGreg Roach $format = str_replace($match, $this->formatGedcomCalendarEscape(), $format); 6634a83f5d7SGreg Roach break; 6644a83f5d7SGreg Roach case '%A': 6654a83f5d7SGreg Roach $format = str_replace($match, $this->formatGedcomDay(), $format); 6664a83f5d7SGreg Roach break; 6674a83f5d7SGreg Roach case '%O': 6684a83f5d7SGreg Roach $format = str_replace($match, $this->formatGedcomMonth(), $format); 6694a83f5d7SGreg Roach break; 6704a83f5d7SGreg Roach case '%E': 6714a83f5d7SGreg Roach $format = str_replace($match, $this->formatGedcomYear(), $format); 6724a83f5d7SGreg Roach break; 6734a83f5d7SGreg Roach } 6744a83f5d7SGreg Roach } 6754a83f5d7SGreg Roach 6764a83f5d7SGreg Roach return $format; 6774a83f5d7SGreg Roach } 6784a83f5d7SGreg Roach 6794a83f5d7SGreg Roach /** 6804a83f5d7SGreg Roach * Generate the %d format for a date. 6814a83f5d7SGreg Roach * 6824a83f5d7SGreg Roach * @return string 6834a83f5d7SGreg Roach */ 6844a83f5d7SGreg Roach protected function formatDayZeros(): string 6854a83f5d7SGreg Roach { 6864a83f5d7SGreg Roach if ($this->day > 9) { 6874a83f5d7SGreg Roach return I18N::digits($this->day); 6884a83f5d7SGreg Roach } 6894a83f5d7SGreg Roach 6904a83f5d7SGreg Roach return I18N::digits('0' . $this->day); 6914a83f5d7SGreg Roach } 6924a83f5d7SGreg Roach 6934a83f5d7SGreg Roach /** 6944a83f5d7SGreg Roach * Generate the %j format for a date. 6954a83f5d7SGreg Roach * 6964a83f5d7SGreg Roach * @return string 6974a83f5d7SGreg Roach */ 6984a83f5d7SGreg Roach protected function formatDay(): string 6994a83f5d7SGreg Roach { 7004a83f5d7SGreg Roach return I18N::digits($this->day); 7014a83f5d7SGreg Roach } 7024a83f5d7SGreg Roach 7034a83f5d7SGreg Roach /** 7044a83f5d7SGreg Roach * Generate the %l format for a date. 7054a83f5d7SGreg Roach * 7064a83f5d7SGreg Roach * @return string 7074a83f5d7SGreg Roach */ 7084a83f5d7SGreg Roach protected function formatLongWeekday(): string 7094a83f5d7SGreg Roach { 7104a83f5d7SGreg Roach return $this->dayNames($this->minimum_julian_day % $this->calendar->daysInWeek()); 7114a83f5d7SGreg Roach } 7124a83f5d7SGreg Roach 7134a83f5d7SGreg Roach /** 7144a83f5d7SGreg Roach * Generate the %D format for a date. 7154a83f5d7SGreg Roach * 7164a83f5d7SGreg Roach * @return string 7174a83f5d7SGreg Roach */ 7184a83f5d7SGreg Roach protected function formatShortWeekday(): string 7194a83f5d7SGreg Roach { 7204a83f5d7SGreg Roach return $this->dayNamesAbbreviated($this->minimum_julian_day % $this->calendar->daysInWeek()); 7214a83f5d7SGreg Roach } 7224a83f5d7SGreg Roach 7234a83f5d7SGreg Roach /** 7244a83f5d7SGreg Roach * Generate the %N format for a date. 7254a83f5d7SGreg Roach * 7264a83f5d7SGreg Roach * @return string 7274a83f5d7SGreg Roach */ 7284a83f5d7SGreg Roach protected function formatIsoWeekday(): string 7294a83f5d7SGreg Roach { 7304a83f5d7SGreg Roach return I18N::digits($this->minimum_julian_day % 7 + 1); 7314a83f5d7SGreg Roach } 7324a83f5d7SGreg Roach 7334a83f5d7SGreg Roach /** 7344a83f5d7SGreg Roach * Generate the %w format for a date. 7354a83f5d7SGreg Roach * 7364a83f5d7SGreg Roach * @return string 7374a83f5d7SGreg Roach */ 7384a83f5d7SGreg Roach protected function formatNumericWeekday(): string 7394a83f5d7SGreg Roach { 7404a83f5d7SGreg Roach return I18N::digits(($this->minimum_julian_day + 1) % $this->calendar->daysInWeek()); 7414a83f5d7SGreg Roach } 7424a83f5d7SGreg Roach 7434a83f5d7SGreg Roach /** 7444a83f5d7SGreg Roach * Generate the %z format for a date. 7454a83f5d7SGreg Roach * 7464a83f5d7SGreg Roach * @return string 7474a83f5d7SGreg Roach */ 7484a83f5d7SGreg Roach protected function formatDayOfYear(): string 7494a83f5d7SGreg Roach { 7504a83f5d7SGreg Roach return I18N::digits($this->minimum_julian_day - $this->calendar->ymdToJd($this->year, 1, 1)); 7514a83f5d7SGreg Roach } 7524a83f5d7SGreg Roach 7534a83f5d7SGreg Roach /** 7544a83f5d7SGreg Roach * Generate the %n format for a date. 7554a83f5d7SGreg Roach * 7564a83f5d7SGreg Roach * @return string 7574a83f5d7SGreg Roach */ 7584a83f5d7SGreg Roach protected function formatMonth(): string 7594a83f5d7SGreg Roach { 7604a83f5d7SGreg Roach return I18N::digits($this->month); 7614a83f5d7SGreg Roach } 7624a83f5d7SGreg Roach 7634a83f5d7SGreg Roach /** 7644a83f5d7SGreg Roach * Generate the %m format for a date. 7654a83f5d7SGreg Roach * 7664a83f5d7SGreg Roach * @return string 7674a83f5d7SGreg Roach */ 7684a83f5d7SGreg Roach protected function formatMonthZeros(): string 7694a83f5d7SGreg Roach { 7704a83f5d7SGreg Roach if ($this->month > 9) { 7714a83f5d7SGreg Roach return I18N::digits($this->month); 7724a83f5d7SGreg Roach } 7734a83f5d7SGreg Roach 7744a83f5d7SGreg Roach return I18N::digits('0' . $this->month); 7754a83f5d7SGreg Roach } 7764a83f5d7SGreg Roach 7774a83f5d7SGreg Roach /** 7784a83f5d7SGreg Roach * Generate the %F format for a date. 7794a83f5d7SGreg Roach * 7804a83f5d7SGreg Roach * @param string $case Which grammatical case shall we use 7814a83f5d7SGreg Roach * 7824a83f5d7SGreg Roach * @return string 7834a83f5d7SGreg Roach */ 7844a83f5d7SGreg Roach protected function formatLongMonth($case = 'NOMINATIVE'): string 7854a83f5d7SGreg Roach { 7864a83f5d7SGreg Roach switch ($case) { 7874a83f5d7SGreg Roach case 'GENITIVE': 7884a83f5d7SGreg Roach return $this->monthNameGenitiveCase($this->month, $this->isLeapYear()); 7894a83f5d7SGreg Roach case 'NOMINATIVE': 7904a83f5d7SGreg Roach return $this->monthNameNominativeCase($this->month, $this->isLeapYear()); 7914a83f5d7SGreg Roach case 'LOCATIVE': 7924a83f5d7SGreg Roach return $this->monthNameLocativeCase($this->month, $this->isLeapYear()); 7934a83f5d7SGreg Roach case 'INSTRUMENTAL': 7944a83f5d7SGreg Roach return $this->monthNameInstrumentalCase($this->month, $this->isLeapYear()); 7954a83f5d7SGreg Roach default: 79691495569SGreg Roach throw new InvalidArgumentException($case); 7974a83f5d7SGreg Roach } 7984a83f5d7SGreg Roach } 7994a83f5d7SGreg Roach 8004a83f5d7SGreg Roach /** 8017bb2eb25SGreg Roach * Full month name in genitive case. 8027bb2eb25SGreg Roach * 8037bb2eb25SGreg Roach * @param int $month 8047bb2eb25SGreg Roach * @param bool $leap_year Some calendars use leap months 8057bb2eb25SGreg Roach * 8067bb2eb25SGreg Roach * @return string 8077bb2eb25SGreg Roach */ 8087bb2eb25SGreg Roach abstract protected function monthNameGenitiveCase(int $month, bool $leap_year): string; 8097bb2eb25SGreg Roach 8107bb2eb25SGreg Roach /** 8117bb2eb25SGreg Roach * Full month name in nominative case. 8127bb2eb25SGreg Roach * 8137bb2eb25SGreg Roach * @param int $month 8147bb2eb25SGreg Roach * @param bool $leap_year Some calendars use leap months 8157bb2eb25SGreg Roach * 8167bb2eb25SGreg Roach * @return string 8177bb2eb25SGreg Roach */ 8187bb2eb25SGreg Roach abstract protected function monthNameNominativeCase(int $month, bool $leap_year): string; 8197bb2eb25SGreg Roach 8207bb2eb25SGreg Roach /** 8217bb2eb25SGreg Roach * Full month name in locative case. 8227bb2eb25SGreg Roach * 8237bb2eb25SGreg Roach * @param int $month 8247bb2eb25SGreg Roach * @param bool $leap_year Some calendars use leap months 8257bb2eb25SGreg Roach * 8267bb2eb25SGreg Roach * @return string 8277bb2eb25SGreg Roach */ 8287bb2eb25SGreg Roach abstract protected function monthNameLocativeCase(int $month, bool $leap_year): string; 8297bb2eb25SGreg Roach 8307bb2eb25SGreg Roach /** 8317bb2eb25SGreg Roach * Full month name in instrumental case. 8327bb2eb25SGreg Roach * 8337bb2eb25SGreg Roach * @param int $month 8347bb2eb25SGreg Roach * @param bool $leap_year Some calendars use leap months 8357bb2eb25SGreg Roach * 8367bb2eb25SGreg Roach * @return string 8377bb2eb25SGreg Roach */ 8387bb2eb25SGreg Roach abstract protected function monthNameInstrumentalCase(int $month, bool $leap_year): string; 8397bb2eb25SGreg Roach 8407bb2eb25SGreg Roach /** 8417bb2eb25SGreg Roach * Abbreviated month name 8427bb2eb25SGreg Roach * 8437bb2eb25SGreg Roach * @param int $month 8447bb2eb25SGreg Roach * @param bool $leap_year Some calendars use leap months 8457bb2eb25SGreg Roach * 8467bb2eb25SGreg Roach * @return string 8477bb2eb25SGreg Roach */ 8487bb2eb25SGreg Roach abstract protected function monthNameAbbreviated(int $month, bool $leap_year): string; 8497bb2eb25SGreg Roach 8507bb2eb25SGreg Roach /** 8514a83f5d7SGreg Roach * Generate the %M format for a date. 8524a83f5d7SGreg Roach * 8534a83f5d7SGreg Roach * @return string 8544a83f5d7SGreg Roach */ 8554a83f5d7SGreg Roach protected function formatShortMonth(): string 8564a83f5d7SGreg Roach { 8574a83f5d7SGreg Roach return $this->monthNameAbbreviated($this->month, $this->isLeapYear()); 8584a83f5d7SGreg Roach } 8594a83f5d7SGreg Roach 8604a83f5d7SGreg Roach /** 8614a83f5d7SGreg Roach * Generate the %y format for a date. 8624a83f5d7SGreg Roach * NOTE Short year is NOT a 2-digit year. It is for calendars such as hebrew 8634a83f5d7SGreg Roach * which have a 3-digit form of 4-digit years. 8644a83f5d7SGreg Roach * 8654a83f5d7SGreg Roach * @return string 8664a83f5d7SGreg Roach */ 8674a83f5d7SGreg Roach protected function formatShortYear(): string 8684a83f5d7SGreg Roach { 8694a83f5d7SGreg Roach return $this->formatLongYear(); 8704a83f5d7SGreg Roach } 8714a83f5d7SGreg Roach 8724a83f5d7SGreg Roach /** 8734a83f5d7SGreg Roach * Generate the %A format for a date. 8744a83f5d7SGreg Roach * 8754a83f5d7SGreg Roach * @return string 8764a83f5d7SGreg Roach */ 8774a83f5d7SGreg Roach protected function formatGedcomDay(): string 8784a83f5d7SGreg Roach { 879e364afe4SGreg Roach if ($this->day === 0) { 8804a83f5d7SGreg Roach return ''; 8814a83f5d7SGreg Roach } 8824a83f5d7SGreg Roach 8834a83f5d7SGreg Roach return sprintf('%02d', $this->day); 8844a83f5d7SGreg Roach } 8854a83f5d7SGreg Roach 8864a83f5d7SGreg Roach /** 8874a83f5d7SGreg Roach * Generate the %O format for a date. 8884a83f5d7SGreg Roach * 8894a83f5d7SGreg Roach * @return string 8904a83f5d7SGreg Roach */ 8914a83f5d7SGreg Roach protected function formatGedcomMonth(): string 8924a83f5d7SGreg Roach { 8934a83f5d7SGreg Roach // Our simple lookup table doesn't work correctly for Adar on leap years 894e364afe4SGreg Roach if ($this->month === 7 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) { 8954a83f5d7SGreg Roach return 'ADR'; 8964a83f5d7SGreg Roach } 8974a83f5d7SGreg Roach 89822d65e5aSGreg Roach return array_search($this->month, static::MONTH_ABBREVIATIONS, true); 8994a83f5d7SGreg Roach } 9004a83f5d7SGreg Roach 9014a83f5d7SGreg Roach /** 9024a83f5d7SGreg Roach * Generate the %E format for a date. 9034a83f5d7SGreg Roach * 9044a83f5d7SGreg Roach * @return string 9054a83f5d7SGreg Roach */ 9064a83f5d7SGreg Roach protected function formatGedcomYear(): string 9074a83f5d7SGreg Roach { 908e364afe4SGreg Roach if ($this->year === 0) { 9094a83f5d7SGreg Roach return ''; 9104a83f5d7SGreg Roach } 9114a83f5d7SGreg Roach 9124a83f5d7SGreg Roach return sprintf('%04d', $this->year); 9134a83f5d7SGreg Roach } 9144a83f5d7SGreg Roach 9154a83f5d7SGreg Roach /** 9164a83f5d7SGreg Roach * Generate the %@ format for a calendar escape. 9174a83f5d7SGreg Roach * 9184a83f5d7SGreg Roach * @return string 9194a83f5d7SGreg Roach */ 9204a83f5d7SGreg Roach protected function formatGedcomCalendarEscape(): string 9214a83f5d7SGreg Roach { 9224a83f5d7SGreg Roach return static::ESCAPE; 9234a83f5d7SGreg Roach } 9244a83f5d7SGreg Roach 9254a83f5d7SGreg Roach /** 9264a83f5d7SGreg Roach * Generate the %Y format for a date. 9274a83f5d7SGreg Roach * 9284a83f5d7SGreg Roach * @return string 9294a83f5d7SGreg Roach */ 9304a83f5d7SGreg Roach protected function formatLongYear(): string 9314a83f5d7SGreg Roach { 9324a83f5d7SGreg Roach return I18N::digits($this->year); 9334a83f5d7SGreg Roach } 9344a83f5d7SGreg Roach 9354a83f5d7SGreg Roach /** 9364a83f5d7SGreg Roach * Which months follows this one? Calendars with leap-months should provide their own implementation. 9374a83f5d7SGreg Roach * 9384a83f5d7SGreg Roach * @return int[] 9394a83f5d7SGreg Roach */ 9404a83f5d7SGreg Roach protected function nextMonth(): array 9414a83f5d7SGreg Roach { 9424a83f5d7SGreg Roach return [ 9434a83f5d7SGreg Roach $this->month === $this->calendar->monthsInYear() ? $this->nextYear($this->year) : $this->year, 9442cebb4b4SGreg Roach $this->month % $this->calendar->monthsInYear() + 1, 9454a83f5d7SGreg Roach ]; 9464a83f5d7SGreg Roach } 9474a83f5d7SGreg Roach 9484a83f5d7SGreg Roach /** 9494a83f5d7SGreg Roach * Get today’s date in the current calendar. 9504a83f5d7SGreg Roach * 9514a83f5d7SGreg Roach * @return int[] 9524a83f5d7SGreg Roach */ 9534a83f5d7SGreg Roach public function todayYmd(): array 9544a83f5d7SGreg Roach { 9554459dc9aSGreg Roach return $this->calendar->jdToYmd(Carbon::now()->julianDay()); 9564a83f5d7SGreg Roach } 9574a83f5d7SGreg Roach 9584a83f5d7SGreg Roach /** 9594a83f5d7SGreg Roach * Convert to today’s date. 9604a83f5d7SGreg Roach * 9614a83f5d7SGreg Roach * @return AbstractCalendarDate 9624a83f5d7SGreg Roach */ 9634a83f5d7SGreg Roach public function today(): AbstractCalendarDate 9644a83f5d7SGreg Roach { 9654a83f5d7SGreg Roach $tmp = clone $this; 9664a83f5d7SGreg Roach $ymd = $tmp->todayYmd(); 9674a83f5d7SGreg Roach $tmp->year = $ymd[0]; 9684a83f5d7SGreg Roach $tmp->month = $ymd[1]; 9694a83f5d7SGreg Roach $tmp->day = $ymd[2]; 9704a83f5d7SGreg Roach $tmp->setJdFromYmd(); 9714a83f5d7SGreg Roach 9724a83f5d7SGreg Roach return $tmp; 9734a83f5d7SGreg Roach } 9744a83f5d7SGreg Roach 9754a83f5d7SGreg Roach /** 9764a83f5d7SGreg Roach * Create a URL that links this date to the WT calendar 9774a83f5d7SGreg Roach * 9784a83f5d7SGreg Roach * @param string $date_format 97949d5f1d7SGreg Roach * @param Tree $tree 9804a83f5d7SGreg Roach * 9814a83f5d7SGreg Roach * @return string 9824a83f5d7SGreg Roach */ 98349d5f1d7SGreg Roach public function calendarUrl(string $date_format, Tree $tree): string 9844a83f5d7SGreg Roach { 985e364afe4SGreg Roach if ($this->day !== 0 && strpbrk($date_format, 'dDj')) { 9864a83f5d7SGreg Roach // If the format includes a day, and the date also includes a day, then use the day view 9874a83f5d7SGreg Roach $view = 'day'; 988e364afe4SGreg Roach } elseif ($this->month !== 0 && strpbrk($date_format, 'FMmn')) { 9894a83f5d7SGreg Roach // If the format includes a month, and the date also includes a month, then use the month view 9904a83f5d7SGreg Roach $view = 'month'; 9914a83f5d7SGreg Roach } else { 9924a83f5d7SGreg Roach // Use the year view 9934a83f5d7SGreg Roach $view = 'year'; 9944a83f5d7SGreg Roach } 9954a83f5d7SGreg Roach 9964a83f5d7SGreg Roach return route('calendar', [ 9974a83f5d7SGreg Roach 'cal' => $this->calendar->gedcomCalendarEscape(), 9984a83f5d7SGreg Roach 'year' => $this->formatGedcomYear(), 9994a83f5d7SGreg Roach 'month' => $this->formatGedcomMonth(), 10004a83f5d7SGreg Roach 'day' => $this->formatGedcomDay(), 10014a83f5d7SGreg Roach 'view' => $view, 1002d72b284aSGreg Roach 'tree' => $tree->name(), 10034a83f5d7SGreg Roach ]); 10044a83f5d7SGreg Roach } 10054a83f5d7SGreg Roach} 1006