Hebrew calendar

The immutable Hebrew (Jewish) date class with ordinal month numbers, Adar I and Adar II in leap years, and month names in English, Hebrew and Persian. Create, parse, format, shift and measure dates.

On this page
  1. What the class does
  2. Month numbering
  3. Creating dates
    1. String semantics
  4. Supported range and errors
  5. Reading values
    1. Weekday numbering
  6. Formatting
  7. Shifting and snapping
  8. Comparing and measuring
  9. Calendar arithmetic

What the class does#

RtlyKit\Calendar\Hebrew is an immutable date-time value in the Hebrew (Jewish) calendar. It is plain PHP and needs no ext-calendar. It implements the fixed arithmetic calendar: the molad of Tishrei, the four dehiyyot postponements, and year lengths of 353, 354 or 355 days (regular) and 383, 384 or 385 days (leap). Like the other calendars, it holds one DateTimeImmutable and implements the CalendarDate contract (see Convert and compare dates).

<?php
require 'vendor/autoload.php';

use RtlyKit\Calendar\Hebrew;
use RtlyKit\Exceptions\InvalidDateException;
use function RtlyKit\hebrew_date;   // namespaced helper, same as Hebrew::make()

$utc = new DateTimeZone('UTC');
Good to know. A Hebrew day begins at sunset. This class, like the other calendars in the library, changes the date at civil midnight. So an evening time, such as the eve of a holiday, still shows the Hebrew date of the same civil day, even though by the religious count the next date has already begun. The class does not compute holidays, parashot or the Omer count.

Month numbering#

Months are numbered by their position in the year, counting from Tishrei (the civil new year). In a leap year, Adar I and Adar II are two separate months:

  • Months 1 to 5 are always Tishrei, Cheshvan, Kislev, Tevet, Shevat.
  • Regular year: 6 = Adar, 7 = Nisan, 8 = Iyar, 9 = Sivan, 10 = Tammuz, 11 = Av, 12 = Elul.
  • Leap year: 6 = Adar I, 7 = Adar II, 8 = Nisan, 9 = Iyar, 10 = Sivan, 11 = Tammuz, 12 = Av, 13 = Elul.
Good to know. This numbering differs from Hebcal (Nisan = 1) and from PHP ext-calendar. Elul is month 12 in a regular year and 13 in a leap year, and Nisan is month 7 or 8. Take month numbers from Hebrew itself (getMonth(), monthName(), monthsInYear()) and not from tables of another library.
foreach ([5784, 5785] as $y) {
    $a = [];
    for ($m = 1; $m <= Hebrew::monthsInYear($y); $m++) {
        $a[] = $m.':'.Hebrew::monthName($y, $m).'('.Hebrew::daysInMonth($y, $m).')';
    }
    echo $y, ' ', implode(' ', $a), "\n";
}
// 5784 1:Tishrei(30) 2:Cheshvan(29) 3:Kislev(29) 4:Tevet(29) 5:Shevat(30) 6:Adar I(30) 7:Adar II(29) 8:Nisan(30) 9:Iyar(29) 10:Sivan(30) 11:Tammuz(29) 12:Av(30) 13:Elul(29)
// 5785 1:Tishrei(30) 2:Cheshvan(30) 3:Kislev(30) 4:Tevet(29) 5:Shevat(30) 6:Adar(29) 7:Nisan(30) 8:Iyar(29) 9:Sivan(30) 10:Tammuz(29) 11:Av(30) 12:Elul(29)

Hebrew::monthName($year, $month, $locale = 'en') supports en, he and fa. An unknown locale uses en. In a regular year, slot 6 is the plain Adar.

Creating dates#

Hebrew::create($year, $month, $day, $hour = 0, $minute = 0, $second = 0, $timezone = null) checks the date against the real length of that month in that year. Cheshvan and Kislev change between 29 and 30 days.

$e = Hebrew::create(5785, 1, 1, 0, 0, 0, $utc);

