xref: /webtrees/app/Date/AbstractCalendarDate.php (revision d8809d62a7e3fb7260ce3624ee766b7cb2dd1a21)
14a83f5d7SGreg Roach<?php
23976b470SGreg Roach
34a83f5d7SGreg Roach/**
44a83f5d7SGreg Roach * webtrees: online genealogy
589f7189bSGreg Roach * Copyright (C) 2021 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
1589f7189bSGreg Roach * along with this program. If not, see <https://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;
25b00cb080SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\CalendarPage;
264a83f5d7SGreg Roachuse Fisharebest\Webtrees\I18N;
2749d5f1d7SGreg Roachuse Fisharebest\Webtrees\Tree;
2891495569SGreg Roachuse InvalidArgumentException;
294a83f5d7SGreg Roach
301934fe30SGreg Roachuse function array_key_exists;
311934fe30SGreg Roachuse function array_search;
321934fe30SGreg Roachuse function get_class;
331934fe30SGreg Roachuse function intdiv;
341934fe30SGreg Roachuse function is_array;
351934fe30SGreg Roachuse function is_int;
361934fe30SGreg Roachuse function preg_match;
371934fe30SGreg Roachuse function route;
381934fe30SGreg Roachuse function sprintf;
3952eb92f9SGreg Roachuse function str_contains;
401934fe30SGreg Roachuse function strpbrk;
41dec352c1SGreg Roachuse function strtr;
421934fe30SGreg Roachuse function trim;
43054771e9SGreg Roach
444a83f5d7SGreg Roach/**
454a83f5d7SGreg Roach * Classes for Gedcom Date/Calendar functionality.
464a83f5d7SGreg Roach *
474a83f5d7SGreg Roach * CalendarDate is a base class for classes such as GregorianDate, etc.
484a83f5d7SGreg Roach * + All supported calendars have non-zero days/months/years.
494a83f5d7SGreg Roach * + We store dates as both Y/M/D and Julian Days.
504a83f5d7SGreg Roach * + For imprecise dates such as "JAN 2000" we store the start/end julian day.
514a83f5d7SGreg Roach *
524a83f5d7SGreg Roach * NOTE: Since different calendars start their days at different times, (civil
534a83f5d7SGreg Roach * midnight, solar midnight, sunset, sunrise, etc.), we convert on the basis of
544a83f5d7SGreg Roach * midday.
554a83f5d7SGreg Roach */
567bb2eb25SGreg Roachabstract class AbstractCalendarDate
574a83f5d7SGreg Roach{
584a83f5d7SGreg Roach    // GEDCOM calendar escape
5916d6367aSGreg Roach    public const ESCAPE = '@#DUNKNOWN@';
604a83f5d7SGreg Roach
614a83f5d7SGreg Roach    // Convert GEDCOM month names to month numbers.
62*d8809d62SGreg Roach    protected const MONTH_TO_NUMBER = [];
63*d8809d62SGreg Roach    protected const NUMBER_TO_MONTH = [];
644a83f5d7SGreg Roach
6533c746f1SGreg Roach    protected CalendarInterface $calendar;
664a83f5d7SGreg Roach
6733c746f1SGreg Roach    public int $year;
684a83f5d7SGreg Roach
6933c746f1SGreg Roach    public int $month;
704a83f5d7SGreg Roach
7133c746f1SGreg Roach    public int $day;
724a83f5d7SGreg Roach
7333c746f1SGreg Roach    private int $minimum_julian_day;
744a83f5d7SGreg Roach
7533c746f1SGreg Roach    private int $maximum_julian_day;
764a83f5d7SGreg Roach
774a83f5d7SGreg Roach    /**
784a83f5d7SGreg Roach     * Create a date from either:
794a83f5d7SGreg Roach     * a Julian day number
804a83f5d7SGreg Roach     * day/month/year strings from a GEDCOM date
814a83f5d7SGreg Roach     * another CalendarDate object
824a83f5d7SGreg Roach     *
83f4c767fdSGreg Roach     * @param array<string>|int|AbstractCalendarDate $date
844a83f5d7SGreg Roach     */
854a83f5d7SGreg Roach    protected function __construct($date)
864a83f5d7SGreg Roach    {
874a83f5d7SGreg Roach        // Construct from an integer (a julian day number)
884a83f5d7SGreg Roach        if (is_int($date)) {
894a83f5d7SGreg Roach            $this->minimum_julian_day = $date;
904a83f5d7SGreg Roach            $this->maximum_julian_day = $date;
9165e02381SGreg Roach            [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($date);
924a83f5d7SGreg Roach
934a83f5d7SGreg Roach            return;
944a83f5d7SGreg Roach        }
954a83f5d7SGreg Roach
964a83f5d7SGreg Roach        // Construct from an array (of three gedcom-style strings: "1900", "FEB", "4")
974a83f5d7SGreg Roach        if (is_array($date)) {
984a83f5d7SGreg Roach            $this->day = (int) $date[2];
99*d8809d62SGreg Roach            $this->month = static::MONTH_TO_NUMBER[$date[1]] ?? 0;
100*d8809d62SGreg Roach
101*d8809d62SGreg Roach            if ($this->month === 0) {
1024a83f5d7SGreg Roach                $this->day   = 0;
1034a83f5d7SGreg Roach            }
104*d8809d62SGreg Roach
1054a83f5d7SGreg Roach            $this->year = $this->extractYear($date[0]);
1064a83f5d7SGreg Roach
1074a83f5d7SGreg Roach            // Our simple lookup table above does not take into account Adar and leap-years.
1084a83f5d7SGreg Roach            if ($this->month === 6 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) {
1094a83f5d7SGreg Roach                $this->month = 7;
1104a83f5d7SGreg Roach            }
1114a83f5d7SGreg Roach
1124a83f5d7SGreg Roach            $this->setJdFromYmd();
1134a83f5d7SGreg Roach
1144a83f5d7SGreg Roach            return;
1154a83f5d7SGreg Roach        }
1164a83f5d7SGreg Roach
117fceda430SGreg Roach        // Construct from a CalendarDate
1184a83f5d7SGreg Roach        $this->minimum_julian_day = $date->minimum_julian_day;
1194a83f5d7SGreg Roach        $this->maximum_julian_day = $date->maximum_julian_day;
1204a83f5d7SGreg Roach
1214a83f5d7SGreg Roach        // Construct from an equivalent xxxxDate object
122e364afe4SGreg Roach        if (get_class($this) === get_class($date)) {
1234a83f5d7SGreg Roach            $this->year  = $date->year;
1244a83f5d7SGreg Roach            $this->month = $date->month;
1254a83f5d7SGreg Roach            $this->day   = $date->day;
1264a83f5d7SGreg Roach
1274a83f5d7SGreg Roach            return;
1284a83f5d7SGreg Roach        }
1294a83f5d7SGreg Roach
1304a83f5d7SGreg Roach        // Not all dates can be converted
1314a83f5d7SGreg Roach        if (!$this->inValidRange()) {
1324a83f5d7SGreg Roach            $this->year  = 0;
1334a83f5d7SGreg Roach            $this->month = 0;
1344a83f5d7SGreg Roach            $this->day   = 0;
1354a83f5d7SGreg Roach
1364a83f5d7SGreg Roach            return;
1374a83f5d7SGreg Roach        }
1384a83f5d7SGreg Roach
1394a83f5d7SGreg Roach        // ...else construct an inequivalent xxxxDate object
140e364afe4SGreg Roach        if ($date->year === 0) {
1414a83f5d7SGreg Roach            // Incomplete date - convert on basis of anniversary in current year
1424459dc9aSGreg Roach            $today = $date->calendar->jdToYmd(Carbon::now()->julianDay());
143e364afe4SGreg Roach            $jd    = $date->calendar->ymdToJd($today[0], $date->month, $date->day === 0 ? $today[2] : $date->day);
1444a83f5d7SGreg Roach        } else {
1454a83f5d7SGreg Roach            // Complete date
1464a83f5d7SGreg Roach            $jd = intdiv($date->maximum_julian_day + $date->minimum_julian_day, 2);
1474a83f5d7SGreg Roach        }
14865e02381SGreg Roach        [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($jd);
1494a83f5d7SGreg Roach        // New date has same precision as original date
150e364afe4SGreg Roach        if ($date->year === 0) {
1514a83f5d7SGreg Roach            $this->year = 0;
1524a83f5d7SGreg Roach        }
153e364afe4SGreg Roach        if ($date->month === 0) {
1544a83f5d7SGreg Roach            $this->month = 0;
1554a83f5d7SGreg Roach        }
156e364afe4SGreg Roach        if ($date->day === 0) {
1574a83f5d7SGreg Roach            $this->day = 0;
1584a83f5d7SGreg Roach        }
1594a83f5d7SGreg Roach        $this->setJdFromYmd();
1604a83f5d7SGreg Roach    }
1614a83f5d7SGreg Roach
1624a83f5d7SGreg Roach    /**
1634a83f5d7SGreg Roach     * @return int
1644a83f5d7SGreg Roach     */
1654a83f5d7SGreg Roach    public function maximumJulianDay(): int
1664a83f5d7SGreg Roach    {
1674a83f5d7SGreg Roach        return $this->maximum_julian_day;
1684a83f5d7SGreg Roach    }
1694a83f5d7SGreg Roach
1704a83f5d7SGreg Roach    /**
1714a83f5d7SGreg Roach     * @return int
1724a83f5d7SGreg Roach     */
1734a83f5d7SGreg Roach    public function year(): int
1744a83f5d7SGreg Roach    {
1754a83f5d7SGreg Roach        return $this->year;
1764a83f5d7SGreg Roach    }
1774a83f5d7SGreg Roach
1784a83f5d7SGreg Roach    /**
1794a83f5d7SGreg Roach     * @return int
1804a83f5d7SGreg Roach     */
1814a83f5d7SGreg Roach    public function month(): int
1824a83f5d7SGreg Roach    {
1834a83f5d7SGreg Roach        return $this->month;
1844a83f5d7SGreg Roach    }
1854a83f5d7SGreg Roach
1864a83f5d7SGreg Roach    /**
1874a83f5d7SGreg Roach     * @return int
1884a83f5d7SGreg Roach     */
1894a83f5d7SGreg Roach    public function day(): int
1904a83f5d7SGreg Roach    {
1914a83f5d7SGreg Roach        return $this->day;
1924a83f5d7SGreg Roach    }
1934a83f5d7SGreg Roach
1944a83f5d7SGreg Roach    /**
1954a83f5d7SGreg Roach     * @return int
1964a83f5d7SGreg Roach     */
1974a83f5d7SGreg Roach    public function minimumJulianDay(): int
1984a83f5d7SGreg Roach    {
1994a83f5d7SGreg Roach        return $this->minimum_julian_day;
2004a83f5d7SGreg Roach    }
2014a83f5d7SGreg Roach
2024a83f5d7SGreg Roach    /**
2034a83f5d7SGreg Roach     * Is the current year a leap year?
2044a83f5d7SGreg Roach     *
2054a83f5d7SGreg Roach     * @return bool
2064a83f5d7SGreg Roach     */
2074a83f5d7SGreg Roach    public function isLeapYear(): bool
2084a83f5d7SGreg Roach    {
2094a83f5d7SGreg Roach        return $this->calendar->isLeapYear($this->year);
2104a83f5d7SGreg Roach    }
2114a83f5d7SGreg Roach
2124a83f5d7SGreg Roach    /**
2134a83f5d7SGreg Roach     * Set the object’s Julian day number from a potentially incomplete year/month/day
2144a83f5d7SGreg Roach     *
2154a83f5d7SGreg Roach     * @return void
2164a83f5d7SGreg Roach     */
217e364afe4SGreg Roach    public function setJdFromYmd(): void
2184a83f5d7SGreg Roach    {
219e364afe4SGreg Roach        if ($this->year === 0) {
2204a83f5d7SGreg Roach            $this->minimum_julian_day = 0;
2214a83f5d7SGreg Roach            $this->maximum_julian_day = 0;
222e364afe4SGreg Roach        } elseif ($this->month === 0) {
2234a83f5d7SGreg Roach            $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, 1, 1);
2244a83f5d7SGreg Roach            $this->maximum_julian_day = $this->calendar->ymdToJd($this->nextYear($this->year), 1, 1) - 1;
225e364afe4SGreg Roach        } elseif ($this->day === 0) {
22665e02381SGreg Roach            [$ny, $nm] = $this->nextMonth();
2274a83f5d7SGreg Roach            $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, 1);
2284a83f5d7SGreg Roach            $this->maximum_julian_day = $this->calendar->ymdToJd($ny, $nm, 1) - 1;
2294a83f5d7SGreg Roach        } else {
2304a83f5d7SGreg Roach            $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, $this->day);
2314a83f5d7SGreg Roach            $this->maximum_julian_day = $this->minimum_julian_day;
2324a83f5d7SGreg Roach        }
2334a83f5d7SGreg Roach    }
2344a83f5d7SGreg Roach
2354a83f5d7SGreg Roach    /**
2364a83f5d7SGreg Roach     * Full day of the week
2374a83f5d7SGreg Roach     *
2384a83f5d7SGreg Roach     * @param int $day_number
2394a83f5d7SGreg Roach     *
2404a83f5d7SGreg Roach     * @return string
2414a83f5d7SGreg Roach     */
2424a83f5d7SGreg Roach    public function dayNames(int $day_number): string
2434a83f5d7SGreg Roach    {
2444a83f5d7SGreg Roach        static $translated_day_names;
2454a83f5d7SGreg Roach
2464a83f5d7SGreg Roach        if ($translated_day_names === null) {
2474a83f5d7SGreg Roach            $translated_day_names = [
2484a83f5d7SGreg Roach                0 => I18N::translate('Monday'),
2494a83f5d7SGreg Roach                1 => I18N::translate('Tuesday'),
2504a83f5d7SGreg Roach                2 => I18N::translate('Wednesday'),
2514a83f5d7SGreg Roach                3 => I18N::translate('Thursday'),
2524a83f5d7SGreg Roach                4 => I18N::translate('Friday'),
2534a83f5d7SGreg Roach                5 => I18N::translate('Saturday'),
2544a83f5d7SGreg Roach                6 => I18N::translate('Sunday'),
2554a83f5d7SGreg Roach            ];
2564a83f5d7SGreg Roach        }
2574a83f5d7SGreg Roach
2584a83f5d7SGreg Roach        return $translated_day_names[$day_number];
2594a83f5d7SGreg Roach    }
2604a83f5d7SGreg Roach
2614a83f5d7SGreg Roach    /**
2624a83f5d7SGreg Roach     * Abbreviated day of the week
2634a83f5d7SGreg Roach     *
2644a83f5d7SGreg Roach     * @param int $day_number
2654a83f5d7SGreg Roach     *
2664a83f5d7SGreg Roach     * @return string
2674a83f5d7SGreg Roach     */
2684a83f5d7SGreg Roach    protected function dayNamesAbbreviated(int $day_number): string
2694a83f5d7SGreg Roach    {
2704a83f5d7SGreg Roach        static $translated_day_names;
2714a83f5d7SGreg Roach
2724a83f5d7SGreg Roach        if ($translated_day_names === null) {
2734a83f5d7SGreg Roach            $translated_day_names = [
2744a83f5d7SGreg Roach                /* I18N: abbreviation for Monday */
2754a83f5d7SGreg Roach                0 => I18N::translate('Mon'),
2764a83f5d7SGreg Roach                /* I18N: abbreviation for Tuesday */
2774a83f5d7SGreg Roach                1 => I18N::translate('Tue'),
2784a83f5d7SGreg Roach                /* I18N: abbreviation for Wednesday */
2794a83f5d7SGreg Roach                2 => I18N::translate('Wed'),
2804a83f5d7SGreg Roach                /* I18N: abbreviation for Thursday */
2814a83f5d7SGreg Roach                3 => I18N::translate('Thu'),
2824a83f5d7SGreg Roach                /* I18N: abbreviation for Friday */
2834a83f5d7SGreg Roach                4 => I18N::translate('Fri'),
2844a83f5d7SGreg Roach                /* I18N: abbreviation for Saturday */
2854a83f5d7SGreg Roach                5 => I18N::translate('Sat'),
2864a83f5d7SGreg Roach                /* I18N: abbreviation for Sunday */
2874a83f5d7SGreg Roach                6 => I18N::translate('Sun'),
2884a83f5d7SGreg Roach            ];
2894a83f5d7SGreg Roach        }
2904a83f5d7SGreg Roach
2914a83f5d7SGreg Roach        return $translated_day_names[$day_number];
2924a83f5d7SGreg Roach    }
2934a83f5d7SGreg Roach
2944a83f5d7SGreg Roach    /**
2954a83f5d7SGreg Roach     * Most years are 1 more than the previous, but not always (e.g. 1BC->1AD)
2964a83f5d7SGreg Roach     *
2974a83f5d7SGreg Roach     * @param int $year
2984a83f5d7SGreg Roach     *
2994a83f5d7SGreg Roach     * @return int
3004a83f5d7SGreg Roach     */
3014a83f5d7SGreg Roach    protected function nextYear(int $year): int
3024a83f5d7SGreg Roach    {
3034a83f5d7SGreg Roach        return $year + 1;
3044a83f5d7SGreg Roach    }
3054a83f5d7SGreg Roach
3064a83f5d7SGreg Roach    /**
3074a83f5d7SGreg Roach     * Calendars that use suffixes, etc. (e.g. “B.C.”) or OS/NS notation should redefine this.
3084a83f5d7SGreg Roach     *
3094a83f5d7SGreg Roach     * @param string $year
3104a83f5d7SGreg Roach     *
3114a83f5d7SGreg Roach     * @return int
3124a83f5d7SGreg Roach     */
3134a83f5d7SGreg Roach    protected function extractYear(string $year): int
3144a83f5d7SGreg Roach    {
3154a83f5d7SGreg Roach        return (int) $year;
3164a83f5d7SGreg Roach    }
3174a83f5d7SGreg Roach
3184a83f5d7SGreg Roach    /**
3194a83f5d7SGreg Roach     * Compare two dates, for sorting
3204a83f5d7SGreg Roach     *
3214a83f5d7SGreg Roach     * @param AbstractCalendarDate $d1
3224a83f5d7SGreg Roach     * @param AbstractCalendarDate $d2
3234a83f5d7SGreg Roach     *
3244a83f5d7SGreg Roach     * @return int
3254a83f5d7SGreg Roach     */
3264a83f5d7SGreg Roach    public static function compare(AbstractCalendarDate $d1, AbstractCalendarDate $d2): int
3274a83f5d7SGreg Roach    {
3284a83f5d7SGreg Roach        if ($d1->maximum_julian_day < $d2->minimum_julian_day) {
3294a83f5d7SGreg Roach            return -1;
3304a83f5d7SGreg Roach        }
3314a83f5d7SGreg Roach
3324a83f5d7SGreg Roach        if ($d2->maximum_julian_day < $d1->minimum_julian_day) {
3334a83f5d7SGreg Roach            return 1;
3344a83f5d7SGreg Roach        }
3354a83f5d7SGreg Roach
3364a83f5d7SGreg Roach        return 0;
3374a83f5d7SGreg Roach    }
3384a83f5d7SGreg Roach
3394a83f5d7SGreg Roach    /**
3404a83f5d7SGreg Roach     * Calculate the years/months/days between this date and another date.
3414a83f5d7SGreg Roach     * Results assume you add the days first, then the months.
3424a83f5d7SGreg Roach     * 4 February -> 3 July is 27 days (3 March) and 4 months.
3434a83f5d7SGreg Roach     * It is not 4 months (4 June) and 29 days.
3444a83f5d7SGreg Roach     *
3454a83f5d7SGreg Roach     * @param AbstractCalendarDate $date
3464a83f5d7SGreg Roach     *
34709482a55SGreg Roach     * @return array<int> Age in years/months/days
3484a83f5d7SGreg Roach     */
3494a83f5d7SGreg Roach    public function ageDifference(AbstractCalendarDate $date): array
3504a83f5d7SGreg Roach    {
3514a83f5d7SGreg Roach        // Incomplete dates
3524a83f5d7SGreg Roach        if ($this->year === 0 || $date->year === 0) {
3534a83f5d7SGreg Roach            return [-1, -1, -1];
3544a83f5d7SGreg Roach        }
3554a83f5d7SGreg Roach
3564a83f5d7SGreg Roach        // Overlapping dates
3574a83f5d7SGreg Roach        if (self::compare($this, $date) === 0) {
3584a83f5d7SGreg Roach            return [0, 0, 0];
3594a83f5d7SGreg Roach        }
3604a83f5d7SGreg Roach
3614a83f5d7SGreg Roach        // Perform all calculations using the calendar of the first date
36265e02381SGreg Roach        [$year1, $month1, $day1] = $this->calendar->jdToYmd($this->minimum_julian_day);
36365e02381SGreg Roach        [$year2, $month2, $day2] = $this->calendar->jdToYmd($date->minimum_julian_day);
3644a83f5d7SGreg Roach
3654a83f5d7SGreg Roach        $years  = $year2 - $year1;
3664a83f5d7SGreg Roach        $months = $month2 - $month1;
3674a83f5d7SGreg Roach        $days   = $day2 - $day1;
3684a83f5d7SGreg Roach
3694a83f5d7SGreg Roach        if ($days < 0) {
3704a83f5d7SGreg Roach            $days += $this->calendar->daysInMonth($year1, $month1);
3714a83f5d7SGreg Roach            $months--;
3724a83f5d7SGreg Roach        }
3734a83f5d7SGreg Roach
3744a83f5d7SGreg Roach        if ($months < 0) {
3754a83f5d7SGreg Roach            $months += $this->calendar->monthsInYear($year2);
3764a83f5d7SGreg Roach            $years--;
3774a83f5d7SGreg Roach        }
3784a83f5d7SGreg Roach
3794a83f5d7SGreg Roach        return [$years, $months, $days];
3804a83f5d7SGreg Roach    }
3814a83f5d7SGreg Roach
3824a83f5d7SGreg Roach    /**
3834a83f5d7SGreg Roach     * Convert a date from one calendar to another.
3844a83f5d7SGreg Roach     *
3854a83f5d7SGreg Roach     * @param string $calendar
3864a83f5d7SGreg Roach     *
3874a83f5d7SGreg Roach     * @return AbstractCalendarDate
3884a83f5d7SGreg Roach     */
3894a83f5d7SGreg Roach    public function convertToCalendar(string $calendar): AbstractCalendarDate
3904a83f5d7SGreg Roach    {
3914a83f5d7SGreg Roach        switch ($calendar) {
3924a83f5d7SGreg Roach            case 'gregorian':
3934a83f5d7SGreg Roach                return new GregorianDate($this);
3944a83f5d7SGreg Roach            case 'julian':
3954a83f5d7SGreg Roach                return new JulianDate($this);
3964a83f5d7SGreg Roach            case 'jewish':
3974a83f5d7SGreg Roach                return new JewishDate($this);
3984a83f5d7SGreg Roach            case 'french':
3994a83f5d7SGreg Roach                return new FrenchDate($this);
4004a83f5d7SGreg Roach            case 'hijri':
4014a83f5d7SGreg Roach                return new HijriDate($this);
4024a83f5d7SGreg Roach            case 'jalali':
4034a83f5d7SGreg Roach                return new JalaliDate($this);
4044a83f5d7SGreg Roach            default:
4054a83f5d7SGreg Roach                return $this;
4064a83f5d7SGreg Roach        }
4074a83f5d7SGreg Roach    }
4084a83f5d7SGreg Roach
4094a83f5d7SGreg Roach    /**
4104a83f5d7SGreg Roach     * Is this date within the valid range of the calendar
4114a83f5d7SGreg Roach     *
4124a83f5d7SGreg Roach     * @return bool
4134a83f5d7SGreg Roach     */
4144a83f5d7SGreg Roach    public function inValidRange(): bool
4154a83f5d7SGreg Roach    {
4164a83f5d7SGreg Roach        return $this->minimum_julian_day >= $this->calendar->jdStart() && $this->maximum_julian_day <= $this->calendar->jdEnd();
4174a83f5d7SGreg Roach    }
4184a83f5d7SGreg Roach
4194a83f5d7SGreg Roach    /**
4204a83f5d7SGreg Roach     * How many months in a year
4214a83f5d7SGreg Roach     *
4224a83f5d7SGreg Roach     * @return int
4234a83f5d7SGreg Roach     */
4244a83f5d7SGreg Roach    public function monthsInYear(): int
4254a83f5d7SGreg Roach    {
4264a83f5d7SGreg Roach        return $this->calendar->monthsInYear();
4274a83f5d7SGreg Roach    }
4284a83f5d7SGreg Roach
4294a83f5d7SGreg Roach    /**
4304a83f5d7SGreg Roach     * How many days in the current month
4314a83f5d7SGreg Roach     *
4324a83f5d7SGreg Roach     * @return int
4334a83f5d7SGreg Roach     */
4344a83f5d7SGreg Roach    public function daysInMonth(): int
4354a83f5d7SGreg Roach    {
4364a83f5d7SGreg Roach        try {
4374a83f5d7SGreg Roach            return $this->calendar->daysInMonth($this->year, $this->month);
43891495569SGreg Roach        } catch (InvalidArgumentException $ex) {
4394a83f5d7SGreg Roach            // calendar.php calls this with "DD MMM" dates, for which we cannot calculate
4404a83f5d7SGreg Roach            // the length of a month. Should we validate this before calling this function?
4414a83f5d7SGreg Roach            return 0;
4424a83f5d7SGreg Roach        }
4434a83f5d7SGreg Roach    }
4444a83f5d7SGreg Roach
4454a83f5d7SGreg Roach    /**
4464a83f5d7SGreg Roach     * How many days in the current week
4474a83f5d7SGreg Roach     *
4484a83f5d7SGreg Roach     * @return int
4494a83f5d7SGreg Roach     */
4504a83f5d7SGreg Roach    public function daysInWeek(): int
4514a83f5d7SGreg Roach    {
4524a83f5d7SGreg Roach        return $this->calendar->daysInWeek();
4534a83f5d7SGreg Roach    }
4544a83f5d7SGreg Roach
4554a83f5d7SGreg Roach    /**
4564a83f5d7SGreg Roach     * Format a date, using similar codes to the PHP date() function.
4574a83f5d7SGreg Roach     *
458ad3143ccSGreg Roach     * @param string $format    See https://php.net/date
4594a83f5d7SGreg Roach     * @param string $qualifier GEDCOM qualifier, so we can choose the right case for the month name.
4604a83f5d7SGreg Roach     *
4614a83f5d7SGreg Roach     * @return string
4624a83f5d7SGreg Roach     */
4634a83f5d7SGreg Roach    public function format(string $format, string $qualifier = ''): string
4644a83f5d7SGreg Roach    {
46552eb92f9SGreg Roach        // Dates can include additional punctuation and symbols. e.g.
46652eb92f9SGreg Roach        // %F %j, %Y
46752eb92f9SGreg Roach        // %Y. %F %d.
46852eb92f9SGreg Roach        // %Y年 %n月 %j日
46952eb92f9SGreg Roach        // %j. %F %Y
47052eb92f9SGreg Roach        // Don’t show exact details or unnecessary punctuation for inexact dates.
47152eb92f9SGreg Roach        if ($this->day === 0) {
47252eb92f9SGreg Roach            $format = strtr($format, ['%d' => '', '%j日' => '', '%j,' => '', '%j' => '', '%l' => '', '%D' => '', '%N' => '', '%S' => '', '%w' => '', '%z' => '']);
47352eb92f9SGreg Roach        }
47452eb92f9SGreg Roach        if ($this->month === 0) {
47552eb92f9SGreg Roach            $format = strtr($format, ['%F' => '', '%m' => '', '%M' => '', '年 %n月' => '', '%n' => '', '%t' => '']);
47652eb92f9SGreg Roach        }
47752eb92f9SGreg Roach        if ($this->year === 0) {
47852eb92f9SGreg Roach            $format = strtr($format, ['%t' => '', '%L' => '', '%G' => '', '%y' => '', '%Y年' => '', '%Y' => '']);
47952eb92f9SGreg Roach        }
48052eb92f9SGreg Roach        $format = trim($format, ',. /-');
4811934fe30SGreg Roach
48252eb92f9SGreg Roach        if ($this->day !== 0 && preg_match('/%[djlDNSwz]/', $format)) {
4834a83f5d7SGreg Roach            // If we have a day-number *and* we are being asked to display it, then genitive
4844a83f5d7SGreg Roach            $case = 'GENITIVE';
4854a83f5d7SGreg Roach        } else {
4864a83f5d7SGreg Roach            switch ($qualifier) {
4874a83f5d7SGreg Roach                case 'TO':
4884a83f5d7SGreg Roach                case 'ABT':
4894a83f5d7SGreg Roach                case 'FROM':
4904a83f5d7SGreg Roach                    $case = 'GENITIVE';
4914a83f5d7SGreg Roach                    break;
4924a83f5d7SGreg Roach                case 'AFT':
4934a83f5d7SGreg Roach                    $case = 'LOCATIVE';
4944a83f5d7SGreg Roach                    break;
4954a83f5d7SGreg Roach                case 'BEF':
4964a83f5d7SGreg Roach                case 'BET':
4974a83f5d7SGreg Roach                case 'AND':
4984a83f5d7SGreg Roach                    $case = 'INSTRUMENTAL';
4994a83f5d7SGreg Roach                    break;
5004a83f5d7SGreg Roach                case '':
5014a83f5d7SGreg Roach                case 'INT':
5024a83f5d7SGreg Roach                case 'EST':
5034a83f5d7SGreg Roach                case 'CAL':
5044a83f5d7SGreg Roach                default: // There shouldn't be any other options...
5054a83f5d7SGreg Roach                    $case = 'NOMINATIVE';
5064a83f5d7SGreg Roach                    break;
5074a83f5d7SGreg Roach            }
5084a83f5d7SGreg Roach        }
50952eb92f9SGreg Roach        // Build up the formatted date, character at a time
51052eb92f9SGreg Roach        if (str_contains($format, '%d')) {
51152eb92f9SGreg Roach            $format = strtr($format, ['%d' => $this->formatDayZeros()]);
51252eb92f9SGreg Roach        }
51352eb92f9SGreg Roach        if (str_contains($format, '%j')) {
51452eb92f9SGreg Roach            $format = strtr($format, ['%j' => $this->formatDay()]);
51552eb92f9SGreg Roach        }
51652eb92f9SGreg Roach        if (str_contains($format, '%l')) {
51752eb92f9SGreg Roach            $format = strtr($format, ['%l' => $this->formatLongWeekday()]);
51852eb92f9SGreg Roach        }
51952eb92f9SGreg Roach        if (str_contains($format, '%D')) {
52052eb92f9SGreg Roach            $format = strtr($format, ['%D' => $this->formatShortWeekday()]);
52152eb92f9SGreg Roach        }
52252eb92f9SGreg Roach        if (str_contains($format, '%N')) {
52352eb92f9SGreg Roach            $format = strtr($format, ['%N' => $this->formatIsoWeekday()]);
52452eb92f9SGreg Roach        }
52552eb92f9SGreg Roach        if (str_contains($format, '%w')) {
52652eb92f9SGreg Roach            $format = strtr($format, ['%w' => $this->formatNumericWeekday()]);
52752eb92f9SGreg Roach        }
52852eb92f9SGreg Roach        if (str_contains($format, '%z')) {
52952eb92f9SGreg Roach            $format = strtr($format, ['%z' => $this->formatDayOfYear()]);
53052eb92f9SGreg Roach        }
53152eb92f9SGreg Roach        if (str_contains($format, '%F')) {
53252eb92f9SGreg Roach            $format = strtr($format, ['%F' => $this->formatLongMonth($case)]);
53352eb92f9SGreg Roach        }
53452eb92f9SGreg Roach        if (str_contains($format, '%m')) {
53552eb92f9SGreg Roach            $format = strtr($format, ['%m' => $this->formatMonthZeros()]);
53652eb92f9SGreg Roach        }
53752eb92f9SGreg Roach        if (str_contains($format, '%M')) {
53852eb92f9SGreg Roach            $format = strtr($format, ['%M' => $this->formatShortMonth()]);
53952eb92f9SGreg Roach        }
54052eb92f9SGreg Roach        if (str_contains($format, '%n')) {
541b2d30019SGreg Roach            $format = strtr($format, ['%n' => $this->formatMonth()]);
54252eb92f9SGreg Roach        }
54352eb92f9SGreg Roach        if (str_contains($format, '%t')) {
544b2d30019SGreg Roach            $format = strtr($format, ['%t' => (string) $this->daysInMonth()]);
54552eb92f9SGreg Roach        }
54652eb92f9SGreg Roach        if (str_contains($format, '%L')) {
54752eb92f9SGreg Roach            $format = strtr($format, ['%L' => $this->isLeapYear() ? '1' : '0']);
54852eb92f9SGreg Roach        }
54952eb92f9SGreg Roach        if (str_contains($format, '%Y')) {
55052eb92f9SGreg Roach            $format = strtr($format, ['%Y' => $this->formatLongYear()]);
55152eb92f9SGreg Roach        }
55252eb92f9SGreg Roach        if (str_contains($format, '%y')) {
55352eb92f9SGreg Roach            $format = strtr($format, ['%y' => $this->formatShortYear()]);
55452eb92f9SGreg Roach        }
555dec352c1SGreg Roach        // These 4 extensions are useful for re-formatting gedcom dates.
55652eb92f9SGreg Roach        if (str_contains($format, '%@')) {
55752eb92f9SGreg Roach            $format = strtr($format, ['%@' => $this->formatGedcomCalendarEscape()]);
55852eb92f9SGreg Roach        }
55952eb92f9SGreg Roach        if (str_contains($format, '%A')) {
56052eb92f9SGreg Roach            $format = strtr($format, ['%A' => $this->formatGedcomDay()]);
56152eb92f9SGreg Roach        }
56252eb92f9SGreg Roach        if (str_contains($format, '%O')) {
56352eb92f9SGreg Roach            $format = strtr($format, ['%O' => $this->formatGedcomMonth()]);
56452eb92f9SGreg Roach        }
56552eb92f9SGreg Roach        if (str_contains($format, '%E')) {
56652eb92f9SGreg Roach            $format = strtr($format, ['%E' => $this->formatGedcomYear()]);
56752eb92f9SGreg Roach        }
56852eb92f9SGreg Roach
56952eb92f9SGreg Roach        return $format;
5704a83f5d7SGreg Roach    }
5714a83f5d7SGreg Roach
5724a83f5d7SGreg Roach    /**
5734a83f5d7SGreg Roach     * Generate the %d format for a date.
5744a83f5d7SGreg Roach     *
5754a83f5d7SGreg Roach     * @return string
5764a83f5d7SGreg Roach     */
5774a83f5d7SGreg Roach    protected function formatDayZeros(): string
5784a83f5d7SGreg Roach    {
5794a83f5d7SGreg Roach        if ($this->day > 9) {
5804a83f5d7SGreg Roach            return I18N::digits($this->day);
5814a83f5d7SGreg Roach        }
5824a83f5d7SGreg Roach
5834a83f5d7SGreg Roach        return I18N::digits('0' . $this->day);
5844a83f5d7SGreg Roach    }
5854a83f5d7SGreg Roach
5864a83f5d7SGreg Roach    /**
5874a83f5d7SGreg Roach     * Generate the %j format for a date.
5884a83f5d7SGreg Roach     *
5894a83f5d7SGreg Roach     * @return string
5904a83f5d7SGreg Roach     */
5914a83f5d7SGreg Roach    protected function formatDay(): string
5924a83f5d7SGreg Roach    {
5934a83f5d7SGreg Roach        return I18N::digits($this->day);
5944a83f5d7SGreg Roach    }
5954a83f5d7SGreg Roach
5964a83f5d7SGreg Roach    /**
5974a83f5d7SGreg Roach     * Generate the %l format for a date.
5984a83f5d7SGreg Roach     *
5994a83f5d7SGreg Roach     * @return string
6004a83f5d7SGreg Roach     */
6014a83f5d7SGreg Roach    protected function formatLongWeekday(): string
6024a83f5d7SGreg Roach    {
6034a83f5d7SGreg Roach        return $this->dayNames($this->minimum_julian_day % $this->calendar->daysInWeek());
6044a83f5d7SGreg Roach    }
6054a83f5d7SGreg Roach
6064a83f5d7SGreg Roach    /**
6074a83f5d7SGreg Roach     * Generate the %D format for a date.
6084a83f5d7SGreg Roach     *
6094a83f5d7SGreg Roach     * @return string
6104a83f5d7SGreg Roach     */
6114a83f5d7SGreg Roach    protected function formatShortWeekday(): string
6124a83f5d7SGreg Roach    {
6134a83f5d7SGreg Roach        return $this->dayNamesAbbreviated($this->minimum_julian_day % $this->calendar->daysInWeek());
6144a83f5d7SGreg Roach    }
6154a83f5d7SGreg Roach
6164a83f5d7SGreg Roach    /**
6174a83f5d7SGreg Roach     * Generate the %N format for a date.
6184a83f5d7SGreg Roach     *
6194a83f5d7SGreg Roach     * @return string
6204a83f5d7SGreg Roach     */
6214a83f5d7SGreg Roach    protected function formatIsoWeekday(): string
6224a83f5d7SGreg Roach    {
6234a83f5d7SGreg Roach        return I18N::digits($this->minimum_julian_day % 7 + 1);
6244a83f5d7SGreg Roach    }
6254a83f5d7SGreg Roach
6264a83f5d7SGreg Roach    /**
6274a83f5d7SGreg Roach     * Generate the %w format for a date.
6284a83f5d7SGreg Roach     *
6294a83f5d7SGreg Roach     * @return string
6304a83f5d7SGreg Roach     */
6314a83f5d7SGreg Roach    protected function formatNumericWeekday(): string
6324a83f5d7SGreg Roach    {
6334a83f5d7SGreg Roach        return I18N::digits(($this->minimum_julian_day + 1) % $this->calendar->daysInWeek());
6344a83f5d7SGreg Roach    }
6354a83f5d7SGreg Roach
6364a83f5d7SGreg Roach    /**
6374a83f5d7SGreg Roach     * Generate the %z format for a date.
6384a83f5d7SGreg Roach     *
6394a83f5d7SGreg Roach     * @return string
6404a83f5d7SGreg Roach     */
6414a83f5d7SGreg Roach    protected function formatDayOfYear(): string
6424a83f5d7SGreg Roach    {
6434a83f5d7SGreg Roach        return I18N::digits($this->minimum_julian_day - $this->calendar->ymdToJd($this->year, 1, 1));
6444a83f5d7SGreg Roach    }
6454a83f5d7SGreg Roach
6464a83f5d7SGreg Roach    /**
6474a83f5d7SGreg Roach     * Generate the %n format for a date.
6484a83f5d7SGreg Roach     *
6494a83f5d7SGreg Roach     * @return string
6504a83f5d7SGreg Roach     */
6514a83f5d7SGreg Roach    protected function formatMonth(): string
6524a83f5d7SGreg Roach    {
6534a83f5d7SGreg Roach        return I18N::digits($this->month);
6544a83f5d7SGreg Roach    }
6554a83f5d7SGreg Roach
6564a83f5d7SGreg Roach    /**
6574a83f5d7SGreg Roach     * Generate the %m format for a date.
6584a83f5d7SGreg Roach     *
6594a83f5d7SGreg Roach     * @return string
6604a83f5d7SGreg Roach     */
6614a83f5d7SGreg Roach    protected function formatMonthZeros(): string
6624a83f5d7SGreg Roach    {
6634a83f5d7SGreg Roach        if ($this->month > 9) {
6644a83f5d7SGreg Roach            return I18N::digits($this->month);
6654a83f5d7SGreg Roach        }
6664a83f5d7SGreg Roach
6674a83f5d7SGreg Roach        return I18N::digits('0' . $this->month);
6684a83f5d7SGreg Roach    }
6694a83f5d7SGreg Roach
6704a83f5d7SGreg Roach    /**
6714a83f5d7SGreg Roach     * Generate the %F format for a date.
6724a83f5d7SGreg Roach     *
6734a83f5d7SGreg Roach     * @param string $case Which grammatical case shall we use
6744a83f5d7SGreg Roach     *
6754a83f5d7SGreg Roach     * @return string
6764a83f5d7SGreg Roach     */
67773d58381SGreg Roach    protected function formatLongMonth(string $case = 'NOMINATIVE'): string
6784a83f5d7SGreg Roach    {
6794a83f5d7SGreg Roach        switch ($case) {
6804a83f5d7SGreg Roach            case 'GENITIVE':
6814a83f5d7SGreg Roach                return $this->monthNameGenitiveCase($this->month, $this->isLeapYear());
6824a83f5d7SGreg Roach            case 'NOMINATIVE':
6834a83f5d7SGreg Roach                return $this->monthNameNominativeCase($this->month, $this->isLeapYear());
6844a83f5d7SGreg Roach            case 'LOCATIVE':
6854a83f5d7SGreg Roach                return $this->monthNameLocativeCase($this->month, $this->isLeapYear());
6864a83f5d7SGreg Roach            case 'INSTRUMENTAL':
6874a83f5d7SGreg Roach                return $this->monthNameInstrumentalCase($this->month, $this->isLeapYear());
6884a83f5d7SGreg Roach            default:
68991495569SGreg Roach                throw new InvalidArgumentException($case);
6904a83f5d7SGreg Roach        }
6914a83f5d7SGreg Roach    }
6924a83f5d7SGreg Roach
6934a83f5d7SGreg Roach    /**
6947bb2eb25SGreg Roach     * Full month name in genitive case.
6957bb2eb25SGreg Roach     *
6967bb2eb25SGreg Roach     * @param int  $month
6977bb2eb25SGreg Roach     * @param bool $leap_year Some calendars use leap months
6987bb2eb25SGreg Roach     *
6997bb2eb25SGreg Roach     * @return string
7007bb2eb25SGreg Roach     */
7017bb2eb25SGreg Roach    abstract protected function monthNameGenitiveCase(int $month, bool $leap_year): string;
7027bb2eb25SGreg Roach
7037bb2eb25SGreg Roach    /**
7047bb2eb25SGreg Roach     * Full month name in nominative case.
7057bb2eb25SGreg Roach     *
7067bb2eb25SGreg Roach     * @param int  $month
7077bb2eb25SGreg Roach     * @param bool $leap_year Some calendars use leap months
7087bb2eb25SGreg Roach     *
7097bb2eb25SGreg Roach     * @return string
7107bb2eb25SGreg Roach     */
7117bb2eb25SGreg Roach    abstract protected function monthNameNominativeCase(int $month, bool $leap_year): string;
7127bb2eb25SGreg Roach
7137bb2eb25SGreg Roach    /**
7147bb2eb25SGreg Roach     * Full month name in locative case.
7157bb2eb25SGreg Roach     *
7167bb2eb25SGreg Roach     * @param int  $month
7177bb2eb25SGreg Roach     * @param bool $leap_year Some calendars use leap months
7187bb2eb25SGreg Roach     *
7197bb2eb25SGreg Roach     * @return string
7207bb2eb25SGreg Roach     */
7217bb2eb25SGreg Roach    abstract protected function monthNameLocativeCase(int $month, bool $leap_year): string;
7227bb2eb25SGreg Roach
7237bb2eb25SGreg Roach    /**
7247bb2eb25SGreg Roach     * Full month name in instrumental case.
7257bb2eb25SGreg Roach     *
7267bb2eb25SGreg Roach     * @param int  $month
7277bb2eb25SGreg Roach     * @param bool $leap_year Some calendars use leap months
7287bb2eb25SGreg Roach     *
7297bb2eb25SGreg Roach     * @return string
7307bb2eb25SGreg Roach     */
7317bb2eb25SGreg Roach    abstract protected function monthNameInstrumentalCase(int $month, bool $leap_year): string;
7327bb2eb25SGreg Roach
7337bb2eb25SGreg Roach    /**
7347bb2eb25SGreg Roach     * Abbreviated month name
7357bb2eb25SGreg Roach     *
7367bb2eb25SGreg Roach     * @param int  $month
7377bb2eb25SGreg Roach     * @param bool $leap_year Some calendars use leap months
7387bb2eb25SGreg Roach     *
7397bb2eb25SGreg Roach     * @return string
7407bb2eb25SGreg Roach     */
7417bb2eb25SGreg Roach    abstract protected function monthNameAbbreviated(int $month, bool $leap_year): string;
7427bb2eb25SGreg Roach
7437bb2eb25SGreg Roach    /**
7444a83f5d7SGreg Roach     * Generate the %M format for a date.
7454a83f5d7SGreg Roach     *
7464a83f5d7SGreg Roach     * @return string
7474a83f5d7SGreg Roach     */
7484a83f5d7SGreg Roach    protected function formatShortMonth(): string
7494a83f5d7SGreg Roach    {
7504a83f5d7SGreg Roach        return $this->monthNameAbbreviated($this->month, $this->isLeapYear());
7514a83f5d7SGreg Roach    }
7524a83f5d7SGreg Roach
7534a83f5d7SGreg Roach    /**
7544a83f5d7SGreg Roach     * Generate the %y format for a date.
7554a83f5d7SGreg Roach     * NOTE Short year is NOT a 2-digit year. It is for calendars such as hebrew
7564a83f5d7SGreg Roach     * which have a 3-digit form of 4-digit years.
7574a83f5d7SGreg Roach     *
7584a83f5d7SGreg Roach     * @return string
7594a83f5d7SGreg Roach     */
7604a83f5d7SGreg Roach    protected function formatShortYear(): string
7614a83f5d7SGreg Roach    {
7624a83f5d7SGreg Roach        return $this->formatLongYear();
7634a83f5d7SGreg Roach    }
7644a83f5d7SGreg Roach
7654a83f5d7SGreg Roach    /**
7664a83f5d7SGreg Roach     * Generate the %A format for a date.
7674a83f5d7SGreg Roach     *
7684a83f5d7SGreg Roach     * @return string
7694a83f5d7SGreg Roach     */
7704a83f5d7SGreg Roach    protected function formatGedcomDay(): string
7714a83f5d7SGreg Roach    {
772e364afe4SGreg Roach        if ($this->day === 0) {
7734a83f5d7SGreg Roach            return '';
7744a83f5d7SGreg Roach        }
7754a83f5d7SGreg Roach
7764a83f5d7SGreg Roach        return sprintf('%02d', $this->day);
7774a83f5d7SGreg Roach    }
7784a83f5d7SGreg Roach
7794a83f5d7SGreg Roach    /**
7804a83f5d7SGreg Roach     * Generate the %O format for a date.
7814a83f5d7SGreg Roach     *
7824a83f5d7SGreg Roach     * @return string
7834a83f5d7SGreg Roach     */
7844a83f5d7SGreg Roach    protected function formatGedcomMonth(): string
7854a83f5d7SGreg Roach    {
7864a83f5d7SGreg Roach        // Our simple lookup table doesn't work correctly for Adar on leap years
787e364afe4SGreg Roach        if ($this->month === 7 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) {
7884a83f5d7SGreg Roach            return 'ADR';
7894a83f5d7SGreg Roach        }
7904a83f5d7SGreg Roach
791*d8809d62SGreg Roach        return static::NUMBER_TO_MONTH[$this->month] ?? '';
7924a83f5d7SGreg Roach    }
7934a83f5d7SGreg Roach
7944a83f5d7SGreg Roach    /**
7954a83f5d7SGreg Roach     * Generate the %E format for a date.
7964a83f5d7SGreg Roach     *
7974a83f5d7SGreg Roach     * @return string
7984a83f5d7SGreg Roach     */
7994a83f5d7SGreg Roach    protected function formatGedcomYear(): string
8004a83f5d7SGreg Roach    {
801e364afe4SGreg Roach        if ($this->year === 0) {
8024a83f5d7SGreg Roach            return '';
8034a83f5d7SGreg Roach        }
8044a83f5d7SGreg Roach
8054a83f5d7SGreg Roach        return sprintf('%04d', $this->year);
8064a83f5d7SGreg Roach    }
8074a83f5d7SGreg Roach
8084a83f5d7SGreg Roach    /**
8094a83f5d7SGreg Roach     * Generate the %@ format for a calendar escape.
8104a83f5d7SGreg Roach     *
8114a83f5d7SGreg Roach     * @return string
8124a83f5d7SGreg Roach     */
8134a83f5d7SGreg Roach    protected function formatGedcomCalendarEscape(): string
8144a83f5d7SGreg Roach    {
8154a83f5d7SGreg Roach        return static::ESCAPE;
8164a83f5d7SGreg Roach    }
8174a83f5d7SGreg Roach
8184a83f5d7SGreg Roach    /**
8194a83f5d7SGreg Roach     * Generate the %Y format for a date.
8204a83f5d7SGreg Roach     *
8214a83f5d7SGreg Roach     * @return string
8224a83f5d7SGreg Roach     */
8234a83f5d7SGreg Roach    protected function formatLongYear(): string
8244a83f5d7SGreg Roach    {
8254a83f5d7SGreg Roach        return I18N::digits($this->year);
8264a83f5d7SGreg Roach    }
8274a83f5d7SGreg Roach
8284a83f5d7SGreg Roach    /**
8294a83f5d7SGreg Roach     * Which months follows this one? Calendars with leap-months should provide their own implementation.
8304a83f5d7SGreg Roach     *
83109482a55SGreg Roach     * @return array<int>
8324a83f5d7SGreg Roach     */
8334a83f5d7SGreg Roach    protected function nextMonth(): array
8344a83f5d7SGreg Roach    {
8354a83f5d7SGreg Roach        return [
8364a83f5d7SGreg Roach            $this->month === $this->calendar->monthsInYear() ? $this->nextYear($this->year) : $this->year,
8372cebb4b4SGreg Roach            $this->month % $this->calendar->monthsInYear() + 1,
8384a83f5d7SGreg Roach        ];
8394a83f5d7SGreg Roach    }
8404a83f5d7SGreg Roach
8414a83f5d7SGreg Roach    /**
8424a83f5d7SGreg Roach     * Get today’s date in the current calendar.
8434a83f5d7SGreg Roach     *
84409482a55SGreg Roach     * @return array<int>
8454a83f5d7SGreg Roach     */
8464a83f5d7SGreg Roach    public function todayYmd(): array
8474a83f5d7SGreg Roach    {
8484459dc9aSGreg Roach        return $this->calendar->jdToYmd(Carbon::now()->julianDay());
8494a83f5d7SGreg Roach    }
8504a83f5d7SGreg Roach
8514a83f5d7SGreg Roach    /**
8524a83f5d7SGreg Roach     * Convert to today’s date.
8534a83f5d7SGreg Roach     *
8544a83f5d7SGreg Roach     * @return AbstractCalendarDate
8554a83f5d7SGreg Roach     */
8564a83f5d7SGreg Roach    public function today(): AbstractCalendarDate
8574a83f5d7SGreg Roach    {
8584a83f5d7SGreg Roach        $tmp        = clone $this;
8594a83f5d7SGreg Roach        $ymd        = $tmp->todayYmd();
8604a83f5d7SGreg Roach        $tmp->year  = $ymd[0];
8614a83f5d7SGreg Roach        $tmp->month = $ymd[1];
8624a83f5d7SGreg Roach        $tmp->day   = $ymd[2];
8634a83f5d7SGreg Roach        $tmp->setJdFromYmd();
8644a83f5d7SGreg Roach
8654a83f5d7SGreg Roach        return $tmp;
8664a83f5d7SGreg Roach    }
8674a83f5d7SGreg Roach
8684a83f5d7SGreg Roach    /**
8694a83f5d7SGreg Roach     * Create a URL that links this date to the WT calendar
8704a83f5d7SGreg Roach     *
8714a83f5d7SGreg Roach     * @param string $date_format
87249d5f1d7SGreg Roach     * @param Tree   $tree
8734a83f5d7SGreg Roach     *
8744a83f5d7SGreg Roach     * @return string
8754a83f5d7SGreg Roach     */
87649d5f1d7SGreg Roach    public function calendarUrl(string $date_format, Tree $tree): string
8774a83f5d7SGreg Roach    {
878e364afe4SGreg Roach        if ($this->day !== 0 && strpbrk($date_format, 'dDj')) {
8794a83f5d7SGreg Roach            // If the format includes a day, and the date also includes a day, then use the day view
8804a83f5d7SGreg Roach            $view = 'day';
881e364afe4SGreg Roach        } elseif ($this->month !== 0 && strpbrk($date_format, 'FMmn')) {
8824a83f5d7SGreg Roach            // If the format includes a month, and the date also includes a month, then use the month view
8834a83f5d7SGreg Roach            $view = 'month';
8844a83f5d7SGreg Roach        } else {
8854a83f5d7SGreg Roach            // Use the year view
8864a83f5d7SGreg Roach            $view = 'year';
8874a83f5d7SGreg Roach        }
8884a83f5d7SGreg Roach
889b00cb080SGreg Roach        return route(CalendarPage::class, [
8904a83f5d7SGreg Roach            'cal'   => $this->calendar->gedcomCalendarEscape(),
8914a83f5d7SGreg Roach            'year'  => $this->formatGedcomYear(),
8924a83f5d7SGreg Roach            'month' => $this->formatGedcomMonth(),
8934a83f5d7SGreg Roach            'day'   => $this->formatGedcomDay(),
8944a83f5d7SGreg Roach            'view'  => $view,
895d72b284aSGreg Roach            'tree'  => $tree->name(),
8964a83f5d7SGreg Roach        ]);
8974a83f5d7SGreg Roach    }
8984a83f5d7SGreg Roach}
899