Skip to content

Commit

Permalink
Normative: Fix intermediate value in ZonedDateTime difference
Browse files Browse the repository at this point in the history
To find the difference between two ZonedDateTimes, we first find the
calendar difference between their date parts. Then we find the time
difference between an intermediate ZonedDateTime value (calculated by
adding the calendar parts to the first ZonedDateTime) and the second
ZonedDateTime.

Previously we would calculate the intermediate value by adding years,
months, and weeks from the date difference, because the days were
calculated as part of the time difference.

This was incorrect, because the intermediate value can shift if it falls
in the middle of a DST transition. However, that could be on a completely
different date than would be expected, leading to surprising results like
this:

const duration = Temporal.Duration.from({ months: 1, days: 15, hours: 12 });
const past = Temporal.ZonedDateTime.from('2024-02-10T02:00[America/New_York]');
const future = past.add(duration);

duration  // => 1 month, 15 days, 12 hours
past.until(future, { largestUnit: 'months' })  // => 1 month, 15 days, 11 hours

This result would occur because of a DST change on 2024-03-10T02:00,
unrelated to either the start date of 2024-02-10 or the end date of
2024-03-25. With this change, the intermediate value occurs on
2024-03-25T02:00 and would only shift if that was when the DST change
occurred.
  • Loading branch information
ptomato committed Feb 20, 2024
1 parent 7b29094 commit cd903c7
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 47 deletions.
91 changes: 50 additions & 41 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3925,49 +3925,58 @@ export function DifferenceZonedDateTime(
const dtStart = precalculatedDtStart ?? GetPlainDateTimeFor(timeZoneRec, start, calendarRec.receiver);
const dtEnd = GetPlainDateTimeFor(timeZoneRec, end, calendarRec.receiver);

let { years, months, weeks } = DifferenceISODateTime(
GetSlot(dtStart, ISO_YEAR),
GetSlot(dtStart, ISO_MONTH),
GetSlot(dtStart, ISO_DAY),
GetSlot(dtStart, ISO_HOUR),
GetSlot(dtStart, ISO_MINUTE),
GetSlot(dtStart, ISO_SECOND),
GetSlot(dtStart, ISO_MILLISECOND),
GetSlot(dtStart, ISO_MICROSECOND),
GetSlot(dtStart, ISO_NANOSECOND),
GetSlot(dtEnd, ISO_YEAR),
GetSlot(dtEnd, ISO_MONTH),
GetSlot(dtEnd, ISO_DAY),
GetSlot(dtEnd, ISO_HOUR),
GetSlot(dtEnd, ISO_MINUTE),
GetSlot(dtEnd, ISO_SECOND),
GetSlot(dtEnd, ISO_MILLISECOND),
GetSlot(dtEnd, ISO_MICROSECOND),
GetSlot(dtEnd, ISO_NANOSECOND),
calendarRec,
largestUnit,
options
);
let intermediateNs = AddZonedDateTime(
start,
timeZoneRec,
calendarRec,
years,
months,
weeks,
0,
TimeDuration.ZERO,
dtStart
);
// may disambiguate
const sign = nsDiff.lt(0) ? -1 : 1;
for (let dayCorrection = 0; dayCorrection <= 1; dayCorrection++) {
const { year, month, day } = BalanceISODate(
GetSlot(dtEnd, ISO_YEAR),
GetSlot(dtEnd, ISO_MONTH),
GetSlot(dtEnd, ISO_DAY) + dayCorrection * -sign
);

let norm = TimeDuration.fromEpochNsDiff(ns2, intermediateNs);
const intermediate = CreateTemporalZonedDateTime(intermediateNs, timeZoneRec.receiver, calendarRec.receiver);
let days;
({ norm, days } = NormalizedTimeDurationToDays(norm, intermediate, timeZoneRec));
const { years, months, weeks, days } = DifferenceISODateTime(
GetSlot(dtStart, ISO_YEAR),
GetSlot(dtStart, ISO_MONTH),
GetSlot(dtStart, ISO_DAY),
GetSlot(dtStart, ISO_HOUR),
GetSlot(dtStart, ISO_MINUTE),
GetSlot(dtStart, ISO_SECOND),
GetSlot(dtStart, ISO_MILLISECOND),
GetSlot(dtStart, ISO_MICROSECOND),
GetSlot(dtStart, ISO_NANOSECOND),
year,
month,
day,
GetSlot(dtEnd, ISO_HOUR),
GetSlot(dtEnd, ISO_MINUTE),
GetSlot(dtEnd, ISO_SECOND),
GetSlot(dtEnd, ISO_MILLISECOND),
GetSlot(dtEnd, ISO_MICROSECOND),
GetSlot(dtEnd, ISO_NANOSECOND),
calendarRec,
largestUnit,
options
);
const intermediateNs = AddZonedDateTime(
start,
timeZoneRec,
calendarRec,
years,
months,
weeks,
days,
TimeDuration.ZERO,
dtStart
);
// may disambiguate

CombineDateAndNormalizedTimeDuration(years, months, weeks, days, norm);
return { years, months, weeks, days, norm };
const norm = TimeDuration.fromEpochNsDiff(ns2, intermediateNs);
const dateSign = DurationSign(years, months, weeks, days, 0, 0, 0, 0, 0, 0);
const timeSign = norm.sign();
if (dateSign == 0 || timeSign == 0 || dateSign == timeSign) {
return { years, months, weeks, days, norm };
}
}
throw new Error('assert not reached: more than 1 day correction needed');
}