echo $e;                                   // 5785/01/01 00:00:00
echo $e->toGregorian()->format('Y-m-d');   // 2024-10-03
echo Hebrew::create(5785, 7, 15, 0, 0, 0, $utc)->toGregorian()->format('Y-m-d');  // 2025-04-13  (15 Nisan 5785)
echo Hebrew::create(5786, 1, 10, 0, 0, 0, $utc)->toGregorian()->format('Y-m-d l'); // 2025-10-02 Thursday

Hebrew::make($time = null, $timezone = null) accepts a DateTimeInterface, any CalendarDate, an int timestamp, a string or null. hebrew_date() is the helper. A Hebrew instance comes back unchanged.

echo Hebrew::make('2025-10-07', $utc);                       // 5786/01/15 00:00:00
echo hebrew_date('2025-10-07')->format('Y/m/d F');           // 5786/01/15 Tishrei
echo Hebrew::make('5785/01/10', $utc);                       // 5785/01/10 00:00:00
echo Hebrew::make('0001-09-06')->format('Y/m/d');            // 3762/01/01

String semantics#

  1. Persian and Arabic digits become English digits, and the string is trimmed. An empty string throws InvalidDateException.
  2. A string like Y/m/d or Y-m-d (optional H:i[:s]) with a year of 3000 or more is read as a Hebrew date, with the month numbers above. The threshold works the other way round from Jalali and Hijri, which read years below 1700 as their own.
  3. Every other string is read as Gregorian or free text.

So '5785/01/10' is Hebrew and '2025-10-07' is Gregorian. '3000/01/01' is read as Hebrew year 3000. That is below the supported minimum, so it throws Invalid Hebrew date: 3000/1/1.

Supported range and errors#

Hebrew years Hebrew::MIN_YEAR = 3762 to Hebrew::MAX_YEAR = 13759 are supported. These are the years inside Gregorian years 1 to 9999. The first day is 1 Tishrei 3762 (0001-09-06) and the last is the last day of Elul 13759 (9999-11-03). Anything outside throws InvalidDateException. isValid() returns false instead.

foreach ([[3761, 1, 1], [13760, 1, 1], [5785, 13, 1], [5785, 3, 31]] as [$y, $m, $d]) {
    try { Hebrew::create($y, $m, $d); }
    catch (InvalidDateException $x) { echo $x->getMessage(), "\n"; }
}
// Invalid Hebrew date: 3761/1/1
// Invalid Hebrew date: 13760/1/1
// Invalid Hebrew date: 5785/13/1       (5785 is a regular year: 12 months)
// Invalid Hebrew date: 5785/3/31

echo Hebrew::create(5784, 13, 1) instanceof Hebrew ? 'ok' : '';   // ok  (5784 is a leap year)

try { Hebrew::make('0001-01-01'); }
catch (InvalidDateException $x) { echo $x->getMessage(); }
// Date out of the supported Hebrew range (3762..13759): 3761

The pure arithmetic helpers daysInYear(), daysInMonth() and monthName() accept years 1 to 13759. Year 0 throws Hebrew year out of the supported range: 0. Only building a date uses the higher minimum of 3762.

Reading values#

The getters are the ones in the contract: getYear(), getMonth(), getDay(), the time parts, getTimestamp(), getTimezone(), toGregorian(), toDateString() and toDateTimeString(). getMonth() is the month number described above.

Weekday numbering#

getDayOfWeek() and the w token start on Sunday: 0 = Sunday to 6 = Saturday. Hijri and PHP do the same. Jalali does not. N is the ISO number (Monday = 1).

echo $e->getDayOfWeek();   // 4   (1 Tishrei 5785 was a Thursday)
foreach (range(0, 6) as $i) { echo $e->addDays($i)->getDayOfWeek(); }  // 4560123

Formatting#

format($pattern = 'Y/m/d H:i:s', $locale = 'en'), where $locale is en, he or fa. Digits are always Latin. A backslash escapes the next character, and a trailing backslash is dropped. A pattern longer than Hebrew::MAX_FORMAT_LENGTH (256 bytes) throws InvalidDateException with the code input_too_long.

echo $e->format('l j F Y');         // Thursday 1 Tishrei 5785
echo $e->format('l j F Y', 'he');   // חמישי 1 תשרי 5785
echo $e->format('l j F Y', 'fa');   // پنجشنبه 1 تشری 5785

