On this page
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.
| Source | Years | What it means |
|---|---|---|
Official | 1394 and 1396 to 1405 | The dates of the official yearly calendar of the Calendar Center at the University of Tehran, checked against a second source (time.ir) |
Reported | 1380 to 1393 and 1395 | Dates 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 |
Estimated | every other year | Calculated 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#
| Method | What 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:
- Your changes:
withHoliday(),withoutHoliday()andwithHijriMonthStart(). - The official table, for the years it covers, while
withOfficialData(true)is on. - 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);
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#
withHijriMonthStart() for exact months. Where the data came from is described in Accuracy and data.Related: Iranian holidays, Hijri calendar, Laravel setup.