Source code
Revision control
Copy as Markdown
Other Tools
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=8 sts=2 et sw=2 tw=80:
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
#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"
#include "mozilla/Maybe.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <initializer_list>
#include <stdint.h>
#include <type_traits>
#include <utility>
#include "jsnum.h"
#include "jspubtd.h"
#include "NamespaceImports.h"
#include "builtin/temporal/Calendar.h"
#include "builtin/temporal/CalendarFields.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/PlainTime.h"
#include "builtin/temporal/Temporal.h"
#include "builtin/temporal/TemporalParser.h"
#include "builtin/temporal/TemporalRoundingMode.h"
#include "builtin/temporal/TemporalTypes.h"
#include "builtin/temporal/TemporalUnit.h"
#include "builtin/temporal/TimeZone.h"
#include "builtin/temporal/ZonedDateTime.h"
#include "gc/AllocKind.h"
#include "gc/Barrier.h"
#include "gc/GCEnum.h"
#include "js/CallArgs.h"
#include "js/CallNonGenericMethod.h"
#include "js/Class.h"
#include "js/Conversions.h"
#include "js/ErrorReport.h"
#include "js/friend/ErrorMessages.h"
#include "js/GCVector.h"
#include "js/Id.h"
#include "js/Printer.h"
#include "js/PropertyDescriptor.h"
#include "js/PropertySpec.h"
#include "js/RootingAPI.h"
#include "js/Value.h"
#include "util/StringBuilder.h"
#include "vm/BytecodeUtil.h"
#include "vm/GlobalObject.h"
#include "vm/JSAtomState.h"
#include "vm/JSContext.h"
#include "vm/JSObject.h"
#include "vm/ObjectOperations.h"
#include "vm/PlainObject.h"
#include "vm/StringType.h"
#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"
#include "vm/ObjectOperations-inl.h"
using namespace js;
using namespace js::temporal;
static inline bool IsDuration(Handle<Value> v) {
return v.isObject() && v.toObject().is<DurationObject>();
}
#ifdef DEBUG
static bool IsIntegerOrInfinity(double d) {
return IsInteger(d) || std::isinf(d);
}
static bool IsIntegerOrInfinityDuration(const Duration& duration) {
const auto& [years, months, weeks, days, hours, minutes, seconds,
milliseconds, microseconds, nanoseconds] = duration;
// Integers exceeding the Number range are represented as infinity.
return IsIntegerOrInfinity(years) && IsIntegerOrInfinity(months) &&
IsIntegerOrInfinity(weeks) && IsIntegerOrInfinity(days) &&
IsIntegerOrInfinity(hours) && IsIntegerOrInfinity(minutes) &&
IsIntegerOrInfinity(seconds) && IsIntegerOrInfinity(milliseconds) &&
IsIntegerOrInfinity(microseconds) && IsIntegerOrInfinity(nanoseconds);
}
static bool IsIntegerDuration(const Duration& 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) &&
IsInteger(seconds) && IsInteger(milliseconds) &&
IsInteger(microseconds) && IsInteger(nanoseconds);
}
#endif
/**
* DurationSign ( duration )
*/
int32_t js::temporal::DurationSign(const Duration& duration) {
MOZ_ASSERT(IsIntegerOrInfinityDuration(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,
milliseconds, microseconds, nanoseconds}) {
// Step 1.a.
if (v < 0) {
return -1;
}
// Step 1.b.
if (v > 0) {
return 1;
}
}
// Step 2.
return 0;
}
/**
* DateDurationSign ( dateDuration )
*/
int32_t js::temporal::DateDurationSign(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;
}
/**
* InternalDurationSign ( internalDuration )
*/
static int32_t InternalDurationSign(const InternalDuration& duration) {
MOZ_ASSERT(IsValidDuration(duration));
if (int32_t sign = DateDurationSign(duration.date)) {
return sign;
}
return TimeDurationSign(duration.time);
}
/**
* Create a time duration from a nanoseconds amount.
*/
static TimeDuration TimeDurationFromNanoseconds(const Int96& nanoseconds) {
// Split into seconds and nanoseconds.
auto [seconds, nanos] = nanoseconds / ToNanoseconds(TemporalUnit::Second);
return {seconds, nanos};
}
/**
* Create a time duration from a nanoseconds amount. Return Nothing if the value
* is too large.
*/
static mozilla::Maybe<TimeDuration> TimeDurationFromNanoseconds(
double nanoseconds) {
MOZ_ASSERT(IsInteger(nanoseconds));
if (auto int96 = Int96::fromInteger(nanoseconds)) {
// The number of time duration 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(TimeDurationFromNanoseconds(*int96));
}
}
return mozilla::Nothing();
}
/**
* Create a time duration from a microseconds amount.
*/
static TimeDuration TimeDurationFromMicroseconds(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};
}
/**
* Create a time duration from a microseconds amount. Return Nothing if the
* value is too large.
*/
static mozilla::Maybe<TimeDuration> TimeDurationFromMicroseconds(
double microseconds) {
MOZ_ASSERT(IsInteger(microseconds));
if (auto int96 = Int96::fromInteger(microseconds)) {
// The number of time duration 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(TimeDurationFromMicroseconds(*int96));
}
}
return mozilla::Nothing();
}
/**
* Create a time duration from a duration. Return Nothing if any duration
* value is too large.
*/
static mozilla::Maybe<TimeDuration> TimeDurationFromDuration(
const Duration& duration) {
do {
auto nanoseconds = TimeDurationFromNanoseconds(duration.nanoseconds);
if (!nanoseconds) {
break;
}
MOZ_ASSERT(IsValidTimeDuration(*nanoseconds));
auto microseconds = TimeDurationFromMicroseconds(duration.microseconds);
if (!microseconds) {
break;
}
MOZ_ASSERT(IsValidTimeDuration(*microseconds));
// Overflows for millis/seconds/minutes/hours/days always result in an
// invalid 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 = TimeDuration::fromMilliseconds(millis.value());
if (!IsValidTimeDuration(milli)) {
break;
}
// Compute the overall time duration.
auto result = milli + *microseconds + *nanoseconds;
if (!IsValidTimeDuration(result)) {
break;
}
return mozilla::Some(result);
} while (false);
return mozilla::Nothing();
}
/**
* TimeDurationFromComponents ( hours, minutes, seconds, milliseconds,
* microseconds, nanoseconds )
*/
static TimeDuration TimeDurationFromComponents(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 timeDuration = TimeDuration::fromMilliseconds(millis.value());
// Step 4.
auto micros = Int96::fromInteger(microseconds);
MOZ_ASSERT(micros);
timeDuration += TimeDurationFromMicroseconds(*micros);
// Step 5.
auto nanos = Int96::fromInteger(nanoseconds);
MOZ_ASSERT(nanos);
timeDuration += TimeDurationFromNanoseconds(*nanos);
// Step 6.
MOZ_ASSERT(IsValidTimeDuration(timeDuration));
// Step 7.
return timeDuration;
}
/**
* TimeDurationFromComponents ( hours, minutes, seconds, milliseconds,
* microseconds, nanoseconds )
*/
TimeDuration js::temporal::TimeDurationFromComponents(
const Duration& duration) {
MOZ_ASSERT(IsValidDuration(duration));
return ::TimeDurationFromComponents(
duration.hours, duration.minutes, duration.seconds, duration.milliseconds,
duration.microseconds, duration.nanoseconds);
}
/**
* Add24HourDaysToTimeDuration ( d, days )
*/
static bool Add24HourDaysToTimeDuration(JSContext* cx, const TimeDuration& d,
int64_t days, TimeDuration* result) {
MOZ_ASSERT(IsValidTimeDuration(d));
// Step 1.
if (days > TimeDuration::max().toDays()) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME);
return false;
}
auto timeDurationDays = TimeDuration::fromDays(days);
if (!IsValidTimeDuration(timeDurationDays)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME);
return false;
}
// Step 2.
auto sum = d + timeDurationDays;
if (!IsValidTimeDuration(sum)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME);
return false;
}
// Step 3.
*result = sum;
return true;
}
/**
* ToInternalDurationRecordWith24HourDays ( duration )
*/
InternalDuration js::temporal::ToInternalDurationRecordWith24HourDays(
const Duration& duration) {
MOZ_ASSERT(IsValidDuration(duration));
// Step 1.
auto timeDuration = TimeDurationFromComponents(duration);
// Step 2. (Inlined Add24HourDaysToTimeDuration)
timeDuration += TimeDuration::fromDays(int64_t(duration.days));
// Step 3.
auto dateDuration = DateDuration{
int64_t(duration.years),
int64_t(duration.months),
int64_t(duration.weeks),
0,
};
// Step 4. (Inlined CombineDateAndTimeDuration)
return InternalDuration{dateDuration, timeDuration};
}
/**
* ToDateDurationRecordWithoutTime ( duration )
*/
DateDuration js::temporal::ToDateDurationRecordWithoutTime(
const Duration& duration) {
// Step 1.
auto internalDuration = ToInternalDurationRecordWith24HourDays(duration);
// Step 2.
int64_t days = internalDuration.time.toDays();
// Step 3.
auto result = DateDuration{
internalDuration.date.years,
internalDuration.date.months,
internalDuration.date.weeks,
days,
};
// TODO: This is fallible per spec, but is it really fallible?
MOZ_ASSERT(IsValidDuration(result));
return result;
}
/**
* TemporalDurationFromInternal ( internalDuration, largestUnit )
*/
static Duration TemporalDurationFromInternal(const TimeDuration& timeDuration,
TemporalUnit largestUnit) {
MOZ_ASSERT(IsValidTimeDuration(timeDuration));
MOZ_ASSERT(largestUnit <= TemporalUnit::Second,
"fallible fractional seconds units");
auto [seconds, nanoseconds] = timeDuration.denormalize();
// Step 1.
int64_t days = 0;
int64_t hours = 0;
int64_t minutes = 0;
int64_t milliseconds = 0;
int64_t microseconds = 0;
// 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 4-11.
switch (largestUnit) {
// Step 4.
case TemporalUnit::Year:
case TemporalUnit::Month:
case TemporalUnit::Week:
case TemporalUnit::Day: {
// Step 4.a.
microseconds = nanoseconds / 1000;
// Step 4.b.
nanoseconds = nanoseconds % 1000;
// Step 4.c.
milliseconds = microseconds / 1000;
// Step 4.d.
microseconds = microseconds % 1000;
// Steps 4.e-f. (Not applicable)
MOZ_ASSERT(std::abs(milliseconds) <= 999);
// Step 4.g.
minutes = seconds / 60;
// Step 4.h.
seconds = seconds % 60;
// Step 4.i.
hours = minutes / 60;
// Step 4.j.
minutes = minutes % 60;
// Step 4.k.
days = hours / 24;
// Step 4.l.
hours = hours % 24;
break;
}
// Step 5.
case TemporalUnit::Hour: {
// Step 5.a.
microseconds = nanoseconds / 1000;
// Step 5.b.
nanoseconds = nanoseconds % 1000;
// Step 5.c.
milliseconds = microseconds / 1000;
// Step 5.d.
microseconds = microseconds % 1000;
// Steps 5.e-f. (Not applicable)
MOZ_ASSERT(std::abs(milliseconds) <= 999);
// Step 5.g.
minutes = seconds / 60;
// Step 5.h.
seconds = seconds % 60;
// Step 5.i.
hours = minutes / 60;
// Step 5.j.
minutes = minutes % 60;
break;
}
case TemporalUnit::Minute: {
// Step 6.a.
microseconds = nanoseconds / 1000;
// Step 6.b.
nanoseconds = nanoseconds % 1000;
// Step 6.c.
milliseconds = microseconds / 1000;
// Step 6.d.
microseconds = microseconds % 1000;
// Steps 6.e-f. (Not applicable)
MOZ_ASSERT(std::abs(milliseconds) <= 999);
// Step 6.g.
minutes = seconds / 60;
// Step 6.h.
seconds = seconds % 60;
break;
}
// Step 7.
case TemporalUnit::Second: {
// Step 7.a.
microseconds = nanoseconds / 1000;
// Step 7.b.
nanoseconds = nanoseconds % 1000;
// Step 7.c.
milliseconds = microseconds / 1000;
// Step 7.d.
microseconds = microseconds % 1000;
// Steps 7.e-f. (Not applicable)
MOZ_ASSERT(std::abs(milliseconds) <= 999);
break;
}
// Steps 8-11. (Not applicable in our implementation)
case TemporalUnit::Millisecond:
case TemporalUnit::Microsecond:
case TemporalUnit::Nanosecond:
case TemporalUnit::Auto:
MOZ_CRASH("Unexpected temporal unit");
}
// Step 12.
auto result = Duration{
0,
0,
0,
double(days),
double(hours),
double(minutes),
double(seconds),
double(milliseconds),
double(microseconds),
double(nanoseconds),
};
MOZ_ASSERT(IsValidDuration(result));
return result;
}
/**
* TemporalDurationFromInternal ( internalDuration, largestUnit )
*/
bool js::temporal::TemporalDurationFromInternal(
JSContext* cx, const TimeDuration& timeDuration, TemporalUnit largestUnit,
Duration* result) {
MOZ_ASSERT(IsValidTimeDuration(timeDuration));
auto [seconds, nanoseconds] = timeDuration.denormalize();
// 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) {
// Steps 4-7.
case TemporalUnit::Year:
case TemporalUnit::Month:
case TemporalUnit::Week:
case TemporalUnit::Day:
case TemporalUnit::Hour:
case TemporalUnit::Minute:
case TemporalUnit::Second:
*result = ::TemporalDurationFromInternal(timeDuration, largestUnit);
return true;
// Step 8.
case TemporalUnit::Millisecond: {
// Valid time durations must be below |limit|.
constexpr auto limit = TimeDuration::max().toMilliseconds() + 1;
// The largest possible milliseconds value whose double representation
// doesn't exceed the time duration limit.
constexpr auto max = int64_t(0x7cff'ffff'ffff'fdff);
// Assert |max| is the maximum allowed milliseconds value.
static_assert(double(max) < double(limit));
static_assert(double(max + 1) >= double(limit));
static_assert((TimeDuration::max().seconds + 1) *
ToMilliseconds(TemporalUnit::Second) <=
INT64_MAX,
"total number duration milliseconds fits into int64");
// Step 8.a.
int64_t microseconds = nanoseconds / 1000;
// Step 8.b.
nanoseconds = nanoseconds % 1000;
// Step 8.c.
int64_t milliseconds = microseconds / 1000;
MOZ_ASSERT(std::abs(milliseconds) <= 999);
// Step 8.d.
microseconds = microseconds % 1000;
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;
}
// Step 11.
*result = {0,
0,
0,
0,
0,
0,
0,
double(millis),
double(microseconds),
double(nanoseconds)};
MOZ_ASSERT(IsValidDuration(*result));
return true;
}
// Step 9.
case TemporalUnit::Microsecond: {
// Valid time durations must be below |limit|.
constexpr auto limit =
Uint128{TimeDuration::max().toMicroseconds()} + Uint128{1};
// The largest possible microseconds value whose double representation
// doesn't exceed the time duration limit.
constexpr auto max =
(Uint128{0x1e8} << 64) + Uint128{0x47ff'ffff'fff7'ffff};
static_assert(max < limit);
// Assert |max| is the maximum allowed microseconds value.
MOZ_ASSERT(double(max) < double(limit));
MOZ_ASSERT(double(max + Uint128{1}) >= double(limit));
// Step 9.a.
int64_t microseconds = nanoseconds / 1000;
MOZ_ASSERT(std::abs(microseconds) <= 999'999);
// Step 9.b.
nanoseconds = nanoseconds % 1000;
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;
}
// Step 11.
*result = {0, 0, 0, 0, 0, 0, 0, 0, double(micros), double(nanoseconds)};
MOZ_ASSERT(IsValidDuration(*result));
return true;
}
// Step 10.
case TemporalUnit::Nanosecond: {
// Valid time durations must be below |limit|.
constexpr auto limit =
Uint128{TimeDuration::max().toNanoseconds()} + Uint128{1};
// The largest possible nanoseconds value whose double representation
// doesn't exceed the time duration 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;
}
// Step 11.
*result = {0, 0, 0, 0, 0, 0, 0, 0, 0, double(nanos)};
MOZ_ASSERT(IsValidDuration(*result));
return true;
}
case TemporalUnit::Auto:
break;
}
MOZ_CRASH("Unexpected temporal unit");
}
/**
* TemporalDurationFromInternal ( internalDuration, largestUnit )
*/
bool js::temporal::TemporalDurationFromInternal(
JSContext* cx, const InternalDuration& internalDuration,
TemporalUnit largestUnit, Duration* result) {
MOZ_ASSERT(IsValidDuration(internalDuration.date));
MOZ_ASSERT(IsValidTimeDuration(internalDuration.time));
// Steps 1-11.
Duration duration;
if (!TemporalDurationFromInternal(cx, internalDuration.time, largestUnit,
&duration)) {
return false;
}
MOZ_ASSERT(IsValidDuration(duration));
// Step 12.
auto days = mozilla::CheckedInt64(internalDuration.date.days) +
mozilla::AssertedCast<int64_t>(duration.days);
MOZ_ASSERT(days.isValid(), "valid duration days can't overflow");
*result = {
double(internalDuration.date.years),
double(internalDuration.date.months),
double(internalDuration.date.weeks),
double(days.value()),
duration.hours,
duration.minutes,
duration.seconds,
duration.milliseconds,
duration.microseconds,
duration.nanoseconds,
};
return ThrowIfInvalidDuration(cx, *result);
}
/**
* TimeDurationFromEpochNanosecondsDifference ( one, two )
*/
TimeDuration js::temporal::TimeDurationFromEpochNanosecondsDifference(
const EpochNanoseconds& one, const EpochNanoseconds& two) {
MOZ_ASSERT(IsValidEpochNanoseconds(one));
MOZ_ASSERT(IsValidEpochNanoseconds(two));
// Step 1.
auto result = one - two;
// Step 2.
MOZ_ASSERT(IsValidEpochDuration(result));
// Step 3.
return result.to<TimeDuration>();
}
#ifdef DEBUG
/**
* IsValidDuration ( years, months, weeks, days, hours, minutes, seconds,
* milliseconds, microseconds, nanoseconds )
*/
bool js::temporal::IsValidDuration(const Duration& duration) {
MOZ_ASSERT(IsIntegerOrInfinityDuration(duration));
const auto& [years, months, weeks, days, hours, minutes, seconds,
milliseconds, microseconds, nanoseconds] = duration;
// Step 1.
int32_t sign = 0;
// Step 2.
for (auto v : {years, months, weeks, days, hours, minutes, seconds,
milliseconds, microseconds, nanoseconds}) {
// Step 2.a.
if (!std::isfinite(v)) {
return false;
}
// Step 2.b.
if (v < 0) {
// Step 2.b.i.
if (sign > 0) {
return false;
}
// Step 2.b.ii.
sign = -1;
}
// Step 2.c.
else if (v > 0) {
// Step 2.c.i.
if (sign < 0) {
return false;
}
// Step 2.c.ii.
sign = 1;
}
}
// 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 (!TimeDurationFromDuration(duration)) {
return false;
}
// Step 9.
return true;
}
/**
* 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 InternalDuration& duration) {
if (!IsValidTimeDuration(duration.time)) {
return false;
}
auto d = duration.date.toDuration();
auto [seconds, nanoseconds] = duration.time.denormalize();
d.seconds = double(seconds);
d.nanoseconds = double(nanoseconds);
return IsValidDuration(d);
}
#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 )
*/
bool js::temporal::ThrowIfInvalidDuration(JSContext* cx,
const Duration& duration) {
MOZ_ASSERT(IsIntegerOrInfinityDuration(duration));
const auto& [years, months, weeks, days, hours, minutes, seconds,
milliseconds, microseconds, nanoseconds] = duration;
// Step 1.
int32_t sign = DurationSign(duration);
auto throwIfInvalid = [&](double v, const char* name) {
// Step 2.a.
if (!std::isfinite(v)) {
return ThrowInvalidDurationPart(
cx, v, name, JSMSG_TEMPORAL_DURATION_INVALID_NON_FINITE);
}
// Steps 2.b-c.
if ((v < 0 && sign > 0) || (v > 0 && sign < 0)) {
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;
}
if (!throwIfInvalid(months, "months")) {
return false;
}
if (!throwIfInvalid(weeks, "weeks")) {
return false;
}
if (!throwIfInvalid(days, "days")) {
return false;
}
if (!throwIfInvalid(hours, "hours")) {
return false;
}
if (!throwIfInvalid(minutes, "minutes")) {
return false;
}
if (!throwIfInvalid(seconds, "seconds")) {
return false;
}
if (!throwIfInvalid(milliseconds, "milliseconds")) {
return false;
}
if (!throwIfInvalid(microseconds, "microseconds")) {
return false;
}
if (!throwIfInvalid(nanoseconds, "nanoseconds")) {
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 (!TimeDurationFromDuration(duration)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME);
return false;
}
MOZ_ASSERT(IsValidDuration(duration));
// Step 9.
return true;
}
/**
* DefaultTemporalLargestUnit ( duration )
*/
static TemporalUnit DefaultTemporalLargestUnit(const Duration& duration) {
MOZ_ASSERT(IsIntegerDuration(duration));
// Step 1.
if (duration.years != 0) {
return TemporalUnit::Year;
}
// Step 2.
if (duration.months != 0) {
return TemporalUnit::Month;
}
// Step 3.
if (duration.weeks != 0) {
return TemporalUnit::Week;
}
// Step 4.
if (duration.days != 0) {
return TemporalUnit::Day;
}
// Step 5.
if (duration.hours != 0) {
return TemporalUnit::Hour;
}
// Step 6.
if (duration.minutes != 0) {
return TemporalUnit::Minute;
}
// Step 7.
if (duration.seconds != 0) {
return TemporalUnit::Second;
}
// Step 8.
if (duration.milliseconds != 0) {
return TemporalUnit::Millisecond;
}
// Step 9.
if (duration.microseconds != 0) {
return TemporalUnit::Microsecond;
}
// Step 10.
return TemporalUnit::Nanosecond;
}
/**
* CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds,
* milliseconds, microseconds, nanoseconds [ , newTarget ] )
*/
static DurationObject* CreateTemporalDuration(JSContext* cx,
const CallArgs& args,
const Duration& duration) {
const auto& [years, months, weeks, days, hours, minutes, seconds,
milliseconds, microseconds, nanoseconds] = duration;
// Step 1.
if (!ThrowIfInvalidDuration(cx, duration)) {
return nullptr;
}
// Steps 2-3.
Rooted<JSObject*> proto(cx);
if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Duration, &proto)) {
return nullptr;
}
auto* object = NewObjectWithClassProto<DurationObject>(cx, proto);
if (!object) {
return nullptr;
}
// Steps 4-13.
// Add zero to convert -0 to +0.
object->setFixedSlot(DurationObject::YEARS_SLOT, NumberValue(years + (+0.0)));
object->setFixedSlot(DurationObject::MONTHS_SLOT,
NumberValue(months + (+0.0)));
object->setFixedSlot(DurationObject::WEEKS_SLOT, NumberValue(weeks + (+0.0)));
object->setFixedSlot(DurationObject::DAYS_SLOT, NumberValue(days + (+0.0)));
object->setFixedSlot(DurationObject::HOURS_SLOT, NumberValue(hours + (+0.0)));
object->setFixedSlot(DurationObject::MINUTES_SLOT,
NumberValue(minutes + (+0.0)));
object->setFixedSlot(DurationObject::SECONDS_SLOT,
NumberValue(seconds + (+0.0)));
object->setFixedSlot(DurationObject::MILLISECONDS_SLOT,
NumberValue(milliseconds + (+0.0)));
object->setFixedSlot(DurationObject::MICROSECONDS_SLOT,
NumberValue(microseconds + (+0.0)));
object->setFixedSlot(DurationObject::NANOSECONDS_SLOT,
NumberValue(nanoseconds + (+0.0)));
// Step 14.
return object;
}
/**
* CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds,
* milliseconds, microseconds, nanoseconds [ , newTarget ] )
*/
DurationObject* js::temporal::CreateTemporalDuration(JSContext* cx,
const Duration& duration) {
const auto& [years, months, weeks, days, hours, minutes, seconds,
milliseconds, microseconds, nanoseconds] = duration;
MOZ_ASSERT(IsInteger(years));
MOZ_ASSERT(IsInteger(months));
MOZ_ASSERT(IsInteger(weeks));
MOZ_ASSERT(IsInteger(days));
MOZ_ASSERT(IsInteger(hours));
MOZ_ASSERT(IsInteger(minutes));
MOZ_ASSERT(IsInteger(seconds));
MOZ_ASSERT(IsInteger(milliseconds));
MOZ_ASSERT(IsInteger(microseconds));
MOZ_ASSERT(IsInteger(nanoseconds));
// Step 1.
if (!ThrowIfInvalidDuration(cx, duration)) {
return nullptr;
}
// Steps 2-3.
auto* object = NewBuiltinClassInstance<DurationObject>(cx);
if (!object) {
return nullptr;
}
// Steps 4-13.
// Add zero to convert -0 to +0.
object->setFixedSlot(DurationObject::YEARS_SLOT, NumberValue(years + (+0.0)));
object->setFixedSlot(DurationObject::MONTHS_SLOT,
NumberValue(months + (+0.0)));
object->setFixedSlot(DurationObject::WEEKS_SLOT, NumberValue(weeks + (+0.0)));
object->setFixedSlot(DurationObject::DAYS_SLOT, NumberValue(days + (+0.0)));
object->setFixedSlot(DurationObject::HOURS_SLOT, NumberValue(hours + (+0.0)));
object->setFixedSlot(DurationObject::MINUTES_SLOT,
NumberValue(minutes + (+0.0)));
object->setFixedSlot(DurationObject::SECONDS_SLOT,
NumberValue(seconds + (+0.0)));
object->setFixedSlot(DurationObject::MILLISECONDS_SLOT,
NumberValue(milliseconds + (+0.0)));
object->setFixedSlot(DurationObject::MICROSECONDS_SLOT,
NumberValue(microseconds + (+0.0)));
object->setFixedSlot(DurationObject::NANOSECONDS_SLOT,
NumberValue(nanoseconds + (+0.0)));
// Step 14.
return object;
}
/**
* ToIntegerIfIntegral ( argument )
*/
static bool ToIntegerIfIntegral(JSContext* cx, const char* name,
Handle<Value> argument, double* num) {
// Step 1.
double d;
if (!JS::ToNumber(cx, argument, &d)) {
return false;
}
// Step 2.
if (!js::IsInteger(d)) {
ToCStringBuf cbuf;
const char* numStr = NumberToCString(&cbuf, d);
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_NOT_INTEGER, numStr,
name);
return false;
}
// Step 3.
*num = d;
return true;
}
/**
* ToIntegerIfIntegral ( argument )
*/
static bool ToIntegerIfIntegral(JSContext* cx, Handle<PropertyName*> name,
Handle<Value> argument, double* result) {
// Step 1.
double d;
if (!JS::ToNumber(cx, argument, &d)) {
return false;
}
// Step 2.
if (!js::IsInteger(d)) {
if (auto nameStr = js::QuoteString(cx, name)) {
ToCStringBuf cbuf;
const char* numStr = NumberToCString(&cbuf, d);
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_NOT_INTEGER, numStr,
nameStr.get());
}
return false;
}
// Step 3.
*result = d;
return true;
}
/**
* ToTemporalPartialDurationRecord ( temporalDurationLike )
*/
static bool ToTemporalPartialDurationRecord(
JSContext* cx, Handle<JSObject*> temporalDurationLike, Duration* result) {
// Steps 1-3. (Not applicable in our implementation.)
Rooted<Value> value(cx);
bool any = false;
auto getDurationProperty = [&](Handle<PropertyName*> name, double* num) {
if (!GetProperty(cx, temporalDurationLike, temporalDurationLike, name,
&value)) {
return false;
}
if (!value.isUndefined()) {
any = true;
if (!ToIntegerIfIntegral(cx, name, value, num)) {
return false;
}
}
return true;
};
// Steps 4-23.
if (!getDurationProperty(cx->names().days, &result->days)) {
return false;
}
if (!getDurationProperty(cx->names().hours, &result->hours)) {
return false;
}
if (!getDurationProperty(cx->names().microseconds, &result->microseconds)) {
return false;
}
if (!getDurationProperty(cx->names().milliseconds, &result->milliseconds)) {
return false;
}
if (!getDurationProperty(cx->names().minutes, &result->minutes)) {
return false;
}
if (!getDurationProperty(cx->names().months, &result->months)) {
return false;
}
if (!getDurationProperty(cx->names().nanoseconds, &result->nanoseconds)) {
return false;
}
if (!getDurationProperty(cx->names().seconds, &result->seconds)) {
return false;
}
if (!getDurationProperty(cx->names().weeks, &result->weeks)) {
return false;
}
if (!getDurationProperty(cx->names().years, &result->years)) {
return false;
}
// Step 24.
if (!any) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_MISSING_UNIT);
return false;
}
// Step 25.
return true;
}
/**
* ToTemporalDuration ( item )
*/
bool js::temporal::ToTemporalDuration(JSContext* cx, Handle<Value> item,
Duration* result) {
// Steps 1 and 3-15.
if (item.isObject()) {
Rooted<JSObject*> itemObj(cx, &item.toObject());
// Step 1.
if (auto* duration = itemObj->maybeUnwrapIf<DurationObject>()) {
*result = ToDuration(duration);
return true;
}
// Step 3. (Reordered)
Duration duration = {};
// Steps 4-14.
if (!ToTemporalPartialDurationRecord(cx, itemObj, &duration)) {
return false;
}
// Step 15.
if (!ThrowIfInvalidDuration(cx, duration)) {
return false;
}
*result = duration;
return true;
}
// Step 2.a.
if (!item.isString()) {
ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, item,
nullptr, "not a string");
return false;
}
Rooted<JSString*> string(cx, item.toString());
// Step 2.b.
return ParseTemporalDurationString(cx, string, result);
}
/**
* DateDurationDays ( dateDuration, plainRelativeTo )
*/
static bool DateDurationDays(JSContext* cx, const DateDuration& duration,
Handle<PlainDate> plainRelativeTo,
int64_t* result) {
MOZ_ASSERT(IsValidDuration(duration));
auto [years, months, weeks, days] = duration;
// Step 1.
auto yearsMonthsWeeksDuration = DateDuration{years, months, weeks};
// Step 2.
if (yearsMonthsWeeksDuration == DateDuration{}) {
*result = days;
return true;
}
// Moved from caller.
if (!plainRelativeTo) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_UNCOMPARABLE,
"relativeTo");
return false;
}
// Step 3.
ISODate later;
if (!CalendarDateAdd(cx, plainRelativeTo.calendar(), plainRelativeTo,
yearsMonthsWeeksDuration, TemporalOverflow::Constrain,
&later)) {
return false;
}
// Step 4.
int32_t epochDays1 = MakeDay(plainRelativeTo);
MOZ_ASSERT(MinEpochDay <= epochDays1 && epochDays1 <= MaxEpochDay);
// Step 5.
int32_t epochDays2 = MakeDay(later);
MOZ_ASSERT(MinEpochDay <= epochDays2 && epochDays2 <= MaxEpochDay);
// Step 4.
int32_t yearsMonthsWeeksInDay = epochDays2 - epochDays1;
// Step 5.
*result = days + yearsMonthsWeeksInDay;
return true;
}
static bool NumberToStringBuilder(JSContext* cx, double num,
JSStringBuilder& sb) {
MOZ_ASSERT(IsInteger(num));
MOZ_ASSERT(num >= 0);
MOZ_ASSERT(num < DOUBLE_INTEGRAL_PRECISION_LIMIT);
ToCStringBuf cbuf;
size_t length;
const char* numStr = NumberToCString(&cbuf, num, &length);
return sb.append(numStr, length);
}
static Duration AbsoluteDuration(const Duration& duration) {
return {
std::abs(duration.years), std::abs(duration.months),
std::abs(duration.weeks), std::abs(duration.days),
std::abs(duration.hours), std::abs(duration.minutes),
std::abs(duration.seconds), std::abs(duration.milliseconds),
std::abs(duration.microseconds), std::abs(duration.nanoseconds),
};
}
/**
* FormatFractionalSeconds ( subSecondNanoseconds, precision )
*/
[[nodiscard]] static bool FormatFractionalSeconds(JSStringBuilder& result,
int32_t subSecondNanoseconds,
Precision precision) {
MOZ_ASSERT(0 <= subSecondNanoseconds && subSecondNanoseconds < 1'000'000'000);
MOZ_ASSERT(precision != Precision::Minute());
// Steps 1-2.
if (precision == Precision::Auto()) {
// Step 1.a.
if (subSecondNanoseconds == 0) {
return true;
}
// Step 3. (Reordered)
if (!result.append('.')) {
return false;
}
// Steps 1.b-c.
int32_t k = 100'000'000;
do {
if (!result.append(char('0' + (subSecondNanoseconds / k)))) {
return false;
}
subSecondNanoseconds %= k;
k /= 10;
} while (subSecondNanoseconds);
} else {
// Step 2.a.
uint8_t p = precision.value();
if (p == 0) {
return true;
}
// Step 3. (Reordered)
if (!result.append('.')) {
return false;
}
// Steps 2.b-c.
int32_t k = 100'000'000;
for (uint8_t i = 0; i < precision.value(); i++) {
if (!result.append(char('0' + (subSecondNanoseconds / k)))) {
return false;
}
subSecondNanoseconds %= k;
k /= 10;
}
}
return true;
}
/**
* TemporalDurationToString ( duration, precision )
*/
static JSString* TemporalDurationToString(JSContext* cx,
const Duration& duration,
Precision precision) {
MOZ_ASSERT(IsValidDuration(duration));
MOZ_ASSERT(precision != Precision::Minute());
// Fast path for zero durations.
if (duration == Duration{} &&
(precision == Precision::Auto() || precision.value() == 0)) {
return NewStringCopyZ<CanGC>(cx, "PT0S");
}
// 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);
// 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);
// Step 1.
int32_t sign = DurationSign(duration);
// Steps 2 and 7.
JSStringBuilder result(cx);
// Step 14. (Reordered)
if (sign < 0) {
if (!result.append('-')) {
return nullptr;
}
}
// Step 15. (Reordered)
if (!result.append('P')) {
return nullptr;
}
// Step 3.
if (years != 0) {
if (!NumberToStringBuilder(cx, years, result)) {
return nullptr;
}
if (!result.append('Y')) {
return nullptr;
}
}
// Step 4.
if (months != 0) {
if (!NumberToStringBuilder(cx, months, result)) {
return nullptr;
}
if (!result.append('M')) {
return nullptr;
}
}
// Step 5.
if (weeks != 0) {
if (!NumberToStringBuilder(cx, weeks, result)) {
return nullptr;
}
if (!result.append('W')) {
return nullptr;
}
}
// Step 6.
if (days != 0) {
if (!NumberToStringBuilder(cx, days, result)) {
return nullptr;
}
if (!result.append('D')) {
return nullptr;
}
}
// Step 7. (Moved above)
// Steps 10-11. (Reordered)
bool zeroMinutesAndHigher = years == 0 && months == 0 && weeks == 0 &&
days == 0 && hours == 0 && minutes == 0;
// Step 12.
auto secondsDuration = TimeDurationFromComponents(
0.0, 0.0, seconds, milliseconds, microseconds, nanoseconds);
// Steps 8-9, 13, and 16.
bool hasSecondsPart = (secondsDuration != TimeDuration{}) ||
zeroMinutesAndHigher || precision != Precision::Auto();
if (hours != 0 || minutes != 0 || hasSecondsPart) {
// Step 16. (Reordered)
if (!result.append('T')) {
return nullptr;
}
// Step 8.
if (hours != 0) {
if (!NumberToStringBuilder(cx, hours, result)) {
return nullptr;
}
if (!result.append('H')) {
return nullptr;
}
}
// Step 9.
if (minutes != 0) {
if (!NumberToStringBuilder(cx, minutes, result)) {
return nullptr;
}
if (!result.append('M')) {
return nullptr;
}
}
// Step 13.
if (hasSecondsPart) {
// Step 13.a.
if (!NumberToStringBuilder(cx, double(secondsDuration.seconds), result)) {
return nullptr;
}
// Step 13.b.
if (!FormatFractionalSeconds(result, secondsDuration.nanoseconds,
precision)) {
return nullptr;
}
// Step 13.c.
if (!result.append('S')) {
return nullptr;
}
}
}
// Steps 14-16. (Moved above)
// Step 17.
return result.finishString();
}
/**
* GetTemporalRelativeToOption ( options )
*/
static bool GetTemporalRelativeToOption(
JSContext* cx, Handle<JSObject*> options,
MutableHandle<PlainDate> plainRelativeTo,
MutableHandle<ZonedDateTime> zonedRelativeTo) {
// Default initialize both return values.
plainRelativeTo.set(PlainDate{});
zonedRelativeTo.set(ZonedDateTime{});
// Step 1.
Rooted<Value> value(cx);
if (!GetProperty(cx, options, options, cx->names().relativeTo, &value)) {
return false;
}
// Step 2.
if (value.isUndefined()) {
return true;
}
// Step 3.
auto offsetBehaviour = OffsetBehaviour::Option;
// Step 4.
auto matchBehaviour = MatchBehaviour::MatchExactly;
// Steps 5-6.
EpochNanoseconds epochNanoseconds;
Rooted<TimeZoneValue> timeZone(cx);
Rooted<CalendarValue> calendar(cx);
if (value.isObject()) {
Rooted<JSObject*> obj(cx, &value.toObject());
// Step 5.a.
if (auto* zonedDateTime = obj->maybeUnwrapIf<ZonedDateTimeObject>()) {
auto epochNs = zonedDateTime->epochNanoseconds();
Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone());
Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar());
if (!timeZone.wrap(cx)) {
return false;
}
if (!calendar.wrap(cx)) {
return false;
}
// Step 5.a.i.
zonedRelativeTo.set(ZonedDateTime{epochNs, timeZone, calendar});
return true;
}
// Step 5.b.
if (auto* plainDate = obj->maybeUnwrapIf<PlainDateObject>()) {
auto date = plainDate->date();
Rooted<CalendarValue> calendar(cx, plainDate->calendar());
if (!calendar.wrap(cx)) {
return false;
}
// Step 5.b.i.
plainRelativeTo.set(PlainDate{date, calendar});
return true;
}
// Step 5.c.
if (auto* dateTime = obj->maybeUnwrapIf<PlainDateTimeObject>()) {
auto date = dateTime->date();
Rooted<CalendarValue> calendar(cx, dateTime->calendar());
if (!calendar.wrap(cx)) {
return false;
}
// Steps 5.c.i-ii.
plainRelativeTo.set(PlainDate{date, calendar});
return true;
}
// Step 5.d.
if (!GetTemporalCalendarWithISODefault(cx, obj, &calendar)) {
return false;
}
// Step 5.e.
Rooted<CalendarFields> fields(cx);
if (!PrepareCalendarFields(cx, calendar, obj,
{
CalendarField::Year,
CalendarField::Month,
CalendarField::MonthCode,
CalendarField::Day,
CalendarField::Hour,
CalendarField::Minute,
CalendarField::Second,
CalendarField::Millisecond,
CalendarField::Microsecond,
CalendarField::Nanosecond,
CalendarField::Offset,
CalendarField::TimeZone,
},
&fields)) {
return false;
}
// Step 5.f.
ISODateTime dateTime;
if (!InterpretTemporalDateTimeFields(
cx, calendar, fields, TemporalOverflow::Constrain, &dateTime)) {
return false;
}
// Step 5.g.
timeZone = fields.timeZone();
// Step 5.h.
auto offset = fields.offset();
// Step 5.j.
if (!fields.has(CalendarField::Offset)) {
offsetBehaviour = OffsetBehaviour::Wall;
}
// Step 7.
if (!timeZone) {
// Steps 7.a-b.
return CreateTemporalDate(cx, dateTime.date, calendar, plainRelativeTo);
}
// Steps 8-9.
int64_t offsetNs = 0;
if (offsetBehaviour == OffsetBehaviour::Option) {
// Step 8.a.
offsetNs = int64_t(offset);
}
// Step 10.
if (!InterpretISODateTimeOffset(
cx, dateTime, offsetBehaviour, offsetNs, timeZone,
TemporalDisambiguation::Compatible, TemporalOffset::Reject,
matchBehaviour, &epochNanoseconds)) {
return false;
}
} else {
// Step 6.a.
if (!value.isString()) {
ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, value,
nullptr, "not a string");
return false;
}
Rooted<JSString*> string(cx, value.toString());
// Step 6.b.
Rooted<ParsedZonedDateTime> parsed(cx);
if (!ParseTemporalRelativeToString(cx, string, &parsed)) {
return false;
}
// Steps 6.c-e. (Not applicable in our implementation.)
// Step 6.f.
if (parsed.timeZoneAnnotation()) {
// Step 6.f.i.
if (!ToTemporalTimeZone(cx, parsed.timeZoneAnnotation(), &timeZone)) {
return false;
}
// Steps 6.f.ii-iii.
if (parsed.isUTC()) {
offsetBehaviour = OffsetBehaviour::Exact;
} else if (!parsed.hasOffset()) {
offsetBehaviour = OffsetBehaviour::Wall;
}
// Step 6.f.iv.
matchBehaviour = MatchBehaviour::MatchMinutes;
} else {
MOZ_ASSERT(!timeZone);
}
// Steps 6.g-i.
if (parsed.calendar()) {
if (!CanonicalizeCalendar(cx, parsed.calendar(), &calendar)) {
return false;
}
} else {
calendar.set(CalendarValue(CalendarId::ISO8601));
}
// Step 7.
if (!timeZone) {
// Steps 7.a-b.
return CreateTemporalDate(cx, parsed.dateTime().date, calendar,
plainRelativeTo);
}
// Steps 8-9.
int64_t offsetNs;
if (offsetBehaviour == OffsetBehaviour::Option) {
MOZ_ASSERT(parsed.hasOffset());
// Step 8.a.
offsetNs = parsed.timeZoneOffset();
} else {
// Step 9.
offsetNs = 0;
}
// Step 10.
if (parsed.isStartOfDay()) {
if (!InterpretISODateTimeOffset(
cx, parsed.dateTime().date, offsetBehaviour, offsetNs, timeZone,
TemporalDisambiguation::Compatible, TemporalOffset::Reject,
matchBehaviour, &epochNanoseconds)) {
return false;
}
} else {
if (!InterpretISODateTimeOffset(
cx, parsed.dateTime(), offsetBehaviour, offsetNs, timeZone,
TemporalDisambiguation::Compatible, TemporalOffset::Reject,
matchBehaviour, &epochNanoseconds)) {
return false;
}
}
}
MOZ_ASSERT(IsValidEpochNanoseconds(epochNanoseconds));
// Steps 11-12.
zonedRelativeTo.set(ZonedDateTime{epochNanoseconds, timeZone, calendar});
return true;
}
/**
* RoundTimeDurationToIncrement ( d, increment, roundingMode )
*/
static TimeDuration RoundTimeDurationToIncrement(
const TimeDuration& duration, const TemporalUnit unit, Increment increment,
TemporalRoundingMode roundingMode) {
MOZ_ASSERT(IsValidTimeDuration(duration));
MOZ_ASSERT(unit >= TemporalUnit::Day);
MOZ_ASSERT_IF(unit >= TemporalUnit::Hour,
increment <= MaximumTemporalDurationRoundingIncrement(unit));
auto divisor = Int128{ToNanoseconds(unit)} * Int128{increment.value()};
MOZ_ASSERT(divisor > Int128{0});
MOZ_ASSERT_IF(unit >= TemporalUnit::Hour,
divisor <= Int128{ToNanoseconds(TemporalUnit::Day)});
auto totalNanoseconds = duration.toNanoseconds();
auto rounded =
RoundNumberToIncrement(totalNanoseconds, divisor, roundingMode);
return TimeDuration::fromNanoseconds(rounded);
}
/**
* TotalTimeDuration ( timeDuration, unit )
*/
double js::temporal::TotalTimeDuration(const TimeDuration& duration,
TemporalUnit unit) {
MOZ_ASSERT(IsValidTimeDuration(duration));
MOZ_ASSERT(unit >= TemporalUnit::Day);
auto numerator = duration.toNanoseconds();
auto denominator = Int128{ToNanoseconds(unit)};
return FractionToDouble(numerator, denominator);
}
/**
* RoundTimeDuration ( duration, increment, unit, roundingMode )
*/
static bool RoundTimeDuration(JSContext* cx, const TimeDuration& duration,
Increment increment, TemporalUnit unit,
TemporalRoundingMode roundingMode,
TimeDuration* result) {
MOZ_ASSERT(IsValidTimeDuration(duration));
MOZ_ASSERT(increment <= Increment::max());
MOZ_ASSERT(unit > TemporalUnit::Day);
// Step 1-2.
auto rounded =
RoundTimeDurationToIncrement(duration, unit, increment, roundingMode);
if (!IsValidTimeDuration(rounded)) {
JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr,
JSMSG_TEMPORAL_DURATION_INVALID_NORMALIZED_TIME);
return false;
}
*result = rounded;
return true;
}
/**
* RoundTimeDuration ( duration, increment, unit, roundingMode )
*/
TimeDuration js::temporal::RoundTimeDuration(
const TimeDuration& duration, Increment increment, TemporalUnit unit,
TemporalRoundingMode roundingMode) {
MOZ_ASSERT(IsValidTimeDuration(duration));
MOZ_ASSERT(increment <= Increment::max());
MOZ_ASSERT(unit > TemporalUnit::Day);
auto result =
RoundTimeDurationToIncrement(duration, unit, increment, roundingMode);
MOZ_ASSERT(IsValidTimeDuration(result));
return result;
}
#ifdef DEBUG
/**
* Return true if the input is within the valid epoch nanoseconds limits with a
* time zone offset applied, i.e. it's smaller than ±(8.64 × 10^21 + nsPerDay).
*/
static bool IsValidLocalNanoseconds(const EpochNanoseconds& epochNanoseconds) {
MOZ_ASSERT(0 <= epochNanoseconds.nanoseconds &&
epochNanoseconds.nanoseconds <= 999'999'999);
// Time zone offsets can't exceed 24 hours.
constexpr auto oneDay = EpochDuration::fromDays(1);
// Exclusive limits.
constexpr auto min = EpochNanoseconds::min() - oneDay;
constexpr auto max = EpochNanoseconds::max() + oneDay;
return min < epochNanoseconds && epochNanoseconds < max;
}
#endif
enum class UnsignedRoundingMode {
Zero,
Infinity,
HalfZero,
HalfInfinity,
HalfEven
};
/**
* GetUnsignedRoundingMode ( roundingMode, sign )
*/
static UnsignedRoundingMode GetUnsignedRoundingMode(
TemporalRoundingMode roundingMode, bool isNegative) {
switch (roundingMode) {
case TemporalRoundingMode::Ceil:
return isNegative ? UnsignedRoundingMode::Zero
: UnsignedRoundingMode::Infinity;
case TemporalRoundingMode::Floor:
return isNegative ? UnsignedRoundingMode::Infinity
: UnsignedRoundingMode::Zero;
case TemporalRoundingMode::Expand:
return UnsignedRoundingMode::Infinity;
case TemporalRoundingMode::Trunc:
return UnsignedRoundingMode::Zero;
case TemporalRoundingMode::HalfCeil:
return isNegative ? UnsignedRoundingMode::HalfZero
: UnsignedRoundingMode::HalfInfinity;
case TemporalRoundingMode::HalfFloor:
return isNegative ? UnsignedRoundingMode::HalfInfinity
: UnsignedRoundingMode::HalfZero;
case TemporalRoundingMode::HalfExpand:
return UnsignedRoundingMode::HalfInfinity;
case TemporalRoundingMode::HalfTrunc:
return UnsignedRoundingMode::HalfZero;
case TemporalRoundingMode::HalfEven:
return UnsignedRoundingMode::HalfEven;
}
MOZ_CRASH("invalid rounding mode");
}
struct DurationNudge {
InternalDuration duration;
EpochNanoseconds epochNs;
double total = 0;
bool didExpandCalendarUnit = false;
};
/**
* NudgeToCalendarUnit ( sign, duration, destEpochNs, isoDateTime, timeZone,
* calendar, increment, unit, roundingMode )
*/
static bool NudgeToCalendarUnit(JSContext* cx, const InternalDuration& duration,
const EpochNanoseconds& destEpochNs,
const ISODateTime& isoDateTime,
Handle<TimeZoneValue> timeZone,
Handle<CalendarValue> calendar,
Increment increment, TemporalUnit unit,
TemporalRoundingMode roundingMode,
DurationNudge* result) {
MOZ_ASSERT(IsValidDuration(duration));
MOZ_ASSERT_IF(timeZone, IsValidEpochNanoseconds(destEpochNs));
MOZ_ASSERT_IF(!timeZone, IsValidLocalNanoseconds(destEpochNs));
MOZ_ASSERT(ISODateTimeWithinLimits(isoDateTime));
MOZ_ASSERT(unit <= TemporalUnit::Day);
int32_t sign = InternalDurationSign(duration) < 0 ? -1 : 1;
// Steps 1-4.
int64_t r1;
int64_t r2;
DateDuration startDuration;
DateDuration endDuration;
if (unit == TemporalUnit::Year) {
// Step 1.a.
int64_t years = RoundNumberToIncrement(duration.date.years, increment,
TemporalRoundingMode::Trunc);
// Step 1.b.
r1 = years;
// Step 1.c.
r2 = years + int64_t(increment.value()) * sign;
// Step 1.d.
startDuration = {r1};
// Step 1.e.
endDuration = {r2};
} else if (unit == TemporalUnit::Month) {
// Step 2.a.
int64_t months = RoundNumberToIncrement(duration.date.months, increment,
TemporalRoundingMode::Trunc);
// Step 2.b.
r1 = months;
// Step 2.c.
r2 = months + int64_t(increment.value()) * sign;
// Step 2.d.
startDuration = {duration.date.years, r1};
// Step 2.e.
endDuration = {duration.date.years, r2};
} else if (unit == TemporalUnit::Week) {
// Step 3.a.
auto yearsMonths = DateDuration{duration.date.years, duration.date.months};
// Step 3.b.
ISODate weeksStart;
if (!CalendarDateAdd(cx, calendar, isoDateTime.date, yearsMonths,
TemporalOverflow::Constrain, &weeksStart)) {
return false;
}
MOZ_ASSERT(ISODateWithinLimits(weeksStart));
// Step 3.c.
ISODate weeksEnd;
if (!BalanceISODate(cx, weeksStart, duration.date.days, &weeksEnd)) {
return false;
}
MOZ_ASSERT(ISODateWithinLimits(weeksEnd));
// Step 3.d.
DateDuration untilResult;
if (!CalendarDateUntil(cx, calendar, weeksStart, weeksEnd,
TemporalUnit::Week, &untilResult)) {
return false;
}
// Step 3.e.
int64_t weeks =
RoundNumberToIncrement(duration.date.weeks + untilResult.weeks,
increment, TemporalRoundingMode::Trunc);
// Step 3.f.
r1 = weeks;
// Step 3.g.
r2 = weeks + int64_t(increment.value()) * sign;
// Step 3.h.
startDuration = {duration.date.years, duration.date.months, r1};
// Step 3.i.
endDuration = {duration.date.years, duration.date.months, r2};
} else {
// Step 4.a.
MOZ_ASSERT(unit == TemporalUnit::Day);