Skip to main content

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).

PyPI Python License: MIT CI Dependencies Live demo

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

hijridate is 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. Use DiyanetCalendar, or scope="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, 26).holiday("tr")   # 'Kadir Gecesi'  -- the evening it begins
HijriDate(1447, 9, 27).holiday("tr", observed=False)   # same night, by Hijri day

Holy nights are marked on their eve. The Islamic day starts at sunset, so the night of 27 Ramadan begins on the evening of the 26th — which is the date calendars print (Diyanet lists Laylat al-Qadr as 26 Ramadan, Mawlid as 11 Rabi al-awwal). Day lookups follow that convention by default, so a calendar grid and year_holidays() always name the same Gregorian day; ReligiousDay carries both (.observed / .observed_hijri_date and .gregorian / .hijri).


🧰 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_qura configuration 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/ircica engine 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/scope behaviour, i18n and the app-builder helpers. Run them with python run_tests.py (no pytest needed) or pytest.

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.3.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hijrical 1.3.1
File Size Uploaded
hijrical-1.3.1.tar.gz 68.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for hijrical 1.3.1
File Interpreter ABI Platform
hijrical-1.3.1-py3-none-any.whl Python 3 none any Details

Total release size: 122.4 kB

Release files / hijrical-1.3.1.tar.gz

Download URL hijrical-1.3.1.tar.gz
Size 68.4 kB
Tags Source
SHA-256 checksum
How to use checksums
29cf604082277e2a6a2cdcc943ac59ceb0d72b79f1ae665b25ec40859d3db701
BLAKE2b-256 checksum
How to use checksums
88b532a3ae25a21886c29b41212f870e9b2f2868bc7fd9c641bcf2e41dafc746
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

Release files / hijrical-1.3.1-py3-none-any.whl

Download URL hijrical-1.3.1-py3-none-any.whl
Size 54.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b46ebdd68eab95675e0a9764491b2a72c4dd82c1c2bce4125b57db3ed141666a
BLAKE2b-256 checksum
How to use checksums
7bf9166d0f3c4363d43bc63e9015e4f712d1c0f8f2f0fe20406283b2ce674f08
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

Release history Release notifications | RSS feed

This release

1.3.1 This release

2 release files

1.3.0

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.1

2 release files

1.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page