diff --git a/polyfill/lib/ecmascript.mjs b/polyfill/lib/ecmascript.mjs index 700768a731..525010a3b6 100644 --- a/polyfill/lib/ecmascript.mjs +++ b/polyfill/lib/ecmascript.mjs @@ -3900,54 +3900,106 @@ export function DifferenceZonedDateTime( norm: TimeDuration.ZERO }; } + const sign = nsDiff.lt(0) ? -1 : 1; - // Find the difference in dates only. + // Convert start/end instants to datetimes const TemporalInstant = GetIntrinsic('%Temporal.Instant%'); const start = new TemporalInstant(ns1); const end = new TemporalInstant(ns2); 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), + // Simulate moving ns1 as many years/months/weeks/days as possible without + // surpassing ns2. This value is stored in intermediateDateTime/intermediateInstant/intermediateNs. + // 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. + // The number of days we move backward is stored in dayCorrection. + // Credit to Adam Shaw for devising this algorithm. + let dayCorrection = 0; + let intermediateDateTime; + let norm; + + // The max number of allowed day corrections depends on the direction of travel. + // Both directions allow for 1 day correction due to an ISO wall-clock overshoot (see below). + // Only the forward direction allows for an additional 1 day correction caused by a push-forward + // 'compatible' DST transition causing the wall-clock to overshoot again. + // This max value is inclusive. + let maxDayCorrection = sign === 1 ? 2 : 1; + + // Detect ISO wall-clock overshoot. + // If the diff of the ISO wall-clock times is opposite to the overall diff's sign, + // we are guaranteed to need at least one day correction. + let timeDuration = DifferenceTime( 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 + GetSlot(dtEnd, ISO_NANOSECOND) ); - // may disambiguate + if (timeDuration.sign() === -sign) { + dayCorrection++; + } + + for (; dayCorrection <= maxDayCorrection; dayCorrection++) { + const intermediateDate = 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 + intermediateDateTime = CreateTemporalDateTime( + intermediateDate.year, + intermediateDate.month, + intermediateDate.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 + ); + + // Convert intermediate datetime to epoch-nanoseconds (may disambiguate) + const intermediateInstant = GetInstantFor(timeZoneRec, intermediateDateTime, 'compatible'); + const intermediateNs = GetSlot(intermediateInstant, EPOCHNANOSECONDS); + + // Compute the nanosecond diff between the intermediate instant and the final destination + norm = TimeDuration.fromEpochNsDiff(ns2, intermediateNs); + + // Did intermediateNs NOT surpass ns2? + // If so, exit the loop with success (without incrementing dayCorrection past maxDayCorrection) + if (norm.sign() !== -sign) { + break; + } + } + + if (dayCorrection > maxDayCorrection) { + throw new RangeError( + `inconsistent return from calendar or time zone method: more than ${maxDayCorrection} day correction needed` + ); + } + + // 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 }; diff --git a/polyfill/test262 b/polyfill/test262 index 0fd1675f7e..1e3d8cbb37 160000 --- a/polyfill/test262 +++ b/polyfill/test262 @@ -1 +1 @@ -Subproject commit 0fd1675f7ed02723772d30e718f04e6af455d3c9 +Subproject commit 1e3d8cbb37476cd957a6a5c7295004901f30b2c4 diff --git a/spec/zoneddatetime.html b/spec/zoneddatetime.html index 4193144aaa..b0767b70d0 100644 --- a/spec/zoneddatetime.html +++ b/spec/zoneddatetime.html @@ -1401,12 +1401,32 @@

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_ < 0, let _sign_ be -1; else let _sign_ be 1. + 1. If _sign_ = 1, let _maxDayCorrection_ be 2; else let _maxDayCorrection_ be 1. + 1. Let _dayCorrection_ be 0. + 1. Let _timeDuration_ be DifferenceTime(_startDateTime_.[[ISOHour]], _startDateTime_.[[ISOMinute]], _startDateTime_.[[ISOSecond]], _startDateTime_.[[ISOMillisecond]], _startDateTime_.[[ISOMicrosecond]], _startDateTime_.[[ISONanosecond]], _endDateTime_.[[ISOHour]], _endDateTime_.[[ISOMinute]], _endDateTime_.[[ISOSecond]], _endDateTime_.[[ISOMillisecond]], _endDateTime_.[[ISOMicrosecond]], _endDateTime_.[[ISONanosecond]]). + 1. If NormalizedTimeDurationSign(_timeDuration_) = -_sign_, set _dayCorrection_ to _dayCorrection_ + 1. + 1. Let _success_ be *false*. + 1. Repeat, while _dayCorrection_ ≤ _maxDayCorrection_ and _success_ is *false*, + 1. Let _intermediateDate_ be BalanceISODate(_endDateTime_.[[ISOYear]], _endDateTime_.[[ISOMonth]], _endDateTime_.[[ISODay]] - _dayCorrection_ × _sign_). + 1. Let _intermediateDateTime_ be ! CreateTemporalDateTime(_intermediateDate_.[[Year]], _intermediateDate_.[[Month]], _intermediateDate_.[[Day]], _startDateTime_.[[ISOHour]], _startDateTime_.[[ISOMinute]], _startDateTime_.[[ISOSecond]], _startDateTime_.[[ISOMillisecond]], _startDateTime_.[[ISOMicrosecond]], _startDateTime_.[[ISONanosecond]], _calendarRec_.[[Receiver]]). + 1. Let _intermediateInstant_ be ? GetInstantFor(_timeZoneRec_, _intermediateDateTime_, *"compatible"*). + 1. Let _intermediateNs_ be _intermediateInstant_.[[Nanoseconds]]. + 1. Let _norm_ be NormalizedTimeDurationFromEpochNanosecondsDifference(_ns2_, _intermediateNs_). + 1. Let _timeSign_ be NormalizedTimeDurationSign(_norm_). + 1. If _sign_ ≠ -_timeSign_, then + 1. Set _success_ to *true*. + 1. Set _dayCorrection_ to _dayCorrection_ + 1. + 1. If _success_ is *true*, then + 1. Let _date1_ be ! CreateTemporalDate(_startDateTime_.[[ISOYear]], _startDateTime_.[[ISOMonth]], _startDateTime_.[[ISODay]], _calendarRec_.[[Receiver]]). + 1. Let _date2_ be ! CreateTemporalDate(_intermediateDateTime_.[[Year]], _intermediateDateTime_.[[Month]], _intermediateDateTime_.[[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. NOTE: This step is only reached when custom calendar or time zone methods return inconsistent values. + 1. Throw a *RangeError* exception.