Temporal: a persian year the .NET table stops inside is measured by the calendar - #3528
Merged
Merged
Conversation
…he calendar
PersianCalendar ends at ISO 9999-12-31 because DateTime does, not because the
Persian calendar does, and that date falls inside Persian 9378. Asked about that
year the table answers with the part it holds -- ten months, 289 days, a tenth
month thirteen days long -- and answers consistently, so nothing it says gives
the truncation away.
A month step that stopped inside that tenth month had its day clamped to the
thirteenth and carried the loss forward, while a step that flew over the month
kept it, so add({ months: 8 }) and eight single steps from ISO 9999-06-01 landed
a day apart.
The band a backing Calendar is trusted for now covers only the years its table
holds whole, so 9378's month count and month lengths come from the arithmetic
reckoning that already answered for every year past it. Where a date sits is
unchanged: the table placed every ISO date it covers and still does.
Fixes sebastienros#3523
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S
lahma
force-pushed
the
temporal-persian-table-edge
branch
from
August 31, 2026 18:49
6162a04 to
1e195b9
Compare
lahma
added a commit
that referenced
this pull request
Sep 9, 2026
… on the 33-year cycle, so the ends of Temporal's range land in the right Persian year (#4006) Backport of PR #3751 (commit 5a57a7d) from main. `PersianCalendar` spans ISO 622-03-22 to 9999-12-31 and derives those years astronomically; everything outside them is answered by an arithmetic rule, because no ephemeris covers a quarter of a million years. That rule was the 2820-year cycle (Reingold-Dershowitz / Birashk), and it is not the one the platform's `persian` calendar is: ICU places a year's first day 365 * (year - 1) + floor((8 * year + 21) / 33) days after the epoch and calls a year a leap year when floorMod(25 * year + 11, 33) < 8. Over Temporal's own range the two drift about two months apart, which put both ends of it in the wrong Persian year. Where the hand-off happens is now an ISO window this repository states -- 622-03-22 through 9999-12-31, held as `PersianTableFirstJdn`/`PersianTableLastJdn` and, for the direction that asks in Persian fields, `PersianTableHolds` -- rather than the platform's `MinSupportedDateTime`/`MaxSupportedDateTime` and the `ArgumentOutOfRangeException` `ToDateTime` raises outside them. Adapted for 4.x: - `PersianDateToIso`'s maximum-day lookup keeps 4.x's `try`/`catch` around `PersianCal.GetDaysInMonth`; main's `SupportsYear`/`TryGetDaysInMonth` helpers come from the un-ported #3502/#3528 chain and do not exist here. - The `HebrewMonthSteppingTests` hunk is dropped: that file arrived with #3525, which 4.x does not carry. - Tests transcribed from NUnit to xUnit, which is what 4.x's `Jint.Tests` still is. - Only one of main's four test262 exclusions comes out here. `ZonedDateTime/from/extreme-dates.js` passes; the three `*/prototype/withCalendar/extreme-dates.js` files get past their persian rows and stop at `chinese minimum non-approximated date`, which is the lunisolar reckoning chain (#3482/#3502/#3519) 4.x does not carry, so they stay excluded with that reason recorded. - `TheYearTheWindowStopsInsideIsStillAWholeYear` is narrowed to `TheLastDayTheWindowHoldsIsStillTheTablesToPlace`: its three length assertions pin #3528's answer for the part-year 9378, and #3528 is not on this branch. Claude-Session: https://claude.ai/code/session_01SLCujwvKtTvtWD9f6RTyiF Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3523. Present on
maintoday at2255b5c9— #3525 landed after the issue was filed and touched theone site the issue names, and reproduces it unchanged.
What is wrong
PersianCalendarends at ISO 9999-12-31 becauseDateTimedoes, not because the Persian calendar does, andthat date falls inside Persian year 9378. Asked about that year the table answers with the part it holds —
ten months, 289 days, and a tenth month thirteen days long — and answers consistently:
GetMonthsInYearsays 10,
GetDaysInYearsays 289,GetDaysInMonth(9378, 10)says 13 andGetDayOfYear(MaxSupportedDateTime)says 289. Nothing the table says contradicts anything else it says, which is why a truncated year is invisible
from inside it.
A month step that stopped inside that tenth month therefore had its day clamped to the thirteenth and
carried the loss forward, while a step that flew over the month kept it:
Steps +1 to +7 agree; the two spellings part at +8 and stay a day apart after it. Swept day by day over the
last thirty years the table covers and the first thirty past it, 2,496 of 560,976 additions disagreed, and
the stepped route collapses hundreds of distinct starts onto the same answer:
untilwas collateral: a difference measured across the seam came back mixed-sign, which no Temporalduration may be —
persianfrom ISO +010000-02-16 back to +010000-01-07 answered-1 monthand+7 days,because the intermediate the month walk left off at had been clamped onto 9999-12-31.
The fix
The band a backing
Calendaris trusted for now covers only the years its table holds whole. #3525'sSupportsYearis still the one site that decides which reckoning answers for a year;YearBand.Ofnow trimsa boundary year off the band when the table cannot state a length for the last month that year holds — asked
of the calendar's own reckoning, which is the only thing the table's self-consistent account can be checked
against. So Persian 9378's month count and month lengths come from the same arithmetic reckoning that already
answered for every year past it, and its tenth month is thirty days long.
The issue's other option — asking the table how many months a year holds — was rejected: it would have made
9378 a ten-month year, so a month step would skip its eleventh and twelfth months entirely, some fifty-nine
days at a stride.
Where a date sits is not what this decides. The table placed every ISO date it covers and still does, so
the placement in
PersianDateToIsostill triesToDateTimefirst and falls to the reckoning only for a datethe table has no room for. That is what keeps
fields → ISO → fieldsthe identity, which is the propertyadditivity actually needs, and it is why no date moved.
The one other change is that
PersianToCalendarDateno longer reports the table'sdaysInMonthanddaysInYearfor a year it stops inside — a date whose fields the engine reports has to be a date whosearithmetic the engine performs (#3483), and 289 days in a twelve-month year was neither.
Hebrew and Um al-Qura both end where their data ends, on a year boundary, and the trim is a no-op for them:
Hebrew 5343 begins before
MinSupportedDateTimebut still answers with twelve months the table can measure,so it stays in the band.
What changed, measured
Every observable answer was dumped before and after, on
net8.0, and diffed:persian, all in 9378add(years × months × overflow)persian, all landing in 9378-M10constrainandreject, boundary years)persian9378-M10 days 14–31until(4 largest units, 8 spans)persiandifferences at the seamEverything that differs is inside Persian 9378 and nowhere else:
every one of the 417,136 field reads, including all 289 in 9378;
daysInMonthin its tenth month is 30 rather than 13 anddaysInYearanywhere in it is 365 rather than 289(ISO 9999-03-18 to 9999-12-31);
rejected under
overflow: 'reject', and now place at ISO +010000-01-02 through +010000-01-18;untildifferences all lose the mixed sign described above.No other calendar's answers changed:
hebrew,islamic-umalqura,islamic-civil,chinese,copticandindianare byte-identical across all four dumps.Tests
Jint.Tests/Runtime/CalendarBandEdgeTests.cs, which fails on unmodifiedmainin four of its fourteen casesand passes here:
ones, stepped one month at a time and in one go, forwards and backwards, thirteen months each way.
persian: 560,976 cases, 2,496 disagreeing onmain, none here. The Hebrew band edges get the identicalsweep — 578,214 cases at the bottom and 578,188 at the top — and report nothing before or after, which is
what says the shape does not hide there. Temporal: adding months in hebrew counts the Metonic cycle #3525's sweep stepped by 37 days and could step over a seam one day
wide; this one is dense on purpose. Only starts on day 29 or earlier are asserted, since a start above that
legitimately diverges in every calendar (ISO 2024-01-31 plus two months is March 31, and one month twice is
March 29);
a.add(a.until(b))reachingbacross each edge, in all four largest units;year of 366 days there and a common year of 365 by the reckoning, so the assertion says which one answered.
Jint.Testsis green onnet472,net8.0andnet10.0;Jint.Tests.PublicInterface,Jint.Tests.CommonScriptsand
Jint.Tests.SourceGeneratorstoo. test262: 102,537 passed / 0 failed / 151 skipped of 102,688, thecontrol figure exactly.
No benchmark is offered and none would say anything: the change is a comparison against two cached
ints on apath that already made one.
Migration guide
§4.87, at the end of chapter 4.
🤖 Generated with Claude Code
https://claude.ai/code/session_014W5mbjGhyvgAS4pivXoc4S