diff options
Diffstat (limited to 'js/src/builtin/temporal/Duration.cpp')
-rw-r--r-- | js/src/builtin/temporal/Duration.cpp | 5034 |
1 files changed, 2001 insertions, 3033 deletions
diff --git a/js/src/builtin/temporal/Duration.cpp b/js/src/builtin/temporal/Duration.cpp index 7e922aa68b..8f336a9a14 100644 --- a/js/src/builtin/temporal/Duration.cpp +++ b/js/src/builtin/temporal/Duration.cpp @@ -7,6 +7,7 @@ #include "builtin/temporal/Duration.h" #include "mozilla/Assertions.h" +#include "mozilla/Casting.h" #include "mozilla/CheckedInt.h" #include "mozilla/EnumSet.h" #include "mozilla/FloatingPoint.h" @@ -26,6 +27,8 @@ #include "builtin/temporal/Calendar.h" #include "builtin/temporal/Instant.h" +#include "builtin/temporal/Int128.h" +#include "builtin/temporal/Int96.h" #include "builtin/temporal/PlainDate.h" #include "builtin/temporal/PlainDateTime.h" #include "builtin/temporal/Temporal.h" @@ -54,7 +57,6 @@ #include "js/RootingAPI.h" #include "js/Value.h" #include "util/StringBuffer.h" -#include "vm/BigIntType.h" #include "vm/BytecodeUtil.h" #include "vm/GlobalObject.h" #include "vm/JSAtomState.h" @@ -81,8 +83,8 @@ static bool IsIntegerOrInfinity(double d) { } static bool IsIntegerOrInfinityDuration(const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Integers exceeding the Number range are represented as infinity. @@ -94,8 +96,8 @@ static bool IsIntegerOrInfinityDuration(const Duration& duration) { } static bool IsIntegerDuration(const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; return IsInteger(years) && IsInteger(months) && IsInteger(weeks) && IsInteger(days) && IsInteger(hours) && IsInteger(minutes) && @@ -104,6 +106,12 @@ static bool IsIntegerDuration(const Duration& duration) { } #endif +static constexpr bool IsSafeInteger(int64_t x) { + constexpr int64_t MaxSafeInteger = int64_t(1) << 53; + constexpr int64_t MinSafeInteger = -MaxSafeInteger; + return MinSafeInteger < x && x < MaxSafeInteger; +} + /** * DurationSign ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds ) @@ -111,8 +119,8 @@ static bool IsIntegerDuration(const Duration& duration) { int32_t js::temporal::DurationSign(const Duration& duration) { MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Step 1. for (auto v : {years, months, weeks, days, hours, minutes, seconds, @@ -133,14 +141,418 @@ int32_t js::temporal::DurationSign(const Duration& duration) { } /** + * DurationSign ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +int32_t js::temporal::DurationSign(const DateDuration& duration) { + const auto& [years, months, weeks, days] = duration; + + // Step 1. + for (auto v : {years, months, weeks, days}) { + // Step 1.a. + if (v < 0) { + return -1; + } + + // Step 1.b. + if (v > 0) { + return 1; + } + } + + // Step 2. + return 0; +} + +/** + * Normalize a nanoseconds amount into a time duration. + */ +static NormalizedTimeDuration NormalizeNanoseconds(const Int96& nanoseconds) { + // Split into seconds and nanoseconds. + auto [seconds, nanos] = nanoseconds / ToNanoseconds(TemporalUnit::Second); + + return {seconds, nanos}; +} + +/** + * Normalize a nanoseconds amount into a time duration. Return Nothing if the + * value is too large. + */ +static mozilla::Maybe<NormalizedTimeDuration> NormalizeNanoseconds( + double nanoseconds) { + MOZ_ASSERT(IsInteger(nanoseconds)); + + if (auto int96 = Int96::fromInteger(nanoseconds)) { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = + Int96{uint64_t(1) << 53} * ToNanoseconds(TemporalUnit::Second); + + if (int96->abs() < limit) { + return mozilla::Some(NormalizeNanoseconds(*int96)); + } + } + return mozilla::Nothing(); +} + +/** + * Normalize a microseconds amount into a time duration. + */ +static NormalizedTimeDuration NormalizeMicroseconds(const Int96& microseconds) { + // Split into seconds and microseconds. + auto [seconds, micros] = microseconds / ToMicroseconds(TemporalUnit::Second); + + // Scale microseconds to nanoseconds. + int32_t nanos = micros * int32_t(ToNanoseconds(TemporalUnit::Microsecond)); + + return {seconds, nanos}; +} + +/** + * Normalize a microseconds amount into a time duration. Return Nothing if the + * value is too large. + */ +static mozilla::Maybe<NormalizedTimeDuration> NormalizeMicroseconds( + double microseconds) { + MOZ_ASSERT(IsInteger(microseconds)); + + if (auto int96 = Int96::fromInteger(microseconds)) { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = + Int96{uint64_t(1) << 53} * ToMicroseconds(TemporalUnit::Second); + + if (int96->abs() < limit) { + return mozilla::Some(NormalizeMicroseconds(*int96)); + } + } + return mozilla::Nothing(); +} + +/** + * Normalize a duration into a time duration. Return Nothing if any duration + * value is too large. + */ +static mozilla::Maybe<NormalizedTimeDuration> NormalizeSeconds( + const Duration& duration) { + do { + auto nanoseconds = NormalizeNanoseconds(duration.nanoseconds); + if (!nanoseconds) { + break; + } + MOZ_ASSERT(IsValidNormalizedTimeDuration(*nanoseconds)); + + auto microseconds = NormalizeMicroseconds(duration.microseconds); + if (!microseconds) { + break; + } + MOZ_ASSERT(IsValidNormalizedTimeDuration(*microseconds)); + + // Overflows for millis/seconds/minutes/hours/days always result in an + // invalid normalized time duration. + + int64_t milliseconds; + if (!mozilla::NumberEqualsInt64(duration.milliseconds, &milliseconds)) { + break; + } + + int64_t seconds; + if (!mozilla::NumberEqualsInt64(duration.seconds, &seconds)) { + break; + } + + int64_t minutes; + if (!mozilla::NumberEqualsInt64(duration.minutes, &minutes)) { + break; + } + + int64_t hours; + if (!mozilla::NumberEqualsInt64(duration.hours, &hours)) { + break; + } + + int64_t days; + if (!mozilla::NumberEqualsInt64(duration.days, &days)) { + break; + } + + // Compute the overall amount of milliseconds. + mozilla::CheckedInt64 millis = days; + millis *= 24; + millis += hours; + millis *= 60; + millis += minutes; + millis *= 60; + millis += seconds; + millis *= 1000; + millis += milliseconds; + if (!millis.isValid()) { + break; + } + + auto milli = NormalizedTimeDuration::fromMilliseconds(millis.value()); + if (!IsValidNormalizedTimeDuration(milli)) { + break; + } + + // Compute the overall time duration. + auto result = milli + *microseconds + *nanoseconds; + if (!IsValidNormalizedTimeDuration(result)) { + break; + } + + return mozilla::Some(result); + } while (false); + + return mozilla::Nothing(); +} + +/** + * Normalize a days amount into a time duration. Return Nothing if the value is + * too large. + */ +static mozilla::Maybe<NormalizedTimeDuration> NormalizeDays(int64_t days) { + do { + // Compute the overall amount of milliseconds. + auto millis = + mozilla::CheckedInt64(days) * ToMilliseconds(TemporalUnit::Day); + if (!millis.isValid()) { + break; + } + + auto result = NormalizedTimeDuration::fromMilliseconds(millis.value()); + if (!IsValidNormalizedTimeDuration(result)) { + break; + } + + return mozilla::Some(result); + } while (false); + + return mozilla::Nothing(); +} + +/** + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) + */ +static NormalizedTimeDuration NormalizeTimeDuration( + double hours, double minutes, double seconds, double milliseconds, + double microseconds, double nanoseconds) { + MOZ_ASSERT(IsInteger(hours)); + MOZ_ASSERT(IsInteger(minutes)); + MOZ_ASSERT(IsInteger(seconds)); + MOZ_ASSERT(IsInteger(milliseconds)); + MOZ_ASSERT(IsInteger(microseconds)); + MOZ_ASSERT(IsInteger(nanoseconds)); + + // Steps 1-3. + mozilla::CheckedInt64 millis = int64_t(hours); + millis *= 60; + millis += int64_t(minutes); + millis *= 60; + millis += int64_t(seconds); + millis *= 1000; + millis += int64_t(milliseconds); + MOZ_ASSERT(millis.isValid()); + + auto normalized = NormalizedTimeDuration::fromMilliseconds(millis.value()); + + // Step 4. + auto micros = Int96::fromInteger(microseconds); + MOZ_ASSERT(micros); + + normalized += NormalizeMicroseconds(*micros); + + // Step 5. + auto nanos = Int96::fromInteger(nanoseconds); + MOZ_ASSERT(nanos); + + normalized += NormalizeNanoseconds(*nanos); + + // Step 6. + MOZ_ASSERT(IsValidNormalizedTimeDuration(normalized)); + + // Step 7. + return normalized; +} + +/** + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) + */ +NormalizedTimeDuration js::temporal::NormalizeTimeDuration( + int32_t hours, int32_t minutes, int32_t seconds, int32_t milliseconds, + int32_t microseconds, int32_t nanoseconds) { + // Steps 1-3. + mozilla::CheckedInt64 millis = int64_t(hours); + millis *= 60; + millis += int64_t(minutes); + millis *= 60; + millis += int64_t(seconds); + millis *= 1000; + millis += int64_t(milliseconds); + MOZ_ASSERT(millis.isValid()); + + auto normalized = NormalizedTimeDuration::fromMilliseconds(millis.value()); + + // Step 4. + normalized += NormalizeMicroseconds(Int96{microseconds}); + + // Step 5. + normalized += NormalizeNanoseconds(Int96{nanoseconds}); + + // Step 6. + MOZ_ASSERT(IsValidNormalizedTimeDuration(normalized)); + + // Step 7. + return normalized; +} + +/** + * NormalizeTimeDuration ( hours, minutes, seconds, milliseconds, microseconds, + * nanoseconds ) + */ +NormalizedTimeDuration js::temporal::NormalizeTimeDuration( + const Duration& duration) { + MOZ_ASSERT(IsValidDuration(duration)); + + return ::NormalizeTimeDuration(duration.hours, duration.minutes, + duration.seconds, duration.milliseconds, + duration.microseconds, duration.nanoseconds); +} + +/** + * AddNormalizedTimeDuration ( one, two ) + */ +static bool AddNormalizedTimeDuration(JSContext* cx, + const NormalizedTimeDuration& one, + const NormalizedTimeDuration& two, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(one)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(two)); + + // Step 1. + auto sum = one + two; + + // Step 2. + if (!IsValidNormalizedTimeDuration(sum)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } + + // Step 3. + *result = sum; + return true; +} + +/** + * SubtractNormalizedTimeDuration ( one, two ) + */ +static bool SubtractNormalizedTimeDuration(JSContext* cx, + const NormalizedTimeDuration& one, + const NormalizedTimeDuration& two, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(one)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(two)); + + // Step 1. + auto sum = one - two; + + // Step 2. + if (!IsValidNormalizedTimeDuration(sum)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } + + // Step 3. + *result = sum; + return true; +} + +/** + * Add24HourDaysToNormalizedTimeDuration ( d, days ) + */ +bool js::temporal::Add24HourDaysToNormalizedTimeDuration( + JSContext* cx, const NormalizedTimeDuration& d, int64_t days, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(d)); + + // Step 1. + auto normalizedDays = NormalizeDays(days); + if (!normalizedDays) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } + + // Step 2. + auto sum = d + *normalizedDays; + if (!IsValidNormalizedTimeDuration(sum)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } + + // Step 3. + *result = sum; + return true; +} + +/** + * CombineDateAndNormalizedTimeDuration ( dateDurationRecord, norm ) + */ +bool js::temporal::CombineDateAndNormalizedTimeDuration( + JSContext* cx, const DateDuration& date, const NormalizedTimeDuration& time, + NormalizedDuration* result) { + MOZ_ASSERT(IsValidDuration(date)); + MOZ_ASSERT(IsValidNormalizedTimeDuration(time)); + + // Step 1. + int32_t dateSign = DurationSign(date); + + // Step 2. + int32_t timeSign = NormalizedTimeDurationSign(time); + + // Step 3 + if ((dateSign * timeSign) < 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_COMBINE_INVALID_SIGN); + return false; + } + + // Step 4. + *result = {date, time}; + return true; +} + +/** + * NormalizedTimeDurationFromEpochNanosecondsDifference ( one, two ) + */ +NormalizedTimeDuration +js::temporal::NormalizedTimeDurationFromEpochNanosecondsDifference( + const Instant& one, const Instant& two) { + MOZ_ASSERT(IsValidEpochInstant(one)); + MOZ_ASSERT(IsValidEpochInstant(two)); + + // Step 1. + auto result = one - two; + + // Step 2. + MOZ_ASSERT(IsValidInstantSpan(result)); + + // Step 3. + return result.to<NormalizedTimeDuration>(); +} + +/** * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds ) */ bool js::temporal::IsValidDuration(const Duration& duration) { MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Step 1. int32_t sign = DurationSign(duration); @@ -165,9 +577,61 @@ bool js::temporal::IsValidDuration(const Duration& duration) { } // Step 3. + if (std::abs(years) >= double(int64_t(1) << 32)) { + return false; + } + + // Step 4. + if (std::abs(months) >= double(int64_t(1) << 32)) { + return false; + } + + // Step 5. + if (std::abs(weeks) >= double(int64_t(1) << 32)) { + return false; + } + + // Steps 6-8. + if (!NormalizeSeconds(duration)) { + return false; + } + + // Step 9. return true; } +#ifdef DEBUG +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool js::temporal::IsValidDuration(const DateDuration& duration) { + return IsValidDuration(duration.toDuration()); +} + +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool js::temporal::IsValidDuration(const NormalizedDuration& duration) { + return IsValidDuration(duration.date) && + IsValidNormalizedTimeDuration(duration.time) && + (DurationSign(duration.date) * + NormalizedTimeDurationSign(duration.time) >= + 0); +} +#endif + +static bool ThrowInvalidDurationPart(JSContext* cx, double value, + const char* name, unsigned errorNumber) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, value); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, errorNumber, name, + numStr); + return false; +} + /** * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, * milliseconds, microseconds, nanoseconds ) @@ -176,36 +640,36 @@ bool js::temporal::ThrowIfInvalidDuration(JSContext* cx, const Duration& duration) { MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Step 1. int32_t sign = DurationSign(duration); - auto report = [&](double v, const char* name, unsigned errorNumber) { - ToCStringBuf cbuf; - const char* numStr = NumberToCString(&cbuf, v); - - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, errorNumber, name, - numStr); - }; - auto throwIfInvalid = [&](double v, const char* name) { // Step 2.a. if (!std::isfinite(v)) { - report(v, name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); - return false; + return ThrowInvalidDurationPart( + cx, v, name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); } // Steps 2.b-c. if ((v < 0 && sign > 0) || (v > 0 && sign < 0)) { - report(v, name, JSMSG_TEMPORAL_DURATION_INVALID_SIGN); - return false; + return ThrowInvalidDurationPart(cx, v, name, + JSMSG_TEMPORAL_DURATION_INVALID_SIGN); } return true; }; + auto throwIfTooLarge = [&](double v, const char* name) { + if (std::abs(v) >= double(int64_t(1) << 32)) { + return ThrowInvalidDurationPart( + cx, v, name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + return true; + }; + // Step 2. if (!throwIfInvalid(years, "years")) { return false; @@ -238,9 +702,104 @@ bool js::temporal::ThrowIfInvalidDuration(JSContext* cx, return false; } + // Step 3. + if (!throwIfTooLarge(years, "years")) { + return false; + } + + // Step 4. + if (!throwIfTooLarge(months, "months")) { + return false; + } + + // Step 5. + if (!throwIfTooLarge(weeks, "weeks")) { + return false; + } + + // Steps 6-8. + if (!NormalizeSeconds(duration)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } + MOZ_ASSERT(IsValidDuration(duration)); + // Step 9. + return true; +} + +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool js::temporal::ThrowIfInvalidDuration(JSContext* cx, + const DateDuration& duration) { + const auto& [years, months, weeks, days] = duration; + + // Step 1. + int32_t sign = DurationSign(duration); + + auto throwIfInvalid = [&](int64_t v, const char* name) { + // Step 2.a. (Not applicable) + + // Steps 2.b-c. + if ((v < 0 && sign > 0) || (v > 0 && sign < 0)) { + return ThrowInvalidDurationPart(cx, double(v), name, + JSMSG_TEMPORAL_DURATION_INVALID_SIGN); + } + + return true; + }; + + auto throwIfTooLarge = [&](int64_t v, const char* name) { + if (std::abs(v) >= (int64_t(1) << 32)) { + return ThrowInvalidDurationPart( + cx, double(v), name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + return true; + }; + + // Step 2. + if (!throwIfInvalid(years, "years")) { + return false; + } + if (!throwIfInvalid(months, "months")) { + return false; + } + if (!throwIfInvalid(weeks, "weeks")) { + return false; + } + if (!throwIfInvalid(days, "days")) { + return false; + } + // Step 3. + if (!throwIfTooLarge(years, "years")) { + return false; + } + + // Step 4. + if (!throwIfTooLarge(months, "months")) { + return false; + } + + // Step 5. + if (!throwIfTooLarge(weeks, "weeks")) { + return false; + } + + // Steps 6-8. + if (std::abs(days) > ((int64_t(1) << 53) / 86400)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); + return false; + } + + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 9. return true; } @@ -307,8 +866,8 @@ static TemporalUnit DefaultTemporalLargestUnit(const Duration& duration) { static DurationObject* CreateTemporalDuration(JSContext* cx, const CallArgs& args, const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; // Step 1. if (!ThrowIfInvalidDuration(cx, duration)) { @@ -355,8 +914,8 @@ static DurationObject* CreateTemporalDuration(JSContext* cx, */ DurationObject* js::temporal::CreateTemporalDuration(JSContext* cx, const Duration& duration) { - auto& [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = duration; MOZ_ASSERT(IsInteger(years)); MOZ_ASSERT(IsInteger(months)); @@ -621,11 +1180,12 @@ int32_t js::temporal::DaysUntil(const PlainDate& earlier, // Steps 1-2. int32_t epochDaysEarlier = MakeDay(earlier); - MOZ_ASSERT(std::abs(epochDaysEarlier) <= 100'000'000); + MOZ_ASSERT(MinEpochDay <= epochDaysEarlier && + epochDaysEarlier <= MaxEpochDay); // Steps 3-4. int32_t epochDaysLater = MakeDay(later); - MOZ_ASSERT(std::abs(epochDaysLater) <= 100'000'000); + MOZ_ASSERT(MinEpochDay <= epochDaysLater && epochDaysLater <= MaxEpochDay); // Step 5. return epochDaysLater - epochDaysEarlier; @@ -636,7 +1196,7 @@ int32_t js::temporal::DaysUntil(const PlainDate& earlier, */ static bool MoveRelativeDate( JSContext* cx, Handle<CalendarRecord> calendar, - Handle<Wrapped<PlainDateObject*>> relativeTo, const Duration& duration, + Handle<Wrapped<PlainDateObject*>> relativeTo, const DateDuration& duration, MutableHandle<Wrapped<PlainDateObject*>> relativeToResult, int32_t* daysResult) { auto* unwrappedRelativeTo = relativeTo.unwrap(cx); @@ -655,7 +1215,7 @@ static bool MoveRelativeDate( // Step 2. *daysResult = DaysUntil(relativeToDate, later); - MOZ_ASSERT(std::abs(*daysResult) <= 200'000'000); + MOZ_ASSERT(std::abs(*daysResult) <= MaxEpochDaysDuration); // Step 3. return true; @@ -668,7 +1228,7 @@ static bool MoveRelativeDate( static bool MoveRelativeZonedDateTime( JSContext* cx, Handle<ZonedDateTime> zonedDateTime, Handle<CalendarRecord> calendar, Handle<TimeZoneRecord> timeZone, - const Duration& duration, + const DateDuration& duration, mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, MutableHandle<ZonedDateTime> result) { // Step 1. @@ -683,13 +1243,13 @@ static bool MoveRelativeZonedDateTime( Instant intermediateNs; if (precalculatedPlainDateTime) { if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, - duration.date(), *precalculatedPlainDateTime, + duration, *precalculatedPlainDateTime, &intermediateNs)) { return false; } } else { if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, - duration.date(), &intermediateNs)) { + duration, &intermediateNs)) { return false; } } @@ -702,235 +1262,27 @@ static bool MoveRelativeZonedDateTime( } /** - * TotalDurationNanoseconds ( hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) - */ -static mozilla::Maybe<int64_t> TotalDurationNanoseconds( - const Duration& duration) { - // Our implementation supports |duration.days| to avoid computing |days * 24| - // in the caller, which may not be representable as a double value. - int64_t days; - if (!mozilla::NumberEqualsInt64(duration.days, &days)) { - return mozilla::Nothing(); - } - int64_t hours; - if (!mozilla::NumberEqualsInt64(duration.hours, &hours)) { - return mozilla::Nothing(); - } - mozilla::CheckedInt64 result = days; - result *= 24; - result += hours; - - // Step 1. - int64_t minutes; - if (!mozilla::NumberEqualsInt64(duration.minutes, &minutes)) { - return mozilla::Nothing(); - } - result *= 60; - result += minutes; - - // Step 2. - int64_t seconds; - if (!mozilla::NumberEqualsInt64(duration.seconds, &seconds)) { - return mozilla::Nothing(); - } - result *= 60; - result += seconds; - - // Step 3. - int64_t milliseconds; - if (!mozilla::NumberEqualsInt64(duration.milliseconds, &milliseconds)) { - return mozilla::Nothing(); - } - result *= 1000; - result += milliseconds; - - // Step 4. - int64_t microseconds; - if (!mozilla::NumberEqualsInt64(duration.microseconds, µseconds)) { - return mozilla::Nothing(); - } - result *= 1000; - result += microseconds; - - // Step 5. - int64_t nanoseconds; - if (!mozilla::NumberEqualsInt64(duration.nanoseconds, &nanoseconds)) { - return mozilla::Nothing(); - } - result *= 1000; - result += nanoseconds; - - // Step 5 (Return). - if (!result.isValid()) { - return mozilla::Nothing(); - } - return mozilla::Some(result.value()); -} - -/** - * TotalDurationNanoseconds ( hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds ) - */ -static BigInt* TotalDurationNanosecondsSlow(JSContext* cx, - const Duration& duration) { - // Our implementation supports |duration.days| to avoid computing |days * 24| - // in the caller, which may not be representable as a double value. - Rooted<BigInt*> result(cx, BigInt::createFromDouble(cx, duration.days)); - if (!result) { - return nullptr; - } - - Rooted<BigInt*> temp(cx); - auto multiplyAdd = [&](int32_t factor, double number) { - temp = BigInt::createFromInt64(cx, factor); - if (!temp) { - return false; - } - - result = BigInt::mul(cx, result, temp); - if (!result) { - return false; - } - - temp = BigInt::createFromDouble(cx, number); - if (!temp) { - return false; - } - - result = BigInt::add(cx, result, temp); - return !!result; - }; - - if (!multiplyAdd(24, duration.hours)) { - return nullptr; - } - - // Step 1. - if (!multiplyAdd(60, duration.minutes)) { - return nullptr; - } - - // Step 2. - if (!multiplyAdd(60, duration.seconds)) { - return nullptr; - } - - // Step 3. - if (!multiplyAdd(1000, duration.milliseconds)) { - return nullptr; - } - - // Step 4. - if (!multiplyAdd(1000, duration.microseconds)) { - return nullptr; - } - - // Step 5. - if (!multiplyAdd(1000, duration.nanoseconds)) { - return nullptr; - } - - // Step 5 (Return). - return result; -} - -struct NanosecondsAndDays final { - int32_t days = 0; - int64_t nanoseconds = 0; -}; - -/** - * Split duration into full days and remainding nanoseconds. - */ -static ::NanosecondsAndDays NanosecondsToDays(int64_t nanoseconds) { - constexpr int64_t dayLengthNs = ToNanoseconds(TemporalUnit::Day); - - static_assert(INT64_MAX / dayLengthNs <= INT32_MAX, - "days doesn't exceed INT32_MAX"); - - return {int32_t(nanoseconds / dayLengthNs), nanoseconds % dayLengthNs}; -} - -/** - * Split duration into full days and remainding nanoseconds. - */ -static bool NanosecondsToDaysSlow( - JSContext* cx, Handle<BigInt*> nanoseconds, - MutableHandle<temporal::NanosecondsAndDays> result) { - constexpr int64_t dayLengthNs = ToNanoseconds(TemporalUnit::Day); - - Rooted<BigInt*> dayLength(cx, BigInt::createFromInt64(cx, dayLengthNs)); - if (!dayLength) { - return false; - } - - Rooted<BigInt*> days(cx); - Rooted<BigInt*> nanos(cx); - if (!BigInt::divmod(cx, nanoseconds, dayLength, &days, &nanos)) { - return false; - } - - result.set(temporal::NanosecondsAndDays::from( - days, ToInstantSpan(nanos), InstantSpan::fromNanoseconds(dayLengthNs))); - return true; -} - -/** * Split duration into full days and remainding nanoseconds. */ -static bool NanosecondsToDays( - JSContext* cx, const Duration& duration, - MutableHandle<temporal::NanosecondsAndDays> result) { - if (auto total = TotalDurationNanoseconds(duration.time())) { - auto nanosAndDays = ::NanosecondsToDays(*total); - - result.set(temporal::NanosecondsAndDays::from( - nanosAndDays.days, - InstantSpan::fromNanoseconds(nanosAndDays.nanoseconds), - InstantSpan::fromNanoseconds(ToNanoseconds(TemporalUnit::Day)))); - return true; - } +static NormalizedTimeAndDays NormalizedTimeDurationToDays( + const NormalizedTimeDuration& duration) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); - Rooted<BigInt*> nanoseconds( - cx, TotalDurationNanosecondsSlow(cx, duration.time())); - if (!nanoseconds) { - return false; + auto [seconds, nanoseconds] = duration; + if (seconds < 0 && nanoseconds > 0) { + seconds += 1; + nanoseconds -= 1'000'000'000; } - return ::NanosecondsToDaysSlow(cx, nanoseconds, result); -} - -/** - * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , - * precalculatedPlainDateTime ] ) - */ -static bool NanosecondsToDays( - JSContext* cx, const Duration& duration, - Handle<ZonedDateTime> zonedRelativeTo, Handle<TimeZoneRecord> timeZone, - MutableHandle<temporal::NanosecondsAndDays> result) { - if (auto total = TotalDurationNanoseconds(duration.time())) { - auto nanoseconds = InstantSpan::fromNanoseconds(*total); - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); + int64_t days = seconds / ToSeconds(TemporalUnit::Day); + seconds = seconds % ToSeconds(TemporalUnit::Day); - return NanosecondsToDays(cx, nanoseconds, zonedRelativeTo, timeZone, - result); - } + int64_t time = seconds * ToNanoseconds(TemporalUnit::Second) + nanoseconds; - auto* nanoseconds = TotalDurationNanosecondsSlow(cx, duration.time()); - if (!nanoseconds) { - return false; - } + constexpr int64_t dayLength = ToNanoseconds(TemporalUnit::Day); + MOZ_ASSERT(std::abs(time) < dayLength); - // NanosecondsToDays, step 6. - if (!IsValidInstantSpan(nanoseconds)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_INSTANT_INVALID); - return false; - } - - return NanosecondsToDays(cx, ToInstantSpan(nanoseconds), zonedRelativeTo, - timeZone, result); + return {days, time, dayLength}; } /** @@ -943,14 +1295,28 @@ static TimeDuration CreateTimeDurationRecord(int64_t days, int64_t hours, int64_t microseconds, int64_t nanoseconds) { // Step 1. - MOZ_ASSERT(IsValidDuration({0, 0, 0, double(days), double(hours), - double(minutes), double(seconds), - double(microseconds), double(nanoseconds)})); + MOZ_ASSERT(IsValidDuration( + {0, 0, 0, double(days), double(hours), double(minutes), double(seconds), + double(milliseconds), double(microseconds), double(nanoseconds)})); + + // All values are safe integers, so we don't need to convert to `double` and + // back for the `ℝ(𝔽(x))` conversion. + MOZ_ASSERT(IsSafeInteger(days)); + MOZ_ASSERT(IsSafeInteger(hours)); + MOZ_ASSERT(IsSafeInteger(minutes)); + MOZ_ASSERT(IsSafeInteger(seconds)); + MOZ_ASSERT(IsSafeInteger(milliseconds)); + MOZ_ASSERT(IsSafeInteger(microseconds)); + MOZ_ASSERT(IsSafeInteger(nanoseconds)); // Step 2. return { - double(days), double(hours), double(minutes), - double(seconds), double(milliseconds), double(microseconds), + days, + hours, + minutes, + seconds, + milliseconds, + double(microseconds), double(nanoseconds), }; } @@ -959,647 +1325,372 @@ static TimeDuration CreateTimeDurationRecord(int64_t days, int64_t hours, * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, * microseconds, nanoseconds ) */ -static TimeDuration CreateTimeDurationRecord(double days, double hours, - double minutes, double seconds, - double milliseconds, - double microseconds, - double nanoseconds) { +static TimeDuration CreateTimeDurationRecord(int64_t milliseconds, + const Int128& microseconds, + const Int128& nanoseconds) { // Step 1. - MOZ_ASSERT(IsValidDuration({0, 0, 0, days, hours, minutes, seconds, - milliseconds, microseconds, nanoseconds})); + MOZ_ASSERT(IsValidDuration({0, 0, 0, 0, 0, 0, 0, double(milliseconds), + double(microseconds), double(nanoseconds)})); // Step 2. - // NB: Adds +0.0 to correctly handle negative zero. return { - days + (+0.0), hours + (+0.0), minutes + (+0.0), - seconds + (+0.0), milliseconds + (+0.0), microseconds + (+0.0), - nanoseconds + (+0.0), + 0, 0, 0, 0, milliseconds, double(microseconds), double(nanoseconds), }; } /** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - * - * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit ) + * BalanceTimeDuration ( norm, largestUnit ) */ -static TimeDuration BalanceTimeDuration(int64_t nanoseconds, - TemporalUnit largestUnit) { - // Step 1. (Handled in caller.) +TimeDuration js::temporal::BalanceTimeDuration( + const NormalizedTimeDuration& duration, TemporalUnit largestUnit) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(largestUnit <= TemporalUnit::Second, + "fallible fractional seconds units"); - // Step 2. + auto [seconds, nanoseconds] = duration; + + // Negative nanoseconds are represented as the difference to 1'000'000'000. + // Convert these back to their absolute value and adjust the seconds part + // accordingly. + // + // For example the nanoseconds duration |-1n| is represented as the + // duration {seconds: -1, nanoseconds: 999'999'999}. + if (seconds < 0 && nanoseconds > 0) { + seconds += 1; + nanoseconds -= ToNanoseconds(TemporalUnit::Second); + } + + // Step 1. int64_t days = 0; int64_t hours = 0; int64_t minutes = 0; - int64_t seconds = 0; int64_t milliseconds = 0; int64_t microseconds = 0; - // Steps 3-4. (Not applicable in our implementation.) + // Steps 2-3. (Not applicable in our implementation.) // // We don't need to convert to positive numbers, because integer division // truncates and the %-operator has modulo semantics. - // Steps 5-11. + // Steps 4-10. switch (largestUnit) { - // Step 5. + // Step 4. case TemporalUnit::Year: case TemporalUnit::Month: case TemporalUnit::Week: case TemporalUnit::Day: { - // Step 5.a. + // Step 4.a. microseconds = nanoseconds / 1000; - // Step 5.b. + // Step 4.b. nanoseconds = nanoseconds % 1000; - // Step 5.c. + // Step 4.c. milliseconds = microseconds / 1000; - // Step 5.d. + // Step 4.d. microseconds = microseconds % 1000; - // Step 5.e. - seconds = milliseconds / 1000; + // Steps 4.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Step 5.f. - milliseconds = milliseconds % 1000; - - // Step 5.g. + // Step 4.g. minutes = seconds / 60; - // Step 5.h. + // Step 4.h. seconds = seconds % 60; - // Step 5.i. + // Step 4.i. hours = minutes / 60; - // Step 5.j. + // Step 4.j. minutes = minutes % 60; - // Step 5.k. + // Step 4.k. days = hours / 24; - // Step 5.l. + // Step 4.l. hours = hours % 24; break; } + // Step 5. case TemporalUnit::Hour: { - // Step 6.a. + // Step 5.a. microseconds = nanoseconds / 1000; - // Step 6.b. + // Step 5.b. nanoseconds = nanoseconds % 1000; - // Step 6.c. + // Step 5.c. milliseconds = microseconds / 1000; - // Step 6.d. + // Step 5.d. microseconds = microseconds % 1000; - // Step 6.e. - seconds = milliseconds / 1000; - - // Step 6.f. - milliseconds = milliseconds % 1000; + // Steps 5.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Step 6.g. + // Step 5.g. minutes = seconds / 60; - // Step 6.h. + // Step 5.h. seconds = seconds % 60; - // Step 6.i. + // Step 5.i. hours = minutes / 60; - // Step 6.j. + // Step 5.j. minutes = minutes % 60; break; } - // Step 7. case TemporalUnit::Minute: { - // Step 7.a. + // Step 6.a. microseconds = nanoseconds / 1000; - // Step 7.b. + // Step 6.b. nanoseconds = nanoseconds % 1000; - // Step 7.c. + // Step 6.c. milliseconds = microseconds / 1000; - // Step 7.d. + // Step 6.d. microseconds = microseconds % 1000; - // Step 7.e. - seconds = milliseconds / 1000; + // Steps 6.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Step 7.f. - milliseconds = milliseconds % 1000; - - // Step 7.g. + // Step 6.g. minutes = seconds / 60; - // Step 7.h. + // Step 6.h. seconds = seconds % 60; break; } - // Step 8. + // Step 7. case TemporalUnit::Second: { - // Step 8.a. - microseconds = nanoseconds / 1000; - - // Step 8.b. - nanoseconds = nanoseconds % 1000; - - // Step 8.c. - milliseconds = microseconds / 1000; - - // Step 8.d. - microseconds = microseconds % 1000; - - // Step 8.e. - seconds = milliseconds / 1000; - - // Step 8.f. - milliseconds = milliseconds % 1000; - - break; - } - - // Step 9. - case TemporalUnit::Millisecond: { - // Step 9.a. + // Step 7.a. microseconds = nanoseconds / 1000; - // Step 9.b. + // Step 7.b. nanoseconds = nanoseconds % 1000; - // Step 9.c. + // Step 7.c. milliseconds = microseconds / 1000; - // Step 9.d. + // Step 7.d. microseconds = microseconds % 1000; - break; - } - - // Step 10. - case TemporalUnit::Microsecond: { - // Step 10.a. - microseconds = nanoseconds / 1000; - - // Step 10.b. - nanoseconds = nanoseconds % 1000; + // Steps 7.e-f. (Not applicable) + MOZ_ASSERT(std::abs(milliseconds) <= 999); break; } - // Step 11. - case TemporalUnit::Nanosecond: { - // Nothing to do. - break; - } - + case TemporalUnit::Millisecond: + case TemporalUnit::Microsecond: + case TemporalUnit::Nanosecond: case TemporalUnit::Auto: MOZ_CRASH("Unexpected temporal unit"); } - // Step 12. (Not applicable, all values are finite) - - // Step 13. + // Step 11. return CreateTimeDurationRecord(days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds); } /** - * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit ) + * BalanceTimeDuration ( norm, largestUnit ) */ -static bool BalancePossiblyInfiniteTimeDurationSlow(JSContext* cx, - Handle<BigInt*> nanos, - TemporalUnit largestUnit, - TimeDuration* result) { - // Steps 1-2. (Handled in caller.) - - BigInt* zero = BigInt::zero(cx); - if (!zero) { - return false; - } +bool js::temporal::BalanceTimeDuration(JSContext* cx, + const NormalizedTimeDuration& duration, + TemporalUnit largestUnit, + TimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); - // Step 3. - Rooted<BigInt*> days(cx, zero); - Rooted<BigInt*> hours(cx, zero); - Rooted<BigInt*> minutes(cx, zero); - Rooted<BigInt*> seconds(cx, zero); - Rooted<BigInt*> milliseconds(cx, zero); - Rooted<BigInt*> microseconds(cx, zero); - Rooted<BigInt*> nanoseconds(cx, nanos); + auto [seconds, nanoseconds] = duration; - // Steps 4-5. + // Negative nanoseconds are represented as the difference to 1'000'000'000. + // Convert these back to their absolute value and adjust the seconds part + // accordingly. // - // We don't need to convert to positive numbers, because BigInt division - // truncates and BigInt modulo has modulo semantics. - - // Steps 6-12. - Rooted<BigInt*> thousand(cx, BigInt::createFromInt64(cx, 1000)); - if (!thousand) { - return false; + // For example the nanoseconds duration |-1n| is represented as the + // duration {seconds: -1, nanoseconds: 999'999'999}. + if (seconds < 0 && nanoseconds > 0) { + seconds += 1; + nanoseconds -= ToNanoseconds(TemporalUnit::Second); } - Rooted<BigInt*> sixty(cx, BigInt::createFromInt64(cx, 60)); - if (!sixty) { - return false; - } - - Rooted<BigInt*> twentyfour(cx, BigInt::createFromInt64(cx, 24)); - if (!twentyfour) { - return false; - } + // Steps 1-3. (Not applicable in our implementation.) + // + // We don't need to convert to positive numbers, because integer division + // truncates and the %-operator has modulo semantics. + // Steps 4-10. switch (largestUnit) { - // Step 6. + // Steps 4-7. case TemporalUnit::Year: case TemporalUnit::Month: case TemporalUnit::Week: - case TemporalUnit::Day: { - // Steps 6.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } - - // Steps 6.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } - - // Steps 6.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } - - // Steps 6.g-h. - if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { - return false; - } - - // Steps 6.i-j. - if (!BigInt::divmod(cx, minutes, sixty, &hours, &minutes)) { - return false; - } - - // Steps 6.k-l. - if (!BigInt::divmod(cx, hours, twentyfour, &days, &hours)) { - return false; - } - - break; - } + case TemporalUnit::Day: + case TemporalUnit::Hour: + case TemporalUnit::Minute: + case TemporalUnit::Second: + *result = BalanceTimeDuration(duration, largestUnit); + return true; - // Step 7. - case TemporalUnit::Hour: { - // Steps 7.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } + // Step 8. + case TemporalUnit::Millisecond: { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = + (int64_t(1) << 53) * ToMilliseconds(TemporalUnit::Second); - // Steps 7.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } + // The largest possible milliseconds value whose double representation + // doesn't exceed the normalized seconds limit. + constexpr auto max = int64_t(0x7cff'ffff'ffff'fdff); - // Steps 7.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } + // Assert |max| is the maximum allowed milliseconds value. + static_assert(double(max) < double(limit)); + static_assert(double(max + 1) >= double(limit)); - // Steps 7.g-h. - if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { - return false; - } + static_assert((NormalizedTimeDuration::max().seconds + 1) * + ToMilliseconds(TemporalUnit::Second) <= + INT64_MAX, + "total number duration milliseconds fits into int64"); - // Steps 7.i-j. - if (!BigInt::divmod(cx, minutes, sixty, &hours, &minutes)) { - return false; - } - - break; - } + // Step 8.a. + int64_t microseconds = nanoseconds / 1000; - // Step 8. - case TemporalUnit::Minute: { - // Steps 8.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } + // Step 8.b. + nanoseconds = nanoseconds % 1000; - // Steps 8.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } + // Step 8.c. + int64_t milliseconds = microseconds / 1000; + MOZ_ASSERT(std::abs(milliseconds) <= 999); - // Steps 8.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } + // Step 8.d. + microseconds = microseconds % 1000; - // Steps 8.g-h. - if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { + auto millis = + (seconds * ToMilliseconds(TemporalUnit::Second)) + milliseconds; + if (std::abs(millis) > max) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } - break; + // Step 11. + *result = CreateTimeDurationRecord(millis, Int128{microseconds}, + Int128{nanoseconds}); + return true; } // Step 9. - case TemporalUnit::Second: { - // Steps 9.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } + case TemporalUnit::Microsecond: { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = Uint128{int64_t(1) << 53} * + Uint128{ToMicroseconds(TemporalUnit::Second)}; - // Steps 9.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { - return false; - } + // The largest possible microseconds value whose double representation + // doesn't exceed the normalized seconds limit. + constexpr auto max = + (Uint128{0x1e8} << 64) + Uint128{0x47ff'ffff'fff7'ffff}; + static_assert(max < limit); - // Steps 9.e-f. - if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, - &milliseconds)) { - return false; - } + // Assert |max| is the maximum allowed microseconds value. + MOZ_ASSERT(double(max) < double(limit)); + MOZ_ASSERT(double(max + Uint128{1}) >= double(limit)); - break; - } + // Step 9.a. + int64_t microseconds = nanoseconds / 1000; + MOZ_ASSERT(std::abs(microseconds) <= 999'999); - // Step 10. - case TemporalUnit::Millisecond: { - // Steps 10.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { - return false; - } + // Step 9.b. + nanoseconds = nanoseconds % 1000; - // Steps 10.c-d. - if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, - µseconds)) { + auto micros = + (Int128{seconds} * Int128{ToMicroseconds(TemporalUnit::Second)}) + + Int128{microseconds}; + if (micros.abs() > max) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } - break; + // Step 11. + *result = CreateTimeDurationRecord(0, micros, Int128{nanoseconds}); + return true; } - // Step 11. - case TemporalUnit::Microsecond: { - // Steps 11.a-b. - if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, - &nanoseconds)) { + // Step 10. + case TemporalUnit::Nanosecond: { + // The number of normalized seconds must not exceed `2**53 - 1`. + constexpr auto limit = Uint128{int64_t(1) << 53} * + Uint128{ToNanoseconds(TemporalUnit::Second)}; + + // The largest possible nanoseconds value whose double representation + // doesn't exceed the normalized seconds limit. + constexpr auto max = + (Uint128{0x77359} << 64) + Uint128{0x3fff'ffff'dfff'ffff}; + static_assert(max < limit); + + // Assert |max| is the maximum allowed nanoseconds value. + MOZ_ASSERT(double(max) < double(limit)); + MOZ_ASSERT(double(max + Uint128{1}) >= double(limit)); + + MOZ_ASSERT(std::abs(nanoseconds) <= 999'999'999); + + auto nanos = + (Int128{seconds} * Int128{ToNanoseconds(TemporalUnit::Second)}) + + Int128{nanoseconds}; + if (nanos.abs() > max) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } - break; - } - - // Step 12. - case TemporalUnit::Nanosecond: { - // Nothing to do. - break; - } - - case TemporalUnit::Auto: - MOZ_CRASH("Unexpected temporal unit"); - } - - double daysNumber = BigInt::numberValue(days); - double hoursNumber = BigInt::numberValue(hours); - double minutesNumber = BigInt::numberValue(minutes); - double secondsNumber = BigInt::numberValue(seconds); - double millisecondsNumber = BigInt::numberValue(milliseconds); - double microsecondsNumber = BigInt::numberValue(microseconds); - double nanosecondsNumber = BigInt::numberValue(nanoseconds); - - // Step 13. - for (double v : {daysNumber, hoursNumber, minutesNumber, secondsNumber, - millisecondsNumber, microsecondsNumber, nanosecondsNumber}) { - if (std::isinf(v)) { - *result = { - daysNumber, hoursNumber, minutesNumber, - secondsNumber, millisecondsNumber, microsecondsNumber, - nanosecondsNumber, - }; + // Step 11. + *result = CreateTimeDurationRecord(0, Int128{}, nanos); return true; } - } - - // Step 14. - *result = CreateTimeDurationRecord(daysNumber, hoursNumber, minutesNumber, - secondsNumber, millisecondsNumber, - microsecondsNumber, nanosecondsNumber); - return true; -} - -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - */ -static bool BalanceTimeDurationSlow(JSContext* cx, Handle<BigInt*> nanoseconds, - TemporalUnit largestUnit, - TimeDuration* result) { - // Step 1. - if (!BalancePossiblyInfiniteTimeDurationSlow(cx, nanoseconds, largestUnit, - result)) { - return false; - } - - // Steps 2-3. - return ThrowIfInvalidDuration(cx, result->toDuration()); -} - -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - */ -static bool BalanceTimeDuration(JSContext* cx, const Duration& one, - const Duration& two, TemporalUnit largestUnit, - TimeDuration* result) { - MOZ_ASSERT(IsValidDuration(one)); - MOZ_ASSERT(IsValidDuration(two)); - MOZ_ASSERT(largestUnit >= TemporalUnit::Day); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto oneNanoseconds = TotalDurationNanoseconds(one)) { - if (auto twoNanoseconds = TotalDurationNanoseconds(two)) { - mozilla::CheckedInt64 nanoseconds = *oneNanoseconds; - nanoseconds += *twoNanoseconds; - if (nanoseconds.isValid()) { - *result = ::BalanceTimeDuration(nanoseconds.value(), largestUnit); - return true; - } - } - } - - Rooted<BigInt*> oneNanoseconds(cx, TotalDurationNanosecondsSlow(cx, one)); - if (!oneNanoseconds) { - return false; - } - - Rooted<BigInt*> twoNanoseconds(cx, TotalDurationNanosecondsSlow(cx, two)); - if (!twoNanoseconds) { - return false; - } - - Rooted<BigInt*> nanoseconds(cx, - BigInt::add(cx, oneNanoseconds, twoNanoseconds)); - if (!nanoseconds) { - return false; - } - - return BalanceTimeDurationSlow(cx, nanoseconds, largestUnit, result); -} - -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - */ -static bool BalanceTimeDuration(JSContext* cx, double days, const Duration& one, - const Duration& two, TemporalUnit largestUnit, - TimeDuration* result) { - MOZ_ASSERT(IsInteger(days)); - MOZ_ASSERT(IsValidDuration(one)); - MOZ_ASSERT(IsValidDuration(two)); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto oneNanoseconds = TotalDurationNanoseconds(one)) { - if (auto twoNanoseconds = TotalDurationNanoseconds(two)) { - int64_t intDays; - if (mozilla::NumberEqualsInt64(days, &intDays)) { - mozilla::CheckedInt64 daysNanoseconds = intDays; - daysNanoseconds *= ToNanoseconds(TemporalUnit::Day); - - mozilla::CheckedInt64 nanoseconds = *oneNanoseconds; - nanoseconds += *twoNanoseconds; - nanoseconds += daysNanoseconds; - - if (nanoseconds.isValid()) { - *result = ::BalanceTimeDuration(nanoseconds.value(), largestUnit); - return true; - } - } - } - } - - Rooted<BigInt*> oneNanoseconds(cx, TotalDurationNanosecondsSlow(cx, one)); - if (!oneNanoseconds) { - return false; - } - - Rooted<BigInt*> twoNanoseconds(cx, TotalDurationNanosecondsSlow(cx, two)); - if (!twoNanoseconds) { - return false; - } - - Rooted<BigInt*> nanoseconds(cx, - BigInt::add(cx, oneNanoseconds, twoNanoseconds)); - if (!nanoseconds) { - return false; - } - - if (days) { - Rooted<BigInt*> daysNanoseconds( - cx, TotalDurationNanosecondsSlow(cx, {0, 0, 0, days})); - if (!daysNanoseconds) { - return false; - } - - nanoseconds = BigInt::add(cx, nanoseconds, daysNanoseconds); - if (!nanoseconds) { - return false; - } - } - - return BalanceTimeDurationSlow(cx, nanoseconds, largestUnit, result); -} -/** - * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit ) - */ -static bool BalancePossiblyInfiniteTimeDuration(JSContext* cx, - const Duration& duration, - TemporalUnit largestUnit, - TimeDuration* result) { - // NB: |duration.days| can have a different sign than the time components. - MOZ_ASSERT(IsValidDuration(duration.time())); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto nanoseconds = TotalDurationNanoseconds(duration)) { - *result = ::BalanceTimeDuration(*nanoseconds, largestUnit); - return true; - } - - // Steps 1-2. - Rooted<BigInt*> nanoseconds(cx, TotalDurationNanosecondsSlow(cx, duration)); - if (!nanoseconds) { - return false; - } - - // Steps 3-14. - return ::BalancePossiblyInfiniteTimeDurationSlow(cx, nanoseconds, largestUnit, - result); -} - -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - */ -bool js::temporal::BalanceTimeDuration(JSContext* cx, const Duration& duration, - TemporalUnit largestUnit, - TimeDuration* result) { - if (!::BalancePossiblyInfiniteTimeDuration(cx, duration, largestUnit, - result)) { - return false; + case TemporalUnit::Auto: + break; } - return ThrowIfInvalidDuration(cx, result->toDuration()); + MOZ_CRASH("Unexpected temporal unit"); } /** - * BalancePossiblyInfiniteTimeDurationRelative ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit, zonedRelativeTo, - * timeZoneRec [ , precalculatedPlainDateTime ] ) + * BalanceTimeDurationRelative ( days, norm, largestUnit, zonedRelativeTo, + * timeZoneRec, precalculatedPlainDateTime ) */ -static bool BalancePossiblyInfiniteTimeDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, +static bool BalanceTimeDurationRelative( + JSContext* cx, const NormalizedDuration& duration, TemporalUnit largestUnit, Handle<ZonedDateTime> relativeTo, Handle<TimeZoneRecord> timeZone, mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, TimeDuration* result) { - // Step 1. (Not applicable) + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 1. + const auto& startNs = relativeTo.instant(); // Step 2. - auto intermediateNs = relativeTo.instant(); + const auto& startInstant = startNs; // Step 3. - const auto& startInstant = relativeTo.instant(); + auto intermediateNs = startNs; // Step 4. PlainDateTime startDateTime; - if (duration.days != 0) { + if (duration.date.days != 0) { // Step 4.a. if (!precalculatedPlainDateTime) { if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &startDateTime)) { @@ -1612,7 +1703,7 @@ static bool BalancePossiblyInfiniteTimeDurationRelative( // Steps 4.b-c. Rooted<CalendarValue> isoCalendar(cx, CalendarValue(cx->names().iso8601)); if (!AddDaysToZonedDateTime(cx, startInstant, *precalculatedPlainDateTime, - timeZone, isoCalendar, duration.days, + timeZone, isoCalendar, duration.date.days, &intermediateNs)) { return false; } @@ -1620,23 +1711,23 @@ static bool BalancePossiblyInfiniteTimeDurationRelative( // Step 5. Instant endNs; - if (!AddInstant(cx, intermediateNs, duration.time(), &endNs)) { + if (!AddInstant(cx, intermediateNs, duration.time, &endNs)) { return false; } MOZ_ASSERT(IsValidEpochInstant(endNs)); // Step 6. - auto nanoseconds = endNs - relativeTo.instant(); - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); + auto normalized = + NormalizedTimeDurationFromEpochNanosecondsDifference(endNs, startInstant); // Step 7. - if (nanoseconds == InstantSpan{}) { + if (normalized == NormalizedTimeDuration{}) { *result = {}; return true; } // Steps 8-9. - double days = 0; + int64_t days = 0; if (TemporalUnit::Year <= largestUnit && largestUnit <= TemporalUnit::Day) { // Step 8.a. if (!precalculatedPlainDateTime) { @@ -1648,61 +1739,32 @@ static bool BalancePossiblyInfiniteTimeDurationRelative( } // Step 8.b. - Rooted<temporal::NanosecondsAndDays> nanosAndDays(cx); - if (!NanosecondsToDays(cx, nanoseconds, relativeTo, timeZone, - *precalculatedPlainDateTime, &nanosAndDays)) { + NormalizedTimeAndDays timeAndDays; + if (!NormalizedTimeDurationToDays(cx, normalized, relativeTo, timeZone, + *precalculatedPlainDateTime, + &timeAndDays)) { return false; } - // NB: |days| is passed to CreateTimeDurationRecord, which performs - // |ℝ(𝔽(days))|, so it's safe to convert from BigInt to double here. - // Step 8.c. - days = nanosAndDays.daysNumber(); - MOZ_ASSERT(IsInteger(days)); - - // FIXME: spec issue - `result.[[Nanoseconds]]` not created in all branches - // https://github.com/tc39/proposal-temporal/issues/2616 + days = timeAndDays.days; // Step 8.d. - nanoseconds = nanosAndDays.nanoseconds(); - MOZ_ASSERT_IF(days > 0, nanoseconds >= InstantSpan{}); - MOZ_ASSERT_IF(days < 0, nanoseconds <= InstantSpan{}); + normalized = NormalizedTimeDuration::fromNanoseconds(timeAndDays.time); + MOZ_ASSERT_IF(days > 0, normalized >= NormalizedTimeDuration{}); + MOZ_ASSERT_IF(days < 0, normalized <= NormalizedTimeDuration{}); // Step 8.e. largestUnit = TemporalUnit::Hour; } - // Step 10. (Not applicable in our implementation.) - - // Steps 11-12. + // Step 10. TimeDuration balanceResult; - if (auto nanos = nanoseconds.toNanoseconds(); nanos.isValid()) { - // Step 11. - balanceResult = ::BalanceTimeDuration(nanos.value(), largestUnit); - - // Step 12. - MOZ_ASSERT(IsValidDuration(balanceResult.toDuration())); - } else { - Rooted<BigInt*> ns(cx, ToEpochNanoseconds(cx, nanoseconds)); - if (!ns) { - return false; - } - - // Step 11. - if (!::BalancePossiblyInfiniteTimeDurationSlow(cx, ns, largestUnit, - &balanceResult)) { - return false; - } - - // Step 12. - if (!IsValidDuration(balanceResult.toDuration())) { - *result = balanceResult; - return true; - } + if (!BalanceTimeDuration(cx, normalized, largestUnit, &balanceResult)) { + return false; } - // Step 13. + // Step 11. *result = { days, balanceResult.hours, @@ -1712,99 +1774,44 @@ static bool BalancePossiblyInfiniteTimeDurationRelative( balanceResult.microseconds, balanceResult.nanoseconds, }; + MOZ_ASSERT(IsValidDuration(result->toDuration())); return true; } /** - * BalancePossiblyInfiniteTimeDurationRelative ( days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, largestUnit, zonedRelativeTo, - * timeZoneRec [ , precalculatedPlainDateTime ] ) - */ -static bool BalancePossiblyInfiniteTimeDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - Handle<ZonedDateTime> relativeTo, Handle<TimeZoneRecord> timeZone, - TimeDuration* result) { - return BalancePossiblyInfiniteTimeDurationRelative( - cx, duration, largestUnit, relativeTo, timeZone, mozilla::Nothing(), - result); -} - -/** - * BalanceTimeDurationRelative ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit, zonedRelativeTo, timeZoneRec, - * precalculatedPlainDateTime ) - */ -static bool BalanceTimeDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, - Handle<ZonedDateTime> relativeTo, Handle<TimeZoneRecord> timeZone, - mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, - TimeDuration* result) { - // Step 1. - if (!BalancePossiblyInfiniteTimeDurationRelative( - cx, duration, largestUnit, relativeTo, timeZone, - precalculatedPlainDateTime, result)) { - return false; - } - - // Steps 2-3. - return ThrowIfInvalidDuration(cx, result->toDuration()); -} - -/** - * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, - * microseconds, nanoseconds, largestUnit ) - */ -bool js::temporal::BalanceTimeDuration(JSContext* cx, - const InstantSpan& nanoseconds, - TemporalUnit largestUnit, - TimeDuration* result) { - MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); - - // Steps 1-3. (Not applicable) - - // Fast-path when we can perform the whole computation with int64 values. - if (auto nanos = nanoseconds.toNanoseconds(); nanos.isValid()) { - *result = ::BalanceTimeDuration(nanos.value(), largestUnit); - return true; - } - - Rooted<BigInt*> nanos(cx, ToEpochNanoseconds(cx, nanoseconds)); - if (!nanos) { - return false; - } - - // Steps 4-16. - return ::BalanceTimeDurationSlow(cx, nanos, largestUnit, result); -} - -/** * CreateDateDurationRecord ( years, months, weeks, days ) */ -static DateDuration CreateDateDurationRecord(double years, double months, - double weeks, double days) { - MOZ_ASSERT(IsValidDuration({years, months, weeks, days})); +static DateDuration CreateDateDurationRecord(int64_t years, int64_t months, + int64_t weeks, int64_t days) { + MOZ_ASSERT(IsValidDuration(Duration{ + double(years), + double(months), + double(weeks), + double(days), + })); return {years, months, weeks, days}; } /** * CreateDateDurationRecord ( years, months, weeks, days ) */ -static bool CreateDateDurationRecord(JSContext* cx, double years, double months, - double weeks, double days, - DateDuration* result) { - if (!ThrowIfInvalidDuration(cx, {years, months, weeks, days})) { +static bool CreateDateDurationRecord(JSContext* cx, int64_t years, + int64_t months, int64_t weeks, + int64_t days, DateDuration* result) { + auto duration = DateDuration{years, months, weeks, days}; + if (!ThrowIfInvalidDuration(cx, duration)) { return false; } - *result = {years, months, weeks, days}; + *result = duration; return true; } -static bool UnbalanceDateDurationRelativeHasEffect(const Duration& duration, +static bool UnbalanceDateDurationRelativeHasEffect(const DateDuration& duration, TemporalUnit largestUnit) { MOZ_ASSERT(largestUnit != TemporalUnit::Auto); - // Steps 2, 3.a-b, 4.a-b, 6-7. + // Steps 2-4. return (largestUnit > TemporalUnit::Year && duration.years != 0) || (largestUnit > TemporalUnit::Month && duration.months != 0) || (largestUnit > TemporalUnit::Week && duration.weeks != 0); @@ -1815,84 +1822,67 @@ static bool UnbalanceDateDurationRelativeHasEffect(const Duration& duration, * plainRelativeTo, calendarRec ) */ static bool UnbalanceDateDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, + JSContext* cx, const DateDuration& duration, TemporalUnit largestUnit, Handle<Wrapped<PlainDateObject*>> plainRelativeTo, Handle<CalendarRecord> calendar, DateDuration* result) { MOZ_ASSERT(IsValidDuration(duration)); - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + auto [years, months, weeks, days] = duration; // Step 1. (Not applicable in our implementation.) - // Steps 2, 3.a, 4.a, and 6. + // Steps 2-4. if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { - // Steps 2.a, 3.a, 4.a, and 6. - *result = CreateDateDurationRecord(years, months, weeks, days); + *result = duration; return true; } - // Step 3. - if (largestUnit == TemporalUnit::Month) { - // Step 3.a. (Handled above) - MOZ_ASSERT(years != 0); + // Step 5. + MOZ_ASSERT(largestUnit != TemporalUnit::Year); - // Step 3.b. (Not applicable in our implementation.) + // Step 6. (Not applicable in our implementation.) - // Step 3.c. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + // Step 7. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); - // Step 3.d. + // Step 8. + if (largestUnit == TemporalUnit::Month) { + // Step 8.a. MOZ_ASSERT( CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); - // Step 3.e. - auto yearsDuration = Duration{years}; + // Step 8.b. + auto yearsDuration = DateDuration{years}; - // Step 3.f. + // Step 8.c. Rooted<Wrapped<PlainDateObject*>> later( cx, CalendarDateAdd(cx, calendar, plainRelativeTo, yearsDuration)); if (!later) { return false; } - // Steps 3.g-i. - Duration untilResult; + // Steps 8.d-f. + DateDuration untilResult; if (!CalendarDateUntil(cx, calendar, plainRelativeTo, later, TemporalUnit::Month, &untilResult)) { return false; } - // Step 3.j. - double yearsInMonths = untilResult.months; + // Step 8.g. + int64_t yearsInMonths = untilResult.months; - // Step 3.k. - // - // The addition |months + yearsInMonths| can be imprecise, but this is - // safe to ignore, because all values are passed to - // CreateDateDurationRecord, which converts the values to Numbers. + // Step 8.h. return CreateDateDurationRecord(cx, 0, months + yearsInMonths, weeks, days, result); } - // Step 4. + // Step 9. if (largestUnit == TemporalUnit::Week) { - // Step 4.a. (Handled above) - MOZ_ASSERT(years != 0 || months != 0); - - // Step 4.b. (Not applicable in our implementation.) - - // Step 4.c. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); - - // Step 4.d. - auto yearsMonthsDuration = Duration{years, months}; + // Step 9.a. + auto yearsMonthsDuration = DateDuration{years, months}; - // Step 4.e. + // Step 9.b. auto later = CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsDuration); if (!later) { @@ -1906,35 +1896,20 @@ static bool UnbalanceDateDurationRelative( } auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 4.f. + // Step 9.c. int32_t yearsMonthsInDays = DaysUntil(relativeToDate, laterDate); - // Step 4.g. - // - // The addition |days + yearsMonthsInDays| can be imprecise, but this is - // safe to ignore, because all values are passed to - // CreateDateDurationRecord, which converts the values to Numbers. + // Step 9.d. return CreateDateDurationRecord(cx, 0, 0, weeks, days + yearsMonthsInDays, result); } - // Step 5. (Not applicable in our implementation.) - - // Step 6. (Handled above) - MOZ_ASSERT(years != 0 || months != 0 || weeks != 0); - - // FIXME: why don't we unconditionally throw an error for missing calendars? - - // Step 7. (Not applicable in our implementation.) - - // Step 8. - MOZ_ASSERT( - CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + // Step 10. (Not applicable in our implementation.) - // Step 9. - auto yearsMonthsWeeksDuration = Duration{years, months, weeks}; + // Step 11. + auto yearsMonthsWeeksDuration = DateDuration{years, months, weeks}; - // Step 10. + // Step 12. auto later = CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsWeeksDuration); if (!later) { @@ -1948,14 +1923,10 @@ static bool UnbalanceDateDurationRelative( } auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 11. + // Step 13. int32_t yearsMonthsWeeksInDay = DaysUntil(relativeToDate, laterDate); - // Step 12. - // - // The addition |days + yearsMonthsWeeksInDay| can be imprecise, but this is - // safe to ignore, because all values are passed to CreateDateDurationRecord, - // which converts the values to Numbers. + // Step 14. return CreateDateDurationRecord(cx, 0, 0, 0, days + yearsMonthsWeeksInDay, result); } @@ -1965,28 +1936,23 @@ static bool UnbalanceDateDurationRelative( * plainRelativeTo, calendarRec ) */ static bool UnbalanceDateDurationRelative(JSContext* cx, - const Duration& duration, + const DateDuration& duration, TemporalUnit largestUnit, DateDuration* result) { MOZ_ASSERT(IsValidDuration(duration)); - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; - // Step 1. (Not applicable.) - // Steps 2, 3.a, 4.a, and 6. + // Step 2-4. if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { - // Steps 2.a, 3.a, 4.a, and 6. - *result = CreateDateDurationRecord(years, months, weeks, days); + *result = duration; return true; } - // Step 5. (Not applicable.) + // Step 5. + MOZ_ASSERT(largestUnit != TemporalUnit::Year); - // Steps 3.b, 4.b, and 7. + // Steps 6. JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, "calendar"); return false; @@ -1997,17 +1963,14 @@ static bool UnbalanceDateDurationRelative(JSContext* cx, * smallestUnit, plainRelativeTo, calendarRec ) */ static bool BalanceDateDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, + JSContext* cx, const DateDuration& duration, TemporalUnit largestUnit, TemporalUnit smallestUnit, Handle<Wrapped<PlainDateObject*>> plainRelativeTo, Handle<CalendarRecord> calendar, DateDuration* result) { MOZ_ASSERT(IsValidDuration(duration)); MOZ_ASSERT(largestUnit <= smallestUnit); - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + auto [years, months, weeks, days] = duration; // FIXME: spec issue - effectful code paths should be more fine-grained // similar to UnbalanceDateDurationRelative. For example: @@ -2023,7 +1986,7 @@ static bool BalanceDateDurationRelative( if (largestUnit > TemporalUnit::Week || (years == 0 && months == 0 && weeks == 0 && days == 0)) { // Step 4.a. - *result = CreateDateDurationRecord(years, months, weeks, days); + *result = duration; return true; } @@ -2045,7 +2008,8 @@ static bool BalanceDateDurationRelative( // Steps 8-9. (Not applicable in our implementation.) - auto untilAddedDate = [&](const Duration& duration, Duration* untilResult) { + auto untilAddedDate = [&](const DateDuration& duration, + DateDuration* untilResult) { Rooted<Wrapped<PlainDateObject*>> later( cx, AddDate(cx, calendar, plainRelativeTo, duration)); if (!later) { @@ -2064,16 +2028,14 @@ static bool BalanceDateDurationRelative( MOZ_ASSERT(days == 0); // Step 10.a.ii. - auto yearsMonthsDuration = Duration{years, months}; + auto yearsMonthsDuration = DateDuration{years, months}; // Steps 10.a.iii-iv. - Duration untilResult; + DateDuration untilResult; if (!untilAddedDate(yearsMonthsDuration, &untilResult)) { return false; } - // FIXME: spec bug - CreateDateDurationRecord is infallible - // Step 10.a.v. *result = CreateDateDurationRecord(untilResult.years, untilResult.months, weeks, 0); @@ -2081,17 +2043,14 @@ static bool BalanceDateDurationRelative( } // Step 10.b. - auto yearsMonthsWeeksDaysDuration = Duration{years, months, weeks, days}; + const auto& yearsMonthsWeeksDaysDuration = duration; // Steps 10.c-d. - Duration untilResult; + DateDuration untilResult; if (!untilAddedDate(yearsMonthsWeeksDaysDuration, &untilResult)) { return false; } - // FIXME: spec bug - CreateDateDurationRecord is infallible - // https://github.com/tc39/proposal-temporal/issues/2750 - // Step 10.e. *result = CreateDateDurationRecord(untilResult.years, untilResult.months, untilResult.weeks, untilResult.days); @@ -2114,17 +2073,14 @@ static bool BalanceDateDurationRelative( } // Step 11.c. - auto monthsWeeksDaysDuration = Duration{0, months, weeks, days}; + const auto& monthsWeeksDaysDuration = duration; // Steps 11.d-e. - Duration untilResult; + DateDuration untilResult; if (!untilAddedDate(monthsWeeksDaysDuration, &untilResult)) { return false; } - // FIXME: spec bug - CreateDateDurationRecord is infallible - // https://github.com/tc39/proposal-temporal/issues/2750 - // Step 11.f. *result = CreateDateDurationRecord(0, untilResult.months, untilResult.weeks, untilResult.days); @@ -2141,17 +2097,14 @@ static bool BalanceDateDurationRelative( MOZ_ASSERT(months == 0); // Step 15. - auto weeksDaysDuration = Duration{0, 0, weeks, days}; + const auto& weeksDaysDuration = duration; // Steps 16-17. - Duration untilResult; + DateDuration untilResult; if (!untilAddedDate(weeksDaysDuration, &untilResult)) { return false; } - // FIXME: spec bug - CreateDateDurationRecord is infallible - // https://github.com/tc39/proposal-temporal/issues/2750 - // Step 18. *result = CreateDateDurationRecord(0, 0, untilResult.weeks, untilResult.days); return true; @@ -2162,7 +2115,7 @@ static bool BalanceDateDurationRelative( * smallestUnit, plainRelativeTo, calendarRec ) */ bool js::temporal::BalanceDateDurationRelative( - JSContext* cx, const Duration& duration, TemporalUnit largestUnit, + JSContext* cx, const DateDuration& duration, TemporalUnit largestUnit, TemporalUnit smallestUnit, Handle<Wrapped<PlainDateObject*>> plainRelativeTo, Handle<CalendarRecord> calendar, DateDuration* result) { @@ -2179,7 +2132,7 @@ bool js::temporal::BalanceDateDurationRelative( * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) */ static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, - Duration* duration) { + Duration* result) { MOZ_ASSERT(IsValidDuration(one)); MOZ_ASSERT(IsValidDuration(two)); @@ -2194,7 +2147,13 @@ static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, // Step 5. auto largestUnit = std::min(largestUnit1, largestUnit2); - // Step 6.a. + // Step 6. + auto normalized1 = NormalizeTimeDuration(one); + + // Step 7. + auto normalized2 = NormalizeTimeDuration(two); + + // Step 8.a. if (largestUnit <= TemporalUnit::Week) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, @@ -2202,14 +2161,31 @@ static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, return false; } - // Step 6.b. - TimeDuration result; - if (!BalanceTimeDuration(cx, one, two, largestUnit, &result)) { + // Step 8.b. + NormalizedTimeDuration normalized; + if (!AddNormalizedTimeDuration(cx, normalized1, normalized2, &normalized)) { + return false; + } + + // Step 8.c. + int64_t days1 = mozilla::AssertedCast<int64_t>(one.days); + int64_t days2 = mozilla::AssertedCast<int64_t>(two.days); + auto totalDays = mozilla::CheckedInt64(days1) + days2; + MOZ_ASSERT(totalDays.isValid(), "adding two duration days can't overflow"); + + if (!Add24HourDaysToNormalizedTimeDuration(cx, normalized, totalDays.value(), + &normalized)) { return false; } - // Steps 6.c. - *duration = result.toDuration(); + // Step 8.d. + TimeDuration balanced; + if (!temporal::BalanceTimeDuration(cx, normalized, largestUnit, &balanced)) { + return false; + } + + // Steps 8.e. + *result = balanced.toDuration(); return true; } @@ -2220,15 +2196,12 @@ static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, */ static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, Handle<Wrapped<PlainDateObject*>> plainRelativeTo, - Handle<CalendarRecord> calendar, Duration* duration) { + Handle<CalendarRecord> calendar, Duration* result) { MOZ_ASSERT(IsValidDuration(one)); MOZ_ASSERT(IsValidDuration(two)); // Steps 1-2. (Not applicable) - // FIXME: spec issue - calendarRec is not undefined when plainRelativeTo is - // not undefined. - // Step 3. auto largestUnit1 = DefaultTemporalLargestUnit(one); @@ -2238,66 +2211,75 @@ static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, // Step 5. auto largestUnit = std::min(largestUnit1, largestUnit2); - // Step 6. (Not applicable) - - // Step 7.a. (Not applicable in our implementation.) + // Step 6. + auto normalized1 = NormalizeTimeDuration(one); - // Step 7.b. - auto dateDuration1 = one.date(); + // Step 7. + auto normalized2 = NormalizeTimeDuration(two); - // Step 7.c. - auto dateDuration2 = two.date(); + // Step 8. (Not applicable) - // FIXME: spec issue - calendarUnitsPresent is unused. + // Step 9.a. (Not applicable in our implementation.) - // Step 7.d. - [[maybe_unused]] bool calendarUnitsPresent = true; + // Step 9.b. + auto dateDuration1 = one.toDateDuration(); - // Step 7.e. - if (dateDuration1.years == 0 && dateDuration1.months == 0 && - dateDuration1.weeks == 0 && dateDuration2.years == 0 && - dateDuration2.months == 0 && dateDuration2.weeks == 0) { - calendarUnitsPresent = false; - } + // Step 9.c. + auto dateDuration2 = two.toDateDuration(); - // Step 7.f. + // Step 9.d. Rooted<Wrapped<PlainDateObject*>> intermediate( cx, AddDate(cx, calendar, plainRelativeTo, dateDuration1)); if (!intermediate) { return false; } - // Step 7.g. + // Step 9.e. Rooted<Wrapped<PlainDateObject*>> end( cx, AddDate(cx, calendar, intermediate, dateDuration2)); if (!end) { return false; } - // Step 7.h. + // Step 9.f. auto dateLargestUnit = std::min(TemporalUnit::Day, largestUnit); - // Steps 7.i-k. - Duration dateDifference; + // Steps 9.g-i. + DateDuration dateDifference; if (!DifferenceDate(cx, calendar, plainRelativeTo, end, dateLargestUnit, &dateDifference)) { return false; } - // Step 7.l. - TimeDuration result; - if (!BalanceTimeDuration(cx, dateDifference.days, one.time(), two.time(), - largestUnit, &result)) { + // Step 9.j. + NormalizedTimeDuration normalized1WithDays; + if (!Add24HourDaysToNormalizedTimeDuration( + cx, normalized1, dateDifference.days, &normalized1WithDays)) { + return false; + } + + // Step 9.k. + NormalizedTimeDuration normalized; + if (!AddNormalizedTimeDuration(cx, normalized1WithDays, normalized2, + &normalized)) { + return false; + } + + // Step 9.l. + TimeDuration balanced; + if (!temporal::BalanceTimeDuration(cx, normalized, largestUnit, &balanced)) { return false; } - // Steps 7.m. - *duration = { - dateDifference.years, dateDifference.months, dateDifference.weeks, - result.days, result.hours, result.minutes, - result.seconds, result.milliseconds, result.microseconds, - result.nanoseconds, + // Steps 9.m. + *result = { + double(dateDifference.years), double(dateDifference.months), + double(dateDifference.weeks), double(balanced.days), + double(balanced.hours), double(balanced.minutes), + double(balanced.seconds), double(balanced.milliseconds), + balanced.microseconds, balanced.nanoseconds, }; + MOZ_ASSERT(IsValidDuration(*result)); return true; } @@ -2323,89 +2305,107 @@ static bool AddDuration( // Step 5. auto largestUnit = std::min(largestUnit1, largestUnit2); - // Steps 6-7. (Not applicable) + // Step 6. + auto normalized1 = NormalizeTimeDuration(one); - // Steps 8-9. (Not applicable in our implementation.) + // Step 7. + auto normalized2 = NormalizeTimeDuration(two); - // FIXME: spec issue - GetPlainDateTimeFor called unnecessarily - // - // clang-format off - // - // 10. If largestUnit is one of "year", "month", "week", or "day", then - // a. If precalculatedPlainDateTime is undefined, then - // i. Let startDateTime be ? GetPlainDateTimeFor(timeZone, zonedRelativeTo.[[Nanoseconds]], calendar). - // b. Else, - // i. Let startDateTime be precalculatedPlainDateTime. - // c. Let intermediateNs be ? AddZonedDateTime(zonedRelativeTo.[[Nanoseconds]], timeZone, calendar, y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, startDateTime). - // d. Let endNs be ? AddZonedDateTime(intermediateNs, timeZone, calendar, y2, mon2, w2, d2, h2, min2, s2, ms2, mus2, ns2). - // e. Return ? DifferenceZonedDateTime(zonedRelativeTo.[[Nanoseconds]], endNs, timeZone, calendar, largestUnit, OrdinaryObjectCreate(null), startDateTime). - // 11. Let intermediateNs be ? AddInstant(zonedRelativeTo.[[Nanoseconds]], h1, min1, s1, ms1, mus1, ns1). - // 12. Let endNs be ? AddInstant(intermediateNs, h2, min2, s2, ms2, mus2, ns2). - // 13. Let result be DifferenceInstant(zonedRelativeTo.[[Nanoseconds]], endNs, 1, "nanosecond", largestUnit, "halfExpand"). - // 14. Return ! CreateDurationRecord(0, 0, 0, 0, result.[[Hours]], result.[[Minutes]], result.[[Seconds]], result.[[Milliseconds]], result.[[Microseconds]], result.[[Nanoseconds]]). - // - // clang-format on + // Steps 8-9. (Not applicable) - // Step 10. + // Steps 10-11. (Not applicable in our implementation.) + + // Step 12. bool startDateTimeNeeded = largestUnit <= TemporalUnit::Day; - // Steps 11-14 and 16. - if (startDateTimeNeeded) { - // Steps 11-12. - PlainDateTime startDateTime; - if (!precalculatedPlainDateTime) { - if (!GetPlainDateTimeFor(cx, timeZone, zonedRelativeTo.instant(), - &startDateTime)) { - return false; - } - } else { - startDateTime = *precalculatedPlainDateTime; - } + // Steps 13-17. + if (!startDateTimeNeeded) { + // Steps 13-14. (Not applicable) - // Step 13. + // Step 15. (Inlined AddZonedDateTime, step 6.) Instant intermediateNs; - if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, - one, startDateTime, &intermediateNs)) { + if (!AddInstant(cx, zonedRelativeTo.instant(), normalized1, + &intermediateNs)) { return false; } MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); - // Step 14. + // Step 16. (Inlined AddZonedDateTime, step 6.) Instant endNs; - if (!AddZonedDateTime(cx, intermediateNs, timeZone, calendar, two, - &endNs)) { + if (!AddInstant(cx, intermediateNs, normalized2, &endNs)) { return false; } MOZ_ASSERT(IsValidEpochInstant(endNs)); - // Step 15. (Not applicable) + // Step 17.a. + auto normalized = NormalizedTimeDurationFromEpochNanosecondsDifference( + endNs, zonedRelativeTo.instant()); - // Step 16. - return DifferenceZonedDateTime(cx, zonedRelativeTo.instant(), endNs, - timeZone, calendar, largestUnit, - startDateTime, result); + // Step 17.b. + TimeDuration balanced; + if (!BalanceTimeDuration(cx, normalized, largestUnit, &balanced)) { + return false; + } + + // Step 17.c. + *result = balanced.toDuration(); + return true; } - // Steps 11-12. (Not applicable) + // Steps 13-14. + PlainDateTime startDateTime; + if (!precalculatedPlainDateTime) { + if (!GetPlainDateTimeFor(cx, timeZone, zonedRelativeTo.instant(), + &startDateTime)) { + return false; + } + } else { + startDateTime = *precalculatedPlainDateTime; + } - // Step 13. (Inlined AddZonedDateTime, step 6.) + // Step 15. + auto norm1 = + CreateNormalizedDurationRecord(one.toDateDuration(), normalized1); Instant intermediateNs; - if (!AddInstant(cx, zonedRelativeTo.instant(), one, &intermediateNs)) { + if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, + norm1, startDateTime, &intermediateNs)) { return false; } MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); - // Step 14. (Inlined AddZonedDateTime, step 6.) + // Step 16. + auto norm2 = + CreateNormalizedDurationRecord(two.toDateDuration(), normalized2); Instant endNs; - if (!AddInstant(cx, intermediateNs, two, &endNs)) { + if (!AddZonedDateTime(cx, intermediateNs, timeZone, calendar, norm2, + &endNs)) { return false; } MOZ_ASSERT(IsValidEpochInstant(endNs)); - // Steps 15.a-b. - return DifferenceInstant(cx, zonedRelativeTo.instant(), endNs, Increment{1}, - TemporalUnit::Nanosecond, largestUnit, - TemporalRoundingMode::HalfExpand, result); + // Step 17. (Not applicable) + + // Step 18. + NormalizedDuration difference; + if (!DifferenceZonedDateTime(cx, zonedRelativeTo.instant(), endNs, timeZone, + calendar, largestUnit, startDateTime, + &difference)) { + return false; + } + + // Step 19. + auto balanced = BalanceTimeDuration(difference.time, TemporalUnit::Hour); + + // Step 20. + *result = { + double(difference.date.years), double(difference.date.months), + double(difference.date.weeks), double(difference.date.days), + double(balanced.hours), double(balanced.minutes), + double(balanced.seconds), double(balanced.milliseconds), + balanced.microseconds, balanced.nanoseconds, + }; + MOZ_ASSERT(IsValidDuration(*result)); + return true; } /** @@ -2421,214 +2421,18 @@ static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, mozilla::Nothing(), result); } -static bool RoundDuration(JSContext* cx, int64_t totalNanoseconds, - TemporalUnit unit, Increment increment, - TemporalRoundingMode roundingMode, Duration* result) { - MOZ_ASSERT(unit >= TemporalUnit::Hour); - - double rounded; - if (!RoundNumberToIncrement(cx, totalNanoseconds, unit, increment, - roundingMode, &rounded)) { - return false; - } - - double hours = 0; - double minutes = 0; - double seconds = 0; - double milliseconds = 0; - double microseconds = 0; - double nanoseconds = 0; - - switch (unit) { - case TemporalUnit::Auto: - case TemporalUnit::Year: - case TemporalUnit::Week: - case TemporalUnit::Month: - case TemporalUnit::Day: - MOZ_CRASH("Unexpected temporal unit"); - - case TemporalUnit::Hour: - hours = rounded; - break; - case TemporalUnit::Minute: - minutes = rounded; - break; - case TemporalUnit::Second: - seconds = rounded; - break; - case TemporalUnit::Millisecond: - milliseconds = rounded; - break; - case TemporalUnit::Microsecond: - microseconds = rounded; - break; - case TemporalUnit::Nanosecond: - nanoseconds = rounded; - break; - } - - *result = { - 0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, - nanoseconds, - }; - return ThrowIfInvalidDuration(cx, *result); -} - -static bool RoundDuration(JSContext* cx, Handle<BigInt*> totalNanoseconds, - TemporalUnit unit, Increment increment, - TemporalRoundingMode roundingMode, Duration* result) { - MOZ_ASSERT(unit >= TemporalUnit::Hour); - - double rounded; - if (!RoundNumberToIncrement(cx, totalNanoseconds, unit, increment, - roundingMode, &rounded)) { - return false; - } - - double hours = 0; - double minutes = 0; - double seconds = 0; - double milliseconds = 0; - double microseconds = 0; - double nanoseconds = 0; - - switch (unit) { - case TemporalUnit::Auto: - case TemporalUnit::Year: - case TemporalUnit::Week: - case TemporalUnit::Month: - case TemporalUnit::Day: - MOZ_CRASH("Unexpected temporal unit"); - - case TemporalUnit::Hour: - hours = rounded; - break; - case TemporalUnit::Minute: - minutes = rounded; - break; - case TemporalUnit::Second: - seconds = rounded; - break; - case TemporalUnit::Millisecond: - milliseconds = rounded; - break; - case TemporalUnit::Microsecond: - microseconds = rounded; - break; - case TemporalUnit::Nanosecond: - nanoseconds = rounded; - break; - } - - *result = { - 0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, - nanoseconds, - }; - return ThrowIfInvalidDuration(cx, *result); -} - /** - * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, increment, unit, - * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, - * precalculatedPlainDateTime ) - */ -static bool AdjustRoundedDurationDaysSlow( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle<ZonedDateTime> zonedRelativeTo, Handle<CalendarRecord> calendar, - Handle<TimeZoneRecord> timeZone, - mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, - InstantSpan dayLength, Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - MOZ_ASSERT(IsValidInstantSpan(dayLength)); - - // Step 3. - Rooted<BigInt*> timeRemainderNs( - cx, TotalDurationNanosecondsSlow(cx, duration.time())); - if (!timeRemainderNs) { - return false; - } - - // Steps 4-6. - int32_t direction = timeRemainderNs->sign(); - - // Steps 7-10. (Computed in caller) - - // Step 11. - Rooted<BigInt*> dayLengthNs(cx, ToEpochNanoseconds(cx, dayLength)); - if (!dayLengthNs) { - return false; - } - MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); - - // Step 12. - Rooted<BigInt*> oneDayLess(cx, BigInt::sub(cx, timeRemainderNs, dayLengthNs)); - if (!oneDayLess) { - return false; - } - - // Step 13. - if ((direction > 0 && oneDayLess->sign() < 0) || - (direction < 0 && oneDayLess->sign() > 0)) { - *result = duration; - return true; - } - - // Step 14. - Duration adjustedDateDuration; - if (!AddDuration(cx, - { - duration.years, - duration.months, - duration.weeks, - duration.days, - }, - {0, 0, 0, double(direction)}, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, - &adjustedDateDuration)) { - return false; - } - - // Step 15. - Duration roundedTimeDuration; - if (!RoundDuration(cx, oneDayLess, unit, increment, roundingMode, - &roundedTimeDuration)) { - return false; - } - - // Step 16. - TimeDuration adjustedTimeDuration; - if (!BalanceTimeDuration(cx, roundedTimeDuration, TemporalUnit::Hour, - &adjustedTimeDuration)) { - return false; - } - - // Step 17. - *result = { - adjustedDateDuration.years, adjustedDateDuration.months, - adjustedDateDuration.weeks, adjustedDateDuration.days, - adjustedTimeDuration.hours, adjustedTimeDuration.minutes, - adjustedTimeDuration.seconds, adjustedTimeDuration.milliseconds, - adjustedTimeDuration.microseconds, adjustedTimeDuration.nanoseconds, - }; - MOZ_ASSERT(IsValidDuration(*result)); - return true; -} - -/** - * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, increment, unit, - * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, + * AdjustRoundedDurationDays ( years, months, weeks, days, norm, increment, + * unit, roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, * precalculatedPlainDateTime ) */ static bool AdjustRoundedDurationDays( - JSContext* cx, const Duration& duration, Increment increment, + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, Handle<ZonedDateTime> zonedRelativeTo, Handle<CalendarRecord> calendar, Handle<TimeZoneRecord> timeZone, mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, - Duration* result) { + NormalizedDuration* result) { MOZ_ASSERT(IsValidDuration(duration)); // Step 1. @@ -2644,27 +2448,25 @@ static bool AdjustRoundedDurationDays( // Step 2. MOZ_ASSERT(precalculatedPlainDateTime); - // Steps 4-6. - // - // Step 3 is moved below, so compute |direction| through DurationSign. - int32_t direction = DurationSign(duration.time()); + // Step 3. + int32_t direction = NormalizedTimeDurationSign(duration.time); - // Steps 7-8. + // Steps 4-5. Instant dayStart; if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, - duration.date(), *precalculatedPlainDateTime, + duration.date, *precalculatedPlainDateTime, &dayStart)) { return false; } MOZ_ASSERT(IsValidEpochInstant(dayStart)); - // Step 9. + // Step 6. PlainDateTime dayStartDateTime; if (!GetPlainDateTimeFor(cx, timeZone, dayStart, &dayStartDateTime)) { return false; } - // Step 10. + // Step 7. Instant dayEnd; if (!AddDaysToZonedDateTime(cx, dayStart, dayStartDateTime, timeZone, zonedRelativeTo.calendar(), direction, &dayEnd)) { @@ -2672,153 +2474,74 @@ static bool AdjustRoundedDurationDays( } MOZ_ASSERT(IsValidEpochInstant(dayEnd)); - // Step 11. - auto dayLength = dayEnd - dayStart; - MOZ_ASSERT(IsValidInstantSpan(dayLength)); - - // Step 3. (Reordered) - auto timeRemainderNs = TotalDurationNanoseconds(duration.time()); - if (!timeRemainderNs) { - return AdjustRoundedDurationDaysSlow( - cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, dayLength, result); - } + // Step 8. + auto dayLengthNs = + NormalizedTimeDurationFromEpochNanosecondsDifference(dayEnd, dayStart); + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs.to<InstantSpan>())); - // Step 12. - auto checkedOneDayLess = *timeRemainderNs - dayLength.toNanoseconds(); - if (!checkedOneDayLess.isValid()) { - return AdjustRoundedDurationDaysSlow( - cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, dayLength, result); + // Step 9. + NormalizedTimeDuration oneDayLess; + if (!SubtractNormalizedTimeDuration(cx, duration.time, dayLengthNs, + &oneDayLess)) { + return false; } - auto oneDayLess = checkedOneDayLess.value(); - // Step 13. - if ((direction > 0 && oneDayLess < 0) || (direction < 0 && oneDayLess > 0)) { + // Step 10. + int32_t oneDayLessSign = NormalizedTimeDurationSign(oneDayLess); + if ((direction > 0 && oneDayLessSign < 0) || + (direction < 0 && oneDayLessSign > 0)) { *result = duration; return true; } - // Step 14. + // Step 11. Duration adjustedDateDuration; - if (!AddDuration(cx, - { - duration.years, - duration.months, - duration.weeks, - duration.days, - }, - {0, 0, 0, double(direction)}, zonedRelativeTo, calendar, - timeZone, precalculatedPlainDateTime, - &adjustedDateDuration)) { - return false; - } - - // Step 15. - Duration roundedTimeDuration; - if (!RoundDuration(cx, oneDayLess, unit, increment, roundingMode, - &roundedTimeDuration)) { + if (!AddDuration(cx, duration.date.toDuration(), {0, 0, 0, double(direction)}, + zonedRelativeTo, calendar, timeZone, + precalculatedPlainDateTime, &adjustedDateDuration)) { return false; } - // Step 16. - TimeDuration adjustedTimeDuration; - if (!BalanceTimeDuration(cx, roundedTimeDuration, TemporalUnit::Hour, - &adjustedTimeDuration)) { + // Step 12. + NormalizedTimeDuration roundedTime; + if (!RoundDuration(cx, oneDayLess, increment, unit, roundingMode, + &roundedTime)) { return false; } - // FIXME: spec bug - CreateDurationRecord is fallible because the adjusted - // date and time durations can be have different signs. - // https://github.com/tc39/proposal-temporal/issues/2536 - // - // clang-format off - // - // { - // let calendar = new class extends Temporal.Calendar { - // dateAdd(date, duration, options) { - // console.log(`dateAdd(${date}, ${duration})`); - // if (duration.days === 10) { - // return super.dateAdd(date, duration.negated(), options); - // } - // return super.dateAdd(date, duration, options); - // } - // }("iso8601"); - // - // let zdt = new Temporal.ZonedDateTime(0n, "UTC", calendar); - // - // let d = Temporal.Duration.from({ - // days: 10, - // hours: 25, - // }); - // - // let r = d.round({ - // smallestUnit: "nanoseconds", - // roundingIncrement: 5, - // relativeTo: zdt, - // }); - // console.log(r.toString()); - // } - // - // clang-format on - - // Step 17. - *result = { - adjustedDateDuration.years, adjustedDateDuration.months, - adjustedDateDuration.weeks, adjustedDateDuration.days, - adjustedTimeDuration.hours, adjustedTimeDuration.minutes, - adjustedTimeDuration.seconds, adjustedTimeDuration.milliseconds, - adjustedTimeDuration.microseconds, adjustedTimeDuration.nanoseconds, - }; - return ThrowIfInvalidDuration(cx, *result); + // Step 13. + return CombineDateAndNormalizedTimeDuration( + cx, adjustedDateDuration.toDateDuration(), roundedTime, result); } /** - * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, increment, unit, - * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, + * AdjustRoundedDurationDays ( years, months, weeks, days, norm, increment, + * unit, roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, * precalculatedPlainDateTime ) */ bool js::temporal::AdjustRoundedDurationDays( - JSContext* cx, const Duration& duration, Increment increment, + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, Handle<ZonedDateTime> zonedRelativeTo, Handle<CalendarRecord> calendar, Handle<TimeZoneRecord> timeZone, - const PlainDateTime& precalculatedPlainDateTime, Duration* result) { + const PlainDateTime& precalculatedPlainDateTime, + NormalizedDuration* result) { return ::AdjustRoundedDurationDays( cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, timeZone, mozilla::SomeRef(precalculatedPlainDateTime), result); } -static bool BigIntToStringBuilder(JSContext* cx, Handle<BigInt*> num, - JSStringBuilder& sb) { - MOZ_ASSERT(!num->isNegative()); - - JSLinearString* str = BigInt::toString<CanGC>(cx, num, 10); - if (!str) { - return false; - } - return sb.append(str); -} - static bool NumberToStringBuilder(JSContext* cx, double num, JSStringBuilder& sb) { MOZ_ASSERT(IsInteger(num)); MOZ_ASSERT(num >= 0); + MOZ_ASSERT(num < DOUBLE_INTEGRAL_PRECISION_LIMIT); - if (num < DOUBLE_INTEGRAL_PRECISION_LIMIT) { - ToCStringBuf cbuf; - size_t length; - const char* numStr = NumberToCString(&cbuf, num, &length); - - return sb.append(numStr, length); - } + ToCStringBuf cbuf; + size_t length; + const char* numStr = NumberToCString(&cbuf, num, &length); - Rooted<BigInt*> bi(cx, BigInt::createFromDouble(cx, num)); - if (!bi) { - return false; - } - return BigIntToStringBuilder(cx, bi, sb); + return sb.append(numStr, length); } static Duration AbsoluteDuration(const Duration& duration) { @@ -2853,7 +2576,7 @@ static Duration AbsoluteDuration(const Duration& duration) { } // Steps 1.b-c. - uint32_t k = 100'000'000; + int32_t k = 100'000'000; do { if (!result.append(char('0' + (subSecondNanoseconds / k)))) { return false; @@ -2874,7 +2597,7 @@ static Duration AbsoluteDuration(const Duration& duration) { } // Steps 2.b-c. - uint32_t k = 100'000'000; + int32_t k = 100'000'000; for (uint8_t i = 0; i < precision.value(); i++) { if (!result.append(char('0' + (subSecondNanoseconds / k)))) { return false; @@ -2889,7 +2612,7 @@ static Duration AbsoluteDuration(const Duration& duration) { /** * TemporalDurationToString ( years, months, weeks, days, hours, minutes, - * seconds, milliseconds, microseconds, nanoseconds, precision ) + * normSeconds, precision ) */ static JSString* TemporalDurationToString(JSContext* cx, const Duration& duration, @@ -2897,187 +2620,49 @@ static JSString* TemporalDurationToString(JSContext* cx, MOZ_ASSERT(IsValidDuration(duration)); MOZ_ASSERT(precision != Precision::Minute()); - // Convert to absolute values up front. This is okay to do, because when the - // duration is valid, all components have the same sign. - const auto& [years, months, weeks, days, hours, minutes, seconds, - milliseconds, microseconds, nanoseconds] = - AbsoluteDuration(duration); - // Fast path for zero durations. - if (years == 0 && months == 0 && weeks == 0 && days == 0 && hours == 0 && - minutes == 0 && seconds == 0 && milliseconds == 0 && microseconds == 0 && - nanoseconds == 0 && + if (duration == Duration{} && (precision == Precision::Auto() || precision.value() == 0)) { return NewStringCopyZ<CanGC>(cx, "PT0S"); } - Rooted<BigInt*> totalSecondsBigInt(cx); - double totalSeconds = seconds; - int32_t fraction = 0; - if (milliseconds != 0 || microseconds != 0 || nanoseconds != 0) { - bool imprecise = false; - do { - int64_t sec; - int64_t milli; - int64_t micro; - int64_t nano; - if (!mozilla::NumberEqualsInt64(seconds, &sec) || - !mozilla::NumberEqualsInt64(milliseconds, &milli) || - !mozilla::NumberEqualsInt64(microseconds, µ) || - !mozilla::NumberEqualsInt64(nanoseconds, &nano)) { - imprecise = true; - break; - } - - mozilla::CheckedInt64 intermediate; - - // Step 2. - intermediate = micro; - intermediate += (nano / 1000); - if (!intermediate.isValid()) { - imprecise = true; - break; - } - micro = intermediate.value(); - - // Step 3. - nano %= 1000; - - // Step 4. - intermediate = milli; - intermediate += (micro / 1000); - if (!intermediate.isValid()) { - imprecise = true; - break; - } - milli = intermediate.value(); - - // Step 5. - micro %= 1000; - - // Step 6. - intermediate = sec; - intermediate += (milli / 1000); - if (!intermediate.isValid()) { - imprecise = true; - break; - } - sec = intermediate.value(); - - // Step 7. - milli %= 1000; - - if (sec < int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { - totalSeconds = double(sec); - } else { - totalSecondsBigInt = BigInt::createFromInt64(cx, sec); - if (!totalSecondsBigInt) { - return nullptr; - } - } - - // These are now all in the range [0, 999]. - MOZ_ASSERT(0 <= milli && milli <= 999); - MOZ_ASSERT(0 <= micro && micro <= 999); - MOZ_ASSERT(0 <= nano && nano <= 999); - - // Step 20.b. (Reordered) - fraction = milli * 1'000'000 + micro * 1'000 + nano; - MOZ_ASSERT(0 <= fraction && fraction < 1'000'000'000); - } while (false); - - // If a result was imprecise, recompute with BigInt to get full precision. - if (imprecise) { - Rooted<BigInt*> secs(cx, BigInt::createFromDouble(cx, seconds)); - if (!secs) { - return nullptr; - } - - Rooted<BigInt*> millis(cx, BigInt::createFromDouble(cx, milliseconds)); - if (!millis) { - return nullptr; - } - - Rooted<BigInt*> micros(cx, BigInt::createFromDouble(cx, microseconds)); - if (!micros) { - return nullptr; - } - - Rooted<BigInt*> nanos(cx, BigInt::createFromDouble(cx, nanoseconds)); - if (!nanos) { - return nullptr; - } - - Rooted<BigInt*> thousand(cx, BigInt::createFromInt64(cx, 1000)); - if (!thousand) { - return nullptr; - } - - // Steps 2-3. - Rooted<BigInt*> quotient(cx); - if (!BigInt::divmod(cx, nanos, thousand, "ient, &nanos)) { - return nullptr; - } - - micros = BigInt::add(cx, micros, quotient); - if (!micros) { - return nullptr; - } - - // Steps 4-5. - if (!BigInt::divmod(cx, micros, thousand, "ient, µs)) { - return nullptr; - } - - millis = BigInt::add(cx, millis, quotient); - if (!millis) { - return nullptr; - } - - // Steps 6-7. - if (!BigInt::divmod(cx, millis, thousand, "ient, &millis)) { - return nullptr; - } - - totalSecondsBigInt = BigInt::add(cx, secs, quotient); - if (!totalSecondsBigInt) { - return nullptr; - } + // Convert to absolute values up front. This is okay to do, because when the + // duration is valid, all components have the same sign. + const auto& [years, months, weeks, days, hours, minutes, seconds, + milliseconds, microseconds, nanoseconds] = + AbsoluteDuration(duration); - // These are now all in the range [0, 999]. - int64_t milli = BigInt::toInt64(millis); - int64_t micro = BigInt::toInt64(micros); - int64_t nano = BigInt::toInt64(nanos); + // Years to seconds parts are all safe integers for valid durations. + MOZ_ASSERT(years < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(months < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(weeks < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(days < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(hours < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(minutes < DOUBLE_INTEGRAL_PRECISION_LIMIT); + MOZ_ASSERT(seconds < DOUBLE_INTEGRAL_PRECISION_LIMIT); - MOZ_ASSERT(0 <= milli && milli <= 999); - MOZ_ASSERT(0 <= micro && micro <= 999); - MOZ_ASSERT(0 <= nano && nano <= 999); + auto secondsDuration = NormalizeTimeDuration(0.0, 0.0, seconds, milliseconds, + microseconds, nanoseconds); - // Step 20.b. (Reordered) - fraction = milli * 1'000'000 + micro * 1'000 + nano; - MOZ_ASSERT(0 <= fraction && fraction < 1'000'000'000); - } - } + // Step 1. + int32_t sign = DurationSign(duration); - // Steps 8 and 13. + // Steps 2 and 7. JSStringBuilder result(cx); - // Step 1. (Reordered) - int32_t sign = DurationSign(duration); - - // Step 21. (Reordered) + // Step 13. (Reordered) if (sign < 0) { if (!result.append('-')) { return nullptr; } } - // Step 22. (Reordered) + // Step 14. (Reordered) if (!result.append('P')) { return nullptr; } - // Step 9. + // Step 3. if (years != 0) { if (!NumberToStringBuilder(cx, years, result)) { return nullptr; @@ -3087,7 +2672,7 @@ static JSString* TemporalDurationToString(JSContext* cx, } } - // Step 10. + // Step 4. if (months != 0) { if (!NumberToStringBuilder(cx, months, result)) { return nullptr; @@ -3097,7 +2682,7 @@ static JSString* TemporalDurationToString(JSContext* cx, } } - // Step 11. + // Step 5. if (weeks != 0) { if (!NumberToStringBuilder(cx, weeks, result)) { return nullptr; @@ -3107,7 +2692,7 @@ static JSString* TemporalDurationToString(JSContext* cx, } } - // Step 12. + // Step 6. if (days != 0) { if (!NumberToStringBuilder(cx, days, result)) { return nullptr; @@ -3117,29 +2702,22 @@ static JSString* TemporalDurationToString(JSContext* cx, } } - // Steps 16-17. - bool nonzeroSecondsAndLower = seconds != 0 || milliseconds != 0 || - microseconds != 0 || nanoseconds != 0; - MOZ_ASSERT(nonzeroSecondsAndLower == - (totalSeconds != 0 || - (totalSecondsBigInt && !totalSecondsBigInt->isZero()) || - fraction != 0)); + // Step 7. (Moved above) - // Steps 18-19. + // Steps 10-11. (Reordered) bool zeroMinutesAndHigher = years == 0 && months == 0 && weeks == 0 && days == 0 && hours == 0 && minutes == 0; - // Step 20. (if-condition) - bool hasSecondsPart = nonzeroSecondsAndLower || zeroMinutesAndHigher || - precision != Precision::Auto(); - + // Steps 8-9, 12, and 15. + bool hasSecondsPart = (secondsDuration != NormalizedTimeDuration{}) || + zeroMinutesAndHigher || precision != Precision::Auto(); if (hours != 0 || minutes != 0 || hasSecondsPart) { - // Step 23. (Reordered) + // Step 15. (Reordered) if (!result.append('T')) { return nullptr; } - // Step 14. + // Step 8. if (hours != 0) { if (!NumberToStringBuilder(cx, hours, result)) { return nullptr; @@ -3149,7 +2727,7 @@ static JSString* TemporalDurationToString(JSContext* cx, } } - // Step 15. + // Step 9. if (minutes != 0) { if (!NumberToStringBuilder(cx, minutes, result)) { return nullptr; @@ -3159,34 +2737,29 @@ static JSString* TemporalDurationToString(JSContext* cx, } } - // Step 20. + // Step 12. if (hasSecondsPart) { - // Step 20.a. - if (totalSecondsBigInt) { - if (!BigIntToStringBuilder(cx, totalSecondsBigInt, result)) { - return nullptr; - } - } else { - if (!NumberToStringBuilder(cx, totalSeconds, result)) { - return nullptr; - } + // Step 12.a. + if (!NumberToStringBuilder(cx, double(secondsDuration.seconds), result)) { + return nullptr; } - // Step 20.b. (Moved above) - - // Step 20.c. - if (!FormatFractionalSeconds(result, fraction, precision)) { + // Step 12.b. + if (!FormatFractionalSeconds(result, secondsDuration.nanoseconds, + precision)) { return nullptr; } - // Step 20.d. + // Step 12.c. if (!result.append('S')) { return nullptr; } } } - // Step 24. + // Steps 13-15. (Moved above) + + // Step 16. return result.finishString(); } @@ -3206,9 +2779,6 @@ static bool ToRelativeTemporalObject( // Step 2. if (value.isUndefined()) { - // FIXME: spec issue - switch return record fields for consistency. - // FIXME: spec bug - [[TimeZoneRec]] field not created - plainRelativeTo.set(nullptr); zonedRelativeTo.set(ZonedDateTime{}); timeZoneRecord.set(TimeZoneRecord{}); @@ -3414,20 +2984,20 @@ static bool ToRelativeTemporalObject( bool isUTC; bool hasOffset; int64_t timeZoneOffset; - Rooted<ParsedTimeZone> timeZoneName(cx); + Rooted<ParsedTimeZone> timeZoneAnnotation(cx); Rooted<JSString*> calendarString(cx); if (!ParseTemporalRelativeToString(cx, string, &dateTime, &isUTC, &hasOffset, &timeZoneOffset, - &timeZoneName, &calendarString)) { + &timeZoneAnnotation, &calendarString)) { return false; } // Step 6.c. (Not applicable in our implementation.) // Steps 6.e-f. - if (timeZoneName) { + if (timeZoneAnnotation) { // Step 6.f.i. - if (!ToTemporalTimeZone(cx, timeZoneName, &timeZone)) { + if (!ToTemporalTimeZone(cx, timeZoneAnnotation, &timeZone)) { return false; } @@ -3545,820 +3115,216 @@ static bool CreateCalendarMethodsRecordFromRelativeTo( return true; } -static constexpr bool IsSafeInteger(int64_t x) { - constexpr int64_t MaxSafeInteger = int64_t(1) << 53; - constexpr int64_t MinSafeInteger = -MaxSafeInteger; - return MinSafeInteger < x && x < MaxSafeInteger; -} +struct RoundedDuration final { + NormalizedDuration duration; + double total = 0; +}; -/** - * RoundNumberToIncrement ( x, increment, roundingMode ) - */ -static void TruncateNumber(int64_t numerator, int64_t denominator, - double* quotient, double* total) { - // Computes the quotient and real number value of the rational number - // |numerator / denominator|. - - // Int64 division truncates. - int64_t q = numerator / denominator; - int64_t r = numerator % denominator; - - // The total value is stored as a mathematical number in the draft proposal, - // so we can't convert it to a double without loss of precision. We use two - // different approaches to compute the total value based on the input range. - // - // For example: - // - // When |numerator = 1000001| and |denominator = 60 * 1000|, the exact result - // is |16.66668333...| and the best possible approximation is - // |16.666683333333335070...𝔽|. We can this approximation when casting both - // numerator and denominator to doubles and then performing a double division. - // - // When |numerator = 14400000000000001| and |denominator = 3600000000000|, we - // can't use double division, because |14400000000000001| can't be represented - // as an exact double value. The exact result is |4000.0000000000002777...|. - // - // The best possible approximation is |4000.0000000000004547...𝔽|, which can - // be computed through |q + r / denominator|. - if (::IsSafeInteger(numerator) && ::IsSafeInteger(denominator)) { - *quotient = double(q); - *total = double(numerator) / double(denominator); - } else { - *quotient = double(q); - *total = double(q) + double(r) / double(denominator); - } -} +enum class ComputeRemainder : bool { No, Yes }; /** - * RoundNumberToIncrement ( x, increment, roundingMode ) + * RoundNormalizedTimeDurationToIncrement ( d, increment, roundingMode ) */ -static bool TruncateNumber(JSContext* cx, Handle<BigInt*> numerator, - Handle<BigInt*> denominator, double* quotient, - double* total) { - MOZ_ASSERT(!denominator->isNegative()); - MOZ_ASSERT(!denominator->isZero()); - - // Dividing zero is always zero. - if (numerator->isZero()) { - *quotient = 0; - *total = 0; - return true; - } +static NormalizedTimeDuration RoundNormalizedTimeDurationToIncrement( + const NormalizedTimeDuration& duration, const TemporalUnit unit, + Increment increment, TemporalRoundingMode roundingMode) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); + MOZ_ASSERT(increment <= MaximumTemporalDurationRoundingIncrement(unit)); - int64_t num, denom; - if (BigInt::isInt64(numerator, &num) && - BigInt::isInt64(denominator, &denom)) { - TruncateNumber(num, denom, quotient, total); - return true; - } + int64_t divisor = ToNanoseconds(unit) * increment.value(); + MOZ_ASSERT(divisor > 0); + MOZ_ASSERT(divisor <= ToNanoseconds(TemporalUnit::Day)); - // BigInt division truncates. - Rooted<BigInt*> quot(cx); - Rooted<BigInt*> rem(cx); - if (!BigInt::divmod(cx, numerator, denominator, ", &rem)) { - return false; - } - - double q = BigInt::numberValue(quot); - *quotient = q; - *total = q + BigInt::numberValue(rem) / BigInt::numberValue(denominator); - return true; + auto totalNanoseconds = duration.toNanoseconds(); + auto rounded = + RoundNumberToIncrement(totalNanoseconds, Int128{divisor}, roundingMode); + return NormalizedTimeDuration::fromNanoseconds(rounded); } /** - * RoundNumberToIncrement ( x, increment, roundingMode ) + * RoundNormalizedTimeDurationToIncrement ( d, increment, roundingMode ) */ -static bool TruncateNumber(JSContext* cx, const Duration& toRound, - TemporalUnit unit, double* quotient, double* total) { - MOZ_ASSERT(unit >= TemporalUnit::Day); - - int64_t denominator = ToNanoseconds(unit); - MOZ_ASSERT(denominator > 0); - MOZ_ASSERT(denominator <= 86'400'000'000'000); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto numerator = TotalDurationNanoseconds(toRound)) { - TruncateNumber(*numerator, denominator, quotient, total); - return true; - } - - Rooted<BigInt*> numerator(cx, TotalDurationNanosecondsSlow(cx, toRound)); - if (!numerator) { - return false; - } - - // Division by one has no remainder. - if (denominator == 1) { - double q = BigInt::numberValue(numerator); - *quotient = q; - *total = q; - return true; - } - - Rooted<BigInt*> denom(cx, BigInt::createFromInt64(cx, denominator)); - if (!denom) { - return false; - } +static bool RoundNormalizedTimeDurationToIncrement( + JSContext* cx, const NormalizedTimeDuration& duration, + const TemporalUnit unit, Increment increment, + TemporalRoundingMode roundingMode, NormalizedTimeDuration* result) { + // Step 1. + auto rounded = RoundNormalizedTimeDurationToIncrement( + duration, unit, increment, roundingMode); - // BigInt division truncates. - Rooted<BigInt*> quot(cx); - Rooted<BigInt*> rem(cx); - if (!BigInt::divmod(cx, numerator, denom, ", &rem)) { + // Step 2. + if (!IsValidNormalizedTimeDuration(rounded)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME); return false; } - double q = BigInt::numberValue(quot); - *quotient = q; - *total = q + BigInt::numberValue(rem) / double(denominator); + // Step 3. + *result = rounded; return true; } /** - * RoundNumberToIncrement ( x, increment, roundingMode ) + * DivideNormalizedTimeDuration ( d, divisor ) */ -static bool RoundNumberToIncrement(JSContext* cx, const Duration& toRound, - TemporalUnit unit, Increment increment, - TemporalRoundingMode roundingMode, - double* result) { - MOZ_ASSERT(unit >= TemporalUnit::Day); - - // Fast-path when we can perform the whole computation with int64 values. - if (auto total = TotalDurationNanoseconds(toRound)) { - return RoundNumberToIncrement(cx, *total, unit, increment, roundingMode, - result); - } +static double TotalNormalizedTimeDuration( + const NormalizedTimeDuration& duration, const TemporalUnit unit) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); - Rooted<BigInt*> totalNs(cx, TotalDurationNanosecondsSlow(cx, toRound)); - if (!totalNs) { - return false; - } - - return RoundNumberToIncrement(cx, totalNs, unit, increment, roundingMode, - result); + auto numerator = duration.toNanoseconds(); + auto denominator = Int128{ToNanoseconds(unit)}; + return FractionToDouble(numerator, denominator); } -struct RoundedDuration final { - Duration duration; - double total = 0; -}; - -enum class ComputeRemainder : bool { No, Yes }; - /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -static bool RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, - ComputeRemainder computeRemainder, - RoundedDuration* result) { - // The remainder is only needed when called from |Duration_total|. And `total` - // always passes |increment=1| and |roundingMode=trunc|. - MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, - increment == Increment{1}); - MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, - roundingMode == TemporalRoundingMode::Trunc); +NormalizedTimeDuration js::temporal::RoundDuration( + const NormalizedTimeDuration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); - auto [years, months, weeks, days, hours, minutes, seconds, milliseconds, - microseconds, nanoseconds] = duration; + // Steps 1-12. (Not applicable) - // Steps 1-5. (Not applicable.) - - // Step 6. - if (unit <= TemporalUnit::Week) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, - JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, - "relativeTo"); - return false; - } - - // TODO: We could directly return here if unit=nanoseconds and increment=1, - // because in that case this operation is a no-op. This case happens for - // example when calling Temporal.PlainTime.prototype.{since,until} without an - // options object. - // - // But maybe this can be even more efficiently handled in the callers. For - // example when Temporal.PlainTime.prototype.{since,until} is called without - // an options object, we can not only skip the RoundDuration call, but also - // the following BalanceTimeDuration call. - - // Step 7. (Not applicable.) - - // Step 8. (Moved below.) - - // Step 9. (Not applicable.) - - // Steps 10-19. - Duration toRound; - double* roundedTime; - switch (unit) { - case TemporalUnit::Auto: - case TemporalUnit::Year: - case TemporalUnit::Week: - case TemporalUnit::Month: - // Steps 10-12. (Not applicable.) - MOZ_CRASH("Unexpected temporal unit"); - - case TemporalUnit::Day: { - // clang-format off - // - // Relevant steps from the spec algorithm: - // - // 6.a Let nanoseconds be ! TotalDurationNanoseconds(0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, 0). - // 6.d Let result be ? NanosecondsToDays(nanoseconds, intermediate). - // 6.e Set days to days + result.[[Days]] + result.[[Nanoseconds]] / abs(result.[[DayLength]]). - // ... - // 12.a Let fractionalDays be days. - // 12.b Set days to ? RoundNumberToIncrement(days, increment, roundingMode). - // 12.c Set remainder to fractionalDays - days. - // - // Where `result.[[Days]]` is `the integral part of nanoseconds / dayLengthNs` - // and `result.[[Nanoseconds]]` is `nanoseconds modulo dayLengthNs`. - // With `dayLengthNs = 8.64 × 10^13`. - // - // So we have: - // d + r.days + (r.nanoseconds / len) - // = d + [ns / len] + ((ns % len) / len) - // = d + [ns / len] + ((ns - ([ns / len] × len)) / len) - // = d + [ns / len] + (ns / len) - (([ns / len] × len) / len) - // = d + [ns / len] + (ns / len) - [ns / len] - // = d + (ns / len) - // = ((d × len) / len) + (ns / len) - // = ((d × len) + ns) / len - // - // `((d × len) + ns)` is the result of calling TotalDurationNanoseconds(), - // which means we can use the same code for all time computations in this - // function. - // - // clang-format on - - MOZ_ASSERT(increment <= Increment{1'000'000'000}, - "limited by ToTemporalRoundingIncrement"); - - // Steps 7.a, 7.c, and 13.a-b. - toRound = duration; - roundedTime = &days; - - // Step 7.b. (Not applicable) - - // Steps 7.d-e. - hours = 0; - minutes = 0; - seconds = 0; - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; - } - - case TemporalUnit::Hour: { - MOZ_ASSERT(increment <= Increment{24}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 8 and 14.a-c. - toRound = { - 0, - 0, - 0, - 0, - hours, - minutes, - seconds, - milliseconds, - microseconds, - nanoseconds, - }; - roundedTime = &hours; - - // Step 14.d. - minutes = 0; - seconds = 0; - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; - } - - case TemporalUnit::Minute: { - MOZ_ASSERT(increment <= Increment{60}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 8 and 15.a-c. - toRound = { - 0, 0, 0, 0, 0, minutes, seconds, milliseconds, microseconds, - nanoseconds, - }; - roundedTime = &minutes; - - // Step 15.d. - seconds = 0; - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; - } - - case TemporalUnit::Second: { - MOZ_ASSERT(increment <= Increment{60}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 8 and 16.a-b. - toRound = { - 0, 0, 0, 0, 0, 0, seconds, milliseconds, microseconds, nanoseconds, - }; - roundedTime = &seconds; - - // Step 16.c. - milliseconds = 0; - microseconds = 0; - nanoseconds = 0; - break; - } - - case TemporalUnit::Millisecond: { - MOZ_ASSERT(increment <= Increment{1000}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 17.a-c. - toRound = {0, 0, 0, 0, 0, 0, 0, milliseconds, microseconds, nanoseconds}; - roundedTime = &milliseconds; - - // Step 17.d. - microseconds = 0; - nanoseconds = 0; - break; - } - - case TemporalUnit::Microsecond: { - MOZ_ASSERT(increment <= Increment{1000}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Steps 18.a-c. - toRound = {0, 0, 0, 0, 0, 0, 0, 0, microseconds, nanoseconds}; - roundedTime = µseconds; - - // Step 18.d. - nanoseconds = 0; - break; - } - - case TemporalUnit::Nanosecond: { - MOZ_ASSERT(increment <= Increment{1000}, - "limited by MaximumTemporalDurationRoundingIncrement"); - - // Step 19.a. (Implicit) - - // Steps 19.b-c. - toRound = {0, 0, 0, 0, 0, 0, 0, 0, 0, nanoseconds}; - roundedTime = &nanoseconds; - break; - } - } + // Steps 13-18. + auto rounded = RoundNormalizedTimeDurationToIncrement( + duration, unit, increment, roundingMode); + MOZ_ASSERT(IsValidNormalizedTimeDuration(rounded)); - // clang-format off - // - // The specification uses mathematical values in its computations, which - // requires to be able to represent decimals with arbitrary precision. To - // avoid having to struggle with decimals, we can transform the steps to work - // on integer values, which we can conveniently represent with BigInts. - // - // As an example here are the transformation steps for "hours", but all other - // units can be handled similarly. - // - // Relevant spec steps: - // - // 8.a Let fractionalSeconds be nanoseconds × 10^9 + microseconds × 10^6 + milliseconds × 10^3 + seconds. - // ... - // 14.a Let fractionalHours be (fractionalSeconds / 60 + minutes) / 60 + hours. - // 14.b Set hours to ? RoundNumberToIncrement(fractionalHours, increment, roundingMode). - // - // And from RoundNumberToIncrement: - // - // 1. Let quotient be x / increment. - // 2-7. Let rounded be op(quotient). - // 8. Return rounded × increment. - // - // With `fractionalHours = (totalNs / nsPerHour)`, the rounding operation - // computes: - // - // op(fractionalHours / increment) × increment - // = op((totalNs / nsPerHour) / increment) × increment - // = op(totalNs / (nsPerHour × increment)) × increment - // - // So when we pass `totalNs` and `nsPerHour` as separate arguments to - // RoundNumberToIncrement, we can avoid any precision losses and instead - // compute with exact values. - // - // clang-format on - - double total = 0; - if (computeRemainder == ComputeRemainder::No) { - if (!RoundNumberToIncrement(cx, toRound, unit, increment, roundingMode, - roundedTime)) { - return false; - } - } else { - // clang-format off - // - // The remainder is only used for Duration.prototype.total(), which calls - // this operation with increment=1 and roundingMode=trunc. - // - // That means the remainder computation is actually just - // `(totalNs % toNanos) / toNanos`, where `totalNs % toNanos` is already - // computed in RoundNumberToIncrement(): - // - // rounded = trunc(totalNs / toNanos) - // = [totalNs / toNanos] - // - // roundedTime = ℝ(𝔽(rounded)) - // - // remainder = (totalNs - (rounded * toNanos)) / toNanos - // = (totalNs - ([totalNs / toNanos] * toNanos)) / toNanos - // = (totalNs % toNanos) / toNanos - // - // When used in Duration.prototype.total(), the overall computed value is - // `[totalNs / toNanos] + (totalNs % toNanos) / toNanos`. - // - // Applying normal math rules would allow to simplify this to: - // - // [totalNs / toNanos] + (totalNs % toNanos) / toNanos - // = [totalNs / toNanos] + (totalNs - [totalNs / toNanos] * toNanos) / toNanos - // = total / toNanos - // - // We can't apply this simplification because it'd introduce double - // precision issues. Instead of that, we use a specialized version of - // RoundNumberToIncrement which directly returns the remainder. The - // remainder `(totalNs % toNanos) / toNanos` is a value near zero, so this - // approach is as exact as possible. (Double numbers near zero can be - // computed more precisely than large numbers with fractional parts.) - // - // clang-format on - - MOZ_ASSERT(increment == Increment{1}); - MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); - - if (!TruncateNumber(cx, toRound, unit, roundedTime, &total)) { - return false; - } - } - - MOZ_ASSERT(years == duration.years); - MOZ_ASSERT(months == duration.months); - MOZ_ASSERT(weeks == duration.weeks); - MOZ_ASSERT(IsIntegerOrInfinity(days)); - - // Step 20. - Duration resultDuration = {years, months, weeks, days, - hours, minutes, seconds, milliseconds, - microseconds, nanoseconds}; - if (!ThrowIfInvalidDuration(cx, resultDuration)) { - return false; - } - - // Step 21. - *result = {resultDuration, total}; - return true; + // Step 19. + return rounded; } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -static bool RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, double* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - // Only called from |Duration_total|, which always passes |increment=1| and - // |roundingMode=trunc|. - MOZ_ASSERT(increment == Increment{1}); - MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); +bool js::temporal::RoundDuration(JSContext* cx, + const NormalizedTimeDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + NormalizedTimeDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration)); + MOZ_ASSERT(unit > TemporalUnit::Day); - RoundedDuration rounded; - if (!::RoundDuration(cx, duration, increment, unit, roundingMode, - ComputeRemainder::Yes, &rounded)) { - return false; - } + // Steps 1-12. (Not applicable) - *result = rounded.total; - return true; + // Steps 13-19. + return RoundNormalizedTimeDurationToIncrement(cx, duration, unit, increment, + roundingMode, result); } -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -static bool RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); +#ifdef DEBUG +// Valid duration days are smaller than ⌈(2**53) / (24 * 60 * 60)⌉. +static constexpr int64_t MaxDurationDays = (int64_t(1) << 53) / (24 * 60 * 60); - RoundedDuration rounded; - if (!::RoundDuration(cx, duration, increment, unit, roundingMode, - ComputeRemainder::No, &rounded)) { - return false; - } +// Maximum number of days in |FractionalDays|. +static constexpr int64_t MaxFractionalDays = + 2 * MaxDurationDays + 2 * MaxEpochDaysDuration; +#endif - *result = rounded.duration; - return true; -} +struct FractionalDays final { + int64_t days = 0; + int64_t time = 0; + int64_t dayLength = 0; -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -bool js::temporal::RoundDuration(JSContext* cx, const Duration& duration, - Increment increment, TemporalUnit unit, - TemporalRoundingMode roundingMode, - Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); + FractionalDays() = default; - return ::RoundDuration(cx, duration, increment, unit, roundingMode, result); -} + explicit FractionalDays(int64_t durationDays, + const NormalizedTimeAndDays& timeAndDays) + : days(durationDays + timeAndDays.days), + time(timeAndDays.time), + dayLength(timeAndDays.dayLength) { + MOZ_ASSERT(std::abs(durationDays) <= MaxDurationDays); + MOZ_ASSERT(std::abs(timeAndDays.days) <= MaxDurationDays); + MOZ_ASSERT(std::abs(days) <= MaxFractionalDays); -static mozilla::Maybe<int64_t> DaysFrom( - const temporal::NanosecondsAndDays& nanosAndDays) { - if (auto* days = nanosAndDays.days) { - int64_t daysInt; - if (BigInt::isInt64(days, &daysInt)) { - return mozilla::Some(daysInt); - } - return mozilla::Nothing(); - } - return mozilla::Some(nanosAndDays.daysInt); -} + // NormalizedTimeDurationToDays guarantees that |dayLength| is strictly + // positive and less than 2**53. + MOZ_ASSERT(dayLength > 0); + MOZ_ASSERT(dayLength < int64_t(1) << 53); -static BigInt* DaysFrom(JSContext* cx, - Handle<temporal::NanosecondsAndDays> nanosAndDays) { - if (auto days = nanosAndDays.days()) { - return days; + // NormalizedTimeDurationToDays guarantees that |abs(timeAndDays.time)| is + // less than |timeAndDays.dayLength|. + MOZ_ASSERT(std::abs(time) < dayLength); } - return BigInt::createFromInt64(cx, nanosAndDays.daysInt()); -} - -static bool TruncateDays(JSContext* cx, - Handle<temporal::NanosecondsAndDays> nanosAndDays, - double days, int32_t daysToAdd, double* result) { - do { - int64_t intDays; - if (!mozilla::NumberEqualsInt64(days, &intDays)) { - break; - } - auto nanoDays = DaysFrom(nanosAndDays); - if (!nanoDays) { - break; - } + FractionalDays operator+=(int32_t epochDays) { + MOZ_ASSERT(std::abs(epochDays) <= MaxEpochDaysDuration); + days += epochDays; + MOZ_ASSERT(std::abs(days) <= MaxFractionalDays); + return *this; + } - auto totalDays = mozilla::CheckedInt64(intDays); - totalDays += *nanoDays; - totalDays += daysToAdd; - if (!totalDays.isValid()) { - break; - } + FractionalDays operator-=(int32_t epochDays) { + MOZ_ASSERT(std::abs(epochDays) <= MaxEpochDaysDuration); + days -= epochDays; + MOZ_ASSERT(std::abs(days) <= MaxFractionalDays); + return *this; + } - int64_t truncatedDays = totalDays.value(); - if (nanosAndDays.nanoseconds() > InstantSpan{}) { + int64_t truncate() const { + int64_t truncatedDays = days; + if (time > 0) { // Round toward positive infinity when the integer days are negative and // the fractional part is positive. if (truncatedDays < 0) { truncatedDays += 1; } - } else if (nanosAndDays.nanoseconds() < InstantSpan{}) { + } else if (time < 0) { // Round toward negative infinity when the integer days are positive and // the fractional part is negative. if (truncatedDays > 0) { truncatedDays -= 1; } } - - *result = double(truncatedDays); - return true; - } while (false); - - Rooted<BigInt*> biDays(cx, BigInt::createFromDouble(cx, days)); - if (!biDays) { - return false; - } - - Rooted<BigInt*> biNanoDays(cx, DaysFrom(cx, nanosAndDays)); - if (!biNanoDays) { - return false; - } - - Rooted<BigInt*> biDaysToAdd(cx, BigInt::createFromInt64(cx, daysToAdd)); - if (!biDaysToAdd) { - return false; - } - - Rooted<BigInt*> truncatedDays(cx, BigInt::add(cx, biDays, biNanoDays)); - if (!truncatedDays) { - return false; - } - - truncatedDays = BigInt::add(cx, truncatedDays, biDaysToAdd); - if (!truncatedDays) { - return false; + MOZ_ASSERT(std::abs(truncatedDays) <= MaxFractionalDays + 1); + return truncatedDays; } - if (nanosAndDays.nanoseconds() > InstantSpan{}) { - // Round toward positive infinity when the integer days are negative and - // the fractional part is positive. - if (truncatedDays->isNegative()) { - truncatedDays = BigInt::inc(cx, truncatedDays); - if (!truncatedDays) { - return false; - } - } - } else if (nanosAndDays.nanoseconds() < InstantSpan{}) { - // Round toward negative infinity when the integer days are positive and - // the fractional part is negative. - if (!truncatedDays->isNegative() && !truncatedDays->isZero()) { - truncatedDays = BigInt::dec(cx, truncatedDays); - if (!truncatedDays) { - return false; - } + int32_t sign() const { + if (days != 0) { + return days < 0 ? -1 : 1; } + return time < 0 ? -1 : time > 0 ? 1 : 0; } - - *result = BigInt::numberValue(truncatedDays); - return true; -} - -static bool DaysIsNegative(double days, - Handle<temporal::NanosecondsAndDays> nanosAndDays, - int32_t daysToAdd) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; - - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); - - // We don't need the exact value, so it's safe to convert from BigInt. - double nanoDays = nanosAndDays.daysNumber(); - - // When non-zero |days| and |nanoDays| have oppositive signs, the absolute - // value of |days| is less-or-equal to |epochDays|. That means when adding - // |days + nanoDays| we don't have to worry about a case like: - // - // days = 9007199254740991 and - // nanoDays = 𝔽(-9007199254740993) = -9007199254740992 - // - // ℝ(𝔽(days) + 𝔽(nanoDays)) is -1, whereas the correct result is -2. - MOZ_ASSERT((days <= 0 && nanoDays <= 0) || (days >= 0 && nanoDays >= 0) || - std::abs(days) <= epochDays); - - // This addition can be imprecise, so |daysApproximation| is only an - // approximation of the actual value. - double daysApproximation = days + nanoDays; - - if (std::abs(daysApproximation) <= epochDays * 2) { - int32_t intDays = int32_t(daysApproximation) + daysToAdd; - return intDays < 0 || - (intDays == 0 && nanosAndDays.nanoseconds() < InstantSpan{}); - } - - // |daysApproximation| is too large, adding |daysToAdd| and |daysToSubtract| - // doesn't change the sign. - return daysApproximation < 0; -} - -struct RoundedNumber { - double rounded; - double total; }; -static bool RoundNumberToIncrementSlow( - JSContext* cx, double durationAmount, double amountPassed, - double durationDays, int32_t daysToAdd, - Handle<temporal::NanosecondsAndDays> nanosAndDays, int32_t oneUnitDays, - Increment increment, TemporalRoundingMode roundingMode, - ComputeRemainder computeRemainder, RoundedNumber* result) { - MOZ_ASSERT(nanosAndDays.dayLength() > InstantSpan{}); - MOZ_ASSERT(nanosAndDays.nanoseconds().abs() < nanosAndDays.dayLength().abs()); - MOZ_ASSERT(oneUnitDays != 0); - - Rooted<BigInt*> biAmount(cx, BigInt::createFromDouble(cx, durationAmount)); - if (!biAmount) { - return false; - } - - Rooted<BigInt*> biAmountPassed(cx, - BigInt::createFromDouble(cx, amountPassed)); - if (!biAmountPassed) { - return false; - } +struct Fraction final { + int64_t numerator = 0; + int32_t denominator = 0; - biAmount = BigInt::add(cx, biAmount, biAmountPassed); - if (!biAmount) { - return false; - } + constexpr Fraction() = default; - Rooted<BigInt*> days(cx, BigInt::createFromDouble(cx, durationDays)); - if (!days) { - return false; - } - - Rooted<BigInt*> nanoDays(cx, DaysFrom(cx, nanosAndDays)); - if (!nanoDays) { - return false; - } - - Rooted<BigInt*> biDaysToAdd(cx, BigInt::createFromInt64(cx, daysToAdd)); - if (!biDaysToAdd) { - return false; - } - - days = BigInt::add(cx, days, nanoDays); - if (!days) { - return false; - } - - days = BigInt::add(cx, days, biDaysToAdd); - if (!days) { - return false; - } - - Rooted<BigInt*> nanoseconds( - cx, ToEpochNanoseconds(cx, nanosAndDays.nanoseconds())); - if (!nanoseconds) { - return false; - } - - Rooted<BigInt*> dayLength(cx, - ToEpochNanoseconds(cx, nanosAndDays.dayLength())); - if (!dayLength) { - return false; - } - - Rooted<BigInt*> denominator( - cx, BigInt::createFromInt64(cx, std::abs(oneUnitDays))); - if (!denominator) { - return false; - } - - denominator = BigInt::mul(cx, denominator, dayLength); - if (!denominator) { - return false; - } - - Rooted<BigInt*> totalNanoseconds(cx, BigInt::mul(cx, days, dayLength)); - if (!totalNanoseconds) { - return false; - } - - totalNanoseconds = BigInt::add(cx, totalNanoseconds, nanoseconds); - if (!totalNanoseconds) { - return false; - } - - Rooted<BigInt*> amountNanos(cx, BigInt::mul(cx, biAmount, denominator)); - if (!amountNanos) { - return false; - } - - totalNanoseconds = BigInt::add(cx, totalNanoseconds, amountNanos); - if (!totalNanoseconds) { - return false; + constexpr Fraction(int64_t numerator, int32_t denominator) + : numerator(numerator), denominator(denominator) { + MOZ_ASSERT(denominator > 0); } +}; - double rounded; +struct RoundedNumber final { + Int128 rounded; double total = 0; - if (computeRemainder == ComputeRemainder::No) { - if (!temporal::RoundNumberToIncrement(cx, totalNanoseconds, denominator, - increment, roundingMode, &rounded)) { - return false; - } - } else { - if (!::TruncateNumber(cx, totalNanoseconds, denominator, &rounded, - &total)) { - return false; - } - } - - *result = {rounded, total}; - return true; -} +}; -static bool RoundNumberToIncrement( - JSContext* cx, double durationAmount, double amountPassed, - double durationDays, int32_t daysToAdd, - Handle<temporal::NanosecondsAndDays> nanosAndDays, int32_t oneUnitDays, +static RoundedNumber RoundNumberToIncrement( + const Fraction& fraction, const FractionalDays& fractionalDays, Increment increment, TemporalRoundingMode roundingMode, - ComputeRemainder computeRemainder, RoundedNumber* result) { - MOZ_ASSERT(nanosAndDays.dayLength() > InstantSpan{}); - MOZ_ASSERT(nanosAndDays.nanoseconds().abs() < nanosAndDays.dayLength().abs()); - MOZ_ASSERT(oneUnitDays != 0); - - // TODO(anba): Rename variables. + ComputeRemainder computeRemainder) { + MOZ_ASSERT(std::abs(fraction.numerator) < (int64_t(1) << 32) * 2); + MOZ_ASSERT(fraction.denominator > 0); + MOZ_ASSERT(fraction.denominator <= MaxEpochDaysDuration); + MOZ_ASSERT(std::abs(fractionalDays.days) <= MaxFractionalDays); + MOZ_ASSERT(fractionalDays.dayLength > 0); + MOZ_ASSERT(fractionalDays.dayLength < (int64_t(1) << 53)); + MOZ_ASSERT(std::abs(fractionalDays.time) < fractionalDays.dayLength); + MOZ_ASSERT(increment <= Increment::max()); // clang-format off // @@ -4373,7 +3339,7 @@ static bool RoundNumberToIncrement( // // where days' = days + nanoseconds / dayLength. // - // The fractional part |nanoseconds / dayLength| is from step 4. + // The fractional part |nanoseconds / dayLength| is from step 7. // // The denominator for |fractionalWeeks| is |dayLength * abs(oneWeekDays)|. // @@ -4382,236 +3348,259 @@ static bool RoundNumberToIncrement( // = weeks + days / abs(oneWeekDays) + nanoseconds / (dayLength * abs(oneWeekDays)) // = (weeks * dayLength * abs(oneWeekDays) + days * dayLength + nanoseconds) / (dayLength * abs(oneWeekDays)) // + // Because |abs(nanoseconds / dayLength) < 0|, this operation can be rewritten + // to omit the multiplication by |dayLength| when the rounding conditions are + // appropriately modified to account for the |nanoseconds / dayLength| part. + // This allows to implement rounding using only int64 values. + // + // This optimization is currently only implemented when |nanoseconds| is zero. + // + // Example how to expand this optimization for non-zero |nanoseconds|: + // + // |Round(fraction / increment) * increment| with: + // fraction = numerator / denominator + // numerator = weeks * dayLength * abs(oneWeekDays) + days * dayLength + nanoseconds + // denominator = dayLength * abs(oneWeekDays) + // + // When ignoring the |nanoseconds / dayLength| part, this can be simplified to: + // + // |Round(fraction / increment) * increment| with: + // fraction = numerator / denominator + // numerator = weeks * abs(oneWeekDays) + days + // denominator = abs(oneWeekDays) + // + // Where: + // fraction / increment + // = (numerator / denominator) / increment + // = numerator / (denominator * increment) + // + // And |numerator| and |denominator * increment| both fit into int64. + // + // The "ceiling" operation has to be modified from: + // + // CeilDiv(dividend, divisor) + // quot, rem = dividend / divisor + // return quot + (rem > 0) + // + // To: + // + // CeilDiv(dividend, divisor, fractional) + // quot, rem = dividend / divisor + // return quot + ((rem > 0) || (fractional > 0)) + // + // To properly account for the fractional |nanoseconds| part. Alternatively + // |dividend| can be modified before calling `CeilDiv`. + // // clang-format on - do { - auto nanoseconds = nanosAndDays.nanoseconds().toNanoseconds(); - if (!nanoseconds.isValid()) { - break; - } + if (fractionalDays.time == 0) { + auto [numerator, denominator] = fraction; + int64_t totalDays = fractionalDays.days + denominator * numerator; - auto dayLength = nanosAndDays.dayLength().toNanoseconds(); - if (!dayLength.isValid()) { - break; + if (computeRemainder == ComputeRemainder::Yes) { + constexpr auto rounded = Int128{0}; + double total = FractionToDouble(totalDays, denominator); + return {rounded, total}; } - auto denominator = dayLength * std::abs(oneUnitDays); - if (!denominator.isValid()) { - break; - } - - int64_t intDays; - if (!mozilla::NumberEqualsInt64(durationDays, &intDays)) { - break; - } + auto rounded = + RoundNumberToIncrement(totalDays, denominator, increment, roundingMode); + constexpr double total = 0; + return {rounded, total}; + } - auto nanoDays = DaysFrom(nanosAndDays); - if (!nanoDays) { - break; - } + do { + auto dayLength = mozilla::CheckedInt64(fractionalDays.dayLength); - auto totalDays = mozilla::CheckedInt64(intDays); - totalDays += *nanoDays; - totalDays += daysToAdd; - if (!totalDays.isValid()) { + auto denominator = dayLength * fraction.denominator; + if (!denominator.isValid()) { break; } - auto totalNanoseconds = dayLength * totalDays; - if (!totalNanoseconds.isValid()) { + auto amountNanos = denominator * fraction.numerator; + if (!amountNanos.isValid()) { break; } - totalNanoseconds += nanoseconds; + auto totalNanoseconds = dayLength * fractionalDays.days; + totalNanoseconds += fractionalDays.time; + totalNanoseconds += amountNanos; if (!totalNanoseconds.isValid()) { break; } - int64_t intAmount; - if (!mozilla::NumberEqualsInt64(durationAmount, &intAmount)) { - break; + if (computeRemainder == ComputeRemainder::Yes) { + constexpr auto rounded = Int128{0}; + double total = + FractionToDouble(totalNanoseconds.value(), denominator.value()); + return {rounded, total}; } - int64_t intAmountPassed; - if (!mozilla::NumberEqualsInt64(amountPassed, &intAmountPassed)) { - break; - } - - auto totalAmount = mozilla::CheckedInt64(intAmount) + intAmountPassed; - if (!totalAmount.isValid()) { - break; - } + auto rounded = RoundNumberToIncrement( + totalNanoseconds.value(), denominator.value(), increment, roundingMode); + constexpr double total = 0; + return {rounded, total}; + } while (false); - auto amountNanos = denominator * totalAmount; - if (!amountNanos.isValid()) { - break; - } + // Use int128 when values are too large for int64. Additionally assert all + // values fit into int128. - totalNanoseconds += amountNanos; - if (!totalNanoseconds.isValid()) { - break; - } + // `dayLength` < 2**53 + auto dayLength = Int128{fractionalDays.dayLength}; + MOZ_ASSERT(dayLength < Int128{1} << 53); - double rounded; - double total = 0; - if (computeRemainder == ComputeRemainder::No) { - if (!temporal::RoundNumberToIncrement(cx, totalNanoseconds.value(), - denominator.value(), increment, - roundingMode, &rounded)) { - return false; - } - } else { - TruncateNumber(totalNanoseconds.value(), denominator.value(), &rounded, - &total); - } + // `fraction.denominator` < MaxEpochDaysDuration + // log2(MaxEpochDaysDuration) = ~27.57. + auto denominator = dayLength * Int128{fraction.denominator}; + MOZ_ASSERT(denominator < Int128{1} << (53 + 28)); - *result = {rounded, total}; - return true; - } while (false); + // log2(24*60*60) = ~16.4 and log2(2 * MaxEpochDaysDuration) = ~28.57. + // + // `abs(MaxFractionalDays)` + // = `abs(2 * MaxDurationDays + 2 * MaxEpochDaysDuration)` + // = `abs(2 * 2**(53 - 16) + 2 * MaxEpochDaysDuration)` + // ≤ 2 * 2**37 + 2**29 + // ≤ 2**39 + auto totalDays = Int128{fractionalDays.days}; + MOZ_ASSERT(totalDays.abs() <= Uint128{1} << 39); + + // `abs(fraction.numerator)` ≤ (2**33) + auto totalAmount = Int128{fraction.numerator}; + MOZ_ASSERT(totalAmount.abs() <= Uint128{1} << 33); + + // `denominator` < 2**(53 + 28) + // `abs(totalAmount)` <= 2**33 + // + // `denominator * totalAmount` + // ≤ 2**(53 + 28) * 2**33 + // = 2**(53 + 28 + 33) + // = 2**114 + auto amountNanos = denominator * totalAmount; + MOZ_ASSERT(amountNanos.abs() <= Uint128{1} << 114); + + // `dayLength` < 2**53 + // `totalDays` ≤ 2**39 + // `fractionalDays.time` < `dayLength` < 2**53 + // `amountNanos` ≤ 2**114 + // + // `dayLength * totalDays` + // ≤ 2**(53 + 39) = 2**92 + // + // `dayLength * totalDays + fractionalDays.time` + // ≤ 2**93 + // + // `dayLength * totalDays + fractionalDays.time + amountNanos` + // ≤ 2**115 + auto totalNanoseconds = dayLength * totalDays; + totalNanoseconds += Int128{fractionalDays.time}; + totalNanoseconds += amountNanos; + MOZ_ASSERT(totalNanoseconds.abs() <= Uint128{1} << 115); - return RoundNumberToIncrementSlow( - cx, durationAmount, amountPassed, durationDays, daysToAdd, nanosAndDays, - oneUnitDays, increment, roundingMode, computeRemainder, result); -} + if (computeRemainder == ComputeRemainder::Yes) { + constexpr auto rounded = Int128{0}; + double total = FractionToDouble(totalNanoseconds, denominator); + return {rounded, total}; + } -static bool RoundNumberToIncrement( - JSContext* cx, double durationDays, - Handle<temporal::NanosecondsAndDays> nanosAndDays, Increment increment, - TemporalRoundingMode roundingMode, ComputeRemainder computeRemainder, - RoundedNumber* result) { - constexpr double daysAmount = 0; - constexpr double daysPassed = 0; - constexpr int32_t oneDayDays = 1; - constexpr int32_t daysToAdd = 0; - - return RoundNumberToIncrement(cx, daysAmount, daysPassed, durationDays, - daysToAdd, nanosAndDays, oneDayDays, increment, - roundingMode, computeRemainder, result); + auto rounded = RoundNumberToIncrement(totalNanoseconds, denominator, + increment, roundingMode); + constexpr double total = 0; + return {rounded, total}; } -static bool RoundDurationYear(JSContext* cx, const Duration& duration, - Handle<temporal::NanosecondsAndDays> nanosAndDays, +static bool RoundDurationYear(JSContext* cx, const NormalizedDuration& duration, + FractionalDays fractionalDays, Increment increment, TemporalRoundingMode roundingMode, Handle<Wrapped<PlainDateObject*>> dateRelativeTo, Handle<CalendarRecord> calendar, ComputeRemainder computeRemainder, RoundedDuration* result) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; + auto [years, months, weeks, days] = duration.date; - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; - - // Step 10.a. - Duration yearsDuration = {years}; + // Step 9.a. + auto yearsDuration = DateDuration{years}; - // Step 10.b. + // Step 9.b. auto yearsLater = AddDate(cx, calendar, dateRelativeTo, yearsDuration); if (!yearsLater) { return false; } auto yearsLaterDate = ToPlainDate(&yearsLater.unwrap()); - // Step 10.f. (Reordered) + // Step 9.f. (Reordered) Rooted<Wrapped<PlainDateObject*>> newRelativeTo(cx, yearsLater); - // Step 10.c. - Duration yearsMonthsWeeks = {years, months, weeks}; + // Step 9.c. + auto yearsMonthsWeeks = DateDuration{years, months, weeks}; - // Step 10.d. + // Step 9.d. PlainDate yearsMonthsWeeksLater; if (!AddDate(cx, calendar, dateRelativeTo, yearsMonthsWeeks, &yearsMonthsWeeksLater)) { return false; } - // Step 10.e. + // Step 9.e. int32_t monthsWeeksInDays = DaysUntil(yearsLaterDate, yearsMonthsWeeksLater); - MOZ_ASSERT(std::abs(monthsWeeksInDays) <= epochDays); + MOZ_ASSERT(std::abs(monthsWeeksInDays) <= MaxEpochDaysDuration); - // Step 10.f. (Moved up) + // Step 9.f. (Moved up) - // Step 10.g. - // Our implementation keeps |days| and |monthsWeeksInDays| separate. + // Step 9.g. + fractionalDays += monthsWeeksInDays; // FIXME: spec issue - truncation doesn't match the spec polyfill. // https://github.com/tc39/proposal-temporal/issues/2540 - // Step 10.h. - double truncatedDays; - if (!TruncateDays(cx, nanosAndDays, days, monthsWeeksInDays, - &truncatedDays)) { - return false; - } - - // FIXME: spec bug - truncated days can be infinity: - // - // Temporal.Duration.from({ - // days: Number.MAX_VALUE, - // hours: Number.MAX_VALUE, - // }).round({ - // smallestUnit: "years", - // relativeTo: "1970-01-01", - // }); - if (!IsInteger(truncatedDays)) { - MOZ_ASSERT(std::isinf(truncatedDays)); - JS_ReportErrorASCII(cx, "truncated days is infinity"); - return false; - } - + // Step 9.h. PlainDate isoResult; - if (!AddISODate(cx, yearsLaterDate, {0, 0, 0, truncatedDays}, - TemporalOverflow::Constrain, &isoResult)) { + if (!BalanceISODate(cx, yearsLaterDate, fractionalDays.truncate(), + &isoResult)) { return false; } - // Step 10.i. + // Step 9.i. Rooted<PlainDateObject*> wholeDaysLater( cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); if (!wholeDaysLater) { return false; } - // Steps 10.j-l. - Duration timePassed; + // Steps 9.j-l. + DateDuration timePassed; if (!DifferenceDate(cx, calendar, newRelativeTo, wholeDaysLater, TemporalUnit::Year, &timePassed)) { return false; } - // Step 10.m. - double yearsPassed = timePassed.years; + // Step 9.m. + int64_t yearsPassed = timePassed.years; - // Step 10.n. - // Our implementation keeps |years| and |yearsPassed| separate. + // Step 9.n. + years += yearsPassed; - // Step 10.o. - Duration yearsPassedDuration = {yearsPassed}; + // Step 9.o. + auto yearsPassedDuration = DateDuration{yearsPassed}; - // Steps 10.p-r. + // Steps 9.p-r. int32_t daysPassed; if (!MoveRelativeDate(cx, calendar, newRelativeTo, yearsPassedDuration, &newRelativeTo, &daysPassed)) { return false; } - MOZ_ASSERT(std::abs(daysPassed) <= epochDays); + MOZ_ASSERT(std::abs(daysPassed) <= MaxEpochDaysDuration); - // Step 10.s. - // - // Our implementation keeps |days| and |daysPassed| separate. - int32_t daysToAdd = monthsWeeksInDays - daysPassed; - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); + // Step 9.s. + fractionalDays -= daysPassed; - // Steps 10.t. - double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + // Steps 9.t. + int32_t sign = fractionalDays.sign() < 0 ? -1 : 1; - // Step 10.u. - Duration oneYear = {sign}; + // Step 9.u. + auto oneYear = DateDuration{sign}; - // Steps 10.v-w. + // Steps 9.v-w. Rooted<Wrapped<PlainDateObject*>> moveResultIgnored(cx); int32_t oneYearDays; if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneYear, @@ -4619,158 +3608,137 @@ static bool RoundDurationYear(JSContext* cx, const Duration& duration, return false; } - // Step 10.x. + // Step 9.x. if (oneYearDays == 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_NUMBER, "days"); return false; } - // Steps 10.y-aa. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, years, yearsPassed, days, daysToAdd, - nanosAndDays, oneYearDays, increment, - roundingMode, computeRemainder, &rounded)) { - return false; - } - auto [numYears, total] = rounded; + // Steps 9.y. + auto fractionalYears = Fraction{years, std::abs(oneYearDays)}; - // Step 10.ab. - double numMonths = 0; - double numWeeks = 0; + // Steps 9.z-aa. + auto [numYears, total] = + RoundNumberToIncrement(fractionalYears, fractionalDays, increment, + roundingMode, computeRemainder); - // Step 20. - Duration resultDuration = {numYears, numMonths, numWeeks}; + // Step 9.ab. + int64_t numMonths = 0; + int64_t numWeeks = 0; + + // Step 9.ac. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + if (numYears.abs() >= (Uint128{1} << 32)) { + return ThrowInvalidDurationPart(cx, double(numYears), "years", + JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + + auto resultDuration = DateDuration{int64_t(numYears), numMonths, numWeeks}; if (!ThrowIfInvalidDuration(cx, resultDuration)) { return false; } - // Step 21. - *result = {resultDuration, total}; + *result = {{resultDuration, time}, total}; return true; } -static bool RoundDurationMonth( - JSContext* cx, const Duration& duration, - Handle<temporal::NanosecondsAndDays> nanosAndDays, Increment increment, - TemporalRoundingMode roundingMode, - Handle<Wrapped<PlainDateObject*>> dateRelativeTo, - Handle<CalendarRecord> calendar, ComputeRemainder computeRemainder, - RoundedDuration* result) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; +static bool RoundDurationMonth(JSContext* cx, + const NormalizedDuration& duration, + FractionalDays fractionalDays, + Increment increment, + TemporalRoundingMode roundingMode, + Handle<Wrapped<PlainDateObject*>> dateRelativeTo, + Handle<CalendarRecord> calendar, + ComputeRemainder computeRemainder, + RoundedDuration* result) { + auto [years, months, weeks, days] = duration.date; - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; - - // Step 11.a. - Duration yearsMonths = {years, months}; + // Step 10.a. + auto yearsMonths = DateDuration{years, months}; - // Step 11.b. + // Step 10.b. auto yearsMonthsLater = AddDate(cx, calendar, dateRelativeTo, yearsMonths); if (!yearsMonthsLater) { return false; } auto yearsMonthsLaterDate = ToPlainDate(&yearsMonthsLater.unwrap()); - // Step 11.f. (Reordered) + // Step 10.f. (Reordered) Rooted<Wrapped<PlainDateObject*>> newRelativeTo(cx, yearsMonthsLater); - // Step 11.c. - Duration yearsMonthsWeeks = {years, months, weeks}; + // Step 10.c. + auto yearsMonthsWeeks = DateDuration{years, months, weeks}; - // Step 11.d. + // Step 10.d. PlainDate yearsMonthsWeeksLater; if (!AddDate(cx, calendar, dateRelativeTo, yearsMonthsWeeks, &yearsMonthsWeeksLater)) { return false; } - // Step 11.e. + // Step 10.e. int32_t weeksInDays = DaysUntil(yearsMonthsLaterDate, yearsMonthsWeeksLater); - MOZ_ASSERT(std::abs(weeksInDays) <= epochDays); + MOZ_ASSERT(std::abs(weeksInDays) <= MaxEpochDaysDuration); - // Step 11.f. (Moved up) + // Step 10.f. (Moved up) - // Step 11.g. - // Our implementation keeps |days| and |weeksInDays| separate. + // Step 10.g. + fractionalDays += weeksInDays; // FIXME: spec issue - truncation doesn't match the spec polyfill. // https://github.com/tc39/proposal-temporal/issues/2540 - // Step 11.h. - double truncatedDays; - if (!TruncateDays(cx, nanosAndDays, days, weeksInDays, &truncatedDays)) { - return false; - } - - // FIXME: spec bug - truncated days can be infinity: - // - // Temporal.Duration.from({ - // days: Number.MAX_VALUE, - // hours: Number.MAX_VALUE, - // }).round({ - // smallestUnit: "months", - // relativeTo: "1970-01-01", - // }); - if (!IsInteger(truncatedDays)) { - MOZ_ASSERT(std::isinf(truncatedDays)); - JS_ReportErrorASCII(cx, "truncated days is infinity"); - return false; - } - + // Step 10.h. PlainDate isoResult; - if (!AddISODate(cx, yearsMonthsLaterDate, {0, 0, 0, truncatedDays}, - TemporalOverflow::Constrain, &isoResult)) { + if (!BalanceISODate(cx, yearsMonthsLaterDate, fractionalDays.truncate(), + &isoResult)) { return false; } - // Step 11.i. + // Step 10.i. Rooted<PlainDateObject*> wholeDaysLater( cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); if (!wholeDaysLater) { return false; } - // Steps 11.j-l. - Duration timePassed; + // Steps 10.j-l. + DateDuration timePassed; if (!DifferenceDate(cx, calendar, newRelativeTo, wholeDaysLater, TemporalUnit::Month, &timePassed)) { return false; } - // Step 11.m. - double monthsPassed = timePassed.months; + // Step 10.m. + int64_t monthsPassed = timePassed.months; - // Step 11.n. - // Our implementation keeps |months| and |monthsPassed| separate. + // Step 10.n. + months += monthsPassed; - // Step 11.o. - Duration monthsPassedDuration = {0, monthsPassed}; + // Step 10.o. + auto monthsPassedDuration = DateDuration{0, monthsPassed}; - // Steps 11.p-r. + // Steps 10.p-r. int32_t daysPassed; if (!MoveRelativeDate(cx, calendar, newRelativeTo, monthsPassedDuration, &newRelativeTo, &daysPassed)) { return false; } - MOZ_ASSERT(std::abs(daysPassed) <= epochDays); + MOZ_ASSERT(std::abs(daysPassed) <= MaxEpochDaysDuration); - // Step 11.s. - // - // Our implementation keeps |days| and |daysPassed| separate. - int32_t daysToAdd = weeksInDays - daysPassed; - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); + // Step 10.s. + fractionalDays -= daysPassed; - // Steps 11.t. - double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + // Steps 10.t. + int32_t sign = fractionalDays.sign() < 0 ? -1 : 1; - // Step 11.u. - Duration oneMonth = {0, sign}; + // Step 10.u. + auto oneMonth = DateDuration{0, sign}; - // Steps 11.v-w. + // Steps 10.v-w. Rooted<Wrapped<PlainDateObject*>> moveResultIgnored(cx); int32_t oneMonthDays; if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneMonth, @@ -4778,51 +3746,51 @@ static bool RoundDurationMonth( return false; } - // Step 11.x. + // Step 10.x. if (oneMonthDays == 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_NUMBER, "days"); return false; } - // Steps 11.y-aa. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, months, monthsPassed, days, daysToAdd, - nanosAndDays, oneMonthDays, increment, - roundingMode, computeRemainder, &rounded)) { - return false; - } - auto [numMonths, total] = rounded; + // Step 10.y. + auto fractionalMonths = Fraction{months, std::abs(oneMonthDays)}; - // Step 11.ab. - double numWeeks = 0; + // Steps 10.z-aa. + auto [numMonths, total] = + RoundNumberToIncrement(fractionalMonths, fractionalDays, increment, + roundingMode, computeRemainder); - // Step 20. - Duration resultDuration = {years, numMonths, numWeeks}; + // Step 10.ab. + int64_t numWeeks = 0; + + // Step 10.ac. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + if (numMonths.abs() >= (Uint128{1} << 32)) { + return ThrowInvalidDurationPart(cx, double(numMonths), "months", + JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); + } + + auto resultDuration = DateDuration{years, int64_t(numMonths), numWeeks}; if (!ThrowIfInvalidDuration(cx, resultDuration)) { return false; } - // Step 21. - *result = {resultDuration, total}; + *result = {{resultDuration, time}, total}; return true; } -static bool RoundDurationWeek(JSContext* cx, const Duration& duration, - Handle<temporal::NanosecondsAndDays> nanosAndDays, +static bool RoundDurationWeek(JSContext* cx, const NormalizedDuration& duration, + FractionalDays fractionalDays, Increment increment, TemporalRoundingMode roundingMode, Handle<Wrapped<PlainDateObject*>> dateRelativeTo, Handle<CalendarRecord> calendar, ComputeRemainder computeRemainder, RoundedDuration* result) { - // Numbers of days between nsMinInstant and nsMaxInstant. - static constexpr int32_t epochDays = 200'000'000; - - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + auto [years, months, weeks, days] = duration.date; auto* unwrappedRelativeTo = dateRelativeTo.unwrap(cx); if (!unwrappedRelativeTo) { @@ -4830,78 +3798,55 @@ static bool RoundDurationWeek(JSContext* cx, const Duration& duration, } auto relativeToDate = ToPlainDate(unwrappedRelativeTo); - // Step 12.a - double truncatedDays; - if (!TruncateDays(cx, nanosAndDays, days, 0, &truncatedDays)) { - return false; - } - - // FIXME: spec bug - truncated days can be infinity: - // - // Temporal.Duration.from({ - // days: Number.MAX_VALUE, - // hours: Number.MAX_VALUE, - // }).round({ - // smallestUnit: "weeks", - // relativeTo: "1970-01-01", - // }); - if (!IsInteger(truncatedDays)) { - MOZ_ASSERT(std::isinf(truncatedDays)); - JS_ReportErrorASCII(cx, "truncated days is infinity"); - return false; - } - + // Step 11.a PlainDate isoResult; - if (!AddISODate(cx, relativeToDate, {0, 0, 0, truncatedDays}, - TemporalOverflow::Constrain, &isoResult)) { + if (!BalanceISODate(cx, relativeToDate, fractionalDays.truncate(), + &isoResult)) { return false; } - // Step 12.b. + // Step 11.b. Rooted<PlainDateObject*> wholeDaysLater( cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); if (!wholeDaysLater) { return false; } - // Steps 12.c-e. - Duration timePassed; + // Steps 11.c-e. + DateDuration timePassed; if (!DifferenceDate(cx, calendar, dateRelativeTo, wholeDaysLater, TemporalUnit::Week, &timePassed)) { return false; } - // Step 12.f. - double weeksPassed = timePassed.weeks; + // Step 11.f. + int64_t weeksPassed = timePassed.weeks; - // Step 12.g. - // Our implementation keeps |weeks| and |weeksPassed| separate. + // Step 11.g. + weeks += weeksPassed; - // Step 12.h. - Duration weeksPassedDuration = {0, 0, weeksPassed}; + // Step 11.h. + auto weeksPassedDuration = DateDuration{0, 0, weeksPassed}; - // Steps 12.i-k. + // Steps 11.i-k. Rooted<Wrapped<PlainDateObject*>> newRelativeTo(cx); int32_t daysPassed; if (!MoveRelativeDate(cx, calendar, dateRelativeTo, weeksPassedDuration, &newRelativeTo, &daysPassed)) { return false; } - MOZ_ASSERT(std::abs(daysPassed) <= epochDays); + MOZ_ASSERT(std::abs(daysPassed) <= MaxEpochDaysDuration); - // Step 12.l. - // - // Our implementation keeps |days| and |daysPassed| separate. - int32_t daysToAdd = -daysPassed; - MOZ_ASSERT(std::abs(daysToAdd) <= epochDays); + // Step 11.l. + fractionalDays -= daysPassed; - // Steps 12.m. - double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + // Steps 11.m. + int32_t sign = fractionalDays.sign() < 0 ? -1 : 1; - // Step 12.n. - Duration oneWeek = {0, 0, sign}; + // Step 11.n. + auto oneWeek = DateDuration{0, 0, sign}; - // Steps 12.o-p. + // Steps 11.o-p. Rooted<Wrapped<PlainDateObject*>> moveResultIgnored(cx); int32_t oneWeekDays; if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneWeek, @@ -4909,71 +3854,157 @@ static bool RoundDurationWeek(JSContext* cx, const Duration& duration, return false; } - // Step 12.q. + // Step 11.q. if (oneWeekDays == 0) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_NUMBER, "days"); return false; } - // Steps 12.r-t. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, weeks, weeksPassed, days, daysToAdd, - nanosAndDays, oneWeekDays, increment, - roundingMode, computeRemainder, &rounded)) { - return false; + // Step 11.r. + auto fractionalWeeks = Fraction{weeks, std::abs(oneWeekDays)}; + + // Steps 11.s-t. + auto [numWeeks, total] = + RoundNumberToIncrement(fractionalWeeks, fractionalDays, increment, + roundingMode, computeRemainder); + + // Step 11.u. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + if (numWeeks.abs() >= (Uint128{1} << 32)) { + return ThrowInvalidDurationPart(cx, double(numWeeks), "weeks", + JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE); } - auto [numWeeks, total] = rounded; - // Step 20. - Duration resultDuration = {years, months, numWeeks}; + auto resultDuration = DateDuration{years, months, int64_t(numWeeks)}; if (!ThrowIfInvalidDuration(cx, resultDuration)) { return false; } - // Step 21. - *result = {resultDuration, total}; + *result = {{resultDuration, time}, total}; return true; } -static bool RoundDurationDay(JSContext* cx, const Duration& duration, - Handle<temporal::NanosecondsAndDays> nanosAndDays, +static bool RoundDurationDay(JSContext* cx, const NormalizedDuration& duration, + const FractionalDays& fractionalDays, Increment increment, TemporalRoundingMode roundingMode, ComputeRemainder computeRemainder, RoundedDuration* result) { - double years = duration.years; - double months = duration.months; - double weeks = duration.weeks; - double days = duration.days; + auto [years, months, weeks, days] = duration.date; + + // Pass zero fraction. + constexpr auto zero = Fraction{0, 1}; + + // Steps 12.a-b. + auto [numDays, total] = RoundNumberToIncrement( + zero, fractionalDays, increment, roundingMode, computeRemainder); - // Steps 13.a-b. - RoundedNumber rounded; - if (!RoundNumberToIncrement(cx, days, nanosAndDays, increment, roundingMode, - computeRemainder, &rounded)) { + MOZ_ASSERT(Int128{INT64_MIN} <= numDays && numDays <= Int128{INT64_MAX}, + "rounded days fits in int64"); + + // Step 12.c. + constexpr auto time = NormalizedTimeDuration{}; + + // Step 19. + auto resultDuration = DateDuration{years, months, weeks, int64_t(numDays)}; + if (!ThrowIfInvalidDuration(cx, resultDuration)) { return false; } - auto [numDays, total] = rounded; - // Step 20. - Duration resultDuration = {years, months, weeks, numDays}; - if (!ThrowIfInvalidDuration(cx, resultDuration)) { + *result = {{resultDuration, time}, total}; + return true; +} + +/** + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) + */ +static bool RoundDuration(JSContext* cx, const NormalizedDuration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + ComputeRemainder computeRemainder, + RoundedDuration* result) { + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration.time)); + MOZ_ASSERT_IF(unit > TemporalUnit::Day, IsValidDuration(duration.date)); + + // The remainder is only needed when called from |Duration_total|. And `total` + // always passes |increment=1| and |roundingMode=trunc|. + MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, + increment == Increment{1}); + MOZ_ASSERT_IF(computeRemainder == ComputeRemainder::Yes, + roundingMode == TemporalRoundingMode::Trunc); + + // Steps 1-5. (Not applicable.) + + // Step 6. + if (unit <= TemporalUnit::Week) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, + "relativeTo"); return false; } - // Step 21. - *result = {resultDuration, total}; + // TODO: We could directly return here if unit=nanoseconds and increment=1, + // because in that case this operation is a no-op. This case happens for + // example when calling Temporal.PlainTime.prototype.{since,until} without an + // options object. + // + // But maybe this can be even more efficiently handled in the callers. For + // example when Temporal.PlainTime.prototype.{since,until} is called without + // an options object, we can not only skip the RoundDuration call, but also + // the following BalanceTimeDuration call. + + // Step 7. (Moved below.) + + // Step 8. (Not applicable.) + + // Steps 9-11. (Not applicable.) + + // Step 12. + if (unit == TemporalUnit::Day) { + // Step 7. + auto timeAndDays = NormalizedTimeDurationToDays(duration.time); + auto fractionalDays = FractionalDays{duration.date.days, timeAndDays}; + + return RoundDurationDay(cx, duration, fractionalDays, increment, + roundingMode, computeRemainder, result); + } + + MOZ_ASSERT(TemporalUnit::Hour <= unit && unit <= TemporalUnit::Nanosecond); + + // Steps 13-18. + auto time = duration.time; + double total = 0; + if (computeRemainder == ComputeRemainder::No) { + if (!RoundNormalizedTimeDurationToIncrement(cx, time, unit, increment, + roundingMode, &time)) { + return false; + } + } else { + MOZ_ASSERT(increment == Increment{1}); + MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); + + total = TotalNormalizedTimeDuration(duration.time, unit); + } + MOZ_ASSERT(IsValidNormalizedTimeDuration(time)); + + // Step 19. + MOZ_ASSERT(IsValidDuration(duration.date)); + *result = {{duration.date, time}, total}; return true; } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ static bool RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, Handle<Wrapped<PlainDateObject*>> plainRelativeTo, Handle<CalendarRecord> calendar, Handle<ZonedDateTime> zonedRelativeTo, @@ -4982,9 +4013,11 @@ static bool RoundDuration( ComputeRemainder computeRemainder, RoundedDuration* result) { // Note: |duration.days| can have a different sign than the other date // components. The date and time components can have different signs, too. - MOZ_ASSERT( - IsValidDuration({duration.years, duration.months, duration.weeks})); - MOZ_ASSERT(IsValidDuration(duration.time())); + MOZ_ASSERT(IsValidDuration(Duration{double(duration.date.years), + double(duration.date.months), + double(duration.date.weeks)})); + MOZ_ASSERT(IsValidNormalizedTimeDuration(duration.time)); + MOZ_ASSERT_IF(unit > TemporalUnit::Day, IsValidDuration(duration.date)); MOZ_ASSERT(plainRelativeTo || zonedRelativeTo, "Use RoundDuration without relativeTo when plainRelativeTo and " @@ -5040,69 +4073,61 @@ static bool RoundDuration( MOZ_ASSERT(TemporalUnit::Year <= unit && unit <= TemporalUnit::Day); // Steps 7.a-c. - Rooted<temporal::NanosecondsAndDays> nanosAndDays(cx); + FractionalDays fractionalDays; if (zonedRelativeTo) { - // Step 7.b.i. (Reordered) + // Step 7.a.i. Rooted<ZonedDateTime> intermediate(cx); if (!MoveRelativeZonedDateTime(cx, zonedRelativeTo, calendar, timeZone, - duration.date(), precalculatedPlainDateTime, + duration.date, precalculatedPlainDateTime, &intermediate)) { return false; } - // Steps 7.a and 7.b.ii. - if (!NanosecondsToDays(cx, duration, intermediate, timeZone, - &nanosAndDays)) { + // Steps 7.a.ii. + NormalizedTimeAndDays timeAndDays; + if (!NormalizedTimeDurationToDays(cx, duration.time, intermediate, timeZone, + &timeAndDays)) { return false; } - // Step 7.b.iii. (Not applicable in our implementation.) + // Step 7.a.iii. + fractionalDays = FractionalDays{duration.date.days, timeAndDays}; } else { - // Steps 7.a and 7.c. - if (!::NanosecondsToDays(cx, duration, &nanosAndDays)) { - return false; - } + // Step 7.b. + auto timeAndDays = NormalizedTimeDurationToDays(duration.time); + fractionalDays = FractionalDays{duration.date.days, timeAndDays}; } - // NanosecondsToDays guarantees that |abs(nanosAndDays.nanoseconds)| is less - // than |abs(nanosAndDays.dayLength)|. - MOZ_ASSERT(nanosAndDays.nanoseconds().abs() < nanosAndDays.dayLength()); - - // Step 7.d. (Moved below) - - // Step 7.e. (Implicit) + // Step 7.c. (Moved below) // Step 8. (Not applicable) - // Step 9. - // FIXME: spec issue - `total` doesn't need be initialised. - - // Steps 10-21. + // Steps 9-19. switch (unit) { - // Steps 10 and 20-21. + // Steps 9 and 19. case TemporalUnit::Year: - return RoundDurationYear(cx, duration, nanosAndDays, increment, + return RoundDurationYear(cx, duration, fractionalDays, increment, roundingMode, plainRelativeTo, calendar, computeRemainder, result); - // Steps 11 and 20-21. + // Steps 10 and 19. case TemporalUnit::Month: - return RoundDurationMonth(cx, duration, nanosAndDays, increment, + return RoundDurationMonth(cx, duration, fractionalDays, increment, roundingMode, plainRelativeTo, calendar, computeRemainder, result); - // Steps 12 and 20-21. + // Steps 11 and 19. case TemporalUnit::Week: - return RoundDurationWeek(cx, duration, nanosAndDays, increment, + return RoundDurationWeek(cx, duration, fractionalDays, increment, roundingMode, plainRelativeTo, calendar, computeRemainder, result); - // Steps 13 and 20-21. + // Steps 12 and 19. case TemporalUnit::Day: - return RoundDurationDay(cx, duration, nanosAndDays, increment, + return RoundDurationDay(cx, duration, fractionalDays, increment, roundingMode, computeRemainder, result); - // Steps 14-19. (Handled elsewhere) + // Steps 13-18. (Handled elsewhere) case TemporalUnit::Auto: case TemporalUnit::Hour: case TemporalUnit::Minute: @@ -5117,55 +4142,51 @@ static bool RoundDuration( } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -static bool RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, +bool js::temporal::RoundDuration( + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, Handle<Wrapped<PlainDateObject*>> plainRelativeTo, - Handle<CalendarRecord> calendar, Handle<ZonedDateTime> zonedRelativeTo, - Handle<TimeZoneRecord> timeZone, - mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, - double* result) { - // Only called from |Duration_total|, which always passes |increment=1| and - // |roundingMode=trunc|. - MOZ_ASSERT(increment == Increment{1}); - MOZ_ASSERT(roundingMode == TemporalRoundingMode::Trunc); + Handle<CalendarRecord> calendar, NormalizedDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); + Rooted<ZonedDateTime> zonedRelativeTo(cx, ZonedDateTime{}); + Rooted<TimeZoneRecord> timeZone(cx, TimeZoneRecord{}); + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime{}; RoundedDuration rounded; if (!::RoundDuration(cx, duration, increment, unit, roundingMode, plainRelativeTo, calendar, zonedRelativeTo, timeZone, - precalculatedPlainDateTime, ComputeRemainder::Yes, + precalculatedPlainDateTime, ComputeRemainder::No, &rounded)) { return false; } - *result = rounded.total; + *result = rounded.duration; return true; } /** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) + * RoundDuration ( years, months, weeks, days, norm, increment, unit, + * roundingMode [ , plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , + * timeZoneRec [ , precalculatedPlainDateTime ] ] ] ] ] ) */ -static bool RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, +bool js::temporal::RoundDuration( + JSContext* cx, const NormalizedDuration& duration, Increment increment, TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle<Wrapped<PlainDateObject*>> plainRelativeTo, - Handle<CalendarRecord> calendar, Handle<ZonedDateTime> zonedRelativeTo, - Handle<TimeZoneRecord> timeZone, - mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, - Duration* result) { + Handle<PlainDateObject*> plainRelativeTo, Handle<CalendarRecord> calendar, + Handle<ZonedDateTime> zonedRelativeTo, Handle<TimeZoneRecord> timeZone, + const PlainDateTime& precalculatedPlainDateTime, + NormalizedDuration* result) { + MOZ_ASSERT(IsValidDuration(duration)); + RoundedDuration rounded; if (!::RoundDuration(cx, duration, increment, unit, roundingMode, plainRelativeTo, calendar, zonedRelativeTo, timeZone, - precalculatedPlainDateTime, ComputeRemainder::No, - &rounded)) { + mozilla::SomeRef(precalculatedPlainDateTime), + ComputeRemainder::No, &rounded)) { return false; } @@ -5173,46 +4194,6 @@ static bool RoundDuration( return true; } -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -bool js::temporal::RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle<Wrapped<PlainDateObject*>> plainRelativeTo, - Handle<CalendarRecord> calendar, Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - Rooted<ZonedDateTime> zonedRelativeTo(cx, ZonedDateTime{}); - Rooted<TimeZoneRecord> timeZone(cx, TimeZoneRecord{}); - mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime{}; - return ::RoundDuration(cx, duration, increment, unit, roundingMode, - plainRelativeTo, calendar, zonedRelativeTo, timeZone, - precalculatedPlainDateTime, result); -} - -/** - * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, - * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , - * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , - * precalculatedPlainDateTime ] ] ] ] ] ) - */ -bool js::temporal::RoundDuration( - JSContext* cx, const Duration& duration, Increment increment, - TemporalUnit unit, TemporalRoundingMode roundingMode, - Handle<PlainDateObject*> plainRelativeTo, Handle<CalendarRecord> calendar, - Handle<ZonedDateTime> zonedRelativeTo, Handle<TimeZoneRecord> timeZone, - const PlainDateTime& precalculatedPlainDateTime, Duration* result) { - MOZ_ASSERT(IsValidDuration(duration)); - - return ::RoundDuration(cx, duration, increment, unit, roundingMode, - plainRelativeTo, calendar, zonedRelativeTo, timeZone, - mozilla::SomeRef(precalculatedPlainDateTime), result); -} - enum class DurationOperation { Add, Subtract }; /** @@ -5441,40 +4422,32 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { return false; } - Rooted<Wrapped<PlainDateObject*>> plainRelativeTo(cx); - Rooted<ZonedDateTime> zonedRelativeTo(cx); - Rooted<TimeZoneRecord> timeZone(cx); + // Step 3. + Rooted<JSObject*> options(cx); if (args.hasDefined(2)) { - // Step 3. - Rooted<JSObject*> options( - cx, RequireObjectArg(cx, "options", "compare", args[2])); + options = RequireObjectArg(cx, "options", "compare", args[2]); if (!options) { return false; } + } - // Step 4. - if (one == two) { - args.rval().setInt32(0); - return true; - } + // Step 4. + if (one == two) { + args.rval().setInt32(0); + return true; + } - // Steps 5-8. + // Steps 5-8. + Rooted<Wrapped<PlainDateObject*>> plainRelativeTo(cx); + Rooted<ZonedDateTime> zonedRelativeTo(cx); + Rooted<TimeZoneRecord> timeZone(cx); + if (options) { if (!ToRelativeTemporalObject(cx, options, &plainRelativeTo, &zonedRelativeTo, &timeZone)) { return false; } MOZ_ASSERT(!plainRelativeTo || !zonedRelativeTo); MOZ_ASSERT_IF(zonedRelativeTo, timeZone.receiver()); - } else { - // Step 3. (Not applicable in our implementation.) - - // Step 4. - if (one == two) { - args.rval().setInt32(0); - return true; - } - - // Steps 5-8. (Not applicable in our implementation.) } // Steps 9-10. @@ -5498,7 +4471,7 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { if (zonedRelativeTo && (calendarUnitsPresent || one.days != 0 || two.days != 0)) { // Step 12.a. - auto instant = zonedRelativeTo.instant(); + const auto& instant = zonedRelativeTo.instant(); // Step 12.b. PlainDateTime dateTime; @@ -5507,59 +4480,65 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { } // Step 12.c. + auto normalized1 = CreateNormalizedDurationRecord(one); + + // Step 12.d. + auto normalized2 = CreateNormalizedDurationRecord(two); + + // Step 12.e. Instant after1; - if (!AddZonedDateTime(cx, instant, timeZone, calendar, one, dateTime, - &after1)) { + if (!AddZonedDateTime(cx, instant, timeZone, calendar, normalized1, + dateTime, &after1)) { return false; } - // Step 12.d. + // Step 12.f. Instant after2; - if (!AddZonedDateTime(cx, instant, timeZone, calendar, two, dateTime, - &after2)) { + if (!AddZonedDateTime(cx, instant, timeZone, calendar, normalized2, + dateTime, &after2)) { return false; } - // Steps 12.e-g. + // Steps 12.g-i. args.rval().setInt32(after1 < after2 ? -1 : after1 > after2 ? 1 : 0); return true; } // Steps 13-14. - double days1, days2; + int64_t days1, days2; if (calendarUnitsPresent) { // FIXME: spec issue - directly throw an error if plainRelativeTo is undef. // Step 13.a. DateDuration unbalanceResult1; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, one, TemporalUnit::Day, - plainRelativeTo, calendar, - &unbalanceResult1)) { + if (!UnbalanceDateDurationRelative(cx, one.toDateDuration(), + TemporalUnit::Day, plainRelativeTo, + calendar, &unbalanceResult1)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, one, TemporalUnit::Day, - &unbalanceResult1)) { + if (!UnbalanceDateDurationRelative( + cx, one.toDateDuration(), TemporalUnit::Day, &unbalanceResult1)) { return false; } - MOZ_ASSERT(one.date() == unbalanceResult1.toDuration()); + MOZ_ASSERT(one.toDateDuration() == unbalanceResult1); } // Step 13.b. DateDuration unbalanceResult2; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, two, TemporalUnit::Day, - plainRelativeTo, calendar, - &unbalanceResult2)) { + if (!UnbalanceDateDurationRelative(cx, two.toDateDuration(), + TemporalUnit::Day, plainRelativeTo, + calendar, &unbalanceResult2)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, two, TemporalUnit::Day, - &unbalanceResult2)) { + if (!UnbalanceDateDurationRelative( + cx, two.toDateDuration(), TemporalUnit::Day, &unbalanceResult2)) { return false; } - MOZ_ASSERT(two.date() == unbalanceResult2.toDuration()); + MOZ_ASSERT(two.toDateDuration() == unbalanceResult2); } // Step 13.c. @@ -5569,74 +4548,32 @@ static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { days2 = unbalanceResult2.days; } else { // Step 14.a. - days1 = one.days; + days1 = mozilla::AssertedCast<int64_t>(one.days); // Step 14.b. - days2 = two.days; + days2 = mozilla::AssertedCast<int64_t>(two.days); } - // Note: duration units can be arbitrary doubles, so we need to use BigInts - // Test case: - // - // Temporal.Duration.compare({ - // milliseconds: 10000000000000, microseconds: 4, nanoseconds: 95 - // }, { - // nanoseconds:10000000000000004000 - // }) - // - // This must return -1, but would return 0 when |double| is used. - // - // Note: BigInt(10000000000000004000) is 10000000000000004096n - - Duration oneTotal = { - 0, - 0, - 0, - days1, - one.hours, - one.minutes, - one.seconds, - one.milliseconds, - one.microseconds, - one.nanoseconds, - }; - Duration twoTotal = { - 0, - 0, - 0, - days2, - two.hours, - two.minutes, - two.seconds, - two.milliseconds, - two.microseconds, - two.nanoseconds, - }; - - // Steps 15-21. - // - // Fast path when the total duration amount fits into an int64. - if (auto ns1 = TotalDurationNanoseconds(oneTotal)) { - if (auto ns2 = TotalDurationNanoseconds(twoTotal)) { - args.rval().setInt32(*ns1 < *ns2 ? -1 : *ns1 > *ns2 ? 1 : 0); - return true; - } - } + // Step 15. + auto normalized1 = NormalizeTimeDuration(one); - // Steps 15 and 17. - Rooted<BigInt*> ns1(cx, TotalDurationNanosecondsSlow(cx, oneTotal)); - if (!ns1) { + // Step 16. + if (!Add24HourDaysToNormalizedTimeDuration(cx, normalized1, days1, + &normalized1)) { return false; } - // Steps 16 and 18. - auto* ns2 = TotalDurationNanosecondsSlow(cx, twoTotal); - if (!ns2) { + // Step 17. + auto normalized2 = NormalizeTimeDuration(two); + + // Step 18. + if (!Add24HourDaysToNormalizedTimeDuration(cx, normalized2, days2, + &normalized2)) { return false; } - // Step 19-21. - args.rval().setInt32(BigInt::compare(ns1, ns2)); + // Step 19. + args.rval().setInt32(CompareNormalizedTimeDuration(normalized1, normalized2)); return true; } @@ -5834,10 +4771,10 @@ static bool Duration_nanoseconds(JSContext* cx, unsigned argc, Value* vp) { * get Temporal.Duration.prototype.sign */ static bool Duration_sign(JSContext* cx, const CallArgs& args) { + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); + // Step 3. - auto* duration = &args.thisv().toObject().as<DurationObject>(); - int32_t sign = DurationSign(ToDuration(duration)); - args.rval().setInt32(sign); + args.rval().setInt32(DurationSign(duration)); return true; } @@ -5854,12 +4791,10 @@ static bool Duration_sign(JSContext* cx, unsigned argc, Value* vp) { * get Temporal.Duration.prototype.blank */ static bool Duration_blank(JSContext* cx, const CallArgs& args) { - // Step 3. - auto* duration = &args.thisv().toObject().as<DurationObject>(); - int32_t sign = DurationSign(ToDuration(duration)); + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); - // Steps 4-5. - args.rval().setBoolean(sign == 0); + // Steps 3-5. + args.rval().setBoolean(duration == Duration{}); return true; } @@ -5878,10 +4813,8 @@ static bool Duration_blank(JSContext* cx, unsigned argc, Value* vp) { * ToPartialDuration ( temporalDurationLike ) */ static bool Duration_with(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as<DurationObject>(); - // Absent values default to the corresponding values of |this| object. - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); // Steps 3-23. Rooted<JSObject*> temporalDurationLike( @@ -5916,8 +4849,7 @@ static bool Duration_with(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.negated ( ) */ static bool Duration_negated(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as<DurationObject>(); - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); // Step 3. auto* result = CreateTemporalDuration(cx, duration.negate()); @@ -5942,8 +4874,7 @@ static bool Duration_negated(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.abs ( ) */ static bool Duration_abs(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as<DurationObject>(); - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); // Step 3. auto* result = CreateTemporalDuration(cx, AbsoluteDuration(duration)); @@ -6002,8 +4933,7 @@ static bool Duration_subtract(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.round ( roundTo ) */ static bool Duration_round(JSContext* cx, const CallArgs& args) { - auto* durationObj = &args.thisv().toObject().as<DurationObject>(); - auto duration = ToDuration(durationObj); + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); // Step 18. (Reordered) auto existingLargestUnit = DefaultTemporalLargestUnit(duration); @@ -6208,7 +5138,7 @@ static bool Duration_round(JSContext* cx, const CallArgs& args) { PlainDateTime relativeToDateTime; if (zonedRelativeTo && plainDateTimeOrRelativeToWillBeUsed) { // Steps 34.a-b. - auto instant = zonedRelativeTo.instant(); + const auto& instant = zonedRelativeTo.instant(); // Step 34.c. if (!GetPlainDateTimeFor(cx, timeZone, instant, &relativeToDateTime)) { @@ -6240,46 +5170,48 @@ static bool Duration_round(JSContext* cx, const CallArgs& args) { // Step 36. DateDuration unbalanceResult; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, duration, largestUnit, - plainRelativeTo, calendar, + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), + largestUnit, plainRelativeTo, calendar, &unbalanceResult)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, duration, largestUnit, - &unbalanceResult)) { + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), + largestUnit, &unbalanceResult)) { return false; } - MOZ_ASSERT(duration.date() == unbalanceResult.toDuration()); + MOZ_ASSERT(duration.toDateDuration() == unbalanceResult); } + MOZ_ASSERT(IsValidDuration(unbalanceResult)); // Steps 37-38. - Duration roundInput = { - unbalanceResult.years, unbalanceResult.months, unbalanceResult.weeks, - unbalanceResult.days, duration.hours, duration.minutes, - duration.seconds, duration.milliseconds, duration.microseconds, - duration.nanoseconds, - }; - Duration roundResult; + auto roundInput = + NormalizedDuration{unbalanceResult, NormalizeTimeDuration(duration)}; + RoundedDuration rounded; if (plainRelativeTo || zonedRelativeTo) { if (!::RoundDuration(cx, roundInput, roundingIncrement, smallestUnit, roundingMode, plainRelativeTo, calendar, zonedRelativeTo, timeZone, precalculatedPlainDateTime, - &roundResult)) { + ComputeRemainder::No, &rounded)) { return false; } } else { + MOZ_ASSERT(IsValidDuration(roundInput)); + if (!::RoundDuration(cx, roundInput, roundingIncrement, smallestUnit, - roundingMode, &roundResult)) { + roundingMode, ComputeRemainder::No, &rounded)) { return false; } } - // Steps 39-40. + // Step 39. + auto roundResult = rounded.duration; + + // Steps 40-41. TimeDuration balanceResult; if (zonedRelativeTo) { - // Step 39.a. - Duration adjustResult; + // Step 40.a. + NormalizedDuration adjustResult; if (!AdjustRoundedDurationDays(cx, roundResult, roundingIncrement, smallestUnit, roundingMode, zonedRelativeTo, calendar, timeZone, @@ -6288,46 +5220,51 @@ static bool Duration_round(JSContext* cx, const CallArgs& args) { } roundResult = adjustResult; - // Step 39.b. + // Step 40.b. if (!BalanceTimeDurationRelative( cx, roundResult, largestUnit, zonedRelativeTo, timeZone, precalculatedPlainDateTime, &balanceResult)) { return false; } } else { - // Step 40.a. - if (!BalanceTimeDuration(cx, roundResult, largestUnit, &balanceResult)) { + // Step 41.a. + NormalizedTimeDuration withDays; + if (!Add24HourDaysToNormalizedTimeDuration( + cx, roundResult.time, roundResult.date.days, &withDays)) { + return false; + } + + // Step 41.b. + if (!temporal::BalanceTimeDuration(cx, withDays, largestUnit, + &balanceResult)) { return false; } } - // Step 41. - Duration balanceInput = { - roundResult.years, - roundResult.months, - roundResult.weeks, + // Step 42. + auto balanceInput = DateDuration{ + roundResult.date.years, + roundResult.date.months, + roundResult.date.weeks, balanceResult.days, }; - DateDuration result; + DateDuration dateResult; if (!::BalanceDateDurationRelative(cx, balanceInput, largestUnit, smallestUnit, plainRelativeTo, calendar, - &result)) { + &dateResult)) { return false; } - // Step 42. - auto* obj = CreateTemporalDuration(cx, { - result.years, - result.months, - result.weeks, - result.days, - balanceResult.hours, - balanceResult.minutes, - balanceResult.seconds, - balanceResult.milliseconds, - balanceResult.microseconds, - balanceResult.nanoseconds, - }); + // Step 43. + auto result = Duration{ + double(dateResult.years), double(dateResult.months), + double(dateResult.weeks), double(dateResult.days), + double(balanceResult.hours), double(balanceResult.minutes), + double(balanceResult.seconds), double(balanceResult.milliseconds), + balanceResult.microseconds, balanceResult.nanoseconds, + }; + + auto* obj = CreateTemporalDuration(cx, result); if (!obj) { return false; } @@ -6404,14 +5341,13 @@ static bool Duration_total(JSContext* cx, const CallArgs& args) { // Step 13. bool plainDateTimeOrRelativeToWillBeUsed = - unit <= TemporalUnit::Day || duration.years != 0 || - duration.months != 0 || duration.weeks != 0 || duration.days != 0; + unit <= TemporalUnit::Day || duration.toDateDuration() != DateDuration{}; // Step 14. PlainDateTime relativeToDateTime; if (zonedRelativeTo && plainDateTimeOrRelativeToWillBeUsed) { // Steps 14.a-b. - auto instant = zonedRelativeTo.instant(); + const auto& instant = zonedRelativeTo.instant(); // Step 14.c. if (!GetPlainDateTimeFor(cx, timeZone, instant, &relativeToDateTime)) { @@ -6443,34 +5379,27 @@ static bool Duration_total(JSContext* cx, const CallArgs& args) { // Step 16. DateDuration unbalanceResult; if (plainRelativeTo) { - if (!UnbalanceDateDurationRelative(cx, duration, unit, plainRelativeTo, - calendar, &unbalanceResult)) { + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), unit, + plainRelativeTo, calendar, + &unbalanceResult)) { return false; } } else { - if (!UnbalanceDateDurationRelative(cx, duration, unit, &unbalanceResult)) { + if (!UnbalanceDateDurationRelative(cx, duration.toDateDuration(), unit, + &unbalanceResult)) { return false; } - MOZ_ASSERT(duration.date() == unbalanceResult.toDuration()); + MOZ_ASSERT(duration.toDateDuration() == unbalanceResult); } - Duration balanceInput = { - 0, - 0, - 0, - unbalanceResult.days, - duration.hours, - duration.minutes, - duration.seconds, - duration.milliseconds, - duration.microseconds, - duration.nanoseconds, - }; + // Step 17. + int64_t unbalancedDays = unbalanceResult.days; - // Steps 17-18. - TimeDuration balanceResult; + // Steps 18-19. + int64_t days; + NormalizedTimeDuration normTime; if (zonedRelativeTo) { - // Step 17.a + // Step 18.a Rooted<ZonedDateTime> intermediate(cx); if (!MoveRelativeZonedDateTime( cx, zonedRelativeTo, calendar, timeZone, @@ -6480,63 +5409,131 @@ static bool Duration_total(JSContext* cx, const CallArgs& args) { return false; } - // Step 17.b. - if (!BalancePossiblyInfiniteTimeDurationRelative( - cx, balanceInput, unit, intermediate, timeZone, &balanceResult)) { + // Step 18.b. + auto timeDuration = NormalizeTimeDuration(duration); + + // Step 18.c + const auto& startNs = intermediate.instant(); + + // Step 18.d. + const auto& startInstant = startNs; + + // Step 18.e. + mozilla::Maybe<PlainDateTime> startDateTime{}; + + // Steps 18.f-g. + Instant intermediateNs; + if (unbalancedDays != 0) { + // Step 18.f.i. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &dateTime)) { + return false; + } + startDateTime = mozilla::Some(dateTime); + + // Step 18.f.ii. + Rooted<CalendarValue> isoCalendar(cx, CalendarValue(cx->names().iso8601)); + Instant addResult; + if (!AddDaysToZonedDateTime(cx, startInstant, dateTime, timeZone, + isoCalendar, unbalancedDays, &addResult)) { + return false; + } + + // Step 18.f.iii. + intermediateNs = addResult; + } else { + // Step 18.g. + intermediateNs = startNs; + } + + // Step 18.h. + Instant endNs; + if (!AddInstant(cx, intermediateNs, timeDuration, &endNs)) { return false; } + + // Step 18.i. + auto difference = + NormalizedTimeDurationFromEpochNanosecondsDifference(endNs, startNs); + + // Steps 18.j-k. + // + // Avoid calling NormalizedTimeDurationToDays for a zero time difference. + if (TemporalUnit::Year <= unit && unit <= TemporalUnit::Day && + difference != NormalizedTimeDuration{}) { + // Step 18.j.i. + if (!startDateTime) { + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &dateTime)) { + return false; + } + startDateTime = mozilla::Some(dateTime); + } + + // Step 18.j.ii. + NormalizedTimeAndDays timeAndDays; + if (!NormalizedTimeDurationToDays(cx, difference, intermediate, timeZone, + *startDateTime, &timeAndDays)) { + return false; + } + + // Step 18.j.iii. + normTime = NormalizedTimeDuration::fromNanoseconds(timeAndDays.time); + + // Step 18.j.iv. + days = timeAndDays.days; + } else { + // Step 18.k.i. + normTime = difference; + days = 0; + } } else { - // Step 18. - if (!BalancePossiblyInfiniteTimeDuration(cx, balanceInput, unit, - &balanceResult)) { + // Step 19.a. + auto timeDuration = NormalizeTimeDuration(duration); + + // Step 19.b. + if (!Add24HourDaysToNormalizedTimeDuration(cx, timeDuration, unbalancedDays, + &normTime)) { return false; } - } - // Steps 19-20. - for (double v : { - balanceResult.days, - balanceResult.hours, - balanceResult.minutes, - balanceResult.seconds, - balanceResult.milliseconds, - balanceResult.microseconds, - balanceResult.nanoseconds, - }) { - if (std::isinf(v)) { - args.rval().setDouble(v); - return true; - } + // Step 19.c. + days = 0; } - MOZ_ASSERT(IsValidDuration(balanceResult.toDuration())); + MOZ_ASSERT(IsValidNormalizedTimeDuration(normTime)); - // Step 21. (Not applicable in our implementation.) - - // Step 22. - Duration roundInput = { - unbalanceResult.years, unbalanceResult.months, - unbalanceResult.weeks, balanceResult.days, - balanceResult.hours, balanceResult.minutes, - balanceResult.seconds, balanceResult.milliseconds, - balanceResult.microseconds, balanceResult.nanoseconds, + // Step 20. + auto roundInput = NormalizedDuration{ + { + unbalanceResult.years, + unbalanceResult.months, + unbalanceResult.weeks, + days, + }, + normTime, }; - double total; + MOZ_ASSERT_IF(unit > TemporalUnit::Day, IsValidDuration(roundInput.date)); + + RoundedDuration rounded; if (plainRelativeTo || zonedRelativeTo) { if (!::RoundDuration(cx, roundInput, Increment{1}, unit, TemporalRoundingMode::Trunc, plainRelativeTo, calendar, zonedRelativeTo, timeZone, precalculatedPlainDateTime, - &total)) { + ComputeRemainder::Yes, &rounded)) { return false; } } else { + MOZ_ASSERT(IsValidDuration(roundInput)); + if (!::RoundDuration(cx, roundInput, Increment{1}, unit, - TemporalRoundingMode::Trunc, &total)) { + TemporalRoundingMode::Trunc, ComputeRemainder::Yes, + &rounded)) { return false; } } - // Step 23. - args.rval().setNumber(total); + // Step 21. + args.rval().setNumber(rounded.total); return true; } @@ -6605,64 +5602,37 @@ static bool Duration_toString(JSContext* cx, const CallArgs& args) { if (precision.unit != TemporalUnit::Nanosecond || precision.increment != Increment{1}) { // Step 10.a. - auto largestUnit = DefaultTemporalLargestUnit(duration); + auto timeDuration = NormalizeTimeDuration(duration); - // Steps 10.b-c. - auto toRound = Duration{ - 0, - 0, - 0, - 0, - 0, - 0, - duration.seconds, - duration.milliseconds, - duration.microseconds, - duration.nanoseconds, - }; - Duration roundResult; - if (!temporal::RoundDuration(cx, toRound, precision.increment, - precision.unit, roundingMode, &roundResult)) { - return false; - } + // Step 10.b. + auto largestUnit = DefaultTemporalLargestUnit(duration); - // Step 10.d. - auto toBalance = Duration{ - 0, - 0, - 0, - duration.days, - duration.hours, - duration.minutes, - roundResult.seconds, - roundResult.milliseconds, - roundResult.microseconds, - roundResult.nanoseconds, - }; - TimeDuration balanceResult; - if (!BalanceTimeDuration(cx, toBalance, largestUnit, &balanceResult)) { + // Steps 10.c-d. + NormalizedTimeDuration rounded; + if (!RoundDuration(cx, timeDuration, precision.increment, precision.unit, + roundingMode, &rounded)) { return false; } // Step 10.e. + auto balanced = BalanceTimeDuration( + rounded, std::min(largestUnit, TemporalUnit::Second)); + + // Step 10.f. result = { - duration.years, - duration.months, - duration.weeks, - balanceResult.days, - balanceResult.hours, - balanceResult.minutes, - balanceResult.seconds, - balanceResult.milliseconds, - balanceResult.microseconds, - balanceResult.nanoseconds, + duration.years, duration.months, + duration.weeks, duration.days + double(balanced.days), + double(balanced.hours), double(balanced.minutes), + double(balanced.seconds), double(balanced.milliseconds), + balanced.microseconds, balanced.nanoseconds, }; + MOZ_ASSERT(IsValidDuration(duration)); } else { // Step 11. result = duration; } - // Step 12. + // Steps 12-13. JSString* str = TemporalDurationToString(cx, result, precision.precision); if (!str) { return false; @@ -6682,14 +5652,13 @@ static bool Duration_toString(JSContext* cx, unsigned argc, Value* vp) { } /** - * Temporal.Duration.prototype.toJSON ( ) + * Temporal.Duration.prototype.toJSON ( ) */ static bool Duration_toJSON(JSContext* cx, const CallArgs& args) { - auto* duration = &args.thisv().toObject().as<DurationObject>(); + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); - // Step 3. - JSString* str = - TemporalDurationToString(cx, ToDuration(duration), Precision::Auto()); + // Steps 3-4. + JSString* str = TemporalDurationToString(cx, duration, Precision::Auto()); if (!str) { return false; } @@ -6699,7 +5668,7 @@ static bool Duration_toJSON(JSContext* cx, const CallArgs& args) { } /** - * Temporal.Duration.prototype.toJSON ( ) + * Temporal.Duration.prototype.toJSON ( ) */ static bool Duration_toJSON(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. @@ -6711,11 +5680,10 @@ static bool Duration_toJSON(JSContext* cx, unsigned argc, Value* vp) { * Temporal.Duration.prototype.toLocaleString ( [ locales [ , options ] ] ) */ static bool Duration_toLocaleString(JSContext* cx, const CallArgs& args) { - auto* duration = &args.thisv().toObject().as<DurationObject>(); + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); - // Step 3. - JSString* str = - TemporalDurationToString(cx, ToDuration(duration), Precision::Auto()); + // Steps 3-4. + JSString* str = TemporalDurationToString(cx, duration, Precision::Auto()); if (!str) { return false; } |