commit 2cb0264d426c57d78dca652778c06af684d98c03
parent 18c91fcc4f00dd5ed460ece22fa9e212c7d5868b
Author: André Bargull <andre.bargull@gmail.com>
Date: Wed, 5 Nov 2025 15:57:58 +0000
Bug 1997196: Fix ZonedDateTime differences where earlier wall-clock time is later exact time, or vice versa. r=dminor
Applies the changes from <https://github.com/tc39/proposal-temporal/pull/3147>.
Differential Revision: https://phabricator.services.mozilla.com/D270616
Diffstat:
6 files changed, 132 insertions(+), 92 deletions(-)
diff --git a/js/src/builtin/temporal/Duration.cpp b/js/src/builtin/temporal/Duration.cpp
@@ -2031,18 +2031,18 @@ struct DurationNudge {
};
/**
- * NudgeToCalendarUnit ( sign, duration, destEpochNs, isoDateTime, timeZone,
- * calendar, increment, unit, roundingMode )
+ * NudgeToCalendarUnit ( sign, duration, originEpochNs, destEpochNs,
+ * isoDateTime, timeZone, calendar, increment, unit, roundingMode )
*/
-static bool NudgeToCalendarUnit(JSContext* cx, const InternalDuration& duration,
- const EpochNanoseconds& destEpochNs,
- const ISODateTime& isoDateTime,
- Handle<TimeZoneValue> timeZone,
- Handle<CalendarValue> calendar,
- Increment increment, TemporalUnit unit,
- TemporalRoundingMode roundingMode,
- DurationNudge* result) {
+static bool NudgeToCalendarUnit(
+ JSContext* cx, const InternalDuration& duration,
+ const EpochNanoseconds& originEpochNs, const EpochNanoseconds& destEpochNs,
+ const ISODateTime& isoDateTime, Handle<TimeZoneValue> timeZone,
+ Handle<CalendarValue> calendar, Increment increment, TemporalUnit unit,
+ TemporalRoundingMode roundingMode, DurationNudge* result) {
MOZ_ASSERT(IsValidDuration(duration));
+ MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(originEpochNs));
+ MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(originEpochNs));
MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(destEpochNs));
MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(destEpochNs));
MOZ_ASSERT(ISODateTimeWithinLimits(isoDateTime));
@@ -2160,46 +2160,56 @@ static bool NudgeToCalendarUnit(JSContext* cx, const InternalDuration& duration,
// Step 6.
MOZ_ASSERT_IF(sign < 0, r1 <= 0 && r1 > r2);
- // Step 7.
- ISODate start;
- if (!CalendarDateAdd(cx, calendar, isoDateTime.date, startDuration,
- TemporalOverflow::Constrain, &start)) {
- return false;
+ // Steps 7-8.
+ EpochNanoseconds startEpochNs;
+ if (r1 == 0) {
+ // Step 7.a.
+ startEpochNs = originEpochNs;
+ } else {
+ // Step 8.a.
+ ISODate start;
+ if (!CalendarDateAdd(cx, calendar, isoDateTime.date, startDuration,
+ TemporalOverflow::Constrain, &start)) {
+ return false;
+ }
+
+ // Step 8.b.
+ auto startDateTime = ISODateTime{start, isoDateTime.time};
+ MOZ_ASSERT(ISODateTimeWithinLimits(startDateTime));
+
+ // Steps 8.c-d.
+ EpochNanoseconds startEpochNs;
+ if (!timeZone) {
+ // Step 8.c.
+ startEpochNs = GetUTCEpochNanoseconds(startDateTime);
+ } else {
+ // Step 8.d.
+ if (!GetEpochNanosecondsFor(cx, timeZone, startDateTime,
+ TemporalDisambiguation::Compatible,
+ &startEpochNs)) {
+ return false;
+ }
+ }
}
- // Step 8.
+ // Step 9.
ISODate end;
if (!CalendarDateAdd(cx, calendar, isoDateTime.date, endDuration,
TemporalOverflow::Constrain, &end)) {
return false;
}
- // Step 9.
- auto startDateTime = ISODateTime{start, isoDateTime.time};
- MOZ_ASSERT(ISODateTimeWithinLimits(startDateTime));
-
// Step 10.
auto endDateTime = ISODateTime{end, isoDateTime.time};
MOZ_ASSERT(ISODateTimeWithinLimits(endDateTime));
// Steps 11-12.
- EpochNanoseconds startEpochNs;
EpochNanoseconds endEpochNs;
if (!timeZone) {
// Step 11.a.
- startEpochNs = GetUTCEpochNanoseconds(startDateTime);
-
- // Step 11.b.
endEpochNs = GetUTCEpochNanoseconds(endDateTime);
} else {
// Step 12.a.
- if (!GetEpochNanosecondsFor(cx, timeZone, startDateTime,
- TemporalDisambiguation::Compatible,
- &startEpochNs)) {
- return false;
- }
-
- // Step 12.b.
if (!GetEpochNanosecondsFor(cx, timeZone, endDateTime,
TemporalDisambiguation::Compatible,
&endEpochNs)) {
@@ -2667,16 +2677,19 @@ static bool BubbleRelativeDuration(
}
/**
- * RoundRelativeDuration ( duration, destEpochNs, isoDateTime, timeZone,
- * calendar, largestUnit, increment, smallestUnit, roundingMode )
+ * RoundRelativeDuration ( duration, originEpochNs, destEpochNs, isoDateTime,
+ * timeZone, calendar, largestUnit, increment, smallestUnit, roundingMode )
*/
bool js::temporal::RoundRelativeDuration(
JSContext* cx, const InternalDuration& duration,
- const EpochNanoseconds& destEpochNs, const ISODateTime& isoDateTime,
- Handle<TimeZoneValue> timeZone, Handle<CalendarValue> calendar,
- TemporalUnit largestUnit, Increment increment, TemporalUnit smallestUnit,
+ const EpochNanoseconds& originEpochNs, const EpochNanoseconds& destEpochNs,
+ const ISODateTime& isoDateTime, Handle<TimeZoneValue> timeZone,
+ Handle<CalendarValue> calendar, TemporalUnit largestUnit,
+ Increment increment, TemporalUnit smallestUnit,
TemporalRoundingMode roundingMode, InternalDuration* result) {
MOZ_ASSERT(IsValidDuration(duration));
+ MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(originEpochNs));
+ MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(originEpochNs));
MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(destEpochNs));
MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(destEpochNs));
MOZ_ASSERT(ISODateTimeWithinLimits(isoDateTime));
@@ -2692,9 +2705,9 @@ bool js::temporal::RoundRelativeDuration(
DurationNudge nudge;
if (irregularLengthUnit) {
// Step 5.a.
- if (!NudgeToCalendarUnit(cx, duration, destEpochNs, isoDateTime, timeZone,
- calendar, increment, smallestUnit, roundingMode,
- &nudge)) {
+ if (!NudgeToCalendarUnit(cx, duration, originEpochNs, destEpochNs,
+ isoDateTime, timeZone, calendar, increment,
+ smallestUnit, roundingMode, &nudge)) {
return false;
}
} else if (timeZone) {
@@ -2731,17 +2744,17 @@ bool js::temporal::RoundRelativeDuration(
}
/**
- * TotalRelativeDuration ( duration, destEpochNs, isoDateTime, timeZone,
- * calendar, unit )
+ * TotalRelativeDuration ( duration, originEpochNs, destEpochNs, isoDateTime,
+ * timeZone, calendar, unit )
*/
-bool js::temporal::TotalRelativeDuration(JSContext* cx,
- const InternalDuration& duration,
- const EpochNanoseconds& destEpochNs,
- const ISODateTime& isoDateTime,
- JS::Handle<TimeZoneValue> timeZone,
- JS::Handle<CalendarValue> calendar,
- TemporalUnit unit, double* result) {
+bool js::temporal::TotalRelativeDuration(
+ JSContext* cx, const InternalDuration& duration,
+ const EpochNanoseconds& originEpochNs, const EpochNanoseconds& destEpochNs,
+ const ISODateTime& isoDateTime, JS::Handle<TimeZoneValue> timeZone,
+ JS::Handle<CalendarValue> calendar, TemporalUnit unit, double* result) {
MOZ_ASSERT(IsValidDuration(duration));
+ MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(originEpochNs));
+ MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(originEpochNs));
MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(destEpochNs));
MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(destEpochNs));
MOZ_ASSERT(ISODateTimeWithinLimits(isoDateTime));
@@ -2750,8 +2763,8 @@ bool js::temporal::TotalRelativeDuration(JSContext* cx,
// Steps 1.a-b.
DurationNudge nudge;
- if (!NudgeToCalendarUnit(cx, duration, destEpochNs, isoDateTime, timeZone,
- calendar, Increment{1}, unit,
+ if (!NudgeToCalendarUnit(cx, duration, originEpochNs, destEpochNs,
+ isoDateTime, timeZone, calendar, Increment{1}, unit,
TemporalRoundingMode::Trunc, &nudge)) {
return false;
}
diff --git a/js/src/builtin/temporal/Duration.h b/js/src/builtin/temporal/Duration.h
@@ -239,21 +239,23 @@ TimeDuration RoundTimeDuration(const TimeDuration& duration,
TemporalRoundingMode roundingMode);
/**
- * RoundRelativeDuration ( duration, destEpochNs, isoDateTime, timeZone,
- * calendar, largestUnit, increment, smallestUnit, roundingMode )
+ * RoundRelativeDuration ( duration, originEpochNs, destEpochNs, isoDateTime,
+ * timeZone, calendar, largestUnit, increment, smallestUnit, roundingMode )
*/
bool RoundRelativeDuration(
JSContext* cx, const InternalDuration& duration,
- const EpochNanoseconds& destEpochNs, const ISODateTime& isoDateTime,
- JS::Handle<TimeZoneValue> timeZone, JS::Handle<CalendarValue> calendar,
- TemporalUnit largestUnit, Increment increment, TemporalUnit smallestUnit,
+ const EpochNanoseconds& originEpochNs, const EpochNanoseconds& destEpochNs,
+ const ISODateTime& isoDateTime, JS::Handle<TimeZoneValue> timeZone,
+ JS::Handle<CalendarValue> calendar, TemporalUnit largestUnit,
+ Increment increment, TemporalUnit smallestUnit,
TemporalRoundingMode roundingMode, InternalDuration* result);
/**
- * TotalRelativeDuration ( duration, destEpochNs, isoDateTime, timeZone,
- * calendar, unit )
+ * TotalRelativeDuration ( duration, originEpochNs, destEpochNs, isoDateTime,
+ * timeZone, calendar, unit )
*/
bool TotalRelativeDuration(JSContext* cx, const InternalDuration& duration,
+ const EpochNanoseconds& originEpochNs,
const EpochNanoseconds& destEpochNs,
const ISODateTime& isoDateTime,
JS::Handle<TimeZoneValue> timeZone,
diff --git a/js/src/builtin/temporal/PlainDate.cpp b/js/src/builtin/temporal/PlainDate.cpp
@@ -644,16 +644,19 @@ static bool DifferenceTemporalPlainDate(JSContext* cx,
auto isoDateTime = ISODateTime{temporalDate.date(), {}};
// Step 8.b.
- auto isoDateTimeOther = ISODateTime{other.date(), {}};
+ auto originEpochNs = GetUTCEpochNanoseconds(isoDateTime);
// Step 8.c.
- auto destEpochNs = GetUTCEpochNanoseconds(isoDateTimeOther);
+ auto isoDateTimeOther = ISODateTime{other.date(), {}};
// Step 8.d.
+ auto destEpochNs = GetUTCEpochNanoseconds(isoDateTimeOther);
+
+ // Step 8.e.
Rooted<TimeZoneValue> timeZone(cx, TimeZoneValue{});
- if (!RoundRelativeDuration(cx, duration, destEpochNs, isoDateTime, timeZone,
- temporalDate.calendar(), settings.largestUnit,
- settings.roundingIncrement,
+ if (!RoundRelativeDuration(cx, duration, originEpochNs, destEpochNs,
+ isoDateTime, timeZone, temporalDate.calendar(),
+ settings.largestUnit, settings.roundingIncrement,
settings.smallestUnit, settings.roundingMode,
&duration)) {
return false;
diff --git a/js/src/builtin/temporal/PlainDateTime.cpp b/js/src/builtin/temporal/PlainDateTime.cpp
@@ -602,12 +602,15 @@ bool js::temporal::DifferencePlainDateTimeWithRounding(
}
// Step 5.
- auto destEpochNs = GetUTCEpochNanoseconds(isoDateTime2);
+ auto originEpochNs = GetUTCEpochNanoseconds(isoDateTime1);
// Step 6.
+ auto destEpochNs = GetUTCEpochNanoseconds(isoDateTime2);
+
+ // Step 7.
Rooted<TimeZoneValue> timeZone(cx, TimeZoneValue{});
return RoundRelativeDuration(
- cx, diff, destEpochNs, isoDateTime1, timeZone, calendar,
+ cx, diff, originEpochNs, destEpochNs, isoDateTime1, timeZone, calendar,
settings.largestUnit, settings.roundingIncrement, settings.smallestUnit,
settings.roundingMode, result);
}
@@ -661,12 +664,15 @@ bool js::temporal::DifferencePlainDateTimeWithTotal(
}
// Step 5.
- auto destEpochNs = GetUTCEpochNanoseconds(isoDateTime2);
+ auto originEpochNs = GetUTCEpochNanoseconds(isoDateTime1);
// Step 6.
+ auto destEpochNs = GetUTCEpochNanoseconds(isoDateTime2);
+
+ // Step 7.
Rooted<TimeZoneValue> timeZone(cx, TimeZoneValue{});
- return TotalRelativeDuration(cx, diff, destEpochNs, isoDateTime1, timeZone,
- calendar, unit, result);
+ return TotalRelativeDuration(cx, diff, originEpochNs, destEpochNs,
+ isoDateTime1, timeZone, calendar, unit, result);
}
/**
diff --git a/js/src/builtin/temporal/PlainYearMonth.cpp b/js/src/builtin/temporal/PlainYearMonth.cpp
@@ -442,16 +442,22 @@ static bool DifferenceTemporalPlainYearMonth(JSContext* cx,
if (settings.smallestUnit != TemporalUnit::Month ||
settings.roundingIncrement != Increment{1}) {
// Step 16.a.
- auto destEpochNs = GetUTCEpochNanoseconds(ISODateTime{otherDate, {}});
+ auto isoDateTime = ISODateTime{thisDate, {}};
- // Steps 16.b-c.
- auto dateTime = ISODateTime{thisDate, {}};
+ // Step 16.b.
+ auto originEpochNs = GetUTCEpochNanoseconds(isoDateTime);
+
+ // Step 16.c.
+ auto isoDateTimeOther = ISODateTime{otherDate, {}};
// Step 16.d.
+ auto destEpochNs = GetUTCEpochNanoseconds(isoDateTimeOther);
+
+ // Step 16.e.
Rooted<TimeZoneValue> timeZone(cx, TimeZoneValue{});
if (!RoundRelativeDuration(
- cx, duration, destEpochNs, dateTime, timeZone, calendar,
- settings.largestUnit, settings.roundingIncrement,
+ cx, duration, originEpochNs, destEpochNs, isoDateTime, timeZone,
+ calendar, settings.largestUnit, settings.roundingIncrement,
settings.smallestUnit, settings.roundingMode, &duration)) {
return false;
}
diff --git a/js/src/builtin/temporal/ZonedDateTime.cpp b/js/src/builtin/temporal/ZonedDateTime.cpp
@@ -688,29 +688,39 @@ static bool DifferenceZonedDateTime(JSContext* cx, const EpochNanoseconds& ns1,
}
// Step 4.
- int32_t sign = (ns2 - ns1 < EpochDuration{}) ? -1 : 1;
+ if (CompareISODate(startDateTime.date, endDateTime.date) == 0) {
+ // Step 4.a.
+ auto timeDuration = TimeDurationFromEpochNanosecondsDifference(ns2, ns1);
+
+ // Step 4.b.
+ *result = {{}, timeDuration};
+ return true;
+ }
// Step 5.
- int32_t maxDayCorrection = 1 + (sign > 0);
+ int32_t sign = (ns2 - ns1 < EpochDuration{}) ? -1 : 1;
// Step 6.
- int32_t dayCorrection = 0;
+ int32_t maxDayCorrection = 1 + (sign > 0);
// Step 7.
- auto timeDuration = DifferenceTime(startDateTime.time, endDateTime.time);
+ int32_t dayCorrection = 0;
// Step 8.
+ auto timeDuration = DifferenceTime(startDateTime.time, endDateTime.time);
+
+ // Step 9.
if (TimeDurationSign(timeDuration) == -sign) {
dayCorrection += 1;
}
- // Steps 9-10.
+ // Steps 10-11.
while (dayCorrection <= maxDayCorrection) {
- // Step 10.a.
+ // Step 11.a.
auto intermediateDate =
BalanceISODate(endDateTime.date, -dayCorrection * sign);
- // Step 10.b.
+ // Step 11.b.
auto intermediateDateTime =
ISODateTime{intermediateDate, startDateTime.time};
if (!ISODateTimeWithinLimits(intermediateDateTime)) {
@@ -719,7 +729,7 @@ static bool DifferenceZonedDateTime(JSContext* cx, const EpochNanoseconds& ns1,
return false;
}
- // Step 10.c.
+ // Step 11.c.
EpochNanoseconds intermediateNs;
if (!GetEpochNanosecondsFor(cx, timeZone, intermediateDateTime,
TemporalDisambiguation::Compatible,
@@ -727,26 +737,26 @@ static bool DifferenceZonedDateTime(JSContext* cx, const EpochNanoseconds& ns1,
return false;
}
- // Step 10.d.
+ // Step 11.d.
auto timeDuration =
TimeDurationFromEpochNanosecondsDifference(ns2, intermediateNs);
- // Step 10.e.
+ // Step 11.e.
int32_t timeSign = TimeDurationSign(timeDuration);
- // Step 10.f.
+ // Step 11.f.
if (sign != -timeSign) {
- // Step 12.
+ // Step 13.
auto dateLargestUnit = std::min(largestUnit, TemporalUnit::Day);
- // Step 13.
+ // Step 14.
DateDuration dateDifference;
if (!CalendarDateUntil(cx, calendar, startDateTime.date, intermediateDate,
dateLargestUnit, &dateDifference)) {
return false;
}
- // Step 14.
+ // Step 15.
MOZ_ASSERT(DateDurationSign(dateDifference) *
TimeDurationSign(timeDuration) >=
0);
@@ -754,11 +764,11 @@ static bool DifferenceZonedDateTime(JSContext* cx, const EpochNanoseconds& ns1,
return true;
}
- // Step 10.g.
+ // Step 11.g.
dayCorrection += 1;
}
- // Step 11.
+ // Step 12.
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_ZONED_DATE_TIME_INCONSISTENT_INSTANT);
@@ -813,9 +823,9 @@ bool js::temporal::DifferenceZonedDateTimeWithRounding(
// Step 5.
return RoundRelativeDuration(
- cx, difference, ns2, dateTime, timeZone, calendar, settings.largestUnit,
- settings.roundingIncrement, settings.smallestUnit, settings.roundingMode,
- result);
+ cx, difference, ns1, ns2, dateTime, timeZone, calendar,
+ settings.largestUnit, settings.roundingIncrement, settings.smallestUnit,
+ settings.roundingMode, result);
}
/**
@@ -855,7 +865,7 @@ bool js::temporal::DifferenceZonedDateTimeWithTotal(
}
// Step 5.
- return TotalRelativeDuration(cx, difference, ns2, dateTime, timeZone,
+ return TotalRelativeDuration(cx, difference, ns1, ns2, dateTime, timeZone,
calendar, unit, result);
}