Skip to main content

Accurate, location-aware Hijri <-> Gregorian date conversion with crescent-visibility support.

Project description

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

Python License: MIT Dependencies

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

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

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 samples a worldwide grid of locations at their local sunsets and declares the crescent seen as soon as any of them satisfies the criterion. It is therefore never later than a single-location result, and it approximates the "unified" calendars used by several authorities. For matching a specific authority exactly, pick the engine/criterion closest to its policy (umm_al_qura for Saudi Arabia, scope="global" + ircica for a Türkiye-style unified calendar) and treat results as predictions — the final word always belongs to the official sighting/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)
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.
  • 54 unit tests + 18 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

hijrical-1.1.0.tar.gz (54.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

hijrical-1.1.0-py3-none-any.whl (45.6 kB view details)

Uploaded Python 3

File details

Details for the file hijrical-1.1.0.tar.gz.

File metadata

  • Download URL: hijrical-1.1.0.tar.gz
  • Upload date:
  • Size: 54.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for hijrical-1.1.0.tar.gz
Algorithm Hash digest
SHA256 52873012540d5d30fbdf6abe794fa3dcc0685e63ed3a33e127c4f67fc897b382
MD5 8e411b786c4d46e139a8e9d8ebb01d4f
BLAKE2b-256 aea342cf6724528a41f7ca5b531f266a8ef47ad0dac2792370fd392c6196bea1

See more details on using hashes here.

File details

Details for the file hijrical-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: hijrical-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 45.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.13.2

File hashes

Hashes for hijrical-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 75097e53206bf6a6c3eb3d72b5e4868b43b40eb4c53327b76190a6d0b36d8dba
MD5 3362242dbda5a90a8c7b77695d96f66e
BLAKE2b-256 58f675032601a2467d6efa405a5d6e28b86d5874c0964aea322626ab67a70c7d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page