Skip to the guide
All guides

Hijri calendar

The immutable Hijri (Islamic) date class with the Umm al-Qura month table and an arithmetic Tabular variant. Create, parse and format dates in Arabic, Persian or English, shift them, compare them and measure the gap.

app RTLY-Kit 0.2.0checked reading 11 minutes

On this page

What the class does

RtlyKit\Calendar\Hijri is an immutable date-time value in the Islamic (Hijri) calendar. Like the other calendar classes, it holds one DateTimeImmutable and implements the shared CalendarDate contract (see Convert and compare dates). What is special about Hijri is the variant: the rule set that decides how long each month is. Every instance carries its variant, and every modifier keeps it.

The examples assume this header (see Installation):

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

use RtlyKit\Calendar\Hijri;
use RtlyKit\Calendar\HijriVariant;
use RtlyKit\Calendar\Jalali;
use RtlyKit\Exceptions\InvalidDateException;
use function RtlyKit\hdate;   // namespaced helper, same as Hijri::make()

$utc = new DateTimeZone('UTC');

Variants: Umm al-Qura and Tabular

VariantHow month lengths are decided
HijriVariant::UmmAlQura (default)An embedded month-length table for AH 1300 to 1500 (1882-11-12 to 2077-11-16). Outside it, the Tabular rules are used.
HijriVariant::TabularThe arithmetic civil calendar: a 30-year cycle with 11 leap years. It works for every year. It can differ from Umm al-Qura by one or two days.
PHP
$uq  = Hijri::create(1446, 10, 1, 0, 0, 0, $utc);                       // Umm al-Qura
$tab = Hijri::create(1446, 10, 1, 0, 0, 0, $utc, HijriVariant::Tabular);

echo $uq->toGregorian()->format('Y-m-d');   // 2025-03-30
echo $tab->toGregorian()->format('Y-m-d');  // 2025-03-31
echo $uq->getVariant()->name;               // UmmAlQura

Two more anchors: 1 Ramadan 1446 is 2025-03-01 and 1 Muharram 1447 is 2025-06-26 (Umm al-Qura).

Good to know. The Umm al-Qura table was built from the ICU/CLDR islamic-umalqura data. On 2026-10-08 we compared every month start from AH 1318 to AH 1500 (2196 months) with the official KACST Umm Al-Qura calendar and found no difference. The official site does not offer AH 1300 to 1317, so those years rest on the ICU/CLDR data alone. Neither variant follows moon sighting, which some countries use and which can differ by a day. Day boundaries follow civil midnight, not sunset. For religious dates that an authority announces, use the announcement.

Table coverage

Outside AH 1300 to 1500, create() and make() do not throw. They continue with the Tabular rules, and the two rule sets may not join smoothly at the border. Use Hijri::hasUmmAlQuraData($year) and $date->usesUmmAlQuraTable() to see which rule applied. Hijri::ummAlQuraVerifiedRange() returns [1318, 1500], the years checked against KACST, and Hijri::isUmmAlQuraVerified($year) tells you if one year is inside it.

PHP
echo Hijri::hasUmmAlQuraData(1299) ? 1 : 0;   // 0
echo Hijri::hasUmmAlQuraData(1300) ? 1 : 0;   // 1
echo Hijri::hasUmmAlQuraData(1500) ? 1 : 0;   // 1
echo Hijri::hasUmmAlQuraData(1501) ? 1 : 0;   // 0

echo json_encode(Hijri::ummAlQuraVerifiedRange());          // [1318,1500]
var_dump(Hijri::isUmmAlQuraVerified(1317));                 // bool(false)
var_dump(Hijri::isUmmAlQuraVerified(1447));                 // bool(true)

$o = Hijri::create(1600, 1, 1, 0, 0, 0, $utc);
echo $o->toGregorian()->format('Y-m-d');     // 2173-12-06
var_dump($o->usesUmmAlQuraTable());            // bool(false)  (tabular extrapolation)
var_dump(Hijri::create(1446, 9, 1, 0, 0, 0, $utc)->usesUmmAlQuraTable()); // bool(true)

Creating dates

create()

Hijri::create($year, $month, $day, $hour = 0, $minute = 0, $second = 0, $timezone = null, $variant = HijriVariant::UmmAlQura) checks the date against the real month length in the chosen variant.

PHP
$h = Hijri::create(1446, 9, 1, 0, 0, 0, $utc);

echo $h;                                  // 1446/09/01 00:00:00
echo $h->toGregorian()->format('Y-m-d');  // 2025-03-01
var_dump($h->usesUmmAlQuraTable());       // bool(true)

make() and the helper

Hijri::make($time = null, $timezone = null, $variant = HijriVariant::UmmAlQura) accepts a DateTimeInterface, any CalendarDate, an int Unix timestamp, a string or null (now). The helper hdate($time, $timezone) calls it with the default variant. Hijri::now() and Hijri::today() take ($timezone, $variant).

