Metadata-Version: 2.4
Name: hijrical
Version: 1.3.0
Summary: Accurate, location-aware Hijri <-> Gregorian date conversion with crescent-visibility support.
Author: kbycode
License: MIT
Project-URL: Homepage, https://github.com/kbycode/hijrical
Project-URL: Documentation, https://github.com/kbycode/hijrical#readme
Project-URL: Repository, https://github.com/kbycode/hijrical
Project-URL: Issues, https://github.com/kbycode/hijrical/issues
Project-URL: Changelog, https://github.com/kbycode/hijrical/blob/main/CHANGELOG.md
Keywords: hijri,islamic,calendar,gregorian,umm-al-qura,ramadan,crescent,moon-sighting,ircica,diyanet,date-conversion
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Internationalization
Classifier: Topic :: Scientific/Engineering :: Astronomy
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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](https://img.shields.io/pypi/v/hijrical)](https://pypi.org/project/hijrical/)
[![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![CI](https://github.com/kbycode/hijrical/actions/workflows/ci.yml/badge.svg)](https://github.com/kbycode/hijrical/actions/workflows/ci.yml)
[![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](pyproject.toml)
[![Live demo](https://img.shields.io/badge/▶_Live_demo-hijrical_Studio-2dd4bf)](https://kbycode.github.io/hijrical-studio/)

```python
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](https://kbycode.github.io/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](https://github.com/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

```bash
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

```python
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

```python
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:

```python
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:

```text
$ 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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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

```python
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:

```python
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):**

```python
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:**

```python
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:**

```python
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:**

```python
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:

```bash
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:

```python
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

```bash
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](LICENSE).
