xref: /webtrees/app/Date/AbstractCalendarDate.php (revision f4c767fd89cdb62ee54edec032285924cd767af7)
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