Holiday calendar

Use official Iranian holiday dates, see where each date comes from, and fix moon-sighting differences with your own offsets, month starts and extra or removed days.

On this page
  1. What it is
  2. Where the dates come from
  3. Tutorial: correct a year step by step
    1. 1. Shift all estimated dates with an offset
    2. 2. State the real start of a Hijri month
    3. 3. Add or remove a day
    4. 4. See why a day is a holiday
  4. All options
  5. Which rule wins
  6. Laravel config
  7. What changed in 0.2.0
  8. Good to know

What it is#

Iran sets its religious holidays by moon sighting. A table can only estimate them, and an estimate is sometimes a day or two off. RtlyKit\Holiday\HolidayCalendar solves this in two ways. It ships the published official dates for many Jalali years. And it lets you correct everything else with a few small settings.

A HolidayCalendar is immutable and has no global state. Each with*() method returns a new calendar. It has the same lookup methods as the static IranHolidays class (isHoliday(), getTitles(), all(), isBusinessDay() and more). IranHolidays uses the default calendar, which is HolidayCalendar::default().

Where the dates come from#

Every Jalali year has one of three sources. Ask the calendar with sourceOf($year), which returns a HolidaySource.

SourceYearsWhat it means
Official1394 and 1396 to 1405The dates of the official yearly calendar of the Calendar Center at the University of Tehran, checked against a second source (time.ir)
Reported1380 to 1393 and 1395Dates taken from a published list. For 1395 we used PDF digits and a news list of the official holidays. The set of holidays in these years may be incomplete, for example the Imam Reza day is missing in some of them
Estimatedevery other yearCalculated from the Umm al-Qura table, and reported only inside AH 1300 to 1500

Fixed holidays, such as Nowruz or 22 Bahman, fall on the same Jalali date every year. They are exact and are not part of this question.

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

use RtlyKit\Holiday\HolidayCalendar;
use RtlyKit\Holiday\IranHolidays;

$cal = HolidayCalendar::default();    // IranHolidays::calendar() is the same calendar

echo $cal->sourceOf(1405)->value;     // official
echo $cal->sourceOf(1395)->value;     // reported
echo $cal->sourceOf(1406)->value;     // estimated

echo implode(', ', IranHolidays::getTitles(1405, 1, 1));   // جشن نوروز, عید فطر

In the year 1405, Eid al-Fitr 1447 AH falls on 1405/01/01, the same day as Nowruz. That is the official date. The table estimate puts it on 1404/12/29.

Tutorial: correct a year step by step#

The year 1406 has no official data yet, so it is estimated. We will fix it in four steps.

1. Shift all estimated dates with an offset#

If you see that the estimate is one day early all year, move every estimated Islamic holiday with withIslamicOffset(). It accepts -3 to +3 days. It changes estimated years only. Official years and fixed holidays do not move.

$later = $cal->withIslamicOffset(1);

echo implode(', ', $cal->getTitles(1406, 3, 25));     // عاشورای حسینی
echo implode(', ', $later->getTitles(1406, 3, 26));   // عاشورای حسینی

2. State the real start of a Hijri month#

One flat offset cannot follow the moon month by month. When you know the real first day of a month, give it to withHijriMonthStart($hijriYear, $hijriMonth, $firstDay). The date is Gregorian: a '2027-06-08' string (Persian and Arabic digits are fine) or any DateTimeInterface. Every Islamic holiday of that month is worked out from this day. This works in official years too, and it wins over official data.

// Muharram 1449 is computed to start on 2027-06-06. The moon is seen two days later:
$moved = $cal->withHijriMonthStart(1449, 1, '2027-06-08');

echo implode(', ', $moved->getTitles(1406, 3, 25));   // (nothing)
echo implode(', ', $moved->getTitles(1406, 3, 26));   // تاسوعای حسینی
echo implode(', ', $moved->getTitles(1406, 3, 27));   // عاشورای حسینی

The date must be within 3 days of the computed start. If you also gave a neighbouring month, the two starts must be 29 or 30 days apart. Otherwise the call throws InvalidDateException.

3. Add or remove a day#

withHoliday($date, $title) adds a title to a day. withoutHoliday($date) removes the whole day, and withoutHoliday($date, $title) removes one title. A date is a Jalali object or a string such as '1406/02/03'.

$mine = $moved
    ->withHoliday('1406/02/03', 'Company day')
    ->withoutHoliday('1406/01/13');            // remove Nature Day

