hijrical 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. hijrical-1.1.0/CHANGELOG.md +46 -0
  2. hijrical-1.1.0/LICENSE +21 -0
  3. hijrical-1.1.0/MANIFEST.in +7 -0
  4. hijrical-1.1.0/PKG-INFO +451 -0
  5. hijrical-1.1.0/README.md +423 -0
  6. hijrical-1.1.0/examples/quickstart.py +82 -0
  7. hijrical-1.1.0/hijrical/__init__.py +135 -0
  8. hijrical-1.1.0/hijrical/__main__.py +8 -0
  9. hijrical-1.1.0/hijrical/_coords.py +121 -0
  10. hijrical-1.1.0/hijrical/_julian.py +75 -0
  11. hijrical-1.1.0/hijrical/_moon.py +254 -0
  12. hijrical-1.1.0/hijrical/_sun.py +145 -0
  13. hijrical-1.1.0/hijrical/calendars.py +345 -0
  14. hijrical-1.1.0/hijrical/cli.py +169 -0
  15. hijrical-1.1.0/hijrical/core.py +401 -0
  16. hijrical-1.1.0/hijrical/criteria.py +264 -0
  17. hijrical-1.1.0/hijrical/exceptions.py +41 -0
  18. hijrical-1.1.0/hijrical/holidays.py +138 -0
  19. hijrical-1.1.0/hijrical/locales/__init__.py +76 -0
  20. hijrical-1.1.0/hijrical/locales/ar.py +31 -0
  21. hijrical-1.1.0/hijrical/locales/en.py +31 -0
  22. hijrical-1.1.0/hijrical/locales/tr.py +30 -0
  23. hijrical-1.1.0/hijrical/observer.py +89 -0
  24. hijrical-1.1.0/hijrical/parsing.py +102 -0
  25. hijrical-1.1.0/hijrical/py.typed +0 -0
  26. hijrical-1.1.0/hijrical/tools.py +152 -0
  27. hijrical-1.1.0/hijrical.egg-info/PKG-INFO +451 -0
  28. hijrical-1.1.0/hijrical.egg-info/SOURCES.txt +41 -0
  29. hijrical-1.1.0/hijrical.egg-info/dependency_links.txt +1 -0
  30. hijrical-1.1.0/hijrical.egg-info/entry_points.txt +2 -0
  31. hijrical-1.1.0/hijrical.egg-info/top_level.txt +1 -0
  32. hijrical-1.1.0/pyproject.toml +49 -0
  33. hijrical-1.1.0/run_tests.py +58 -0
  34. hijrical-1.1.0/setup.cfg +4 -0
  35. hijrical-1.1.0/tests/__init__.py +1 -0
  36. hijrical-1.1.0/tests/test_arithmetic.py +54 -0
  37. hijrical-1.1.0/tests/test_astronomical.py +106 -0
  38. hijrical-1.1.0/tests/test_core.py +77 -0
  39. hijrical-1.1.0/tests/test_holidays_i18n.py +91 -0
  40. hijrical-1.1.0/tests/test_julian.py +32 -0
  41. hijrical-1.1.0/tests/test_tools.py +102 -0
  42. hijrical-1.1.0/tests/test_visibility.py +46 -0
  43. hijrical-1.1.0/tests/util.py +15 -0
