1<?php
2namespace Fisharebest\ExtCalendar;
3
4use InvalidArgumentException;
5
6/**
7 * Class PersianCalendar - calculations for the Persian (Jalali) calendar.
8 *
9 * Algorithms for Julian days based on https://www.fourmilab.ch/documents/calendar/
10 *
11 * @author    Greg Roach <fisharebest@gmail.com>
12 * @copyright (c) 2014-2017 Greg Roach
13 * @license   This program is free software: you can redistribute it and/or modify
14 *            it under the terms of the GNU General Public License as published by
15 *            the Free Software Foundation, either version 3 of the License, or
16 *            (at your option) any later version.
17 *
18 *            This program is distributed in the hope that it will be useful,
19 *            but WITHOUT ANY WARRANTY; without even the implied warranty of
20 *            MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 *            GNU General Public License for more details.
22 *
23 *            You should have received a copy of the GNU General Public License
24 *            along with this program.  If not, see <http://www.gnu.org/licenses/>.
25 */
26class PersianCalendar implements CalendarInterface
27{
28    /**
29     * In each 128 year cycle, the following years are leap years.
30     *
31     * @var int[]
32     */
33    private static $LEAP_YEAR_CYCLE = array(
34        0, 5, 9, 13, 17, 21, 25, 29, 34, 38, 42, 46, 50, 54, 58, 62, 67, 71, 75, 79, 83, 87, 91, 95, 100, 104, 108, 112, 116, 120, 124
35    );
36
37    /**
38     * Determine the number of days in a specified month, allowing for leap years, etc.
39     *
40     * @param int $year
41     * @param int $month
42     *
43     * @return int
44     */
45    public function daysInMonth($year, $month)
46    {
47        if ($month <= 6) {
48            return 31;
49        } elseif ($month <= 11 || $this->isLeapYear($year)) {
50            return 30;
51        } else {
52            return 29;
53        }
54    }
55
56    /**
57     * Determine the number of days in a week.
58     *
59     * @return int
60     */
61    public function daysInWeek()
62    {
63        return 7;
64    }
65
66    /**
67     * The escape sequence used to indicate this calendar in GEDCOM files.
68     *
69     * @return string
70     */
71    public function gedcomCalendarEscape()
72    {
73        return '@#DJALALI@';
74    }
75
76    /**
77     * Determine whether or not a given year is a leap-year.
78     *
79     * @param int $year
80     *
81     * @return bool
82     */
83    public function isLeapYear($year)
84    {
85        return in_array((($year + 2346) % 2820) % 128, self::$LEAP_YEAR_CYCLE);
86    }
87
88    /**
89     * What is the highest Julian day number that can be converted into this calendar.
90     *
91     * @return int
92     */
93    public function jdEnd()
94    {
95        return PHP_INT_MAX;
96    }
97
98    /**
99     * What is the lowest Julian day number that can be converted into this calendar.
100     *
101     * @return int
102     */
103    public function jdStart()
104    {
105        return 1948321; // 1 Farvardīn 0001 AP, 19 MAR 0622 AD
106    }
107
108    /**
109     * Convert a Julian day number into a year/month/day.
110     *
111     * @param int $julian_day
112     *
113     * @return int[]
114     */
115    public function jdToYmd($julian_day)
116    {
117        $depoch = $julian_day - 2121446; // 1 Farvardīn 475
118        $cycle  = (int) floor($depoch / 1029983);
119        $cyear  = $this->mod($depoch, 1029983);
120        if ($cyear == 1029982) {
121            $ycycle = 2820;
122        } else {
123            $aux1   = (int) ($cyear / 366);
124            $aux2   = $cyear % 366;
125            $ycycle = (int) (((2134 * $aux1) + (2816 * $aux2) + 2815) / 1028522) + $aux1 + 1;
126        }
127        $year = $ycycle + (2820 * $cycle) + 474;
128
129        // If we allowed negative years, we would deal with them here.
130        $yday  = $julian_day - $this->ymdToJd($year, 1, 1) + 1;
131        $month = ($yday <= 186) ? ceil($yday / 31) : ceil(($yday - 6) / 30);
132        $day   = $julian_day - $this->ymdToJd($year, $month, 1) + 1;
133
134        return array((int) $year, (int) $month, (int) $day);
135    }
136
137    /**
138     * Determine the number of months in a year (if given),
139     * or the maximum number of months in any year.
140     *
141     * @param int|null $year
142     *
143     * @return int
144     */
145    public function monthsInYear($year = null)
146    {
147        return 12;
148    }
149
150    /**
151     * Convert a year/month/day to a Julian day number.
152     *
153     * @param int $year
154     * @param int $month
155     * @param int $day
156     *
157     * @return int
158     */
159    public function ymdToJd($year, $month, $day)
160    {
161        if ($month < 1 || $month > $this->monthsInYear()) {
162            throw new InvalidArgumentException('Month ' . $month . ' is invalid for this calendar');
163        }
164
165        $epbase = $year - (($year >= 0) ? 474 : 473);
166        $epyear = 474 + $this->mod($epbase, 2820);
167
168        return
169            $day +
170            (($month <= 7) ? (($month - 1) * 31) : ((($month - 1) * 30) + 6)) +
171            (int) ((($epyear * 682) - 110) / 2816) +
172            ($epyear - 1) * 365 +
173            (int) (floor($epbase / 2820)) * 1029983 +
174            $this->jdStart() - 1;
175    }
176
177    /**
178     * The PHP modulus function returns a negative modulus for a negative dividend.
179     * This algorithm requires a "traditional" modulus function where the modulus is
180     * always positive.
181     *
182     * @param int $dividend
183     * @param int $divisor
184     *
185     * @return int
186     */
187    public function mod($dividend, $divisor)
188    {
189        if ($divisor === 0) {
190            return 0;
191        }
192
193        $modulus = $dividend % $divisor;
194        if ($modulus < 0) {
195            $modulus += $divisor;
196        }
197
198        return $modulus;
199    }
200}
201