Search by

rizqshadi / hijri-date

ShadiRq

Hijri (Islamic) calendar for PHP: today's Hijri date by location, Gregorian <-> Hijri conversion (Umm al-Qura & tabular civil), and a Ramadan countdown.

Package info

github.com/ShadiRq/hijri-date

pkg:composer/rizqshadi/hijri-date

Statistics

Installs: 4

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-26 15:31 UTC

This package is auto-updated.

Last update: 2026-11-12 22:32:05 UTC


README

A Hijri (Islamic) calendar library for PHP 8.1+.

  • Today's Hijri date based on where the user is (timezone, coordinates, or country), with an optional day starts at sunset (maghrib) mode.
  • Gregorian ↔ Hijri conversion. Umm al-Qura (Saudi Arabia's official calendar) is the default, and the tabular civil calendar is available in pure PHP.
  • Ramadan countdown in Hijri months and days, e.g. "4 months and 16 days until Ramadan 1448" / "باقي 4 أشهر و16 يومًا على رمضان 1448".
  • English and Arabic month and day names, Arabic-Indic digits, right-to-left display helpers, JSON serialization, and immutable value objects throughout.

Requirements

  • PHP 8.1 or newer
  • ext-intl for the Umm al-Qura calendar (recommended). Without it, you can still use the civil calendar.

Installation

composer require rizqshadi/hijri-date

About ext-intl

Calendar CalendarType Needs
Umm al-Qura (default) CalendarType::UmmAlQura ext-intl (ICU)
Tabular / civil CalendarType::Civil nothing, pure PHP

If you request Umm al-Qura without intl, an IntlNotAvailableException explains how to install it. The library never silently falls back to the civil calendar, because the two can differ by a day or two.

# Debian/Ubuntu
sudo apt install php-intl
# Windows: enable it in php.ini
extension=intl

Quick start

use RizqShadi\HijriDate\Hijri;
use RizqShadi\HijriDate\Location;

$hijri = new Hijri(Location::fromTimezone('Asia/Riyadh'));

echo $hijri->today()->format('j F Y');         // e.g. 15 Rabi' al-Thani 1448
echo $hijri->today()->format('j F Y', 'ar');   // e.g. 15 ربيع الآخر 1448
echo $hijri->untilRamadan()->toString('en');   // e.g. 4 months and 16 days until Ramadan 1448

Usage

Location

"Today" depends on the local date: 1 am in Riyadh is still the previous day in New York.

use RizqShadi\HijriDate\Location;

Location::fromTimezone('Asia/Riyadh');            // lat/lng + country from the tz database
Location::fromCoordinates(21.4225, 39.8262);      // nearest timezone → Asia/Riyadh
Location::fromCoordinates(21.4225, 39.8262, 'Asia/Riyadh'); // explicit timezone
Location::fromCountry('SA');                      // the country's main timezone
Location::default();                              // PHP's default timezone
  • fromCoordinates() without a timezone picks the zone whose reference city is nearest. A few extra anchor cities (Mecca, Jeddah, Alexandria, Lahore, Delhi…) cover large zones. This is a heuristic, not a border lookup, so pass the timezone explicitly near borders.
  • fromCountry() uses the main timezone of countries that have several (US → America/New_York, RU → Europe/Moscow, AU → Australia/Sydney…). Use fromTimezone() when you know the exact zone.

Today's date

$hijri = new Hijri(Location::fromTimezone('Asia/Riyadh'));

$today = $hijri->today();
$today->year;       // 1448
$today->month;      // 4
$today->day;        // 15
$today->gregorian;  // DateTimeImmutable 2026-09-26 00:00 Asia/Riyadh
(string) $today;    // "1448-04-15"

// The Hijri date at a specific instant, as seen at the location:
$hijri->at(new DateTimeImmutable('2026-09-26 22:30', new DateTimeZone('UTC'))); // 1448-04-16 in Riyadh

Gregorian → Hijri

$hijri->fromGregorian('2026-02-18');   // 1447-09-01
$hijri->fromGregorian('18/02/2026');   // day-first
$hijri->fromGregorian('18-02-2026');
$hijri->fromGregorian('February 18, 2026');
$hijri->fromGregorian('tomorrow');     // anything DateTime understands
$hijri->fromGregorian(new DateTime('2026-02-18'));

$hijri->fromGregorian('2026-02-30');   // throws InvalidDateException (no silent rollover)
$hijri->fromGregorian('0500-01-01');   // throws InvalidDateException (before the Hijri calendar began, July 622)

Hijri → Gregorian

$hijri->toGregorian(1446, 1, 1);   // DateTimeImmutable 2024-07-07 00:00 (location timezone)
$hijri->create(1447, 9, 1);        // validated HijriDate
$hijri->daysInMonth(1445, 8);      // 29

$hijri->toGregorian(1445, 8, 30);  // throws InvalidDateException (Sha'ban 1445 has 29 days)
$hijri->create(1447, 13, 1);       // throws InvalidDateException

Formatting

format($pattern, $locale = 'en', $arabicDigits = false, $rtl = false) accepts PHP date()-style tokens:

Token Meaning Example
j day 1
d day, 2 digits 01
n month 9
m month, 2 digits 09
F month name Ramadan / رمضان
Y year 1447
y year, 2 digits 47
l weekday name Wednesday / الأربعاء

Prefix a character with a backslash to print it literally.

$date = $hijri->fromGregorian('2026-02-18');

$date->format('l, j F Y');                // Wednesday, 1 Ramadan 1447
$date->format('d/m/Y');                   // 01/09/1447
$date->format('l j F Y هـ', 'ar', true);  // الأربعاء ١ رمضان ١٤٤٧ هـ
$date->format('\Y\e\a\r: Y');             // Year: 1447

$date->monthName('ar');   // رمضان
$date->dayName();         // Wednesday
$date->isRamadan();       // true
$date->toArray();         // ['year' => 1447, 'month' => 9, 'day' => 1, 'month_name' => 'Ramadan', ...]
json_encode($date);

Ramadan countdown

$countdown = $hijri->untilRamadan();              // from today
$countdown = $hijri->untilRamadan('2026-09-26');  // from a given date

$countdown->months;        // 4
$countdown->days;          // 16
$countdown->totalDays;     // 135 (real day difference)
$countdown->ramadanStart;  // HijriDate 1448-09-01 (->gregorian = 2027-02-08)
$countdown->from;          // HijriDate you counted from
$countdown->isRamadanNow;  // false
$countdown->dayOfRamadan;  // null (or 1-30 during Ramadan)

echo $countdown->toString('en');  // 4 months and 16 days until Ramadan 1448
echo $countdown->toString('ar');  // باقي 4 أشهر و16 يومًا على رمضان 1448
json_encode($countdown);
  • The months and days are Hijri months and days. The days are what is left of the current month, including today.
  • During Ramadan (day 2 onward), isRamadanNow is true, dayOfRamadan is set, and the countdown targets next year's Ramadan.
  • On 1 Ramadan itself, months = days = totalDays = 0, isRamadanNow = true, and dayOfRamadan = 1.
  • From Shawwal onward, it counts to next year's Ramadan.
  • Arabic output uses the correct plural forms: شهر واحد، شهران، 3–10 أشهر، 11+ شهرًا (and likewise for days).

Arabic and right-to-left display

Arabic output is plain text in logical order. It displays correctly in any right-to-left context, such as an HTML element with dir="rtl":

<p dir="rtl" lang="ar"><?= htmlspecialchars($date->format('l j F Y هـ', 'ar', true)) ?></p>

When Arabic is mixed into left-to-right text (English sentences, logs, LTR UIs), the Unicode bidi algorithm can reorder the surrounding numbers. For example, (1 رمضان 1447) displays as (1447 رمضان 1). Pass rtl: true to wrap Arabic output in an invisible right-to-left isolate (U+2067 … U+2069):

echo "Today: " . $date->format('j F Y', 'ar', rtl: true);             // renders correctly inside LTR text
echo $countdown->toString('ar', arabicDigits: true, rtl: true);       // باقي ٤ أشهر و١٦ يومًا على رمضان ١٤٤٨

use RizqShadi\HijriDate\Rtl;
Rtl::isolate($anyArabicText);
Rtl::strip($text);   // remove bidi control characters before storing or comparing

rtl is off by default so that stored and compared strings contain no invisible characters. It only affects 'ar' output.

Terminals / CLI

Most terminals (the VS Code terminal, Windows Terminal, the Windows console) support neither right-to-left text nor Arabic letter joining. They draw characters left to right in stored order, so Arabic appears reversed with unjoined letters. Rtl::visual() prepares text for them. It joins the letters into their connected forms (including لا) and reorders each line with the Unicode Bidirectional Algorithm, keeping numbers left to right and honouring rtl: true isolates:

use RizqShadi\HijriDate\Rtl;

echo Rtl::visual($countdown->toString('ar', arabicDigits: true, rtl: true)) . PHP_EOL;

// or for a whole CLI script:
ob_start(fn (string $out) => Rtl::visual($out));

Use Rtl::visual() only for such displays. Never use it for HTML, databases or APIs. A bidi-aware display (browser, GNOME Terminal, Konsole) reverses the text itself and would show it backwards.

Calendar choice

use RizqShadi\HijriDate\CalendarType;

$civil = new Hijri($location, CalendarType::Civil);   // no ext-intl required
$hijri = $hijri->withCalendar(CalendarType::Civil);

Moon-sighting adjustment

Local announcements can differ from Umm al-Qura by a day. Shift the calendar by -2 to +2 days:

$hijri = new Hijri($location, adjustment: 1);  // or $hijri->withAdjustment(-1)
$hijri->fromGregorian('2026-02-18');           // 1447-09-02 instead of 1447-09-01
$hijri->toGregorian(1447, 9, 1);               // 2026-02-17

Day starts at sunset

The Islamic day begins at maghrib. With this option, the Hijri date advances once local time passes sunset. Sunset is computed with PHP's built-in date_sun_info() from the location's coordinates:

$hijri = new Hijri(Location::fromCoordinates(21.4225, 39.8262), dayStartsAtSunset: true);
// or $hijri->withDayStartsAtSunset();

$hijri->at(new DateTimeImmutable('2026-09-26 12:00', new DateTimeZone('Asia/Riyadh'))); // 1448-04-15
$hijri->at(new DateTimeImmutable('2026-09-26 20:00', new DateTimeZone('Asia/Riyadh'))); // 1448-04-16
  • The location needs coordinates, or a LocationException is thrown (e.g. Location::fromTimezone('UTC') has none).
  • The result doesn't depend on your server's default timezone.
  • At high latitudes in summer, sunset can come after midnight. The date then changes at that sunset.
  • During polar day or polar night there is no sunset, so the day changes at midnight.
  • fromGregorian() and toGregorian() work with calendar days, so sunset does not affect them.

Immutability

Hijri, Location, HijriDate and RamadanCountdown are immutable. The with*() methods return new instances: withLocation(), withCalendar(), withAdjustment(), withDayStartsAtSunset().

Exceptions

Every exception implements RizqShadi\HijriDate\Exception\HijriDateException, so one catch handles them all:

  • InvalidDateException (extends InvalidArgumentException): a bad Gregorian or Hijri date, or one before the Hijri calendar began.
  • LocationException (extends InvalidArgumentException): an unknown timezone or country, invalid coordinates, or missing coordinates for sunset mode.
  • InvalidArgumentException (extends PHP's InvalidArgumentException): an unsupported locale or an out-of-range adjustment.
  • IntlNotAvailableException (extends RuntimeException): Umm al-Qura was requested without ext-intl.

Low-level converters

use RizqShadi\HijriDate\Converter\IntlConverter;
use RizqShadi\HijriDate\Converter\TabularConverter;

(new TabularConverter())->toHijri(2026, 2, 18);          // [1447, 9, 1]
(new IntlConverter('islamic-umalqura'))->toGregorian(1446, 1, 1); // [2024, 7, 7]

Conversions are done at 12:00 UTC, so DST and midnight edge cases cannot shift the result. The converters don't validate their input; Hijri does.

Example

A runnable example prints today's date in Riyadh (English and Arabic), both conversions and the Ramadan countdown:

php vendor/rizqshadi/hijri-date/examples/demo.php

On a terminal it passes its output through Rtl::visual(). If your terminal handles right-to-left text itself, set HIJRI_TERMINAL_BIDI=1.

Testing

From a clone of the repository:

composer install
composer test

The tests check known Umm al-Qura dates against ICU and round-trip conversions over 20 years. They also compare the pure-PHP civil calendar with ICU's islamic-civil day by day over 40 years, and cover timezones, sunset rollover, adjustments and every Ramadan countdown edge case.

Support

Found a bug or have a question? Open an issue at github.com/ShadiRq/hijri-date.

License

MIT © Shadi Rizq. See LICENSE.