Skip to content

Commit 51b74db

Browse files
committed
Implements cal_to_jd
Fix #5
1 parent 766cb61 commit 51b74db

11 files changed

Lines changed: 403 additions & 19 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This project backports features found in the calendar extension.
44
It is intended to be used when calendar extension is not enabled.
55

66
Currently, functions available are:
7+
- [`cal_to_jd`](https://www.php.net/manual/en/function.cal-to-jd.php) — Converts from a supported calendar to Julian Day Count
78
- [`easter_date`](https://www.php.net/manual/en/function.easter-date.php) — Get Unix timestamp for midnight on Easter of a given year
89
- [`easter_days`](https://www.php.net/manual/en/function.easter-days.php) — Get number of days after March 21 on which Easter falls for a given year
910
- [`jdtogregorian`](https://www.php.net/manual/en/function.jdtogregorian.php) — Converts Julian Day Count to Gregorian date

psalm.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
</projectFiles>
1313

1414
<issueHandlers>
15+
<InvalidArrayOffset>
16+
<errorLevel type="suppress">
17+
<file name="src/Calendar.php" />
18+
</errorLevel>
19+
</InvalidArrayOffset>
20+
1521
<LessSpecificReturnType errorLevel="info" />
1622

1723
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->

runtime/bootstrap.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
declare(strict_types=1);
44

5+
use Roukmoute\Polyfill\Calendar\Calendar;
56
use Roukmoute\Polyfill\Calendar\Easter;
67
use Roukmoute\Polyfill\Calendar\Jewish;
78
use Roukmoute\Polyfill\Calendar\Julian;
@@ -40,3 +41,10 @@ function juliantojd(int $month, int $day, int $year): int
4041
return Julian::juliantojd($month, $day, $year);
4142
}
4243
}
44+
45+
if (!function_exists('cal_to_jd')) {
46+
function cal_to_jd(int $calendar, int $month, int $day, int $year)
47+
{
48+
return Calendar::cal_to_jd($calendar, $month, $day, $year);
49+
}
50+
}

src/Calendar.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Roukmoute\Polyfill\Calendar;
6+
7+
/**
8+
* The calendar extension presents a series of functions to simplify converting
9+
* between different calendar formats. The intermediary or standard it is based
10+
* on is the Julian Day Count. The Julian Day Count is a count of days starting
11+
* from January 1st, 4713 B.C. To convert between calendar systems, you must
12+
* first convert to Julian Day Count, then to the calendar system of your
13+
* choice. Julian Day Count is very different from the Julian Calendar!
14+
*
15+
* @see https://www.php.net/manual/en/book.calendar.php
16+
*/
17+
final class Calendar
18+
{
19+
public const CAL_GREGORIAN = 0;
20+
public const CAL_JULIAN = 1;
21+
public const CAL_JEWISH = 2;
22+
public const CAL_FRENCH = 3;
23+
public const CAL_NUM_CALS = 4;
24+
25+
public const CAL_CONVERSION_TABLE = [
26+
self::CAL_GREGORIAN => Gregor::class,
27+
self::CAL_JULIAN => Julian::class,
28+
self::CAL_JEWISH => Jewish::class,
29+
self::CAL_FRENCH => French::class,
30+
];
31+
32+
public function cal_to_jd(
33+
int $calendar,
34+
int $month,
35+
int $day,
36+
int $year
37+
): int {
38+
if ($calendar < 0 || $calendar >= self::CAL_NUM_CALS) {
39+
throw new ValueError('Argument #1 ($calendar) must be a valid calendar ID');
40+
}
41+
42+
/** @var SDNConversions $calendarType */
43+
$calendarType = self::CAL_CONVERSION_TABLE[$calendar];
44+
45+
return $calendarType::toSDN($year, $month, $day);
46+
}
47+
}

src/Easter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
/**
88
* Based on code by Simon Kershaw <simon@oremus.org>
9-
* @see: http://easter.oremus.org/when/bradley.html
9+
* @see http://easter.oremus.org/when/bradley.html
1010
*/
1111
final class Easter
1212
{

src/French.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Roukmoute\Polyfill\Calendar;
6+
7+
/**
8+
* French Serial Day Number (SDN) conversions
9+
*
10+
* @see https://github.com/php/php-src/blob/PHP-8.1.9/ext/calendar/french.c
11+
*/
12+
class French implements SDNConversions
13+
{
14+
private const FRENCH_SDN_OFFSET = 2375474;
15+
private const DAYS_PER_4_YEARS = 1461;
16+
private const DAYS_PER_MONTH = 30;
17+
18+
/**
19+
* Convert a French republican calendar date to a SDN.
20+
* {@inheritDoc}
21+
*/
22+
public static function toSDN(int $year, int $month, int $day): int
23+
{
24+
/* check for invalid dates */
25+
if ($year < 1 || $year > 14
26+
|| $month < 1
27+
|| $month > 13
28+
|| $day < 1
29+
|| $day > 30) {
30+
return 0;
31+
}
32+
33+
return (int) (($year * self::DAYS_PER_4_YEARS) / 4)
34+
+ ($month - 1) * self::DAYS_PER_MONTH
35+
+ $day
36+
+ self::FRENCH_SDN_OFFSET;
37+
}
38+
}

src/Gregor.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Roukmoute\Polyfill\Calendar;
6+
7+
/**
8+
* Gregorian Serial Day Number (SDN) conversions
9+
*
10+
* @see https://github.com/php/php-src/blob/PHP-8.1.9/ext/calendar/gregor.c
11+
*/
12+
class Gregor implements SDNConversions
13+
{
14+
private const GREGOR_SDN_OFFSET = 32045;
15+
private const DAYS_PER_5_MONTHS = 153;
16+
private const DAYS_PER_4_YEARS = 1461;
17+
private const DAYS_PER_400_YEARS = 146097;
18+
19+
/**
20+
* Convert a Gregorian republican calendar date to a SDN.
21+
* {@inheritDoc}
22+
*/
23+
public static function toSDN(int $year, int $month, int $day): int
24+
{
25+
/* check for invalid dates */
26+
if ($year === 0 || $year < -4714
27+
|| $month <= 0
28+
|| $month > 12
29+
|| $day <= 0
30+
|| $day > 31) {
31+
return 0;
32+
}
33+
34+
/* check for dates before SDN 1 (Nov 25, 4714 B.C.) */
35+
if ($year === -4714) {
36+
if ($month < 11) {
37+
return 0;
38+
}
39+
if ($month === 11 && $day < 25) {
40+
return 0;
41+
}
42+
}
43+
/* Make $year always a positive number. */
44+
if ($year < 0) {
45+
$year = $year + 4801;
46+
} else {
47+
$year = $year + 4800;
48+
}
49+
50+
/* Adjust the start of the $year. */
51+
if ($month > 2) {
52+
$month = $month - 3;
53+
} else {
54+
$month = $month + 9;
55+
--$year;
56+
}
57+
58+
return (int) (((int) ($year / 100) * self::DAYS_PER_400_YEARS) / 4)
59+
+ (int) ((($year % 100) * self::DAYS_PER_4_YEARS) / 4)
60+
+ (int) (($month * self::DAYS_PER_5_MONTHS + 2) / 5)
61+
+ $day
62+
- self::GREGOR_SDN_OFFSET;
63+
}
64+
}

src/Jewish.php

Lines changed: 133 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
namespace Roukmoute\Polyfill\Calendar;
66

7-
final class Jewish
7+
/**
8+
* Jewish Serial Day Number (SDN) conversions
9+
*
10+
* @see https://github.com/php/php-src/blob/PHP-8.1.9/ext/calendar/jewish.c
11+
*/
12+
final class Jewish implements SDNConversions
813
{
914
public const HALAKIM_PER_HOUR = 1080;
1015
public const HALAKIM_PER_DAY = 25920;
@@ -85,17 +90,110 @@ public static function jewishtojd(int $month, int $day, int $year): int
8590

8691
$lengthOfAdarIAndII = (self::getMonthsInYear(($year - 1) % 19) == 12) ? 29 : 59;
8792

88-
if ($month === 4) {
89-
$sdn = $tishri1After + $day - $lengthOfAdarIAndII - 237;
90-
} elseif ($month === 5) {
91-
$sdn = $tishri1After + $day - $lengthOfAdarIAndII - 208;
93+
$sdn = self::calSDN($month, (int) $tishri1After, $day, $lengthOfAdarIAndII);
94+
break;
95+
96+
default:
97+
/* It is Adar II or later - don't need the year length. */
98+
self::findStartOfYear($year + 1, $metonicCycle, $metonicYear, $moladDay, $moladHalakim, $tishri1After);
99+
100+
switch ($month) {
101+
case 7:
102+
$sdn = $tishri1After + $day - 207;
103+
break;
104+
case 8:
105+
$sdn = $tishri1After + $day - 178;
106+
break;
107+
case 9:
108+
$sdn = $tishri1After + $day - 148;
109+
break;
110+
case 10:
111+
$sdn = $tishri1After + $day - 119;
112+
break;
113+
case 11:
114+
$sdn = $tishri1After + $day - 89;
115+
break;
116+
case 12:
117+
$sdn = $tishri1After + $day - 60;
118+
break;
119+
case 13:
120+
$sdn = $tishri1After + $day - 30;
121+
break;
122+
default:
123+
return 0;
124+
}
125+
}
126+
127+
return $sdn + self::JEWISH_SDN_OFFSET;
128+
}
129+
130+
/**
131+
* Convert a Jewish republican calendar date to a SDN.
132+
* {@inheritDoc}
133+
*/
134+
public static function toSDN(int $year, int $month, int $day): int
135+
{
136+
$metonicCycle = 0;
137+
$metonicYear = 0;
138+
$tishri1 = 0;
139+
$tishri1After = 0;
140+
$moladDay = 0;
141+
$moladHalakim = 0;
142+
143+
if ($year <= 0 || $day <= 0 || $day > 30) {
144+
return 0;
145+
}
146+
switch ($month) {
147+
case 1:
148+
case 2:
149+
/* It is Tishri or Heshvan - don't need the $year length. */
150+
self::findStartOfYear($year, $metonicCycle, $metonicYear, $moladDay, $moladHalakim, $tishri1);
151+
if ($month == 1) {
152+
$sdn = $tishri1 + $day - 1;
153+
} else {
154+
$sdn = $tishri1 + $day + 29;
155+
}
156+
break;
157+
158+
case 3:
159+
/* It is Kislev - must find the $year length. */
160+
161+
/* Find the start of the $year. */
162+
self::findStartOfYear($year, $metonicCycle, $metonicYear, $moladDay, $moladHalakim, ${$tishri1});
163+
164+
/* Find the end of the $year. */
165+
$moladHalakim += self::HALAKIM_PER_LUNAR_CYCLE * self::getMonthsInYear($metonicYear);
166+
$moladDay += (int) ($moladHalakim / self::HALAKIM_PER_DAY);
167+
$moladHalakim %= self::HALAKIM_PER_DAY;
168+
$tishri1After = self::tishri1(($metonicYear + 1) % 19, $moladDay, $moladHalakim);
169+
170+
$yearLength = $tishri1After - $tishri1;
171+
172+
if ($yearLength === 355 || $yearLength === 385) {
173+
$sdn = $tishri1 + $day + 59;
92174
} else {
93-
$sdn = $tishri1After + $day - $lengthOfAdarIAndII - 178;
175+
$sdn = $tishri1 + $day + 58;
94176
}
95177
break;
96178

179+
case 4:
180+
case 5:
181+
case 6:
182+
/* It is Tevet, Shevat or Adar I - don't need the $year length. */
183+
184+
self::findStartOfYear($year + 1, $metonicCycle, $metonicYear, $moladDay, $moladHalakim, $tishri1After);
185+
186+
if (self::getMonthsInYear(($year - 1) % 19) === 12) {
187+
$lengthOfAdarIAndII = 29;
188+
} else {
189+
$lengthOfAdarIAndII = 59;
190+
}
191+
192+
$sdn = self::calSDN($month, (int) $tishri1After, $day, $lengthOfAdarIAndII);
193+
break;
194+
97195
default:
98-
/* It is Adar II or later - don't need the year length. */
196+
/* It is Adar II or later - don't need the $year length. */
99197
self::findStartOfYear($year + 1, $metonicCycle, $metonicYear, $moladDay, $moladHalakim, $tishri1After);
100198

101199
switch ($month) {
@@ -128,6 +226,23 @@ public static function jewishtojd(int $month, int $day, int $year): int
128226
return $sdn + self::JEWISH_SDN_OFFSET;
129227
}
130228

229+
private static function calSDN(
230+
int $month,
231+
int $tishri1After,
232+
int $day,
233+
int $lengthOfAdarIAndII
234+
): int {
235+
if ($month === 4) {
236+
$sdn = $tishri1After + $day - $lengthOfAdarIAndII - 237;
237+
} elseif ($month === 5) {
238+
$sdn = $tishri1After + $day - $lengthOfAdarIAndII - 208;
239+
} else {
240+
$sdn = $tishri1After + $day - $lengthOfAdarIAndII - 178;
241+
}
242+
243+
return $sdn;
244+
}
245+
131246
private static function getMonthsInYear($metonicYear): int
132247
{
133248
return self::$monthsPerYear[(int) $metonicYear] ?? 12;
@@ -157,16 +272,21 @@ private static function tishri1(int $metonicYear, int $moladDay, int $moladHalak
157272
$tishri1 = $moladDay;
158273
$dow = $tishri1 % 7;
159274
$leapYear = $metonicYear === 2 || $metonicYear === 5 || $metonicYear === 7
160-
|| $metonicYear === 10 || $metonicYear === 13 || $metonicYear === 16
275+
|| $metonicYear === 10
276+
|| $metonicYear === 13
277+
|| $metonicYear === 16
161278
|| $metonicYear === 18;
162279
$lastWasLeapYear = $metonicYear === 3 || $metonicYear === 6
163-
|| $metonicYear === 8 || $metonicYear === 11 || $metonicYear === 14
164-
|| $metonicYear === 17 || $metonicYear === 0;
280+
|| $metonicYear === 8
281+
|| $metonicYear === 11
282+
|| $metonicYear === 14
283+
|| $metonicYear === 17
284+
|| $metonicYear === 0;
165285

166286
/* Apply rules 2, 3 and 4. */
167-
if (($moladHalakim >= self::NOON) ||
168-
((!$leapYear) && $dow === self::TUESDAY && $moladHalakim >= self::AM3_11_20) ||
169-
($lastWasLeapYear && $dow === self::MONDAY && $moladHalakim >= self::AM9_32_43)
287+
if (($moladHalakim >= self::NOON)
288+
|| ((!$leapYear) && $dow === self::TUESDAY && $moladHalakim >= self::AM3_11_20)
289+
|| ($lastWasLeapYear && $dow === self::MONDAY && $moladHalakim >= self::AM9_32_43)
170290
) {
171291
++$tishri1;
172292
++$dow;

0 commit comments

Comments
 (0)