PHP
echo Hijri::make('2025-03-01', $utc);                     // 1446/09/01 00:00:00
echo hdate('2025-03-01')->format('j F Y', 'en');          // 1 Ramadan 1446
echo Hijri::make(Jalali::create(1404, 1, 1, 0, 0, 0, $utc)); // 1446/09/21 00:00:00
echo Hijri::make('2025-03-31', $utc)->format('Y/m/d');    // 1446/10/02
echo Hijri::make('2025-03-31', $utc, HijriVariant::Tabular)->format('Y/m/d'); // 1446/10/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 (3 or 4 digit year, optional H:i[:s] ) with a year below 1700 is read as a Hijri date in the chosen variant.
  3. Everything else, including years from 1700 on, is read as Gregorian or free text.
PHP
echo Hijri::make('1446/09/01', $utc);   // 1446/09/01 00:00:00   (Hijri)
echo Hijri::make('1700/01/01', $utc);   // 1111/07/10 00:00:00   (Gregorian 1700-01-01)

try { Hijri::make('1446/02/31'); }
catch (InvalidDateException $e) { echo $e->getMessage(); }  // Invalid Hijri date: 1446/2/31

Good to know. Hijri::make($hijriInstance) returns that same instance and ignores the $variant argument. To show a date in another variant, go through the moment: Hijri::make($h->toGregorian(), null, HijriVariant::Tabular).

Supported range and errors

Hijri years Hijri::MIN_YEAR = 1 to Hijri::MAX_YEAR = 9665 are supported. The first day is 1/1/1 (0622-07-19) and the last is 9665/12/last (9999-10-01), so Gregorian years 622 to 9999. Anything outside throws InvalidDateException from create(), make(), integer timestamps, add*(), sub*() and the converters. You never get a TypeError or ValueError.

PHP
foreach ([[0, 1, 1], [9666, 1, 1], [1446, 1, 31], [1446, 13, 1]] as [$y, $m, $d]) {
    try { Hijri::create($y, $m, $d); }
    catch (InvalidDateException $e) { echo $e->getMessage(), "\n"; }
}
// Invalid Hijri date: 0/1/1
// Invalid Hijri date: 9666/1/1
// Invalid Hijri date: 1446/1/31
// Invalid Hijri date: 1446/13/1

try { Hijri::make(new DateTimeImmutable('0600-01-01', $utc)); }
catch (InvalidDateException $e) { echo $e->getMessage(); }
// Date out of the supported Hijri range (1..9665): -22

try { Hijri::create(9665, 1, 1)->addYears(1); }
catch (InvalidDateException $e) { echo $e->getMessage(); }
// Hijri year out of the supported range: 9666

Reading values

The getters are the ones in the contract: getYear(), getMonth(), getDay(), getHour(), getMinute(), getSecond(), getTimestamp(), getTimezone(), toGregorian(), toDateString() (Y/m/d) and toDateTimeString(). Hijri adds getVariant() and usesUmmAlQuraTable().

Weekday numbering

getDayOfWeek() and the w token start on Sunday: 0 = Sunday to 6 = Saturday, the same as PHP. Jalali starts on Saturday. The N token is the ISO number, 1 = Monday to 7 = Sunday.

PHP
$h = Hijri::create(1446, 9, 1, 0, 0, 0, $utc);   // a Saturday
echo $h->getDayOfWeek();                         // 6
foreach (range(0, 6) as $i) { echo $h->addDays($i)->getDayOfWeek(); }  // 6012345

Formatting

format($pattern = 'Y/m/d H:i:s', $locale = 'ar', $digits = 'latin'). $locale is ar, fa or en (an unknown value falls back to en). It sets the month names, weekday names and AM/PM markers. $digits is latin, persian or arabic. A backslash escapes the next character, and a trailing backslash is dropped. A pattern longer than Hijri::MAX_FORMAT_LENGTH (256 bytes) throws InvalidDateException with the code input_too_long. The default locale is Arabic.

PHP
echo $h->format('l j F Y');                 // السبت 1 رمضان 1446
echo $h->format('l j F Y', 'fa');           // شنبه 1 رمضان 1446
echo $h->format('l j F Y', 'en');           // Saturday 1 Ramadan 1446
echo $h->format('Y/m/d', 'ar', 'arabic');   // ١٤٤٦/٠٩/٠١
echo $h->format('j F Y', 'fa', 'persian');  // ۱ رمضان ۱۴۴۶
echo $h->format('t L z N');                 // 29 0 237 6

