Skip to content

fix: derive Jalali conversions from the astronomical calendar model - #69

Closed
majiidd wants to merge 11 commits into
masterfrom
fix/astronomical-jalali-conversions
Closed

fix: derive Jalali conversions from the astronomical calendar model#69
majiidd wants to merge 11 commits into
masterfrom
fix/astronomical-jalali-conversions

Conversation

@majiidd

@majiidd majiidd commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Problem

Reported: persiantools converts Gregorian 623-01-01 to Jalali 1-10-11 while astronomical converters such as time.ir give 1-10-10, and the kabiseh (leap year) logic was internally inconsistent.

Investigation confirmed two real defects on master:

  • Conversion bug (Gregorian ≤ 1600): the year <= 1600 epoch branch in to_jalali was arithmetically wrong — 185,426 days in 622–2130 disagreed with the model the rest of the code implements (e.g. 623-03-21 → 2-1-1, skipping Esfand 30 of leap year 1), and 237 days raised ValueError.
  • Kabiseh inconsistency: is_leap/days_in_month used the ICU4X NON_LEAP_CORRECTION_SET while to_jalali/to_gregorian used plain 33-year arithmetic — so 2124-03-20 was unrepresentable (ValueError) and 1503-12-30 / 1504-01-01 both mapped to 2125-03-21.

What changed

All conversions, ordinals, and leap checks now derive from a single internal model (_days_before_year + is_leap):

  • Epoch corrected to the documented Solar Hijri epoch: Friday 1 Farvardin 1 = 22 March 622 CE proleptic Gregorian (19 March 622 Julian, R.D. 226896).
  • Years 1–1177 follow the astronomical Persian calendar (vernal equinox at 52.5°E, Calendrical Calculations), generated with roozbehp/persiancalendar — the model that reproduces the official leap-year table of the Iranian calendar authority (Calendar Center, Institute of Geophysics, University of Tehran, 1206–1498) exactly. Encoded as 133 flip years against the 33-year rule.
  • Five borderline flip pairs overridden — (978, 979), (1011, 1012), (1044, 1045), (1077, 1078), (1176, 1177): their effects would reach into Gregorian 1601+, where all established implementations agree with the 33-year value, and each hinges on the equinox missing the midday cutoff by 36 s–11 min (inside ephemeris model uncertainty).
  • Years 1178–2987 unchanged (33-year rule + ICU4X corrections); 2988+ unchanged (plain 33-year rule).

Behavior changes

Range Change
Gregorian 622-03-22 … 1568-03-20 Corrected to the astronomical model (623-01-01 → 1-10-10; 237 former ValueError days fixed)
Gregorian 622-03-21 Was 1-1-1; now pre-epoch → ValueError
is_leap, years ≤ 1177 Changes for 133 flip years (e.g. is_leap(1): True → False)
Gregorian 1568-03-21 … 2124-03-19 No change (verified day-by-day)
Gregorian ≥ 2124-03-20 Conversions now follow the correction set (Norouz 1503 = 2124-03-20; no more ValueError/collisions)
toordinal() / fromordinal() All ordinals shift by −1 (epoch moved one day later); round-trips preserved; may affect callers who persisted raw ordinals

Validation

  • Compatibility sweep: all 191,100 days in 1601-01-01 … 2124-03-19 convert identically to master.
  • Astronomical reference: Norouz and leap flag match roozbehp/persiancalendar for every year 1–3000 except the five documented overrides.
  • JDF reference: all 202,709 days of Persian years 947–1501 identical to the jdf.scr.ir algorithm.
  • Integrity: all 3,424,878 representable days (622-03-22 … 9999-03-20) round-trip exactly with monotonic ordinals; every year length matches is_leap.
  • New tests: epoch (Friday 622-03-22), the 623-01-01 regression, a 32-row ancient Norouz table, ancient leap flips, plus the official 1206–1498 kabiseh table test (tests/test_official_kabise.py, all 293 years). make check green; opt-in performance tests pass.

Note: the jdf.scr.ir algorithm itself also yields 1-10-11 for 623-01-01 — the reported time.ir value comes from the astronomical model, which this PR adopts for ancient years on the strength of the documented epoch (Persian Wikipedia: جمعه ۱ فروردین سال ۱ = ۲۲ مارس ۶۲۲) and the official authority's astronomical practice.

🤖 Generated with Claude Code

Majid and others added 3 commits July 16, 2026 03:08
…y data

Add table-driven tests based on the leap-year table (Kabise Shamsi
1206-1498) published by the Calendar Center, Institute of Geophysics,
University of Tehran. Covers Norouz dates in both conversion directions,
is_leap and Esfand length for all 293 years, year-boundary conversions,
and a day-by-day round trip across the entire official range.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
to_jalali, to_gregorian, and toordinal now share a single
_days_before_year helper that is exactly consistent with is_leap
(33-year cycle plus the non-leap correction years), instead of two
independent arithmetic paths. This fixes:

- to_jalali producing off-by-one dates for ~185k days and raising
  ValueError for 237 days in Gregorian 622-1600, because the old
  epoch-shift branch did not start on a 33-year cycle boundary
- one unrepresentable Gregorian day (ValueError) and one double-mapped
  Jalali date per non-leap correction year (first: 2124-03-20 and
  1503-12-30), where conversions ignored the corrections is_leap applies

Verified: exhaustive scan of all 3,424,879 supported days round-trips
with no exceptions and one-day monotonic steps; conversions now match
the astronomical Persian calendar (Calendrical Calculations at 52.5E,
roozbehp/persiancalendar) for every year 1178-3000 and remain identical
to the official University of Tehran table for 1206-1498. Behavior is
unchanged for Jalali years 980-1501.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Correct the epoch to Friday 22 March 622 CE (proleptic Gregorian) and
derive leap years 1..1177 from the astronomical Persian calendar
(Calendrical Calculations at the 52.5E meridian), keeping the consensus
33-year values for five borderline flip pairs so conversions are
unchanged for every day on or after Gregorian 1568-03-21.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Aug 4, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.68050% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.19%. Comparing base (9b95cee) to head (7899a38).

Files with missing lines Patch % Lines
persiantools/jdatetime.py 96.17% 8 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #69      +/-   ##
==========================================
- Coverage   98.47%   98.19%   -0.29%     
==========================================
  Files           5        5              
  Lines        1117     1220     +103     
==========================================
+ Hits         1100     1198      +98     
- Misses         17       22       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@majiidd majiidd closed this Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants