← All modules

Distribution · calendars

Date::Calendar::Hijri

Works

Convert between Gregorian dates and the arithmetic Hijri calendar — a computed thirty-year cycle with no moon sighting — with day names and strftime.

Version
0.1.0 zef:jforget
Depends
Date::Calendar::Strftime
License
Artistic-2.0
Its own test suite
3 files, green
Checked
2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Where it lives
raku.land · source

Install it #

$ rakupp install Date::Calendar::Hijri

zef install Date::Calendar::Hijri writes the same store; either installer leaves the module usable by both engines.

What it is for #

The Islamic calendar is lunar, so its months drift about eleven days a year against the Gregorian one, and a date in one says nothing obvious about the other. Anything that has to display both — a diary, an archive of dated documents, a scheduling system used across the Muslim world — needs the conversion.

This distribution implements the arithmetic variant: a purely computed thirty-year cycle of alternating thirty- and twenty-nine-day months, with no observational input.

Converting a date #

File
use Date::Calendar::Hijri;

my $g = Date.new('2026-09-14');
my $h = Date::Calendar::Hijri.new-from-date($g);

say "gregorian in : $g";
say 'gist         : ', $h.gist;
say 'year/mon/day : ', $h.year, ' / ', $h.month, ' / ', $h.day;
say 'month-name   : ', $h.month-name, '   abbr ', $h.month-abbr;
say 'day-name     : ', $h.day-name, '   abbr ', $h.day-abbr;
say 'day-of-week  : ', $h.day-of-week, '   day-of-year ', $h.day-of-year;
say 'week-number  : ', $h.week-number, '   week-year ', $h.week-year;
say 'daycount     : ', $h.daycount;
say 'strftime     : ', $h.strftime('%Y-%m-%d %A %B');
say '';
say 'round trip   : ', $h.to-date, '   exact : ', $h.to-date == $g;
Output
gregorian in : 2026-09-14
gist         : 1448-04-01
year/mon/day : 1448 / 4 / 1
month-name   : Rabi` al-Thaani   abbr R.T
day-name     : Yaum al-Ithnain   abbr Ith
day-of-week  : 2   day-of-year 90
week-number  : 14   week-year 1448
daycount     : 61297
strftime     : 1448-04-01 Yaum al-Ithnain Rabi` al-Thaani

round trip   : 2026-09-14   exact : True

The round trip is exact. daycount is the Modified Julian Day, which is the common currency between every calendar in this family.

The epoch #

File
use Date::Calendar::Hijri;

for '0622-07-19', '0700-01-01', '1900-01-01' -> $d {
    my $h = Date::Calendar::Hijri.new-from-date(Date.new($d));
    say sprintf('%s -> %s   back=%s exact=%s',
        $d, $h.gist, $h.to-date, $h.to-date == Date.new($d));
}
say '';
say 'earlier than that is refused:';
my $r = try Date::Calendar::Hijri.new-from-date(Date.new('0622-07-18'));
say '  0622-07-18 : ', $! ?? 'refused' !! 'accepted';
Output
0622-07-19 -> 0001-01-01   back=0622-07-19 exact=True
0700-01-01 -> 0080-11-01   back=0700-01-01 exact=True
1900-01-01 -> 1317-08-28   back=1900-01-01 exact=True

earlier than that is refused:
  0622-07-18 : refused

The epoch is 19 July 622, which is Hijri 0001-01-01. Anything earlier is outside the calendar.

Day-of-week numbering #

File
use Date::Calendar::Hijri;

my $g = Date.new('2026-09-14');     # a Monday
say "gregorian $g is a Monday";
say '  Raku Date.day-of-week  : ', $g.day-of-week;
say '  Hijri  .day-of-week    : ', Date::Calendar::Hijri.new-from-date($g).day-of-week;
say '  Hijri  .day-name       : ', Date::Calendar::Hijri.new-from-date($g).day-name;
say '';
say 'the Hijri week starts on Sunday and Raku counts from Monday,';
say 'so the same day has two different numbers.';
Output
gregorian 2026-09-14 is a Monday
  Raku Date.day-of-week  : 1
  Hijri  .day-of-week    : 2
  Hijri  .day-name       : Yaum al-Ithnain

the Hijri week starts on Sunday and Raku counts from Monday,
so the same day has two different numbers.

The one thing to know #

One Hijri date maps to two Gregorian days, and to-date is not the inverse of new-from-date.

File
use Date::Calendar::Hijri;
use Date::Calendar::Strftime;

my $day = Date::Calendar::Hijri.new(year => 1448, month => 4, day => 2);
my $eve = Date::Calendar::Hijri.new(year => 1448, month => 4, day => 2,
                                    daypart => after-sunset());

say 'Hijri 1448-04-02 in daylight     -> ', $day.to-date;
say 'Hijri 1448-04-02 after sunset    -> ', $eve.to-date;
say 'the same Hijri date, a day apart : ', $day.to-date - $eve.to-date;
say '';
my $g = Date.new('2026-09-14');
say "Gregorian $g in daylight  -> ",
    Date::Calendar::Hijri.new-from-daycount($g.daycount, daypart => daylight()).gist;
say "Gregorian $g after sunset -> ",
    Date::Calendar::Hijri.new-from-daycount($g.daycount, daypart => after-sunset()).gist;
Output
Hijri 1448-04-02 in daylight     -> 2026-09-15
Hijri 1448-04-02 after sunset    -> 2026-09-14
the same Hijri date, a day apart : 1

Gregorian 2026-09-14 in daylight  -> 1448-04-01
Gregorian 2026-09-14 after sunset -> 1448-04-02

The Hijri day begins at sunset, and the module models that. The conversion is a mapping between day-parts, not between days. Convert a Gregorian date in and straight back out and you are fine — but hand the module a Hijri date that came from an evening event and to-date lands a day earlier than the same numerals would give you in daylight.

Date::Calendar::Bahai behaves the same way; Date::Calendar::Persian does not, because its daypart is inert. Code written against one and reused on another behaves differently for the same argument, with no error.

Where the two engines differ #

On the exception a bad month or day produces, which matters if you are catching it.

Every class in this family validates twice: a where constraint on the attribute and an explicit range check in BUILD that throws a well-described X::OutOfRange. Rakudo runs BUILD and you get Month out of range. Is: 13, should be in 1..12. Raku++ fills the attributes from the named arguments before BUILD runs, so the where constraint fires first and you get X::TypeCheck::Assignment with no range in the message.

Both refuse the date. Only one tells you what the range was.

Two other things, neither engine-related. This is the arithmetic Hijri only — there is no astronomical or observational variant here, so it will not in general match locally announced dates. And alone among the four calendars in this family it has no locale method, so you get the one set of names.