$pm = Hijri::create(1446, 9, 1, 15, 0, 0, $utc);
echo $pm->format('g:i a', 'ar');            // 3:00 م
echo $pm->format('g:i A', 'en');            // 3:00 PM
TokenMeaning
Y y m n d jYear (4 and 2 digits), month and day with and without leading zero
H G h g i s24-hour and 12-hour clock, minutes, seconds
F MMonth name in the locale
lWeekday name in the locale
w NWeekday number with Sunday = 0. ISO number with Monday = 1
z t LZero-based day of the year. Days in the month. 1 for a leap year (355 days)
a AAM/PM for the locale (ص/م, ق.ظ/ب.ظ, am/pm)
S WAlways empty. ISO week of the Gregorian moment
c rY-m-d\TH:i:sP with the Hijri date. RFC 2822 of the Gregorian moment
U e T P p O Z I u vTaken from the inner moment

The static Hijri::monthName($month, $locale = 'ar') returns a month name. It throws InvalidDateException for a month outside 1 to 12:

PHP
echo Hijri::monthName(9), ' | ', Hijri::monthName(9, 'fa'), ' | ', Hijri::monthName(9, 'en');
// رمضان | رمضان | Ramadan
// Hijri::monthName(13) throws: Invalid Hijri month: 13

Shifting and snapping

The methods are the same as in Jalali: addDays/Hours/Minutes/Seconds, addMonths, addYears, the sub* forms and startOf/endOf for Day, Month and Year. Months are Hijri months of the instance's variant, and the day is clamped to the length of the target month.

PHP
echo Hijri::create(1446, 4, 30, 0, 0, 0, $utc)->addMonths(1);  // 1446/05/29 00:00:00  (clamped)
echo $h->addYears(1);                                          // 1447/09/01 00:00:00
echo $h->subMonths(9);                                         // 1445/12/01 00:00:00
echo $h->addDays(29);                                          // 1446/10/01 00:00:00
echo $h->endOfMonth();                                         // 1446/09/29 23:59:59
echo $h->endOfYear();                                          // 1446/12/29 23:59:59
echo $h->startOfYear();                                        // 1446/01/01 00:00:00
echo $tab->addMonths(1)->getVariant()->name;                  // Tabular  (variant is kept)

Shifting beyond year 1 or 9665, or by a huge amount, throws InvalidDateException.

Comparing and measuring

The comparison methods (eq ne gt gte lt lte equals isBefore isAfter between isPast isFuture isToday) compare moments and accept any calendar. diffInDays() counts whole days. diffInMonths() and diffInYears() count whole Hijri months and years in the variant of this instance (the other date is first shown in that variant). The $absolute flag works as in Jalali.

PHP
echo Hijri::create(1446, 1, 1, 0, 0, 0, $utc)->diffInMonths(Hijri::create(1447, 3, 1, 0, 0, 0, $utc)); // 14
echo Hijri::create(1440, 1, 1, 0, 0, 0, $utc)->diffInYears(Hijri::create(1446, 1, 1, 0, 0, 0, $utc));  // 6
echo $h->diffInDays(Hijri::create(1446, 10, 1, 0, 0, 0, $utc));                                        // 29

Calendar arithmetic

MethodReturns
Hijri::isValid($y, $m, $d, $variant)bool. False for out-of-range values
Hijri::daysInMonth($y, $m, $variant)29 or 30. Throws InvalidDateException for a month outside 1 to 12
Hijri::daysInYear($y, $variant)Sum of the 12 month lengths (354 or 355 in practice)
Hijri::isLeapYear($y, $variant)True when the year has 355 days
Hijri::hasUmmAlQuraData($y)True for AH 1300 to 1500
Hijri::isUmmAlQuraVerified($y)True for AH 1318 to 1500
Hijri::gregorianToHijri($gy, $gm, $gd, $variant)[year, month, day]
Hijri::hijriToGregorian($hy, $hm, $hd, $variant)[year, month, day]
PHP
echo implode(',', array_map(fn ($m) => Hijri::daysInMonth(1446, $m), range(1, 12)));
// 29,30,30,30,29,30,30,29,29,30,29,29
echo Hijri::daysInYear(1446);                                    // 354
echo Hijri::daysInMonth(1446, 12, HijriVariant::Tabular);        // 29
echo json_encode(Hijri::gregorianToHijri(2025, 3, 1));           // [1446,9,1]
echo json_encode(Hijri::hijriToGregorian(1446, 9, 1));           // [2025,3,1]

Practical notes

  • Pick the variant once for your application and pass it everywhere. Mix variants in one comparison only if you want to see the day-level differences.
  • Inside AH 1300 to 1500, the embedded Umm al-Qura table decides. Outside it, results come from the arithmetic rule.
  • To convert to and from the other calendars, see Convert and compare dates . The Carbon helpers ( toHijri() , createFromHijri() ) are in Carbon macros .