fix(date-fns): keep the month grid intact across midnight DST transitions - #2811
Open
fernandolecarosluna wants to merge 1 commit into
Open
fernandolecarosluna wants to merge 1 commit into
fernandolecarosluna wants to merge 1 commit into
Conversation
…ions
The date-fns localizer is the only official one that does not supply its own
date arithmetic, so it falls back on `date-arithmetic`. That implementation
advances a day by adding 24h of elapsed time and then correcting the offset to
preserve the wall-clock time.
In zones where DST starts at midnight the corrected target — 00:00 on the
transition day — does not exist, so the result lands an hour earlier, back on
the previous day. `visibleDays` then emits that day twice: the month grid draws
it in two columns, every later day shifts one column away from its weekday
header, and the extra cell spills into a seventh row. `startOf`/`endOf` reach
the week boundary through `weekday()`, which is built on the same `add`, so
week boundaries land a day early too and the week view renders six or eight
columns.
Thirteen zones in the IANA database transition at midnight — America/Santiago,
America/Coyhaique, America/Havana, America/Asuncion, America/Scoresbysund,
Asia/Beirut, Asia/Damascus, Asia/Amman, Asia/Tehran, Asia/Gaza, Asia/Hebron,
Africa/Cairo and Atlantic/Azores — so the grid breaks there once a year, every
year.
Adding by calendar components ("the day after the 5th is the 6th") is correct
in every zone whether that day ran 23, 24 or 25 hours. Time units keep using
elapsed time and month/year keep delegating to `dates.add`, which is already
component-based, so nothing changes outside the affected transitions. No new
runtime dependency: the localizer receives date-fns functions from the caller,
so the arithmetic is written with plain `Date`.
The added tests assert timezone-independent invariants — a grid is whole weeks
of consecutive days, a week spans seven days, adding one day lands on the next
calendar day — so they guard the fix wherever CI runs. Running them under
`TZ=America/Santiago` reproduces the regression against the previous behaviour.
This branch has not been deployed
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.
The problem
The
date-fnslocalizer is the only official one that does not supply its own date arithmetic —moment,luxonanddayjsall do — so it falls back ondate-arithmetic. That implementation advances a day by adding 24h of elapsed time and then correcting the offset so the wall-clock time is preserved.In zones where DST starts at midnight, the hour that correction aims for does not exist. Chile jumps from
00:00straight to01:00on the transition day, so the corrected result lands an hour earlier — back on the previous day:visibleDaysthen emits that day twice. The month grid draws it in two columns, every later day shifts one column away from its weekday header, and the extra cell spills into a seventh row:Sunday the 6th ends up under the
Monheader, and so does every Sunday for the rest of the month.startOf/endOfreach the week boundary throughweekday(), which is built on the sameadd, so week boundaries land a day early too and the week view renders six or eight columns on the transition week.Thirteen zones in the IANA database transition at midnight, so the grid breaks in each of them once a year, every year:
America/Santiago·America/Coyhaique·America/Havana·America/Asuncion·America/Scoresbysund·Asia/Beirut·Asia/Damascus·Asia/Amman·Asia/Tehran·Asia/Gaza·Asia/Hebron·Africa/Cairo·Atlantic/AzoresThe fix
Add by calendar components rather than elapsed time — "the day after the 5th is the 6th" — which is correct in every zone whether that day ran 23, 24 or 25 hours. The localizer now supplies
add,range,startOf,endOf,firstVisibleDay,lastVisibleDayandvisibleDays, which is the extension pointDateLocalizeralready documents and the other three localizers already use.Deliberately narrow:
month/year/decade/centurykeep delegating todates.add, which is already component-based.startOf/endOfdelegate every unit exceptweek, the only one routed through the brokenadd.Date.Outside the affected transitions the output is unchanged — I verified this separately by rendering a month in
Europe/Madridwith and without the patch and diffing the markup: byte for byte identical.Tests
Four tests added to
test/localizers/date-fns.test.js, sweeping 2020–2035:visibleDaysreturns whole weeks of consecutive daysvisibleDayscovers every day of the month exactly onceaddlands on the next calendar day, whatever its lengthThey assert timezone-independent invariants, so they guard the fix wherever CI runs.
process.env.TZcannot be changed at runtime under Jest — it sandboxesprocess.env, so the mutation never reaches V8's timezone cache — so to watch them fail against the current behaviour, run the suite in an affected zone:That is how I confirmed they are meaningful: 4 failed before the change, all green after.
Full suite on this branch: 948 passed, 47 suites.
eslint srcandprettier --checkclean.Context
Found in production in a Chilean events-management app. Happy to adjust naming, split the commit, or move the tests wherever you prefer.
🤖 Generated with Claude Code