var_dump($mine->isHoliday(1406, 2, 3));        // bool(true)
var_dump($mine->isHoliday(1406, 1, 13));       // bool(false)

4. See why a day is a holiday#

statusOf() returns each title of a day together with its origin. The origin is a HolidayOrigin: Fixed, Official, Reported, Estimated or User.

foreach ($mine->statusOf(1406, 2, 3) as $entry) {
    echo $entry->title, ' ', $entry->origin->value, "\n";   // Company day user
}
foreach ($mine->statusOf(1406, 3, 27) as $entry) {
    echo $entry->title, ' ', $entry->origin->value, "\n";   // عاشورای حسینی user
}
foreach ($cal->statusOf(1405, 1, 1) as $entry) {
    echo $entry->title, ' ', $entry->origin->value, "\n";
}
// جشن نوروز fixed
// عید فطر official

You can use this to show a small note next to dates that are only estimates.

All options#

MethodWhat it does
HolidayCalendar::default()Official data on, offset 0, no changes
HolidayCalendar::fromArray($config)Builds a calendar from an array in the shape of the Laravel config (see below). Unknown keys and bad values throw
withIslamicOffset(int $days)Moves estimated Islamic holidays by -3 to +3 days
withHijriMonthStart($year, $month, $day)Sets the real first day of a Hijri month. Holidays of that month follow it
withHoliday($date, $title)Adds a title to a day
withoutHoliday($date, $title = null)Removes one title, or the whole day
withOfficialData(bool $use)false treats every year as an estimate, as releases before 0.2.0 did
sourceOf($jalaliYear)The HolidaySource of a year, before your changes
statusOf($year, $month, $day)The titles of a day with their HolidayOrigin
getTitles(), getTitle(), isHoliday(), all(), allTitles(), allFixed(), isWeekend(), isBusinessDay(), nextBusinessDay()The same lookups as IranHolidays, with your changes applied. allFixed() always lists the fixed table only

Which rule wins#

For every day and every title the order is the same:

  1. Your changes: withHoliday(), withoutHoliday() and withHijriMonthStart().
  2. The official table, for the years it covers, while withOfficialData(true) is on.
  3. The Umm al-Qura estimate, moved by withIslamicOffset().

Fixed Jalali holidays are never moved by an offset or a month start. You can still remove one with withoutHoliday().

Laravel config#

Publish the config file with:

php artisan vendor:publish --tag=rtly-kit-config

It creates config/rtly-kit.php. The holidays block has these keys:

'holidays' => [
    'islamic_offset' => 0,           // -3..3, estimated years only
    'hijri_month_starts' => [],      // '1447-10' => '2026-03-21'
    'extra' => [],                   // '1405/02/03' => 'Title' or ['Title 1', 'Title 2']
    'removed' => [],                 // '1405/02/03' or '1405/02/03' => 'Title'
    'use_official_data' => true,
],

The service provider builds one HolidayCalendar from this block and binds it in the container. Get it with app(HolidayCalendar::class). It is created the first time you ask for it, so a bad value fails at that point. Missing keys use their defaults.

use RtlyKit\Holiday\HolidayCalendar;

$calendar = app(HolidayCalendar::class);
$calendar->isHoliday(1406, 3, 27);
Good to know. The static IranHolidays class and the helper is_iran_holiday() always use the default calendar, not the one from your config. Use app(HolidayCalendar::class) when you want your settings applied.

What changed in 0.2.0#

In the Jalali years 1380 to 1405 the static methods now return the official or reported dates. They used to return the table estimate. Years outside 1380 to 1405, and calendars built with withOfficialData(false), return what earlier releases returned. Some examples:

  • 1405/01/01 is Eid al-Fitr 1447 AH. 1404/12/29 is no longer a day for it.
  • Eid al-Fitr 1446 AH is on 1404/01/11, not 1404/01/10.
  • In official years, only the titles in the official table appear. For example, the 8 Rabi I day (Imam Hasan Askari) is not in the 1394 and 1395 data.

The side-by-side list is in Upgrade.

Good to know#

Good to know. There is no official data for 1406 yet. When it is published, it will be added to the data file and nothing else changes. For estimated years, a flat offset fixes a steady shift but not month-by-month moon sighting. Use withHijriMonthStart() for exact months. Where the data came from is described in Accuracy and data.

Related: Iranian holidays, Hijri calendar, Laravel setup.