@@ -0,0 +1,46 @@
1
+ # Changelog
2
+
3
+ All notable changes to **hijrical** are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/) and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [1.1.0] - 2026-06-16
8
+
9
+ ### Added
10
+ - App-builder conveniences for calendars, counters and converters:
11
+ - `hijri_range`, `iter_month`, `days_in_month`, `month_calendar` (week grid).
12
+ - `next_occurrence`, `next_holiday`, `upcoming_holidays`, `days_until_holiday`.
13
+ - `HijriDate` gains `strftime` (and `__format__`, so `f"{d:%d %B %Y}"` works),
14
+ `to_dict` (JSON-friendly), `replace`, `fromisoformat`, `day_of_year`,
15
+ `days_until`, `age_in_years` and `HijriDate.range`.
16
+ - CLI: `--json` output, plus `next` (upcoming days with countdowns) and
17
+ `calendar` (month grid) commands.
18
+
19
+ ## [1.0.0] - 2026-06-15
20
+
21
+ ### Added
22
+ - `HijriDate`: immutable, comparable, hashable Hijri date with arithmetic,
23
+ formatting, ISO output, weekday/month names and religious-day lookup.
24
+ - Two interchangeable engines:
25
+ - `ArithmeticCalendar` — exact, reversible tabular calendar with five
26
+ leap-year variants and unbounded date range.
27
+ - `AstronomicalCalendar` — location- and criterion-aware crescent-visibility
28
+ calendar (months are always 29 or 30 days).
29
+ - Crescent-visibility engine (`compute_crescent`, `CrescentInfo`) computing
30
+ elongation, altitude, arc of vision, moon age, moonset lag and crescent width
31
+ from a full Meeus lunar/solar model.
32
+ - Visibility criteria: `ircica` (default), `mabims`, `umm_al_qura`,
33
+ `conjunction`, `odeh`.
34
+ - Global / "unified" calendar mode via `AstronomicalCalendar(..., scope="global")`,
35
+ which declares a month begun once the crescent is visible anywhere on Earth
36
+ (approximating national unified calendars such as the Türkiye Takvimi).
37
+ - `HijriDate.at()` — sunset-aware (maghrib) day boundary for an instant + place.
38
+ - `Observer` with built-in city presets.
39
+ - Forgiving `parse()` understanding ISO strings and month names in any
40
+ registered language.
41
+ - Internationalization (`en`, `tr`, `ar`) with `register_locale()` for adding
42
+ more languages.
43
+ - Religious-day calendar including holy-night eves (`year_holidays`).
44
+ - Command-line interface (`hijrical` / `python -m hijrical`) with `today`,
45
+ `g2h`, `h2g`, `holidays`, `at` and `compare`.
46
+ - Zero runtime dependencies; ships `py.typed`.
hijrical-1.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 hijrical contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include run_tests.py
5
+ include hijrical/py.typed
6
+ recursive-include tests *.py
7
+ recursive-include examples *.py
@@ -0,0 +1,451 @@
1
+ Metadata-Version: 2.4
2
+ Name: hijrical
3
+ Version: 1.1.0
4
+ Summary: Accurate, location-aware Hijri <-> Gregorian date conversion with crescent-visibility support.
5
+ Author: hijrical contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yourname/hijrical
8
+ Project-URL: Documentation, https://github.com/yourname/hijrical#readme
9
+ Project-URL: Issues, https://github.com/yourname/hijrical/issues
10
+ Keywords: hijri,islamic,calendar,gregorian,umm-al-qura,ramadan,crescent,moon-sighting,ircica,diyanet,date-conversion
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Internationalization
23
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Dynamic: license-file
28
+
29
+ # hijrical 🌙
30
+
31
+ **Accurate, location-aware Hijri ⇄ Gregorian date conversion for Python.**
32
+
33
+ A modern, professional alternative to `hijridate` — without its limitations and
34
+ with the things it lacks: an *unbounded* exact calendar, real **crescent
35
+ visibility** that depends on **where you are**, the **sunset day boundary**, and
36
+ **internationalization** (English / Turkish / Arabic, easily extended).
37
+
38
+ [![Python](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)
40
+ [![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](pyproject.toml)
41
+
42
+ ```python
43
+ from hijrical import HijriDate, from_gregorian, to_gregorian
44
+
45
+ from_gregorian(2026, 6, 15) # HijriDate(1447, 12, 29, calendar='arithmetic')
46
+ to_gregorian(1447, 9, 1) # datetime.date(2026, 2, 18)
47
+ HijriDate.parse("15 Ramadan 1447") # parse human input
48
+ ```
49
+
50
+ ---
51
+
52
+ ## Why hijrical?
53
+
54
+ | | `hijridate` | **hijrical** |
55
+ |---|---|---|
56
+ | Date range | **1343–1500 AH only** (1924–2077) | Arithmetic: **unbounded**; astronomical: 1–1600 AH |
57
+ | Methods | One (Umm al-Qura table) | **Arithmetic** + **astronomical/visibility** |
58
+ | Location-aware | ❌ | ✅ Istanbul and Mecca can differ by a day |
59
+ | Sunset (maghrib) day boundary | ❌ | ✅ `HijriDate.at(instant, place)` |
60
+ | Reversible | table-bound | **Pure-integer, exact round-trip** |
61
+ | Religious days / holy nights | ❌ | ✅ with i18n + correct night eves |
62
+ | Languages | English | **en / tr / ar**, pluggable |
63
+ | Dependencies | none | none |
64
+
65
+ > `hijridate` is backed by the Umm al-Qura **table**, so it only works for
66
+ > 1924–2077 and raises outside it. hijrical's arithmetic engine is pure integer
67
+ > math: it works for **any** date and round-trips perfectly. On top of that,
68
+ > hijrical adds a real astronomical engine that models crescent visibility per
69
+ > location — the thing that actually decides when a Hijri month begins.
70
+
71
+ ---
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ pip install hijrical
77
+ ```
78
+
79
+ Zero dependencies, pure Python 3.9+. The package also works straight from a
80
+ checkout (`import hijrical` with the folder on your path).
81
+
82
+ ---
83
+
84
+ ## Quick start
85
+
86
+ ```python
87
+ from hijrical import HijriDate, from_gregorian, to_gregorian
88
+
89
+ # Gregorian -> Hijri
90
+ h = from_gregorian(2026, 6, 15)
91
+ print(h) # 29 Dhu al-Hijjah 1447 AH
92
+ print(h.isoformat()) # 1447-12-29
93
+ print(h.to_gregorian()) # 2026-06-15
94
+
95
+ # Hijri -> Gregorian
96
+ print(to_gregorian(1447, 9, 1)) # 2026-02-18 (start of Ramadan)
97
+
98
+ # Parse anything reasonable
99
+ HijriDate.parse("1447-09-01")
100
+ HijriDate.parse("15 Ramadan 1447")
101
+ HijriDate.parse("12 Rebiülevvel 1447") # Turkish month name
102
+ HijriDate.parse("١ رمضان ١٤٤٧") # Arabic digits + name
103
+
104
+ # Formatting & localization
105
+ h = HijriDate(1447, 9, 1)
106
+ h.format("{day} {month_name} {year}") # '1 Ramadan 1447'
107
+ h.format("{day} {month_name} {year}", lang="tr") # '1 Ramazan 1447'
108
+ h.format("{day} {month_name} {year} {era}", lang="ar") # '1 رمضان 1447 هـ'
109
+
110
+ # Arithmetic & comparison
111
+ h + 30 # 30 days later, as a HijriDate
112
+ HijriDate(1447, 12, 29) - HijriDate(1447, 9, 1) # day difference (int)
113
+ ```
114
+
115
+ ---
116
+
117
+ ## The two engines
118
+
119
+ ```python
120
+ from hijrical import ArithmeticCalendar, AstronomicalCalendar, HijriDate
121
+
122
+ # Arithmetic (default): exact, reversible, unbounded
123
+ HijriDate(1447, 9, 1, calendar=ArithmeticCalendar("kuwaiti"))
124
+
125
+ # Astronomical: real crescent visibility for a place + criterion
126
+ HijriDate(1447, 9, 1, calendar=AstronomicalCalendar("istanbul", "ircica"))
127
+ ```
128
+
129
+ | Engine | Use it for | Guarantee |
130
+ |---|---|---|
131
+ | `ArithmeticCalendar` | civil/database use, history, anything needing a stable, reversible mapping | Mathematically exact; same input → same output, forever |
132
+ | `AstronomicalCalendar` | predicting real religious dates as observed somewhere | A close **prediction**; may differ ±1 day from official decrees |
133
+
134
+ Arithmetic variants: `kuwaiti` (default, Type II / Microsoft), `type1`,
135
+ `type3`, `type4`, `kuwaiti_astronomical`.
136
+
137
+ ---
138
+
139
+ ## 🌍 Location-based crescent visibility (the interesting part)
140
+
141
+ A Hijri month begins when the new crescent (*hilal*) is **seen** — and whether
142
+ it can be seen depends on **where you stand**. Right after the astronomical new
143
+ moon the crescent is thin and low; from one city it clears the horizon by
144
+ sunset, from another it does not. That is exactly why **Ramadan sometimes starts
145
+ a day later in Türkiye than in Saudi Arabia.**
146
+
147
+ hijrical models this directly. For a given `Observer` and `Criterion` it computes
148
+ the Moon's real position at sunset and decides visibility:
149
+
150
+ ```python
151
+ from hijrical import HijriDate, AstronomicalCalendar
152
+
153
+ ramadan = lambda obs, crit: HijriDate(
154
+ 1447, 9, 1, calendar=AstronomicalCalendar(obs, crit)
155
+ ).to_gregorian()
156
+
157
+ ramadan("mecca", "umm_al_qura") # 2026-02-18
158
+ ramadan("istanbul", "ircica") # 2026-02-19 ← one day later
159
+ ramadan("jakarta", "mabims") # 2026-02-19
160
+ ```
161
+
162
+ Or from the command line:
163
+
164
+ ```text
165
+ $ hijrical compare 1447 9 1
166
+ Hijri 1447-09-01 in Gregorian, by method/location:
167
+ arithmetic : 2026-02-18
168
+ Mecca umm_al_qura: 2026-02-18
169
+ Mecca ircica : 2026-02-19
170
+ İstanbul umm_al_qura: 2026-02-18
171
+ İstanbul ircica : 2026-02-19
172
+ Jakarta umm_al_qura: 2026-02-19
173
+ Jakarta ircica : 2026-02-19
174
+ Rabat umm_al_qura: 2026-02-18
175
+ Rabat ircica : 2026-02-19
176
+ ```
177
+
178
+ ### What the engine actually computes
179
+
180
+ For the sunset of each candidate evening it derives, from a full Meeus lunar +
181
+ solar model (validated to arc-seconds against Meeus' own worked example):
182
+
183
+ - **elongation** (arc of light) — Sun–Moon separation,
184
+ - **altitude** — the Moon's *topocentric* altitude (parallax-corrected; the Moon
185
+ sits ~0.95° lower for a surface observer, which matters near the horizon),
186
+ - **arc of vision** (ARCV) — Moon altitude minus Sun altitude,
187
+ - **moon age** — time since conjunction,
188
+ - **lag** — how long after the Sun the Moon sets,
189
+ - **crescent width** — illuminated width in arcminutes.
190
+
191
+ You can inspect these yourself:
192
+
193
+ ```python
194
+ from datetime import date
195
+ from hijrical import compute_crescent, sunset
196
+ from hijrical.observer import resolve_observer
197
+ from hijrical._moon import new_moon_jd_ut
198
+
199
+ obs = resolve_observer("istanbul")
200
+ ss = sunset(date(2026, 2, 17), obs.latitude, obs.longitude, obs.utc_offset)
201
+ info = compute_crescent(obs, ss, new_moon_jd_ut(323))
202
+ print(info) # elong=…, alt=…, ARCV=…, age=…, lag=…, width=…
203
+ ```
204
+
205
+ ### Built-in criteria
206
+
207
+ | Criterion | Rule | Notes |
208
+ |---|---|---|
209
+ | `ircica` *(default)* | elongation ≥ 8°, altitude ≥ 5° | Türkiye / IRCICA unified-calendar thresholds |
210
+ | `mabims` | elongation ≥ 6.4°, altitude ≥ 3° | Southeast Asia (Indonesia/Malaysia/Brunei/Singapore) |
211
+ | `umm_al_qura` | Moon sets after the Sun (lag > 0) and conjunction before sunset | close to the Saudi official calendar |
212
+ | `odeh` | Odeh (2004) ARCV vs crescent-width *q*-test | naked-eye / optical zones |
213
+ | `conjunction` | conjunction before sunset | simplest baseline |
214
+
215
+ Bring your own:
216
+
217
+ ```python
218
+ from hijrical.criteria import AltitudeElongationCriterion
219
+ from hijrical import AstronomicalCalendar
220
+
221
+ my_rule = AltitudeElongationCriterion(min_elongation=7.0, min_altitude=4.0, name="custom")
222
+ AstronomicalCalendar("ankara", my_rule)
223
+ ```
224
+
225
+ ### Local vs. global ("unified") calendars
226
+
227
+ The criteria above judge visibility **at the observer's own location**. Some
228
+ national calendars (e.g. Türkiye's official *Türkiye Takvimi*, adopted in 2016)
229
+ instead use a **global** rule: the month turns over for everyone once the
230
+ crescent is visible *anywhere* on Earth within certain bounds. hijrical supports
231
+ both via the `scope` argument:
232
+
233
+ ```python
234
+ from hijrical import AstronomicalCalendar, HijriDate
235
+
236
+ local = AstronomicalCalendar("istanbul", "ircica") # "is it visible here?"
237
+ global_ = AstronomicalCalendar("mecca", "ircica", scope="global") # "is it visible anywhere?"
238
+
239
+ HijriDate(1447, 9, 1, calendar=local).to_gregorian() # 2026-02-19
240
+ HijriDate(1447, 9, 1, calendar=global_).to_gregorian() # 2026-02-18 (the world has seen it)
241
+ ```
242
+
243
+ The global mode samples a worldwide grid of locations at their local sunsets and
244
+ declares the crescent seen as soon as any of them satisfies the criterion. It is
245
+ therefore never *later* than a single-location result, and it approximates the
246
+ "unified" calendars used by several authorities. For matching a specific
247
+ authority exactly, pick the engine/criterion closest to its policy
248
+ (`umm_al_qura` for Saudi Arabia, `scope="global"` + `ircica` for a Türkiye-style
249
+ unified calendar) and treat results as predictions — the final word always
250
+ belongs to the official sighting/announcement.
251
+
252
+ ---
253
+
254
+ ## 🌇 Sunset (maghrib) day boundary
255
+
256
+ The Islamic day begins at **sunset**, not midnight — so the eve of a feast is
257
+ already, religiously, the feast's first night. `HijriDate.at()` handles this:
258
+
259
+ ```python
260
+ from datetime import datetime
261
+ from hijrical import HijriDate
262
+
263
+ HijriDate.at(datetime(2026, 6, 15, 12, 0), "istanbul").isoformat() # '1447-12-29'
264
+ HijriDate.at(datetime(2026, 6, 15, 22, 0), "istanbul").isoformat() # '1447-12-30'
265
+ ```
266
+
267
+ The same idea drives **holy-night eves** in the holidays API (below).
268
+
269
+ ---
270
+
271
+ ## 🕌 Religious days
272
+
273
+ ```python
274
+ from hijrical import year_holidays, ArithmeticCalendar
275
+
276
+ for d in year_holidays(1447, ArithmeticCalendar("kuwaiti")):
277
+ print(d.gregorian, d.name(lang="tr"), "| night:", d.eve)
278
+ ```
279
+
280
+ Covers Islamic New Year, Ashura, Mawlid, Raghaib (first Friday eve of Rajab),
281
+ Isra & Mi'raj, Mid-Sha'ban, Ramadan, Laylat al-Qadr, Eid al-Fitr (3 days),
282
+ Arafah and Eid al-Adha (4 days). Holy nights carry an `eve` (the Gregorian
283
+ evening the night begins). A single date's holiday:
284
+
285
+ ```python
286
+ HijriDate(1447, 9, 27).holiday("en") # 'Laylat al-Qadr'
287
+ HijriDate(1447, 9, 27).holiday("tr") # 'Kadir Gecesi'
288
+ ```
289
+
290
+ ---
291
+
292
+ ## 🧰 Recipes for app builders
293
+
294
+ Everything you need for **calendar apps, countdown widgets and converters**.
295
+
296
+ **Format dates (strftime-style, works in f-strings):**
297
+
298
+ ```python
299
+ h = HijriDate(1447, 9, 1)
300
+ h.strftime("%d %B %Y (%A)") # '01 Ramadan 1447 (Wednesday)'
301
+ f"{h:%d.%m.%Y}" # '01.09.1447'
302
+ h.strftime("%d %B %Y", lang="tr")
303
+ ```
304
+
305
+ **Iterate and lay out a month grid:**
306
+
307
+ ```python
308
+ from hijrical import hijri_range, iter_month, month_calendar
309
+
310
+ for d in hijri_range(HijriDate(1447, 9, 1), HijriDate(1447, 10, 1)):
311
+ ... # every day of Ramadan 1447
312
+
313
+ weeks = month_calendar(1447, 9) # list of weeks, Monday-first
314
+ for week in weeks: # each week is 7 cells (HijriDate or None)
315
+ print(" ".join(f"{c.day:2}" if c else " " for c in week))
316
+ ```
317
+
318
+ **Countdowns and special-day counters:**
319
+
320
+ ```python
321
+ from hijrical import next_holiday, days_until_holiday, next_occurrence
322
+
323
+ days_until_holiday("ramadan_start") # e.g. 247 (days from today)
324
+ nh = next_holiday(key="eid_al_fitr") # next Eid al-Fitr as a ReligiousDay
325
+ print(nh.gregorian, nh.name("tr"))
326
+
327
+ next_occurrence(1, 1) # next Islamic New Year (annual recurrence)
328
+ HijriDate.today().days_until(nh.gregorian) # generic day countdown
329
+ ```
330
+
331
+ **Serialize for an API / converter UI:**
332
+
333
+ ```python
334
+ HijriDate(1447, 9, 27).to_dict("tr")
335
+ # {'year': 1447, 'month': 9, 'day': 27, 'iso': '1447-09-27',
336
+ # 'gregorian': '2026-03-16', 'jdn': 2461116, 'weekday_index': 0,
337
+ # 'weekday': 'Pazartesi', 'month_name': 'Ramazan', 'method': 'arithmetic',
338
+ # 'holiday': 'Kadir Gecesi'}
339
+ ```
340
+
341
+ **Misc helpers:** `HijriDate.fromisoformat("1447-09-01")`, `.replace(day=15)`,
342
+ `.day_of_year()`, `.age_in_years(birth_on)`, `days_in_month(year, month)`.
343
+
344
+ On the command line:
345
+
346
+ ```bash
347
+ hijrical calendar 1447 9 --lang tr # print a month grid
348
+ hijrical next --lang tr --count 8 # upcoming religious days with countdowns
349
+ hijrical next --key ramadan_start # next start of Ramadan
350
+ hijrical g2h 2026-03-16 --json # machine-readable output
351
+ ```
352
+
353
+ ---
354
+
355
+ ## 🌐 Internationalization
356
+
357
+ Three languages ship in the box; adding one is a dictionary:
358
+
359
+ ```python
360
+ from hijrical import register_locale, HijriDate
361
+
362
+ register_locale({
363
+ "code": "fr", "name": "Français", "era": "AH", "day_suffix": " (jour {n})",
364
+ "months": ("Mouharram", "Safar", "Rabi al-awwal", "Rabi al-thani",
365
+ "Joumada al-oula", "Joumada al-thania", "Rajab", "Chaabane",
366
+ "Ramadan", "Chawwal", "Dhou al-qida", "Dhou al-hijja"),
367
+ "weekdays": ("Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"),
368
+ "holidays": { "new_year": "Nouvel an hégirien", "ramadan_start": "Début du Ramadan",
369
+ # … the remaining keys …
370
+ },
371
+ })
372
+
373
+ HijriDate(1447, 9, 1).format("{day} {month_name} {year}", lang="fr") # '1 Ramadan 1447'
374
+ ```
375
+
376
+ Newly registered month names become parseable automatically.
377
+
378
+ ---
379
+
380
+ ## 🖥️ Command line
381
+
382
+ ```bash
383
+ hijrical today
384
+ hijrical g2h 2026-06-15
385
+ hijrical h2g "15 Ramadan 1447" --lang tr
386
+ hijrical g2h 2026-02-18 --method astronomical --observer istanbul --criterion ircica
387
+ hijrical holidays 1447 --lang tr
388
+ hijrical compare 1447 9 1
389
+ hijrical at "2026-06-15T22:00" --observer istanbul
390
+ ```
391
+
392
+ (Use `python -m hijrical …` if the script isn't on your PATH.)
393
+
394
+ ---
395
+
396
+ ## API reference (essentials)
397
+
398
+ | Symbol | Purpose |
399
+ |---|---|
400
+ | `HijriDate(year, month, day, calendar=None)` | Construct a Hijri date |
401
+ | `HijriDate.from_gregorian(y, m, d, calendar=None)` / `from_gregorian(...)` | Gregorian → Hijri |
402
+ | `HijriDate.from_jdn(jdn)` / `from_date(date)` | From JDN / `date` |
403
+ | `HijriDate.parse(text)` / `parse(text)` | Parse a string |
404
+ | `HijriDate.today()` | Now (civil) |
405
+ | `HijriDate.at(instant, observer)` | Sunset-aware date |
406
+ | `.to_gregorian()` / `to_gregorian(y, m, d)` | Hijri → Gregorian `date` |
407
+ | `.format(pattern, lang)` / `.isoformat()` | Formatting |
408
+ | `.month_name(lang)` / `.weekday_name(lang)` | Localized names |
409
+ | `.holiday(lang)` | Religious-day name or `None` |
410
+ | `.month_length()` / `.year_length()` / `.is_leap_year()` | Calendar info |
411
+ | `ArithmeticCalendar(variant)` | Tabular engine |
412
+ | `AstronomicalCalendar(observer, criterion, scope="local"\|"global")` | Visibility engine (local or unified) |
413
+ | `Observer(name, latitude, longitude, utc_offset)` | A location |
414
+ | `compute_crescent(observer, sunset, conj_jd)` → `CrescentInfo` | Visibility geometry |
415
+ | `get_criterion(name)` / `available_criteria()` | Criteria |
416
+ | `year_holidays(year, calendar)` | Religious days of a year |
417
+ | `hijri_range(start, end, step)` / `HijriDate.range(...)` | Iterate dates |
418
+ | `month_calendar(year, month)` / `iter_month(...)` | Month grid / days |
419
+ | `next_occurrence(month, day, after)` | Next annual recurrence |
420
+ | `next_holiday(after, key)` / `upcoming_holidays(...)` / `days_until_holiday(...)` | Countdowns |
421
+ | `.strftime(fmt)` / `f"{d:%d %B %Y}"` / `.to_dict()` | Formatting & serialization |
422
+ | `.days_until(other)` / `.age_in_years(on)` / `.replace(...)` / `.fromisoformat(...)` | Date math |
423
+ | `register_locale(dict)` / `available_languages()` | i18n |
424
+
425
+ `.format()` fields: `{year} {month} {day} {month02} {day02} {month_name}
426
+ {weekday} {era} {method}`.
427
+
428
+ ---
429
+
430
+ ## Accuracy & validation
431
+
432
+ - **JDN round-trip:** 0 errors over hundreds of thousands of random dates.
433
+ - **Arithmetic round-trip:** 0 errors across all variants and a 600k-day sweep.
434
+ - **Lunar model:** matches Meeus' worked example 47.a to ~0.00004° in longitude;
435
+ distance and latitude exact to the quoted precision.
436
+ - **Umm al-Qura:** the `mecca` + `umm_al_qura` configuration reproduces seven
437
+ official anchor dates (Ramadan starts and both Eids) exactly.
438
+ - 54 unit tests + 18 doctests, including round-trips, the location/`scope`
439
+ behaviour, i18n and the app-builder helpers. Run them with
440
+ `python run_tests.py` (no pytest needed) or `pytest`.
441
+
442
+ > **Disclaimer.** Astronomical/visibility results are predictions. Actual
443
+ > religious dates depend on local moon sighting and the rulings of competent
444
+ > authorities (e.g. Diyanet İşleri Başkanlığı). Use the arithmetic engine when
445
+ > you need determinism and reversibility.
446
+
447
+ ---
448
+
449
+ ## License
450
+
451
+ MIT — see [LICENSE](LICENSE).