export function GetDifferenceSettings(op, options, group, disallowed, fallbackSmallest, smallestLargestDefaultUnit) {
Expand Down
19 changes: 13 additions & 6 deletions spec/zoneddatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -1397,12 +1397,19 @@ <h1>
1. Let _startDateTime_ be _precalculatedPlainDateTime_.
1. Let _endInstant_ be ! CreateTemporalInstant(_ns2_).
1. Let _endDateTime_ be ? GetPlainDateTimeFor(_timeZoneRec_, _endInstant_, _calendarRec_.[[Receiver]]).
1. Let _dateDifference_ be ? DifferenceISODateTime(_startDateTime_.[[ISOYear]], _startDateTime_.[[ISOMonth]], _startDateTime_.[[ISODay]], _startDateTime_.[[ISOHour]], _startDateTime_.[[ISOMinute]], _startDateTime_.[[ISOSecond]], _startDateTime_.[[ISOMillisecond]], _startDateTime_.[[ISOMicrosecond]], _startDateTime_.[[ISONanosecond]], _endDateTime_.[[ISOYear]], _endDateTime_.[[ISOMonth]], _endDateTime_.[[ISODay]], _endDateTime_.[[ISOHour]], _endDateTime_.[[ISOMinute]], _endDateTime_.[[ISOSecond]], _endDateTime_.[[ISOMillisecond]], _endDateTime_.[[ISOMicrosecond]], _endDateTime_.[[ISONanosecond]], _calendarRec_, _largestUnit_, _options_).
1. Let _intermediateNs_ be ? AddZonedDateTime(_ns1_, _timeZoneRec_, _calendarRec_, _dateDifference_.[[Years]], _dateDifference_.[[Months]], _dateDifference_.[[Weeks]], 0, ZeroTimeDuration(), _startDateTime_).
1. Let _norm_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_ns2_, _intermediateNs_).
1. Let _intermediate_ be ! CreateTemporalZonedDateTime(_intermediateNs_, _timeZoneRec_.[[Receiver]], _calendarRec_.[[Receiver]]).
1. Let _result_ be ? NormalizedTimeDurationToDays(_norm_, _intermediate_, _timeZoneRec_).
1. Return ! CreateNormalizedDurationRecord(_dateDifference_.[[Years]], _dateDifference_.[[Months]], _dateDifference_.[[Weeks]], _result_.[[Days]], _result_.[[Remainder]]).
1. If _ns2_ - _ns1_ &lt; 0, let _sign_ be -1; else let _sign_ be 1.
1. Let _dayCorrection_ be 0.
1. Repeat 2 times:
1. Let _correctedEndDateTime_ be BalanceISODate(_endDateTime_.[[ISOYear]], _endDateTime_.[[ISOMonth]], _endDateTime_.[[ISODay]] + _dayCorrection_ &times; -_sign_).
1. Let _dateDifference_ be ? DifferenceISODateTime(_startDateTime_.[[ISOYear]], _startDateTime_.[[ISOMonth]], _startDateTime_.[[ISODay]], _startDateTime_.[[ISOHour]], _startDateTime_.[[ISOMinute]], _startDateTime_.[[ISOSecond]], _startDateTime_.[[ISOMillisecond]], _startDateTime_.[[ISOMicrosecond]], _startDateTime_.[[ISONanosecond]], _correctedEndDateTime_.[[Year]], _correctedEndDateTime_.[[Month]], _correctedEndDateTime_.[[Day]], _endDateTime_.[[ISOHour]], _endDateTime_.[[ISOMinute]], _endDateTime_.[[ISOSecond]], _endDateTime_.[[ISOMillisecond]], _endDateTime_.[[ISOMicrosecond]], _endDateTime_.[[ISONanosecond]], _calendarRec_, _largestUnit_, _options_).
1. Let _intermediateNs_ be ? AddZonedDateTime(_ns1_, _timeZoneRec_, _calendarRec_, _dateDifference_.[[Years]], _dateDifference_.[[Months]], _dateDifference_.[[Weeks]], _dateDifference_.[[Days]], ZeroTimeDuration(), _startDateTime_).
1. Let _norm_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_ns2_, _intermediateNs_).
1. Let _dateSign_ be DurationSign(_dateDifference_.[[Years]], _dateDifference_.[[Months]], _dateDifference_.[[Weeks]], _dateDifference_.[[Days]], 0, 0, 0, 0, 0, 0).
1. Let _timeSign_ be NormalizedTimeDurationSign(_norm_).
1. If _dateSign_ = 0, or _timeSign_ = 0, or _dateSign_ = _timeSign_, then
1. Return ! CreateNormalizedDurationRecord(_dateDifference_.[[Years]], _dateDifference_.[[Months]], _dateDifference_.[[Weeks]], _dateDifference_.[[Days]], _norm_).
1. Set _dayCorrection_ to _dayCorrection_ + 1.
1. Assert: This step is never reached because _dayCorrection_ is at most 1.
</emu-alg>
</emu-clause>

Expand Down

0 comments on commit cd903c7

Please sign in to comment.