hijrical 🌙
Accurate, location-aware Hijri ⇄ Gregorian date conversion for Python.
A modern, professional alternative to hijridate — without its limitations and
with the things it lacks: an unbounded exact calendar, real crescent
visibility that depends on where you are, the sunset day boundary, and
internationalization (English / Turkish / Arabic, easily extended).
from hijrical import HijriDate, from_gregorian, to_gregorian
from_gregorian(2026, 6, 15) # HijriDate(1447, 12, 29, calendar='arithmetic')
to_gregorian(1447, 9, 1) # datetime.date(2026, 2, 18)
HijriDate.parse("15 Ramadan 1447") # parse human input
🌙 Try every feature in your browser — hijrical Studio. An interactive playground (converter, Hijri/Gregorian dual calendar, crescent visibility, religious days) that runs fully client-side via Pyodide — no install. Source: kbycode/hijrical-studio.
Why hijrical?
hijridate |
hijrical | |
|---|---|---|
| Date range | 1343–1500 AH only (1924–2077) | Arithmetic: unbounded; astronomical: 1–1600 AH |
| Methods | One (Umm al-Qura table) | Arithmetic + astronomical/visibility |
| Location-aware | ❌ | ✅ Istanbul and Mecca can differ by a day |
| Sunset (maghrib) day boundary | ❌ | ✅ HijriDate.at(instant, place) |
| Reversible | table-bound | Pure-integer, exact round-trip |
| Religious days / holy nights | ❌ | ✅ with i18n + correct night eves |
| Languages | English | en / tr / ar, pluggable |
| Dependencies | none | none |
hijridateis backed by the Umm al-Qura table, so it only works for 1924–2077 and raises outside it. hijrical's arithmetic engine is pure integer math: it works for any date and round-trips perfectly. On top of that, hijrical adds a real astronomical engine that models crescent visibility per location — the thing that actually decides when a Hijri month begins.
Installation
pip install hijrical
Zero dependencies, pure Python 3.9+. The package also works straight from a
checkout (import hijrical with the folder on your path).
Quick start
from hijrical import HijriDate, from_gregorian, to_gregorian
# Gregorian -> Hijri
h = from_gregorian(2026, 6, 15)
print(h) # 29 Dhu al-Hijjah 1447 AH
print(h.isoformat()) # 1447-12-29
print(h.to_gregorian()) # 2026-06-15
# Hijri -> Gregorian
print(to_gregorian(1447, 9, 1)) # 2026-02-18 (start of Ramadan)
# Parse anything reasonable
HijriDate.parse("1447-09-01")
HijriDate.parse("15 Ramadan 1447")
HijriDate.parse("12 Rebiülevvel 1447") # Turkish month name
HijriDate.parse("١ رمضان ١٤٤٧") # Arabic digits + name
# Formatting & localization
h = HijriDate(1447, 9, 1)
h.format("{day} {month_name} {year}") # '1 Ramadan 1447'
h.format("{day} {month_name} {year}", lang="tr") # '1 Ramazan 1447'
h.format("{day} {month_name} {year} {era}", lang="ar") # '1 رمضان 1447 هـ'
# Arithmetic & comparison
h + 30 # 30 days later, as a HijriDate
HijriDate(1447, 12, 29) - HijriDate(1447, 9, 1) # day difference (int)
The two engines
from hijrical import ArithmeticCalendar, AstronomicalCalendar, HijriDate
# Arithmetic (default): exact, reversible, unbounded
HijriDate(1447, 9, 1, calendar=ArithmeticCalendar("kuwaiti"))
# Astronomical: real crescent visibility for a place + criterion
HijriDate(1447, 9, 1, calendar=AstronomicalCalendar("istanbul", "ircica"))
| Engine | Use it for | Guarantee |
|---|---|---|
ArithmeticCalendar |
civil/database use, history, anything needing a stable, reversible mapping | Mathematically exact; same input → same output, forever |
AstronomicalCalendar |
predicting real religious dates as observed somewhere | A close prediction; may differ ±1 day from official decrees |
Arithmetic variants: kuwaiti (default, Type II / Microsoft), type1,
type3, type4, kuwaiti_astronomical.
🌍 Location-based crescent visibility (the interesting part)
A Hijri month begins when the new crescent (hilal) is seen — and whether it can be seen depends on where you stand. Right after the astronomical new moon the crescent is thin and low; from one city it clears the horizon by sunset, from another it does not. That is exactly why Ramadan sometimes starts a day later in Türkiye than in Saudi Arabia.
hijrical models this directly. For a given Observer and Criterion it computes
the Moon's real position at sunset and decides visibility:
from hijrical import HijriDate, AstronomicalCalendar
ramadan = lambda obs, crit: HijriDate(
1447, 9, 1, calendar=AstronomicalCalendar(obs, crit)
).to_gregorian()
ramadan("mecca", "umm_al_qura") # 2026-02-18
ramadan("istanbul", "ircica") # 2026-02-19 ← one day later
# For Türkiye's official date, use DiyanetCalendar() (see below), not a local observer.
ramadan("jakarta", "mabims") # 2026-02-19
Or from the command line:
$ hijrical compare 1447 9 1
Hijri 1447-09-01 in Gregorian, by method/location:
arithmetic : 2026-02-18
Diyanet (Türkiye) : 2026-02-19 [official]
global/ircica (unified) : 2026-02-19
Mecca umm_al_qura : 2026-02-18
Mecca ircica : 2026-02-19
İstanbul umm_al_qura : 2026-02-18
İstanbul ircica : 2026-02-19
Jakarta umm_al_qura : 2026-02-19
Jakarta ircica : 2026-02-19
Rabat umm_al_qura : 2026-02-18
Rabat ircica : 2026-02-19
What the engine actually computes
For the sunset of each candidate evening it derives, from a full Meeus lunar + solar model (validated to arc-seconds against Meeus' own worked example):
- elongation (arc of light) — Sun–Moon separation,
- altitude — the Moon's topocentric altitude (parallax-corrected; the Moon sits ~0.95° lower for a surface observer, which matters near the horizon),
- arc of vision (ARCV) — Moon altitude minus Sun altitude,
- moon age — time since conjunction,
- lag — how long after the Sun the Moon sets,
- crescent width — illuminated width in arcminutes.
You can inspect these yourself:
from datetime import date
from hijrical import compute_crescent, sunset
from hijrical.observer import resolve_observer
from hijrical._moon import new_moon_jd_ut
obs = resolve_observer("istanbul")
ss = sunset(date(2026, 2, 17), obs.latitude, obs.longitude, obs.utc_offset)
info = compute_crescent(obs, ss, new_moon_jd_ut(323))
print(info) # elong=…, alt=…, ARCV=…, age=…, lag=…, width=…
Built-in criteria
| Criterion | Rule | Notes |
|---|---|---|
ircica (default) |
elongation ≥ 8°, altitude ≥ 5° | Türkiye / IRCICA unified-calendar thresholds |
mabims |
elongation ≥ 6.4°, altitude ≥ 3° | Southeast Asia (Indonesia/Malaysia/Brunei/Singapore) |
umm_al_qura |
Moon sets after the Sun (lag > 0) and conjunction before sunset | close to the Saudi official calendar |
odeh |
Odeh (2004) ARCV vs crescent-width q-test | naked-eye / optical zones |
conjunction |
conjunction before sunset | simplest baseline |
Bring your own:
from hijrical.criteria import AltitudeElongationCriterion
from hijrical import AstronomicalCalendar
my_rule = AltitudeElongationCriterion(min_elongation=7.0, min_altitude=4.0, name="custom")
AstronomicalCalendar("ankara", my_rule)
🇹🇷 Türkiye / Diyanet: use the official calendar
Turkey's calendar is published by Diyanet, and matching it exactly matters.
DiyanetCalendar uses those official tables verbatim, so it agrees with the
printed calendar 100% — validated row by row against 160 official Hijri/Gregorian
pairs from 2022–2027:
from hijrical import HijriDate, DiyanetCalendar, year_holidays
cal = DiyanetCalendar()
HijriDate(1447, 9, 1, calendar=cal).to_gregorian() # 2026-02-19 (Diyanet: 19 Şubat)
HijriDate.from_gregorian(2026, 6, 16, calendar=cal) # 1 Muharrem 1448
for r in year_holidays(1448, cal):
print(r.observed, r.name("tr")) # .observed = the date Diyanet prints
Coverage is explicit — no silent guessing:
cal.coverage() # ((1443, 6), (1449, 8)) official range
cal.is_official(1447, 9) # True -> straight from Diyanet's table
cal.is_official(1460, 9) # False -> astronomical fallback (Diyanet hasn't published it)
Why a table and not a formula? Turkey's rule is the global criterion below, and computing it reproduces Diyanet almost always. But several month boundaries are decided within 0.1° of the 8°/5° threshold, where any difference in ephemeris or refraction model flips the month. A table is the only way to be exactly right for the published years.
Not the Turkish rule: a local observer (
AstronomicalCalendar("istanbul", "ircica")) is a different question — "is it visible from Istanbul?" — and matched only 5 of 10 official anchors. UseDiyanetCalendar, orscope="global"for years past the tables.
Local vs. global ("unified") calendars
The criteria above judge visibility at the observer's own location. Some
national calendars (e.g. Türkiye's official Türkiye Takvimi, adopted in 2016)
instead use a global rule: the month turns over for everyone once the
crescent is visible anywhere on Earth within certain bounds. hijrical supports
both via the scope argument:
from hijrical import AstronomicalCalendar, HijriDate
local = AstronomicalCalendar("istanbul", "ircica") # "is it visible here?"
global_ = AstronomicalCalendar("mecca", "ircica", scope="global") # "is it visible anywhere?"
HijriDate(1447, 9, 1, calendar=local).to_gregorian() # 2026-02-19
HijriDate(1447, 9, 1, calendar=global_).to_gregorian() # 2026-02-18 (the world has seen it)
The global mode sweeps a worldwide sample of inhabited land at each place's
sunset and declares the crescent seen as soon as any of them satisfies the
criterion. Land matters: the 2016 congress disregards a crescent that would only
be visible over open ocean, and honouring that is what makes this mode track
official calendars (it fixed a whole month boundary that a naive grid got a day
early). For a specific authority, pick the closest engine — DiyanetCalendar
for Turkey, umm_al_qura for Saudi Arabia — and treat computed years as
predictions; the final word always belongs to the official announcement.
🌇 Sunset (maghrib) day boundary
The Islamic day begins at sunset, not midnight — so the eve of a feast is
already, religiously, the feast's first night. HijriDate.at() handles this:
from datetime import datetime
from hijrical import HijriDate
HijriDate.at(datetime(2026, 6, 15, 12, 0), "istanbul").isoformat() # '1447-12-29'
HijriDate.at(datetime(2026, 6, 15, 22, 0), "istanbul").isoformat() # '1447-12-30'
The same idea drives holy-night eves in the holidays API (below).
🕌 Religious days
from hijrical import year_holidays, ArithmeticCalendar
for d in year_holidays(1447, ArithmeticCalendar("kuwaiti")):
print(d.gregorian, d.name(lang="tr"), "| night:", d.eve)
Covers Islamic New Year, Ashura, Mawlid, Raghaib (first Friday eve of Rajab),
Isra & Mi'raj, Mid-Sha'ban, Ramadan, Laylat al-Qadr, Eid al-Fitr (3 days),
Arafah and Eid al-Adha (4 days). Holy nights carry an eve (the Gregorian
evening the night begins). A single date's holiday:
HijriDate(1447, 9, 27).holiday("en") # 'Laylat al-Qadr'
HijriDate(1447, 9, 27).holiday("tr") # 'Kadir Gecesi'
🧰 Recipes for app builders
Everything you need for calendar apps, countdown widgets and converters.
Format dates (strftime-style, works in f-strings):
h = HijriDate(1447, 9, 1)
h.strftime("%d %B %Y (%A)") # '01 Ramadan 1447 (Wednesday)'
f"{h:%d.%m.%Y}" # '01.09.1447'
h.strftime("%d %B %Y", lang="tr")
Iterate and lay out a month grid:
from hijrical import hijri_range, iter_month, month_calendar
for d in hijri_range(HijriDate(1447, 9, 1), HijriDate(1447, 10, 1)):
... # every day of Ramadan 1447
weeks = month_calendar(1447, 9) # list of weeks, Monday-first
for week in weeks: # each week is 7 cells (HijriDate or None)
print(" ".join(f"{c.day:2}" if c else " " for c in week))
Countdowns and special-day counters:
from hijrical import next_holiday, days_until_holiday, next_occurrence
days_until_holiday("ramadan_start") # e.g. 247 (days from today)
nh = next_holiday(key="eid_al_fitr") # next Eid al-Fitr as a ReligiousDay
print(nh.gregorian, nh.name("tr"))
next_occurrence(1, 1) # next Islamic New Year (annual recurrence)
HijriDate.today().days_until(nh.gregorian) # generic day countdown
Serialize for an API / converter UI:
HijriDate(1447, 9, 27).to_dict("tr")
# {'year': 1447, 'month': 9, 'day': 27, 'iso': '1447-09-27',
# 'gregorian': '2026-03-16', 'jdn': 2461116, 'weekday_index': 0,
# 'weekday': 'Pazartesi', 'month_name': 'Ramazan', 'method': 'arithmetic',
# 'holiday': 'Kadir Gecesi'}
Misc helpers: HijriDate.fromisoformat("1447-09-01"), .replace(day=15),
.day_of_year(), .age_in_years(birth_on), days_in_month(year, month).
On the command line:
hijrical calendar 1447 9 --lang tr # print a month grid
hijrical next --lang tr --count 8 # upcoming religious days with countdowns
hijrical next --key ramadan_start # next start of Ramadan
hijrical g2h 2026-03-16 --json # machine-readable output
🌐 Internationalization
Three languages ship in the box; adding one is a dictionary:
from hijrical import register_locale, HijriDate
register_locale({
"code": "fr", "name": "Français", "era": "AH", "day_suffix": " (jour {n})",
"months": ("Mouharram", "Safar", "Rabi al-awwal", "Rabi al-thani",
"Joumada al-oula", "Joumada al-thania", "Rajab", "Chaabane",
"Ramadan", "Chawwal", "Dhou al-qida", "Dhou al-hijja"),
"weekdays": ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"),
"holidays": { "new_year": "Nouvel an hégirien", "ramadan_start": "Début du Ramadan",
# … the remaining keys …
},
})
HijriDate(1447, 9, 1).format("{day} {month_name} {year}", lang="fr") # '1 Ramadan 1447'
Newly registered month names become parseable automatically.
🖥️ Command line
hijrical today
hijrical g2h 2026-06-15
hijrical h2g "15 Ramadan 1447" --lang tr
hijrical g2h 2026-02-18 --method astronomical --observer istanbul --criterion ircica
hijrical holidays 1447 --lang tr
hijrical compare 1447 9 1
hijrical at "2026-06-15T22:00" --observer istanbul
(Use python -m hijrical … if the script isn't on your PATH.)
API reference (essentials)
| Symbol | Purpose |
|---|---|
HijriDate(year, month, day, calendar=None) |
Construct a Hijri date |
HijriDate.from_gregorian(y, m, d, calendar=None) / from_gregorian(...) |
Gregorian → Hijri |
HijriDate.from_jdn(jdn) / from_date(date) |
From JDN / date |
HijriDate.parse(text) / parse(text) |
Parse a string |
HijriDate.today() |
Now (civil) |
HijriDate.at(instant, observer) |
Sunset-aware date |
.to_gregorian() / to_gregorian(y, m, d) |
Hijri → Gregorian date |
.format(pattern, lang) / .isoformat() |
Formatting |
.month_name(lang) / .weekday_name(lang) |
Localized names |
.holiday(lang) |
Religious-day name or None |
.month_length() / .year_length() / .is_leap_year() |
Calendar info |
ArithmeticCalendar(variant) |
Tabular engine |
AstronomicalCalendar(observer, criterion, scope="local"|"global") |
Visibility engine (local or unified) |
DiyanetCalendar() |
Turkey's official calendar (exact, from published tables) |
Observer(name, latitude, longitude, utc_offset) |
A location |
compute_crescent(observer, sunset, conj_jd) → CrescentInfo |
Visibility geometry |
get_criterion(name) / available_criteria() |
Criteria |
year_holidays(year, calendar) |
Religious days of a year |
hijri_range(start, end, step) / HijriDate.range(...) |
Iterate dates |
month_calendar(year, month) / iter_month(...) |
Month grid / days |
next_occurrence(month, day, after) |
Next annual recurrence |
next_holiday(after, key) / upcoming_holidays(...) / days_until_holiday(...) |
Countdowns |
.strftime(fmt) / f"{d:%d %B %Y}" / .to_dict() |
Formatting & serialization |
.days_until(other) / .age_in_years(on) / .replace(...) / .fromisoformat(...) |
Date math |
register_locale(dict) / available_languages() |
i18n |
.format() fields: {year} {month} {day} {month02} {day02} {month_name} {weekday} {era} {method}.
Accuracy & validation
- JDN round-trip: 0 errors over hundreds of thousands of random dates.
- Arithmetic round-trip: 0 errors across all variants and a 600k-day sweep.
- Lunar model: matches Meeus' worked example 47.a to ~0.00004° in longitude; distance and latitude exact to the quoted precision.
- Umm al-Qura: the
mecca+umm_al_quraconfiguration reproduces seven official anchor dates (Ramadan starts and both Eids) exactly. - Diyanet: 160/160 official rows reproduced exactly (every published Hijri/Gregorian pair, 2022-2027), plus the full 2026 religious-day list.
- Umm al-Qura: against the official Saudi table (1440-1455, all 192 month
starts),
AstronomicalCalendar("mecca", "umm_al_qura")agrees on 99.0% and is never more than a day out; the global/ircicaengine agrees on 89.6% (it is a different rule by design, not an error). - The arithmetic engine is a fixed rule, not an observation — it differs from observed calendars by up to ~2 days. Use it when you need determinism and reversibility, not when you need the official date.
- 60 unit tests + 25 doctests, including round-trips, the location/
scopebehaviour, i18n and the app-builder helpers. Run them withpython run_tests.py(no pytest needed) orpytest.
Disclaimer. Astronomical/visibility results are predictions. Actual religious dates depend on local moon sighting and the rulings of competent authorities (e.g. Diyanet İşleri Başkanlığı). Use the arithmetic engine when you need determinism and reversibility.
License
MIT — see LICENSE.
Release files for hijrical 1.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hijrical-1.2.1.tar.gz | 64.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hijrical-1.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 117.2 kB
Release files / hijrical-1.2.1.tar.gz
| Download URL | hijrical-1.2.1.tar.gz |
|---|---|
| Size | 64.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2ed45636b1bdc1f09b4bd2919cf51d04e6b49af7ce621dd6be588a90b78b3f5f
|
|
BLAKE2b-256 checksum How to use checksums |
53e7393715874e21dd0c335abda71ace90395e83598f6b7067fb93856a5861a3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency logRelease files / hijrical-1.2.1-py3-none-any.whl
| Download URL | hijrical-1.2.1-py3-none-any.whl |
|---|---|
| Size | 52.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8801358bbca59892c9681b1beeabb554ba0aeac18fbbfd844a8095a2c60436f6
|
|
BLAKE2b-256 checksum How to use checksums |
1928929e008ec2f1b84fd5bcf9b4bf96ae251d4976055f23c92f9ac152fe7499
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 13, 2026.
Transparency log