-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix 2.11->2.12 DST issues with differenceInDays (fix #1750) #1754
Fix 2.11->2.12 DST issues with differenceInDays (fix #1750) #1754
Conversation
My new tests failed in CI for the Atlantic/Azores timezone. Interestingly, the tests failed for Luckily the DST config for Atlantic/Azores is unusual: their standard time is UTC-1 and their DST is UTC, so the problem case seems pretty obvious: transitions to or from DST across or on local date boundaries and/or UTC date boundaries. Kudos to the date-fns team for a great CI setup to catch weird bugs like this! |
Thank you so much for detailed research, your help is invaluable 👏 |
1ca28ee
to
6fdabea
Compare
6fdabea
to
c6f2073
Compare
OK, fixed the problem with Atlantic/Azores. The culprit was my test code to find DST boundaries, which broke if those boundaries were at midnight. Fixed now. Also I rebased to the current master and put my changelog edits on top. Should be ready to merge. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🙏 Such great work, thank you!
I've shipped the change with |
As documented in #1750, date-fns 2.12 (specifically #1630) contained a breaking change that made
differenceInDays
stop matchingaddDays
across a DST boundary. This PR replaces #1630 with an implementation that fixes #533 (like #1630 did) without causing #1750 (like #1630 also did).This PR:
differenceInDays
(so should code-review against 2.11 implementation) but modifying it to fix Incorrect result across DST in differenceInDays #533 without triggering differenceInDays no longer matches addDays (breaking change in 2.12?) #1750. It does this by replacing the use indifferenceInDays
ofcompareAsc
with a new local function (compareLocalAsc
) that compares using local time not UTC timestamps. This change prevents errors when one of the times is on a DST boundary or contains an ambiguous local time due to DST.differenceInDays
to validate the work above. These tests will work in any timezone, thanks to new shared test code that detects DST start in the local timezone. All DST tests in this PR are relative to the DST start/end dates detected for the current timezone. These tests will be skipped in non-DST time zones.yarn test
script command and then manually running tests under each TZ.differenceInDays
to document DST behavior and to explain how to get non-DST behavior (full 24-hour periods only) usingMath.floor(differenceInHours(dateLeft, dateRight)/24)|0
.differenceInWeeks
which is implemented usingdifferenceInDays
and shares its DST behavior.Fix Details
#533, like most JS bugs related to DST, happened because the pre-2.12 implementation of
differenceInDays
mixed UTC and local date calculations:CompareAsc
uses UTC comparisons whileDate.setDate
(used indifferenceInDays
) uses local timezone math. The result: on or around DST boundariesCompareAsc
returned results which suggested that the difference was not a full day when it really was, or vice versa. This led to off-by-one errors in results like #533.The fix was to restore the 2.11 implementation of
differenceInDays
(effectively reverting #1630) and simply replace usage ofcompareAsc
with a new local functioncompareLocalAsc
that uses local date math instead.If you really want to get into the weeds to understand how
setDate
works and why #533 happened, start here:Discussion
"Full Days": 24-hour periods vs. matching local times
Some contributors in #533 suggested that date diffs should be measured in 24-hour periods only and should ignore DST. To see why this is problematic, imagine a user measures a 7-day period from (local) noon to (local) noon one week later. If we only diff 24-hour periods, then that period would measure 7 days most of the year, but would return 6 days if DST started in the middle. This intermittent variation is a certain cause of hard-to-diagnose bugs, and is also different from how date-fns has behaved for a long time. That's why this PR goes back to the 2.11 behavior of taking DST into account. BTW, if a developer wants to ignore DST and just count 24-hour periods, this PR documents one way (
Math.floor(differenceInHours(dateLeft, dateRight)/24)|0
) in user-visible JSDoc comments.API Consistency
I admit that I couldn't fully understand the consistency discussion in #533. To me it seems like the pre-2.12 behavior was more consistent, because in 2.11
differenceInDays
was consistent withaddDays
, anddifferenceInDays
was also consistent withdifferenceInMonths
. And with JS Date APIs likeDate.setDate
.Guidelines
In case it's helpful, here are three general rules of thumb that I use for safe date math:
Date.getTime
with local APIs likeDate.setDate
, it's easy to end up with off-by-one-hour bugs like Incorrect result across DST in differenceInDays #533 or Starting with v2.0.0-beta.1 date-fns doesn't accept strings as arguments. Please useparseISO
to parse strings #1083 1510.Future Investigation Needed
TODO: I suspect that
differenceInMonths
,differenceInISOWeekYears
, anddifferenceInYears
are vulnerable to the same bug but I didn't have the time yet to investigate but I wanted to get this breaking-change fix out ASAP.