Skip to content
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

Normative: Fix intermediate value in ZonedDateTime difference #2760

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 67 additions & 41 deletions polyfill/lib/ecmascript.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3903,49 +3903,75 @@ 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
// Simulate moving ns1 as many years/months/weeks/days as possible without
// surpassing ns2. This value is stored in intermediateDateTime/intermediate.
// We do not literally move years/months/weeks/days with calendar arithmetic,
// but rather assume intermediateDateTime will have the same time-parts as
// dtStart and the date-parts from dtEnd, and move backward from there.
//
// This loop will run 3 times max:
// 1. initial run
// 2. backoff if the time-parts of intermediateDateTime have conflicting sign
// with overall diff direction, just like how DifferenceISODateTime works
// 3. backoff if intermediateDateTime fell into a DST gap and was pushed in a
// direction that would make the diff of the time-parts conflict with the
// sign of the overall direction. (Only possible when sign is +1, because
// a DST gap uses 'compatible' disambiguation resolution and can only move
// the intermediate forward)
//
// Credit to Adam Shaw for devising this algorithm.
const sign = nsDiff.lt(0) ? -1 : 1;
const maxTries = sign === 1 ? 3 : 2;
for (let dayCorrection = 0; dayCorrection < maxTries; dayCorrection++) {
const correctedEndDate = 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));
// Incorporate time parts from dtStart
const intermediateDateTime = CreateTemporalDateTime(
correctedEndDate.year,
correctedEndDate.month,
correctedEndDate.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),
calendarRec.receiver
);
const intermediate = GetInstantFor(timeZoneRec, intermediateDateTime, 'compatible');
// may disambiguate
const intermediateNs = GetSlot(intermediate, EPOCHNANOSECONDS);

// Did intermediateNs surpass ns2?
const norm = TimeDuration.fromEpochNsDiff(ns2, intermediateNs);
const timeSign = norm.sign();
if (sign === 0 || timeSign == 0 || sign === timeSign) {
// sign of timeDuration now compatible with the overall sign

// Similar to what happens in DifferenceISODateTime with date parts only:
const date1 = TemporalDateTimeToDate(dtStart);
const date2 = TemporalDateTimeToDate(intermediateDateTime);
const dateLargestUnit = LargerOfTwoTemporalUnits('day', largestUnit);
const untilOptions = SnapshotOwnProperties(options, null);
untilOptions.largestUnit = dateLargestUnit;
const dateDifference = DifferenceDate(calendarRec, date1, date2, untilOptions);
const years = GetSlot(dateDifference, YEARS);
const months = GetSlot(dateDifference, MONTHS);
const weeks = GetSlot(dateDifference, WEEKS);
const days = GetSlot(dateDifference, DAYS);

CombineDateAndNormalizedTimeDuration(years, months, weeks, days, norm);
return { years, months, weeks, days, norm };
CombineDateAndNormalizedTimeDuration(years, months, weeks, days, norm);
return { years, months, weeks, days, norm };
}
// Else, keep backing off...
}
throw new RangeError(
`inconsistent return from calendar or time zone method: more than ${maxTries - 1} days correction needed`
);
}

export function GetDifferenceSettings(op, options, group, disallowed, fallbackSmallest, smallestLargestDefaultUnit) {
Expand Down
27 changes: 21 additions & 6 deletions spec/zoneddatetime.html
Original file line number Diff line number Diff line change
Expand Up @@ -1397,12 +1397,27 @@ <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. If _sign_ = 1, let _maxTries_ be 3; else let _maxTries_ be 2.
1. Let _dayCorrection_ be 0.
1. Repeat _maxTries_ times:
1. Let _correctedEndDate_ be BalanceISODate(_endDateTime_.[[ISOYear]], _endDateTime_.[[ISOMonth]], _endDateTime_.[[ISODay]] - _dayCorrection_ &times; _sign_).
1. Let _intermediateDateTime_ be ! CreateTemporalDateTime(_correctedEndDate_.[[Year]], _correctedEndDate_.[[Month]], _correctedEndDate_.[[Day]], _startDateTime_.[[ISOHour]], _startDateTime_.[[ISOMinute]], _startDateTime_.[[ISOSecond]], _startDateTime_.[[ISOMillisecond]], _startDateTime_.[[ISOMicrosecond]], _startDateTime_.[[ISONanosecond]], _calendarRec_.[[Receiver]]).
1. Let _intermediate_ be ? GetInstantFor(_timeZoneRec_, _intermediateDateTime_, *"compatible"*).
1. Let _intermediateNs_ be _intermediate_.[[Nanoseconds]].
1. Let _norm_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_ns2_, _intermediateNs_).
1. Let _timeSign_ be NormalizedTimeDurationSign(_norm_).
1. If _sign_ = 0, or _timeSign_ = 0, or _sign_ = _timeSign_, then
1. Let _date1_ be ! CreateTemporalDate(_startDateTime_.[[ISOYear]], _startDateTime_.[[ISOMonth]], _startDateTime_.[[ISODay]], _calendarRec_.[[Receiver]]).
1. Let _date2_ be ! CreateTemporalDate(_correctedEndDate_.[[Year]], _correctedEndDate_.[[Month]], _correctedEndDate_.[[Day]], _calendarRec_.[[Receiver]]).
1. Let _dateLargestUnit_ be LargerOfTwoTemporalUnits(_largestUnit_, *"day"*).
1. Let _untilOptions_ be ? SnapshotOwnProperties(_options_, *null*).
1. Perform ! CreateDataPropertyOrThrow(_untilOptions_, *"largestUnit"*, _dateLargestUnit_).
1. Let _dateDifference_ be ? DifferenceDate(_calendarRec_, _date1_, _date2_, _untilOptions_).
1. Return ? CreateNormalizedDurationRecord(_dateDifference_.[[Years]], _dateDifference_.[[Months]], _dateDifference_.[[Weeks]], _dateDifference_.[[Days]], _norm_).
1. Set _dayCorrection_ to _dayCorrection_ + 1.
1. NOTE: This step is only reached when custom calendar or time zone methods return inconsistent values.
1. Throw a *RangeError* exception.
</emu-alg>
</emu-clause>

Expand Down
Loading