echo Hebrew::create(5784, 6, 15, 0, 0, 0, $utc)->format('j F');   // 15 Adar I
echo Hebrew::create(5784, 7, 15, 0, 0, 0, $utc)->format('j F');   // 15 Adar II
TokenMeaning
Y y m n d jYear, month (by position), day
H G h g i sTime of day (24-hour and 12-hour)
F MMonth name in the locale (Adar I, Adar II or Adar as needed)
lWeekday name in the locale
w NSunday = 0. ISO Monday = 1
z t LZero-based day of the year. Days in the month. 1 for a leap year (13 months)
a Aam/pm and AM/PM
S W c rEmpty. ISO week of the Gregorian moment. Y-m-d\TH:i:sP with the Hebrew date. RFC 2822
U e T P p O Z I u vTaken from the inner moment

Shifting and snapping#

The methods are the same as for Jalali. Two Hebrew rules matter:

  • Months are counted by position, with Adar I and Adar II as two months. The day is clamped to the length of the target month. The work stays small however large $months is (whole 19-year cycles plus at most 19 yearly steps).
  • Years keep the same month. Moving from a leap year to a regular year maps Adar I and Adar II to Adar. Moving from a regular year to a leap year maps Adar to Adar II.
$adar1 = Hebrew::create(5784, 6, 15, 0, 0, 0, $utc);
echo $adar1->addMonths(1)->format('Y/m/d F');                                  // 5784/07/15 Adar II
echo Hebrew::create(5784, 7, 15, 0, 0, 0, $utc)->addYears(1)->format('Y/m/d F'); // 5785/06/15 Adar
echo Hebrew::create(5785, 2, 29, 0, 0, 0, $utc)->addMonths(1);                   // 5785/03/29 00:00:00
echo Hebrew::create(5785, 5, 30, 0, 0, 0, $utc)->addMonths(1)->format('Y/m/d F'); // 5785/06/29 Adar  (clamped)
echo $e->endOfMonth();   // 5785/01/30 23:59:59
echo $e->endOfYear();    // 5785/12/29 23:59:59

Comparing and measuring#

Comparison works on moments and accepts any calendar. diffInMonths() counts whole Hebrew months, and Adar I and II count separately. diffInYears() counts by anniversary. A Hebrew year has 12 or 13 months, so the result is not months divided by 12.

echo Hebrew::create(5784, 1, 1, 0, 0, 0, $utc)->diffInMonths(Hebrew::create(5785, 1, 1, 0, 0, 0, $utc)); // 13
echo Hebrew::create(5780, 3, 3, 0, 0, 0, $utc)->diffInYears(Hebrew::create(5785, 3, 2, 0, 0, 0, $utc));  // 4

Calendar arithmetic#

MethodReturns
Hebrew::isValid($y, $m, $d)bool
Hebrew::isLeapYear($y)True for years 3, 6, 8, 11, 14, 17, 19 of the 19-year cycle
Hebrew::monthsInYear($y)12 or 13
Hebrew::daysInYear($y)353, 354, 355, 383, 384 or 385
Hebrew::daysInMonth($y, $m)29 or 30. Throws for a month beyond the month count of the year
Hebrew::gregorianToHebrew($gy, $gm, $gd)[year, month, day]
Hebrew::hebrewToGregorian($hy, $hm, $hd)[year, month, day]
echo (Hebrew::isLeapYear(5784) ? 'leap' : 'regular'), ' ', (Hebrew::isLeapYear(5785) ? 'leap' : 'regular'); // leap regular
echo Hebrew::daysInYear(5784), ' ', Hebrew::daysInYear(5785), ' ', Hebrew::daysInYear(5786);             // 383 355 354
echo json_encode(Hebrew::gregorianToHebrew(2025, 10, 3));   // [5786,1,11]
echo json_encode(Hebrew::hebrewToGregorian(5786, 1, 1));    // [2025,9,23]

Related: Convert and compare dates for cross-calendar work, and Carbon macros for toHebrew() and createFromHebrew().