diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /js/src/builtin/temporal | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/builtin/temporal')
38 files changed, 44369 insertions, 0 deletions
diff --git a/js/src/builtin/temporal/Calendar.cpp b/js/src/builtin/temporal/Calendar.cpp new file mode 100644 index 0000000000..8a06877093 --- /dev/null +++ b/js/src/builtin/temporal/Calendar.cpp @@ -0,0 +1,4758 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/Calendar.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/EnumSet.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" +#include "mozilla/Range.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <array> +#include <cmath> +#include <cstring> +#include <iterator> +#include <stddef.h> +#include <stdint.h> +#include <utility> + +#include "jsfriendapi.h" +#include "jsnum.h" +#include "jspubtd.h" +#include "jstypes.h" +#include "NamespaceImports.h" + +#include "builtin/Array.h" +#include "builtin/String.h" +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/PlainMonthDay.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.h" +#include "builtin/temporal/TemporalParser.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/TemporalUnit.h" +#include "builtin/temporal/Wrapped.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "gc/GCEnum.h" +#include "gc/Tracer.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/Conversions.h" +#include "js/ErrorReport.h" +#include "js/ForOfIterator.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCAPI.h" +#include "js/GCHashTable.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/TracingAPI.h" +#include "js/Value.h" +#include "util/Text.h" +#include "vm/ArrayObject.h" +#include "vm/BytecodeUtil.h" +#include "vm/Compartment.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" +#include "vm/PropertyInfo.h" +#include "vm/PropertyKey.h" +#include "vm/Realm.h" +#include "vm/Shape.h" +#include "vm/Stack.h" +#include "vm/StringType.h" + +#include "vm/Compartment-inl.h" +#include "vm/JSAtomUtils-inl.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 IsCalendar(Handle<Value> v) { + return v.isObject() && v.toObject().is<CalendarObject>(); +} + +void js::temporal::CalendarValue::trace(JSTracer* trc) { + TraceRoot(trc, &value_, "CalendarValue::value"); +} + +void js::temporal::CalendarRecord::trace(JSTracer* trc) { + receiver_.trace(trc); + TraceNullableRoot(trc, &dateAdd_, "CalendarRecord::dateAdd"); + TraceNullableRoot(trc, &dateFromFields_, "CalendarRecord::dateFromFields"); + TraceNullableRoot(trc, &dateUntil_, "CalendarRecord::dateUntil"); + TraceNullableRoot(trc, &day_, "CalendarRecord::day"); + TraceNullableRoot(trc, &fields_, "CalendarRecord::fields"); + TraceNullableRoot(trc, &mergeFields_, "CalendarRecord::mergeFields"); + TraceNullableRoot(trc, &monthDayFromFields_, + "CalendarRecord::monthDayFromFields"); + TraceNullableRoot(trc, &yearMonthFromFields_, + "CalendarRecord::yearMonthFromFields"); +} + +bool js::temporal::WrapCalendarValue(JSContext* cx, + MutableHandle<JS::Value> calendar) { + MOZ_ASSERT(calendar.isString() || calendar.isObject()); + return cx->compartment()->wrap(cx, calendar); +} + +/** + * IteratorToListOfType ( iteratorRecord, elementTypes ) + * + * With `elementTypes = ยซ String ยป`. + * + * This implementation accepts an iterable instead of an iterator record. + */ +static bool IterableToListOfStrings(JSContext* cx, Handle<Value> items, + MutableHandle<CalendarFieldNames> list) { + JS::ForOfIterator iterator(cx); + if (!iterator.init(items)) { + return false; + } + + // Step 1. (Not applicable in our implementation.) + + // Steps 2-3. + Rooted<Value> nextValue(cx); + Rooted<PropertyKey> value(cx); + while (true) { + bool done; + if (!iterator.next(&nextValue, &done)) { + return false; + } + if (done) { + break; + } + + if (nextValue.isString()) { + if (!PrimitiveValueToId<CanGC>(cx, nextValue, &value)) { + return false; + } + if (!list.append(value)) { + return false; + } + continue; + } + + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, nextValue, + nullptr, "not a string"); + + iterator.closeThrow(); + return false; + } + + // Step 4. + return true; +} + +/** + * IsISOLeapYear ( year ) + */ +static constexpr bool IsISOLeapYear(int32_t year) { + // Steps 1-5. + return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); +} + +/** + * IsISOLeapYear ( year ) + */ +static bool IsISOLeapYear(double year) { + // Step 1. + MOZ_ASSERT(IsInteger(year)); + + // Steps 2-5. + return std::fmod(year, 4) == 0 && + (std::fmod(year, 100) != 0 || std::fmod(year, 400) == 0); +} + +/** + * ISODaysInYear ( year ) + */ +int32_t js::temporal::ISODaysInYear(int32_t year) { + // Steps 1-3. + return IsISOLeapYear(year) ? 366 : 365; +} + +/** + * ISODaysInMonth ( year, month ) + */ +static constexpr int32_t ISODaysInMonth(int32_t year, int32_t month) { + MOZ_ASSERT(1 <= month && month <= 12); + + constexpr uint8_t daysInMonth[2][13] = { + {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; + + // Steps 1-4. + return daysInMonth[IsISOLeapYear(year)][month]; +} + +/** + * ISODaysInMonth ( year, month ) + */ +int32_t js::temporal::ISODaysInMonth(int32_t year, int32_t month) { + return ::ISODaysInMonth(year, month); +} + +/** + * ISODaysInMonth ( year, month ) + */ +int32_t js::temporal::ISODaysInMonth(double year, int32_t month) { + MOZ_ASSERT(1 <= month && month <= 12); + + static constexpr uint8_t daysInMonth[2][13] = { + {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, + {0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}}; + + // Steps 1-4. + return daysInMonth[IsISOLeapYear(year)][month]; +} + +/** + * 21.4.1.6 Week Day + * + * Compute the week day from |day| without first expanding |day| into a full + * date through |MakeDate(day, 0)|: + * + * WeekDay(MakeDate(day, 0)) + * = WeekDay(day ร msPerDay + 0) + * = WeekDay(day ร msPerDay) + * = ๐ฝ(โ(Day(day ร msPerDay) + 4๐ฝ) modulo 7) + * = ๐ฝ(โ(๐ฝ(floor(โ((day ร msPerDay) / msPerDay))) + 4๐ฝ) modulo 7) + * = ๐ฝ(โ(๐ฝ(floor(โ(day))) + 4๐ฝ) modulo 7) + * = ๐ฝ(โ(๐ฝ(day) + 4๐ฝ) modulo 7) + */ +static int32_t WeekDay(int32_t day) { + int32_t result = (day + 4) % 7; + if (result < 0) { + result += 7; + } + return result; +} + +/** + * ToISODayOfWeek ( year, month, day ) + */ +static int32_t ToISODayOfWeek(const PlainDate& date) { + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + // Steps 1-3. (Not applicable in our implementation.) + + // TODO: Check if ES MakeDate + WeekDay is efficient enough. + // + // https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Methods_in_computer_code + + // Step 4. + int32_t day = MakeDay(date); + + // Step 5. + int32_t weekday = WeekDay(day); + return weekday != 0 ? weekday : 7; +} + +static constexpr auto FirstDayOfMonth(int32_t year) { + // The following array contains the day of year for the first day of each + // month, where index 0 is January, and day 0 is January 1. + std::array<int32_t, 13> days = {}; + for (int32_t month = 1; month <= 12; ++month) { + days[month] = days[month - 1] + ::ISODaysInMonth(year, month); + } + return days; +} + +/** + * ToISODayOfYear ( year, month, day ) + */ +static int32_t ToISODayOfYear(int32_t year, int32_t month, int32_t day) { + MOZ_ASSERT(1 <= month && month <= 12); + + // First day of month arrays for non-leap and leap years. + constexpr decltype(FirstDayOfMonth(0)) firstDayOfMonth[2] = { + FirstDayOfMonth(1), FirstDayOfMonth(0)}; + + // Steps 1-3. (Not applicable in our implementation.) + + // Steps 4-5. + // + // Instead of first computing the date and then using DayWithinYear to map the + // date to the day within the year, directly lookup the first day of the month + // and then add the additional days. + return firstDayOfMonth[IsISOLeapYear(year)][month - 1] + day; +} + +/** + * ToISODayOfYear ( year, month, day ) + */ +int32_t js::temporal::ToISODayOfYear(const PlainDate& date) { + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + // Steps 1-5. + auto& [year, month, day] = date; + return ::ToISODayOfYear(year, month, day); +} + +static int32_t FloorDiv(int32_t dividend, int32_t divisor) { + MOZ_ASSERT(divisor > 0); + + int32_t quotient = dividend / divisor; + int32_t remainder = dividend % divisor; + if (remainder < 0) { + quotient -= 1; + } + return quotient; +} + +/** + * 21.4.1.3 Year Number, DayFromYear + */ +static int32_t DayFromYear(int32_t year) { + return 365 * (year - 1970) + FloorDiv(year - 1969, 4) - + FloorDiv(year - 1901, 100) + FloorDiv(year - 1601, 400); +} + +/** + * 21.4.1.11 MakeTime ( hour, min, sec, ms ) + */ +static int64_t MakeTime(const PlainTime& time) { + MOZ_ASSERT(IsValidTime(time)); + + // Step 1 (Not applicable). + + // Step 2. + int64_t h = time.hour; + + // Step 3. + int64_t m = time.minute; + + // Step 4. + int64_t s = time.second; + + // Step 5. + int64_t milli = time.millisecond; + + // Steps 6-7. + return h * ToMilliseconds(TemporalUnit::Hour) + + m * ToMilliseconds(TemporalUnit::Minute) + + s * ToMilliseconds(TemporalUnit::Second) + milli; +} + +/** + * 21.4.1.12 MakeDay ( year, month, date ) + */ +int32_t js::temporal::MakeDay(const PlainDate& date) { + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + return DayFromYear(date.year) + ToISODayOfYear(date) - 1; +} + +/** + * 21.4.1.13 MakeDate ( day, time ) + */ +int64_t js::temporal::MakeDate(const PlainDateTime& dateTime) { + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + // Step 1 (Not applicable). + + // Steps 2-3. + int64_t tv = MakeDay(dateTime.date) * ToMilliseconds(TemporalUnit::Day) + + MakeTime(dateTime.time); + + // Step 4. + return tv; +} + +/** + * 21.4.1.12 MakeDay ( year, month, date ) + */ +static int32_t MakeDay(int32_t year, int32_t month, int32_t day) { + MOZ_ASSERT(1 <= month && month <= 12); + + // FIXME: spec issue - what should happen for invalid years/days? + return DayFromYear(year) + ::ToISODayOfYear(year, month, day) - 1; +} + +/** + * 21.4.1.13 MakeDate ( day, time ) + */ +int64_t js::temporal::MakeDate(int32_t year, int32_t month, int32_t day) { + // NOTE: This version accepts values outside the valid date-time limits. + MOZ_ASSERT(1 <= month && month <= 12); + + // Step 1 (Not applicable). + + // Steps 2-3. + int64_t tv = ::MakeDay(year, month, day) * ToMilliseconds(TemporalUnit::Day); + + // Step 4. + return tv; +} + +struct YearWeek final { + int32_t year = 0; + int32_t week = 0; +}; + +/** + * ToISOWeekOfYear ( year, month, day ) + */ +static YearWeek ToISOWeekOfYear(const PlainDate& date) { + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + auto& [year, month, day] = date; + + // TODO: https://en.wikipedia.org/wiki/Week#The_ISO_week_date_system + // TODO: https://en.wikipedia.org/wiki/ISO_week_date#Algorithms + + // Steps 1-3. (Not applicable in our implementation.) + + // Steps 4-5. + int32_t doy = ToISODayOfYear(date); + int32_t dow = ToISODayOfWeek(date); + + int32_t woy = (10 + doy - dow) / 7; + MOZ_ASSERT(0 <= woy && woy <= 53); + + // An ISO year has 53 weeks if the year starts on a Thursday or if it's a + // leap year which starts on a Wednesday. + auto isLongYear = [](int32_t year) { + int32_t startOfYear = ToISODayOfWeek({year, 1, 1}); + return startOfYear == 4 || (startOfYear == 3 && IsISOLeapYear(year)); + }; + + // Part of last year's last week, which is either week 52 or week 53. + if (woy == 0) { + return {year - 1, 52 + int32_t(isLongYear(year - 1))}; + } + + // Part of next year's first week if the current year isn't a long year. + if (woy == 53 && !isLongYear(year)) { + return {year + 1, 1}; + } + + return {year, woy}; +} + +/** + * ISOMonthCode ( month ) + */ +static JSString* ISOMonthCode(JSContext* cx, int32_t month) { + MOZ_ASSERT(1 <= month && month <= 12); + + // Steps 1-2. + char monthCode[3] = {'M', char('0' + (month / 10)), char('0' + (month % 10))}; + return NewStringCopyN<CanGC>(cx, monthCode, std::size(monthCode)); +} + +template <typename T, typename... Ts> +static bool ToPlainDate(JSObject* temporalDateLike, PlainDate* result) { + if (auto* obj = temporalDateLike->maybeUnwrapIf<T>()) { + *result = ToPlainDate(obj); + return true; + } + if constexpr (sizeof...(Ts) > 0) { + return ToPlainDate<Ts...>(temporalDateLike, result); + } + return false; +} + +template <typename... Ts> +static bool ToPlainDate(JSContext* cx, Handle<Value> temporalDateLike, + PlainDate* result) { + if (temporalDateLike.isObject()) { + if (ToPlainDate<Ts...>(&temporalDateLike.toObject(), result)) { + return true; + } + } + + return ToTemporalDate(cx, temporalDateLike, result); +} + +#ifdef DEBUG +template <typename CharT> +static bool StringIsAsciiLowerCase(mozilla::Range<CharT> str) { + return std::all_of(str.begin().get(), str.end().get(), [](CharT ch) { + return mozilla::IsAscii(ch) && !mozilla::IsAsciiUppercaseAlpha(ch); + }); +} + +static bool StringIsAsciiLowerCase(JSLinearString* str) { + JS::AutoCheckCannotGC nogc; + return str->hasLatin1Chars() + ? StringIsAsciiLowerCase(str->latin1Range(nogc)) + : StringIsAsciiLowerCase(str->twoByteRange(nogc)); +} +#endif + +static bool IsISO8601Calendar(JSLinearString* id) { + return StringEqualsLiteral(id, "iso8601"); +} + +#ifdef DEBUG +static bool IsISO8601Calendar(CalendarObject* calendar) { + return IsISO8601Calendar(calendar->identifier()); +} +#endif + +static bool IsISO8601Calendar(JSContext* cx, JSString* id, bool* result) { + JSLinearString* linear = id->ensureLinear(cx); + if (!linear) { + return false; + } + *result = IsISO8601Calendar(linear); + return true; +} + +/** + * IsBuiltinCalendar ( id ) + */ +static bool IsBuiltinCalendar(JSLinearString* id) { + // Callers must convert to lower case. + MOZ_ASSERT(StringIsAsciiLowerCase(id)); + + // Steps 1-3. + return StringEqualsLiteral(id, "iso8601"); +} + +static JSLinearString* ThrowIfNotBuiltinCalendar(JSContext* cx, + Handle<JSLinearString*> id) { + if (!StringIsAscii(id)) { + if (auto chars = QuoteString(cx, id)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INVALID_ID, chars.get()); + } + return nullptr; + } + + JSString* lower = StringToLowerCase(cx, id); + if (!lower) { + return nullptr; + } + + JSLinearString* linear = lower->ensureLinear(cx); + if (!linear) { + return nullptr; + } + + if (!IsBuiltinCalendar(linear)) { + if (auto chars = QuoteString(cx, id)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INVALID_ID, chars.get()); + } + return nullptr; + } + + return linear; +} + +bool js::temporal::ToBuiltinCalendar(JSContext* cx, Handle<JSString*> id, + MutableHandle<CalendarValue> result) { + Rooted<JSLinearString*> linear(cx, id->ensureLinear(cx)); + if (!linear) { + return false; + } + + auto* identifier = ThrowIfNotBuiltinCalendar(cx, linear); + if (!identifier) { + return false; + } + + result.set(CalendarValue(identifier)); + return true; +} + +/** + * CreateTemporalCalendar ( identifier [ , newTarget ] ) + */ +static CalendarObject* CreateTemporalCalendar( + JSContext* cx, const CallArgs& args, Handle<JSLinearString*> identifier) { + // Step 1. + MOZ_ASSERT(IsBuiltinCalendar(identifier)); + + // Steps 2-3. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Calendar, &proto)) { + return nullptr; + } + + auto* obj = NewObjectWithClassProto<CalendarObject>(cx, proto); + if (!obj) { + return nullptr; + } + + // Step 4. + obj->setFixedSlot(CalendarObject::IDENTIFIER_SLOT, StringValue(identifier)); + + // Step 5. + return obj; +} + +/** + * CreateTemporalCalendar ( identifier [ , newTarget ] ) + */ +static CalendarObject* CreateTemporalCalendar( + JSContext* cx, Handle<JSLinearString*> identifier) { + // Step 1. + MOZ_ASSERT(IsBuiltinCalendar(identifier)); + + // Steps 2-3. + auto* obj = NewBuiltinClassInstance<CalendarObject>(cx); + if (!obj) { + return nullptr; + } + + // Step 4. + obj->setFixedSlot(CalendarObject::IDENTIFIER_SLOT, StringValue(identifier)); + + // Step 5. + return obj; +} + +/** + * ObjectImplementsTemporalCalendarProtocol ( object ) + */ +static bool ObjectImplementsTemporalCalendarProtocol(JSContext* cx, + Handle<JSObject*> object, + bool* result) { + // Step 1. (Not applicable in our implementation.) + MOZ_ASSERT(!object->canUnwrapAs<CalendarObject>(), + "Calendar objects handled in the caller"); + + // Step 2. + for (auto key : { + &JSAtomState::dateAdd, &JSAtomState::dateFromFields, + &JSAtomState::dateUntil, &JSAtomState::day, + &JSAtomState::dayOfWeek, &JSAtomState::dayOfYear, + &JSAtomState::daysInMonth, &JSAtomState::daysInWeek, + &JSAtomState::daysInYear, &JSAtomState::fields, + &JSAtomState::id, &JSAtomState::inLeapYear, + &JSAtomState::mergeFields, &JSAtomState::month, + &JSAtomState::monthCode, &JSAtomState::monthDayFromFields, + &JSAtomState::monthsInYear, &JSAtomState::weekOfYear, + &JSAtomState::year, &JSAtomState::yearMonthFromFields, + &JSAtomState::yearOfWeek, + }) { + // Step 2.a. + bool has; + if (!HasProperty(cx, object, cx->names().*key, &has)) { + return false; + } + if (!has) { + *result = false; + return true; + } + } + + // Step 3. + *result = true; + return true; +} + +template <typename T, typename... Ts> +static bool ToTemporalCalendar(JSContext* cx, Handle<JSObject*> object, + MutableHandle<CalendarValue> result) { + if (auto* unwrapped = object->maybeUnwrapIf<T>()) { + result.set(unwrapped->calendar()); + return result.wrap(cx); + } + + if constexpr (sizeof...(Ts) > 0) { + return ToTemporalCalendar<Ts...>(cx, object, result); + } + + result.set(CalendarValue()); + return true; +} + +/** + * ToTemporalCalendarSlotValue ( temporalCalendarLike [ , default ] ) + */ +bool js::temporal::ToTemporalCalendar(JSContext* cx, + Handle<Value> temporalCalendarLike, + MutableHandle<CalendarValue> result) { + // Step 1. (Not applicable) + + // Step 2. + Rooted<Value> calendarLike(cx, temporalCalendarLike); + if (calendarLike.isObject()) { + Rooted<JSObject*> obj(cx, &calendarLike.toObject()); + + // Step 2.b. (Partial) + if (obj->canUnwrapAs<CalendarObject>()) { + result.set(CalendarValue(obj)); + return true; + } + + // Step 2.a. + Rooted<CalendarValue> calendar(cx); + if (!::ToTemporalCalendar<PlainDateObject, PlainDateTimeObject, + PlainMonthDayObject, PlainYearMonthObject, + ZonedDateTimeObject>(cx, obj, &calendar)) { + return false; + } + if (calendar) { + result.set(calendar); + return true; + } + + // Step 2.b. + bool implementsCalendarProtocol; + if (!ObjectImplementsTemporalCalendarProtocol( + cx, obj, &implementsCalendarProtocol)) { + return false; + } + if (!implementsCalendarProtocol) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_OBJECT, + "Temporal.Calendar", obj->getClass()->name); + return false; + } + + // Step 2.c. + result.set(CalendarValue(obj)); + return true; + } + + // Step 3. + if (!calendarLike.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, + calendarLike, nullptr, "not a string"); + return false; + } + Rooted<JSString*> str(cx, calendarLike.toString()); + + // Step 4. + Rooted<JSLinearString*> identifier(cx, ParseTemporalCalendarString(cx, str)); + if (!identifier) { + return false; + } + + // Step 5. + identifier = ThrowIfNotBuiltinCalendar(cx, identifier); + if (!identifier) { + return false; + } + + // Step 6. + result.set(CalendarValue(identifier)); + return true; +} + +/** + * ToTemporalCalendarSlotValue ( temporalCalendarLike [ , default ] ) + * + * When called with `default = "iso8601"`. + */ +bool js::temporal::ToTemporalCalendarWithISODefault( + JSContext* cx, Handle<Value> temporalCalendarLike, + MutableHandle<CalendarValue> result) { + // Step 1. + if (temporalCalendarLike.isUndefined()) { + result.set(CalendarValue(cx->names().iso8601)); + return true; + } + + // Steps 2-6. + return ToTemporalCalendar(cx, temporalCalendarLike, result); +} + +/** + * GetTemporalCalendarSlotValueWithISODefault ( item ) + */ +bool js::temporal::GetTemporalCalendarWithISODefault( + JSContext* cx, Handle<JSObject*> item, + MutableHandle<CalendarValue> result) { + // Step 1. + Rooted<CalendarValue> calendar(cx); + if (!::ToTemporalCalendar<PlainDateObject, PlainDateTimeObject, + PlainMonthDayObject, PlainYearMonthObject, + ZonedDateTimeObject>(cx, item, &calendar)) { + return false; + } + if (calendar) { + result.set(calendar); + return true; + } + + // Step 2. + Rooted<Value> calendarValue(cx); + if (!GetProperty(cx, item, item, cx->names().calendar, &calendarValue)) { + return false; + } + + // Step 3. + return ToTemporalCalendarWithISODefault(cx, calendarValue, result); +} + +/** + * ToTemporalCalendarIdentifier ( calendarSlotValue ) + */ +JSString* js::temporal::ToTemporalCalendarIdentifier( + JSContext* cx, Handle<CalendarValue> calendar) { + // Step 1. + if (calendar.isString()) { + // Step 1.a. + MOZ_ASSERT(IsBuiltinCalendar(calendar.toString())); + + // Step 1.b. + return calendar.toString(); + } + + // Step 2. + Rooted<JSObject*> calendarObj(cx, calendar.toObject()); + Rooted<Value> identifier(cx); + if (!GetProperty(cx, calendarObj, calendarObj, cx->names().id, &identifier)) { + return nullptr; + } + + // Step 3. + if (!identifier.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, identifier, + nullptr, "not a string"); + return nullptr; + } + + // Step 4. + return identifier.toString(); +} + +/** + * ToTemporalCalendarObject ( calendarSlotValue ) + */ +JSObject* js::temporal::ToTemporalCalendarObject( + JSContext* cx, Handle<CalendarValue> calendar) { + // Step 1. + if (calendar.isObject()) { + return calendar.toObject(); + } + + // Step 2. + Rooted<JSLinearString*> calendarId(cx, calendar.toString()); + return CreateTemporalCalendar(cx, calendarId); +} + +static bool Calendar_dateAdd(JSContext* cx, unsigned argc, Value* vp); +static bool Calendar_dateFromFields(JSContext* cx, unsigned argc, Value* vp); +static bool Calendar_dateUntil(JSContext* cx, unsigned argc, Value* vp); +static bool Calendar_day(JSContext* cx, unsigned argc, Value* vp); +static bool Calendar_fields(JSContext* cx, unsigned argc, Value* vp); +static bool Calendar_mergeFields(JSContext* cx, unsigned argc, Value* vp); +static bool Calendar_monthDayFromFields(JSContext* cx, unsigned argc, + Value* vp); +static bool Calendar_yearMonthFromFields(JSContext* cx, unsigned argc, + Value* vp); + +/** + * CalendarMethodsRecordLookup ( calendarRec, methodName ) + */ +static bool CalendarMethodsRecordLookup(JSContext* cx, + MutableHandle<CalendarRecord> calendar, + CalendarMethod methodName) { + // Step 1. (Not applicable in our implementation.) + + // Steps 2-10. + Rooted<JSObject*> object(cx, calendar.receiver().toObject()); + + auto lookup = [&](Handle<PropertyName*> name, JSNative native, + MutableHandle<JSObject*> result) { + auto* method = GetMethod(cx, object, name); + if (!method) { + return false; + } + + // As an optimization we only store the method if the receiver is either + // a custom calendar object or if the method isn't the default, built-in + // calender method. + if (!object->is<CalendarObject>() || !IsNativeFunction(method, native)) { + result.set(method); + } + return true; + }; + + switch (methodName) { + // Steps 2 and 10. + case CalendarMethod::DateAdd: + return lookup(cx->names().dateAdd, Calendar_dateAdd, calendar.dateAdd()); + + // Steps 3 and 10. + case CalendarMethod::DateFromFields: + return lookup(cx->names().dateFromFields, Calendar_dateFromFields, + calendar.dateFromFields()); + + // Steps 4 and 10. + case CalendarMethod::DateUntil: + return lookup(cx->names().dateUntil, Calendar_dateUntil, + calendar.dateUntil()); + + // Steps 5 and 10. + case CalendarMethod::Day: + return lookup(cx->names().day, Calendar_day, calendar.day()); + + // Steps 6 and 10. + case CalendarMethod::Fields: + return lookup(cx->names().fields, Calendar_fields, calendar.fields()); + + // Steps 7 and 10. + case CalendarMethod::MergeFields: + return lookup(cx->names().mergeFields, Calendar_mergeFields, + calendar.mergeFields()); + + // Steps 8 and 10. + case CalendarMethod::MonthDayFromFields: + return lookup(cx->names().monthDayFromFields, Calendar_monthDayFromFields, + calendar.monthDayFromFields()); + + // Steps 9 and 10. + case CalendarMethod::YearMonthFromFields: + return lookup(cx->names().yearMonthFromFields, + Calendar_yearMonthFromFields, + calendar.yearMonthFromFields()); + } + + MOZ_CRASH("invalid calendar method"); +} + +/** + * CreateCalendarMethodsRecord ( calendar, methods ) + */ +bool js::temporal::CreateCalendarMethodsRecord( + JSContext* cx, Handle<CalendarValue> calendar, + mozilla::EnumSet<CalendarMethod> methods, + MutableHandle<CalendarRecord> result) { + MOZ_ASSERT(!methods.isEmpty()); + + // Step 1. + result.set(CalendarRecord{calendar}); + +#ifdef DEBUG + // Remember the set of looked-up methods for assertions. + result.get().lookedUp() += methods; +#endif + + // Built-in calendars don't perform observable lookups. + if (calendar.isString()) { + return true; + } + + // Step 2. + for (auto method : methods) { + if (!CalendarMethodsRecordLookup(cx, result, method)) { + return false; + } + } + + // Step 3. + return true; +} + +static bool ToCalendarField(JSContext* cx, JSLinearString* linear, + CalendarField* result) { + if (StringEqualsLiteral(linear, "year")) { + *result = CalendarField::Year; + return true; + } + if (StringEqualsLiteral(linear, "month")) { + *result = CalendarField::Month; + return true; + } + if (StringEqualsLiteral(linear, "monthCode")) { + *result = CalendarField::MonthCode; + return true; + } + if (StringEqualsLiteral(linear, "day")) { + *result = CalendarField::Day; + return true; + } + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INVALID_FIELD, + chars.get()); + } + return false; +} + +static PropertyName* ToPropertyName(JSContext* cx, CalendarField field) { + switch (field) { + case CalendarField::Year: + return cx->names().year; + case CalendarField::Month: + return cx->names().month; + case CalendarField::MonthCode: + return cx->names().monthCode; + case CalendarField::Day: + return cx->names().day; + } + MOZ_CRASH("invalid calendar field name"); +} + +#ifdef DEBUG +static const char* ToCString(CalendarField field) { + switch (field) { + case CalendarField::Year: + return "year"; + case CalendarField::Month: + return "month"; + case CalendarField::MonthCode: + return "monthCode"; + case CalendarField::Day: + return "day"; + } + MOZ_CRASH("invalid calendar field name"); +} +#endif + +/** + * Temporal.Calendar.prototype.fields ( fields ) + */ +static bool BuiltinCalendarFields( + JSContext* cx, std::initializer_list<CalendarField> fieldNames, + CalendarFieldNames& result) { + MOZ_ASSERT(result.empty()); + + // Steps 1-5. (Not applicable.) + + // Reserve space for the append operation. + if (!result.reserve(fieldNames.size())) { + return false; + } + + // Steps 6-7. + for (auto fieldName : fieldNames) { + auto* name = ToPropertyName(cx, fieldName); + + // Steps 7.a and 7.b.i-iv. (Not applicable) + + // Step 7.b.v. + result.infallibleAppend(NameToId(name)); + } + + // Step 8. (Not applicable) + return true; +} + +#ifdef DEBUG +static bool IsSorted(std::initializer_list<CalendarField> fieldNames) { + return std::is_sorted(fieldNames.begin(), fieldNames.end(), + [](auto x, auto y) { + auto* a = ToCString(x); + auto* b = ToCString(y); + return std::strcmp(a, b) < 0; + }); +} +#endif + +/** + * CalendarFields ( calendarRec, fieldNames ) + */ +bool js::temporal::CalendarFields( + JSContext* cx, Handle<CalendarRecord> calendar, + std::initializer_list<CalendarField> fieldNames, + MutableHandle<CalendarFieldNames> result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::Fields)); + + // FIXME: spec issue - the input is already sorted, let's assert this, too. + MOZ_ASSERT(IsSorted(fieldNames)); + + // FIXME: spec issue - the input shouldn't have duplicate elements. Let's + // assert this, too. + MOZ_ASSERT(std::adjacent_find(fieldNames.begin(), fieldNames.end()) == + fieldNames.end()); + + // Step 1. + auto fields = calendar.fields(); + if (!fields) { + bool arrayIterationSane; + if (calendar.receiver().isString()) { + // "String" calendars don't perform observable array iteration. + arrayIterationSane = true; + } else { + // "Object" calendars need to ensure array iteration is still sane. + if (!IsArrayIterationSane(cx, &arrayIterationSane)) { + return false; + } + } + + if (arrayIterationSane) { + // Steps 1.a-b. (Not applicable in our implementation.) + + // Step 1.c. + return BuiltinCalendarFields(cx, fieldNames, result.get()); + + // Steps 1.d-f. (Not applicable in our implementation.) + } + } + + // Step 2. (Inlined call to CalendarMethodsRecordCall.) + + auto* array = NewDenseFullyAllocatedArray(cx, fieldNames.size()); + if (!array) { + return false; + } + array->setDenseInitializedLength(fieldNames.size()); + + for (size_t i = 0; i < fieldNames.size(); i++) { + auto* name = ToPropertyName(cx, fieldNames.begin()[i]); + array->initDenseElement(i, StringValue(name)); + } + + Rooted<Value> fieldsFn(cx, ObjectValue(*fields)); + auto thisv = calendar.receiver().toValue(); + Rooted<Value> fieldsArray(cx, ObjectValue(*array)); + if (!Call(cx, fieldsFn, thisv, fieldsArray, &fieldsArray)) { + return false; + } + + // Steps 3-4. + if (!IterableToListOfStrings(cx, fieldsArray, result)) { + return false; + } + + // The spec sorts the field names in PrepareTemporalFields. Sorting is only + // needed for user-defined calendars, so our implementation performs this step + // here instead of in PrepareTemporalFields. + return SortTemporalFieldNames(cx, result.get()); +} + +static bool RequireIntegralNumber(JSContext* cx, Handle<Value> value, + Handle<PropertyName*> name, + MutableHandle<Value> result) { + if (MOZ_LIKELY(value.isInt32())) { + result.set(value); + return true; + } + + if (value.isDouble()) { + double d = value.toDouble(); + if (js::IsInteger(d)) { + result.setNumber(d); + return true; + } + + if (auto str = QuoteString(cx, name)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_INTEGER, str.get()); + } + return false; + } + + if (auto str = QuoteString(cx, name)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, str.get(), "not a number"); + } + return false; +} + +static bool RequireIntegralPositiveNumber(JSContext* cx, Handle<Value> value, + Handle<PropertyName*> name, + MutableHandle<Value> result) { + if (!RequireIntegralNumber(cx, value, name, result)) { + return false; + } + + if (result.toNumber() <= 0) { + if (auto str = QuoteString(cx, name)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_NUMBER, str.get()); + } + return false; + } + return true; +} + +static bool RequireString(JSContext* cx, Handle<Value> value, + Handle<PropertyName*> name, + MutableHandle<Value> result) { + if (MOZ_LIKELY(value.isString())) { + result.set(value); + return true; + } + + if (auto str = QuoteString(cx, name)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, str.get(), "not a string"); + } + return false; +} + +static bool RequireBoolean(JSContext* cx, Handle<Value> value, + Handle<PropertyName*> name, + MutableHandle<Value> result) { + if (MOZ_LIKELY(value.isBoolean())) { + result.set(value); + return true; + } + + if (auto str = QuoteString(cx, name)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_UNEXPECTED_TYPE, str.get(), + "not a boolean"); + } + return false; +} + +using BuiltinCalendarMethod = bool (*)(JSContext* cx, const PlainDate&, + MutableHandle<Value>); + +using CalendarConversion = bool (*)(JSContext*, Handle<Value>, + Handle<PropertyName*>, + MutableHandle<Value>); + +template <BuiltinCalendarMethod builtin, CalendarConversion conversion> +static bool CallCalendarMethod(JSContext* cx, Handle<PropertyName*> name, + JSNative native, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Step 1. + if (calendar.isString()) { + return builtin(cx, date, result); + } + + // Step 2. + Rooted<JSObject*> calendarObj(cx, calendar.toObject()); + JSObject* fn = GetMethod(cx, calendarObj, name); + if (!fn) { + return false; + } + + // Fast-path for the default implementation. + if (calendarObj->is<CalendarObject>() && IsNativeFunction(fn, native)) { + return builtin(cx, date, result); + } + + Rooted<JS::Value> fnVal(cx, ObjectValue(*fn)); + Rooted<JS::Value> dateLikeValue(cx, ObjectValue(*dateLike)); + if (!Call(cx, fnVal, calendarObj, dateLikeValue, result)) { + return false; + } + + // Steps 3-5. + return conversion(cx, result, name, result); +} + +/** + * Temporal.Calendar.prototype.year ( temporalDateLike ) + */ +static bool BuiltinCalendarYear(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-6. + result.setInt32(date.year); + return true; +} + +static bool Calendar_year(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarYear ( calendar, dateLike ) + */ +static bool CalendarYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-5. + return CallCalendarMethod<BuiltinCalendarYear, RequireIntegralNumber>( + cx, cx->names().year, Calendar_year, calendar, dateLike, date, result); +} + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarYear(cx, calendar, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarYear(cx, calendar, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainYearMonthObject*> dateLike, + MutableHandle<Value> result) { + return CalendarYear(cx, calendar, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarYear(JSContext* cx, Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarYear(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.month ( temporalDateLike ) + */ +static bool BuiltinCalendarMonth(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-5. (Not applicable.) + + // Steps 6-7. + result.setInt32(date.month); + return true; +} + +static bool Calendar_month(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarMonth ( calendar, dateLike ) + */ +static bool CalendarMonth(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-6. + return CallCalendarMethod<BuiltinCalendarMonth, + RequireIntegralPositiveNumber>( + cx, cx->names().month, Calendar_month, calendar, dateLike, date, result); +} + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonth(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarMonth(cx, calendar, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonth(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarMonth(cx, calendar, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonth(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainYearMonthObject*> dateLike, + MutableHandle<Value> result) { + return CalendarMonth(cx, calendar, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonth(JSContext* cx, Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarMonth(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.monthCode ( temporalDateLike ) + */ +static bool BuiltinCalendarMonthCode(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-6. + JSString* str = ISOMonthCode(cx, date.month); + if (!str) { + return false; + } + + result.setString(str); + return true; +} + +static bool Calendar_monthCode(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +static bool CalendarMonthCode(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. + return CallCalendarMethod<BuiltinCalendarMonthCode, RequireString>( + cx, cx->names().monthCode, Calendar_monthCode, calendar, dateLike, date, + result); +} + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthCode(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarMonthCode(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthCode(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarMonthCode(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthCode(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainMonthDayObject*> dateLike, + MutableHandle<Value> result) { + return CalendarMonthCode(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthCode(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainYearMonthObject*> dateLike, + MutableHandle<Value> result) { + return CalendarMonthCode(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthCode(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarMonthCode(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.day ( temporalDateLike ) + */ +static bool BuiltinCalendarDay(const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-6. + result.setInt32(date.day); + return true; +} + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +static bool CalendarDay(JSContext* cx, Handle<CalendarRecord> calendar, + Handle<JSObject*> dateLike, const PlainDate& date, + MutableHandle<Value> result) { + MOZ_ASSERT(CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::Day)); + + // Step 2. (Reordered) + auto day = calendar.day(); + if (!day) { + return BuiltinCalendarDay(date, result); + } + + // Step 1. (Inlined call to CalendarMethodsRecordCall.) + Rooted<Value> fn(cx, ObjectValue(*day)); + auto thisv = calendar.receiver().toValue(); + Rooted<JS::Value> dateLikeValue(cx, ObjectValue(*dateLike)); + if (!Call(cx, fn, thisv, dateLikeValue, result)) { + return false; + } + + // Steps 3-6. + return RequireIntegralPositiveNumber(cx, result, cx->names().day, result); +} + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool js::temporal::CalendarDay(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + Rooted<CalendarRecord> calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::Day, + }, + &calendarRec)) { + return false; + } + + return CalendarDay(cx, calendarRec, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool js::temporal::CalendarDay(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + Rooted<CalendarRecord> calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::Day, + }, + &calendarRec)) { + return false; + } + + return CalendarDay(cx, calendarRec, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool js::temporal::CalendarDay(JSContext* cx, Handle<CalendarValue> calendar, + Handle<PlainMonthDayObject*> dateLike, + MutableHandle<Value> result) { + Rooted<CalendarRecord> calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::Day, + }, + &calendarRec)) { + return false; + } + + return CalendarDay(cx, calendarRec, dateLike, ToPlainDate(dateLike), result); +} + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool js::temporal::CalendarDay(JSContext* cx, Handle<CalendarRecord> calendar, + const PlainDate& date, + MutableHandle<Value> result) { + Rooted<PlainDateObject*> dateLike( + cx, CreateTemporalDate(cx, date, calendar.receiver())); + if (!dateLike) { + return false; + } + + return ::CalendarDay(cx, calendar, dateLike, date, result); +} + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool js::temporal::CalendarDay(JSContext* cx, Handle<CalendarRecord> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar.receiver())); + if (!dateLike) { + return false; + } + + return ::CalendarDay(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.dayOfWeek ( temporalDateLike ) + */ +static bool BuiltinCalendarDayOfWeek(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-9. + result.setInt32(ToISODayOfWeek(date)); + return true; +} + +static bool Calendar_dayOfWeek(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarDayOfWeek ( calendar, dateLike ) + */ +static bool CalendarDayOfWeek(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-6. + return CallCalendarMethod<BuiltinCalendarDayOfWeek, + RequireIntegralPositiveNumber>( + cx, cx->names().dayOfWeek, Calendar_dayOfWeek, calendar, dateLike, date, + result); +} + +/** + * CalendarDayOfWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarDayOfWeek(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDayOfWeek(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDayOfWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarDayOfWeek(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDayOfWeek(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDayOfWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarDayOfWeek(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarDayOfWeek(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.dayOfYear ( temporalDateLike ) + */ +static bool BuiltinCalendarDayOfYear(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-7. + result.setInt32(ToISODayOfYear(date)); + return true; +} + +static bool Calendar_dayOfYear(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarDayOfYear ( calendar, dateLike ) + */ +static bool CalendarDayOfYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-6. + return CallCalendarMethod<BuiltinCalendarDayOfYear, + RequireIntegralPositiveNumber>( + cx, cx->names().dayOfYear, Calendar_dayOfYear, calendar, dateLike, date, + result); +} + +/** + * CalendarDayOfYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarDayOfYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDayOfYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDayOfYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarDayOfYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDayOfYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDayOfYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarDayOfYear(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarDayOfYear(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.weekOfYear ( temporalDateLike ) + */ +static bool BuiltinCalendarWeekOfYear(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-6. + result.setInt32(ToISOWeekOfYear(date).week); + return true; +} + +static bool Calendar_weekOfYear(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarWeekOfYear ( calendar, dateLike ) + */ +static bool CalendarWeekOfYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-6. + return CallCalendarMethod<BuiltinCalendarWeekOfYear, + RequireIntegralPositiveNumber>( + cx, cx->names().weekOfYear, Calendar_weekOfYear, calendar, dateLike, date, + result); +} + +/** + * CalendarWeekOfYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarWeekOfYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarWeekOfYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarWeekOfYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarWeekOfYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarWeekOfYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarWeekOfYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarWeekOfYear(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarWeekOfYear(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.yearOfWeek ( temporalDateLike ) + */ +static bool BuiltinCalendarYearOfWeek(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-6. + result.setInt32(ToISOWeekOfYear(date).year); + return true; +} + +static bool Calendar_yearOfWeek(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarYearOfWeek ( calendar, dateLike ) + */ +static bool CalendarYearOfWeek(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-5. + return CallCalendarMethod<BuiltinCalendarYearOfWeek, RequireIntegralNumber>( + cx, cx->names().yearOfWeek, Calendar_yearOfWeek, calendar, dateLike, date, + result); +} + +/** + * CalendarYearOfWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarYearOfWeek(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarYearOfWeek(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarYearOfWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarYearOfWeek(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarYearOfWeek(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarYearOfWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarYearOfWeek(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarYearOfWeek(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.daysInWeek ( temporalDateLike ) + */ +static bool BuiltinCalendarDaysInWeek(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Step 5. + result.setInt32(7); + return true; +} + +static bool Calendar_daysInWeek(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarDaysInWeek ( calendar, dateLike ) + */ +static bool CalendarDaysInWeek(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-6. + return CallCalendarMethod<BuiltinCalendarDaysInWeek, + RequireIntegralPositiveNumber>( + cx, cx->names().daysInWeek, Calendar_daysInWeek, calendar, dateLike, date, + result); +} + +/** + * CalendarDaysInWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInWeek(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInWeek(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInWeek(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInWeek(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInWeek ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInWeek(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarDaysInWeek(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.daysInMonth ( temporalDateLike ) + */ +static bool BuiltinCalendarDaysInMonth(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Step 5. + result.setInt32(::ISODaysInMonth(date.year, date.month)); + return true; +} + +static bool Calendar_daysInMonth(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +static bool CalendarDaysInMonth(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Step 1-6. + return CallCalendarMethod<BuiltinCalendarDaysInMonth, + RequireIntegralPositiveNumber>( + cx, cx->names().daysInMonth, Calendar_daysInMonth, calendar, dateLike, + date, result); +} + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInMonth(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInMonth(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInMonth(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInMonth(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInMonth(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainYearMonthObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInMonth(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInMonth(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarDaysInMonth(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.daysInYear ( temporalDateLike ) + */ +static bool BuiltinCalendarDaysInYear(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Step 5. + result.setInt32(ISODaysInYear(date.year)); + return true; +} + +static bool Calendar_daysInYear(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +static bool CalendarDaysInYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Step 1-6. + return CallCalendarMethod<BuiltinCalendarDaysInYear, + RequireIntegralPositiveNumber>( + cx, cx->names().daysInYear, Calendar_daysInYear, calendar, dateLike, date, + result); +} + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainYearMonthObject*> dateLike, + MutableHandle<Value> result) { + return CalendarDaysInYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarDaysInYear(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarDaysInYear(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.monthsInYear ( temporalDateLike ) + */ +static bool BuiltinCalendarMonthsInYear(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Step 5. + result.setInt32(12); + return true; +} + +static bool Calendar_monthsInYear(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +static bool CalendarMonthsInYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Step 1-6. + return CallCalendarMethod<BuiltinCalendarMonthsInYear, + RequireIntegralPositiveNumber>( + cx, cx->names().monthsInYear, Calendar_monthsInYear, calendar, dateLike, + date, result); +} + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthsInYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return ::CalendarMonthsInYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthsInYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return ::CalendarMonthsInYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthsInYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainYearMonthObject*> dateLike, + MutableHandle<Value> result) { + return ::CalendarMonthsInYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarMonthsInYear(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarMonthsInYear(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * Temporal.Calendar.prototype.inLeapYear ( temporalDateLike ) + */ +static bool BuiltinCalendarInLeapYear(JSContext* cx, const PlainDate& date, + MutableHandle<Value> result) { + // Steps 1-4. (Not applicable.) + + // Steps 5-6. + result.setBoolean(IsISOLeapYear(date.year)); + return true; +} + +static bool Calendar_inLeapYear(JSContext* cx, unsigned argc, Value* vp); + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +static bool CalendarInLeapYear(JSContext* cx, Handle<CalendarValue> calendar, + Handle<JSObject*> dateLike, + const PlainDate& date, + MutableHandle<Value> result) { + // Step 1-4. + return CallCalendarMethod<BuiltinCalendarInLeapYear, RequireBoolean>( + cx, cx->names().inLeapYear, Calendar_inLeapYear, calendar, dateLike, date, + result); +} + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarInLeapYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateObject*> dateLike, + MutableHandle<Value> result) { + return ::CalendarInLeapYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarInLeapYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainDateTimeObject*> dateLike, + MutableHandle<Value> result) { + return ::CalendarInLeapYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarInLeapYear(JSContext* cx, + Handle<CalendarValue> calendar, + Handle<PlainYearMonthObject*> dateLike, + MutableHandle<Value> result) { + return ::CalendarInLeapYear(cx, calendar, dateLike, ToPlainDate(dateLike), + result); +} + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool js::temporal::CalendarInLeapYear(JSContext* cx, + Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + MutableHandle<Value> result) { + Rooted<PlainDateTimeObject*> dateLike( + cx, CreateTemporalDateTime(cx, dateTime, calendar)); + if (!dateLike) { + return false; + } + + return ::CalendarInLeapYear(cx, calendar, dateLike, dateTime.date, result); +} + +/** + * ISOResolveMonth ( fields ) + */ +static bool ISOResolveMonth(JSContext* cx, + MutableHandle<TemporalFields> fields) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + double month = fields.month(); + + // Step 3. + MOZ_ASSERT((IsInteger(month) && month > 0) || std::isnan(month)); + + // Step 4. + Handle<JSString*> monthCode = fields.monthCode(); + + // Step 5. + if (!monthCode) { + // Step 5.a. + if (std::isnan(month)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_MISSING_FIELD, + "monthCode"); + return false; + } + + // Step 5.b. + return true; + } + + // Steps 6-7. (Not applicable in our implementation.) + + // Step 8. + if (monthCode->length() != 3) { + if (auto code = QuoteString(cx, monthCode)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, + code.get()); + } + return false; + } + + JSLinearString* linear = monthCode->ensureLinear(cx); + if (!linear) { + return false; + } + + char16_t chars[3] = { + linear->latin1OrTwoByteChar(0), + linear->latin1OrTwoByteChar(1), + linear->latin1OrTwoByteChar(2), + }; + + // Steps 9-11. (Partial) + if (chars[0] != 'M' || !mozilla::IsAsciiDigit(chars[1]) || + !mozilla::IsAsciiDigit(chars[2])) { + if (auto code = QuoteString(cx, linear)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, + code.get()); + } + return false; + } + + // Step 12. + int32_t monthCodeInteger = + AsciiDigitToNumber(chars[1]) * 10 + AsciiDigitToNumber(chars[2]); + + // Step 11. (Partial) + if (monthCodeInteger < 1 || monthCodeInteger > 12) { + if (auto code = QuoteString(cx, linear)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, + code.get()); + } + return false; + } + + // Step 13. (Not applicable in our implementation.) + + // Step 14. + if (!std::isnan(month) && month != monthCodeInteger) { + if (auto code = QuoteString(cx, linear)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INVALID_MONTHCODE, + code.get()); + } + return false; + } + + // Step 15. + fields.month() = monthCodeInteger; + + // Step 16. + return true; +} + +/** + * ISODateFromFields ( fields, overflow ) + */ +static bool ISODateFromFields(JSContext* cx, Handle<TemporalFields> fields, + TemporalOverflow overflow, PlainDate* result) { + // Steps 1-2. (Not applicable in our implementation.) + + // Step 3. + double year = fields.year(); + + // Step 4. + double month = fields.month(); + + // Step 5. + double day = fields.day(); + + // Step 6. + MOZ_ASSERT(!std::isnan(year) && !std::isnan(month) && !std::isnan(day)); + + // Step 7. + RegulatedISODate regulated; + if (!RegulateISODate(cx, year, month, day, overflow, ®ulated)) { + return false; + } + + // The result is used to create a new PlainDateObject, so it's okay to + // directly throw an error for invalid years. That way we don't have to worry + // about representing doubles in PlainDate structs. + int32_t intYear; + if (!mozilla::NumberEqualsInt32(regulated.year, &intYear)) { + // CreateTemporalDate, steps 1-2. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + *result = {intYear, regulated.month, regulated.day}; + return true; +} + +/** + * Temporal.Calendar.prototype.dateFromFields ( fields [ , options ] ) + */ +static PlainDateObject* BuiltinCalendarDateFromFields( + JSContext* cx, Handle<JSObject*> fields, Handle<JSObject*> maybeOptions) { + // Steps 1-5. (Not applicable) + + // Step 6. + Rooted<TemporalFields> dateFields(cx); + if (!PrepareTemporalFields(cx, fields, + {TemporalField::Day, TemporalField::Month, + TemporalField::MonthCode, TemporalField::Year}, + {TemporalField::Day, TemporalField::Year}, + &dateFields)) { + return nullptr; + } + + // Step 7. + auto overflow = TemporalOverflow::Constrain; + if (maybeOptions) { + if (!ToTemporalOverflow(cx, maybeOptions, &overflow)) { + return nullptr; + } + } + + // Step 8. + if (!ISOResolveMonth(cx, &dateFields)) { + return nullptr; + } + + // Step 9. + PlainDate result; + if (!ISODateFromFields(cx, dateFields, overflow, &result)) { + return nullptr; + } + + // Step 10. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + return CreateTemporalDate(cx, result, calendar); +} + +/** + * CalendarDateFromFields ( calendarRec, fields [ , options ] ) + */ +static Wrapped<PlainDateObject*> CalendarDateFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<JSObject*> fields, + Handle<PlainObject*> maybeOptions) { + MOZ_ASSERT(CalendarMethodsRecordHasLookedUp(calendar, + CalendarMethod::DateFromFields)); + + // Step 1. (Not applicable in our implemetation.) + + // Step 3. (Reordered) + auto dateFromFields = calendar.dateFromFields(); + if (!dateFromFields) { + return BuiltinCalendarDateFromFields(cx, fields, maybeOptions); + } + + // Step 2. (Inlined call to CalendarMethodsRecordCall.) + + Rooted<Value> dateFromFieldsFn(cx, ObjectValue(*dateFromFields)); + auto thisv = calendar.receiver().toValue(); + Rooted<Value> rval(cx); + + FixedInvokeArgs<2> args(cx); + args[0].setObject(*fields); + if (maybeOptions) { + args[1].setObject(*maybeOptions); + } else { + args[1].setUndefined(); + } + + if (!Call(cx, dateFromFieldsFn, thisv, args, &rval)) { + return nullptr; + } + + // Step 4. + if (!rval.isObject() || !rval.toObject().canUnwrapAs<PlainDateObject>()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, rval, + nullptr, "not a PlainDate object"); + return nullptr; + } + + // Step 5. + return &rval.toObject(); +} + +/** + * CalendarDateFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::CalendarDateFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<PlainObject*> fields) { + // Steps 1-6. + return ::CalendarDateFromFields(cx, calendar, fields, nullptr); +} + +/** + * CalendarDateFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::CalendarDateFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<PlainObject*> fields, + Handle<PlainObject*> options) { + // Steps 1-6. + return ::CalendarDateFromFields(cx, calendar, fields, options); +} + +struct RegulatedISOYearMonth final { + double year; + int32_t month; +}; + +/** + * RegulateISOYearMonth ( year, month, overflow ) + */ +static bool RegulateISOYearMonth(JSContext* cx, double year, double month, + TemporalOverflow overflow, + RegulatedISOYearMonth* result) { + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (overflow == TemporalOverflow::Constrain) { + // Step 3.a. + month = std::clamp(month, 1.0, 12.0); + + // Step 3.b. + *result = {year, int32_t(month)}; + return true; + } + + // Step 4.a. + MOZ_ASSERT(overflow == TemporalOverflow::Reject); + + // Step 4.b. + if (month < 1 || month > 12) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID); + return false; + } + + // Step 4.c. + *result = {year, int32_t(month)}; + return true; +} + +/** + * ISOYearMonthFromFields ( fields, overflow ) + */ +static bool ISOYearMonthFromFields(JSContext* cx, Handle<TemporalFields> fields, + TemporalOverflow overflow, + PlainDate* result) { + // Steps 1-2. (Not applicable in our implementation.) + + // Step 3. + double year = fields.year(); + + // Step 4. + double month = fields.month(); + + // Step 5. + MOZ_ASSERT(!std::isnan(year) && !std::isnan(month)); + + // Step 6. + RegulatedISOYearMonth regulated; + if (!RegulateISOYearMonth(cx, year, month, overflow, ®ulated)) { + return false; + } + + // Step 7. + + // The result is used to create a new PlainYearMonthObject, so it's okay to + // directly throw an error for invalid years. That way we don't have to worry + // about representing doubles in PlainDate structs. + int32_t intYear; + if (!mozilla::NumberEqualsInt32(regulated.year, &intYear)) { + // CreateTemporalYearMonth, steps 1-2. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID); + return false; + } + + *result = {intYear, regulated.month, 1}; + return true; +} + +/** + * Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] ) + */ +static PlainYearMonthObject* BuiltinCalendarYearMonthFromFields( + JSContext* cx, Handle<JSObject*> fields, Handle<JSObject*> maybeOptions) { + // Steps 1-5. (Not applicable) + + // Step 6. + Rooted<TemporalFields> dateFields(cx); + if (!PrepareTemporalFields( + cx, fields, + {TemporalField::Month, TemporalField::MonthCode, TemporalField::Year}, + {TemporalField::Year}, &dateFields)) { + return nullptr; + } + + // Step 7. + auto overflow = TemporalOverflow::Constrain; + if (maybeOptions) { + if (!ToTemporalOverflow(cx, maybeOptions, &overflow)) { + return nullptr; + } + } + + // Step 8. + if (!ISOResolveMonth(cx, &dateFields)) { + return nullptr; + } + + // Step 9. + PlainDate result; + if (!ISOYearMonthFromFields(cx, dateFields, overflow, &result)) { + return nullptr; + } + + // Step 10. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + return CreateTemporalYearMonth(cx, result, calendar); +} + +/** + * CalendarYearMonthFromFields ( calendarRec, fields [ , options ] ) + */ +static Wrapped<PlainYearMonthObject*> CalendarYearMonthFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<JSObject*> fields, + Handle<PlainObject*> maybeOptions) { + MOZ_ASSERT(CalendarMethodsRecordHasLookedUp( + calendar, CalendarMethod::YearMonthFromFields)); + + // Step 1. (Not applicable in our implementation.) + + // Step 3. (Reordered) + auto yearMonthFromFields = calendar.yearMonthFromFields(); + if (!yearMonthFromFields) { + return BuiltinCalendarYearMonthFromFields(cx, fields, maybeOptions); + } + + // Step 2. (Inlined call to CalendarMethodsRecordCall.) + + Rooted<Value> yearMonthFromFieldsFn(cx, ObjectValue(*yearMonthFromFields)); + auto thisv = calendar.receiver().toValue(); + Rooted<Value> rval(cx); + + FixedInvokeArgs<2> args(cx); + args[0].setObject(*fields); + if (maybeOptions) { + args[1].setObject(*maybeOptions); + } else { + args[1].setUndefined(); + } + + if (!Call(cx, yearMonthFromFieldsFn, thisv, args, &rval)) { + return nullptr; + } + + // Step 4. + if (!rval.isObject() || + !rval.toObject().canUnwrapAs<PlainYearMonthObject>()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, rval, + nullptr, "not a PlainYearMonth object"); + return nullptr; + } + + // Step 5. + return &rval.toObject(); +} + +/** + * CalendarYearMonthFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainYearMonthObject*> js::temporal::CalendarYearMonthFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<PlainObject*> fields) { + // Steps 1-4. + return ::CalendarYearMonthFromFields(cx, calendar, fields, nullptr); +} + +/** + * CalendarYearMonthFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainYearMonthObject*> js::temporal::CalendarYearMonthFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<PlainYearMonthObject*> fields) { + // Steps 1-4. + return ::CalendarYearMonthFromFields(cx, calendar, fields, nullptr); +} + +/** + * CalendarYearMonthFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainYearMonthObject*> js::temporal::CalendarYearMonthFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<PlainObject*> fields, + Handle<PlainObject*> options) { + // Steps 1-4. + return ::CalendarYearMonthFromFields(cx, calendar, fields, options); +} + +/** + * ISOMonthDayFromFields ( fields, overflow ) + */ +static bool ISOMonthDayFromFields(JSContext* cx, Handle<TemporalFields> fields, + TemporalOverflow overflow, + PlainDate* result) { + // Steps 1-2. (Not applicable in our implementation.) + + // Step 3. + double month = fields.month(); + + // Step 4. + double day = fields.day(); + + // Step 5. + MOZ_ASSERT(!std::isnan(month)); + MOZ_ASSERT(!std::isnan(day)); + + // Step 6. + double year = fields.year(); + + // Step 7. + int32_t referenceISOYear = 1972; + + // Steps 8-9. + double y = std::isnan(year) ? referenceISOYear : year; + RegulatedISODate regulated; + if (!RegulateISODate(cx, y, month, day, overflow, ®ulated)) { + return false; + } + + // Step 10. + *result = {referenceISOYear, regulated.month, regulated.day}; + return true; +} + +/** + * Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] ) + */ +static PlainMonthDayObject* BuiltinCalendarMonthDayFromFields( + JSContext* cx, Handle<JSObject*> fields, Handle<JSObject*> maybeOptions) { + // Steps 1-5. (Not applicable) + + // Step 6. + Rooted<TemporalFields> dateFields(cx); + if (!PrepareTemporalFields(cx, fields, + {TemporalField::Day, TemporalField::Month, + TemporalField::MonthCode, TemporalField::Year}, + {TemporalField::Day}, &dateFields)) { + return nullptr; + } + + // Step 7. + auto overflow = TemporalOverflow::Constrain; + if (maybeOptions) { + if (!ToTemporalOverflow(cx, maybeOptions, &overflow)) { + return nullptr; + } + } + + // Step 8. + if (!ISOResolveMonth(cx, &dateFields)) { + return nullptr; + } + + // Step 9. + PlainDate result; + if (!ISOMonthDayFromFields(cx, dateFields, overflow, &result)) { + return nullptr; + } + + // Step 10. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + return CreateTemporalMonthDay(cx, result, calendar); +} + +/** + * CalendarMonthDayFromFields ( calendarRec, fields [ , options ] ) + */ +static Wrapped<PlainMonthDayObject*> CalendarMonthDayFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<JSObject*> fields, + Handle<PlainObject*> maybeOptions) { + MOZ_ASSERT(CalendarMethodsRecordHasLookedUp( + calendar, CalendarMethod::MonthDayFromFields)); + + // Step 1. (Not applicable in our implementation.) + + // Step 3. (Reordered) + auto monthDayFromFields = calendar.monthDayFromFields(); + if (!monthDayFromFields) { + return BuiltinCalendarMonthDayFromFields(cx, fields, maybeOptions); + } + + // Step 2. (Inlined call to CalendarMethodsRecordCall.) + + Rooted<Value> monthDayFromFieldsFn(cx, ObjectValue(*monthDayFromFields)); + auto thisv = calendar.receiver().toValue(); + Rooted<Value> rval(cx); + + FixedInvokeArgs<2> args(cx); + args[0].setObject(*fields); + if (maybeOptions) { + args[1].setObject(*maybeOptions); + } else { + args[1].setUndefined(); + } + + if (!Call(cx, monthDayFromFieldsFn, thisv, args, &rval)) { + return nullptr; + } + + // Step 4. + if (!rval.isObject() || !rval.toObject().canUnwrapAs<PlainMonthDayObject>()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, rval, + nullptr, "not a PlainMonthDay object"); + return nullptr; + } + + // Step 5. + return &rval.toObject(); +} + +/** + * CalendarMonthDayFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainMonthDayObject*> js::temporal::CalendarMonthDayFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<PlainObject*> fields) { + // Steps 1-4. + return ::CalendarMonthDayFromFields(cx, calendar, fields, nullptr); +} + +/** + * CalendarMonthDayFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainMonthDayObject*> js::temporal::CalendarMonthDayFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<PlainMonthDayObject*> fields) { + // Steps 1-4. + return ::CalendarMonthDayFromFields(cx, calendar, fields, nullptr); +} + +/** + * CalendarMonthDayFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainMonthDayObject*> js::temporal::CalendarMonthDayFromFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<PlainObject*> fields, + Handle<PlainObject*> options) { + // Steps 1-4. + return ::CalendarMonthDayFromFields(cx, calendar, fields, options); +} + +using PropertyHashSet = JS::GCHashSet<JS::PropertyKey>; +using PropertyVector = JS::StackGCVector<JS::PropertyKey>; + +/** + * ISOFieldKeysToIgnore ( keys ) + */ +static bool ISOFieldKeysToIgnore(JSContext* cx, const PropertyVector& keys, + PropertyHashSet& ignoredKeys) { + MOZ_ASSERT(ignoredKeys.empty(), "expected an empty output hashset"); + + // Step 1. (Not applicable in our implementation.) + + if (!ignoredKeys.reserve(keys.length())) { + return false; + } + + // Step 2. + bool seenMonthOrMonthCode = false; + for (auto& key : keys) { + // Reorder the substeps in order to use |putNew| instead of |put|, because + // the former is slightly faster. + + // Steps 2.a-c. + if (key.isAtom(cx->names().month) || key.isAtom(cx->names().monthCode)) { + // Steps 2.b-c. + if (!seenMonthOrMonthCode) { + seenMonthOrMonthCode = true; + + // Add both keys at once. + if (!ignoredKeys.putNew(NameToId(cx->names().month)) || + !ignoredKeys.putNew(NameToId(cx->names().monthCode))) { + return false; + } + } + } else { + // Step 2.a. + if (!ignoredKeys.putNew(key)) { + return false; + } + } + } + + // Step 3. + return true; +} + +/** + * Temporal.Calendar.prototype.mergeFields ( fields, additionalFields ) + */ +static PlainObject* BuiltinCalendarMergeFields( + JSContext* cx, Handle<JSObject*> fields, + Handle<JSObject*> additionalFields) { + // NOTE: This function is only called from CalendarMergeFields and its + // result is always passed to PrepareTemporalFields. PrepareTemporalFields + // sorts the incoming property keys, so it doesn't matter in which order the + // properties are created in this function. This allows to reorder the steps + // to process |additionalFields| first. + + // TODO: Consider additionally passing the fieldNames from the succeeding + // PrepareTemporalFields call, so we don't have to process unnecessary keys. + + // Steps 1-2. (Not applicable in our implementation.) + + // Step 13. + Rooted<PlainObject*> merged(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!merged) { + return nullptr; + } + + // Contrary to the spec, add the properties from |additionalFields| first. + + // Step 6. (Not applicable in our implementation.) + + // Steps 7-8. + // + // PrepareTemporalFields ignores symbol property keys, so we don't need to + // pass JSITER_SYMBOLS. |additionalFields| contains no non-enumerable + // properties, therefore JSITER_HIDDEN isn't needed, too. + JS::RootedVector<PropertyKey> keys(cx); + if (!GetPropertyKeys(cx, additionalFields, JSITER_OWNONLY, &keys)) { + return nullptr; + } + + // Steps 9-11. (Not applicable in our implementation.) + + // Step 12. + Rooted<PropertyHashSet> ignoredKeys(cx, PropertyHashSet(cx)); + if (!ignoredKeys.reserve(keys.length())) { + return nullptr; + } + + // Steps 7-8, 12, and 15. + Rooted<mozilla::Maybe<PropertyDescriptor>> desc(cx); + Rooted<Value> propValue(cx); + bool seenMonthOrMonthCode = false; + for (size_t i = 0; i < keys.length(); i++) { + Handle<PropertyKey> key = keys[i]; + + if (!GetOwnPropertyDescriptor(cx, additionalFields, key, &desc)) { + return nullptr; + } + + propValue.set(desc->value()); + + // Skip |undefined| properties per step 8. + if (propValue.isUndefined()) { + continue; + } + + // Step 15. + if (!DefineDataProperty(cx, merged, key, propValue)) { + return nullptr; + } + + // Step 12. (Inlined ISOFieldKeysToIgnore) + if (key.isAtom(cx->names().month) || key.isAtom(cx->names().monthCode)) { + // ISOFieldKeysToIgnore, steps 2.b-c. + if (!seenMonthOrMonthCode) { + seenMonthOrMonthCode = true; + + if (!ignoredKeys.putNew(NameToId(cx->names().month)) || + !ignoredKeys.putNew(NameToId(cx->names().monthCode))) { + return nullptr; + } + } + } else { + // ISOFieldKeysToIgnore, step 2.a. + if (!ignoredKeys.putNew(key)) { + return nullptr; + } + } + } + + // Now add the properties from |field|. + + // Step 3. (Not applicable in our implementation.) + + // Reuse |keys| to avoid extra allocations. + keys.clear(); + + // Steps 4-5. + // + // See above why neither JSITER_SYMBOLS nor JSITER_HIDDEN is needed. + if (!GetPropertyKeys(cx, fields, JSITER_OWNONLY, &keys)) { + return nullptr; + } + + // Steps 4-5 and 14. + for (size_t i = 0; i < keys.length(); i++) { + Handle<PropertyKey> key = keys[i]; + + // Skip ignored keys per step 14. + if (ignoredKeys.has(key)) { + continue; + } + + if (!GetOwnPropertyDescriptor(cx, fields, key, &desc)) { + return nullptr; + } + + propValue.set(desc->value()); + + // Skip |undefined| properties per step 5. + if (propValue.isUndefined()) { + continue; + } + + // Step 14. + if (!DefineDataProperty(cx, merged, key, propValue)) { + return nullptr; + } + } + + // Step 16. + return merged; +} + +#ifdef DEBUG +static bool IsPlainDataObject(PlainObject* obj) { + for (ShapePropertyIter<NoGC> iter(obj->shape()); !iter.done(); iter++) { + if (iter->flags() != PropertyFlags::defaultDataPropFlags) { + return false; + } + } + return true; +} +#endif + +/** + * CalendarMergeFields ( calendarRec, fields, additionalFields ) + */ +JSObject* js::temporal::CalendarMergeFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<PlainObject*> fields, + Handle<PlainObject*> additionalFields) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::MergeFields)); + + MOZ_ASSERT(IsPlainDataObject(fields)); + MOZ_ASSERT(IsPlainDataObject(additionalFields)); + + // Step 2. (Reordered) + auto mergeFields = calendar.mergeFields(); + if (!mergeFields) { + return BuiltinCalendarMergeFields(cx, fields, additionalFields); + } + + // Step 1. (Inlined call to CalendarMethodsRecordCall.) + + Rooted<Value> mergeFieldsFn(cx, ObjectValue(*mergeFields)); + auto thisv = calendar.receiver().toValue(); + Rooted<Value> result(cx); + + FixedInvokeArgs<2> args(cx); + args[0].setObject(*fields); + args[1].setObject(*additionalFields); + + if (!Call(cx, mergeFieldsFn, thisv, args, &result)) { + return nullptr; + } + + // Steps 3-4. + return RequireObject(cx, result); +} + +/** + * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) + */ +static bool BuiltinCalendarAdd(JSContext* cx, const PlainDate& date, + const Duration& duration, + Handle<JSObject*> options, PlainDate* result) { + MOZ_ASSERT(IsValidISODate(date)); + MOZ_ASSERT(IsValidDuration(duration)); + + // Steps 1-6. (Not applicable) + + // Step 7. + auto overflow = TemporalOverflow::Constrain; + if (options) { + if (!ToTemporalOverflow(cx, options, &overflow)) { + return false; + } + } + + // Step 8. + TimeDuration balanceResult; + if (!BalanceTimeDuration(cx, duration, TemporalUnit::Day, &balanceResult)) { + return false; + } + + // Step 9. + Duration addDuration = {duration.years, duration.months, duration.weeks, + balanceResult.days}; + return AddISODate(cx, date, addDuration, overflow, result); +} + +/** + * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) + */ +static PlainDateObject* BuiltinCalendarAdd(JSContext* cx, const PlainDate& date, + const Duration& duration, + Handle<JSObject*> options) { + // Steps 1-6. (Not applicable) + + // Steps 7-9. + PlainDate result; + if (!BuiltinCalendarAdd(cx, date, duration, options, &result)) { + return nullptr; + } + + // Step 10. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + return CreateTemporalDate(cx, result, calendar); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +static Wrapped<PlainDateObject*> CalendarDateAddSlow( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> duration, Handle<JSObject*> options) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + MOZ_ASSERT(calendar.receiver().isObject()); + MOZ_ASSERT(calendar.dateAdd()); + + // Step 1. (Not applicable). + + // Step 2. (Inlined call to CalendarMethodsRecordCall.) + Rooted<JS::Value> dateAdd(cx, ObjectValue(*calendar.dateAdd())); + auto thisv = calendar.receiver().toValue(); + Rooted<Value> rval(cx); + + FixedInvokeArgs<3> args(cx); + args[0].setObject(*date); + args[1].setObject(*duration); + if (options) { + args[2].setObject(*options); + } else { + args[2].setUndefined(); + } + + if (!Call(cx, dateAdd, thisv, args, &rval)) { + return nullptr; + } + + // Step 3. (Not applicable) + MOZ_ASSERT(!CalendarMethodsRecordIsBuiltin(calendar)); + + // Step 4. + if (!rval.isObject() || !rval.toObject().canUnwrapAs<PlainDateObject>()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, rval, + nullptr, "not a PlainDate object"); + return nullptr; + } + + // Step 5. + return &rval.toObject(); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +static Wrapped<PlainDateObject*> CalendarDateAdd( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, const Duration& duration, + Handle<JSObject*> options) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 1. (Not applicable). + + // Step 3. (Reordered) + if (!calendar.dateAdd()) { + auto* unwrappedDate = date.unwrap(cx); + if (!unwrappedDate) { + return nullptr; + } + auto date = ToPlainDate(unwrappedDate); + + return BuiltinCalendarAdd(cx, date, duration, options); + } + + // Steps 2 and 4-5. + Rooted<DurationObject*> durationObj(cx, CreateTemporalDuration(cx, duration)); + if (!durationObj) { + return nullptr; + } + return CalendarDateAddSlow(cx, calendar, date, durationObj, options); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +static Wrapped<PlainDateObject*> CalendarDateAdd( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> duration, Handle<JSObject*> options) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 1. (Not applicable). + + // Step 3. (Reordered) + if (!calendar.dateAdd()) { + auto* unwrappedDate = date.unwrap(cx); + if (!unwrappedDate) { + return nullptr; + } + auto date = ToPlainDate(unwrappedDate); + + auto* unwrappedDuration = duration.unwrap(cx); + if (!unwrappedDuration) { + return nullptr; + } + auto duration = ToDuration(unwrappedDuration); + + return BuiltinCalendarAdd(cx, date, duration, options); + } + + // Steps 2 and 4-5. + return CalendarDateAddSlow(cx, calendar, date, duration, options); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +static bool CalendarDateAdd(JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, Handle<JSObject*> options, + PlainDate* result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 1. (Not applicable). + + // Step 3. (Reordered) + if (!calendar.dateAdd()) { + auto* unwrappedDate = date.unwrap(cx); + if (!unwrappedDate) { + return false; + } + auto date = ToPlainDate(unwrappedDate); + + return BuiltinCalendarAdd(cx, date, duration, options, result); + } + + // Steps 2 and 4-5. + + Rooted<DurationObject*> durationObj(cx, CreateTemporalDuration(cx, duration)); + if (!durationObj) { + return false; + } + + auto obj = CalendarDateAddSlow(cx, calendar, date, durationObj, options); + if (!obj) { + return false; + } + + *result = ToPlainDate(&obj.unwrap()); + return true; +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +static bool CalendarDateAdd(JSContext* cx, Handle<CalendarRecord> calendar, + const PlainDate& date, const Duration& duration, + Handle<JSObject*> options, PlainDate* result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 1. (Not applicable). + + // Step 3. (Reordered) + if (!calendar.dateAdd()) { + return BuiltinCalendarAdd(cx, date, duration, options, result); + } + + // Steps 2 and 4-5. + + Rooted<PlainDateObject*> dateObj( + cx, CreateTemporalDate(cx, date, calendar.receiver())); + if (!dateObj) { + return false; + } + + Rooted<DurationObject*> durationObj(cx, CreateTemporalDuration(cx, duration)); + if (!durationObj) { + return false; + } + + auto obj = CalendarDateAddSlow(cx, calendar, dateObj, durationObj, options); + if (!obj) { + return false; + } + + *result = ToPlainDate(&obj.unwrap()); + return true; +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::CalendarDateAdd( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, const Duration& duration, + Handle<JSObject*> options) { + // Step 1. (Not applicable). + + // Steps 2-5. + return ::CalendarDateAdd(cx, calendar, date, duration, options); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::CalendarDateAdd( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, const Duration& duration) { + // Step 1. + Handle<JSObject*> options = nullptr; + + // Steps 2-5. + return ::CalendarDateAdd(cx, calendar, date, duration, options); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::CalendarDateAdd( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> duration) { + // Step 1. + Handle<JSObject*> options = nullptr; + + // Steps 2-5. + return ::CalendarDateAdd(cx, calendar, date, duration, options); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::CalendarDateAdd( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> duration, Handle<JSObject*> options) { + // Step 1. (Not applicable). + + // Steps 2-5. + return ::CalendarDateAdd(cx, calendar, date, duration, options); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +bool js::temporal::CalendarDateAdd(JSContext* cx, + Handle<CalendarRecord> calendar, + const PlainDate& date, + const Duration& duration, + PlainDate* result) { + // Step 1. + Handle<JSObject*> options = nullptr; + + // Steps 2-5. + return ::CalendarDateAdd(cx, calendar, date, duration, options, result); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +bool js::temporal::CalendarDateAdd( + JSContext* cx, Handle<CalendarRecord> calendar, const PlainDate& date, + const Duration& duration, Handle<JSObject*> options, PlainDate* result) { + // Step 1. (Not applicable) + + // Steps 2-5. + return ::CalendarDateAdd(cx, calendar, date, duration, options, result); +} + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +bool js::temporal::CalendarDateAdd(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, + PlainDate* result) { + // Step 1. + Handle<JSObject*> options = nullptr; + + // Steps 2-5. + return ::CalendarDateAdd(cx, calendar, date, duration, options, result); +} + +/** + * Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ) + */ +static Duration BuiltinCalendarDateUntil(const PlainDate& one, + const PlainDate& two, + TemporalUnit largestUnit) { + // Steps 1-3. (Not applicable) + + // Steps 4-8. (Not applicable) + + // Step 9. + auto difference = DifferenceISODate(one, two, largestUnit); + + // Step 10. + return difference.toDuration(); +} + +/** + * Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ) + */ +static bool BuiltinCalendarDateUntil(JSContext* cx, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + TemporalUnit largestUnit, + Duration* result) { + MOZ_ASSERT(largestUnit <= TemporalUnit::Day); + + auto* unwrappedOne = one.unwrap(cx); + if (!unwrappedOne) { + return false; + } + auto dateOne = ToPlainDate(unwrappedOne); + + auto* unwrappedTwo = two.unwrap(cx); + if (!unwrappedTwo) { + return false; + } + auto dateTwo = ToPlainDate(unwrappedTwo); + + // Steps 1-10. + *result = BuiltinCalendarDateUntil(dateOne, dateTwo, largestUnit); + return true; +} + +/** + * Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ) + */ +static bool BuiltinCalendarDateUntil(JSContext* cx, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + Handle<JSObject*> options, + Duration* result) { + // Steps 1-6. (Not applicable) + + // Steps 7-8. + auto largestUnit = TemporalUnit::Day; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::LargestUnit, + TemporalUnitGroup::Date, &largestUnit)) { + return false; + } + + // Steps 9-10. + return BuiltinCalendarDateUntil(cx, one, two, largestUnit, result); +} + +static bool CalendarDateUntilSlow(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + Handle<JSObject*> options, Duration* result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + MOZ_ASSERT(calendar.receiver().isObject()); + MOZ_ASSERT(calendar.dateUntil()); + + // Step 1. (Inlined call to CalendarMethodsRecordCall.) + Rooted<JS::Value> dateUntil(cx, ObjectValue(*calendar.dateUntil())); + auto thisv = calendar.receiver().toValue(); + Rooted<Value> rval(cx); + + FixedInvokeArgs<3> args(cx); + args[0].setObject(*one); + args[1].setObject(*two); + args[2].setObject(*options); + + if (!Call(cx, dateUntil, thisv, args, &rval)) { + return false; + } + + // Step 2. (Not applicable) + MOZ_ASSERT(!CalendarMethodsRecordIsBuiltin(calendar)); + + // Step 3. + if (!rval.isObject() || !rval.toObject().canUnwrapAs<DurationObject>()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, rval, + nullptr, "not a Duration object"); + return false; + } + + // Step 4. + *result = ToDuration(&rval.toObject().unwrapAs<DurationObject>()); + return true; +} + +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool js::temporal::CalendarDateUntil(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + Handle<PlainObject*> options, + Duration* result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + + // Step 2. (Reordered) + if (!calendar.dateUntil()) { + return BuiltinCalendarDateUntil(cx, one, two, options, result); + } + + // Steps 1 and 3-4. + return CalendarDateUntilSlow(cx, calendar, one, two, options, result); +} + +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool js::temporal::CalendarDateUntil(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + TemporalUnit largestUnit, + Duration* result) { + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + MOZ_ASSERT(largestUnit <= TemporalUnit::Day); + + // Step 2. (Reordered) + if (!calendar.dateUntil()) { + return BuiltinCalendarDateUntil(cx, one, two, largestUnit, result); + } + + Rooted<PlainObject*> untilOptions(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!untilOptions) { + return false; + } + + Rooted<Value> value(cx, StringValue(TemporalUnitToString(cx, largestUnit))); + if (!DefineDataProperty(cx, untilOptions, cx->names().largestUnit, value)) { + return false; + } + + // Steps 1 and 3-4. + return CalendarDateUntilSlow(cx, calendar, one, two, untilOptions, result); +} + +/** + * CalendarEquals ( one, two ) + */ +bool js::temporal::CalendarEquals(JSContext* cx, Handle<CalendarValue> one, + Handle<CalendarValue> two, bool* equals) { + // Step 1. + if (one.isObject() && two.isObject() && one.toObject() == two.toObject()) { + *equals = true; + return true; + } + + // Step 2. + Rooted<JSString*> calendarOne(cx, ToTemporalCalendarIdentifier(cx, one)); + if (!calendarOne) { + return false; + } + + // Step 3. + JSString* calendarTwo = ToTemporalCalendarIdentifier(cx, two); + if (!calendarTwo) { + return false; + } + + // Steps 4-5. + return EqualStrings(cx, calendarOne, calendarTwo, equals); +} + +/** + * CalendarEquals ( one, two ) + */ +bool js::temporal::CalendarEqualsOrThrow(JSContext* cx, + Handle<CalendarValue> one, + Handle<CalendarValue> two) { + // Step 1. + if (one.isObject() && two.isObject() && one.toObject() == two.toObject()) { + return true; + } + + // Step 2. + Rooted<JSString*> calendarOne(cx, ToTemporalCalendarIdentifier(cx, one)); + if (!calendarOne) { + return false; + } + + // Step 3. + JSString* calendarTwo = ToTemporalCalendarIdentifier(cx, two); + if (!calendarTwo) { + return false; + } + + // Steps 4-5. + bool equals; + if (!EqualStrings(cx, calendarOne, calendarTwo, &equals)) { + return false; + } + if (equals) { + return true; + } + + // Throw an error when the calendar identifiers don't match. Used when unequal + // calendars throw a RangeError. + if (auto charsOne = QuoteString(cx, calendarOne)) { + if (auto charsTwo = QuoteString(cx, calendarTwo)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE, + charsOne.get(), charsTwo.get()); + } + } + return false; +} + +/** + * ConsolidateCalendars ( one, two ) + */ +bool js::temporal::ConsolidateCalendars(JSContext* cx, + Handle<CalendarValue> one, + Handle<CalendarValue> two, + MutableHandle<CalendarValue> result) { + // Step 1. + if (one.isObject() && two.isObject() && one.toObject() == two.toObject()) { + result.set(two); + return true; + } + + // Step 2. + Rooted<JSString*> calendarOne(cx, ToTemporalCalendarIdentifier(cx, one)); + if (!calendarOne) { + return false; + } + + // Step 3. + Rooted<JSString*> calendarTwo(cx, ToTemporalCalendarIdentifier(cx, two)); + if (!calendarTwo) { + return false; + } + + // Step 4. + bool equals; + if (!EqualStrings(cx, calendarOne, calendarTwo, &equals)) { + return false; + } + if (equals) { + result.set(two); + return true; + } + + // Step 5. + bool isoCalendarOne; + if (!IsISO8601Calendar(cx, calendarOne, &isoCalendarOne)) { + return false; + } + if (isoCalendarOne) { + result.set(two); + return true; + } + + // Step 6. + bool isoCalendarTwo; + if (!IsISO8601Calendar(cx, calendarTwo, &isoCalendarTwo)) { + return false; + } + if (isoCalendarTwo) { + result.set(one); + return true; + } + + // Step 7. + if (auto charsOne = QuoteString(cx, calendarOne)) { + if (auto charsTwo = QuoteString(cx, calendarTwo)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE, + charsOne.get(), charsTwo.get()); + } + } + return false; +} + +/** + * Temporal.Calendar ( id ) + */ +static bool CalendarConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.Calendar")) { + return false; + } + + // Step 2. + if (!args.requireAtLeast(cx, "Temporal.Calendar", 1)) { + return false; + } + + if (!args[0].isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, "not a string"); + return false; + } + + Rooted<JSLinearString*> identifier(cx, args[0].toString()->ensureLinear(cx)); + if (!identifier) { + return false; + } + + // Step 3. + identifier = ThrowIfNotBuiltinCalendar(cx, identifier); + if (!identifier) { + return false; + } + + // Step 4. + auto* calendar = CreateTemporalCalendar(cx, args, identifier); + if (!calendar) { + return false; + } + + args.rval().setObject(*calendar); + return true; +} + +/** + * Temporal.Calendar.from ( item ) + */ +static bool Calendar_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // Step 2. + auto* obj = ToTemporalCalendarObject(cx, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * get Temporal.Calendar.prototype.id + */ +static bool Calendar_id(JSContext* cx, const CallArgs& args) { + auto* calendar = &args.thisv().toObject().as<CalendarObject>(); + + // Step 3. + args.rval().setString(calendar->identifier()); + return true; +} + +/** + * get Temporal.Calendar.prototype.id + */ +static bool Calendar_id(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_id>(cx, args); +} + +/** + * Temporal.Calendar.prototype.dateFromFields ( fields [ , options ] ) + */ +static bool Calendar_dateFromFields(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + Rooted<JSObject*> fields( + cx, RequireObjectArg(cx, "fields", "dateFromFields", args.get(0))); + if (!fields) { + return false; + } + + // Step 5. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "dateFromFields", args[1]); + if (!options) { + return false; + } + } + + // Steps 6-10. + auto* obj = BuiltinCalendarDateFromFields(cx, fields, options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.Calendar.prototype.dateFromFields ( fields [ , options ] ) + */ +static bool Calendar_dateFromFields(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_dateFromFields>(cx, args); +} + +/** + * Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] ) + */ +static bool Calendar_yearMonthFromFields(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + Rooted<JSObject*> fields( + cx, RequireObjectArg(cx, "fields", "yearMonthFromFields", args.get(0))); + if (!fields) { + return false; + } + + // Step 5. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "yearMonthFromFields", args[1]); + if (!options) { + return false; + } + } + + // Steps 6-10. + auto* obj = BuiltinCalendarYearMonthFromFields(cx, fields, options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.Calendar.prototype.yearMonthFromFields ( fields [ , options ] ) + */ +static bool Calendar_yearMonthFromFields(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_yearMonthFromFields>(cx, + args); +} + +/** + * Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] ) + */ +static bool Calendar_monthDayFromFields(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + Rooted<JSObject*> fields( + cx, RequireObjectArg(cx, "fields", "monthDayFromFields", args.get(0))); + if (!fields) { + return false; + } + + // Step 5. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "monthDayFromFields", args[1]); + if (!options) { + return false; + } + } + + // Steps 6-10. + auto* obj = BuiltinCalendarMonthDayFromFields(cx, fields, options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.Calendar.prototype.monthDayFromFields ( fields [ , options ] ) + */ +static bool Calendar_monthDayFromFields(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_monthDayFromFields>(cx, + args); +} + +/** + * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) + */ +static bool Calendar_dateAdd(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToTemporalDate(cx, args.get(0), &date)) { + return false; + } + + // Step 5. + Duration duration; + if (!ToTemporalDuration(cx, args.get(1), &duration)) { + return false; + } + + // Step 6. + Rooted<JSObject*> options(cx); + if (args.hasDefined(2)) { + options = RequireObjectArg(cx, "options", "dateAdd", args[2]); + if (!options) { + return false; + } + } + + // Steps 7-10. + auto* obj = BuiltinCalendarAdd(cx, date, duration, options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.Calendar.prototype.dateAdd ( date, duration [ , options ] ) + */ +static bool Calendar_dateAdd(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_dateAdd>(cx, args); +} + +/** + * Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ) + */ +static bool Calendar_dateUntil(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate one; + if (!ToTemporalDate(cx, args.get(0), &one)) { + return false; + } + + // Step 5. + PlainDate two; + if (!ToTemporalDate(cx, args.get(1), &two)) { + return false; + } + + // Steps 6-8. + auto largestUnit = TemporalUnit::Day; + if (args.hasDefined(2)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "dateUntil", args[2])); + if (!options) { + return false; + } + + // Steps 7-8. + if (!GetTemporalUnit(cx, options, TemporalUnitKey::LargestUnit, + TemporalUnitGroup::Date, &largestUnit)) { + return false; + } + } + + // Steps 9-10. + auto duration = BuiltinCalendarDateUntil(one, two, largestUnit); + + auto* obj = CreateTemporalDuration(cx, duration); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.Calendar.prototype.dateUntil ( one, two [ , options ] ) + */ +static bool Calendar_dateUntil(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_dateUntil>(cx, args); +} + +/** + * Temporal.Calendar.prototype.year ( temporalDateLike ) + */ +static bool Calendar_year(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainYearMonthObject>( + cx, args.get(0), &date)) { + return false; + } + + // Steps 5-6. + return BuiltinCalendarYear(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.year ( temporalDateLike ) + */ +static bool Calendar_year(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_year>(cx, args); +} + +/** + * Temporal.Calendar.prototype.month ( temporalDateLike ) + */ +static bool Calendar_month(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + Handle<Value> temporalDateLike = args.get(0); + + // Step 4. + if (temporalDateLike.isObject() && + temporalDateLike.toObject().canUnwrapAs<PlainMonthDayObject>()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + temporalDateLike, nullptr, "a PlainMonthDay object"); + return false; + } + + // Step 5. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainYearMonthObject>( + cx, temporalDateLike, &date)) { + return false; + } + + // Steps 6-7. + return BuiltinCalendarMonth(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.month ( temporalDateLike ) + */ +static bool Calendar_month(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_month>(cx, args); +} + +/** + * Temporal.Calendar.prototype.monthCode ( temporalDateLike ) + */ +static bool Calendar_monthCode(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainMonthDayObject, + PlainYearMonthObject>(cx, args.get(0), &date)) { + return false; + } + + // Steps 5-6. + return BuiltinCalendarMonthCode(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.monthCode ( temporalDateLike ) + */ +static bool Calendar_monthCode(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_monthCode>(cx, args); +} + +/** + * Temporal.Calendar.prototype.day ( temporalDateLike ) + */ +static bool Calendar_day(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainMonthDayObject>( + cx, args.get(0), &date)) { + return false; + } + + // Steps 5-6. + return BuiltinCalendarDay(date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.day ( temporalDateLike ) + */ +static bool Calendar_day(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_day>(cx, args); +} + +/** + * Temporal.Calendar.prototype.dayOfWeek ( temporalDateLike ) + */ +static bool Calendar_dayOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToTemporalDate(cx, args.get(0), &date)) { + return false; + } + + // Steps 5-9. + return BuiltinCalendarDayOfWeek(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.dayOfWeek ( temporalDateLike ) + */ +static bool Calendar_dayOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_dayOfWeek>(cx, args); +} + +/** + * Temporal.Calendar.prototype.dayOfYear ( temporalDateLike ) + */ +static bool Calendar_dayOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToTemporalDate(cx, args.get(0), &date)) { + return false; + } + + // Steps 5-7. + return BuiltinCalendarDayOfYear(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.dayOfYear ( temporalDateLike ) + */ +static bool Calendar_dayOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_dayOfYear>(cx, args); +} + +/** + * Temporal.Calendar.prototype.weekOfYear ( temporalDateLike ) + */ +static bool Calendar_weekOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToTemporalDate(cx, args.get(0), &date)) { + return false; + } + + // Steps 5-6. + return BuiltinCalendarWeekOfYear(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.weekOfYear ( temporalDateLike ) + */ +static bool Calendar_weekOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_weekOfYear>(cx, args); +} + +/** + * Temporal.Calendar.prototype.yearOfWeek ( temporalDateLike ) + */ +static bool Calendar_yearOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToTemporalDate(cx, args.get(0), &date)) { + return false; + } + + // Steps 5-6. + return BuiltinCalendarYearOfWeek(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.yearOfWeek ( temporalDateLike ) + */ +static bool Calendar_yearOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_yearOfWeek>(cx, args); +} + +/** + * Temporal.Calendar.prototype.daysInWeek ( temporalDateLike ) + */ +static bool Calendar_daysInWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToTemporalDate(cx, args.get(0), &date)) { + return false; + } + + // Step 5. + return BuiltinCalendarDaysInWeek(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.daysInWeek ( temporalDateLike ) + */ +static bool Calendar_daysInWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_daysInWeek>(cx, args); +} + +/** + * Temporal.Calendar.prototype.daysInMonth ( temporalDateLike ) + */ +static bool Calendar_daysInMonth(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainYearMonthObject>( + cx, args.get(0), &date)) { + return false; + } + + // Step 5. + return BuiltinCalendarDaysInMonth(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.daysInMonth ( temporalDateLike ) + */ +static bool Calendar_daysInMonth(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_daysInMonth>(cx, args); +} + +/** + * Temporal.Calendar.prototype.daysInYear ( temporalDateLike ) + */ +static bool Calendar_daysInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainYearMonthObject>( + cx, args.get(0), &date)) { + return false; + } + + // Step 5. + return BuiltinCalendarDaysInYear(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.daysInYear ( temporalDateLike ) + */ +static bool Calendar_daysInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_daysInYear>(cx, args); +} + +/** + * Temporal.Calendar.prototype.monthsInYear ( temporalDateLike ) + */ +static bool Calendar_monthsInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainYearMonthObject>( + cx, args.get(0), &date)) { + return false; + } + + // Step 5. + return BuiltinCalendarMonthsInYear(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.monthsInYear ( temporalDateLike ) + */ +static bool Calendar_monthsInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_monthsInYear>(cx, args); +} + +/** + * Temporal.Calendar.prototype.inLeapYear ( temporalDateLike ) + */ +static bool Calendar_inLeapYear(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + PlainDate date; + if (!ToPlainDate<PlainDateObject, PlainDateTimeObject, PlainYearMonthObject>( + cx, args.get(0), &date)) { + return false; + } + + // Steps 5-6. + return BuiltinCalendarInLeapYear(cx, date, args.rval()); +} + +/** + * Temporal.Calendar.prototype.inLeapYear ( temporalDateLike ) + */ +static bool Calendar_inLeapYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_inLeapYear>(cx, args); +} + +/** + * Temporal.Calendar.prototype.fields ( fields ) + */ +static bool Calendar_fields(JSContext* cx, const CallArgs& args) { + // Step 3. + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 4. + JS::ForOfIterator iterator(cx); + if (!iterator.init(args.get(0))) { + return false; + } + + // Step 5. + JS::RootedVector<Value> fieldNames(cx); + mozilla::EnumSet<CalendarField> seen; + + // Steps 6-7. + Rooted<Value> nextValue(cx); + Rooted<JSLinearString*> linear(cx); + while (true) { + // Steps 7.a and 7.b.i. + bool done; + if (!iterator.next(&nextValue, &done)) { + return false; + } + if (done) { + break; + } + + // Step 7.b.ii. + if (!nextValue.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, nextValue, + nullptr, "not a string"); + iterator.closeThrow(); + return false; + } + + linear = nextValue.toString()->ensureLinear(cx); + if (!linear) { + return false; + } + + // Step 7.b.iv. (Reordered) + CalendarField field; + if (!ToCalendarField(cx, linear, &field)) { + iterator.closeThrow(); + return false; + } + + // Step 7.b.iii. + if (seen.contains(field)) { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_CALENDAR_DUPLICATE_FIELD, + chars.get()); + } + iterator.closeThrow(); + return false; + } + + // Step 7.b.v. + if (!fieldNames.append(nextValue)) { + return false; + } + seen += field; + } + + // Step 8. + auto* array = + NewDenseCopiedArray(cx, fieldNames.length(), fieldNames.begin()); + if (!array) { + return false; + } + + args.rval().setObject(*array); + return true; +} + +/** + * Temporal.Calendar.prototype.fields ( fields ) + */ +static bool Calendar_fields(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_fields>(cx, args); +} + +/** + * Temporal.Calendar.prototype.mergeFields ( fields, additionalFields ) + */ +static bool Calendar_mergeFields(JSContext* cx, const CallArgs& args) { + // Step 7. (Reordered) + MOZ_ASSERT(IsISO8601Calendar(&args.thisv().toObject().as<CalendarObject>())); + + // Step 3. + Rooted<JSObject*> fields(cx, JS::ToObject(cx, args.get(0))); + if (!fields) { + return false; + } + + Rooted<PlainObject*> fieldsCopy( + cx, SnapshotOwnPropertiesIgnoreUndefined(cx, fields)); + if (!fieldsCopy) { + return false; + } + + // Step 4. + Rooted<JSObject*> additionalFields(cx, JS::ToObject(cx, args.get(1))); + if (!additionalFields) { + return false; + } + + Rooted<PlainObject*> additionalFieldsCopy( + cx, SnapshotOwnPropertiesIgnoreUndefined(cx, additionalFields)); + if (!additionalFieldsCopy) { + return false; + } + + // Steps 5-6. + // + // JSITER_HIDDEN doesn't need to be passed, because CopyDataProperties creates + // all properties as enumerable. + JS::RootedVector<PropertyKey> additionalKeys(cx); + if (!GetPropertyKeys(cx, additionalFieldsCopy, + JSITER_OWNONLY | JSITER_SYMBOLS, &additionalKeys)) { + return false; + } + + // Step 8. + Rooted<PropertyHashSet> overriddenKeys(cx, PropertyHashSet(cx)); + if (!ISOFieldKeysToIgnore(cx, additionalKeys, overriddenKeys.get())) { + return false; + } + + // Step 9. + Rooted<PlainObject*> merged(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!merged) { + return false; + } + + // Steps 10-11. + // + // JSITER_HIDDEN doesn't need to be passed, because CopyDataProperties creates + // all properties as enumerable. + JS::RootedVector<PropertyKey> fieldsKeys(cx); + if (!GetPropertyKeys(cx, fieldsCopy, JSITER_OWNONLY | JSITER_SYMBOLS, + &fieldsKeys)) { + return false; + } + + // Step 12. + Rooted<Value> propValue(cx); + for (size_t i = 0; i < fieldsKeys.length(); i++) { + Handle<PropertyKey> key = fieldsKeys[i]; + + // Step 12.a. + // FIXME: spec issue - unnecessary initialisation + // https://github.com/tc39/proposal-temporal/issues/2549 + + // Steps 12.b-c. + if (overriddenKeys.has(key)) { + if (!GetProperty(cx, additionalFieldsCopy, additionalFieldsCopy, key, + &propValue)) { + return false; + } + + // Step 12.d. (Reordered) + if (propValue.isUndefined()) { + // The property can be undefined if the key is "month" or "monthCode". + MOZ_ASSERT(key.isAtom(cx->names().month) || + key.isAtom(cx->names().monthCode)); + + continue; + } + } else { + if (!GetProperty(cx, fieldsCopy, fieldsCopy, key, &propValue)) { + return false; + } + + // All properties of |fieldsCopy| have a non-undefined value. + MOZ_ASSERT(!propValue.isUndefined()); + } + + // Step 12.d. + if (!DefineDataProperty(cx, merged, key, propValue)) { + return false; + } + } + + // Step 13. + if (!CopyDataProperties(cx, merged, additionalFieldsCopy)) { + return false; + } + + // Step 14. + args.rval().setObject(*merged); + return true; +} + +/** + * Temporal.Calendar.prototype.mergeFields ( fields, additionalFields ) + */ +static bool Calendar_mergeFields(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_mergeFields>(cx, args); +} + +/** + * Temporal.Calendar.prototype.toString ( ) + */ +static bool Calendar_toString(JSContext* cx, const CallArgs& args) { + auto* calendar = &args.thisv().toObject().as<CalendarObject>(); + + // Step 3. + args.rval().setString(calendar->identifier()); + return true; +} + +/** + * Temporal.Calendar.prototype.toString ( ) + */ +static bool Calendar_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_toString>(cx, args); +} + +/** + * Temporal.Calendar.prototype.toJSON ( ) + */ +static bool Calendar_toJSON(JSContext* cx, const CallArgs& args) { + auto* calendar = &args.thisv().toObject().as<CalendarObject>(); + + // Step 3. + args.rval().setString(calendar->identifier()); + return true; +} + +/** + * Temporal.Calendar.prototype.toJSON ( ) + */ +static bool Calendar_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsCalendar, Calendar_toJSON>(cx, args); +} + +const JSClass CalendarObject::class_ = { + "Temporal.Calendar", + JSCLASS_HAS_RESERVED_SLOTS(CalendarObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Calendar), + JS_NULL_CLASS_OPS, + &CalendarObject::classSpec_, +}; + +const JSClass& CalendarObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec Calendar_methods[] = { + JS_FN("from", Calendar_from, 1, 0), + JS_FS_END, +}; + +static const JSFunctionSpec Calendar_prototype_methods[] = { + JS_FN("dateFromFields", Calendar_dateFromFields, 1, 0), + JS_FN("yearMonthFromFields", Calendar_yearMonthFromFields, 1, 0), + JS_FN("monthDayFromFields", Calendar_monthDayFromFields, 1, 0), + JS_FN("dateAdd", Calendar_dateAdd, 2, 0), + JS_FN("dateUntil", Calendar_dateUntil, 2, 0), + JS_FN("year", Calendar_year, 1, 0), + JS_FN("month", Calendar_month, 1, 0), + JS_FN("monthCode", Calendar_monthCode, 1, 0), + JS_FN("day", Calendar_day, 1, 0), + JS_FN("dayOfWeek", Calendar_dayOfWeek, 1, 0), + JS_FN("dayOfYear", Calendar_dayOfYear, 1, 0), + JS_FN("weekOfYear", Calendar_weekOfYear, 1, 0), + JS_FN("yearOfWeek", Calendar_yearOfWeek, 1, 0), + JS_FN("daysInWeek", Calendar_daysInWeek, 1, 0), + JS_FN("daysInMonth", Calendar_daysInMonth, 1, 0), + JS_FN("daysInYear", Calendar_daysInYear, 1, 0), + JS_FN("monthsInYear", Calendar_monthsInYear, 1, 0), + JS_FN("inLeapYear", Calendar_inLeapYear, 1, 0), + JS_FN("fields", Calendar_fields, 1, 0), + JS_FN("mergeFields", Calendar_mergeFields, 2, 0), + JS_FN("toString", Calendar_toString, 0, 0), + JS_FN("toJSON", Calendar_toJSON, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec Calendar_prototype_properties[] = { + JS_PSG("id", Calendar_id, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.Calendar", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec CalendarObject::classSpec_ = { + GenericCreateConstructor<CalendarConstructor, 1, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<CalendarObject>, + Calendar_methods, + nullptr, + Calendar_prototype_methods, + Calendar_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; + +struct MOZ_STACK_CLASS CalendarNameAndNative final { + PropertyName* name; + JSNative native; +}; + +static CalendarNameAndNative GetCalendarNameAndNative(JSContext* cx, + CalendarField fieldName) { + switch (fieldName) { + case CalendarField::Year: + return {cx->names().year, Calendar_year}; + case CalendarField::Month: + return {cx->names().month, Calendar_month}; + case CalendarField::MonthCode: + return {cx->names().monthCode, Calendar_monthCode}; + case CalendarField::Day: + return {cx->names().day, Calendar_day}; + } + MOZ_CRASH("invalid temporal field name"); +} + +bool js::temporal::IsBuiltinAccess( + JSContext* cx, Handle<CalendarObject*> calendar, + std::initializer_list<CalendarField> fieldNames) { + // Don't optimize when the object has any own properties which may shadow the + // built-in methods. + if (!calendar->empty()) { + return false; + } + + JSObject* proto = cx->global()->maybeGetPrototype(JSProto_Calendar); + + // Don't attempt to optimize when the class isn't yet initialized. + if (!proto) { + return false; + } + + // Don't optimize when the prototype isn't the built-in prototype. + if (calendar->staticPrototype() != proto) { + return false; + } + + auto* nproto = &proto->as<NativeObject>(); + for (auto fieldName : fieldNames) { + auto [name, native] = GetCalendarNameAndNative(cx, fieldName); + auto prop = nproto->lookupPure(name); + + // Return if the property isn't a data property. + if (!prop || !prop->isDataProperty()) { + return false; + } + + // Return if the property isn't the initial method. + if (!IsNativeFunction(nproto->getSlot(prop->slot()), native)) { + return false; + } + } + + // TODO: Pass accessor list from caller to avoid excessive checks. + + // Additionally check the various calendar fields operations. + for (const auto& [name, native] : (CalendarNameAndNative[]){ + {cx->names().fields, Calendar_fields}, + {cx->names().mergeFields, Calendar_mergeFields}, + {cx->names().dateFromFields, Calendar_dateFromFields}, + {cx->names().monthDayFromFields, Calendar_monthDayFromFields}, + {cx->names().yearMonthFromFields, Calendar_yearMonthFromFields}, + }) { + auto prop = nproto->lookupPure(name); + + // Return if the property isn't a data property. + if (!prop || !prop->isDataProperty()) { + return false; + } + + // Return if the property isn't the initial method. + if (!IsNativeFunction(nproto->getSlot(prop->slot()), native)) { + return false; + } + } + + // CalendarFields observably uses array iteration. + bool arrayIterationSane; + if (!IsArrayIterationSane(cx, &arrayIterationSane)) { + cx->recoverFromOutOfMemory(); + return false; + } + if (!arrayIterationSane) { + return false; + } + + // Success! The access can be optimized. + return true; +} diff --git a/js/src/builtin/temporal/Calendar.h b/js/src/builtin/temporal/Calendar.h new file mode 100644 index 0000000000..f80f528d83 --- /dev/null +++ b/js/src/builtin/temporal/Calendar.h @@ -0,0 +1,956 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_Calendar_h +#define builtin_temporal_Calendar_h + +#include "mozilla/Assertions.h" +#include "mozilla/EnumSet.h" + +#include <initializer_list> +#include <stdint.h> + +#include "builtin/temporal/Wrapped.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" +#include "vm/StringType.h" + +class JS_PUBLIC_API JSTracer; + +namespace js { +struct ClassSpec; +class PlainObject; +} // namespace js + +namespace js::temporal { + +class CalendarObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t IDENTIFIER_SLOT = 0; + static constexpr uint32_t SLOT_COUNT = 1; + + JSLinearString* identifier() const { + return &getFixedSlot(IDENTIFIER_SLOT).toString()->asLinear(); + } + + private: + static const ClassSpec classSpec_; +}; + +/** + * Calendar value, which is either a string containing a canonical calendar + * identifier or an object. + */ +class CalendarValue final { + JS::Value value_{}; + + public: + /** + * Default initialize this CalendarValue. + */ + CalendarValue() = default; + + /** + * Default initialize this CalendarValue. + */ + explicit CalendarValue(const JS::Value& value) : value_(value) { + MOZ_ASSERT(value.isString() || value.isObject()); + MOZ_ASSERT_IF(value.isString(), value.toString()->isLinear()); + } + + /** + * Initialize this CalendarValue with a canonical calendar identifier. + */ + explicit CalendarValue(JSLinearString* calendarId) + : value_(JS::StringValue(calendarId)) {} + + /** + * Initialize this CalendarValue with a calendar object. + */ + explicit CalendarValue(JSObject* calendar) + : value_(JS::ObjectValue(*calendar)) {} + + /** + * Return true iff this CalendarValue is initialized with either a canonical + * calendar identifier or a calendar object. + */ + explicit operator bool() const { return !value_.isUndefined(); } + + /** + * Return this CalendarValue as a JS::Value. + */ + JS::Value toValue() const { return value_; } + + /** + * Return true if this CalendarValue is a string. + */ + bool isString() const { return value_.isString(); } + + /** + * Return true if this CalendarValue is an object. + */ + bool isObject() const { return value_.isObject(); } + + /** + * Return the calendar identifier. + */ + JSLinearString* toString() const { return &value_.toString()->asLinear(); } + + /** + * Return the calendar object. + */ + JSObject* toObject() const { return &value_.toObject(); } + + void trace(JSTracer* trc); + + JS::Value* valueDoNotUse() { return &value_; } + JS::Value const* valueDoNotUse() const { return &value_; } +}; + +enum class CalendarMethod { + DateAdd, + DateFromFields, + DateUntil, + Day, + Fields, + MergeFields, + MonthDayFromFields, + YearMonthFromFields, +}; + +class CalendarRecord { + CalendarValue receiver_; + + // Null unless non-builtin calendar methods are used. + JSObject* dateAdd_ = nullptr; + JSObject* dateFromFields_ = nullptr; + JSObject* dateUntil_ = nullptr; + JSObject* day_ = nullptr; + JSObject* fields_ = nullptr; + JSObject* mergeFields_ = nullptr; + JSObject* monthDayFromFields_ = nullptr; + JSObject* yearMonthFromFields_ = nullptr; + +#ifdef DEBUG + mozilla::EnumSet<CalendarMethod> lookedUp_{}; +#endif + + public: + /** + * Default initialize this CalendarRecord. + */ + CalendarRecord() = default; + + explicit CalendarRecord(const CalendarValue& receiver) + : receiver_(receiver) {} + + const auto& receiver() const { return receiver_; } + auto* dateAdd() const { return dateAdd_; } + auto* dateFromFields() const { return dateFromFields_; } + auto* dateUntil() const { return dateUntil_; } + auto* day() const { return day_; } + auto* fields() const { return fields_; } + auto* mergeFields() const { return mergeFields_; } + auto* monthDayFromFields() const { return monthDayFromFields_; } + auto* yearMonthFromFields() const { return yearMonthFromFields_; } + +#ifdef DEBUG + auto& lookedUp() const { return lookedUp_; } + auto& lookedUp() { return lookedUp_; } +#endif + + // Helper methods for (Mutable)WrappedPtrOperations. + auto* receiverDoNotUse() const { return &receiver_; } + auto* dateAddDoNotUse() const { return &dateAdd_; } + auto* dateAddDoNotUse() { return &dateAdd_; } + auto* dateFromFieldsDoNotUse() const { return &dateFromFields_; } + auto* dateFromFieldsDoNotUse() { return &dateFromFields_; } + auto* dateUntilDoNotUse() const { return &dateUntil_; } + auto* dateUntilDoNotUse() { return &dateUntil_; } + auto* dayDoNotUse() const { return &day_; } + auto* dayDoNotUse() { return &day_; } + auto* fieldsDoNotUse() const { return &fields_; } + auto* fieldsDoNotUse() { return &fields_; } + auto* mergeFieldsDoNotUse() const { return &mergeFields_; } + auto* mergeFieldsDoNotUse() { return &mergeFields_; } + auto* monthDayFromFieldsDoNotUse() const { return &monthDayFromFields_; } + auto* monthDayFromFieldsDoNotUse() { return &monthDayFromFields_; } + auto* yearMonthFromFieldsDoNotUse() const { return &yearMonthFromFields_; } + auto* yearMonthFromFieldsDoNotUse() { return &yearMonthFromFields_; } + + // Trace implementation. + void trace(JSTracer* trc); +}; + +struct Duration; +struct PlainDate; +struct PlainDateTime; +class DurationObject; +class PlainDateObject; +class PlainDateTimeObject; +class PlainMonthDayObject; +class PlainYearMonthObject; +enum class CalendarOption; +enum class TemporalUnit; + +/** + * ISODaysInYear ( year ) + */ +int32_t ISODaysInYear(int32_t year); + +/** + * ISODaysInMonth ( year, month ) + */ +int32_t ISODaysInMonth(int32_t year, int32_t month); + +/** + * ISODaysInMonth ( year, month ) + */ +int32_t ISODaysInMonth(double year, int32_t month); + +/** + * ToISODayOfYear ( year, month, day ) + */ +int32_t ToISODayOfYear(const PlainDate& date); + +/** + * 21.4.1.12 MakeDay ( year, month, date ) + */ +int32_t MakeDay(const PlainDate& date); + +/** + * 21.4.1.13 MakeDate ( day, time ) + */ +int64_t MakeDate(const PlainDateTime& dateTime); + +/** + * 21.4.1.13 MakeDate ( day, time ) + */ +int64_t MakeDate(int32_t year, int32_t month, int32_t day); + +/** + * Return the case-normalized calendar identifier if |id| is a built-in calendar + * identifier. Otherwise throws a RangeError. + */ +bool ToBuiltinCalendar(JSContext* cx, JS::Handle<JSString*> id, + JS::MutableHandle<CalendarValue> result); + +/** + * ToTemporalCalendarSlotValue ( temporalCalendarLike [ , default ] ) + */ +bool ToTemporalCalendar(JSContext* cx, + JS::Handle<JS::Value> temporalCalendarLike, + JS::MutableHandle<CalendarValue> result); + +/** + * ToTemporalCalendarSlotValue ( temporalCalendarLike [ , default ] ) + */ +bool ToTemporalCalendarWithISODefault( + JSContext* cx, JS::Handle<JS::Value> temporalCalendarLike, + JS::MutableHandle<CalendarValue> result); + +/** + * GetTemporalCalendarWithISODefault ( item ) + */ +bool GetTemporalCalendarWithISODefault(JSContext* cx, + JS::Handle<JSObject*> item, + JS::MutableHandle<CalendarValue> result); + +/** + * ToTemporalCalendarIdentifier ( calendarSlotValue ) + */ +JSString* ToTemporalCalendarIdentifier(JSContext* cx, + JS::Handle<CalendarValue> calendar); + +/** + * ToTemporalCalendarObject ( calendarSlotValue ) + */ +JSObject* ToTemporalCalendarObject(JSContext* cx, + JS::Handle<CalendarValue> calendar); + +enum class CalendarField { + Year, + Month, + MonthCode, + Day, +}; + +using CalendarFieldNames = JS::StackGCVector<JS::PropertyKey>; + +/** + * CalendarFields ( calendarRec, fieldNames ) + */ +bool CalendarFields(JSContext* cx, JS::Handle<CalendarRecord> calendar, + std::initializer_list<CalendarField> fieldNames, + JS::MutableHandle<CalendarFieldNames> result); + +/** + * CalendarMergeFields ( calendarRec, fields, additionalFields ) + */ +JSObject* CalendarMergeFields(JSContext* cx, + JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields, + JS::Handle<PlainObject*> additionalFields); + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> CalendarDateAdd( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, const Duration& duration); + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> CalendarDateAdd( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, const Duration& duration, + JS::Handle<JSObject*> options); + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> CalendarDateAdd( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + JS::Handle<Wrapped<DurationObject*>> duration); + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> CalendarDateAdd( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + JS::Handle<Wrapped<DurationObject*>> duration, + JS::Handle<JSObject*> options); + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +bool CalendarDateAdd(JSContext* cx, JS::Handle<CalendarRecord> calendar, + const PlainDate& date, const Duration& duration, + PlainDate* result); + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +bool CalendarDateAdd(JSContext* cx, JS::Handle<CalendarRecord> calendar, + const PlainDate& date, const Duration& duration, + JS::Handle<JSObject*> options, PlainDate* result); + +/** + * CalendarDateAdd ( calendarRec, date, duration [ , options ] ) + */ +bool CalendarDateAdd(JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, PlainDate* result); + +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool CalendarDateUntil(JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> one, + JS::Handle<Wrapped<PlainDateObject*>> two, + JS::Handle<PlainObject*> options, Duration* result); + +/** + * CalendarDateUntil ( calendarRec, one, two, options ) + */ +bool CalendarDateUntil(JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> one, + JS::Handle<Wrapped<PlainDateObject*>> two, + TemporalUnit largestUnit, Duration* result); + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool CalendarYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool CalendarYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool CalendarYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainYearMonthObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarYear ( calendar, dateLike ) + */ +bool CalendarYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool CalendarMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool CalendarMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool CalendarMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainYearMonthObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonth ( calendar, dateLike ) + */ +bool CalendarMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool CalendarMonthCode(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool CalendarMonthCode(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool CalendarMonthCode(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainMonthDayObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool CalendarMonthCode(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainYearMonthObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthCode ( calendar, dateLike ) + */ +bool CalendarMonthCode(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool CalendarDay(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool CalendarDay(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool CalendarDay(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainMonthDayObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool CalendarDay(JSContext* cx, JS::Handle<CalendarRecord> calendar, + const PlainDate& date, JS::MutableHandle<JS::Value> result); + +/** + * CalendarDay ( calendarRec, dateLike ) + */ +bool CalendarDay(JSContext* cx, JS::Handle<CalendarRecord> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDayOfWeek ( calendar, dateLike ) + */ +bool CalendarDayOfWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDayOfWeek ( calendar, dateLike ) + */ +bool CalendarDayOfWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDayOfWeek ( calendar, dateLike ) + */ +bool CalendarDayOfWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDayOfYear ( calendar, dateLike ) + */ +bool CalendarDayOfYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDayOfYear ( calendar, dateLike ) + */ +bool CalendarDayOfYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDayOfYear ( calendar, dateLike ) + */ +bool CalendarDayOfYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarWeekOfYear ( calendar, dateLike ) + */ +bool CalendarWeekOfYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarWeekOfYear ( calendar, dateLike ) + */ +bool CalendarWeekOfYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarWeekOfYear ( calendar, dateLike ) + */ +bool CalendarWeekOfYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarYearOfWeek ( calendar, dateLike ) + */ +bool CalendarYearOfWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarYearOfWeek ( calendar, dateLike ) + */ +bool CalendarYearOfWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarYearOfWeek ( calendar, dateLike ) + */ +bool CalendarYearOfWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInWeek ( calendar, dateLike ) + */ +bool CalendarDaysInWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInWeek ( calendar, dateLike ) + */ +bool CalendarDaysInWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInWeek ( calendar, dateLike ) + */ +bool CalendarDaysInWeek(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool CalendarDaysInMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool CalendarDaysInMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool CalendarDaysInMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainYearMonthObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInMonth ( calendar, dateLike ) + */ +bool CalendarDaysInMonth(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool CalendarDaysInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool CalendarDaysInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool CalendarDaysInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainYearMonthObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDaysInYear ( calendar, dateLike ) + */ +bool CalendarDaysInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool CalendarMonthsInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool CalendarMonthsInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool CalendarMonthsInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainYearMonthObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarMonthsInYear ( calendar, dateLike ) + */ +bool CalendarMonthsInYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool CalendarInLeapYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool CalendarInLeapYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainDateTimeObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool CalendarInLeapYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + JS::Handle<PlainYearMonthObject*> dateLike, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarInLeapYear ( calendar, dateLike ) + */ +bool CalendarInLeapYear(JSContext* cx, JS::Handle<CalendarValue> calendar, + const PlainDateTime& dateTime, + JS::MutableHandle<JS::Value> result); + +/** + * CalendarDateFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainDateObject*> CalendarDateFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields); + +/** + * CalendarDateFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainDateObject*> CalendarDateFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields, JS::Handle<PlainObject*> options); + +/** + * CalendarYearMonthFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainYearMonthObject*> CalendarYearMonthFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields); + +/** + * CalendarYearMonthFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainYearMonthObject*> CalendarYearMonthFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainYearMonthObject*> fields); + +/** + * CalendarYearMonthFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainYearMonthObject*> CalendarYearMonthFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields, JS::Handle<PlainObject*> options); + +/** + * CalendarMonthDayFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainMonthDayObject*> CalendarMonthDayFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields); + +/** + * CalendarMonthDayFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainMonthDayObject*> CalendarMonthDayFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainMonthDayObject*> fields); + +/** + * CalendarMonthDayFromFields ( calendarRec, fields [ , options ] ) + */ +Wrapped<PlainMonthDayObject*> CalendarMonthDayFromFields( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields, JS::Handle<PlainObject*> options); + +/** + * CalendarEquals ( one, two ) + */ +bool CalendarEquals(JSContext* cx, JS::Handle<CalendarValue> one, + JS::Handle<CalendarValue> two, bool* equals); + +/** + * CalendarEquals ( one, two ) + */ +bool CalendarEqualsOrThrow(JSContext* cx, JS::Handle<CalendarValue> one, + JS::Handle<CalendarValue> two); + +/** + * ConsolidateCalendars ( one, two ) + */ +bool ConsolidateCalendars(JSContext* cx, JS::Handle<CalendarValue> one, + JS::Handle<CalendarValue> two, + JS::MutableHandle<CalendarValue> result); + +/** + * CreateCalendarMethodsRecord ( calendar, methods ) + */ +bool CreateCalendarMethodsRecord(JSContext* cx, + JS::Handle<CalendarValue> calendar, + mozilla::EnumSet<CalendarMethod> methods, + JS::MutableHandle<CalendarRecord> result); + +#ifdef DEBUG +/** + * CalendarMethodsRecordHasLookedUp ( calendarRec, methodName ) + */ +inline bool CalendarMethodsRecordHasLookedUp(const CalendarRecord& calendar, + CalendarMethod methodName) { + // Steps 1-10. + return calendar.lookedUp().contains(methodName); +} +#endif + +/** + * CalendarMethodsRecordIsBuiltin ( calendarRec ) + */ +inline bool CalendarMethodsRecordIsBuiltin(const CalendarRecord& calendar) { + // Steps 1-2. + return calendar.receiver().isString(); +} + +/** + * Return true when accessing the calendar fields |fieldNames| can be optimized. + * Otherwise returns false. + */ +bool IsBuiltinAccess(JSContext* cx, JS::Handle<CalendarObject*> calendar, + std::initializer_list<CalendarField> fieldNames); + +// Helper for MutableWrappedPtrOperations. +bool WrapCalendarValue(JSContext* cx, JS::MutableHandle<JS::Value> calendar); + +} /* namespace js::temporal */ + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<temporal::CalendarValue, Wrapper> { + const auto& container() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + explicit operator bool() const { return bool(container()); } + + JS::Handle<JS::Value> toValue() const { + return JS::Handle<JS::Value>::fromMarkedLocation( + container().valueDoNotUse()); + } + + bool isString() const { return container().isString(); } + + bool isObject() const { return container().isObject(); } + + JSLinearString* toString() const { return container().toString(); } + + JSObject* toObject() const { return container().toObject(); } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<temporal::CalendarValue, Wrapper> + : public WrappedPtrOperations<temporal::CalendarValue, Wrapper> { + auto& container() { return static_cast<Wrapper*>(this)->get(); } + + JS::MutableHandle<JS::Value> toMutableValue() { + return JS::MutableHandle<JS::Value>::fromMarkedLocation( + container().valueDoNotUse()); + } + + public: + bool wrap(JSContext* cx) { + return temporal::WrapCalendarValue(cx, toMutableValue()); + } +}; + +template <typename Wrapper> +class WrappedPtrOperations<temporal::CalendarRecord, Wrapper> { + const auto& container() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + JS::Handle<temporal::CalendarValue> receiver() const { + return JS::Handle<temporal::CalendarValue>::fromMarkedLocation( + container().receiverDoNotUse()); + } + + JS::Handle<JSObject*> dateAdd() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().dateAddDoNotUse()); + } + JS::Handle<JSObject*> dateFromFields() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().dateFromFieldsDoNotUse()); + } + JS::Handle<JSObject*> dateUntil() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().dateUntilDoNotUse()); + } + JS::Handle<JSObject*> day() const { + return JS::Handle<JSObject*>::fromMarkedLocation(container().dayDoNotUse()); + } + JS::Handle<JSObject*> fields() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().fieldsDoNotUse()); + } + JS::Handle<JSObject*> mergeFields() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().mergeFieldsDoNotUse()); + } + JS::Handle<JSObject*> monthDayFromFields() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().monthDayFromFieldsDoNotUse()); + } + JS::Handle<JSObject*> yearMonthFromFields() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().yearMonthFromFieldsDoNotUse()); + } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<temporal::CalendarRecord, Wrapper> + : public WrappedPtrOperations<temporal::CalendarRecord, Wrapper> { + auto& container() { return static_cast<Wrapper*>(this)->get(); } + + public: + JS::MutableHandle<JSObject*> dateAdd() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().dateAddDoNotUse()); + } + JS::MutableHandle<JSObject*> dateFromFields() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().dateFromFieldsDoNotUse()); + } + JS::MutableHandle<JSObject*> dateUntil() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().dateUntilDoNotUse()); + } + JS::MutableHandle<JSObject*> day() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().dayDoNotUse()); + } + JS::MutableHandle<JSObject*> fields() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().fieldsDoNotUse()); + } + JS::MutableHandle<JSObject*> mergeFields() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().mergeFieldsDoNotUse()); + } + JS::MutableHandle<JSObject*> monthDayFromFields() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().monthDayFromFieldsDoNotUse()); + } + JS::MutableHandle<JSObject*> yearMonthFromFields() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().yearMonthFromFieldsDoNotUse()); + } +}; + +} /* namespace js */ + +#endif /* builtin_temporal_Calendar_h */ diff --git a/js/src/builtin/temporal/Duration.cpp b/js/src/builtin/temporal/Duration.cpp new file mode 100644 index 0000000000..7e922aa68b --- /dev/null +++ b/js/src/builtin/temporal/Duration.cpp @@ -0,0 +1,6802 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/Duration.h" + +#include "mozilla/Assertions.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/Instant.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.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/Wrapped.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/StringBuffer.h" +#include "vm/BigIntType.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) { + 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) { + 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 ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +int32_t js::temporal::DurationSign(const Duration& duration) { + MOZ_ASSERT(IsIntegerOrInfinityDuration(duration)); + + 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; +} + +/** + * 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; + + // Step 1. + int32_t sign = DurationSign(duration); + + // 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 && sign > 0) { + return false; + } + + // Step 2.c. + if (v > 0 && sign < 0) { + return false; + } + } + + // Step 3. + return true; +} + +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +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; + + // 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; + } + + // Steps 2.b-c. + if ((v < 0 && sign > 0) || (v > 0 && sign < 0)) { + report(v, name, JSMSG_TEMPORAL_DURATION_INVALID_SIGN); + return false; + } + + 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; + } + + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 3. + return true; +} + +/** + * DefaultTemporalLargestUnit ( years, months, weeks, days, hours, minutes, + * seconds, milliseconds, microseconds ) + */ +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) { + 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) { + 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; +} + +/** + * ToTemporalDurationRecord ( temporalDurationLike ) + */ +bool js::temporal::ToTemporalDurationRecord(JSContext* cx, + Handle<Value> temporalDurationLike, + Duration* result) { + // Step 1. + if (!temporalDurationLike.isObject()) { + // Step 1.a. + if (!temporalDurationLike.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, + temporalDurationLike, nullptr, "not a string"); + return false; + } + Rooted<JSString*> string(cx, temporalDurationLike.toString()); + + // Step 1.b. + return ParseTemporalDurationString(cx, string, result); + } + + Rooted<JSObject*> durationLike(cx, &temporalDurationLike.toObject()); + + // Step 2. + if (auto* duration = durationLike->maybeUnwrapIf<DurationObject>()) { + *result = ToDuration(duration); + return true; + } + + // Step 3. + Duration duration = {}; + + // Steps 4-14. + if (!ToTemporalPartialDurationRecord(cx, durationLike, &duration)) { + return false; + } + + // Step 15. + if (!ThrowIfInvalidDuration(cx, duration)) { + return false; + } + + // Step 16. + *result = duration; + return true; +} + +/** + * ToTemporalDuration ( item ) + */ +Wrapped<DurationObject*> js::temporal::ToTemporalDuration(JSContext* cx, + Handle<Value> item) { + // Step 1. + if (item.isObject()) { + JSObject* itemObj = &item.toObject(); + if (itemObj->canUnwrapAs<DurationObject>()) { + return itemObj; + } + } + + // Step 2. + Duration result; + if (!ToTemporalDurationRecord(cx, item, &result)) { + return nullptr; + } + + // Step 3. + return CreateTemporalDuration(cx, result); +} + +/** + * ToTemporalDuration ( item ) + */ +bool js::temporal::ToTemporalDuration(JSContext* cx, Handle<Value> item, + Duration* result) { + auto obj = ToTemporalDuration(cx, item); + if (!obj) { + return false; + } + + *result = ToDuration(&obj.unwrap()); + return true; +} + +/** + * DaysUntil ( earlier, later ) + */ +int32_t js::temporal::DaysUntil(const PlainDate& earlier, + const PlainDate& later) { + MOZ_ASSERT(ISODateTimeWithinLimits(earlier)); + MOZ_ASSERT(ISODateTimeWithinLimits(later)); + + // Steps 1-2. + int32_t epochDaysEarlier = MakeDay(earlier); + MOZ_ASSERT(std::abs(epochDaysEarlier) <= 100'000'000); + + // Steps 3-4. + int32_t epochDaysLater = MakeDay(later); + MOZ_ASSERT(std::abs(epochDaysLater) <= 100'000'000); + + // Step 5. + return epochDaysLater - epochDaysEarlier; +} + +/** + * MoveRelativeDate ( calendarRec, relativeTo, duration ) + */ +static bool MoveRelativeDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> relativeTo, const Duration& duration, + MutableHandle<Wrapped<PlainDateObject*>> relativeToResult, + int32_t* daysResult) { + auto* unwrappedRelativeTo = relativeTo.unwrap(cx); + if (!unwrappedRelativeTo) { + return false; + } + auto relativeToDate = ToPlainDate(unwrappedRelativeTo); + + // Step 1. + auto newDate = AddDate(cx, calendar, relativeTo, duration); + if (!newDate) { + return false; + } + auto later = ToPlainDate(&newDate.unwrap()); + relativeToResult.set(newDate); + + // Step 2. + *daysResult = DaysUntil(relativeToDate, later); + MOZ_ASSERT(std::abs(*daysResult) <= 200'000'000); + + // Step 3. + return true; +} + +/** + * MoveRelativeZonedDateTime ( zonedDateTime, calendarRec, timeZoneRec, years, + * months, weeks, days, precalculatedPlainDateTime ) + */ +static bool MoveRelativeZonedDateTime( + JSContext* cx, Handle<ZonedDateTime> zonedDateTime, + Handle<CalendarRecord> calendar, Handle<TimeZoneRecord> timeZone, + const Duration& duration, + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, + MutableHandle<ZonedDateTime> result) { + // Step 1. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Step 2. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + // Step 3. + Instant intermediateNs; + if (precalculatedPlainDateTime) { + if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, + duration.date(), *precalculatedPlainDateTime, + &intermediateNs)) { + return false; + } + } else { + if (!AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, + duration.date(), &intermediateNs)) { + return false; + } + } + MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); + + // Step 4. + result.set(ZonedDateTime{intermediateNs, zonedDateTime.timeZone(), + zonedDateTime.calendar()}); + return true; +} + +/** + * 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; + } + + Rooted<BigInt*> nanoseconds( + cx, TotalDurationNanosecondsSlow(cx, duration.time())); + if (!nanoseconds) { + return false; + } + + 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)); + + return NanosecondsToDays(cx, nanoseconds, zonedRelativeTo, timeZone, + result); + } + + auto* nanoseconds = TotalDurationNanosecondsSlow(cx, duration.time()); + if (!nanoseconds) { + return false; + } + + // 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); +} + +/** + * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds ) + */ +static TimeDuration CreateTimeDurationRecord(int64_t days, int64_t hours, + int64_t minutes, int64_t seconds, + int64_t milliseconds, + 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)})); + + // Step 2. + return { + double(days), double(hours), double(minutes), + double(seconds), double(milliseconds), double(microseconds), + double(nanoseconds), + }; +} + +/** + * 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) { + // Step 1. + MOZ_ASSERT(IsValidDuration({0, 0, 0, days, hours, minutes, seconds, + milliseconds, microseconds, 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), + }; +} + +/** + * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds, largestUnit ) + * + * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds, largestUnit ) + */ +static TimeDuration BalanceTimeDuration(int64_t nanoseconds, + TemporalUnit largestUnit) { + // Step 1. (Handled in caller.) + + // Step 2. + 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.) + // + // We don't need to convert to positive numbers, because integer division + // truncates and the %-operator has modulo semantics. + + // Steps 5-11. + switch (largestUnit) { + // Step 5. + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Day: { + // Step 5.a. + microseconds = nanoseconds / 1000; + + // Step 5.b. + nanoseconds = nanoseconds % 1000; + + // Step 5.c. + milliseconds = microseconds / 1000; + + // Step 5.d. + microseconds = microseconds % 1000; + + // Step 5.e. + seconds = milliseconds / 1000; + + // Step 5.f. + milliseconds = milliseconds % 1000; + + // Step 5.g. + minutes = seconds / 60; + + // Step 5.h. + seconds = seconds % 60; + + // Step 5.i. + hours = minutes / 60; + + // Step 5.j. + minutes = minutes % 60; + + // Step 5.k. + days = hours / 24; + + // Step 5.l. + hours = hours % 24; + + break; + } + + case TemporalUnit::Hour: { + // Step 6.a. + microseconds = nanoseconds / 1000; + + // Step 6.b. + nanoseconds = nanoseconds % 1000; + + // Step 6.c. + milliseconds = microseconds / 1000; + + // Step 6.d. + microseconds = microseconds % 1000; + + // Step 6.e. + seconds = milliseconds / 1000; + + // Step 6.f. + milliseconds = milliseconds % 1000; + + // Step 6.g. + minutes = seconds / 60; + + // Step 6.h. + seconds = seconds % 60; + + // Step 6.i. + hours = minutes / 60; + + // Step 6.j. + minutes = minutes % 60; + + break; + } + + // Step 7. + case TemporalUnit::Minute: { + // Step 7.a. + microseconds = nanoseconds / 1000; + + // Step 7.b. + nanoseconds = nanoseconds % 1000; + + // Step 7.c. + milliseconds = microseconds / 1000; + + // Step 7.d. + microseconds = microseconds % 1000; + + // Step 7.e. + seconds = milliseconds / 1000; + + // Step 7.f. + milliseconds = milliseconds % 1000; + + // Step 7.g. + minutes = seconds / 60; + + // Step 7.h. + seconds = seconds % 60; + + break; + } + + // Step 8. + 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. + microseconds = nanoseconds / 1000; + + // Step 9.b. + nanoseconds = nanoseconds % 1000; + + // Step 9.c. + milliseconds = microseconds / 1000; + + // Step 9.d. + microseconds = microseconds % 1000; + + break; + } + + // Step 10. + case TemporalUnit::Microsecond: { + // Step 10.a. + microseconds = nanoseconds / 1000; + + // Step 10.b. + nanoseconds = nanoseconds % 1000; + + break; + } + + // Step 11. + case TemporalUnit::Nanosecond: { + // Nothing to do. + break; + } + + case TemporalUnit::Auto: + MOZ_CRASH("Unexpected temporal unit"); + } + + // Step 12. (Not applicable, all values are finite) + + // Step 13. + return CreateTimeDurationRecord(days, hours, minutes, seconds, milliseconds, + microseconds, nanoseconds); +} + +/** + * BalancePossiblyInfiniteTimeDuration ( days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds, 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; + } + + // 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); + + // Steps 4-5. + // + // 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; + } + + Rooted<BigInt*> sixty(cx, BigInt::createFromInt64(cx, 60)); + if (!sixty) { + return false; + } + + Rooted<BigInt*> twentyfour(cx, BigInt::createFromInt64(cx, 24)); + if (!twentyfour) { + return false; + } + + switch (largestUnit) { + // Step 6. + 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; + } + + // Step 7. + case TemporalUnit::Hour: { + // Steps 7.a-b. + if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, + &nanoseconds)) { + return false; + } + + // Steps 7.c-d. + if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, + µseconds)) { + return false; + } + + // Steps 7.e-f. + if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, + &milliseconds)) { + return false; + } + + // Steps 7.g-h. + if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { + return false; + } + + // Steps 7.i-j. + if (!BigInt::divmod(cx, minutes, sixty, &hours, &minutes)) { + return false; + } + + break; + } + + // Step 8. + case TemporalUnit::Minute: { + // Steps 8.a-b. + if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, + &nanoseconds)) { + return false; + } + + // Steps 8.c-d. + if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, + µseconds)) { + return false; + } + + // Steps 8.e-f. + if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, + &milliseconds)) { + return false; + } + + // Steps 8.g-h. + if (!BigInt::divmod(cx, seconds, sixty, &minutes, &seconds)) { + return false; + } + + break; + } + + // Step 9. + case TemporalUnit::Second: { + // Steps 9.a-b. + if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, + &nanoseconds)) { + return false; + } + + // Steps 9.c-d. + if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, + µseconds)) { + return false; + } + + // Steps 9.e-f. + if (!BigInt::divmod(cx, milliseconds, thousand, &seconds, + &milliseconds)) { + return false; + } + + break; + } + + // Step 10. + case TemporalUnit::Millisecond: { + // Steps 10.a-b. + if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, + &nanoseconds)) { + return false; + } + + // Steps 10.c-d. + if (!BigInt::divmod(cx, microseconds, thousand, &milliseconds, + µseconds)) { + return false; + } + + break; + } + + // Step 11. + case TemporalUnit::Microsecond: { + // Steps 11.a-b. + if (!BigInt::divmod(cx, nanoseconds, thousand, µseconds, + &nanoseconds)) { + 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, + }; + 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; + } + return ThrowIfInvalidDuration(cx, result->toDuration()); +} + +/** + * 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, + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, + TimeDuration* result) { + // Step 1. (Not applicable) + + // Step 2. + auto intermediateNs = relativeTo.instant(); + + // Step 3. + const auto& startInstant = relativeTo.instant(); + + // Step 4. + PlainDateTime startDateTime; + if (duration.days != 0) { + // Step 4.a. + if (!precalculatedPlainDateTime) { + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &startDateTime)) { + return false; + } + precalculatedPlainDateTime = + mozilla::SomeRef<const PlainDateTime>(startDateTime); + } + + // Steps 4.b-c. + Rooted<CalendarValue> isoCalendar(cx, CalendarValue(cx->names().iso8601)); + if (!AddDaysToZonedDateTime(cx, startInstant, *precalculatedPlainDateTime, + timeZone, isoCalendar, duration.days, + &intermediateNs)) { + return false; + } + } + + // Step 5. + Instant 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)); + + // Step 7. + if (nanoseconds == InstantSpan{}) { + *result = {}; + return true; + } + + // Steps 8-9. + double days = 0; + if (TemporalUnit::Year <= largestUnit && largestUnit <= TemporalUnit::Day) { + // Step 8.a. + if (!precalculatedPlainDateTime) { + if (!GetPlainDateTimeFor(cx, timeZone, startInstant, &startDateTime)) { + return false; + } + precalculatedPlainDateTime = + mozilla::SomeRef<const PlainDateTime>(startDateTime); + } + + // Step 8.b. + Rooted<temporal::NanosecondsAndDays> nanosAndDays(cx); + if (!NanosecondsToDays(cx, nanoseconds, relativeTo, timeZone, + *precalculatedPlainDateTime, &nanosAndDays)) { + 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 + + // Step 8.d. + nanoseconds = nanosAndDays.nanoseconds(); + MOZ_ASSERT_IF(days > 0, nanoseconds >= InstantSpan{}); + MOZ_ASSERT_IF(days < 0, nanoseconds <= InstantSpan{}); + + // Step 8.e. + largestUnit = TemporalUnit::Hour; + } + + // Step 10. (Not applicable in our implementation.) + + // Steps 11-12. + 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; + } + } + + // Step 13. + *result = { + days, + balanceResult.hours, + balanceResult.minutes, + balanceResult.seconds, + balanceResult.milliseconds, + balanceResult.microseconds, + balanceResult.nanoseconds, + }; + 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})); + 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})) { + return false; + } + + *result = {years, months, weeks, days}; + return true; +} + +static bool UnbalanceDateDurationRelativeHasEffect(const Duration& duration, + TemporalUnit largestUnit) { + MOZ_ASSERT(largestUnit != TemporalUnit::Auto); + + // Steps 2, 3.a-b, 4.a-b, 6-7. + return (largestUnit > TemporalUnit::Year && duration.years != 0) || + (largestUnit > TemporalUnit::Month && duration.months != 0) || + (largestUnit > TemporalUnit::Week && duration.weeks != 0); +} + +/** + * UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * plainRelativeTo, calendarRec ) + */ +static bool UnbalanceDateDurationRelative( + JSContext* cx, const Duration& 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; + + // Step 1. (Not applicable in our implementation.) + + // Steps 2, 3.a, 4.a, and 6. + if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { + // Steps 2.a, 3.a, 4.a, and 6. + *result = CreateDateDurationRecord(years, months, weeks, days); + return true; + } + + // Step 3. + if (largestUnit == TemporalUnit::Month) { + // Step 3.a. (Handled above) + MOZ_ASSERT(years != 0); + + // Step 3.b. (Not applicable in our implementation.) + + // Step 3.c. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 3.d. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + + // Step 3.e. + auto yearsDuration = Duration{years}; + + // Step 3.f. + Rooted<Wrapped<PlainDateObject*>> later( + cx, CalendarDateAdd(cx, calendar, plainRelativeTo, yearsDuration)); + if (!later) { + return false; + } + + // Steps 3.g-i. + Duration untilResult; + if (!CalendarDateUntil(cx, calendar, plainRelativeTo, later, + TemporalUnit::Month, &untilResult)) { + return false; + } + + // Step 3.j. + double 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. + return CreateDateDurationRecord(cx, 0, months + yearsInMonths, weeks, days, + result); + } + + // Step 4. + 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 4.e. + auto later = + CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsDuration); + if (!later) { + return false; + } + auto laterDate = ToPlainDate(&later.unwrap()); + + auto* unwrappedRelativeTo = plainRelativeTo.unwrap(cx); + if (!unwrappedRelativeTo) { + return false; + } + auto relativeToDate = ToPlainDate(unwrappedRelativeTo); + + // Step 4.f. + 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. + 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 9. + auto yearsMonthsWeeksDuration = Duration{years, months, weeks}; + + // Step 10. + auto later = + CalendarDateAdd(cx, calendar, plainRelativeTo, yearsMonthsWeeksDuration); + if (!later) { + return false; + } + auto laterDate = ToPlainDate(&later.unwrap()); + + auto* unwrappedRelativeTo = plainRelativeTo.unwrap(cx); + if (!unwrappedRelativeTo) { + return false; + } + auto relativeToDate = ToPlainDate(unwrappedRelativeTo); + + // Step 11. + 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. + return CreateDateDurationRecord(cx, 0, 0, 0, days + yearsMonthsWeeksInDay, + result); +} + +/** + * UnbalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * plainRelativeTo, calendarRec ) + */ +static bool UnbalanceDateDurationRelative(JSContext* cx, + const Duration& 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. + if (!UnbalanceDateDurationRelativeHasEffect(duration, largestUnit)) { + // Steps 2.a, 3.a, 4.a, and 6. + *result = CreateDateDurationRecord(years, months, weeks, days); + return true; + } + + // Step 5. (Not applicable.) + + // Steps 3.b, 4.b, and 7. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, "calendar"); + return false; +} + +/** + * BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * smallestUnit, plainRelativeTo, calendarRec ) + */ +static bool BalanceDateDurationRelative( + JSContext* cx, const Duration& 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; + + // FIXME: spec issue - effectful code paths should be more fine-grained + // similar to UnbalanceDateDurationRelative. For example: + // 1. If largestUnit = "year" and days = 0 and months = 0, then no-op. + // 2. Else if largestUnit = "month" and days = 0, then no-op. + // 3. Else if days = 0, then no-op. + // + // Also note that |weeks| is never balanced, even when non-zero. + + // Step 1. (Not applicable in our implementation.) + + // Steps 2-4. + if (largestUnit > TemporalUnit::Week || + (years == 0 && months == 0 && weeks == 0 && days == 0)) { + // Step 4.a. + *result = CreateDateDurationRecord(years, months, weeks, days); + return true; + } + + // Step 5. + if (!plainRelativeTo) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, + "relativeTo"); + return false; + } + + // Step 6. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 7. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + + // Steps 8-9. (Not applicable in our implementation.) + + auto untilAddedDate = [&](const Duration& duration, Duration* untilResult) { + Rooted<Wrapped<PlainDateObject*>> later( + cx, AddDate(cx, calendar, plainRelativeTo, duration)); + if (!later) { + return false; + } + + return CalendarDateUntil(cx, calendar, plainRelativeTo, later, largestUnit, + untilResult); + }; + + // Step 10. + if (largestUnit == TemporalUnit::Year) { + // Step 10.a. + if (smallestUnit == TemporalUnit::Week) { + // Step 10.a.i. + MOZ_ASSERT(days == 0); + + // Step 10.a.ii. + auto yearsMonthsDuration = Duration{years, months}; + + // Steps 10.a.iii-iv. + Duration 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); + return true; + } + + // Step 10.b. + auto yearsMonthsWeeksDaysDuration = Duration{years, months, weeks, days}; + + // Steps 10.c-d. + Duration 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); + return true; + } + + // Step 11. + if (largestUnit == TemporalUnit::Month) { + // Step 11.a. + MOZ_ASSERT(years == 0); + + // Step 11.b. + if (smallestUnit == TemporalUnit::Week) { + // Step 10.b.i. + MOZ_ASSERT(days == 0); + + // Step 10.b.ii. + *result = CreateDateDurationRecord(0, months, weeks, 0); + return true; + } + + // Step 11.c. + auto monthsWeeksDaysDuration = Duration{0, months, weeks, days}; + + // Steps 11.d-e. + Duration 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); + return true; + } + + // Step 12. + MOZ_ASSERT(largestUnit == TemporalUnit::Week); + + // Step 13. + MOZ_ASSERT(years == 0); + + // Step 14. + MOZ_ASSERT(months == 0); + + // Step 15. + auto weeksDaysDuration = Duration{0, 0, weeks, days}; + + // Steps 16-17. + Duration 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; +} + +/** + * BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * smallestUnit, plainRelativeTo, calendarRec ) + */ +bool js::temporal::BalanceDateDurationRelative( + JSContext* cx, const Duration& duration, TemporalUnit largestUnit, + TemporalUnit smallestUnit, + Handle<Wrapped<PlainDateObject*>> plainRelativeTo, + Handle<CalendarRecord> calendar, DateDuration* result) { + MOZ_ASSERT(plainRelativeTo); + MOZ_ASSERT(calendar.receiver()); + + return ::BalanceDateDurationRelative(cx, duration, largestUnit, smallestUnit, + plainRelativeTo, calendar, result); +} + +/** + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + */ +static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, + Duration* duration) { + MOZ_ASSERT(IsValidDuration(one)); + MOZ_ASSERT(IsValidDuration(two)); + + // Steps 1-2. (Not applicable) + + // Step 3. + auto largestUnit1 = DefaultTemporalLargestUnit(one); + + // Step 4. + auto largestUnit2 = DefaultTemporalLargestUnit(two); + + // Step 5. + auto largestUnit = std::min(largestUnit1, largestUnit2); + + // Step 6.a. + if (largestUnit <= TemporalUnit::Week) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_UNCOMPARABLE, + "relativeTo"); + return false; + } + + // Step 6.b. + TimeDuration result; + if (!BalanceTimeDuration(cx, one, two, largestUnit, &result)) { + return false; + } + + // Steps 6.c. + *duration = result.toDuration(); + return true; +} + +/** + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + */ +static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, + Handle<Wrapped<PlainDateObject*>> plainRelativeTo, + Handle<CalendarRecord> calendar, Duration* duration) { + 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); + + // Step 4. + auto largestUnit2 = DefaultTemporalLargestUnit(two); + + // Step 5. + auto largestUnit = std::min(largestUnit1, largestUnit2); + + // Step 6. (Not applicable) + + // Step 7.a. (Not applicable in our implementation.) + + // Step 7.b. + auto dateDuration1 = one.date(); + + // Step 7.c. + auto dateDuration2 = two.date(); + + // FIXME: spec issue - calendarUnitsPresent is unused. + + // Step 7.d. + [[maybe_unused]] bool calendarUnitsPresent = true; + + // 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 7.f. + Rooted<Wrapped<PlainDateObject*>> intermediate( + cx, AddDate(cx, calendar, plainRelativeTo, dateDuration1)); + if (!intermediate) { + return false; + } + + // Step 7.g. + Rooted<Wrapped<PlainDateObject*>> end( + cx, AddDate(cx, calendar, intermediate, dateDuration2)); + if (!end) { + return false; + } + + // Step 7.h. + auto dateLargestUnit = std::min(TemporalUnit::Day, largestUnit); + + // Steps 7.i-k. + Duration 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)) { + 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, + }; + return true; +} + +/** + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + */ +static bool AddDuration( + JSContext* cx, const Duration& one, const Duration& two, + Handle<ZonedDateTime> zonedRelativeTo, Handle<CalendarRecord> calendar, + Handle<TimeZoneRecord> timeZone, + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, + Duration* result) { + // Steps 1-2. (Not applicable) + + // Step 3. + auto largestUnit1 = DefaultTemporalLargestUnit(one); + + // Step 4. + auto largestUnit2 = DefaultTemporalLargestUnit(two); + + // Step 5. + auto largestUnit = std::min(largestUnit1, largestUnit2); + + // Steps 6-7. (Not applicable) + + // Steps 8-9. (Not applicable in our implementation.) + + // 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 + + // Step 10. + 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; + } + + // Step 13. + Instant intermediateNs; + if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, + one, startDateTime, &intermediateNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); + + // Step 14. + Instant endNs; + if (!AddZonedDateTime(cx, intermediateNs, timeZone, calendar, two, + &endNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(endNs)); + + // Step 15. (Not applicable) + + // Step 16. + return DifferenceZonedDateTime(cx, zonedRelativeTo.instant(), endNs, + timeZone, calendar, largestUnit, + startDateTime, result); + } + + // Steps 11-12. (Not applicable) + + // Step 13. (Inlined AddZonedDateTime, step 6.) + Instant intermediateNs; + if (!AddInstant(cx, zonedRelativeTo.instant(), one, &intermediateNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); + + // Step 14. (Inlined AddZonedDateTime, step 6.) + Instant endNs; + if (!AddInstant(cx, intermediateNs, two, &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); +} + +/** + * AddDuration ( y1, mon1, w1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, w2, + * d2, h2, min2, s2, ms2, mus2, ns2, plainRelativeTo, calendarRec, + * zonedRelativeTo, timeZoneRec [ , precalculatedPlainDateTime ] ) + */ +static bool AddDuration(JSContext* cx, const Duration& one, const Duration& two, + Handle<ZonedDateTime> zonedRelativeTo, + Handle<CalendarRecord> calendar, + Handle<TimeZoneRecord> timeZone, Duration* result) { + return AddDuration(cx, one, two, zonedRelativeTo, calendar, timeZone, + 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, + * precalculatedPlainDateTime ) + */ +static bool AdjustRoundedDurationDays( + 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, + Duration* result) { + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 1. + if ((TemporalUnit::Year <= unit && unit <= TemporalUnit::Day) || + (unit == TemporalUnit::Nanosecond && increment == Increment{1})) { + *result = duration; + return true; + } + + // The increment is limited for all smaller temporal units. + MOZ_ASSERT(increment < MaximumTemporalDurationRoundingIncrement(unit)); + + // Step 2. + MOZ_ASSERT(precalculatedPlainDateTime); + + // Steps 4-6. + // + // Step 3 is moved below, so compute |direction| through DurationSign. + int32_t direction = DurationSign(duration.time()); + + // Steps 7-8. + Instant dayStart; + if (!AddZonedDateTime(cx, zonedRelativeTo.instant(), timeZone, calendar, + duration.date(), *precalculatedPlainDateTime, + &dayStart)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(dayStart)); + + // Step 9. + PlainDateTime dayStartDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, dayStart, &dayStartDateTime)) { + return false; + } + + // Step 10. + Instant dayEnd; + if (!AddDaysToZonedDateTime(cx, dayStart, dayStartDateTime, timeZone, + zonedRelativeTo.calendar(), direction, &dayEnd)) { + return false; + } + 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 12. + auto checkedOneDayLess = *timeRemainderNs - dayLength.toNanoseconds(); + if (!checkedOneDayLess.isValid()) { + return AdjustRoundedDurationDaysSlow( + cx, duration, increment, unit, roundingMode, zonedRelativeTo, calendar, + timeZone, precalculatedPlainDateTime, dayLength, result); + } + auto oneDayLess = checkedOneDayLess.value(); + + // Step 13. + if ((direction > 0 && oneDayLess < 0) || (direction < 0 && oneDayLess > 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; + } + + // 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); +} + +/** + * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, + * seconds, milliseconds, microseconds, nanoseconds, increment, unit, + * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, + * precalculatedPlainDateTime ) + */ +bool js::temporal::AdjustRoundedDurationDays( + JSContext* cx, const Duration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + Handle<ZonedDateTime> zonedRelativeTo, Handle<CalendarRecord> calendar, + Handle<TimeZoneRecord> timeZone, + const PlainDateTime& precalculatedPlainDateTime, Duration* 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); + + if (num < DOUBLE_INTEGRAL_PRECISION_LIMIT) { + ToCStringBuf cbuf; + size_t length; + const char* numStr = NumberToCString(&cbuf, num, &length); + + return sb.append(numStr, length); + } + + Rooted<BigInt*> bi(cx, BigInt::createFromDouble(cx, num)); + if (!bi) { + return false; + } + return BigIntToStringBuilder(cx, bi, sb); +} + +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. + uint32_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. + uint32_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 ( years, months, weeks, days, hours, minutes, + * seconds, milliseconds, microseconds, nanoseconds, precision ) + */ +static JSString* TemporalDurationToString(JSContext* cx, + const Duration& duration, + Precision precision) { + 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 && + (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; + } + + // 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); + + 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); + } + } + + // Steps 8 and 13. + JSStringBuilder result(cx); + + // Step 1. (Reordered) + int32_t sign = DurationSign(duration); + + // Step 21. (Reordered) + if (sign < 0) { + if (!result.append('-')) { + return nullptr; + } + } + + // Step 22. (Reordered) + if (!result.append('P')) { + return nullptr; + } + + // Step 9. + if (years != 0) { + if (!NumberToStringBuilder(cx, years, result)) { + return nullptr; + } + if (!result.append('Y')) { + return nullptr; + } + } + + // Step 10. + if (months != 0) { + if (!NumberToStringBuilder(cx, months, result)) { + return nullptr; + } + if (!result.append('M')) { + return nullptr; + } + } + + // Step 11. + if (weeks != 0) { + if (!NumberToStringBuilder(cx, weeks, result)) { + return nullptr; + } + if (!result.append('W')) { + return nullptr; + } + } + + // Step 12. + if (days != 0) { + if (!NumberToStringBuilder(cx, days, result)) { + return nullptr; + } + if (!result.append('D')) { + return nullptr; + } + } + + // Steps 16-17. + bool nonzeroSecondsAndLower = seconds != 0 || milliseconds != 0 || + microseconds != 0 || nanoseconds != 0; + MOZ_ASSERT(nonzeroSecondsAndLower == + (totalSeconds != 0 || + (totalSecondsBigInt && !totalSecondsBigInt->isZero()) || + fraction != 0)); + + // Steps 18-19. + 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(); + + if (hours != 0 || minutes != 0 || hasSecondsPart) { + // Step 23. (Reordered) + if (!result.append('T')) { + return nullptr; + } + + // Step 14. + if (hours != 0) { + if (!NumberToStringBuilder(cx, hours, result)) { + return nullptr; + } + if (!result.append('H')) { + return nullptr; + } + } + + // Step 15. + if (minutes != 0) { + if (!NumberToStringBuilder(cx, minutes, result)) { + return nullptr; + } + if (!result.append('M')) { + return nullptr; + } + } + + // Step 20. + if (hasSecondsPart) { + // Step 20.a. + if (totalSecondsBigInt) { + if (!BigIntToStringBuilder(cx, totalSecondsBigInt, result)) { + return nullptr; + } + } else { + if (!NumberToStringBuilder(cx, totalSeconds, result)) { + return nullptr; + } + } + + // Step 20.b. (Moved above) + + // Step 20.c. + if (!FormatFractionalSeconds(result, fraction, precision)) { + return nullptr; + } + + // Step 20.d. + if (!result.append('S')) { + return nullptr; + } + } + } + + // Step 24. + return result.finishString(); +} + +/** + * ToRelativeTemporalObject ( options ) + */ +static bool ToRelativeTemporalObject( + JSContext* cx, Handle<JSObject*> options, + MutableHandle<Wrapped<PlainDateObject*>> plainRelativeTo, + MutableHandle<ZonedDateTime> zonedRelativeTo, + MutableHandle<TimeZoneRecord> timeZoneRecord) { + // Step 1. + Rooted<Value> value(cx); + if (!GetProperty(cx, options, options, cx->names().relativeTo, &value)) { + return false; + } + + // 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{}); + return true; + } + + // Step 3. + auto offsetBehaviour = OffsetBehaviour::Option; + + // Step 4. + auto matchBehaviour = MatchBehaviour::MatchExactly; + + // Steps 5-6. + PlainDateTime dateTime; + Rooted<CalendarValue> calendar(cx); + Rooted<TimeZoneValue> timeZone(cx); + int64_t offsetNs; + if (value.isObject()) { + Rooted<JSObject*> obj(cx, &value.toObject()); + + // Step 5.a. + if (auto* zonedDateTime = obj->maybeUnwrapIf<ZonedDateTimeObject>()) { + auto instant = ToInstant(zonedDateTime); + 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. + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord( + cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { + return false; + } + + // Step 5.a.ii. + plainRelativeTo.set(nullptr); + zonedRelativeTo.set(ZonedDateTime{instant, timeZone, calendar}); + timeZoneRecord.set(timeZoneRec); + return true; + } + + // Step 5.b. + if (obj->canUnwrapAs<PlainDateObject>()) { + plainRelativeTo.set(obj); + zonedRelativeTo.set(ZonedDateTime{}); + timeZoneRecord.set(TimeZoneRecord{}); + return true; + } + + // Step 5.c. + if (auto* dateTime = obj->maybeUnwrapIf<PlainDateTimeObject>()) { + auto plainDateTime = ToPlainDate(dateTime); + + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + // Step 5.c.i. + auto* plainDate = CreateTemporalDate(cx, plainDateTime, calendar); + if (!plainDate) { + return false; + } + + // Step 5.c.ii. + plainRelativeTo.set(plainDate); + zonedRelativeTo.set(ZonedDateTime{}); + timeZoneRecord.set(TimeZoneRecord{}); + return true; + } + + // Step 5.d. + if (!GetTemporalCalendarWithISODefault(cx, obj, &calendar)) { + return false; + } + + // Step 5.e. + Rooted<CalendarRecord> calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + }, + &calendarRec)) { + return false; + } + + // Step 5.f. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendarRec, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 5.g. + if (!AppendSorted(cx, fieldNames.get(), + { + TemporalField::Hour, + TemporalField::Microsecond, + TemporalField::Millisecond, + TemporalField::Minute, + TemporalField::Nanosecond, + TemporalField::Offset, + TemporalField::Second, + TemporalField::TimeZone, + })) { + return false; + } + + // Step 5.h. + Rooted<PlainObject*> fields(cx, PrepareTemporalFields(cx, obj, fieldNames)); + if (!fields) { + return false; + } + + // Step 5.i. + Rooted<PlainObject*> dateOptions(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!dateOptions) { + return false; + } + + // Step 5.j. + Rooted<Value> overflow(cx, StringValue(cx->names().constrain)); + if (!DefineDataProperty(cx, dateOptions, cx->names().overflow, overflow)) { + return false; + } + + // Step 5.k. + if (!InterpretTemporalDateTimeFields(cx, calendarRec, fields, dateOptions, + &dateTime)) { + return false; + } + + // Step 5.l. + Rooted<Value> offset(cx); + if (!GetProperty(cx, fields, fields, cx->names().offset, &offset)) { + return false; + } + + // Step 5.m. + Rooted<Value> timeZoneValue(cx); + if (!GetProperty(cx, fields, fields, cx->names().timeZone, + &timeZoneValue)) { + return false; + } + + // Step 5.n. + if (!timeZoneValue.isUndefined()) { + if (!ToTemporalTimeZone(cx, timeZoneValue, &timeZone)) { + return false; + } + } + + // Step 5.o. + if (offset.isUndefined()) { + offsetBehaviour = OffsetBehaviour::Wall; + } + + // Steps 8-9. + if (timeZone) { + if (offsetBehaviour == OffsetBehaviour::Option) { + MOZ_ASSERT(!offset.isUndefined()); + MOZ_ASSERT(offset.isString()); + + // Step 8.a. + Rooted<JSString*> offsetString(cx, offset.toString()); + if (!offsetString) { + return false; + } + + // Step 8.b. + if (!ParseDateTimeUTCOffset(cx, offsetString, &offsetNs)) { + return false; + } + } else { + // Step 9. + offsetNs = 0; + } + } + } 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. + bool isUTC; + bool hasOffset; + int64_t timeZoneOffset; + Rooted<ParsedTimeZone> timeZoneName(cx); + Rooted<JSString*> calendarString(cx); + if (!ParseTemporalRelativeToString(cx, string, &dateTime, &isUTC, + &hasOffset, &timeZoneOffset, + &timeZoneName, &calendarString)) { + return false; + } + + // Step 6.c. (Not applicable in our implementation.) + + // Steps 6.e-f. + if (timeZoneName) { + // Step 6.f.i. + if (!ToTemporalTimeZone(cx, timeZoneName, &timeZone)) { + return false; + } + + // Steps 6.f.ii-iii. + if (isUTC) { + offsetBehaviour = OffsetBehaviour::Exact; + } else if (!hasOffset) { + offsetBehaviour = OffsetBehaviour::Wall; + } + + // Step 6.f.iv. + matchBehaviour = MatchBehaviour::MatchMinutes; + } else { + MOZ_ASSERT(!timeZone); + } + + // Steps 6.g-j. + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendar)) { + return false; + } + } else { + calendar.set(CalendarValue(cx->names().iso8601)); + } + + // Steps 8-9. + if (timeZone) { + if (offsetBehaviour == OffsetBehaviour::Option) { + MOZ_ASSERT(hasOffset); + + // Step 8.a. + offsetNs = timeZoneOffset; + } else { + // Step 9. + offsetNs = 0; + } + } + } + + // Step 7. + if (!timeZone) { + // Step 7.a. + auto* plainDate = CreateTemporalDate(cx, dateTime.date, calendar); + if (!plainDate) { + return false; + } + + plainRelativeTo.set(plainDate); + zonedRelativeTo.set(ZonedDateTime{}); + timeZoneRecord.set(TimeZoneRecord{}); + return true; + } + + // Steps 8-9. (Moved above) + + // Step 10. + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { + return false; + } + + // Step 11. + Instant epochNanoseconds; + if (!InterpretISODateTimeOffset( + cx, dateTime, offsetBehaviour, offsetNs, timeZoneRec, + TemporalDisambiguation::Compatible, TemporalOffset::Reject, + matchBehaviour, &epochNanoseconds)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(epochNanoseconds)); + + // Step 12. + plainRelativeTo.set(nullptr); + zonedRelativeTo.set(ZonedDateTime{epochNanoseconds, timeZone, calendar}); + timeZoneRecord.set(timeZoneRec); + return true; +} + +/** + * CreateCalendarMethodsRecordFromRelativeTo ( plainRelativeTo, zonedRelativeTo, + * methods ) + */ +static bool CreateCalendarMethodsRecordFromRelativeTo( + JSContext* cx, Handle<Wrapped<PlainDateObject*>> plainRelativeTo, + Handle<ZonedDateTime> zonedRelativeTo, + mozilla::EnumSet<CalendarMethod> methods, + MutableHandle<CalendarRecord> result) { + // Step 1. + if (zonedRelativeTo) { + return CreateCalendarMethodsRecord(cx, zonedRelativeTo.calendar(), methods, + result); + } + + // Step 2. + if (plainRelativeTo) { + auto* unwrapped = plainRelativeTo.unwrap(cx); + if (!unwrapped) { + return false; + } + + Rooted<CalendarValue> calendar(cx, unwrapped->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + return CreateCalendarMethodsRecord(cx, calendar, methods, result); + } + + // Step 3. + 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; +} + +/** + * 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); + } +} + +/** + * RoundNumberToIncrement ( x, 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; + } + + int64_t num, denom; + if (BigInt::isInt64(numerator, &num) && + BigInt::isInt64(denominator, &denom)) { + TruncateNumber(num, denom, quotient, total); + return true; + } + + // 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; +} + +/** + * RoundNumberToIncrement ( x, 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; + } + + // BigInt division truncates. + Rooted<BigInt*> quot(cx); + Rooted<BigInt*> rem(cx); + if (!BigInt::divmod(cx, numerator, denom, ", &rem)) { + return false; + } + + double q = BigInt::numberValue(quot); + *quotient = q; + *total = q + BigInt::numberValue(rem) / double(denominator); + return true; +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +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); + } + + Rooted<BigInt*> totalNs(cx, TotalDurationNanosecondsSlow(cx, toRound)); + if (!totalNs) { + return false; + } + + return RoundNumberToIncrement(cx, totalNs, unit, increment, roundingMode, + result); +} + +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 ] ] ] ] ] ) + */ +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); + + auto [years, months, weeks, days, hours, minutes, seconds, milliseconds, + microseconds, nanoseconds] = duration; + + // 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; + } + } + + // 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; +} + +/** + * 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, 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); + + RoundedDuration rounded; + if (!::RoundDuration(cx, duration, increment, unit, roundingMode, + ComputeRemainder::Yes, &rounded)) { + return false; + } + + *result = rounded.total; + return true; +} + +/** + * 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)); + + RoundedDuration rounded; + if (!::RoundDuration(cx, duration, increment, unit, roundingMode, + ComputeRemainder::No, &rounded)) { + return false; + } + + *result = rounded.duration; + 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, + Duration* result) { + MOZ_ASSERT(IsValidDuration(duration)); + + return ::RoundDuration(cx, duration, increment, unit, roundingMode, result); +} + +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); +} + +static BigInt* DaysFrom(JSContext* cx, + Handle<temporal::NanosecondsAndDays> nanosAndDays) { + if (auto days = nanosAndDays.days()) { + return days; + } + 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; + } + + auto totalDays = mozilla::CheckedInt64(intDays); + totalDays += *nanoDays; + totalDays += daysToAdd; + if (!totalDays.isValid()) { + break; + } + + int64_t truncatedDays = totalDays.value(); + if (nanosAndDays.nanoseconds() > InstantSpan{}) { + // 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{}) { + // 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; + } + + 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; + } + } + } + + *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; + } + + biAmount = BigInt::add(cx, biAmount, biAmountPassed); + if (!biAmount) { + return false; + } + + 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; + } + + double 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, + 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. + + // clang-format off + // + // Change the representation of |fractionalWeeks| from a real number to a + // rational number, because we don't support arbitrary precision real + // numbers. + // + // |fractionalWeeks| is defined as: + // + // fractionalWeeks + // = weeks + days' / abs(oneWeekDays) + // + // where days' = days + nanoseconds / dayLength. + // + // The fractional part |nanoseconds / dayLength| is from step 4. + // + // The denominator for |fractionalWeeks| is |dayLength * abs(oneWeekDays)|. + // + // fractionalWeeks + // = weeks + (days + nanoseconds / dayLength) / abs(oneWeekDays) + // = weeks + days / abs(oneWeekDays) + nanoseconds / (dayLength * abs(oneWeekDays)) + // = (weeks * dayLength * abs(oneWeekDays) + days * dayLength + nanoseconds) / (dayLength * abs(oneWeekDays)) + // + // clang-format on + + do { + auto nanoseconds = nanosAndDays.nanoseconds().toNanoseconds(); + if (!nanoseconds.isValid()) { + break; + } + + auto dayLength = nanosAndDays.dayLength().toNanoseconds(); + if (!dayLength.isValid()) { + break; + } + + auto denominator = dayLength * std::abs(oneUnitDays); + if (!denominator.isValid()) { + break; + } + + int64_t intDays; + if (!mozilla::NumberEqualsInt64(durationDays, &intDays)) { + break; + } + + auto nanoDays = DaysFrom(nanosAndDays); + if (!nanoDays) { + break; + } + + auto totalDays = mozilla::CheckedInt64(intDays); + totalDays += *nanoDays; + totalDays += daysToAdd; + if (!totalDays.isValid()) { + break; + } + + auto totalNanoseconds = dayLength * totalDays; + if (!totalNanoseconds.isValid()) { + break; + } + + totalNanoseconds += nanoseconds; + if (!totalNanoseconds.isValid()) { + break; + } + + int64_t intAmount; + if (!mozilla::NumberEqualsInt64(durationAmount, &intAmount)) { + break; + } + + int64_t intAmountPassed; + if (!mozilla::NumberEqualsInt64(amountPassed, &intAmountPassed)) { + break; + } + + auto totalAmount = mozilla::CheckedInt64(intAmount) + intAmountPassed; + if (!totalAmount.isValid()) { + break; + } + + auto amountNanos = denominator * totalAmount; + if (!amountNanos.isValid()) { + break; + } + + totalNanoseconds += amountNanos; + if (!totalNanoseconds.isValid()) { + break; + } + + 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); + } + + *result = {rounded, total}; + return true; + } while (false); + + return RoundNumberToIncrementSlow( + cx, durationAmount, amountPassed, durationDays, daysToAdd, nanosAndDays, + oneUnitDays, increment, roundingMode, computeRemainder, result); +} + +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); +} + +static bool RoundDurationYear(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; + + double years = duration.years; + double months = duration.months; + double weeks = duration.weeks; + double days = duration.days; + + // Step 10.a. + Duration yearsDuration = {years}; + + // Step 10.b. + auto yearsLater = AddDate(cx, calendar, dateRelativeTo, yearsDuration); + if (!yearsLater) { + return false; + } + auto yearsLaterDate = ToPlainDate(&yearsLater.unwrap()); + + // Step 10.f. (Reordered) + Rooted<Wrapped<PlainDateObject*>> newRelativeTo(cx, yearsLater); + + // Step 10.c. + Duration yearsMonthsWeeks = {years, months, weeks}; + + // Step 10.d. + PlainDate yearsMonthsWeeksLater; + if (!AddDate(cx, calendar, dateRelativeTo, yearsMonthsWeeks, + &yearsMonthsWeeksLater)) { + return false; + } + + // Step 10.e. + int32_t monthsWeeksInDays = DaysUntil(yearsLaterDate, yearsMonthsWeeksLater); + MOZ_ASSERT(std::abs(monthsWeeksInDays) <= epochDays); + + // Step 10.f. (Moved up) + + // Step 10.g. + // Our implementation keeps |days| and |monthsWeeksInDays| separate. + + // 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; + } + + PlainDate isoResult; + if (!AddISODate(cx, yearsLaterDate, {0, 0, 0, truncatedDays}, + TemporalOverflow::Constrain, &isoResult)) { + return false; + } + + // Step 10.i. + Rooted<PlainDateObject*> wholeDaysLater( + cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); + if (!wholeDaysLater) { + return false; + } + + // Steps 10.j-l. + Duration timePassed; + if (!DifferenceDate(cx, calendar, newRelativeTo, wholeDaysLater, + TemporalUnit::Year, &timePassed)) { + return false; + } + + // Step 10.m. + double yearsPassed = timePassed.years; + + // Step 10.n. + // Our implementation keeps |years| and |yearsPassed| separate. + + // Step 10.o. + Duration yearsPassedDuration = {yearsPassed}; + + // Steps 10.p-r. + int32_t daysPassed; + if (!MoveRelativeDate(cx, calendar, newRelativeTo, yearsPassedDuration, + &newRelativeTo, &daysPassed)) { + return false; + } + MOZ_ASSERT(std::abs(daysPassed) <= epochDays); + + // Step 10.s. + // + // Our implementation keeps |days| and |daysPassed| separate. + int32_t daysToAdd = monthsWeeksInDays - daysPassed; + MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); + + // Steps 10.t. + double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + + // Step 10.u. + Duration oneYear = {sign}; + + // Steps 10.v-w. + Rooted<Wrapped<PlainDateObject*>> moveResultIgnored(cx); + int32_t oneYearDays; + if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneYear, + &moveResultIgnored, &oneYearDays)) { + return false; + } + + // Step 10.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; + + // Step 10.ab. + double numMonths = 0; + double numWeeks = 0; + + // Step 20. + Duration resultDuration = {numYears, numMonths, numWeeks}; + if (!ThrowIfInvalidDuration(cx, resultDuration)) { + return false; + } + + // Step 21. + *result = {resultDuration, 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; + + double years = duration.years; + double months = duration.months; + double weeks = duration.weeks; + double days = duration.days; + + // Step 11.a. + Duration yearsMonths = {years, months}; + + // Step 11.b. + auto yearsMonthsLater = AddDate(cx, calendar, dateRelativeTo, yearsMonths); + if (!yearsMonthsLater) { + return false; + } + auto yearsMonthsLaterDate = ToPlainDate(&yearsMonthsLater.unwrap()); + + // Step 11.f. (Reordered) + Rooted<Wrapped<PlainDateObject*>> newRelativeTo(cx, yearsMonthsLater); + + // Step 11.c. + Duration yearsMonthsWeeks = {years, months, weeks}; + + // Step 11.d. + PlainDate yearsMonthsWeeksLater; + if (!AddDate(cx, calendar, dateRelativeTo, yearsMonthsWeeks, + &yearsMonthsWeeksLater)) { + return false; + } + + // Step 11.e. + int32_t weeksInDays = DaysUntil(yearsMonthsLaterDate, yearsMonthsWeeksLater); + MOZ_ASSERT(std::abs(weeksInDays) <= epochDays); + + // Step 11.f. (Moved up) + + // Step 11.g. + // Our implementation keeps |days| and |weeksInDays| separate. + + // 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; + } + + PlainDate isoResult; + if (!AddISODate(cx, yearsMonthsLaterDate, {0, 0, 0, truncatedDays}, + TemporalOverflow::Constrain, &isoResult)) { + return false; + } + + // Step 11.i. + Rooted<PlainDateObject*> wholeDaysLater( + cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); + if (!wholeDaysLater) { + return false; + } + + // Steps 11.j-l. + Duration timePassed; + if (!DifferenceDate(cx, calendar, newRelativeTo, wholeDaysLater, + TemporalUnit::Month, &timePassed)) { + return false; + } + + // Step 11.m. + double monthsPassed = timePassed.months; + + // Step 11.n. + // Our implementation keeps |months| and |monthsPassed| separate. + + // Step 11.o. + Duration monthsPassedDuration = {0, monthsPassed}; + + // Steps 11.p-r. + int32_t daysPassed; + if (!MoveRelativeDate(cx, calendar, newRelativeTo, monthsPassedDuration, + &newRelativeTo, &daysPassed)) { + return false; + } + MOZ_ASSERT(std::abs(daysPassed) <= epochDays); + + // Step 11.s. + // + // Our implementation keeps |days| and |daysPassed| separate. + int32_t daysToAdd = weeksInDays - daysPassed; + MOZ_ASSERT(std::abs(daysToAdd) <= epochDays * 2); + + // Steps 11.t. + double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + + // Step 11.u. + Duration oneMonth = {0, sign}; + + // Steps 11.v-w. + Rooted<Wrapped<PlainDateObject*>> moveResultIgnored(cx); + int32_t oneMonthDays; + if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneMonth, + &moveResultIgnored, &oneMonthDays)) { + return false; + } + + // Step 11.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 11.ab. + double numWeeks = 0; + + // Step 20. + Duration resultDuration = {years, numMonths, numWeeks}; + if (!ThrowIfInvalidDuration(cx, resultDuration)) { + return false; + } + + // Step 21. + *result = {resultDuration, total}; + return true; +} + +static bool RoundDurationWeek(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; + + double years = duration.years; + double months = duration.months; + double weeks = duration.weeks; + double days = duration.days; + + auto* unwrappedRelativeTo = dateRelativeTo.unwrap(cx); + if (!unwrappedRelativeTo) { + return false; + } + 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; + } + + PlainDate isoResult; + if (!AddISODate(cx, relativeToDate, {0, 0, 0, truncatedDays}, + TemporalOverflow::Constrain, &isoResult)) { + return false; + } + + // Step 12.b. + Rooted<PlainDateObject*> wholeDaysLater( + cx, CreateTemporalDate(cx, isoResult, calendar.receiver())); + if (!wholeDaysLater) { + return false; + } + + // Steps 12.c-e. + Duration timePassed; + if (!DifferenceDate(cx, calendar, dateRelativeTo, wholeDaysLater, + TemporalUnit::Week, &timePassed)) { + return false; + } + + // Step 12.f. + double weeksPassed = timePassed.weeks; + + // Step 12.g. + // Our implementation keeps |weeks| and |weeksPassed| separate. + + // Step 12.h. + Duration weeksPassedDuration = {0, 0, weeksPassed}; + + // Steps 12.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); + + // Step 12.l. + // + // Our implementation keeps |days| and |daysPassed| separate. + int32_t daysToAdd = -daysPassed; + MOZ_ASSERT(std::abs(daysToAdd) <= epochDays); + + // Steps 12.m. + double sign = DaysIsNegative(days, nanosAndDays, daysToAdd) ? -1 : 1; + + // Step 12.n. + Duration oneWeek = {0, 0, sign}; + + // Steps 12.o-p. + Rooted<Wrapped<PlainDateObject*>> moveResultIgnored(cx); + int32_t oneWeekDays; + if (!MoveRelativeDate(cx, calendar, newRelativeTo, oneWeek, + &moveResultIgnored, &oneWeekDays)) { + return false; + } + + // Step 12.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; + } + auto [numWeeks, total] = rounded; + + // Step 20. + Duration resultDuration = {years, months, numWeeks}; + if (!ThrowIfInvalidDuration(cx, resultDuration)) { + return false; + } + + // Step 21. + *result = {resultDuration, total}; + return true; +} + +static bool RoundDurationDay(JSContext* cx, const Duration& duration, + Handle<temporal::NanosecondsAndDays> nanosAndDays, + Increment increment, + TemporalRoundingMode roundingMode, + ComputeRemainder computeRemainder, + RoundedDuration* result) { + double years = duration.years; + double months = duration.months; + double weeks = duration.weeks; + double days = duration.days; + + // Steps 13.a-b. + RoundedNumber rounded; + if (!RoundNumberToIncrement(cx, days, nanosAndDays, increment, roundingMode, + computeRemainder, &rounded)) { + return false; + } + auto [numDays, total] = rounded; + + // Step 20. + Duration resultDuration = {years, months, weeks, numDays}; + if (!ThrowIfInvalidDuration(cx, resultDuration)) { + return false; + } + + // Step 21. + *result = {resultDuration, total}; + return true; +} + +/** + * 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, + Handle<Wrapped<PlainDateObject*>> plainRelativeTo, + Handle<CalendarRecord> calendar, Handle<ZonedDateTime> zonedRelativeTo, + Handle<TimeZoneRecord> timeZone, + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, + 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(plainRelativeTo || zonedRelativeTo, + "Use RoundDuration without relativeTo when plainRelativeTo and " + "zonedRelativeTo are both undefined"); + + // 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 in our implementation.) + + // Step 6.a. (Not applicable in our implementation.) + MOZ_ASSERT_IF(unit <= TemporalUnit::Week, plainRelativeTo); + + // Step 6.b. + MOZ_ASSERT_IF( + unit <= TemporalUnit::Week, + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 6.c. + MOZ_ASSERT_IF( + unit <= TemporalUnit::Week, + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + + switch (unit) { + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + break; + case TemporalUnit::Day: + // We can't take the faster code path when |zonedRelativeTo| is present. + if (zonedRelativeTo) { + break; + } + [[fallthrough]]; + case TemporalUnit::Hour: + case TemporalUnit::Minute: + case TemporalUnit::Second: + case TemporalUnit::Millisecond: + case TemporalUnit::Microsecond: + case TemporalUnit::Nanosecond: + // Steps 7-9 and 13-21. + return ::RoundDuration(cx, duration, increment, unit, roundingMode, + computeRemainder, result); + case TemporalUnit::Auto: + MOZ_CRASH("Unexpected temporal unit"); + } + + // Step 7. + MOZ_ASSERT(TemporalUnit::Year <= unit && unit <= TemporalUnit::Day); + + // Steps 7.a-c. + Rooted<temporal::NanosecondsAndDays> nanosAndDays(cx); + if (zonedRelativeTo) { + // Step 7.b.i. (Reordered) + Rooted<ZonedDateTime> intermediate(cx); + if (!MoveRelativeZonedDateTime(cx, zonedRelativeTo, calendar, timeZone, + duration.date(), precalculatedPlainDateTime, + &intermediate)) { + return false; + } + + // Steps 7.a and 7.b.ii. + if (!NanosecondsToDays(cx, duration, intermediate, timeZone, + &nanosAndDays)) { + return false; + } + + // Step 7.b.iii. (Not applicable in our implementation.) + } else { + // Steps 7.a and 7.c. + if (!::NanosecondsToDays(cx, duration, &nanosAndDays)) { + return false; + } + } + + // 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 8. (Not applicable) + + // Step 9. + // FIXME: spec issue - `total` doesn't need be initialised. + + // Steps 10-21. + switch (unit) { + // Steps 10 and 20-21. + case TemporalUnit::Year: + return RoundDurationYear(cx, duration, nanosAndDays, increment, + roundingMode, plainRelativeTo, calendar, + computeRemainder, result); + + // Steps 11 and 20-21. + case TemporalUnit::Month: + return RoundDurationMonth(cx, duration, nanosAndDays, increment, + roundingMode, plainRelativeTo, calendar, + computeRemainder, result); + + // Steps 12 and 20-21. + case TemporalUnit::Week: + return RoundDurationWeek(cx, duration, nanosAndDays, increment, + roundingMode, plainRelativeTo, calendar, + computeRemainder, result); + + // Steps 13 and 20-21. + case TemporalUnit::Day: + return RoundDurationDay(cx, duration, nanosAndDays, increment, + roundingMode, computeRemainder, result); + + // Steps 14-19. (Handled elsewhere) + case TemporalUnit::Auto: + case TemporalUnit::Hour: + case TemporalUnit::Minute: + case TemporalUnit::Second: + case TemporalUnit::Millisecond: + case TemporalUnit::Microsecond: + case TemporalUnit::Nanosecond: + break; + } + + MOZ_CRASH("Unexpected temporal unit"); +} + +/** + * 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, + 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); + + RoundedDuration rounded; + if (!::RoundDuration(cx, duration, increment, unit, roundingMode, + plainRelativeTo, calendar, zonedRelativeTo, timeZone, + precalculatedPlainDateTime, ComputeRemainder::Yes, + &rounded)) { + return false; + } + + *result = rounded.total; + return true; +} + +/** + * 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, + Handle<Wrapped<PlainDateObject*>> plainRelativeTo, + Handle<CalendarRecord> calendar, Handle<ZonedDateTime> zonedRelativeTo, + Handle<TimeZoneRecord> timeZone, + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, + Duration* result) { + RoundedDuration rounded; + if (!::RoundDuration(cx, duration, increment, unit, roundingMode, + plainRelativeTo, calendar, zonedRelativeTo, timeZone, + precalculatedPlainDateTime, ComputeRemainder::No, + &rounded)) { + return false; + } + + *result = rounded.duration; + 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 }; + +/** + * AddDurationToOrSubtractDurationFromDuration ( operation, duration, other, + * options ) + */ +static bool AddDurationToOrSubtractDurationFromDuration( + JSContext* cx, DurationOperation operation, const CallArgs& args) { + auto* durationObj = &args.thisv().toObject().as<DurationObject>(); + auto duration = ToDuration(durationObj); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Duration other; + if (!ToTemporalDurationRecord(cx, args.get(0), &other)) { + return false; + } + + Rooted<Wrapped<PlainDateObject*>> plainRelativeTo(cx); + Rooted<ZonedDateTime> zonedRelativeTo(cx); + Rooted<TimeZoneRecord> timeZone(cx); + if (args.hasDefined(1)) { + const char* name = operation == DurationOperation::Add ? "add" : "subtract"; + + // Step 3. + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", name, args[1])); + if (!options) { + return false; + } + + // Steps 4-7. + if (!ToRelativeTemporalObject(cx, options, &plainRelativeTo, + &zonedRelativeTo, &timeZone)) { + return false; + } + MOZ_ASSERT(!plainRelativeTo || !zonedRelativeTo); + MOZ_ASSERT_IF(zonedRelativeTo, timeZone.receiver()); + } + + // Step 8. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecordFromRelativeTo(cx, plainRelativeTo, + zonedRelativeTo, + { + CalendarMethod::DateAdd, + CalendarMethod::DateUntil, + }, + &calendar)) { + return false; + } + + // Step 9. + if (operation == DurationOperation::Subtract) { + other = other.negate(); + } + + Duration result; + if (plainRelativeTo) { + if (!AddDuration(cx, duration, other, plainRelativeTo, calendar, &result)) { + return false; + } + } else if (zonedRelativeTo) { + if (!AddDuration(cx, duration, other, zonedRelativeTo, calendar, timeZone, + &result)) { + return false; + } + } else { + if (!AddDuration(cx, duration, other, &result)) { + return false; + } + } + + // Step 10. + auto* obj = CreateTemporalDuration(cx, result); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.Duration ( [ years [ , months [ , weeks [ , days [ , hours [ , + * minutes [ , seconds [ , milliseconds [ , microseconds [ , nanoseconds ] ] ] ] + * ] ] ] ] ] ] ) + */ +static bool DurationConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.Duration")) { + return false; + } + + // Step 2. + double years = 0; + if (args.hasDefined(0) && + !ToIntegerIfIntegral(cx, "years", args[0], &years)) { + return false; + } + + // Step 3. + double months = 0; + if (args.hasDefined(1) && + !ToIntegerIfIntegral(cx, "months", args[1], &months)) { + return false; + } + + // Step 4. + double weeks = 0; + if (args.hasDefined(2) && + !ToIntegerIfIntegral(cx, "weeks", args[2], &weeks)) { + return false; + } + + // Step 5. + double days = 0; + if (args.hasDefined(3) && !ToIntegerIfIntegral(cx, "days", args[3], &days)) { + return false; + } + + // Step 6. + double hours = 0; + if (args.hasDefined(4) && + !ToIntegerIfIntegral(cx, "hours", args[4], &hours)) { + return false; + } + + // Step 7. + double minutes = 0; + if (args.hasDefined(5) && + !ToIntegerIfIntegral(cx, "minutes", args[5], &minutes)) { + return false; + } + + // Step 8. + double seconds = 0; + if (args.hasDefined(6) && + !ToIntegerIfIntegral(cx, "seconds", args[6], &seconds)) { + return false; + } + + // Step 9. + double milliseconds = 0; + if (args.hasDefined(7) && + !ToIntegerIfIntegral(cx, "milliseconds", args[7], &milliseconds)) { + return false; + } + + // Step 10. + double microseconds = 0; + if (args.hasDefined(8) && + !ToIntegerIfIntegral(cx, "microseconds", args[8], µseconds)) { + return false; + } + + // Step 11. + double nanoseconds = 0; + if (args.hasDefined(9) && + !ToIntegerIfIntegral(cx, "nanoseconds", args[9], &nanoseconds)) { + return false; + } + + // Step 12. + auto* duration = CreateTemporalDuration( + cx, args, + {years, months, weeks, days, hours, minutes, seconds, milliseconds, + microseconds, nanoseconds}); + if (!duration) { + return false; + } + + args.rval().setObject(*duration); + return true; +} + +/** + * Temporal.Duration.from ( item ) + */ +static bool Duration_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + Handle<Value> item = args.get(0); + + // Step 1. + if (item.isObject()) { + if (auto* duration = item.toObject().maybeUnwrapIf<DurationObject>()) { + auto* result = CreateTemporalDuration(cx, ToDuration(duration)); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; + } + } + + // Step 2. + auto result = ToTemporalDuration(cx, item); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Duration.compare ( one, two [ , options ] ) + */ +static bool Duration_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Duration one; + if (!ToTemporalDuration(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + Duration two; + if (!ToTemporalDuration(cx, args.get(1), &two)) { + return false; + } + + Rooted<Wrapped<PlainDateObject*>> plainRelativeTo(cx); + Rooted<ZonedDateTime> zonedRelativeTo(cx); + Rooted<TimeZoneRecord> timeZone(cx); + if (args.hasDefined(2)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "compare", args[2])); + if (!options) { + return false; + } + + // Step 4. + if (one == two) { + args.rval().setInt32(0); + return true; + } + + // Steps 5-8. + 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. + auto hasCalendarUnit = [](const auto& d) { + return d.years != 0 || d.months != 0 || d.weeks != 0; + }; + bool calendarUnitsPresent = hasCalendarUnit(one) || hasCalendarUnit(two); + + // Step 11. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecordFromRelativeTo(cx, plainRelativeTo, + zonedRelativeTo, + { + CalendarMethod::DateAdd, + }, + &calendar)) { + return false; + } + + // Step 12. + if (zonedRelativeTo && + (calendarUnitsPresent || one.days != 0 || two.days != 0)) { + // Step 12.a. + auto instant = zonedRelativeTo.instant(); + + // Step 12.b. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, instant, &dateTime)) { + return false; + } + + // Step 12.c. + Instant after1; + if (!AddZonedDateTime(cx, instant, timeZone, calendar, one, dateTime, + &after1)) { + return false; + } + + // Step 12.d. + Instant after2; + if (!AddZonedDateTime(cx, instant, timeZone, calendar, two, dateTime, + &after2)) { + return false; + } + + // Steps 12.e-g. + args.rval().setInt32(after1 < after2 ? -1 : after1 > after2 ? 1 : 0); + return true; + } + + // Steps 13-14. + double 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)) { + return false; + } + } else { + if (!UnbalanceDateDurationRelative(cx, one, TemporalUnit::Day, + &unbalanceResult1)) { + return false; + } + MOZ_ASSERT(one.date() == unbalanceResult1.toDuration()); + } + + // Step 13.b. + DateDuration unbalanceResult2; + if (plainRelativeTo) { + if (!UnbalanceDateDurationRelative(cx, two, TemporalUnit::Day, + plainRelativeTo, calendar, + &unbalanceResult2)) { + return false; + } + } else { + if (!UnbalanceDateDurationRelative(cx, two, TemporalUnit::Day, + &unbalanceResult2)) { + return false; + } + MOZ_ASSERT(two.date() == unbalanceResult2.toDuration()); + } + + // Step 13.c. + days1 = unbalanceResult1.days; + + // Step 13.d. + days2 = unbalanceResult2.days; + } else { + // Step 14.a. + days1 = one.days; + + // Step 14.b. + days2 = 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; + } + } + + // Steps 15 and 17. + Rooted<BigInt*> ns1(cx, TotalDurationNanosecondsSlow(cx, oneTotal)); + if (!ns1) { + return false; + } + + // Steps 16 and 18. + auto* ns2 = TotalDurationNanosecondsSlow(cx, twoTotal); + if (!ns2) { + return false; + } + + // Step 19-21. + args.rval().setInt32(BigInt::compare(ns1, ns2)); + return true; +} + +/** + * get Temporal.Duration.prototype.years + */ +static bool Duration_years(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->years()); + return true; +} + +/** + * get Temporal.Duration.prototype.years + */ +static bool Duration_years(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_years>(cx, args); +} + +/** + * get Temporal.Duration.prototype.months + */ +static bool Duration_months(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->months()); + return true; +} + +/** + * get Temporal.Duration.prototype.months + */ +static bool Duration_months(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_months>(cx, args); +} + +/** + * get Temporal.Duration.prototype.weeks + */ +static bool Duration_weeks(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->weeks()); + return true; +} + +/** + * get Temporal.Duration.prototype.weeks + */ +static bool Duration_weeks(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_weeks>(cx, args); +} + +/** + * get Temporal.Duration.prototype.days + */ +static bool Duration_days(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->days()); + return true; +} + +/** + * get Temporal.Duration.prototype.days + */ +static bool Duration_days(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_days>(cx, args); +} + +/** + * get Temporal.Duration.prototype.hours + */ +static bool Duration_hours(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->hours()); + return true; +} + +/** + * get Temporal.Duration.prototype.hours + */ +static bool Duration_hours(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_hours>(cx, args); +} + +/** + * get Temporal.Duration.prototype.minutes + */ +static bool Duration_minutes(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->minutes()); + return true; +} + +/** + * get Temporal.Duration.prototype.minutes + */ +static bool Duration_minutes(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_minutes>(cx, args); +} + +/** + * get Temporal.Duration.prototype.seconds + */ +static bool Duration_seconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->seconds()); + return true; +} + +/** + * get Temporal.Duration.prototype.seconds + */ +static bool Duration_seconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_seconds>(cx, args); +} + +/** + * get Temporal.Duration.prototype.milliseconds + */ +static bool Duration_milliseconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->milliseconds()); + return true; +} + +/** + * get Temporal.Duration.prototype.milliseconds + */ +static bool Duration_milliseconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_milliseconds>(cx, args); +} + +/** + * get Temporal.Duration.prototype.microseconds + */ +static bool Duration_microseconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->microseconds()); + return true; +} + +/** + * get Temporal.Duration.prototype.microseconds + */ +static bool Duration_microseconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_microseconds>(cx, args); +} + +/** + * get Temporal.Duration.prototype.nanoseconds + */ +static bool Duration_nanoseconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + args.rval().setNumber(duration->nanoseconds()); + return true; +} + +/** + * get Temporal.Duration.prototype.nanoseconds + */ +static bool Duration_nanoseconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_nanoseconds>(cx, args); +} + +/** + * get Temporal.Duration.prototype.sign + */ +static bool Duration_sign(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* duration = &args.thisv().toObject().as<DurationObject>(); + int32_t sign = DurationSign(ToDuration(duration)); + args.rval().setInt32(sign); + return true; +} + +/** + * get Temporal.Duration.prototype.sign + */ +static bool Duration_sign(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_sign>(cx, args); +} + +/** + * 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)); + + // Steps 4-5. + args.rval().setBoolean(sign == 0); + return true; +} + +/** + * get Temporal.Duration.prototype.blank + */ +static bool Duration_blank(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_blank>(cx, args); +} + +/** + * Temporal.Duration.prototype.with ( temporalDurationLike ) + * + * 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); + + // Steps 3-23. + Rooted<JSObject*> temporalDurationLike( + cx, RequireObjectArg(cx, "temporalDurationLike", "with", args.get(0))); + if (!temporalDurationLike) { + return false; + } + if (!ToTemporalPartialDurationRecord(cx, temporalDurationLike, &duration)) { + return false; + } + + // Step 24. + auto* result = CreateTemporalDuration(cx, duration); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Duration.prototype.with ( temporalDurationLike ) + */ +static bool Duration_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_with>(cx, args); +} + +/** + * Temporal.Duration.prototype.negated ( ) + */ +static bool Duration_negated(JSContext* cx, const CallArgs& args) { + auto* durationObj = &args.thisv().toObject().as<DurationObject>(); + auto duration = ToDuration(durationObj); + + // Step 3. + auto* result = CreateTemporalDuration(cx, duration.negate()); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Duration.prototype.negated ( ) + */ +static bool Duration_negated(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_negated>(cx, args); +} + +/** + * Temporal.Duration.prototype.abs ( ) + */ +static bool Duration_abs(JSContext* cx, const CallArgs& args) { + auto* durationObj = &args.thisv().toObject().as<DurationObject>(); + auto duration = ToDuration(durationObj); + + // Step 3. + auto* result = CreateTemporalDuration(cx, AbsoluteDuration(duration)); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Duration.prototype.abs ( ) + */ +static bool Duration_abs(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_abs>(cx, args); +} + +/** + * Temporal.Duration.prototype.add ( other [ , options ] ) + */ +static bool Duration_add(JSContext* cx, const CallArgs& args) { + return AddDurationToOrSubtractDurationFromDuration(cx, DurationOperation::Add, + args); +} + +/** + * Temporal.Duration.prototype.add ( other [ , options ] ) + */ +static bool Duration_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_add>(cx, args); +} + +/** + * Temporal.Duration.prototype.subtract ( other [ , options ] ) + */ +static bool Duration_subtract(JSContext* cx, const CallArgs& args) { + return AddDurationToOrSubtractDurationFromDuration( + cx, DurationOperation::Subtract, args); +} + +/** + * Temporal.Duration.prototype.subtract ( other [ , options ] ) + */ +static bool Duration_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_subtract>(cx, args); +} + +/** + * 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); + + // Step 18. (Reordered) + auto existingLargestUnit = DefaultTemporalLargestUnit(duration); + + // Steps 3-25. + auto smallestUnit = TemporalUnit::Auto; + TemporalUnit largestUnit; + auto roundingMode = TemporalRoundingMode::HalfExpand; + auto roundingIncrement = Increment{1}; + Rooted<JSObject*> relativeTo(cx); + Rooted<Wrapped<PlainDateObject*>> plainRelativeTo(cx); + Rooted<ZonedDateTime> zonedRelativeTo(cx); + Rooted<TimeZoneRecord> timeZone(cx); + if (args.get(0).isString()) { + // Step 4. (Not applicable in our implementation.) + + // Steps 6-15. (Not applicable) + + // Step 16. + Rooted<JSString*> paramString(cx, args[0].toString()); + if (!GetTemporalUnit(cx, paramString, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::DateTime, &smallestUnit)) { + return false; + } + + // Step 17. (Not applicable) + + // Step 18. (Moved above) + + // Step 19. + auto defaultLargestUnit = std::min(existingLargestUnit, smallestUnit); + + // Step 20. (Not applicable) + + // Step 20.a. (Not applicable) + + // Step 20.b. + largestUnit = defaultLargestUnit; + + // Steps 21-25. (Not applicable) + } else { + // Steps 3 and 5. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "roundTo", "round", args.get(0))); + if (!options) { + return false; + } + + // Step 6. + bool smallestUnitPresent = true; + + // Step 7. + bool largestUnitPresent = true; + + // Steps 8-9. + // + // Inlined GetTemporalUnit and GetOption so we can more easily detect an + // absent "largestUnit" value. + Rooted<Value> largestUnitValue(cx); + if (!GetProperty(cx, options, options, cx->names().largestUnit, + &largestUnitValue)) { + return false; + } + + if (!largestUnitValue.isUndefined()) { + Rooted<JSString*> largestUnitStr(cx, JS::ToString(cx, largestUnitValue)); + if (!largestUnitStr) { + return false; + } + + largestUnit = TemporalUnit::Auto; + if (!GetTemporalUnit(cx, largestUnitStr, TemporalUnitKey::LargestUnit, + TemporalUnitGroup::DateTime, &largestUnit)) { + return false; + } + } + + // Steps 10-13. + if (!ToRelativeTemporalObject(cx, options, &plainRelativeTo, + &zonedRelativeTo, &timeZone)) { + return false; + } + MOZ_ASSERT(!plainRelativeTo || !zonedRelativeTo); + MOZ_ASSERT_IF(zonedRelativeTo, timeZone.receiver()); + + // Step 14. + if (!ToTemporalRoundingIncrement(cx, options, &roundingIncrement)) { + return false; + } + + // Step 15. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 16. + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::DateTime, &smallestUnit)) { + return false; + } + + // Step 17. + if (smallestUnit == TemporalUnit::Auto) { + // Step 17.a. + smallestUnitPresent = false; + + // Step 17.b. + smallestUnit = TemporalUnit::Nanosecond; + } + + // Step 18. (Moved above) + + // Step 19. + auto defaultLargestUnit = std::min(existingLargestUnit, smallestUnit); + + // Steps 20-21. + if (largestUnitValue.isUndefined()) { + // Step 20.a. + largestUnitPresent = false; + + // Step 20.b. + largestUnit = defaultLargestUnit; + } else if (largestUnit == TemporalUnit::Auto) { + // Step 21.a + largestUnit = defaultLargestUnit; + } + + // Step 22. + if (!smallestUnitPresent && !largestUnitPresent) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DURATION_MISSING_UNIT_SPECIFIER); + return false; + } + + // Step 23. + if (largestUnit > smallestUnit) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_RANGE); + return false; + } + + // Steps 24-25. + if (smallestUnit > TemporalUnit::Day) { + // Step 24. + auto maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit); + + // Step 25. + if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, + false)) { + return false; + } + } + } + + // Step 26. + bool hoursToDaysConversionMayOccur = false; + + // Step 27. + if (duration.days != 0 && zonedRelativeTo) { + hoursToDaysConversionMayOccur = true; + } + + // Step 28. + else if (std::abs(duration.hours) >= 24) { + hoursToDaysConversionMayOccur = true; + } + + // Step 29. + bool roundingGranularityIsNoop = smallestUnit == TemporalUnit::Nanosecond && + roundingIncrement == Increment{1}; + + // Step 30. + bool calendarUnitsPresent = + duration.years != 0 || duration.months != 0 || duration.weeks != 0; + + // Step 31. + if (roundingGranularityIsNoop && largestUnit == existingLargestUnit && + !calendarUnitsPresent && !hoursToDaysConversionMayOccur && + std::abs(duration.minutes) < 60 && std::abs(duration.seconds) < 60 && + std::abs(duration.milliseconds) < 1000 && + std::abs(duration.microseconds) < 1000 && + std::abs(duration.nanoseconds) < 1000) { + // Steps 31.a-b. + auto* obj = CreateTemporalDuration(cx, duration); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 32. + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime{}; + + // Step 33. + bool plainDateTimeOrRelativeToWillBeUsed = + !roundingGranularityIsNoop || largestUnit <= TemporalUnit::Day || + calendarUnitsPresent || duration.days != 0; + + // Step 34. + PlainDateTime relativeToDateTime; + if (zonedRelativeTo && plainDateTimeOrRelativeToWillBeUsed) { + // Steps 34.a-b. + auto instant = zonedRelativeTo.instant(); + + // Step 34.c. + if (!GetPlainDateTimeFor(cx, timeZone, instant, &relativeToDateTime)) { + return false; + } + precalculatedPlainDateTime = + mozilla::SomeRef<const PlainDateTime>(relativeToDateTime); + + // Step 34.d. + plainRelativeTo = CreateTemporalDate(cx, relativeToDateTime.date, + zonedRelativeTo.calendar()); + if (!plainRelativeTo) { + return false; + } + } + + // Step 35. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecordFromRelativeTo(cx, plainRelativeTo, + zonedRelativeTo, + { + CalendarMethod::DateAdd, + CalendarMethod::DateUntil, + }, + &calendar)) { + return false; + } + + // Step 36. + DateDuration unbalanceResult; + if (plainRelativeTo) { + if (!UnbalanceDateDurationRelative(cx, duration, largestUnit, + plainRelativeTo, calendar, + &unbalanceResult)) { + return false; + } + } else { + if (!UnbalanceDateDurationRelative(cx, duration, largestUnit, + &unbalanceResult)) { + return false; + } + MOZ_ASSERT(duration.date() == unbalanceResult.toDuration()); + } + + // 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; + if (plainRelativeTo || zonedRelativeTo) { + if (!::RoundDuration(cx, roundInput, roundingIncrement, smallestUnit, + roundingMode, plainRelativeTo, calendar, + zonedRelativeTo, timeZone, precalculatedPlainDateTime, + &roundResult)) { + return false; + } + } else { + if (!::RoundDuration(cx, roundInput, roundingIncrement, smallestUnit, + roundingMode, &roundResult)) { + return false; + } + } + + // Steps 39-40. + TimeDuration balanceResult; + if (zonedRelativeTo) { + // Step 39.a. + Duration adjustResult; + if (!AdjustRoundedDurationDays(cx, roundResult, roundingIncrement, + smallestUnit, roundingMode, zonedRelativeTo, + calendar, timeZone, + precalculatedPlainDateTime, &adjustResult)) { + return false; + } + roundResult = adjustResult; + + // Step 39.b. + if (!BalanceTimeDurationRelative( + cx, roundResult, largestUnit, zonedRelativeTo, timeZone, + precalculatedPlainDateTime, &balanceResult)) { + return false; + } + } else { + // Step 40.a. + if (!BalanceTimeDuration(cx, roundResult, largestUnit, &balanceResult)) { + return false; + } + } + + // Step 41. + Duration balanceInput = { + roundResult.years, + roundResult.months, + roundResult.weeks, + balanceResult.days, + }; + DateDuration result; + if (!::BalanceDateDurationRelative(cx, balanceInput, largestUnit, + smallestUnit, plainRelativeTo, calendar, + &result)) { + 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, + }); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.Duration.prototype.round ( options ) + */ +static bool Duration_round(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_round>(cx, args); +} + +/** + * Temporal.Duration.prototype.total ( totalOf ) + */ +static bool Duration_total(JSContext* cx, const CallArgs& args) { + auto* durationObj = &args.thisv().toObject().as<DurationObject>(); + auto duration = ToDuration(durationObj); + + // Steps 3-11. + Rooted<JSObject*> relativeTo(cx); + Rooted<Wrapped<PlainDateObject*>> plainRelativeTo(cx); + Rooted<ZonedDateTime> zonedRelativeTo(cx); + Rooted<TimeZoneRecord> timeZone(cx); + auto unit = TemporalUnit::Auto; + if (args.get(0).isString()) { + // Step 4. (Not applicable in our implementation.) + + // Steps 6-10. (Implicit) + MOZ_ASSERT(!plainRelativeTo && !zonedRelativeTo); + + // Step 11. + Rooted<JSString*> paramString(cx, args[0].toString()); + if (!GetTemporalUnit(cx, paramString, TemporalUnitKey::Unit, + TemporalUnitGroup::DateTime, &unit)) { + return false; + } + } else { + // Steps 3 and 5. + Rooted<JSObject*> totalOf( + cx, RequireObjectArg(cx, "totalOf", "total", args.get(0))); + if (!totalOf) { + return false; + } + + // Steps 6-10. + if (!ToRelativeTemporalObject(cx, totalOf, &plainRelativeTo, + &zonedRelativeTo, &timeZone)) { + return false; + } + MOZ_ASSERT(!plainRelativeTo || !zonedRelativeTo); + MOZ_ASSERT_IF(zonedRelativeTo, timeZone.receiver()); + + // Step 11. + if (!GetTemporalUnit(cx, totalOf, TemporalUnitKey::Unit, + TemporalUnitGroup::DateTime, &unit)) { + return false; + } + + if (unit == TemporalUnit::Auto) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_OPTION, "unit"); + return false; + } + } + + // Step 12. + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime{}; + + // Step 13. + bool plainDateTimeOrRelativeToWillBeUsed = + unit <= TemporalUnit::Day || duration.years != 0 || + duration.months != 0 || duration.weeks != 0 || duration.days != 0; + + // Step 14. + PlainDateTime relativeToDateTime; + if (zonedRelativeTo && plainDateTimeOrRelativeToWillBeUsed) { + // Steps 14.a-b. + auto instant = zonedRelativeTo.instant(); + + // Step 14.c. + if (!GetPlainDateTimeFor(cx, timeZone, instant, &relativeToDateTime)) { + return false; + } + precalculatedPlainDateTime = + mozilla::SomeRef<const PlainDateTime>(relativeToDateTime); + + // Step 14.d + plainRelativeTo = CreateTemporalDate(cx, relativeToDateTime.date, + zonedRelativeTo.calendar()); + if (!plainRelativeTo) { + return false; + } + } + + // Step 15. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecordFromRelativeTo(cx, plainRelativeTo, + zonedRelativeTo, + { + CalendarMethod::DateAdd, + CalendarMethod::DateUntil, + }, + &calendar)) { + return false; + } + + // Step 16. + DateDuration unbalanceResult; + if (plainRelativeTo) { + if (!UnbalanceDateDurationRelative(cx, duration, unit, plainRelativeTo, + calendar, &unbalanceResult)) { + return false; + } + } else { + if (!UnbalanceDateDurationRelative(cx, duration, unit, &unbalanceResult)) { + return false; + } + MOZ_ASSERT(duration.date() == unbalanceResult.toDuration()); + } + + Duration balanceInput = { + 0, + 0, + 0, + unbalanceResult.days, + duration.hours, + duration.minutes, + duration.seconds, + duration.milliseconds, + duration.microseconds, + duration.nanoseconds, + }; + + // Steps 17-18. + TimeDuration balanceResult; + if (zonedRelativeTo) { + // Step 17.a + Rooted<ZonedDateTime> intermediate(cx); + if (!MoveRelativeZonedDateTime( + cx, zonedRelativeTo, calendar, timeZone, + {unbalanceResult.years, unbalanceResult.months, + unbalanceResult.weeks, 0}, + precalculatedPlainDateTime, &intermediate)) { + return false; + } + + // Step 17.b. + if (!BalancePossiblyInfiniteTimeDurationRelative( + cx, balanceInput, unit, intermediate, timeZone, &balanceResult)) { + return false; + } + } else { + // Step 18. + if (!BalancePossiblyInfiniteTimeDuration(cx, balanceInput, unit, + &balanceResult)) { + 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; + } + } + MOZ_ASSERT(IsValidDuration(balanceResult.toDuration())); + + // 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, + }; + double total; + if (plainRelativeTo || zonedRelativeTo) { + if (!::RoundDuration(cx, roundInput, Increment{1}, unit, + TemporalRoundingMode::Trunc, plainRelativeTo, calendar, + zonedRelativeTo, timeZone, precalculatedPlainDateTime, + &total)) { + return false; + } + } else { + if (!::RoundDuration(cx, roundInput, Increment{1}, unit, + TemporalRoundingMode::Trunc, &total)) { + return false; + } + } + + // Step 23. + args.rval().setNumber(total); + return true; +} + +/** + * Temporal.Duration.prototype.total ( totalOf ) + */ +static bool Duration_total(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_total>(cx, args); +} + +/** + * Temporal.Duration.prototype.toString ( [ options ] ) + */ +static bool Duration_toString(JSContext* cx, const CallArgs& args) { + auto duration = ToDuration(&args.thisv().toObject().as<DurationObject>()); + + // Steps 3-9. + SecondsStringPrecision precision = {Precision::Auto(), + TemporalUnit::Nanosecond, Increment{1}}; + auto roundingMode = TemporalRoundingMode::Trunc; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Steps 4-5. + auto digits = Precision::Auto(); + if (!ToFractionalSecondDigits(cx, options, &digits)) { + return false; + } + + // Step 6. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 7. + auto smallestUnit = TemporalUnit::Auto; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + // Step 8. + if (smallestUnit == TemporalUnit::Hour || + smallestUnit == TemporalUnit::Minute) { + const char* smallestUnitStr = + smallestUnit == TemporalUnit::Hour ? "hour" : "minute"; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_OPTION, + smallestUnitStr, "smallestUnit"); + return false; + } + + // Step 9. + precision = ToSecondsStringPrecision(smallestUnit, digits); + } + + // Steps 10-11. + Duration result; + if (precision.unit != TemporalUnit::Nanosecond || + precision.increment != Increment{1}) { + // Step 10.a. + auto largestUnit = DefaultTemporalLargestUnit(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.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)) { + return false; + } + + // Step 10.e. + result = { + duration.years, + duration.months, + duration.weeks, + balanceResult.days, + balanceResult.hours, + balanceResult.minutes, + balanceResult.seconds, + balanceResult.milliseconds, + balanceResult.microseconds, + balanceResult.nanoseconds, + }; + } else { + // Step 11. + result = duration; + } + + // Step 12. + JSString* str = TemporalDurationToString(cx, result, precision.precision); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.Duration.prototype.toString ( [ options ] ) + */ +static bool Duration_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_toString>(cx, args); +} + +/** + * Temporal.Duration.prototype.toJSON ( ) + */ +static bool Duration_toJSON(JSContext* cx, const CallArgs& args) { + auto* duration = &args.thisv().toObject().as<DurationObject>(); + + // Step 3. + JSString* str = + TemporalDurationToString(cx, ToDuration(duration), Precision::Auto()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.Duration.prototype.toJSON ( ) + */ +static bool Duration_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_toJSON>(cx, args); +} + +/** + * Temporal.Duration.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool Duration_toLocaleString(JSContext* cx, const CallArgs& args) { + auto* duration = &args.thisv().toObject().as<DurationObject>(); + + // Step 3. + JSString* str = + TemporalDurationToString(cx, ToDuration(duration), Precision::Auto()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.Duration.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool Duration_toLocaleString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsDuration, Duration_toLocaleString>(cx, args); +} + +/** + * Temporal.Duration.prototype.valueOf ( ) + */ +static bool Duration_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "Duration", "primitive type"); + return false; +} + +const JSClass DurationObject::class_ = { + "Temporal.Duration", + JSCLASS_HAS_RESERVED_SLOTS(DurationObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Duration), + JS_NULL_CLASS_OPS, + &DurationObject::classSpec_, +}; + +const JSClass& DurationObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec Duration_methods[] = { + JS_FN("from", Duration_from, 1, 0), + JS_FN("compare", Duration_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec Duration_prototype_methods[] = { + JS_FN("with", Duration_with, 1, 0), + JS_FN("negated", Duration_negated, 0, 0), + JS_FN("abs", Duration_abs, 0, 0), + JS_FN("add", Duration_add, 1, 0), + JS_FN("subtract", Duration_subtract, 1, 0), + JS_FN("round", Duration_round, 1, 0), + JS_FN("total", Duration_total, 1, 0), + JS_FN("toString", Duration_toString, 0, 0), + JS_FN("toJSON", Duration_toJSON, 0, 0), + JS_FN("toLocaleString", Duration_toLocaleString, 0, 0), + JS_FN("valueOf", Duration_valueOf, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec Duration_prototype_properties[] = { + JS_PSG("years", Duration_years, 0), + JS_PSG("months", Duration_months, 0), + JS_PSG("weeks", Duration_weeks, 0), + JS_PSG("days", Duration_days, 0), + JS_PSG("hours", Duration_hours, 0), + JS_PSG("minutes", Duration_minutes, 0), + JS_PSG("seconds", Duration_seconds, 0), + JS_PSG("milliseconds", Duration_milliseconds, 0), + JS_PSG("microseconds", Duration_microseconds, 0), + JS_PSG("nanoseconds", Duration_nanoseconds, 0), + JS_PSG("sign", Duration_sign, 0), + JS_PSG("blank", Duration_blank, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.Duration", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec DurationObject::classSpec_ = { + GenericCreateConstructor<DurationConstructor, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<DurationObject>, + Duration_methods, + nullptr, + Duration_prototype_methods, + Duration_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/Duration.h b/js/src/builtin/temporal/Duration.h new file mode 100644 index 0000000000..47708458f4 --- /dev/null +++ b/js/src/builtin/temporal/Duration.h @@ -0,0 +1,210 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_Duration_h +#define builtin_temporal_Duration_h + +#include <stdint.h> + +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/Wrapped.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +namespace js { +struct ClassSpec; +} + +namespace js::temporal { + +class DurationObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t YEARS_SLOT = 0; + static constexpr uint32_t MONTHS_SLOT = 1; + static constexpr uint32_t WEEKS_SLOT = 2; + static constexpr uint32_t DAYS_SLOT = 3; + static constexpr uint32_t HOURS_SLOT = 4; + static constexpr uint32_t MINUTES_SLOT = 5; + static constexpr uint32_t SECONDS_SLOT = 6; + static constexpr uint32_t MILLISECONDS_SLOT = 7; + static constexpr uint32_t MICROSECONDS_SLOT = 8; + static constexpr uint32_t NANOSECONDS_SLOT = 9; + static constexpr uint32_t SLOT_COUNT = 10; + + double years() const { return getFixedSlot(YEARS_SLOT).toNumber(); } + double months() const { return getFixedSlot(MONTHS_SLOT).toNumber(); } + double weeks() const { return getFixedSlot(WEEKS_SLOT).toNumber(); } + double days() const { return getFixedSlot(DAYS_SLOT).toNumber(); } + double hours() const { return getFixedSlot(HOURS_SLOT).toNumber(); } + double minutes() const { return getFixedSlot(MINUTES_SLOT).toNumber(); } + double seconds() const { return getFixedSlot(SECONDS_SLOT).toNumber(); } + double milliseconds() const { + return getFixedSlot(MILLISECONDS_SLOT).toNumber(); + } + double microseconds() const { + return getFixedSlot(MICROSECONDS_SLOT).toNumber(); + } + double nanoseconds() const { + return getFixedSlot(NANOSECONDS_SLOT).toNumber(); + } + + private: + static const ClassSpec classSpec_; +}; + +/** + * Extract the duration fields from the Duration object. + */ +inline Duration ToDuration(const DurationObject* duration) { + return { + duration->years(), duration->months(), + duration->weeks(), duration->days(), + duration->hours(), duration->minutes(), + duration->seconds(), duration->milliseconds(), + duration->microseconds(), duration->nanoseconds(), + }; +} + +class Increment; +class CalendarRecord; +class PlainDateObject; +class TimeZoneRecord; +class ZonedDateTime; +class ZonedDateTimeObject; +enum class TemporalRoundingMode; +enum class TemporalUnit; + +/** + * DurationSign ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +int32_t DurationSign(const Duration& duration); + +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool IsValidDuration(const Duration& duration); + +/** + * IsValidDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +bool ThrowIfInvalidDuration(JSContext* cx, const Duration& duration); + +/** + * CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds [ , newTarget ] ) + */ +DurationObject* CreateTemporalDuration(JSContext* cx, const Duration& duration); + +/** + * ToTemporalDuration ( item ) + */ +Wrapped<DurationObject*> ToTemporalDuration(JSContext* cx, + JS::Handle<JS::Value> item); + +/** + * ToTemporalDuration ( item ) + */ +bool ToTemporalDuration(JSContext* cx, JS::Handle<JS::Value> item, + Duration* result); + +/** + * ToTemporalDurationRecord ( temporalDurationLike ) + */ +bool ToTemporalDurationRecord(JSContext* cx, + JS::Handle<JS::Value> temporalDurationLike, + Duration* result); + +/** + * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds, largestUnit ) + */ +bool BalanceTimeDuration(JSContext* cx, const Duration& duration, + TemporalUnit largestUnit, TimeDuration* result); + +/** + * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds, largestUnit ) + */ +bool BalanceTimeDuration(JSContext* cx, const InstantSpan& nanoseconds, + TemporalUnit largestUnit, TimeDuration* result); + +/** + * BalanceDateDurationRelative ( years, months, weeks, days, largestUnit, + * smallestUnit, plainRelativeTo, calendarRec ) + */ +bool BalanceDateDurationRelative( + JSContext* cx, const Duration& duration, TemporalUnit largestUnit, + TemporalUnit smallestUnit, + JS::Handle<Wrapped<PlainDateObject*>> plainRelativeTo, + JS::Handle<CalendarRecord> calendar, DateDuration* result); + +/** + * AdjustRoundedDurationDays ( years, months, weeks, days, hours, minutes, + * seconds, milliseconds, microseconds, nanoseconds, increment, unit, + * roundingMode, zonedRelativeTo, calendarRec, timeZoneRec, + * precalculatedPlainDateTime ) + */ +bool AdjustRoundedDurationDays(JSContext* cx, const Duration& duration, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + JS::Handle<ZonedDateTime> relativeTo, + JS::Handle<CalendarRecord> calendar, + JS::Handle<TimeZoneRecord> timeZone, + const PlainDateTime& precalculatedPlainDateTime, + Duration* result); + +/** + * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , + * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , + * precalculatedPlainDateTime ] ] ] ] ] ) + */ +bool RoundDuration(JSContext* cx, const Duration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + Duration* result); + +/** + * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , + * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , + * precalculatedPlainDateTime ] ] ] ] ] ) + */ +bool RoundDuration(JSContext* cx, const Duration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + JS::Handle<Wrapped<PlainDateObject*>> plainRelativeTo, + JS::Handle<CalendarRecord> calendar, Duration* result); + +/** + * RoundDuration ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , + * plainRelativeTo [ , calendarRec [ , zonedRelativeTo [ , timeZoneRec [ , + * precalculatedPlainDateTime ] ] ] ] ] ) + */ +bool RoundDuration(JSContext* cx, const Duration& duration, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + JS::Handle<PlainDateObject*> plainRelativeTo, + JS::Handle<CalendarRecord> calendar, + JS::Handle<ZonedDateTime> zonedRelativeTo, + JS::Handle<TimeZoneRecord> timeZone, + const PlainDateTime& precalculatedPlainDateTime, + Duration* result); + +/** + * DaysUntil ( earlier, later ) + */ +int32_t DaysUntil(const PlainDate& earlier, const PlainDate& later); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_Duration_h */ diff --git a/js/src/builtin/temporal/Instant.cpp b/js/src/builtin/temporal/Instant.cpp new file mode 100644 index 0000000000..78fc15f313 --- /dev/null +++ b/js/src/builtin/temporal/Instant.cpp @@ -0,0 +1,1792 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/Instant.h" + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Maybe.h" +#include "mozilla/Span.h" + +#include <algorithm> +#include <array> +#include <cstdlib> +#include <iterator> +#include <stddef.h> +#include <stdint.h> +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/Int96.h" +#include "builtin/temporal/PlainDateTime.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/ToString.h" +#include "builtin/temporal/Wrapped.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.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/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/BigIntType.h" +#include "vm/BytecodeUtil.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.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 IsInstant(Handle<Value> v) { + return v.isObject() && v.toObject().is<InstantObject>(); +} + +/** + * Check if the absolute value is less-or-equal to the given limit. + */ +template <const auto& digits> +static bool AbsoluteValueIsLessOrEqual(const BigInt* bigInt) { + size_t length = bigInt->digitLength(); + + // Fewer digits than the limit, so definitely in range. + if (length < std::size(digits)) { + return true; + } + + // More digits than the limit, so definitely out of range. + if (length > std::size(digits)) { + return false; + } + + // Compare each digit when the input has the same number of digits. + size_t index = std::size(digits); + for (auto digit : digits) { + auto d = bigInt->digit(--index); + if (d < digit) { + return true; + } + if (d > digit) { + return false; + } + } + return true; +} + +static constexpr auto NanosecondsMaxInstant() { + static_assert(BigInt::DigitBits == 64 || BigInt::DigitBits == 32); + + // ยฑ8.64 ร 10^21 is the nanoseconds from epoch limit. + // 8.64 ร 10^21 is 86_40000_00000_00000_00000 or 0x1d4_60162f51_6f000000. + // Return the BigInt digits of that number for fast BigInt comparisons. + if constexpr (BigInt::DigitBits == 64) { + return std::array{ + BigInt::Digit(0x1d4), + BigInt::Digit(0x6016'2f51'6f00'0000), + }; + } else { + return std::array{ + BigInt::Digit(0x1d4), + BigInt::Digit(0x6016'2f51), + BigInt::Digit(0x6f00'0000), + }; + } +} + +/** + * IsValidEpochNanoseconds ( epochNanoseconds ) + */ +bool js::temporal::IsValidEpochNanoseconds(const BigInt* epochNanoseconds) { + // Steps 1-3. + static constexpr auto epochLimit = NanosecondsMaxInstant(); + return AbsoluteValueIsLessOrEqual<epochLimit>(epochNanoseconds); +} + +static bool IsValidEpochMicroseconds(const BigInt* epochMicroseconds) { + int64_t i; + if (!BigInt::isInt64(epochMicroseconds, &i)) { + return false; + } + + constexpr int64_t MicrosecondsMaxInstant = Instant::max().toMicroseconds(); + return -MicrosecondsMaxInstant <= i && i <= MicrosecondsMaxInstant; +} + +static bool IsValidEpochMilliseconds(double epochMilliseconds) { + MOZ_ASSERT(IsInteger(epochMilliseconds)); + + constexpr int64_t MillisecondsMaxInstant = Instant::max().toMilliseconds(); + return std::abs(epochMilliseconds) <= double(MillisecondsMaxInstant); +} + +static bool IsValidEpochSeconds(double epochSeconds) { + MOZ_ASSERT(IsInteger(epochSeconds)); + + constexpr int64_t SecondsMaxInstant = Instant::max().toSeconds(); + return std::abs(epochSeconds) <= double(SecondsMaxInstant); +} + +/** + * IsValidEpochNanoseconds ( epochNanoseconds ) + */ +bool js::temporal::IsValidEpochInstant(const Instant& instant) { + MOZ_ASSERT(0 <= instant.nanoseconds && instant.nanoseconds <= 999'999'999); + + // Steps 1-3. + return Instant::min() <= instant && instant <= Instant::max(); +} + +static constexpr auto NanosecondsMaxInstantSpan() { + static_assert(BigInt::DigitBits == 64 || BigInt::DigitBits == 32); + + // ยฑ8.64 ร 10^21 is the nanoseconds from epoch limit. + // 2 ร 8.64 ร 10^21 is 172_80000_00000_00000_00000 or 0x3a8_c02c5ea2_de000000. + // Return the BigInt digits of that number for fast BigInt comparisons. + if constexpr (BigInt::DigitBits == 64) { + return std::array{ + BigInt::Digit(0x3a8), + BigInt::Digit(0xc02c'5ea2'de00'0000), + }; + } else { + return std::array{ + BigInt::Digit(0x3a8), + BigInt::Digit(0xc02c'5ea2), + BigInt::Digit(0xde00'0000), + }; + } +} + +/** + * Validates a nanoseconds amount is at most as large as the difference + * between two valid nanoseconds from the epoch instants. + * + * Useful when we want to ensure a BigInt doesn't exceed a certain limit. + */ +bool js::temporal::IsValidInstantSpan(const BigInt* nanoseconds) { + static constexpr auto spanLimit = NanosecondsMaxInstantSpan(); + return AbsoluteValueIsLessOrEqual<spanLimit>(nanoseconds); +} + +bool js::temporal::IsValidInstantSpan(const InstantSpan& span) { + MOZ_ASSERT(0 <= span.nanoseconds && span.nanoseconds <= 999'999'999); + + // Steps 1-3. + return InstantSpan::min() <= span && span <= InstantSpan::max(); +} + +/** + * Return the BigInt as a 96-bit integer. The BigInt digits must not consist of + * more than 96-bits. + */ +static Int96 ToInt96(const BigInt* ns) { + static_assert(BigInt::DigitBits == 64 || BigInt::DigitBits == 32); + + auto digits = ns->digits(); + if constexpr (BigInt::DigitBits == 64) { + BigInt::Digit x = 0, y = 0; + switch (digits.size()) { + case 2: + y = digits[1]; + [[fallthrough]]; + case 1: + x = digits[0]; + [[fallthrough]]; + case 0: + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected digit length"); + } + return Int96{ + Int96::Digits{Int96::Digit(x), Int96::Digit(x >> 32), Int96::Digit(y)}, + ns->isNegative()}; + } else { + BigInt::Digit x = 0, y = 0, z = 0; + switch (digits.size()) { + case 3: + z = digits[2]; + [[fallthrough]]; + case 2: + y = digits[1]; + [[fallthrough]]; + case 1: + x = digits[0]; + [[fallthrough]]; + case 0: + break; + default: + MOZ_ASSERT_UNREACHABLE("unexpected digit length"); + } + return Int96{ + Int96::Digits{Int96::Digit(x), Int96::Digit(y), Int96::Digit(z)}, + ns->isNegative()}; + } +} + +Instant js::temporal::ToInstant(const BigInt* epochNanoseconds) { + MOZ_ASSERT(IsValidEpochNanoseconds(epochNanoseconds)); + + auto [seconds, nanos] = + ToInt96(epochNanoseconds) / ToNanoseconds(TemporalUnit::Second); + return {seconds, nanos}; +} + +InstantSpan js::temporal::ToInstantSpan(const BigInt* nanoseconds) { + MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); + + auto [seconds, nanos] = + ToInt96(nanoseconds) / ToNanoseconds(TemporalUnit::Second); + return {seconds, nanos}; +} + +static BigInt* CreateBigInt(JSContext* cx, + const std::array<uint32_t, 3>& digits, + bool negative) { + static_assert(BigInt::DigitBits == 64 || BigInt::DigitBits == 32); + + if constexpr (BigInt::DigitBits == 64) { + uint64_t x = (uint64_t(digits[1]) << 32) | digits[0]; + uint64_t y = digits[2]; + + size_t length = y ? 2 : x ? 1 : 0; + auto* result = BigInt::createUninitialized(cx, length, negative); + if (!result) { + return nullptr; + } + if (y) { + result->setDigit(1, y); + } + if (x) { + result->setDigit(0, x); + } + return result; + } else { + size_t length = digits[2] ? 3 : digits[1] ? 2 : digits[0] ? 1 : 0; + auto* result = BigInt::createUninitialized(cx, length, negative); + if (!result) { + return nullptr; + } + while (length--) { + result->setDigit(length, digits[length]); + } + return result; + } +} + +static BigInt* ToEpochBigInt(JSContext* cx, const InstantSpan& instant) { + MOZ_ASSERT(IsValidInstantSpan(instant)); + + // Multiplies two uint32_t values and returns the lower 32-bits. The higher + // 32-bits are stored in |high|. + auto digitMul = [](uint32_t a, uint32_t b, uint32_t* high) { + uint64_t result = static_cast<uint64_t>(a) * static_cast<uint64_t>(b); + *high = result >> 32; + return static_cast<uint32_t>(result); + }; + + // Adds two uint32_t values and returns the result. Overflow is added to the + // out-param |carry|. + auto digitAdd = [](uint32_t a, uint32_t b, uint32_t* carry) { + uint32_t result = a + b; + *carry += static_cast<uint32_t>(result < a); + return result; + }; + + constexpr uint32_t secToNanos = ToNanoseconds(TemporalUnit::Second); + + uint64_t seconds = std::abs(instant.seconds); + uint32_t nanoseconds = instant.nanoseconds; + + // 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 from the epoch value |-1n| is represented as + // the instant {seconds: -1, nanoseconds: 999'999'999}. + if (instant.seconds < 0 && nanoseconds != 0) { + nanoseconds = secToNanos - nanoseconds; + seconds -= 1; + } + + // uint32_t digits stored in the same order as BigInt digits, i.e. the least + // significant digit is stored at index zero. + std::array<uint32_t, 2> multiplicand = {uint32_t(seconds), + uint32_t(seconds >> 32)}; + std::array<uint32_t, 3> accumulator = {nanoseconds, 0, 0}; + + // This code follows the implementation of |BigInt::multiplyAccumulate()|. + + uint32_t carry = 0; + { + uint32_t high = 0; + uint32_t low = digitMul(secToNanos, multiplicand[0], &high); + + uint32_t newCarry = 0; + accumulator[0] = digitAdd(accumulator[0], low, &newCarry); + accumulator[1] = digitAdd(high, newCarry, &carry); + } + { + uint32_t high = 0; + uint32_t low = digitMul(secToNanos, multiplicand[1], &high); + + uint32_t newCarry = 0; + accumulator[1] = digitAdd(accumulator[1], low, &carry); + accumulator[2] = digitAdd(high, carry, &newCarry); + MOZ_ASSERT(newCarry == 0); + } + + return CreateBigInt(cx, accumulator, instant.seconds < 0); +} + +BigInt* js::temporal::ToEpochNanoseconds(JSContext* cx, + const Instant& instant) { + MOZ_ASSERT(IsValidEpochInstant(instant)); + return ::ToEpochBigInt(cx, InstantSpan{instant.seconds, instant.nanoseconds}); +} + +BigInt* js::temporal::ToEpochNanoseconds(JSContext* cx, + const InstantSpan& instant) { + MOZ_ASSERT(IsValidInstantSpan(instant)); + return ::ToEpochBigInt(cx, instant); +} + +/** + * Return an Instant for the input nanoseconds if the input is less-or-equal to + * the maximum instant span. Otherwise returns nothing. + */ +static mozilla::Maybe<InstantSpan> NanosecondsToInstantSpan( + double nanoseconds) { + MOZ_ASSERT(IsInteger(nanoseconds)); + + if (auto int96 = Int96::fromInteger(nanoseconds)) { + constexpr auto maximum = Int96{InstantSpan::max().toSeconds()} * + ToNanoseconds(TemporalUnit::Second); + + // Accept if the value is less-or-equal to the maximum instant span. + if (int96->abs() <= maximum) { + // Split into seconds and nanoseconds. + auto [seconds, nanos] = *int96 / ToNanoseconds(TemporalUnit::Second); + + auto result = InstantSpan{seconds, nanos}; + MOZ_ASSERT(IsValidInstantSpan(result)); + return mozilla::Some(result); + } + } + return mozilla::Nothing(); +} + +/** + * Return an Instant for the input microseconds if the input is less-or-equal to + * the maximum instant span. Otherwise returns nothing. + */ +static mozilla::Maybe<InstantSpan> MicrosecondsToInstantSpan( + double microseconds) { + MOZ_ASSERT(IsInteger(microseconds)); + + constexpr int64_t spanLimit = InstantSpan::max().toSeconds(); + constexpr int64_t secToMicros = ToNanoseconds(TemporalUnit::Second) / + ToNanoseconds(TemporalUnit::Microsecond); + constexpr int32_t microToNanos = ToNanoseconds(TemporalUnit::Microsecond); + + // Fast path for the common case. + if (microseconds == 0) { + return mozilla::Some(InstantSpan{}); + } + + // Reject if the value is larger than the maximum instant span. + if (std::abs(microseconds) > double(spanLimit) * double(secToMicros)) { + return mozilla::Nothing(); + } + + // |spanLimit| in microseconds is below UINT64_MAX, so we can use uint64 in + // the following computations. + static_assert(double(spanLimit) * double(secToMicros) <= double(UINT64_MAX)); + + // Use the absolute value and convert it then into uint64_t. + uint64_t absMicros = uint64_t(std::abs(microseconds)); + + // Seconds and remainder are small enough to fit into int64_t resp. int32_t. + int64_t seconds = absMicros / uint64_t(secToMicros); + int32_t remainder = absMicros % uint64_t(secToMicros); + + // Correct the sign of |seconds| and |remainder|, and then constrain + // |remainder| to the range [0, 999'999]. + if (microseconds < 0) { + seconds *= -1; + if (remainder != 0) { + seconds -= 1; + remainder = secToMicros - remainder; + } + } + + InstantSpan result = {seconds, remainder * microToNanos}; + MOZ_ASSERT(IsValidInstantSpan(result)); + return mozilla::Some(result); +} + +/** + * GetUTCEpochNanoseconds ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond [ , offsetNanoseconds ] ) + */ +Instant js::temporal::GetUTCEpochNanoseconds(const PlainDateTime& dateTime) { + auto& [date, time] = dateTime; + + // Step 1. + MOZ_ASSERT(IsValidISODateTime(dateTime)); + + // Additionally ensure the date-time value can be represented as an Instant. + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + // Steps 2-5. + int64_t ms = MakeDate(dateTime); + + // Propagate the input range to the compiler. + int32_t nanos = + std::clamp(time.microsecond * 1'000 + time.nanosecond, 0, 999'999); + + // Steps 6-8. + return Instant::fromMilliseconds(ms) + InstantSpan{0, nanos}; +} + +/** + * GetUTCEpochNanoseconds ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond [ , offsetNanoseconds ] ) + */ +Instant js::temporal::GetUTCEpochNanoseconds( + const PlainDateTime& dateTime, const InstantSpan& offsetNanoseconds) { + MOZ_ASSERT(offsetNanoseconds.abs() < + InstantSpan::fromNanoseconds(ToNanoseconds(TemporalUnit::Day))); + + // Steps 1-6. + auto epochNanoseconds = GetUTCEpochNanoseconds(dateTime); + + // Steps 7-9. + return epochNanoseconds - offsetNanoseconds; +} + +/** + * CompareEpochNanoseconds ( epochNanosecondsOne, epochNanosecondsTwo ) + */ +static int32_t CompareEpochNanoseconds(const Instant& epochNanosecondsOne, + const Instant& epochNanosecondsTwo) { + // Step 1. + if (epochNanosecondsOne > epochNanosecondsTwo) { + return 1; + } + + // Step 2. + if (epochNanosecondsOne < epochNanosecondsTwo) { + return -1; + } + + // Step 3. + return 0; +} + +/** + * CreateTemporalInstant ( epochNanoseconds [ , newTarget ] ) + */ +InstantObject* js::temporal::CreateTemporalInstant(JSContext* cx, + const Instant& instant) { + // Step 1. + MOZ_ASSERT(IsValidEpochInstant(instant)); + + // Steps 2-3. + auto* object = NewBuiltinClassInstance<InstantObject>(cx); + if (!object) { + return nullptr; + } + + // Step 4. + object->setFixedSlot(InstantObject::SECONDS_SLOT, + NumberValue(instant.seconds)); + object->setFixedSlot(InstantObject::NANOSECONDS_SLOT, + Int32Value(instant.nanoseconds)); + + // Step 5. + return object; +} + +/** + * CreateTemporalInstant ( epochNanoseconds [ , newTarget ] ) + */ +static InstantObject* CreateTemporalInstant(JSContext* cx, const CallArgs& args, + Handle<BigInt*> epochNanoseconds) { + // Step 1. + MOZ_ASSERT(IsValidEpochNanoseconds(epochNanoseconds)); + + // Steps 2-3. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_Instant, &proto)) { + return nullptr; + } + + auto* object = NewObjectWithClassProto<InstantObject>(cx, proto); + if (!object) { + return nullptr; + } + + // Step 4. + auto instant = ToInstant(epochNanoseconds); + object->setFixedSlot(InstantObject::SECONDS_SLOT, + NumberValue(instant.seconds)); + object->setFixedSlot(InstantObject::NANOSECONDS_SLOT, + Int32Value(instant.nanoseconds)); + + // Step 5. + return object; +} + +/** + * ToTemporalInstant ( item ) + */ +Wrapped<InstantObject*> js::temporal::ToTemporalInstant(JSContext* cx, + Handle<Value> item) { + // Step 1. + if (item.isObject()) { + JSObject* itemObj = &item.toObject(); + + // Step 1.a. + if (itemObj->canUnwrapAs<InstantObject>()) { + return itemObj; + } + } + + // Steps 1.b-d and 3-6 + Instant epochNanoseconds; + if (!ToTemporalInstant(cx, item, &epochNanoseconds)) { + return nullptr; + } + + // Step 7. + return CreateTemporalInstant(cx, epochNanoseconds); +} + +/** + * ToTemporalInstant ( item ) + */ +bool js::temporal::ToTemporalInstant(JSContext* cx, Handle<Value> item, + Instant* result) { + // Step 1. + Rooted<Value> primitiveValue(cx, item); + if (item.isObject()) { + JSObject* itemObj = &item.toObject(); + + // Step 1.a. + if (auto* instant = itemObj->maybeUnwrapIf<InstantObject>()) { + *result = ToInstant(instant); + return true; + } + + // Step 1.b. + if (auto* zonedDateTime = itemObj->maybeUnwrapIf<ZonedDateTimeObject>()) { + *result = ToInstant(zonedDateTime); + return true; + } + + // Steps 1.c-d. + if (!ToPrimitive(cx, JSTYPE_STRING, &primitiveValue)) { + return false; + } + } + + // Step 2. + if (!primitiveValue.isString()) { + // The value is always on the stack, so JSDVG_SEARCH_STACK can be used for + // better error reporting. + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, + primitiveValue, nullptr, "not a string"); + return false; + } + Rooted<JSString*> string(cx, primitiveValue.toString()); + + // Steps 3-4. + PlainDateTime dateTime; + int64_t offset; + if (!ParseTemporalInstantString(cx, string, &dateTime, &offset)) { + return false; + } + MOZ_ASSERT(std::abs(offset) < ToNanoseconds(TemporalUnit::Day)); + + // Step 6. (Reordered) + if (!ISODateTimeWithinLimits(dateTime)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 5. + auto epochNanoseconds = + GetUTCEpochNanoseconds(dateTime, InstantSpan::fromNanoseconds(offset)); + + // Step 6. + if (!IsValidEpochInstant(epochNanoseconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 7. + *result = epochNanoseconds; + return true; +} + +/** + * AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds ) + */ +bool js::temporal::AddInstant(JSContext* cx, const Instant& instant, + const Duration& duration, Instant* result) { + MOZ_ASSERT(IsValidEpochInstant(instant)); + MOZ_ASSERT(IsValidDuration(duration)); + MOZ_ASSERT(duration.years == 0); + MOZ_ASSERT(duration.months == 0); + MOZ_ASSERT(duration.weeks == 0); + MOZ_ASSERT(duration.days == 0); + + do { + auto nanoseconds = NanosecondsToInstantSpan(duration.nanoseconds); + if (!nanoseconds) { + break; + } + MOZ_ASSERT(IsValidInstantSpan(*nanoseconds)); + + auto microseconds = MicrosecondsToInstantSpan(duration.microseconds); + if (!microseconds) { + break; + } + MOZ_ASSERT(IsValidInstantSpan(*microseconds)); + + // Overflows for millis/seconds/minutes/hours always result in an invalid + // instant. + + 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; + } + + // Compute the overall amount of milliseconds to add. + mozilla::CheckedInt64 millis = hours; + millis *= 60; + millis += minutes; + millis *= 60; + millis += seconds; + millis *= 1000; + millis += milliseconds; + if (!millis.isValid()) { + break; + } + + auto milli = InstantSpan::fromMilliseconds(millis.value()); + if (!IsValidInstantSpan(milli)) { + break; + } + + // Compute the overall instant span. + auto span = milli + *microseconds + *nanoseconds; + if (!IsValidInstantSpan(span)) { + break; + } + + *result = instant + span; + if (IsValidEpochInstant(*result)) { + return true; + } + } while (false); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; +} + +/** + * DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, + * roundingMode ) + */ +bool js::temporal::DifferenceInstant(JSContext* cx, const Instant& ns1, + const Instant& ns2, + Increment roundingIncrement, + TemporalUnit smallestUnit, + TemporalUnit largestUnit, + TemporalRoundingMode roundingMode, + Duration* result) { + MOZ_ASSERT(IsValidEpochInstant(ns1)); + MOZ_ASSERT(IsValidEpochInstant(ns2)); + MOZ_ASSERT(largestUnit > TemporalUnit::Day); + MOZ_ASSERT(largestUnit <= smallestUnit); + MOZ_ASSERT(roundingIncrement <= + MaximumTemporalDurationRoundingIncrement(smallestUnit)); + + // Step 1. + auto diff = ns2 - ns1; + MOZ_ASSERT(IsValidInstantSpan(diff)); + + // Negative nanoseconds are represented as the difference to 1'000'000'000. + auto [seconds, nanoseconds] = diff; + if (seconds < 0 && nanoseconds != 0) { + seconds += 1; + nanoseconds -= ToNanoseconds(TemporalUnit::Second); + } + + // Steps 2-5. + Duration duration = { + 0, + 0, + 0, + 0, + 0, + 0, + double(seconds), + double((nanoseconds / 1000'000) % 1000), + double((nanoseconds / 1000) % 1000), + double(nanoseconds % 1000), + }; + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 6. + if (smallestUnit == TemporalUnit::Nanosecond && + roundingIncrement == Increment{1}) { + TimeDuration balanced; + if (!BalanceTimeDuration(cx, duration, largestUnit, &balanced)) { + return false; + } + MOZ_ASSERT(balanced.days == 0); + + *result = balanced.toDuration().time(); + return true; + } + + // Steps 7-8. + Duration roundResult; + if (!temporal::RoundDuration(cx, duration, roundingIncrement, smallestUnit, + roundingMode, &roundResult)) { + return false; + } + + // Step 9. + MOZ_ASSERT(roundResult.days == 0); + + // Step 10. + TimeDuration balanced; + if (!BalanceTimeDuration(cx, roundResult, largestUnit, &balanced)) { + return false; + } + MOZ_ASSERT(balanced.days == 0); + + *result = balanced.toDuration().time(); + return true; +} + +/** + * RoundNumberToIncrementAsIfPositive ( x, increment, roundingMode ) + */ +static bool RoundNumberToIncrementAsIfPositive( + JSContext* cx, const Instant& x, int64_t increment, + TemporalRoundingMode roundingMode, Instant* result) { + // This operation is equivalent to adjusting the rounding mode through + // |ToPositiveRoundingMode| and then calling |RoundNumberToIncrement|. + return RoundNumberToIncrement(cx, x, increment, + ToPositiveRoundingMode(roundingMode), result); +} + +/** + * RoundTemporalInstant ( ns, increment, unit, roundingMode ) + */ +bool js::temporal::RoundTemporalInstant(JSContext* cx, const Instant& ns, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + Instant* result) { + MOZ_ASSERT(IsValidEpochInstant(ns)); + MOZ_ASSERT(increment >= Increment::min()); + MOZ_ASSERT(uint64_t(increment.value()) <= ToNanoseconds(TemporalUnit::Day)); + MOZ_ASSERT(unit > TemporalUnit::Day); + + // Steps 1-6. + int64_t toNanoseconds = ToNanoseconds(unit); + MOZ_ASSERT( + (increment.value() * toNanoseconds) <= ToNanoseconds(TemporalUnit::Day), + "increment * toNanoseconds shouldn't overflow instant resolution"); + + // Step 7. + return RoundNumberToIncrementAsIfPositive( + cx, ns, increment.value() * toNanoseconds, roundingMode, result); +} + +/** + * DifferenceTemporalInstant ( operation, instant, other, options ) + */ +static bool DifferenceTemporalInstant(JSContext* cx, + TemporalDifference operation, + const CallArgs& args) { + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Instant other; + if (!ToTemporalInstant(cx, args.get(0), &other)) { + return false; + } + + // Steps 3-4. + DifferenceSettings settings; + if (args.hasDefined(1)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", ToName(operation), args[1])); + if (!options) { + return false; + } + + // Step 3. + Rooted<PlainObject*> resolvedOptions(cx, + SnapshotOwnProperties(cx, options)); + if (!resolvedOptions) { + return false; + } + + // Step 4. + if (!GetDifferenceSettings( + cx, operation, resolvedOptions, TemporalUnitGroup::Time, + TemporalUnit::Nanosecond, TemporalUnit::Second, &settings)) { + return false; + } + } else { + // Steps 3-4. + settings = { + TemporalUnit::Nanosecond, + TemporalUnit::Second, + TemporalRoundingMode::Trunc, + Increment{1}, + }; + } + + // Step 5. + Duration difference; + if (!DifferenceInstant(cx, instant, other, settings.roundingIncrement, + settings.smallestUnit, settings.largestUnit, + settings.roundingMode, &difference)) { + return false; + } + + // Step 6. + if (operation == TemporalDifference::Since) { + difference = difference.negate(); + } + + auto* obj = CreateTemporalDuration(cx, difference); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +enum class InstantDuration { Add, Subtract }; + +/** + * AddDurationToOrSubtractDurationFromInstant ( operation, instant, + * temporalDurationLike ) + */ +static bool AddDurationToOrSubtractDurationFromInstant( + JSContext* cx, InstantDuration operation, const CallArgs& args) { + auto* instant = &args.thisv().toObject().as<InstantObject>(); + auto epochNanoseconds = ToInstant(instant); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Duration duration; + if (!ToTemporalDurationRecord(cx, args.get(0), &duration)) { + return false; + } + + // Steps 3-6. + if (duration.years != 0 || duration.months != 0 || duration.weeks != 0 || + duration.days != 0) { + const char* part = duration.years != 0 ? "years" + : duration.months != 0 ? "months" + : duration.weeks != 0 ? "weeks" + : "days"; + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_BAD_DURATION, part); + return false; + } + + // Step 7. + if (operation == InstantDuration::Subtract) { + duration = duration.negate(); + } + + Instant ns; + if (!AddInstant(cx, epochNanoseconds, duration, &ns)) { + return false; + } + + // Step 8. + auto* result = CreateTemporalInstant(cx, ns); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant ( epochNanoseconds ) + */ +static bool InstantConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.Instant")) { + return false; + } + + // Step 2. + Rooted<BigInt*> epochNanoseconds(cx, js::ToBigInt(cx, args.get(0))); + if (!epochNanoseconds) { + return false; + } + + // Step 3. + if (!IsValidEpochNanoseconds(epochNanoseconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 4. + auto* result = CreateTemporalInstant(cx, args, epochNanoseconds); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.from ( item ) + */ +static bool Instant_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Steps 1-2. + Instant epochInstant; + if (!ToTemporalInstant(cx, args.get(0), &epochInstant)) { + return false; + } + + auto* result = CreateTemporalInstant(cx, epochInstant); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.fromEpochSeconds ( epochSeconds ) + */ +static bool Instant_fromEpochSeconds(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + double epochSeconds; + if (!JS::ToNumber(cx, args.get(0), &epochSeconds)) { + return false; + } + + // Step 2. + // + // NumberToBigInt throws a RangeError for non-integral numbers. + if (!IsInteger(epochSeconds)) { + ToCStringBuf cbuf; + const char* str = NumberToCString(&cbuf, epochSeconds); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_NONINTEGER, str); + return false; + } + + // Step 3. (Not applicable) + + // Step 4. + if (!IsValidEpochSeconds(epochSeconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 5. + auto* result = CreateTemporalInstant(cx, Instant::fromSeconds(epochSeconds)); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.fromEpochMilliseconds ( epochMilliseconds ) + */ +static bool Instant_fromEpochMilliseconds(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + double epochMilliseconds; + if (!JS::ToNumber(cx, args.get(0), &epochMilliseconds)) { + return false; + } + + // Step 2. + // + // NumberToBigInt throws a RangeError for non-integral numbers. + if (!IsInteger(epochMilliseconds)) { + ToCStringBuf cbuf; + const char* str = NumberToCString(&cbuf, epochMilliseconds); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_NONINTEGER, str); + return false; + } + + // Step 3. (Not applicable) + + // Step 4. + if (!IsValidEpochMilliseconds(epochMilliseconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 5. + auto* result = + CreateTemporalInstant(cx, Instant::fromMilliseconds(epochMilliseconds)); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.fromEpochMicroseconds ( epochMicroseconds ) + */ +static bool Instant_fromEpochMicroseconds(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<BigInt*> epochMicroseconds(cx, js::ToBigInt(cx, args.get(0))); + if (!epochMicroseconds) { + return false; + } + + // Step 2. (Not applicable) + + // Step 3. + if (!IsValidEpochMicroseconds(epochMicroseconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + int64_t i; + MOZ_ALWAYS_TRUE(BigInt::isInt64(epochMicroseconds, &i)); + + // Step 4. + auto* result = CreateTemporalInstant(cx, Instant::fromMicroseconds(i)); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.fromEpochNanoseconds ( epochNanoseconds ) + */ +static bool Instant_fromEpochNanoseconds(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<BigInt*> epochNanoseconds(cx, js::ToBigInt(cx, args.get(0))); + if (!epochNanoseconds) { + return false; + } + + // Step 2. + if (!IsValidEpochNanoseconds(epochNanoseconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 3. + auto* result = CreateTemporalInstant(cx, ToInstant(epochNanoseconds)); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.compare ( one, two ) + */ +static bool Instant_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Instant one; + if (!ToTemporalInstant(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + Instant two; + if (!ToTemporalInstant(cx, args.get(1), &two)) { + return false; + } + + // Step 3. + args.rval().setInt32(CompareEpochNanoseconds(one, two)); + return true; +} + +/** + * get Temporal.Instant.prototype.epochSeconds + */ +static bool Instant_epochSeconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Steps 4-5. + args.rval().setNumber(instant.seconds); + return true; +} + +/** + * get Temporal.Instant.prototype.epochSeconds + */ +static bool Instant_epochSeconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_epochSeconds>(cx, args); +} + +/** + * get Temporal.Instant.prototype.epochMilliseconds + */ +static bool Instant_epochMilliseconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Step 4-5. + args.rval().setNumber(instant.floorToMilliseconds()); + return true; +} + +/** + * get Temporal.Instant.prototype.epochMilliseconds + */ +static bool Instant_epochMilliseconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_epochMilliseconds>(cx, args); +} + +/** + * get Temporal.Instant.prototype.epochMicroseconds + */ +static bool Instant_epochMicroseconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Step 4. + auto* microseconds = + BigInt::createFromInt64(cx, instant.floorToMicroseconds()); + if (!microseconds) { + return false; + } + + // Step 5. + args.rval().setBigInt(microseconds); + return true; +} + +/** + * get Temporal.Instant.prototype.epochMicroseconds + */ +static bool Instant_epochMicroseconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_epochMicroseconds>(cx, args); +} + +/** + * get Temporal.Instant.prototype.epochNanoseconds + */ +static bool Instant_epochNanoseconds(JSContext* cx, const CallArgs& args) { + // Step 3. + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + auto* nanoseconds = ToEpochNanoseconds(cx, instant); + if (!nanoseconds) { + return false; + } + + // Step 4. + args.rval().setBigInt(nanoseconds); + return true; +} + +/** + * get Temporal.Instant.prototype.epochNanoseconds + */ +static bool Instant_epochNanoseconds(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_epochNanoseconds>(cx, args); +} + +/** + * Temporal.Instant.prototype.add ( temporalDurationLike ) + */ +static bool Instant_add(JSContext* cx, const CallArgs& args) { + return AddDurationToOrSubtractDurationFromInstant(cx, InstantDuration::Add, + args); +} + +/** + * Temporal.Instant.prototype.add ( temporalDurationLike ) + */ +static bool Instant_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_add>(cx, args); +} + +/** + * Temporal.Instant.prototype.subtract ( temporalDurationLike ) + */ +static bool Instant_subtract(JSContext* cx, const CallArgs& args) { + return AddDurationToOrSubtractDurationFromInstant( + cx, InstantDuration::Subtract, args); +} + +/** + * Temporal.Instant.prototype.subtract ( temporalDurationLike ) + */ +static bool Instant_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_subtract>(cx, args); +} + +/** + * Temporal.Instant.prototype.until ( other [ , options ] ) + */ +static bool Instant_until(JSContext* cx, const CallArgs& args) { + return DifferenceTemporalInstant(cx, TemporalDifference::Until, args); +} + +/** + * Temporal.Instant.prototype.until ( other [ , options ] ) + */ +static bool Instant_until(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_until>(cx, args); +} + +/** + * Temporal.Instant.prototype.since ( other [ , options ] ) + */ +static bool Instant_since(JSContext* cx, const CallArgs& args) { + return DifferenceTemporalInstant(cx, TemporalDifference::Since, args); +} + +/** + * Temporal.Instant.prototype.since ( other [ , options ] ) + */ +static bool Instant_since(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_since>(cx, args); +} + +/** + * Temporal.Instant.prototype.round ( roundTo ) + */ +static bool Instant_round(JSContext* cx, const CallArgs& args) { + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Steps 3-16. + auto smallestUnit = TemporalUnit::Auto; + auto roundingMode = TemporalRoundingMode::HalfExpand; + auto roundingIncrement = Increment{1}; + if (args.get(0).isString()) { + // Steps 4 and 6-8. (Not applicable in our implementation.) + + // Step 9. + Rooted<JSString*> paramString(cx, args[0].toString()); + if (!GetTemporalUnit(cx, paramString, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + // Steps 10-16. (Not applicable in our implementation.) + } else { + // Steps 3 and 5. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "roundTo", "round", args.get(0))); + if (!options) { + return false; + } + + // Steps 6-7. + if (!ToTemporalRoundingIncrement(cx, options, &roundingIncrement)) { + return false; + } + + // Step 8. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 9. + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + if (smallestUnit == TemporalUnit::Auto) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_OPTION, "smallestUnit"); + return false; + } + + // Steps 10-15. + uint64_t maximum = UnitsPerDay(smallestUnit); + + // Step 16. + if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, + true)) { + return false; + } + } + + // Step 17. + Instant roundedNs; + if (!RoundTemporalInstant(cx, instant, roundingIncrement, smallestUnit, + roundingMode, &roundedNs)) { + return false; + } + + // Step 18. + auto* result = CreateTemporalInstant(cx, roundedNs); + if (!result) { + return false; + } + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.prototype.round ( options ) + */ +static bool Instant_round(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_round>(cx, args); +} + +/** + * Temporal.Instant.prototype.equals ( other ) + */ +static bool Instant_equals(JSContext* cx, const CallArgs& args) { + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Step 3. + Instant other; + if (!ToTemporalInstant(cx, args.get(0), &other)) { + return false; + } + + // Steps 4-5. + args.rval().setBoolean(instant == other); + return true; +} + +/** + * Temporal.Instant.prototype.equals ( other ) + */ +static bool Instant_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_equals>(cx, args); +} + +/** + * Temporal.Instant.prototype.toString ( [ options ] ) + */ +static bool Instant_toString(JSContext* cx, const CallArgs& args) { + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + Rooted<TimeZoneValue> timeZone(cx); + auto roundingMode = TemporalRoundingMode::Trunc; + SecondsStringPrecision precision = {Precision::Auto(), + TemporalUnit::Nanosecond, Increment{1}}; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Steps 4-5. + auto digits = Precision::Auto(); + if (!ToFractionalSecondDigits(cx, options, &digits)) { + return false; + } + + // Step 6. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 7. + auto smallestUnit = TemporalUnit::Auto; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + // Step 8. + if (smallestUnit == TemporalUnit::Hour) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_OPTION, "hour", + "smallestUnit"); + return false; + } + + // Step 9. + Rooted<Value> value(cx); + if (!GetProperty(cx, options, options, cx->names().timeZone, &value)) { + return false; + } + + // Step 10. + if (!value.isUndefined()) { + if (!ToTemporalTimeZone(cx, value, &timeZone)) { + return false; + } + } + + // Step 11. + precision = ToSecondsStringPrecision(smallestUnit, digits); + } + + // Step 12. + Instant ns; + if (!RoundTemporalInstant(cx, instant, precision.increment, precision.unit, + roundingMode, &ns)) { + return false; + } + + // Step 13. + Rooted<InstantObject*> roundedInstant(cx, CreateTemporalInstant(cx, ns)); + if (!roundedInstant) { + return false; + } + + // Step 14. + JSString* str = TemporalInstantToString(cx, roundedInstant, timeZone, + precision.precision); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.Instant.prototype.toString ( [ options ] ) + */ +static bool Instant_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_toString>(cx, args); +} + +/** + * Temporal.Instant.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool Instant_toLocaleString(JSContext* cx, const CallArgs& args) { + Rooted<InstantObject*> instant(cx, + &args.thisv().toObject().as<InstantObject>()); + + // Step 3. + Rooted<TimeZoneValue> timeZone(cx); + JSString* str = + TemporalInstantToString(cx, instant, timeZone, Precision::Auto()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.Instant.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool Instant_toLocaleString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_toLocaleString>(cx, args); +} + +/** + * Temporal.Instant.prototype.toJSON ( ) + */ +static bool Instant_toJSON(JSContext* cx, const CallArgs& args) { + Rooted<InstantObject*> instant(cx, + &args.thisv().toObject().as<InstantObject>()); + + // Step 3. + Rooted<TimeZoneValue> timeZone(cx); + JSString* str = + TemporalInstantToString(cx, instant, timeZone, Precision::Auto()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.Instant.prototype.toJSON ( ) + */ +static bool Instant_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_toJSON>(cx, args); +} + +/** + * Temporal.Instant.prototype.valueOf ( ) + */ +static bool Instant_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "Instant", "primitive type"); + return false; +} + +/** + * Temporal.Instant.prototype.toZonedDateTime ( item ) + */ +static bool Instant_toZonedDateTime(JSContext* cx, const CallArgs& args) { + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Step 3. + Rooted<JSObject*> item( + cx, RequireObjectArg(cx, "item", "toZonedDateTime", args.get(0))); + if (!item) { + return false; + } + + // Step 4. + Rooted<Value> calendarLike(cx); + if (!GetProperty(cx, item, item, cx->names().calendar, &calendarLike)) { + return false; + } + + // Step 5. + if (calendarLike.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_PROPERTY, "calendar"); + return false; + } + + // Step 6. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, calendarLike, &calendar)) { + return false; + } + + // Step 7. + Rooted<Value> timeZoneLike(cx); + if (!GetProperty(cx, item, item, cx->names().timeZone, &timeZoneLike)) { + return false; + } + + // Step 8. + if (timeZoneLike.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_PROPERTY, "timeZone"); + return false; + } + + // Step 9. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZone(cx, timeZoneLike, &timeZone)) { + return false; + } + + // Step 10. + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.prototype.toZonedDateTime ( item ) + */ +static bool Instant_toZonedDateTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_toZonedDateTime>(cx, args); +} + +/** + * Temporal.Instant.prototype.toZonedDateTimeISO ( item ) + */ +static bool Instant_toZonedDateTimeISO(JSContext* cx, const CallArgs& args) { + auto instant = ToInstant(&args.thisv().toObject().as<InstantObject>()); + + // Step 3. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // Step 4. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Instant.prototype.toZonedDateTimeISO ( item ) + */ +static bool Instant_toZonedDateTimeISO(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsInstant, Instant_toZonedDateTimeISO>(cx, args); +} + +const JSClass InstantObject::class_ = { + "Temporal.Instant", + JSCLASS_HAS_RESERVED_SLOTS(InstantObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_Instant), + JS_NULL_CLASS_OPS, + &InstantObject::classSpec_, +}; + +const JSClass& InstantObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec Instant_methods[] = { + JS_FN("from", Instant_from, 1, 0), + JS_FN("fromEpochSeconds", Instant_fromEpochSeconds, 1, 0), + JS_FN("fromEpochMilliseconds", Instant_fromEpochMilliseconds, 1, 0), + JS_FN("fromEpochMicroseconds", Instant_fromEpochMicroseconds, 1, 0), + JS_FN("fromEpochNanoseconds", Instant_fromEpochNanoseconds, 1, 0), + JS_FN("compare", Instant_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec Instant_prototype_methods[] = { + JS_FN("add", Instant_add, 1, 0), + JS_FN("subtract", Instant_subtract, 1, 0), + JS_FN("until", Instant_until, 1, 0), + JS_FN("since", Instant_since, 1, 0), + JS_FN("round", Instant_round, 1, 0), + JS_FN("equals", Instant_equals, 1, 0), + JS_FN("toString", Instant_toString, 0, 0), + JS_FN("toLocaleString", Instant_toLocaleString, 0, 0), + JS_FN("toJSON", Instant_toJSON, 0, 0), + JS_FN("valueOf", Instant_valueOf, 0, 0), + JS_FN("toZonedDateTime", Instant_toZonedDateTime, 1, 0), + JS_FN("toZonedDateTimeISO", Instant_toZonedDateTimeISO, 1, 0), + JS_FS_END, +}; + +static const JSPropertySpec Instant_prototype_properties[] = { + JS_PSG("epochSeconds", Instant_epochSeconds, 0), + JS_PSG("epochMilliseconds", Instant_epochMilliseconds, 0), + JS_PSG("epochMicroseconds", Instant_epochMicroseconds, 0), + JS_PSG("epochNanoseconds", Instant_epochNanoseconds, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.Instant", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec InstantObject::classSpec_ = { + GenericCreateConstructor<InstantConstructor, 1, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<InstantObject>, + Instant_methods, + nullptr, + Instant_prototype_methods, + Instant_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/Instant.h b/js/src/builtin/temporal/Instant.h new file mode 100644 index 0000000000..edce677d10 --- /dev/null +++ b/js/src/builtin/temporal/Instant.h @@ -0,0 +1,159 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_Instant_h +#define builtin_temporal_Instant_h + +#include "mozilla/Assertions.h" + +#include <stdint.h> + +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/Wrapped.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +namespace js { +struct ClassSpec; +} + +namespace js::temporal { + +class InstantObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t SECONDS_SLOT = 0; + static constexpr uint32_t NANOSECONDS_SLOT = 1; + static constexpr uint32_t SLOT_COUNT = 2; + + int64_t seconds() const { + double seconds = getFixedSlot(SECONDS_SLOT).toNumber(); + MOZ_ASSERT(-8'640'000'000'000 <= seconds && seconds <= 8'640'000'000'000); + return int64_t(seconds); + } + + int32_t nanoseconds() const { + int32_t nanoseconds = getFixedSlot(NANOSECONDS_SLOT).toInt32(); + MOZ_ASSERT(0 <= nanoseconds && nanoseconds <= 999'999'999); + return nanoseconds; + } + + private: + static const ClassSpec classSpec_; +}; + +/** + * Extract the instant fields from the Instant object. + */ +inline Instant ToInstant(const InstantObject* instant) { + return {instant->seconds(), instant->nanoseconds()}; +} + +class Increment; +enum class TemporalUnit; +enum class TemporalRoundingMode; + +/** + * IsValidEpochNanoseconds ( epochNanoseconds ) + */ +bool IsValidEpochNanoseconds(const JS::BigInt* epochNanoseconds); + +/** + * IsValidEpochNanoseconds ( epochNanoseconds ) + */ +bool IsValidEpochInstant(const Instant& instant); + +/** + * Return true if the input is within the valid instant span limits. + */ +bool IsValidInstantSpan(const InstantSpan& span); + +/** + * Return true if the input is within the valid instant span limits. + */ +bool IsValidInstantSpan(const JS::BigInt* nanoseconds); + +/** + * Convert a BigInt to an instant. The input must be a valid epoch nanoseconds + * value. + */ +Instant ToInstant(const JS::BigInt* epochNanoseconds); + +/** + * Convert a BigInt to an instant span. The input must be a valid epoch + * nanoseconds span value. + */ +InstantSpan ToInstantSpan(const JS::BigInt* nanoseconds); + +/** + * Convert an instant to a BigInt. The input must be a valid epoch instant. + */ +JS::BigInt* ToEpochNanoseconds(JSContext* cx, const Instant& instant); + +/** + * Convert an instant span to a BigInt. The input must be a valid instant span. + */ +JS::BigInt* ToEpochNanoseconds(JSContext* cx, const InstantSpan& instant); + +/** + * ToTemporalInstant ( item ) + */ +Wrapped<InstantObject*> ToTemporalInstant(JSContext* cx, + JS::Handle<JS::Value> item); + +/** + * ToTemporalInstant ( item ) + */ +bool ToTemporalInstant(JSContext* cx, JS::Handle<JS::Value> item, + Instant* result); + +/** + * CreateTemporalInstant ( epochNanoseconds [ , newTarget ] ) + */ +InstantObject* CreateTemporalInstant(JSContext* cx, const Instant& instant); + +/** + * GetUTCEpochNanoseconds ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond [ , offsetNanoseconds ] ) + */ +Instant GetUTCEpochNanoseconds(const PlainDateTime& dateTime); + +/** + * GetUTCEpochNanoseconds ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond [ , offsetNanoseconds ] ) + */ +Instant GetUTCEpochNanoseconds(const PlainDateTime& dateTime, + const InstantSpan& offsetNanoseconds); + +/** + * RoundTemporalInstant ( ns, increment, unit, roundingMode ) + */ +bool RoundTemporalInstant(JSContext* cx, const Instant& ns, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + Instant* result); + +/** + * AddInstant ( epochNanoseconds, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds ) + */ +bool AddInstant(JSContext* cx, const Instant& instant, const Duration& duration, + Instant* result); + +/** + * DifferenceInstant ( ns1, ns2, roundingIncrement, smallestUnit, largestUnit, + * roundingMode ) + */ +bool DifferenceInstant(JSContext* cx, const Instant& ns1, const Instant& ns2, + Increment roundingIncrement, TemporalUnit smallestUnit, + TemporalUnit largestUnit, + TemporalRoundingMode roundingMode, Duration* result); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_Instant_h */ diff --git a/js/src/builtin/temporal/Int96.cpp b/js/src/builtin/temporal/Int96.cpp new file mode 100644 index 0000000000..73ea5ce90e --- /dev/null +++ b/js/src/builtin/temporal/Int96.cpp @@ -0,0 +1,81 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/Int96.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Maybe.h" + +#include <cmath> +#include <stddef.h> +#include <stdint.h> + +#include "jsnum.h" + +using namespace js; +using namespace js::temporal; + +mozilla::Maybe<Int96> Int96::fromInteger(double value) { + MOZ_ASSERT(IsInteger(value)); + + // Fast path for the common case. + int64_t intValue; + if (mozilla::NumberEqualsInt64(value, &intValue)) { + return mozilla::Some(Int96{intValue}); + } + + // First double integer which requires more than three digits. + constexpr double maximum = 0x1p+96; + + // Reject if the value needs more than 96 bits. + if (std::abs(value) >= maximum) { + return mozilla::Nothing(); + } + + // Inlined version of |BigInt::createFromDouble()| for DigitBits=32. See the + // comments in |BigInt::createFromDouble()| for how this code works. + constexpr size_t DigitBits = 32; + + // The number can't have more than three digits when it's below |maximum|. + Int96::Digits digits = {}; + + int exponent = mozilla::ExponentComponent(value); + MOZ_ASSERT(0 <= exponent && exponent <= 95, + "exponent is lower than exponent of 0x1p+96"); + + int length = exponent / DigitBits + 1; + MOZ_ASSERT(1 <= length && length <= 3); + + using Double = mozilla::FloatingPoint<double>; + uint64_t mantissa = + mozilla::BitwiseCast<uint64_t>(value) & Double::kSignificandBits; + + // Add implicit high bit. + mantissa |= 1ull << Double::kSignificandWidth; + + // 0-indexed position of the double's most significant bit within the `msd`. + int msdTopBit = exponent % DigitBits; + + // First, build the MSD by shifting the mantissa appropriately. + int remainingMantissaBits = Double::kSignificandWidth - msdTopBit; + digits[--length] = mantissa >> remainingMantissaBits; + + // Fill in digits containing mantissa contributions. + mantissa = mantissa << (64 - remainingMantissaBits); + if (mantissa) { + MOZ_ASSERT(length > 0); + digits[--length] = uint32_t(mantissa >> 32); + + if (uint32_t(mantissa)) { + MOZ_ASSERT(length > 0); + digits[--length] = uint32_t(mantissa); + } + } + + return mozilla::Some(Int96{digits, value < 0}); +} diff --git a/js/src/builtin/temporal/Int96.h b/js/src/builtin/temporal/Int96.h new file mode 100644 index 0000000000..dfb0a5c231 --- /dev/null +++ b/js/src/builtin/temporal/Int96.h @@ -0,0 +1,161 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_Int96_h +#define builtin_temporal_Int96_h + +#include "mozilla/Assertions.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Maybe.h" + +#include <array> +#include <climits> +#include <stddef.h> +#include <stdint.h> +#include <utility> + +namespace js::temporal { + +/** + * 96-bit integer with explicit sign. Supports integers in the range + * [-(2**96 - 1), 2**96 - 1]. + */ +class Int96 final { + public: + using Digit = uint32_t; + using TwoDigit = uint64_t; + + // The 96-bit integer is stored as three separate 32-bit integers. + using Digits = std::array<Digit, 3>; + + private: + // Unsigned number in the range [0, 0xffff'ffff'ffff'ffff'ffff'ffff]. + // + // The least significant digit is stored at index 0. The most significant + // digit is stored at index 2. + Digits digits = {}; + + // Explicit negative sign. + bool negative = false; + + public: + // Default constructor initializes to zero. + constexpr Int96() = default; + + // Create from an 64-bit integer. + constexpr explicit Int96(int64_t value) : negative(value < 0) { + // NB: Not std::abs, because std::abs(INT64_MIN) is undefined behavior. + uint64_t abs = mozilla::Abs(value); + digits[1] = uint32_t(abs >> 32); + digits[0] = uint32_t(abs); + } + + constexpr Int96(Digits digits, bool negative) + : digits(digits), negative(negative) { + // Assert zero is non-negative. + MOZ_ASSERT_IF((digits[0] | digits[1] | digits[2]) == 0, !negative); + } + + constexpr bool operator==(const Int96& other) const { + return digits[0] == other.digits[0] && digits[1] == other.digits[1] && + digits[2] == other.digits[2] && negative == other.negative; + } + + constexpr bool operator<(const Int96& other) const { + if (negative != other.negative) { + return negative; + } + for (size_t i = digits.size(); i != 0; --i) { + Digit x = digits[i - 1]; + Digit y = other.digits[i - 1]; + if (x != y) { + return negative ? x > y : x < y; + } + } + return false; + } + + // Other operators are implemented in terms of operator== and operator<. + constexpr bool operator!=(const Int96& other) const { + return !(*this == other); + } + constexpr bool operator>(const Int96& other) const { return other < *this; } + constexpr bool operator<=(const Int96& other) const { + return !(other < *this); + } + constexpr bool operator>=(const Int96& other) const { + return !(*this < other); + } + + /** + * Multiply this integer with an multiplier. Overflow is not supported. + */ + constexpr Int96& operator*=(Digit multiplier) { + Digit carry = 0; + for (auto& digit : digits) { + TwoDigit d = digit; + d *= multiplier; + d += carry; + + digit = Digit(d); + carry = Digit(d >> 32); + } + MOZ_ASSERT(carry == 0, "unsupported overflow"); + + return *this; + } + + /** + * Multiply this integer with an multiplier. Overflow is not supported. + */ + constexpr Int96 operator*(Digit multiplier) const { + auto result = *this; + result *= multiplier; + return result; + } + + /** + * Divide this integer by the divisor using Euclidean division. The divisor + * must be smaller than the most significant digit of the integer. Returns the + * quotient and the remainder. + */ + constexpr std::pair<int64_t, int32_t> operator/(Digit divisor) const { + MOZ_ASSERT(digits[2] < divisor, "unsupported divisor"); + + Digit quotient[2] = {}; + Digit remainder = digits[2]; + for (int32_t i = 1; i >= 0; i--) { + TwoDigit n = (TwoDigit(remainder) << 32) | digits[i]; + quotient[i] = n / divisor; + remainder = n % divisor; + } + + int64_t result = (TwoDigit(quotient[1]) << 32) | quotient[0]; + if (negative) { + result *= -1; + if (remainder != 0) { + result -= 1; + remainder = divisor - remainder; + } + } + return {result, int32_t(remainder)}; + } + + /** + * Return the absolute value of this integer. + */ + constexpr Int96 abs() const { return {digits, false}; } + + /** + * Return Some(Int96) if the integer value fits into a 96-bit integer. + * Otherwise returns Nothing(). + */ + static mozilla::Maybe<Int96> fromInteger(double value); +}; + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_Int96_h */ diff --git a/js/src/builtin/temporal/PlainDate.cpp b/js/src/builtin/temporal/PlainDate.cpp new file mode 100644 index 0000000000..759456c9cc --- /dev/null +++ b/js/src/builtin/temporal/PlainDate.cpp @@ -0,0 +1,2999 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/PlainDate.h" + +#include "mozilla/Assertions.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 "jstypes.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/PlainMonthDay.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.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/ToString.h" +#include "builtin/temporal/Wrapped.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "ds/IdValuePair.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/Date.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCVector.h" +#include "js/Id.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/BytecodeUtil.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" +#include "vm/PropertyInfo.h" +#include "vm/Realm.h" +#include "vm/Shape.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 IsPlainDate(Handle<Value> v) { + return v.isObject() && v.toObject().is<PlainDateObject>(); +} + +#ifdef DEBUG +/** + * IsValidISODate ( year, month, day ) + */ +template <typename T> +static bool IsValidISODate(T year, T month, T day) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + MOZ_ASSERT(IsInteger(day)); + + // Step 2. + if (month < 1 || month > 12) { + return false; + } + + // Step 3. + int32_t daysInMonth = js::temporal::ISODaysInMonth(year, int32_t(month)); + + // Step 4. + if (day < 1 || day > daysInMonth) { + return false; + } + + // Step 5. + return true; +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::IsValidISODate(const PlainDate& date) { + auto& [year, month, day] = date; + return ::IsValidISODate(year, month, day); +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::IsValidISODate(double year, double month, double day) { + return ::IsValidISODate(year, month, day); +} +#endif + +static void ReportInvalidDateValue(JSContext* cx, const char* name, int32_t min, + int32_t max, double num) { + Int32ToCStringBuf minCbuf; + const char* minStr = Int32ToCString(&minCbuf, min); + + Int32ToCStringBuf maxCbuf; + const char* maxStr = Int32ToCString(&maxCbuf, max); + + ToCStringBuf numCbuf; + const char* numStr = NumberToCString(&numCbuf, num); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID_VALUE, name, + minStr, maxStr, numStr); +} + +template <typename T> +static inline bool ThrowIfInvalidDateValue(JSContext* cx, const char* name, + int32_t min, int32_t max, T num) { + if (min <= num && num <= max) { + return true; + } + ReportInvalidDateValue(cx, name, min, max, num); + return false; +} + +/** + * IsValidISODate ( year, month, day ) + */ +template <typename T> +static bool ThrowIfInvalidISODate(JSContext* cx, T year, T month, T day) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + MOZ_ASSERT(IsInteger(day)); + + // Step 2. + if (!ThrowIfInvalidDateValue(cx, "month", 1, 12, month)) { + return false; + } + + // Step 3. + int32_t daysInMonth = js::temporal::ISODaysInMonth(year, int32_t(month)); + + // Step 4. + if (!ThrowIfInvalidDateValue(cx, "day", 1, daysInMonth, day)) { + return false; + } + + // Step 5. + return true; +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::ThrowIfInvalidISODate(JSContext* cx, const PlainDate& date) { + auto& [year, month, day] = date; + return ::ThrowIfInvalidISODate(cx, year, month, day); +} + +/** + * IsValidISODate ( year, month, day ) + */ +bool js::temporal::ThrowIfInvalidISODate(JSContext* cx, double year, + double month, double day) { + return ::ThrowIfInvalidISODate(cx, year, month, day); +} + +/** + * RegulateISODate ( year, month, day, overflow ) + * + * With |overflow = "constrain"|. + */ +static PlainDate ConstrainISODate(const PlainDate& date) { + auto& [year, month, day] = date; + + // Step 1.a. + int32_t m = std::clamp(month, 1, 12); + + // Step 1.b. + int32_t daysInMonth = temporal::ISODaysInMonth(year, m); + + // Step 1.c. + int32_t d = std::clamp(day, 1, daysInMonth); + + // Step 1.d. + return {year, m, d}; +} + +/** + * RegulateISODate ( year, month, day, overflow ) + */ +bool js::temporal::RegulateISODate(JSContext* cx, const PlainDate& date, + TemporalOverflow overflow, + PlainDate* result) { + // Step 1. + if (overflow == TemporalOverflow::Constrain) { + *result = ::ConstrainISODate(date); + return true; + } + + // Step 2.a. + MOZ_ASSERT(overflow == TemporalOverflow::Reject); + + // Step 2.b. + if (!ThrowIfInvalidISODate(cx, date)) { + return false; + } + + // Step 2.b. (Inlined call to CreateISODateRecord.) + *result = date; + return true; +} + +/** + * RegulateISODate ( year, month, day, overflow ) + */ +bool js::temporal::RegulateISODate(JSContext* cx, double year, double month, + double day, TemporalOverflow overflow, + RegulatedISODate* result) { + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + MOZ_ASSERT(IsInteger(day)); + + // Step 1. + if (overflow == TemporalOverflow::Constrain) { + // Step 1.a. + int32_t m = int32_t(std::clamp(month, 1.0, 12.0)); + + // Step 1.b. + double daysInMonth = double(ISODaysInMonth(year, m)); + + // Step 1.c. + int32_t d = int32_t(std::clamp(day, 1.0, daysInMonth)); + + // Step 1.d. + *result = {year, m, d}; + return true; + } + + // Step 2.a. + MOZ_ASSERT(overflow == TemporalOverflow::Reject); + + // Step 2.b. + if (!ThrowIfInvalidISODate(cx, year, month, day)) { + return false; + } + + // Step 2.b. (Inlined call to CreateISODateRecord.) + *result = {year, int32_t(month), int32_t(day)}; + return true; +} + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +static PlainDateObject* CreateTemporalDate(JSContext* cx, const CallArgs& args, + double isoYear, double isoMonth, + double isoDay, + Handle<CalendarValue> calendar) { + MOZ_ASSERT(IsInteger(isoYear)); + MOZ_ASSERT(IsInteger(isoMonth)); + MOZ_ASSERT(IsInteger(isoDay)); + + // Step 1. + if (!ThrowIfInvalidISODate(cx, isoYear, isoMonth, isoDay)) { + return nullptr; + } + + // Step 2. + if (!ISODateTimeWithinLimits(isoYear, isoMonth, isoDay)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return nullptr; + } + + // Steps 3-4. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PlainDate, + &proto)) { + return nullptr; + } + + auto* object = NewObjectWithClassProto<PlainDateObject>(cx, proto); + if (!object) { + return nullptr; + } + + // Step 5. + object->setFixedSlot(PlainDateObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 6. + object->setFixedSlot(PlainDateObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 7. + object->setFixedSlot(PlainDateObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 8. + object->setFixedSlot(PlainDateObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 9. + return object; +} + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +PlainDateObject* js::temporal::CreateTemporalDate( + JSContext* cx, const PlainDate& date, Handle<CalendarValue> calendar) { + auto& [isoYear, isoMonth, isoDay] = date; + + // Step 1. + if (!ThrowIfInvalidISODate(cx, date)) { + return nullptr; + } + + // Step 2. + if (!ISODateTimeWithinLimits(date)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return nullptr; + } + + // Steps 3-4. + auto* object = NewBuiltinClassInstance<PlainDateObject>(cx); + if (!object) { + return nullptr; + } + + // Step 5. + object->setFixedSlot(PlainDateObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 6. + object->setFixedSlot(PlainDateObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 7. + object->setFixedSlot(PlainDateObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 8. + object->setFixedSlot(PlainDateObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 9. + return object; +} + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +bool js::temporal::CreateTemporalDate( + JSContext* cx, const PlainDate& date, Handle<CalendarValue> calendar, + MutableHandle<PlainDateWithCalendar> result) { + // Step 1. + if (!ThrowIfInvalidISODate(cx, date)) { + return false; + } + + // Step 2. + if (!ISODateTimeWithinLimits(date)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + // Steps 3-9. + result.set(PlainDateWithCalendar{date, calendar}); + return true; +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +static Wrapped<PlainDateObject*> ToTemporalDate( + JSContext* cx, Handle<JSObject*> item, Handle<PlainObject*> maybeOptions) { + // Step 1-2. (Not applicable in our implementation.) + + // Step 3.a. + if (item->canUnwrapAs<PlainDateObject>()) { + return item; + } + + // Step 3.b. + if (auto* zonedDateTime = item->maybeUnwrapIf<ZonedDateTimeObject>()) { + auto epochInstant = ToInstant(zonedDateTime); + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar()); + + if (!timeZone.wrap(cx)) { + return nullptr; + } + if (!calendar.wrap(cx)) { + return nullptr; + } + + // Step 3.b.i. + if (maybeOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeOptions, &ignored)) { + return nullptr; + } + } + + // Steps 3.b.ii-iv. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, epochInstant, &dateTime)) { + return nullptr; + } + + // Step 3.b.v. + return CreateTemporalDate(cx, dateTime.date, calendar); + } + + // Step 3.c. + if (auto* dateTime = item->maybeUnwrapIf<PlainDateTimeObject>()) { + auto date = ToPlainDate(dateTime); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + if (!calendar.wrap(cx)) { + return nullptr; + } + + // Step 3.c.i. + if (maybeOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeOptions, &ignored)) { + return nullptr; + } + } + + // Step 3.c.ii. + return CreateTemporalDate(cx, date, calendar); + } + + // Step 3.d. + Rooted<CalendarValue> calendarValue(cx); + if (!GetTemporalCalendarWithISODefault(cx, item, &calendarValue)) { + return nullptr; + } + + // Step 3.e. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + }, + &calendar)) { + return nullptr; + } + + // Step 3.f. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return nullptr; + } + + // Step 3.g. + Rooted<PlainObject*> fields(cx, PrepareTemporalFields(cx, item, fieldNames)); + if (!fields) { + return nullptr; + } + + // Step 3.h. + if (maybeOptions) { + return temporal::CalendarDateFromFields(cx, calendar, fields, maybeOptions); + } + return temporal::CalendarDateFromFields(cx, calendar, fields); +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +static Wrapped<PlainDateObject*> ToTemporalDate( + JSContext* cx, Handle<Value> item, Handle<JSObject*> maybeOptions) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainObject*> maybeResolvedOptions(cx); + if (maybeOptions) { + maybeResolvedOptions = SnapshotOwnProperties(cx, maybeOptions); + if (!maybeResolvedOptions) { + return nullptr; + } + } + + // Step 3. + if (item.isObject()) { + Rooted<JSObject*> itemObj(cx, &item.toObject()); + return ::ToTemporalDate(cx, itemObj, maybeResolvedOptions); + } + + // Step 4. + if (!item.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, item, + nullptr, "not a string"); + return nullptr; + } + Rooted<JSString*> string(cx, item.toString()); + + // Step 5. + PlainDate result; + Rooted<JSString*> calendarString(cx); + if (!ParseTemporalDateString(cx, string, &result, &calendarString)) { + return nullptr; + } + + // Step 6. + MOZ_ASSERT(IsValidISODate(result)); + + // Steps 7-10. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendar)) { + return nullptr; + } + } + + // Step 11. + if (maybeResolvedOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return nullptr; + } + } + + // Step 12. + return CreateTemporalDate(cx, result, calendar); +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +static Wrapped<PlainDateObject*> ToTemporalDate(JSContext* cx, + Handle<Value> item) { + return ::ToTemporalDate(cx, item, nullptr); +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +bool js::temporal::ToTemporalDate(JSContext* cx, Handle<Value> item, + PlainDate* result) { + auto obj = ::ToTemporalDate(cx, item, nullptr); + if (!obj) { + return false; + } + + *result = ToPlainDate(&obj.unwrap()); + return true; +} + +/** + * ToTemporalDate ( item [ , options ] ) + */ +bool js::temporal::ToTemporalDate(JSContext* cx, Handle<Value> item, + MutableHandle<PlainDateWithCalendar> result) { + auto* obj = ::ToTemporalDate(cx, item, nullptr).unwrapOrNull(); + if (!obj) { + return false; + } + + auto date = ToPlainDate(obj); + Rooted<CalendarValue> calendar(cx, obj->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + result.set(PlainDateWithCalendar{date, calendar}); + return true; +} + +/** + * Mathematical Operations, "modulo" notation. + */ +static int32_t NonNegativeModulo(double x, int32_t y) { + MOZ_ASSERT(IsInteger(x)); + MOZ_ASSERT(y > 0); + + double r = std::fmod(x, y); + + int32_t result; + MOZ_ALWAYS_TRUE(mozilla::NumberEqualsInt32(r, &result)); + + return (result < 0) ? (result + y) : result; +} + +struct BalancedYearMonth final { + double year = 0; + int32_t month = 0; +}; + +/** + * BalanceISOYearMonth ( year, month ) + */ +static BalancedYearMonth BalanceISOYearMonth(double year, double month) { + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + + // Note: If either abs(year) or abs(month) is greater than 2^53 (the double + // integral precision limit), the additions resp. subtractions below are + // imprecise. This doesn't matter for us, because the single caller to this + // function (AddISODate) will throw an error for large values anyway. + + // Step 2. + year = year + std::floor((month - 1) / 12); + MOZ_ASSERT(IsInteger(year) || std::isinf(year)); + + // Step 3. + int32_t mon = NonNegativeModulo(month - 1, 12) + 1; + MOZ_ASSERT(1 <= mon && mon <= 12); + + // Step 4. + return {year, mon}; +} + +static bool CanBalanceISOYear(double year) { + // TODO: Export these values somewhere. + constexpr int32_t minYear = -271821; + constexpr int32_t maxYear = 275760; + + // If the year is below resp. above the min-/max-year, no value of |day| will + // make the resulting date valid. + return minYear <= year && year <= maxYear; +} + +static bool CanBalanceISODay(double day) { + // The maximum number of seconds from the epoch is 8.64 * 10^12. + constexpr int64_t maxInstantSeconds = 8'640'000'000'000; + + // In days that makes 10^8. + constexpr int64_t maxInstantDays = maxInstantSeconds / 60 / 60 / 24; + + // Multiply by two to take both directions into account and add twenty to + // account for the day number of the minimum date "-271821-02-20". + constexpr int64_t maximumDayDifference = 2 * maxInstantDays + 20; + + // When |day| is below |maximumDayDifference|, it can be represented as int32. + static_assert(maximumDayDifference <= INT32_MAX); + + // When the day difference exceeds the maximum valid day difference, the + // overall result won't be a valid date. Detect this early so we don't have to + // struggle with floating point precision issues in BalanceISODate. + // + // This also means BalanceISODate, step 1 doesn't apply to our implementation. + return std::abs(day) <= maximumDayDifference; +} + +/** + * BalanceISODate ( year, month, day ) + */ +PlainDate js::temporal::BalanceISODateNew(int32_t year, int32_t month, + int32_t day) { + MOZ_ASSERT(1 <= month && month <= 12); + + // Steps 1-3. + int64_t ms = MakeDate(year, month, day); + + // FIXME: spec issue - |ms| can be non-finite + // https://github.com/tc39/proposal-temporal/issues/2315 + + // TODO: This approach isn't efficient, because MonthFromTime and DayFromTime + // both recompute YearFromTime. + + // Step 4. + return {int32_t(JS::YearFromTime(ms)), int32_t(JS::MonthFromTime(ms) + 1), + int32_t(JS::DayFromTime(ms))}; +} + +/** + * BalanceISODate ( year, month, day ) + */ +bool js::temporal::BalanceISODate(JSContext* cx, int32_t year, int32_t month, + int64_t day, PlainDate* result) { + if (!CanBalanceISODay(day)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + *result = BalanceISODate(year, month, int32_t(day)); + return true; +} + +/** + * BalanceISODate ( year, month, day ) + */ +PlainDate js::temporal::BalanceISODate(int32_t year, int32_t month, + int32_t day) { + // Check no inputs can lead to floating point precision issues below. This + // also ensures all loops can finish in reasonable time, so we don't need to + // worry about interrupts here. And it ensures there won't be overflows when + // using int32_t values. + MOZ_ASSERT(CanBalanceISOYear(year)); + MOZ_ASSERT(1 <= month && month <= 12); + MOZ_ASSERT(CanBalanceISODay(day)); + + // TODO: BalanceISODate now works using MakeDate + // TODO: Can't use JS::MakeDate, because it expects valid month/day values. + // https://github.com/tc39/proposal-temporal/issues/2315 + + // Step 1. (Not applicable in our implementation.) + + // Steps 3-4. (Not applicable in our implementation.) + + constexpr int32_t daysInNonLeapYear = 365; + + // Skip steps 5-11 for the common case when abs(day) doesn't exceed 365. + if (std::abs(day) > daysInNonLeapYear) { + // Step 5. (Note) + + // Steps 6-7. + int32_t testYear = month > 2 ? year : year - 1; + + // Step 8. + while (day < -ISODaysInYear(testYear)) { + // Step 8.a. + day += ISODaysInYear(testYear); + + // Step 8.b. + year -= 1; + + // Step 8.c. + testYear -= 1; + } + + // Step 9. (Note) + + // Step 10. + testYear += 1; + + // Step 11. + while (day > ISODaysInYear(testYear)) { + // Step 11.a. + day -= ISODaysInYear(testYear); + + // Step 11.b. + year += 1; + + // Step 11.c. + testYear += 1; + } + } + + // Step 12. (Note) + + // Step 13. + while (day < 1) { + // Steps 13.a-b. (Inlined call to BalanceISOYearMonth.) + if (--month == 0) { + month = 12; + year -= 1; + } + + // Step 13.d + day += ISODaysInMonth(year, month); + } + + // Step 14. (Note) + + // Step 15. + while (day > ISODaysInMonth(year, month)) { + // Step 15.a. + day -= ISODaysInMonth(year, month); + + // Steps 15.b-d. (Inlined call to BalanceISOYearMonth.) + if (++month == 13) { + month = 1; + year += 1; + } + } + + MOZ_ASSERT(1 <= month && month <= 12); + MOZ_ASSERT(1 <= day && day <= 31); + + // Step 16. + return {year, month, day}; +} + +/** + * AddISODate ( year, month, day, years, months, weeks, days, overflow ) + */ +bool js::temporal::AddISODate(JSContext* cx, const PlainDate& date, + const Duration& duration, + TemporalOverflow overflow, PlainDate* result) { + MOZ_ASSERT(IsValidISODate(date)); + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + // TODO: Not quite sure if this holds for all callers. But if it does hold, + // then we can directly reject any numbers which can't be represented with + // int32_t. That in turn avoids the precision loss issue noted in + // BalanceISODate. + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 1. + MOZ_ASSERT(IsInteger(duration.years)); + MOZ_ASSERT(IsInteger(duration.months)); + MOZ_ASSERT(IsInteger(duration.weeks)); + MOZ_ASSERT(IsInteger(duration.days)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + auto yearMonth = BalanceISOYearMonth(date.year + duration.years, + date.month + duration.months); + MOZ_ASSERT(IsInteger(yearMonth.year) || std::isinf(yearMonth.year)); + MOZ_ASSERT(1 <= yearMonth.month && yearMonth.month <= 12); + + // FIXME: spec issue? + // new Temporal.PlainDate(2021, 5, 31).subtract({months:1, days:1}).toString() + // returns "2021-04-29", but "2021-04-30" seems more likely expected. + // Note: "2021-04-29" agrees with java.time, though. + // + // Example where this creates inconsistent results: + // + // clang-format off + // + // js> Temporal.PlainDate.from("2021-05-31").since("2021-04-30", {largestUnit:"months"}).toString() + // "P1M1D" + // js> Temporal.PlainDate.from("2021-05-31").subtract("P1M1D").toString() + // "2021-04-29" + // + // clang-format on + // + // Later: This now returns "P1M" instead "P1M1D", so the results are at least + // consistent. Let's add a test case for this behaviour. + // + // Revisit when <https://github.com/tc39/proposal-temporal/issues/2535> has + // been addressed. + + // |yearMonth.year| can only exceed the valid years range when called from + // `Temporal.Calendar.prototype.dateAdd`. And because `dateAdd` uses the + // result of AddISODate to create a new Temporal.PlainDate, we can directly + // throw an error if the result isn't within the valid date-time limits. This + // in turn allows to work on integer values and we don't have to worry about + // imprecise double value computations. + if (!CanBalanceISOYear(yearMonth.year)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + // Step 4. + PlainDate regulated; + if (!RegulateISODate(cx, {int32_t(yearMonth.year), yearMonth.month, date.day}, + overflow, ®ulated)) { + return false; + } + + // NB: BalanceISODate will reject too large days, so we don't have to worry + // about imprecise number arithmetic here. + + // Steps 5-6. + double d = regulated.day + (duration.days + duration.weeks * 7); + + // Just as with |yearMonth.year|, also directly throw an error if the |days| + // value is too large. + if (!CanBalanceISODay(d)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_INVALID); + return false; + } + + // Step 7. + auto balanced = BalanceISODate(regulated.year, regulated.month, int32_t(d)); + MOZ_ASSERT(IsValidISODate(balanced)); + + *result = balanced; + return true; +} + +struct YearMonthDuration { + int32_t years = 0; + int32_t months = 0; +}; + +/** + * AddISODate ( year, month, day, years, months, weeks, days, overflow ) + * + * With |overflow = "constrain"|. + */ +static PlainDate AddISODate(const PlainDate& date, + const YearMonthDuration& duration) { + MOZ_ASSERT(IsValidISODate(date)); + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + + MOZ_ASSERT_IF(duration.years < 0, duration.months <= 0); + MOZ_ASSERT_IF(duration.years > 0, duration.months >= 0); + + // TODO: Export these values somewhere. + [[maybe_unused]] constexpr int32_t minYear = -271821; + [[maybe_unused]] constexpr int32_t maxYear = 275760; + + MOZ_ASSERT(std::abs(duration.years) <= (maxYear - minYear), + "years doesn't exceed the maximum duration between valid years"); + MOZ_ASSERT(std::abs(duration.months) <= 12, + "months duration is at most one year"); + + // Steps 1-2. (Not applicable) + + // Step 3. (Inlined BalanceISOYearMonth) + int32_t year = date.year + duration.years; + int32_t month = date.month + duration.months; + MOZ_ASSERT(-11 <= month && month <= 24); + + if (month > 12) { + month -= 12; + year += 1; + } else if (month <= 0) { + month += 12; + year -= 1; + } + + MOZ_ASSERT(1 <= month && month <= 12); + MOZ_ASSERT(CanBalanceISOYear(year)); + + // Steps 4-7. + return ::ConstrainISODate({year, month, date.day}); +} + +static bool HasYearsMonthsOrWeeks(const Duration& duration) { + return duration.years != 0 || duration.months != 0 || duration.weeks != 0; +} + +static bool AddDate(JSContext* cx, const PlainDate& date, + const Duration& duration, Handle<JSObject*> maybeOptions, + PlainDate* result) { + MOZ_ASSERT(!HasYearsMonthsOrWeeks(duration)); + + // Steps 1-3. (Not applicable) + + // Step 4. + auto overflow = TemporalOverflow::Constrain; + if (maybeOptions) { + if (!ToTemporalOverflow(cx, maybeOptions, &overflow)) { + return false; + } + } + + // Step 5. + TimeDuration daysDuration; + if (!BalanceTimeDuration(cx, duration, TemporalUnit::Day, &daysDuration)) { + return false; + } + + // Step 6. + return AddISODate(cx, date, {0, 0, 0, daysDuration.days}, overflow, result); +} + +static bool AddDate(JSContext* cx, Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, Handle<JSObject*> maybeOptions, + PlainDate* result) { + auto* unwrappedDate = date.unwrap(cx); + if (!unwrappedDate) { + return false; + } + return ::AddDate(cx, ToPlainDate(unwrappedDate), duration, maybeOptions, + result); +} + +static PlainDateObject* AddDate(JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, + Handle<JSObject*> maybeOptions) { + // Steps 1-3. (Not applicable) + + // Steps 4-6. + PlainDate resultDate; + if (!::AddDate(cx, date, duration, maybeOptions, &resultDate)) { + return nullptr; + } + + // Step 7. + return CreateTemporalDate(cx, resultDate, calendar.receiver()); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, const Duration& duration, + Handle<JSObject*> options) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return temporal::CalendarDateAdd(cx, calendar, date, duration, options); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, options); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, const Duration& duration) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return CalendarDateAdd(cx, calendar, date, duration); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, nullptr); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> durationObj) { + auto* unwrappedDuration = durationObj.unwrap(cx); + if (!unwrappedDuration) { + return nullptr; + } + auto duration = ToDuration(unwrappedDuration); + + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return CalendarDateAdd(cx, calendar, date, durationObj); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, nullptr); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> js::temporal::AddDate( + JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + Handle<Wrapped<DurationObject*>> durationObj, Handle<JSObject*> options) { + auto* unwrappedDuration = durationObj.unwrap(cx); + if (!unwrappedDuration) { + return nullptr; + } + auto duration = ToDuration(unwrappedDuration); + + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return temporal::CalendarDateAdd(cx, calendar, date, durationObj, options); + } + + // Steps 4-7. + return ::AddDate(cx, calendar, date, duration, options); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +bool js::temporal::AddDate(JSContext* cx, Handle<CalendarRecord> calendar, + const PlainDate& date, const Duration& duration, + Handle<JSObject*> options, PlainDate* result) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return temporal::CalendarDateAdd(cx, calendar, date, duration, options, + result); + } + + // Steps 4-7. + return ::AddDate(cx, date, duration, options, result); +} + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +bool js::temporal::AddDate(JSContext* cx, Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, PlainDate* result) { + // Step 1. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (HasYearsMonthsOrWeeks(duration)) { + return CalendarDateAdd(cx, calendar, date, duration, result); + } + + // Steps 4-7. + return ::AddDate(cx, date, duration, nullptr, result); +} + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool js::temporal::DifferenceDate(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + Handle<PlainObject*> options, + Duration* result) { + auto* unwrappedOne = one.unwrap(cx); + if (!unwrappedOne) { + return false; + } + auto oneDate = ToPlainDate(unwrappedOne); + + auto* unwrappedTwo = two.unwrap(cx); + if (!unwrappedTwo) { + return false; + } + auto twoDate = ToPlainDate(unwrappedTwo); + + // Steps 1-2. (Not applicable in our implementation.) + + // Step 3. + MOZ_ASSERT(options->staticPrototype() == nullptr); + + // Step 4. + MOZ_ASSERT(options->containsPure(cx->names().largestUnit)); + + // Step 5. + if (oneDate == twoDate) { + *result = {}; + return true; + } + + // Step 6. + Rooted<JS::Value> largestUnit(cx); + if (!GetProperty(cx, options, options, cx->names().largestUnit, + &largestUnit)) { + return false; + } + + if (largestUnit.isString()) { + bool isDay; + if (!EqualStrings(cx, largestUnit.toString(), cx->names().day, &isDay)) { + return false; + } + + if (isDay) { + // Step 6.a. + int32_t days = DaysUntil(oneDate, twoDate); + + // Step 6.b. + *result = {0, 0, 0, double(days)}; + return true; + } + } + + // Step 7. + return CalendarDateUntil(cx, calendar, one, two, options, result); +} + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool js::temporal::DifferenceDate(JSContext* cx, + Handle<CalendarRecord> calendar, + Handle<Wrapped<PlainDateObject*>> one, + Handle<Wrapped<PlainDateObject*>> two, + TemporalUnit largestUnit, Duration* result) { + auto* unwrappedOne = one.unwrap(cx); + if (!unwrappedOne) { + return false; + } + auto oneDate = ToPlainDate(unwrappedOne); + + auto* unwrappedTwo = two.unwrap(cx); + if (!unwrappedTwo) { + return false; + } + auto twoDate = ToPlainDate(unwrappedTwo); + + // Steps 1-4. (Not applicable in our implementation.) + + // Step 5. + if (oneDate == twoDate) { + *result = {}; + return true; + } + + // Step 6. + if (largestUnit == TemporalUnit::Day) { + // Step 6.a. + int32_t days = DaysUntil(oneDate, twoDate); + + // Step 6.b. + *result = {0, 0, 0, double(days)}; + return true; + } + + // Step 7. + return CalendarDateUntil(cx, calendar, one, two, largestUnit, result); +} + +/** + * CompareISODate ( y1, m1, d1, y2, m2, d2 ) + */ +int32_t js::temporal::CompareISODate(const PlainDate& one, + const PlainDate& two) { + // Steps 1-2. + if (one.year != two.year) { + return one.year < two.year ? -1 : 1; + } + + // Steps 3-4. + if (one.month != two.month) { + return one.month < two.month ? -1 : 1; + } + + // Steps 5-6. + if (one.day != two.day) { + return one.day < two.day ? -1 : 1; + } + + // Step 7. + return 0; +} + +/** + * CreateDateDurationRecord ( years, months, weeks, days ) + */ +static DateDuration CreateDateDurationRecord(int32_t years, int32_t months, + int32_t weeks, int32_t days) { + MOZ_ASSERT(IsValidDuration( + {double(years), double(months), double(weeks), double(days)})); + return {double(years), double(months), double(weeks), double(days)}; +} + +/** + * DifferenceISODate ( y1, m1, d1, y2, m2, d2, largestUnit ) + */ +DateDuration js::temporal::DifferenceISODate(const PlainDate& start, + const PlainDate& end, + TemporalUnit largestUnit) { + // Steps 1-2. + MOZ_ASSERT(IsValidISODate(start)); + MOZ_ASSERT(IsValidISODate(end)); + + // Both inputs are also within the date-time limits. + MOZ_ASSERT(ISODateTimeWithinLimits(start)); + MOZ_ASSERT(ISODateTimeWithinLimits(end)); + + // Because both inputs are valid dates, we don't need to worry about integer + // overflow in any of the computations below. + + MOZ_ASSERT(TemporalUnit::Year <= largestUnit && + largestUnit <= TemporalUnit::Day); + + // Step 3. + if (largestUnit == TemporalUnit::Year || largestUnit == TemporalUnit::Month) { + // Step 3.a. + int32_t sign = -CompareISODate(start, end); + + // Step 3.b. + if (sign == 0) { + return CreateDateDurationRecord(0, 0, 0, 0); + } + + // FIXME: spec issue - results can be ambiguous, is this intentional? + // https://github.com/tc39/proposal-temporal/issues/2535 + // + // clang-format off + // js> var end = new Temporal.PlainDate(1970, 2, 28) + // js> var start = new Temporal.PlainDate(1970, 1, 28) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 29) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 30) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 31) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // + // Compare to java.time.temporal + // + // jshell> import java.time.LocalDate + // jshell> var end = LocalDate.of(1970, 2, 28) + // end ==> 1970-02-28 + // jshell> var start = LocalDate.of(1970, 1, 28) + // start ==> 1970-01-28 + // jshell> start.until(end) + // $27 ==> P1M + // jshell> var start = LocalDate.of(1970, 1, 29) + // start ==> 1970-01-29 + // jshell> start.until(end) + // $29 ==> P30D + // jshell> var start = LocalDate.of(1970, 1, 30) + // start ==> 1970-01-30 + // jshell> start.until(end) + // $31 ==> P29D + // jshell> var start = LocalDate.of(1970, 1, 31) + // start ==> 1970-01-31 + // jshell> start.until(end) + // $33 ==> P28D + // + // Also compare to: + // + // js> var end = new Temporal.PlainDate(1970, 2, 27) + // js> var start = new Temporal.PlainDate(1970, 1, 27) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P1M" + // js> var start = new Temporal.PlainDate(1970, 1, 28) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P30D" + // js> var start = new Temporal.PlainDate(1970, 1, 29) + // js> start.calendar.dateUntil(start, end, {largestUnit:"months"}).toString() + // "P29D" + // + // clang-format on + + // Steps 3.c-d. (Not applicable in our implementation.) + + // FIXME: spec issue - consistently use either |end.[[Year]]| or |y2|. + + // Step 3.e. + int32_t years = end.year - start.year; + + // TODO: We could inline this, because the AddISODate call is just a more + // complicated way to perform: + // mid = ConstrainISODate(end.year, start.month, start.day) + // + // The remaining computations can probably simplified similarily. + + // Step 3.f. + auto mid = ::AddISODate(start, {years, 0}); + + // Step 3.g. + int32_t midSign = -CompareISODate(mid, end); + + // Step 3.h. + if (midSign == 0) { + // Step 3.h.i. + if (largestUnit == TemporalUnit::Year) { + return CreateDateDurationRecord(years, 0, 0, 0); + } + + // Step 3.h.ii. + return CreateDateDurationRecord(0, years * 12, 0, 0); + } + + // Step 3.i. + int32_t months = end.month - start.month; + + // Step 3.j. + if (midSign != sign) { + // Step 3.j.i. + years -= sign; + + // Step 3.j.ii. + months += sign * 12; + } + + // Step 3.k. + mid = ::AddISODate(start, {years, months}); + + // Step 3.l. + midSign = -CompareISODate(mid, end); + + // Step 3.m. + if (midSign == 0) { + // Step 3.m.i. + if (largestUnit == TemporalUnit::Year) { + return CreateDateDurationRecord(years, months, 0, 0); + } + + // Step 3.m.ii. + return CreateDateDurationRecord(0, months + years * 12, 0, 0); + } + + // Step 3.n. + if (midSign != sign) { + // Step 3.n.i. + months -= sign; + + // Step 3.n.ii. + mid = ::AddISODate(start, {years, months}); + } + + // Steps 3.o-q. + int32_t days; + if (mid.month == end.month) { + MOZ_ASSERT(mid.year == end.year); + + days = end.day - mid.day; + } else if (sign < 0) { + days = -mid.day - (ISODaysInMonth(end.year, end.month) - end.day); + } else { + days = end.day + (ISODaysInMonth(mid.year, mid.month) - mid.day); + } + + // Step 3.r. + if (largestUnit == TemporalUnit::Month) { + // Step 3.r.i. + months += years * 12; + + // Step 3.r.ii. + years = 0; + } + + // Step 3.s. + return CreateDateDurationRecord(years, months, 0, days); + } + + // Step 4.a. + MOZ_ASSERT(largestUnit == TemporalUnit::Week || + largestUnit == TemporalUnit::Day); + + // Step 4.b. + int32_t epochDaysStart = MakeDay(start); + + // Step 4.c. + int32_t epochDaysEnd = MakeDay(end); + + // Step 4.d. + int32_t days = epochDaysEnd - epochDaysStart; + + // Step 4.e. + int32_t weeks = 0; + + // Step 4.f. + if (largestUnit == TemporalUnit::Week) { + // Step 4.f.i + weeks = days / 7; + + // Step 4.f.ii. + days = days % 7; + } + + // Step 4.g. + return CreateDateDurationRecord(0, 0, weeks, days); +} + +/** + * DifferenceTemporalPlainDate ( operation, temporalDate, other, options ) + */ +static bool DifferenceTemporalPlainDate(JSContext* cx, + TemporalDifference operation, + const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 1. (Not applicable in our implementation) + + // Step 2. + auto wrappedOther = ::ToTemporalDate(cx, args.get(0)); + if (!wrappedOther) { + return false; + } + auto* unwrappedOther = &wrappedOther.unwrap(); + auto otherDate = ToPlainDate(unwrappedOther); + + Rooted<Wrapped<PlainDateObject*>> other(cx, wrappedOther); + Rooted<CalendarValue> otherCalendar(cx, unwrappedOther->calendar()); + if (!otherCalendar.wrap(cx)) { + return false; + } + + // Step 3. + if (!CalendarEqualsOrThrow(cx, calendarValue, otherCalendar)) { + return false; + } + + // Steps 4-5. + DifferenceSettings settings; + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", ToName(operation), args[1])); + if (!options) { + return false; + } + + // Step 4. + resolvedOptions = SnapshotOwnProperties(cx, options); + if (!resolvedOptions) { + return false; + } + + // Step 5. + if (!GetDifferenceSettings(cx, operation, resolvedOptions, + TemporalUnitGroup::Date, TemporalUnit::Day, + TemporalUnit::Day, &settings)) { + return false; + } + } else { + // Steps 4-5. + settings = { + TemporalUnit::Day, + TemporalUnit::Day, + TemporalRoundingMode::Trunc, + Increment{1}, + }; + } + + // Step 6. + if (ToPlainDate(temporalDate) == otherDate) { + auto* obj = CreateTemporalDuration(cx, {}); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 7. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateAdd, + CalendarMethod::DateUntil, + }, + &calendar)) { + return false; + } + + // Steps 8-9. + Duration duration; + if (resolvedOptions) { + // Step 8. + Rooted<Value> largestUnitValue( + cx, StringValue(TemporalUnitToString(cx, settings.largestUnit))); + if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit, + largestUnitValue)) { + return false; + } + + // Step 9. + Duration result; + if (!DifferenceDate(cx, calendar, temporalDate, other, resolvedOptions, + &result)) { + return false; + } + duration = result.date(); + } else { + // Steps 8-9. + Duration result; + if (!DifferenceDate(cx, calendar, temporalDate, other, settings.largestUnit, + &result)) { + return false; + } + duration = result.date(); + } + + // Step 10. + bool roundingGranularityIsNoop = settings.smallestUnit == TemporalUnit::Day && + settings.roundingIncrement == Increment{1}; + + // Step 11. + if (!roundingGranularityIsNoop) { + // Steps 11.a-b. + Duration roundResult; + if (!temporal::RoundDuration(cx, duration.date(), + settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + temporalDate, calendar, &roundResult)) { + return false; + } + + // Step 11.c. + DateDuration balanceResult; + if (!temporal::BalanceDateDurationRelative( + cx, roundResult.date(), settings.largestUnit, settings.smallestUnit, + temporalDate, calendar, &balanceResult)) { + return false; + } + duration = balanceResult.toDuration(); + } + + // Step 12. + if (operation == TemporalDifference::Since) { + duration = duration.negate(); + } + + auto* obj = CreateTemporalDuration(cx, duration.date()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate ( isoYear, isoMonth, isoDay [ , calendarLike ] ) + */ +static bool PlainDateConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.PlainDate")) { + return false; + } + + // Step 2. + double isoYear; + if (!ToIntegerWithTruncation(cx, args.get(0), "year", &isoYear)) { + return false; + } + + // Step 3. + double isoMonth; + if (!ToIntegerWithTruncation(cx, args.get(1), "month", &isoMonth)) { + return false; + } + + // Step 4. + double isoDay; + if (!ToIntegerWithTruncation(cx, args.get(2), "day", &isoDay)) { + return false; + } + + // Step 5. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendarWithISODefault(cx, args.get(3), &calendar)) { + return false; + } + + // Step 6. + auto* temporalDate = + CreateTemporalDate(cx, args, isoYear, isoMonth, isoDay, calendar); + if (!temporalDate) { + return false; + } + + args.rval().setObject(*temporalDate); + return true; +} + +/** + * Temporal.PlainDate.from ( item [ , options ] ) + */ +static bool PlainDate_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "from", args[1]); + if (!options) { + return false; + } + } + + // Step 2. + if (args.get(0).isObject()) { + JSObject* item = &args[0].toObject(); + if (auto* temporalDate = item->maybeUnwrapIf<PlainDateObject>()) { + auto date = ToPlainDate(temporalDate); + + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + if (options) { + // Step 2.a. + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, options, &ignored)) { + return false; + } + } + + // Step 2.b. + auto* result = CreateTemporalDate(cx, date, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; + } + } + + // Step 3. + auto result = ToTemporalDate(cx, args.get(0), options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.compare ( one, two ) + */ +static bool PlainDate_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + PlainDate one; + if (!ToTemporalDate(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + PlainDate two; + if (!ToTemporalDate(cx, args.get(1), &two)) { + return false; + } + + // Step 3. + args.rval().setInt32(CompareISODate(one, two)); + return true; +} + +/** + * get Temporal.PlainDate.prototype.calendarId + */ +static bool PlainDate_calendarId(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 3. + auto* calendarId = ToTemporalCalendarIdentifier(cx, calendar); + if (!calendarId) { + return false; + } + + args.rval().setString(calendarId); + return true; +} + +/** + * get Temporal.PlainDate.prototype.calendarId + */ +static bool PlainDate_calendarId(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_calendarId>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.year + */ +static bool PlainDate_year(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.year + */ +static bool PlainDate_year(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_year>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.month + */ +static bool PlainDate_month(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarMonth(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.month + */ +static bool PlainDate_month(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_month>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.monthCode + */ +static bool PlainDate_monthCode(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarMonthCode(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.monthCode + */ +static bool PlainDate_monthCode(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_monthCode>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.day + */ +static bool PlainDate_day(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDay(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.day + */ +static bool PlainDate_day(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_day>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.dayOfWeek + */ +static bool PlainDate_dayOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDayOfWeek(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.dayOfWeek + */ +static bool PlainDate_dayOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_dayOfWeek>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.dayOfYear + */ +static bool PlainDate_dayOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDayOfYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.dayOfYear + */ +static bool PlainDate_dayOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_dayOfYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.weekOfYear + */ +static bool PlainDate_weekOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarWeekOfYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.weekOfYear + */ +static bool PlainDate_weekOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_weekOfYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.yearOfWeek + */ +static bool PlainDate_yearOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarYearOfWeek(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.yearOfWeek + */ +static bool PlainDate_yearOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_yearOfWeek>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.daysInWeek + */ +static bool PlainDate_daysInWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDaysInWeek(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.daysInWeek + */ +static bool PlainDate_daysInWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_daysInWeek>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.daysInMonth + */ +static bool PlainDate_daysInMonth(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDaysInMonth(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.daysInMonth + */ +static bool PlainDate_daysInMonth(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_daysInMonth>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.daysInYear + */ +static bool PlainDate_daysInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarDaysInYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.daysInYear + */ +static bool PlainDate_daysInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_daysInYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.monthsInYear + */ +static bool PlainDate_monthsInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarMonthsInYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.monthsInYear + */ +static bool PlainDate_monthsInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_monthsInYear>(cx, args); +} + +/** + * get Temporal.PlainDate.prototype.inLeapYear + */ +static bool PlainDate_inLeapYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 4. + return CalendarInLeapYear(cx, calendar, temporalDate, args.rval()); +} + +/** + * get Temporal.PlainDate.prototype.inLeapYear + */ +static bool PlainDate_inLeapYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_inLeapYear>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toPlainYearMonth ( ) + */ +static bool PlainDate_toPlainYearMonth(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return false; + } + + // Step 4. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDate, fieldNames)); + if (!fields) { + return false; + } + + // Step 6. + auto obj = CalendarYearMonthFromFields(cx, calendar, fields); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.toPlainYearMonth ( ) + */ +static bool PlainDate_toPlainYearMonth(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toPlainYearMonth>(cx, + args); +} + +/** + * Temporal.PlainDate.prototype.toPlainMonthDay ( ) + */ +static bool PlainDate_toPlainMonthDay(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Example for the optimisation described in TemporalFields.cpp + + // Optimization for built-in objects. + do { + // Step 4. + static constexpr std::initializer_list<CalendarField> fieldNames = { + CalendarField::Day, CalendarField::MonthCode}; + + // Step 5. + if (calendarValue.isObject()) { + Rooted<JSObject*> calendarObj(cx, calendarValue.toObject()); + if (!calendarObj->is<CalendarObject>()) { + break; + } + auto builtinCalendar = calendarObj.as<CalendarObject>(); + + // Step 5. + if (!IsBuiltinAccess(cx, builtinCalendar, fieldNames)) { + break; + } + } + if (!IsBuiltinAccess(cx, temporalDate, fieldNames)) { + break; + } + + // Step 6. + auto date = ToPlainDate(temporalDate); + auto result = PlainDate{1972 /* referenceISOYear */, date.month, date.day}; + + auto* obj = CreateTemporalMonthDay(cx, result, calendarValue); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } while (false); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::MonthDayFromFields, + }, + &calendar)) { + return false; + } + + // Step 4. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::MonthCode}, + &fieldNames)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDate, fieldNames)); + if (!fields) { + return false; + } + + // Steps 6-7. + auto obj = CalendarMonthDayFromFields(cx, calendar, fields); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.toPlainMonthDay ( ) + */ +static bool PlainDate_toPlainMonthDay(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toPlainMonthDay>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toPlainDateTime ( [ temporalTime ] ) + */ +static bool PlainDate_toPlainDateTime(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Default initialize the time component to all zero. + PlainDateTime dateTime = {ToPlainDate(temporalDate), {}}; + + // Step 4. (Reordered) + if (args.hasDefined(0)) { + if (!ToTemporalTime(cx, args[0], &dateTime.time)) { + return false; + } + } + + // Steps 3 and 5. + auto* obj = CreateTemporalDateTime(cx, dateTime, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.toPlainDateTime ( [ temporalTime ] ) + */ +static bool PlainDate_toPlainDateTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toPlainDateTime>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.getISOFields ( ) + */ +static bool PlainDate_getISOFields(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + auto calendar = temporalDate->calendar(); + + // Step 3. + Rooted<IdValueVector> fields(cx, IdValueVector(cx)); + + // Step 4. + if (!fields.emplaceBack(NameToId(cx->names().calendar), calendar.toValue())) { + return false; + } + + // Step 5. + if (!fields.emplaceBack(NameToId(cx->names().isoDay), Int32Value(date.day))) { + return false; + } + + // Step 6. + if (!fields.emplaceBack(NameToId(cx->names().isoMonth), + Int32Value(date.month))) { + return false; + } + + // Step 7. + if (!fields.emplaceBack(NameToId(cx->names().isoYear), + Int32Value(date.year))) { + return false; + } + + // Step 8. + auto* obj = + NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.getISOFields ( ) + */ +static bool PlainDate_getISOFields(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_getISOFields>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.getCalendar ( ) + */ +static bool PlainDate_getCalendar(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 3. + auto* obj = ToTemporalCalendarObject(cx, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDate.prototype.getCalendar ( ) + */ +static bool PlainDate_getCalendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_getCalendar>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_add(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 3. + Rooted<Wrapped<DurationObject*>> duration( + cx, ToTemporalDuration(cx, args.get(0))); + if (!duration) { + return false; + } + + // Step 4. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "add", args[1]); + } else { + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { + return false; + } + + // Step 5. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateAdd, + }, + &calendar)) { + return false; + } + + // Step 6. + auto result = AddDate(cx, calendar, temporalDate, duration, options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_add>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.subtract ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_subtract(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + + // Step 3. + Duration duration; + if (!ToTemporalDuration(cx, args.get(0), &duration)) { + return false; + } + + // Step 4. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "subtract", args[1]); + } else { + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { + return false; + } + + // Step 5. + auto negatedDuration = duration.negate(); + + // Step 6. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateAdd, + }, + &calendar)) { + return false; + } + + // Step 7. + auto result = + temporal::AddDate(cx, calendar, temporalDate, negatedDuration, options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.subtract ( temporalDurationLike [ , options ] ) + */ +static bool PlainDate_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_subtract>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.with ( temporalDateLike [ , options ] ) + */ +static bool PlainDate_with(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + // Step 3. + Rooted<JSObject*> temporalDateLike( + cx, RequireObjectArg(cx, "temporalDateLike", "with", args.get(0))); + if (!temporalDateLike) { + return false; + } + + // Step 4. + if (!RejectTemporalLikeObject(cx, temporalDateLike)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "with", args[1])); + if (!options) { + return false; + } + resolvedOptions = SnapshotOwnProperties(cx, options); + } else { + resolvedOptions = NewPlainObjectWithProto(cx, nullptr); + } + if (!resolvedOptions) { + return false; + } + + // Step 6. + Rooted<CalendarValue> calendarValue(cx, temporalDate->calendar()); + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + CalendarMethod::MergeFields, + }, + &calendar)) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDate, fieldNames)); + if (!fields) { + return false; + } + + // Step 9. + Rooted<PlainObject*> partialDate( + cx, PreparePartialTemporalFields(cx, temporalDateLike, fieldNames)); + if (!partialDate) { + return false; + } + + // Step 10. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, partialDate)); + if (!mergedFields) { + return false; + } + + // Step 11. + fields = PrepareTemporalFields(cx, mergedFields, fieldNames); + if (!fields) { + return false; + } + + // Step 12. + auto result = + temporal::CalendarDateFromFields(cx, calendar, fields, resolvedOptions); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.with ( temporalDateLike [ , options ] ) + */ +static bool PlainDate_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_with>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.withCalendar ( calendar ) + */ +static bool PlainDate_withCalendar(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + + // Step 3. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // Step 4. + auto* result = CreateTemporalDate(cx, date, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.withCalendar ( calendar ) + */ +static bool PlainDate_withCalendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_withCalendar>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.until ( other [ , options ] ) + */ +static bool PlainDate_until(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainDate(cx, TemporalDifference::Until, args); +} + +/** + * Temporal.PlainDate.prototype.until ( other [ , options ] ) + */ +static bool PlainDate_until(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_until>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.since ( other [ , options ] ) + */ +static bool PlainDate_since(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainDate(cx, TemporalDifference::Since, args); +} + +/** + * Temporal.PlainDate.prototype.since ( other [ , options ] ) + */ +static bool PlainDate_since(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_since>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.equals ( other ) + */ +static bool PlainDate_equals(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Step 3. + Rooted<PlainDateWithCalendar> other(cx); + if (!ToTemporalDate(cx, args.get(0), &other)) { + return false; + } + + // Steps 4-7. + bool equals = date == other.date(); + if (equals && !CalendarEquals(cx, calendar, other.calendar(), &equals)) { + return false; + } + + args.rval().setBoolean(equals); + return true; +} + +/** + * Temporal.PlainDate.prototype.equals ( other ) + */ +static bool PlainDate_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_equals>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toZonedDateTime ( item ) + * + * The |item| argument represents either a time zone or an options object. The + * following cases are supported: + * - |item| is a `Temporal.TimeZone` object. + * - |item| is a user-defined time zone object. + * - |item| is an options object with `timeZone` and `plainTime` properties. + * - |item| is a time zone identifier string. + * + * User-defined time zone objects are distinguished from options objects by the + * `timeZone` property, i.e. if a `timeZone` property is present, the object is + * treated as an options object, otherwise an object is treated as a + * user-defined time zone. + */ +static bool PlainDate_toZonedDateTime(JSContext* cx, const CallArgs& args) { + auto* temporalDate = &args.thisv().toObject().as<PlainDateObject>(); + auto date = ToPlainDate(temporalDate); + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + + // Steps 3-4 + Rooted<TimeZoneValue> timeZone(cx); + Rooted<Value> temporalTime(cx); + if (args.get(0).isObject()) { + Rooted<JSObject*> item(cx, &args[0].toObject()); + + // Steps 3.a-b. + if (item->canUnwrapAs<TimeZoneObject>()) { + // Step 3.a.i. + timeZone.set(TimeZoneValue(item)); + + // Step 3.a.ii. + temporalTime.setUndefined(); + } else { + // Step 3.b.i. + Rooted<Value> timeZoneLike(cx); + if (!GetProperty(cx, item, item, cx->names().timeZone, &timeZoneLike)) { + return false; + } + + // Steps 3.b.ii-iii. + if (timeZoneLike.isUndefined()) { + // Step 3.b.ii.1. + if (!ToTemporalTimeZone(cx, args[0], &timeZone)) { + return false; + } + + // Step 3.b.ii.2. + temporalTime.setUndefined(); + } else { + // Step 3.b.iii.1. + if (!ToTemporalTimeZone(cx, timeZoneLike, &timeZone)) { + return false; + } + + // Step 3.b.iii.2. + if (!GetProperty(cx, item, item, cx->names().plainTime, + &temporalTime)) { + return false; + } + } + } + } else { + // Step 4.a. + if (!ToTemporalTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // Step 4.b. + temporalTime.setUndefined(); + } + + // Step 6.a. + PlainTime time = {}; + if (!temporalTime.isUndefined()) { + if (!ToTemporalTime(cx, temporalTime, &time)) { + return false; + } + } + + // Steps 5.a and 6.b + Rooted<PlainDateTimeWithCalendar> temporalDateTime(cx); + if (!CreateTemporalDateTime(cx, {date, time}, calendar, &temporalDateTime)) { + return false; + } + + // Steps 7-8. + Instant instant; + if (!GetInstantFor(cx, timeZone, temporalDateTime, + TemporalDisambiguation::Compatible, &instant)) { + return false; + } + + // Step 9. + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDate.prototype.toZonedDateTime ( item ) + */ +static bool PlainDate_toZonedDateTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toZonedDateTime>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toString ( [ options ] ) + */ +static bool PlainDate_toString(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + auto showCalendar = CalendarOption::Auto; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Step 4. + if (!ToCalendarNameOption(cx, options, &showCalendar)) { + return false; + } + } + + // Step 5. + JSString* str = TemporalDateToString(cx, temporalDate, showCalendar); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDate.prototype.toString ( [ options ] ) + */ +static bool PlainDate_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toString>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainDate_toLocaleString(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + // Step 3. + JSString* str = TemporalDateToString(cx, temporalDate, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDate.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainDate_toLocaleString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toLocaleString>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.toJSON ( ) + */ +static bool PlainDate_toJSON(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateObject*> temporalDate( + cx, &args.thisv().toObject().as<PlainDateObject>()); + + // Step 3. + JSString* str = TemporalDateToString(cx, temporalDate, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDate.prototype.toJSON ( ) + */ +static bool PlainDate_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDate, PlainDate_toJSON>(cx, args); +} + +/** + * Temporal.PlainDate.prototype.valueOf ( ) + */ +static bool PlainDate_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "PlainDate", "primitive type"); + return false; +} + +const JSClass PlainDateObject::class_ = { + "Temporal.PlainDate", + JSCLASS_HAS_RESERVED_SLOTS(PlainDateObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_PlainDate), + JS_NULL_CLASS_OPS, + &PlainDateObject::classSpec_, +}; + +const JSClass& PlainDateObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec PlainDate_methods[] = { + JS_FN("from", PlainDate_from, 1, 0), + JS_FN("compare", PlainDate_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec PlainDate_prototype_methods[] = { + JS_FN("toPlainMonthDay", PlainDate_toPlainMonthDay, 0, 0), + JS_FN("toPlainYearMonth", PlainDate_toPlainYearMonth, 0, 0), + JS_FN("toPlainDateTime", PlainDate_toPlainDateTime, 0, 0), + JS_FN("getISOFields", PlainDate_getISOFields, 0, 0), + JS_FN("getCalendar", PlainDate_getCalendar, 0, 0), + JS_FN("add", PlainDate_add, 1, 0), + JS_FN("subtract", PlainDate_subtract, 1, 0), + JS_FN("with", PlainDate_with, 1, 0), + JS_FN("withCalendar", PlainDate_withCalendar, 1, 0), + JS_FN("until", PlainDate_until, 1, 0), + JS_FN("since", PlainDate_since, 1, 0), + JS_FN("equals", PlainDate_equals, 1, 0), + JS_FN("toZonedDateTime", PlainDate_toZonedDateTime, 1, 0), + JS_FN("toString", PlainDate_toString, 0, 0), + JS_FN("toLocaleString", PlainDate_toLocaleString, 0, 0), + JS_FN("toJSON", PlainDate_toJSON, 0, 0), + JS_FN("valueOf", PlainDate_valueOf, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec PlainDate_prototype_properties[] = { + JS_PSG("calendarId", PlainDate_calendarId, 0), + JS_PSG("year", PlainDate_year, 0), + JS_PSG("month", PlainDate_month, 0), + JS_PSG("monthCode", PlainDate_monthCode, 0), + JS_PSG("day", PlainDate_day, 0), + JS_PSG("dayOfWeek", PlainDate_dayOfWeek, 0), + JS_PSG("dayOfYear", PlainDate_dayOfYear, 0), + JS_PSG("weekOfYear", PlainDate_weekOfYear, 0), + JS_PSG("yearOfWeek", PlainDate_yearOfWeek, 0), + JS_PSG("daysInWeek", PlainDate_daysInWeek, 0), + JS_PSG("daysInMonth", PlainDate_daysInMonth, 0), + JS_PSG("daysInYear", PlainDate_daysInYear, 0), + JS_PSG("monthsInYear", PlainDate_monthsInYear, 0), + JS_PSG("inLeapYear", PlainDate_inLeapYear, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.PlainDate", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec PlainDateObject::classSpec_ = { + GenericCreateConstructor<PlainDateConstructor, 3, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PlainDateObject>, + PlainDate_methods, + nullptr, + PlainDate_prototype_methods, + PlainDate_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; + +struct PlainDateNameAndNative final { + PropertyName* name; + JSNative native; +}; + +static PlainDateNameAndNative GetPlainDateNameAndNative( + JSContext* cx, CalendarField fieldName) { + switch (fieldName) { + case CalendarField::Year: + return {cx->names().year, PlainDate_year}; + case CalendarField::Month: + return {cx->names().month, PlainDate_month}; + case CalendarField::MonthCode: + return {cx->names().monthCode, PlainDate_monthCode}; + case CalendarField::Day: + return {cx->names().day, PlainDate_day}; + } + MOZ_CRASH("invalid temporal field name"); +} + +bool js::temporal::IsBuiltinAccess( + JSContext* cx, Handle<PlainDateObject*> date, + std::initializer_list<CalendarField> fieldNames) { + // Don't optimize when the object has any own properties which may shadow the + // built-in methods. + if (date->shape()->propMapLength() > 0) { + return false; + } + + JSObject* proto = cx->global()->maybeGetPrototype(JSProto_PlainDate); + + // Don't attempt to optimize when the class isn't yet initialized. + if (!proto) { + return false; + } + + // Don't optimize when the prototype isn't the built-in prototype. + if (date->staticPrototype() != proto) { + return false; + } + + auto* nproto = &proto->as<NativeObject>(); + for (auto fieldName : fieldNames) { + auto [name, native] = GetPlainDateNameAndNative(cx, fieldName); + auto prop = nproto->lookupPure(name); + + // Return if the property isn't a data property. + if (!prop || !prop->isDataProperty()) { + return false; + } + + // Return if the property isn't the initial method. + if (!IsNativeFunction(nproto->getSlot(prop->slot()), native)) { + return false; + } + } + + // Success! The access can be optimized. + return true; +} diff --git a/js/src/builtin/temporal/PlainDate.h b/js/src/builtin/temporal/PlainDate.h new file mode 100644 index 0000000000..75a3a3f2a1 --- /dev/null +++ b/js/src/builtin/temporal/PlainDate.h @@ -0,0 +1,292 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_PlainDate_h +#define builtin_temporal_PlainDate_h + +#include "mozilla/Assertions.h" + +#include <initializer_list> +#include <stdint.h> + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/Wrapped.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +class JS_PUBLIC_API JSTracer; + +namespace js { +struct ClassSpec; +class PlainObject; +} // namespace js + +namespace js::temporal { + +class PlainDateObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + // TODO: Consider compacting fields to reduce object size. + // + // ceil(log2(271821)) + ceil(log2(12)) + ceil(log2(31)) = 28 bits are + // needed to store a date value in a single int32. + + static constexpr uint32_t ISO_YEAR_SLOT = 0; + static constexpr uint32_t ISO_MONTH_SLOT = 1; + static constexpr uint32_t ISO_DAY_SLOT = 2; + static constexpr uint32_t CALENDAR_SLOT = 3; + static constexpr uint32_t SLOT_COUNT = 4; + + int32_t isoYear() const { return getFixedSlot(ISO_YEAR_SLOT).toInt32(); } + + int32_t isoMonth() const { return getFixedSlot(ISO_MONTH_SLOT).toInt32(); } + + int32_t isoDay() const { return getFixedSlot(ISO_DAY_SLOT).toInt32(); } + + CalendarValue calendar() const { + return CalendarValue(getFixedSlot(CALENDAR_SLOT)); + } + + private: + static const ClassSpec classSpec_; +}; + +class PlainDateWithCalendar { + PlainDate date_; + CalendarValue calendar_; + + public: + PlainDateWithCalendar() = default; + + PlainDateWithCalendar(const PlainDate& date, const CalendarValue& calendar) + : date_(date), calendar_(calendar) { + MOZ_ASSERT(ISODateTimeWithinLimits(date)); + } + + const auto& date() const { return date_; } + const auto& calendar() const { return calendar_; } + + // Allow implicit conversion to a calendar-less PlainDate. + operator const PlainDate&() const { return date(); } + + void trace(JSTracer* trc) { calendar_.trace(trc); } + + const auto* calendarDoNotUse() const { return &calendar_; } +}; + +/** + * Extract the date fields from the PlainDate object. + */ +inline PlainDate ToPlainDate(const PlainDateObject* date) { + return {date->isoYear(), date->isoMonth(), date->isoDay()}; +} + +enum class TemporalOverflow; +enum class TemporalUnit; +class DurationObject; +class ZonedDateTimeObject; + +#ifdef DEBUG +/** + * IsValidISODate ( year, month, day ) + */ +bool IsValidISODate(const PlainDate& date); + +/** + * IsValidISODate ( year, month, day ) + */ +bool IsValidISODate(double year, double month, double day); +#endif + +/** + * IsValidISODate ( year, month, day ) + */ +bool ThrowIfInvalidISODate(JSContext* cx, const PlainDate& date); + +/** + * IsValidISODate ( year, month, day ) + */ +bool ThrowIfInvalidISODate(JSContext* cx, double year, double month, + double day); + +/** + * ToTemporalDate ( item [ , options ] ) + */ +bool ToTemporalDate(JSContext* cx, JS::Handle<JS::Value> item, + PlainDate* result); + +/** + * ToTemporalDate ( item [ , options ] ) + */ +bool ToTemporalDate(JSContext* cx, JS::Handle<JS::Value> item, + JS::MutableHandle<PlainDateWithCalendar> result); + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +PlainDateObject* CreateTemporalDate(JSContext* cx, const PlainDate& date, + JS::Handle<CalendarValue> calendar); + +/** + * CreateTemporalDate ( isoYear, isoMonth, isoDay, calendar [ , newTarget ] ) + */ +bool CreateTemporalDate(JSContext* cx, const PlainDate& date, + JS::Handle<CalendarValue> calendar, + JS::MutableHandle<PlainDateWithCalendar> result); + +/** + * RegulateISODate ( year, month, day, overflow ) + */ +bool RegulateISODate(JSContext* cx, const PlainDate& date, + TemporalOverflow overflow, PlainDate* result); + +struct RegulatedISODate final { + double year; + int32_t month; + int32_t day; +}; + +/** + * RegulateISODate ( year, month, day, overflow ) + */ +bool RegulateISODate(JSContext* cx, double year, double month, double day, + TemporalOverflow overflow, RegulatedISODate* result); + +/** + * AddISODate ( year, month, day, years, months, weeks, days, overflow ) + */ +bool AddISODate(JSContext* cx, const PlainDate& date, const Duration& duration, + TemporalOverflow overflow, PlainDate* result); + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> AddDate(JSContext* cx, + JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, + JS::Handle<JSObject*> options); + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> AddDate(JSContext* cx, + JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration); + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> AddDate( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + JS::Handle<Wrapped<DurationObject*>> durationObj, + JS::Handle<JSObject*> options); + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +Wrapped<PlainDateObject*> AddDate( + JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + JS::Handle<Wrapped<DurationObject*>> durationObj); + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +bool AddDate(JSContext* cx, JS::Handle<CalendarRecord> calendar, + const PlainDate& date, const Duration& duration, + JS::Handle<JSObject*> options, PlainDate* result); + +/** + * AddDate ( calendarRec, plainDate, duration [ , options ] ) + */ +bool AddDate(JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> date, + const Duration& duration, PlainDate* result); + +/** + * DifferenceISODate ( y1, m1, d1, y2, m2, d2, largestUnit ) + */ +DateDuration DifferenceISODate(const PlainDate& start, const PlainDate& end, + TemporalUnit largestUnit); + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool DifferenceDate(JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> one, + JS::Handle<Wrapped<PlainDateObject*>> two, + JS::Handle<PlainObject*> options, Duration* result); + +/** + * DifferenceDate ( calendarRec, one, two, options ) + */ +bool DifferenceDate(JSContext* cx, JS::Handle<CalendarRecord> calendar, + JS::Handle<Wrapped<PlainDateObject*>> one, + JS::Handle<Wrapped<PlainDateObject*>> two, + TemporalUnit largestUnit, Duration* result); + +/** + * CompareISODate ( y1, m1, d1, y2, m2, d2 ) + */ +int32_t CompareISODate(const PlainDate& one, const PlainDate& two); + +/** + * BalanceISODate ( year, month, day ) + */ +bool BalanceISODate(JSContext* cx, int32_t year, int32_t month, int64_t day, + PlainDate* result); + +/** + * BalanceISODate ( year, month, day ) + */ +PlainDate BalanceISODate(int32_t year, int32_t month, int32_t day); + +/** + * BalanceISODate ( year, month, day ) + */ +PlainDate BalanceISODateNew(int32_t year, int32_t month, int32_t day); + +/** + * Return true when accessing the calendar fields |fieldNames| can be optimized. + * Otherwise returns false. + */ +bool IsBuiltinAccess(JSContext* cx, JS::Handle<PlainDateObject*> date, + std::initializer_list<CalendarField> fieldNames); + +} /* namespace js::temporal */ + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<temporal::PlainDateWithCalendar, Wrapper> { + const auto& container() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + const auto& date() const { return container().date(); } + + JS::Handle<temporal::CalendarValue> calendar() const { + return JS::Handle<temporal::CalendarValue>::fromMarkedLocation( + container().calendarDoNotUse()); + } + + // Allow implicit conversion to a calendar-less PlainDate. + operator const temporal::PlainDate&() const { return date(); } +}; + +} // namespace js + +#endif /* builtin_temporal_PlainDate_h */ diff --git a/js/src/builtin/temporal/PlainDateTime.cpp b/js/src/builtin/temporal/PlainDateTime.cpp new file mode 100644 index 0000000000..8861b484bd --- /dev/null +++ b/js/src/builtin/temporal/PlainDateTime.cpp @@ -0,0 +1,2856 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/PlainDateTime.h" + +#include "mozilla/Assertions.h" + +#include <algorithm> +#include <type_traits> +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainMonthDay.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.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/ToString.h" +#include "builtin/temporal/Wrapped.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "ds/IdValuePair.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCVector.h" +#include "js/Id.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.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" + +using namespace js; +using namespace js::temporal; + +static inline bool IsPlainDateTime(Handle<Value> v) { + return v.isObject() && v.toObject().is<PlainDateTimeObject>(); +} + +#ifdef DEBUG +/** + * IsValidISODateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond ) + */ +bool js::temporal::IsValidISODateTime(const PlainDateTime& dateTime) { + return IsValidISODate(dateTime.date) && IsValidTime(dateTime.time); +} +#endif + +/** + * IsValidISODateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond ) + */ +static bool ThrowIfInvalidISODateTime(JSContext* cx, + const PlainDateTime& dateTime) { + return ThrowIfInvalidISODate(cx, dateTime.date) && + ThrowIfInvalidTime(cx, dateTime.time); +} + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +template <typename T> +static bool ISODateTimeWithinLimits(T year, T month, T day, T hour, T minute, + T second, T millisecond, T microsecond, + T nanosecond) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(IsInteger(month)); + MOZ_ASSERT(IsInteger(day)); + MOZ_ASSERT(IsInteger(hour)); + MOZ_ASSERT(IsInteger(minute)); + MOZ_ASSERT(IsInteger(second)); + MOZ_ASSERT(IsInteger(millisecond)); + MOZ_ASSERT(IsInteger(microsecond)); + MOZ_ASSERT(IsInteger(nanosecond)); + + MOZ_ASSERT(IsValidISODate(year, month, day)); + MOZ_ASSERT( + IsValidTime(hour, minute, second, millisecond, microsecond, nanosecond)); + + // js> new Date(-8_64000_00000_00000).toISOString() + // "-271821-04-20T00:00:00.000Z" + // + // js> new Date(+8_64000_00000_00000).toISOString() + // "+275760-09-13T00:00:00.000Z" + + constexpr int32_t minYear = -271821; + constexpr int32_t maxYear = 275760; + + // Definitely in range. + if (minYear < year && year < maxYear) { + return true; + } + + // -271821 April, 20 + if (year < 0) { + if (year != minYear) { + return false; + } + if (month != 4) { + return month > 4; + } + if (day != (20 - 1)) { + return day > (20 - 1); + } + // Needs to be past midnight on April, 19. + return !(hour == 0 && minute == 0 && second == 0 && millisecond == 0 && + microsecond == 0 && nanosecond == 0); + } + + // 275760 September, 13 + if (year != maxYear) { + return false; + } + if (month != 9) { + return month < 9; + } + if (day > 13) { + return false; + } + return true; +} + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +template <typename T> +static bool ISODateTimeWithinLimits(T year, T month, T day) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + MOZ_ASSERT(IsValidISODate(year, month, day)); + + // js> new Date(-8_64000_00000_00000).toISOString() + // "-271821-04-20T00:00:00.000Z" + // + // js> new Date(+8_64000_00000_00000).toISOString() + // "+275760-09-13T00:00:00.000Z" + + constexpr int32_t minYear = -271821; + constexpr int32_t maxYear = 275760; + + // ISODateTimeWithinLimits is called with hour=12 and the remaining time + // components set to zero. That means the maximum value is exclusive, whereas + // the minimum value is inclusive. + + // FIXME: spec bug - GetUTCEpochNanoseconds when called with large |year| may + // cause MakeDay to return NaN, which makes MakeDate return NaN, which is + // unexpected in GetUTCEpochNanoseconds, step 4. + // https://github.com/tc39/proposal-temporal/issues/2315 + + // Definitely in range. + if (minYear < year && year < maxYear) { + return true; + } + + // -271821 April, 20 + if (year < 0) { + if (year != minYear) { + return false; + } + if (month != 4) { + return month > 4; + } + if (day < (20 - 1)) { + return false; + } + return true; + } + + // 275760 September, 13 + if (year != maxYear) { + return false; + } + if (month != 9) { + return month < 9; + } + if (day > 13) { + return false; + } + return true; +} + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +bool js::temporal::ISODateTimeWithinLimits(double year, double month, + double day) { + return ::ISODateTimeWithinLimits(year, month, day); +} + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +bool js::temporal::ISODateTimeWithinLimits(const PlainDateTime& dateTime) { + auto& [date, time] = dateTime; + return ::ISODateTimeWithinLimits(date.year, date.month, date.day, time.hour, + time.minute, time.second, time.millisecond, + time.microsecond, time.nanosecond); +} + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +bool js::temporal::ISODateTimeWithinLimits(const PlainDate& date) { + return ::ISODateTimeWithinLimits(date.year, date.month, date.day); +} + +/** + * CreateTemporalDateTime ( isoYear, isoMonth, isoDay, hour, minute, second, + * millisecond, microsecond, nanosecond, calendar [ , newTarget ] ) + */ +static PlainDateTimeObject* CreateTemporalDateTime( + JSContext* cx, const CallArgs& args, double isoYear, double isoMonth, + double isoDay, double hour, double minute, double second, + double millisecond, double microsecond, double nanosecond, + Handle<CalendarValue> calendar) { + MOZ_ASSERT(IsInteger(isoYear)); + MOZ_ASSERT(IsInteger(isoMonth)); + MOZ_ASSERT(IsInteger(isoDay)); + MOZ_ASSERT(IsInteger(hour)); + MOZ_ASSERT(IsInteger(minute)); + MOZ_ASSERT(IsInteger(second)); + MOZ_ASSERT(IsInteger(millisecond)); + MOZ_ASSERT(IsInteger(microsecond)); + MOZ_ASSERT(IsInteger(nanosecond)); + + // Step 1. + if (!ThrowIfInvalidISODate(cx, isoYear, isoMonth, isoDay)) { + return nullptr; + } + + // Step 2. + if (!ThrowIfInvalidTime(cx, hour, minute, second, millisecond, microsecond, + nanosecond)) { + return nullptr; + } + + // Step 3. + if (!ISODateTimeWithinLimits(isoYear, isoMonth, isoDay, hour, minute, second, + millisecond, microsecond, nanosecond)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_TIME_INVALID); + return nullptr; + } + + // Steps 4-5. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PlainDateTime, + &proto)) { + return nullptr; + } + + auto* dateTime = NewObjectWithClassProto<PlainDateTimeObject>(cx, proto); + if (!dateTime) { + return nullptr; + } + + // Step 6. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_YEAR_SLOT, + Int32Value(isoYear)); + + // Step 7. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_MONTH_SLOT, + Int32Value(isoMonth)); + + // Step 8. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 9. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_HOUR_SLOT, Int32Value(hour)); + + // Step 10. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_MINUTE_SLOT, + Int32Value(minute)); + + // Step 11. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_SECOND_SLOT, + Int32Value(second)); + + // Step 12. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_MILLISECOND_SLOT, + Int32Value(millisecond)); + + // Step 13. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_MICROSECOND_SLOT, + Int32Value(microsecond)); + + // Step 14. + dateTime->setFixedSlot(PlainDateTimeObject::ISO_NANOSECOND_SLOT, + Int32Value(nanosecond)); + + // Step 15. + dateTime->setFixedSlot(PlainDateTimeObject::CALENDAR_SLOT, + calendar.toValue()); + + // Step 16. + return dateTime; +} + +/** + * CreateTemporalDateTime ( isoYear, isoMonth, isoDay, hour, minute, second, + * millisecond, microsecond, nanosecond, calendar [ , newTarget ] ) + */ +PlainDateTimeObject* js::temporal::CreateTemporalDateTime( + JSContext* cx, const PlainDateTime& dateTime, + Handle<CalendarValue> calendar) { + auto& [date, time] = dateTime; + auto& [isoYear, isoMonth, isoDay] = date; + auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + + // Steps 1-2. + if (!ThrowIfInvalidISODateTime(cx, dateTime)) { + return nullptr; + } + + // Step 3. + if (!ISODateTimeWithinLimits(dateTime)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_TIME_INVALID); + return nullptr; + } + + // Steps 4-5. + auto* object = NewBuiltinClassInstance<PlainDateTimeObject>(cx); + if (!object) { + return nullptr; + } + + // Step 6. + object->setFixedSlot(PlainDateTimeObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 7. + object->setFixedSlot(PlainDateTimeObject::ISO_MONTH_SLOT, + Int32Value(isoMonth)); + + // Step 8. + object->setFixedSlot(PlainDateTimeObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 9. + object->setFixedSlot(PlainDateTimeObject::ISO_HOUR_SLOT, Int32Value(hour)); + + // Step 10. + object->setFixedSlot(PlainDateTimeObject::ISO_MINUTE_SLOT, + Int32Value(minute)); + + // Step 11. + object->setFixedSlot(PlainDateTimeObject::ISO_SECOND_SLOT, + Int32Value(second)); + + // Step 12. + object->setFixedSlot(PlainDateTimeObject::ISO_MILLISECOND_SLOT, + Int32Value(millisecond)); + + // Step 13. + object->setFixedSlot(PlainDateTimeObject::ISO_MICROSECOND_SLOT, + Int32Value(microsecond)); + + // Step 14. + object->setFixedSlot(PlainDateTimeObject::ISO_NANOSECOND_SLOT, + Int32Value(nanosecond)); + + // Step 15. + object->setFixedSlot(PlainDateTimeObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 16. + return object; +} + +/** + * CreateTemporalDateTime ( isoYear, isoMonth, isoDay, hour, minute, second, + * millisecond, microsecond, nanosecond, calendar [ , newTarget ] ) + */ +bool js::temporal::CreateTemporalDateTime( + JSContext* cx, const PlainDateTime& dateTime, + Handle<CalendarValue> calendar, + MutableHandle<PlainDateTimeWithCalendar> result) { + // Steps 1-2. + if (!ThrowIfInvalidISODateTime(cx, dateTime)) { + return false; + } + + // Step 3. + if (!ISODateTimeWithinLimits(dateTime)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_DATE_TIME_INVALID); + return false; + } + + result.set(PlainDateTimeWithCalendar{dateTime, calendar}); + return true; +} + +/** + * InterpretTemporalDateTimeFields ( calendarRec, fields, options ) + */ +bool js::temporal::InterpretTemporalDateTimeFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<PlainObject*> fields, + Handle<PlainObject*> options, PlainDateTime* result) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + MOZ_ASSERT(CalendarMethodsRecordHasLookedUp(calendar, + CalendarMethod::DateFromFields)); + + // Step 3. + TimeRecord timeResult; + if (!ToTemporalTimeRecord(cx, fields, &timeResult)) { + return false; + } + + // Step 4. + auto overflow = TemporalOverflow::Constrain; + if (!ToTemporalOverflow(cx, options, &overflow)) { + return false; + } + + // Steps 5-6. + Rooted<Value> overflowValue(cx); + if (overflow == TemporalOverflow::Constrain) { + overflowValue.setString(cx->names().constrain); + } else { + MOZ_ASSERT(overflow == TemporalOverflow::Reject); + overflowValue.setString(cx->names().reject); + } + if (!DefineDataProperty(cx, options, cx->names().overflow, overflowValue)) { + return false; + } + + // Step 7. + auto temporalDate = + js::temporal::CalendarDateFromFields(cx, calendar, fields, options); + if (!temporalDate) { + return false; + } + auto date = ToPlainDate(&temporalDate.unwrap()); + + // Step 8. + PlainTime time; + if (!RegulateTime(cx, timeResult, overflow, &time)) { + return false; + } + + // Step 9. + *result = {date, time}; + return true; +} + +/** + * InterpretTemporalDateTimeFields ( calendarRec, fields, options ) + */ +bool js::temporal::InterpretTemporalDateTimeFields( + JSContext* cx, Handle<CalendarRecord> calendar, Handle<PlainObject*> fields, + PlainDateTime* result) { + // TODO: Avoid creating the options object when CalendarDateFromFields calls + // the built-in Calendar.prototype.dateFromFields method. + Rooted<PlainObject*> options(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!options) { + return false; + } + + return InterpretTemporalDateTimeFields(cx, calendar, fields, options, result); +} + +/** + * ToTemporalDateTime ( item [ , options ] ) + */ +static Wrapped<PlainDateTimeObject*> ToTemporalDateTime( + JSContext* cx, Handle<Value> item, Handle<JSObject*> maybeOptions) { + // Step 1. (Not applicable) + + // Step 2. + Rooted<PlainObject*> maybeResolvedOptions(cx); + if (maybeOptions) { + maybeResolvedOptions = SnapshotOwnProperties(cx, maybeOptions); + if (!maybeResolvedOptions) { + return nullptr; + } + } + + // Steps 3-4. + Rooted<CalendarValue> calendar(cx); + PlainDateTime result; + if (item.isObject()) { + Rooted<JSObject*> itemObj(cx, &item.toObject()); + + // Step 3.a. + if (itemObj->canUnwrapAs<PlainDateTimeObject>()) { + return itemObj; + } + + // Step 3.b. + if (auto* zonedDateTime = itemObj->maybeUnwrapIf<ZonedDateTimeObject>()) { + auto epochInstant = ToInstant(zonedDateTime); + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar()); + + if (!timeZone.wrap(cx)) { + return nullptr; + } + if (!calendar.wrap(cx)) { + return nullptr; + } + + // Step 3.b.i. + if (maybeResolvedOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return nullptr; + } + } + + // Steps 3.b.ii-iv. + return GetPlainDateTimeFor(cx, timeZone, epochInstant, calendar); + } + + // Step 3.c. + if (auto* date = itemObj->maybeUnwrapIf<PlainDateObject>()) { + PlainDateTime dateTime = {ToPlainDate(date), {}}; + Rooted<CalendarValue> calendar(cx, date->calendar()); + if (!calendar.wrap(cx)) { + return nullptr; + } + + // Step 3.c.i. + if (maybeResolvedOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return nullptr; + } + } + + // Step 3.c.ii. + return CreateTemporalDateTime(cx, dateTime, calendar); + } + + // Step 3.d. + if (!GetTemporalCalendarWithISODefault(cx, itemObj, &calendar)) { + return nullptr; + } + + // Step 3.e. + Rooted<CalendarRecord> calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + }, + &calendarRec)) { + return nullptr; + } + + // Step 3.f. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendarRec, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return nullptr; + } + + // Step 3.g. + if (!AppendSorted(cx, fieldNames.get(), + { + TemporalField::Hour, + TemporalField::Microsecond, + TemporalField::Millisecond, + TemporalField::Minute, + TemporalField::Nanosecond, + TemporalField::Second, + })) { + return nullptr; + } + + // Step 3.h. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, itemObj, fieldNames)); + if (!fields) { + return nullptr; + } + + // Step 3.i. + if (maybeResolvedOptions) { + if (!InterpretTemporalDateTimeFields(cx, calendarRec, fields, + maybeResolvedOptions, &result)) { + return nullptr; + } + } else { + if (!InterpretTemporalDateTimeFields(cx, calendarRec, fields, &result)) { + return nullptr; + } + } + } else { + // Step 4.a. + if (!item.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, item, + nullptr, "not a string"); + return nullptr; + } + Rooted<JSString*> string(cx, item.toString()); + + // Step 4.b. + Rooted<JSString*> calendarString(cx); + if (!ParseTemporalDateTimeString(cx, string, &result, &calendarString)) { + return nullptr; + } + + // Step 4.c. + MOZ_ASSERT(IsValidISODate(result.date)); + + // Step 4.d. + MOZ_ASSERT(IsValidTime(result.time)); + + // Steps 4.e-h. + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendar)) { + return nullptr; + } + } else { + calendar.set(CalendarValue(cx->names().iso8601)); + } + + // Step 4.i. + if (maybeResolvedOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return nullptr; + } + } + } + + // Step 5. + return CreateTemporalDateTime(cx, result, calendar); +} + +/** + * ToTemporalDateTime ( item [ , options ] ) + */ +Wrapped<PlainDateTimeObject*> js::temporal::ToTemporalDateTime( + JSContext* cx, Handle<Value> item) { + return ::ToTemporalDateTime(cx, item, nullptr); +} + +/** + * ToTemporalDateTime ( item [ , options ] ) + */ +bool js::temporal::ToTemporalDateTime(JSContext* cx, Handle<Value> item, + PlainDateTime* result) { + auto obj = ::ToTemporalDateTime(cx, item, nullptr); + if (!obj) { + return false; + } + + *result = ToPlainDateTime(&obj.unwrap()); + return true; +} + +/** + * ToTemporalDateTime ( item [ , options ] ) + */ +static bool ToTemporalDateTime( + JSContext* cx, Handle<Value> item, + MutableHandle<PlainDateTimeWithCalendar> result) { + HandleObject options = nullptr; + + auto* obj = ::ToTemporalDateTime(cx, item, options).unwrapOrNull(); + if (!obj) { + return false; + } + + auto dateTime = ToPlainDateTime(obj); + Rooted<CalendarValue> calendar(cx, obj->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + result.set(PlainDateTimeWithCalendar{dateTime, calendar}); + return true; +} + +/** + * CompareISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, + * d2, h2, min2, s2, ms2, mus2, ns2 ) + */ +static int32_t CompareISODateTime(const PlainDateTime& one, + const PlainDateTime& two) { + // Step 1. (Not applicable in our implementation.) + + // Steps 2-3. + if (int32_t dateResult = CompareISODate(one.date, two.date)) { + return dateResult; + } + + // Steps 4. + return CompareTemporalTime(one.time, two.time); +} + +/** + * AddDateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond, calendarRec, years, months, weeks, days, hours, + * minutes, seconds, milliseconds, microseconds, nanoseconds, options ) + */ +static bool AddDateTime(JSContext* cx, const PlainDateTime& dateTime, + Handle<CalendarRecord> calendar, + const Duration& duration, Handle<JSObject*> options, + PlainDateTime* result) { + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 1. + MOZ_ASSERT(IsValidISODateTime(dateTime)); + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + // Step 2. + PlainTime timeResult; + double daysResult; + if (!AddTime(cx, dateTime.time, duration, &timeResult, &daysResult)) { + return false; + } + + // Step 3. + const auto& datePart = dateTime.date; + + // Step 4. + Duration dateDuration = {duration.years, duration.months, duration.weeks, + daysResult}; + MOZ_ASSERT(IsValidDuration(duration)); + + // Step 5. + PlainDate addedDate; + if (!AddDate(cx, calendar, datePart, dateDuration, options, &addedDate)) { + return false; + } + + // Step 6. + *result = {addedDate, timeResult}; + return true; +} + +/** + * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, + * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) + */ +static bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, + const PlainDateTime& two, + Handle<CalendarRecord> calendar, + TemporalUnit largestUnit, + Handle<PlainObject*> maybeOptions, + Duration* result) { + // Steps 1-2. + MOZ_ASSERT(IsValidISODateTime(one)); + MOZ_ASSERT(IsValidISODateTime(two)); + MOZ_ASSERT(ISODateTimeWithinLimits(one)); + MOZ_ASSERT(ISODateTimeWithinLimits(two)); + + // Step 3. + MOZ_ASSERT_IF( + one.date != two.date && largestUnit < TemporalUnit::Day, + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateUntil)); + + // Step 4. + auto timeDifference = DifferenceTime(one.time, two.time); + + // Step 5. + int32_t timeSign = DurationSign(timeDifference.toDuration()); + + // Step 6. + int32_t dateSign = CompareISODate(two.date, one.date); + + // Step 7. + auto adjustedDate = one.date; + + // Step 8. + if (timeSign == -dateSign) { + // Step 8.a. + adjustedDate = BalanceISODate(adjustedDate.year, adjustedDate.month, + adjustedDate.day - timeSign); + + // Step 8.b. + if (!BalanceTimeDuration(cx, + { + 0, + 0, + 0, + double(-timeSign), + timeDifference.hours, + timeDifference.minutes, + timeDifference.seconds, + timeDifference.milliseconds, + timeDifference.microseconds, + timeDifference.nanoseconds, + }, + largestUnit, &timeDifference)) { + return false; + } + } + + MOZ_ASSERT(IsValidISODate(adjustedDate)); + MOZ_ASSERT(ISODateTimeWithinLimits(adjustedDate)); + + // TODO: Avoid allocating CreateTemporalDate. + + // Step 9. + Rooted<PlainDateObject*> date1( + cx, CreateTemporalDate(cx, adjustedDate, calendar.receiver())); + if (!date1) { + return false; + } + + // Step 10. + Rooted<PlainDateObject*> date2( + cx, CreateTemporalDate(cx, two.date, calendar.receiver())); + if (!date2) { + return false; + } + + // Step 11. + auto dateLargestUnit = std::min(TemporalUnit::Day, largestUnit); + + Duration dateDifference; + if (maybeOptions) { + // FIXME: spec issue - this copy is no longer needed, all callers have + // already copied the user input object. + // https://github.com/tc39/proposal-temporal/issues/2525 + + // Step 12. + Rooted<PlainObject*> untilOptions(cx, + SnapshotOwnProperties(cx, maybeOptions)); + if (!untilOptions) { + return false; + } + + // Step 13. + Rooted<Value> largestUnitValue( + cx, StringValue(TemporalUnitToString(cx, dateLargestUnit))); + if (!DefineDataProperty(cx, untilOptions, cx->names().largestUnit, + largestUnitValue)) { + return false; + } + + // Step 14. + if (!DifferenceDate(cx, calendar, date1, date2, untilOptions, + &dateDifference)) { + return false; + } + } else { + // Steps 12-14. + if (!DifferenceDate(cx, calendar, date1, date2, dateLargestUnit, + &dateDifference)) { + return false; + } + } + + // Step 15. + TimeDuration balanceResult; + if (!BalanceTimeDuration(cx, + { + 0, + 0, + 0, + dateDifference.days, + timeDifference.hours, + timeDifference.minutes, + timeDifference.seconds, + timeDifference.milliseconds, + timeDifference.microseconds, + timeDifference.nanoseconds, + }, + largestUnit, &balanceResult)) { + return false; + } + + // Step 16. + *result = {dateDifference.years, dateDifference.months, + dateDifference.weeks, balanceResult.days, + balanceResult.hours, balanceResult.minutes, + balanceResult.seconds, balanceResult.milliseconds, + balanceResult.microseconds, balanceResult.nanoseconds}; + MOZ_ASSERT(IsValidDuration(*result)); + return true; +} + +/** + * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, + * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) + */ +bool js::temporal::DifferenceISODateTime(JSContext* cx, + const PlainDateTime& one, + const PlainDateTime& two, + Handle<CalendarRecord> calendar, + TemporalUnit largestUnit, + Duration* result) { + return ::DifferenceISODateTime(cx, one, two, calendar, largestUnit, nullptr, + result); +} + +/** + * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, + * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) + */ +bool js::temporal::DifferenceISODateTime( + JSContext* cx, const PlainDateTime& one, const PlainDateTime& two, + Handle<CalendarRecord> calendar, TemporalUnit largestUnit, + Handle<PlainObject*> options, Duration* result) { + return ::DifferenceISODateTime(cx, one, two, calendar, largestUnit, options, + result); +} + +/** + * RoundISODateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond, increment, unit, roundingMode [ , dayLength ] ) + */ +static PlainDateTime RoundISODateTime(const PlainDateTime& dateTime, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode) { + const auto& [date, time] = dateTime; + + // Step 1. + MOZ_ASSERT(IsValidISODateTime(dateTime)); + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + auto roundedTime = RoundTime(time, increment, unit, roundingMode); + MOZ_ASSERT(0 <= roundedTime.days && roundedTime.days <= 1); + + // Step 4. + auto balanceResult = + BalanceISODate(date.year, date.month, date.day + roundedTime.days); + + // Step 5. + return {balanceResult, roundedTime.time}; +} + +/** + * DifferenceTemporalPlainDateTime ( operation, dateTime, other, options ) + */ +static bool DifferenceTemporalPlainDateTime(JSContext* cx, + TemporalDifference operation, + const CallArgs& args) { + Rooted<PlainDateTimeWithCalendar> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainDateTimeWithCalendar> other(cx); + if (!::ToTemporalDateTime(cx, args.get(0), &other)) { + return false; + } + + // Step 3. + if (!CalendarEqualsOrThrow(cx, dateTime.calendar(), other.calendar())) { + return false; + } + + // Steps 4-5. + DifferenceSettings settings; + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", ToName(operation), args[1])); + if (!options) { + return false; + } + + // Step 4. + resolvedOptions = SnapshotOwnProperties(cx, options); + if (!resolvedOptions) { + return false; + } + + // Step 5. + if (!GetDifferenceSettings( + cx, operation, resolvedOptions, TemporalUnitGroup::DateTime, + TemporalUnit::Nanosecond, TemporalUnit::Day, &settings)) { + return false; + } + } else { + // Steps 4-5. + settings = { + TemporalUnit::Nanosecond, + TemporalUnit::Day, + TemporalRoundingMode::Trunc, + Increment{1}, + }; + } + + // Steps 6-7. + bool datePartsIdentical = dateTime.date() == other.date(); + + // Step 8. + if (datePartsIdentical && dateTime.time() == other.time()) { + auto* obj = CreateTemporalDuration(cx, {}); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 9. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, dateTime.calendar(), + { + CalendarMethod::DateAdd, + CalendarMethod::DateUntil, + }, + &calendar)) { + return false; + } + + // Step 10. + Duration diff; + if (!::DifferenceISODateTime(cx, dateTime, other, calendar, + settings.largestUnit, resolvedOptions, &diff)) { + return false; + } + + // Step 11. + bool roundingGranularityIsNoop = + settings.smallestUnit == TemporalUnit::Nanosecond && + settings.roundingIncrement == Increment{1}; + + // Step 12. + if (roundingGranularityIsNoop) { + if (operation == TemporalDifference::Since) { + diff = diff.negate(); + } + + auto* obj = CreateTemporalDuration(cx, diff); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 13. + Rooted<PlainDateObject*> relativeTo( + cx, CreateTemporalDate(cx, dateTime.date(), dateTime.calendar())); + if (!relativeTo) { + return false; + } + + // Steps 14-15. + Duration roundResult; + if (!temporal::RoundDuration(cx, diff, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + relativeTo, calendar, &roundResult)) { + return false; + } + + // Step 16. + TimeDuration result; + if (!BalanceTimeDuration(cx, roundResult, settings.largestUnit, &result)) { + return false; + } + + // Step 17. + auto toBalance = Duration{ + roundResult.years, + roundResult.months, + roundResult.weeks, + result.days, + }; + DateDuration balanceResult; + if (!temporal::BalanceDateDurationRelative( + cx, toBalance, settings.largestUnit, settings.smallestUnit, + relativeTo, calendar, &balanceResult)) { + return false; + } + + // Step 18. + Duration duration = { + balanceResult.years, balanceResult.months, balanceResult.weeks, + balanceResult.days, result.hours, result.minutes, + result.seconds, result.milliseconds, result.microseconds, + result.nanoseconds, + }; + if (operation == TemporalDifference::Since) { + duration = duration.negate(); + } + + auto* obj = CreateTemporalDuration(cx, duration); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +enum class PlainDateTimeDuration { Add, Subtract }; + +/** + * AddDurationToOrSubtractDurationFromPlainDateTime ( operation, dateTime, + * temporalDurationLike, options ) + */ +static bool AddDurationToOrSubtractDurationFromPlainDateTime( + JSContext* cx, PlainDateTimeDuration operation, const CallArgs& args) { + Rooted<PlainDateTimeWithCalendar> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Duration duration; + if (!ToTemporalDurationRecord(cx, args.get(0), &duration)) { + return false; + } + + // Step 3. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + const char* name = + operation == PlainDateTimeDuration::Add ? "add" : "subtract"; + options = RequireObjectArg(cx, "options", name, args[1]); + } else { + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { + return false; + } + + // Step 4. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, dateTime.calendar(), + { + CalendarMethod::DateAdd, + }, + &calendar)) { + return false; + } + + // Step 5. + if (operation == PlainDateTimeDuration::Subtract) { + duration = duration.negate(); + } + + PlainDateTime result; + if (!AddDateTime(cx, dateTime, calendar, duration, options, &result)) { + return false; + } + + // Steps 6-7. + MOZ_ASSERT(IsValidISODateTime(result)); + + // Step 8. + auto* obj = CreateTemporalDateTime(cx, result, dateTime.calendar()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime ( isoYear, isoMonth, isoDay [ , hour [ , minute [ , + * second [ , millisecond [ , microsecond [ , nanosecond [ , calendarLike ] ] ] + * ] ] ] ] ) + */ +static bool PlainDateTimeConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.PlainDateTime")) { + return false; + } + + // Step 2. + double isoYear; + if (!ToIntegerWithTruncation(cx, args.get(0), "year", &isoYear)) { + return false; + } + + // Step 3. + double isoMonth; + if (!ToIntegerWithTruncation(cx, args.get(1), "month", &isoMonth)) { + return false; + } + + // Step 4. + double isoDay; + if (!ToIntegerWithTruncation(cx, args.get(2), "day", &isoDay)) { + return false; + } + + // Step 5. + double hour = 0; + if (args.hasDefined(3)) { + if (!ToIntegerWithTruncation(cx, args[3], "hour", &hour)) { + return false; + } + } + + // Step 6. + double minute = 0; + if (args.hasDefined(4)) { + if (!ToIntegerWithTruncation(cx, args[4], "minute", &minute)) { + return false; + } + } + + // Step 7. + double second = 0; + if (args.hasDefined(5)) { + if (!ToIntegerWithTruncation(cx, args[5], "second", &second)) { + return false; + } + } + + // Step 8. + double millisecond = 0; + if (args.hasDefined(6)) { + if (!ToIntegerWithTruncation(cx, args[6], "millisecond", &millisecond)) { + return false; + } + } + + // Step 9. + double microsecond = 0; + if (args.hasDefined(7)) { + if (!ToIntegerWithTruncation(cx, args[7], "microsecond", µsecond)) { + return false; + } + } + + // Step 10. + double nanosecond = 0; + if (args.hasDefined(8)) { + if (!ToIntegerWithTruncation(cx, args[8], "nanosecond", &nanosecond)) { + return false; + } + } + + // Step 11. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendarWithISODefault(cx, args.get(9), &calendar)) { + return false; + } + + // Step 12. + auto* temporalDateTime = CreateTemporalDateTime( + cx, args, isoYear, isoMonth, isoDay, hour, minute, second, millisecond, + microsecond, nanosecond, calendar); + if (!temporalDateTime) { + return false; + } + + args.rval().setObject(*temporalDateTime); + return true; +} + +/** + * Temporal.PlainDateTime.from ( item [ , options ] ) + */ +static bool PlainDateTime_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "from", args[1]); + if (!options) { + return false; + } + } + + // Step 2. + if (args.get(0).isObject()) { + JSObject* item = &args[0].toObject(); + if (auto* temporalDateTime = item->maybeUnwrapIf<PlainDateTimeObject>()) { + auto dateTime = ToPlainDateTime(temporalDateTime); + + Rooted<CalendarValue> calendar(cx, temporalDateTime->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + if (options) { + // Step 2.a. + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, options, &ignored)) { + return false; + } + } + + // Step 2.b. + auto* result = CreateTemporalDateTime(cx, dateTime, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; + } + } + + // Step 3. + auto result = ToTemporalDateTime(cx, args.get(0), options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDateTime.compare ( one, two ) + */ +static bool PlainDateTime_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + PlainDateTime one; + if (!ToTemporalDateTime(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + PlainDateTime two; + if (!ToTemporalDateTime(cx, args.get(1), &two)) { + return false; + } + + // Step 3. + args.rval().setInt32(CompareISODateTime(one, two)); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.calendarId + */ +static bool PlainDateTime_calendarId(JSContext* cx, const CallArgs& args) { + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + + // Step 3. + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + auto* calendarId = ToTemporalCalendarIdentifier(cx, calendar); + if (!calendarId) { + return false; + } + + args.rval().setString(calendarId); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.calendarId + */ +static bool PlainDateTime_calendarId(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_calendarId>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.year + */ +static bool PlainDateTime_year(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarYear(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.year + */ +static bool PlainDateTime_year(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_year>(cx, args); +} + +/** + * get Temporal.PlainDateTime.prototype.month + */ +static bool PlainDateTime_month(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarMonth(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.month + */ +static bool PlainDateTime_month(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_month>(cx, args); +} + +/** + * get Temporal.PlainDateTime.prototype.monthCode + */ +static bool PlainDateTime_monthCode(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarMonthCode(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.monthCode + */ +static bool PlainDateTime_monthCode(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_monthCode>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.day + */ +static bool PlainDateTime_day(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarDay(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.day + */ +static bool PlainDateTime_day(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_day>(cx, args); +} + +/** + * get Temporal.PlainDateTime.prototype.hour + */ +static bool PlainDateTime_hour(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + args.rval().setInt32(dateTime->isoHour()); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.hour + */ +static bool PlainDateTime_hour(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_hour>(cx, args); +} + +/** + * get Temporal.PlainDateTime.prototype.minute + */ +static bool PlainDateTime_minute(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + args.rval().setInt32(dateTime->isoMinute()); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.minute + */ +static bool PlainDateTime_minute(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_minute>(cx, args); +} + +/** + * get Temporal.PlainDateTime.prototype.second + */ +static bool PlainDateTime_second(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + args.rval().setInt32(dateTime->isoSecond()); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.second + */ +static bool PlainDateTime_second(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_second>(cx, args); +} + +/** + * get Temporal.PlainDateTime.prototype.millisecond + */ +static bool PlainDateTime_millisecond(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + args.rval().setInt32(dateTime->isoMillisecond()); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.millisecond + */ +static bool PlainDateTime_millisecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_millisecond>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.microsecond + */ +static bool PlainDateTime_microsecond(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + args.rval().setInt32(dateTime->isoMicrosecond()); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.microsecond + */ +static bool PlainDateTime_microsecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_microsecond>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.nanosecond + */ +static bool PlainDateTime_nanosecond(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + args.rval().setInt32(dateTime->isoNanosecond()); + return true; +} + +/** + * get Temporal.PlainDateTime.prototype.nanosecond + */ +static bool PlainDateTime_nanosecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_nanosecond>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.dayOfWeek + */ +static bool PlainDateTime_dayOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarDayOfWeek(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.dayOfWeek + */ +static bool PlainDateTime_dayOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_dayOfWeek>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.dayOfYear + */ +static bool PlainDateTime_dayOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarDayOfYear(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.dayOfYear + */ +static bool PlainDateTime_dayOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_dayOfYear>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.weekOfYear + */ +static bool PlainDateTime_weekOfYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarWeekOfYear(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.weekOfYear + */ +static bool PlainDateTime_weekOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_weekOfYear>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.yearOfWeek + */ +static bool PlainDateTime_yearOfWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarYearOfWeek(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.yearOfWeek + */ +static bool PlainDateTime_yearOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_yearOfWeek>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.daysInWeek + */ +static bool PlainDateTime_daysInWeek(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarDaysInWeek(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.daysInWeek + */ +static bool PlainDateTime_daysInWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_daysInWeek>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.daysInMonth + */ +static bool PlainDateTime_daysInMonth(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarDaysInMonth(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.daysInMonth + */ +static bool PlainDateTime_daysInMonth(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_daysInMonth>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.daysInYear + */ +static bool PlainDateTime_daysInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarDaysInYear(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.daysInYear + */ +static bool PlainDateTime_daysInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_daysInYear>(cx, + args); +} + +/** + * get Temporal.PlainDateTime.prototype.monthsInYear + */ +static bool PlainDateTime_monthsInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarMonthsInYear(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.monthsInYear + */ +static bool PlainDateTime_monthsInYear(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_monthsInYear>( + cx, args); +} + +/** + * get Temporal.PlainDateTime.prototype.inLeapYear + */ +static bool PlainDateTime_inLeapYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 4. + return CalendarInLeapYear(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.PlainDateTime.prototype.inLeapYear + */ +static bool PlainDateTime_inLeapYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_inLeapYear>(cx, + args); +} + +/** + * Temporal.PlainDateTime.prototype.with ( temporalDateTimeLike [ , options ] ) + */ +static bool PlainDateTime_with(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + + // Step 3. + Rooted<JSObject*> temporalDateTimeLike( + cx, RequireObjectArg(cx, "temporalDateTimeLike", "with", args.get(0))); + if (!temporalDateTimeLike) { + return false; + } + + // Step 4. + if (!RejectTemporalLikeObject(cx, temporalDateTimeLike)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "with", args[1])); + if (!options) { + return false; + } + resolvedOptions = SnapshotOwnProperties(cx, options); + } else { + resolvedOptions = NewPlainObjectWithProto(cx, nullptr); + } + if (!resolvedOptions) { + return false; + } + + // Step 6. + Rooted<CalendarValue> calendarValue(cx, dateTime->calendar()); + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + CalendarMethod::MergeFields, + }, + &calendar)) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, dateTime, fieldNames)); + if (!fields) { + return false; + } + + // Steps 9-14. + struct TimeField { + using FieldName = ImmutableTenuredPtr<PropertyName*> JSAtomState::*; + + FieldName name; + int32_t value; + } timeFields[] = { + {&JSAtomState::hour, dateTime->isoHour()}, + {&JSAtomState::minute, dateTime->isoMinute()}, + {&JSAtomState::second, dateTime->isoSecond()}, + {&JSAtomState::millisecond, dateTime->isoMillisecond()}, + {&JSAtomState::microsecond, dateTime->isoMicrosecond()}, + {&JSAtomState::nanosecond, dateTime->isoNanosecond()}, + }; + + Rooted<Value> timeFieldValue(cx); + for (const auto& timeField : timeFields) { + Handle<PropertyName*> name = cx->names().*(timeField.name); + timeFieldValue.setInt32(timeField.value); + + if (!DefineDataProperty(cx, fields, name, timeFieldValue)) { + return false; + } + } + + // Step 15. + if (!AppendSorted(cx, fieldNames.get(), + { + TemporalField::Hour, + TemporalField::Microsecond, + TemporalField::Millisecond, + TemporalField::Minute, + TemporalField::Nanosecond, + TemporalField::Second, + })) { + return false; + } + + // Step 16. + Rooted<PlainObject*> partialDateTime( + cx, PreparePartialTemporalFields(cx, temporalDateTimeLike, fieldNames)); + if (!partialDateTime) { + return false; + } + + // Step 17. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, partialDateTime)); + if (!mergedFields) { + return false; + } + + // Step 18. + fields = PrepareTemporalFields(cx, mergedFields, fieldNames); + if (!fields) { + return false; + } + + // Step 19. + PlainDateTime result; + if (!InterpretTemporalDateTimeFields(cx, calendar, fields, resolvedOptions, + &result)) { + return false; + } + + // Steps 20-21. + MOZ_ASSERT(IsValidISODateTime(result)); + + // Step 22. + auto* obj = CreateTemporalDateTime(cx, result, calendar.receiver()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.with ( temporalDateTimeLike [ , options ] ) + */ +static bool PlainDateTime_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_with>(cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.withPlainTime ( [ plainTimeLike ] ) + */ +static bool PlainDateTime_withPlainTime(JSContext* cx, const CallArgs& args) { + auto* temporalDateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto date = ToPlainDate(temporalDateTime); + Rooted<CalendarValue> calendar(cx, temporalDateTime->calendar()); + + // Step 4. + PlainTime time = {}; + if (args.hasDefined(0)) { + if (!ToTemporalTime(cx, args[0], &time)) { + return false; + } + } + + // Steps 3 and 5. + auto* obj = CreateTemporalDateTime(cx, {date, time}, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.withPlainTime ( [ plainTimeLike ] ) + */ +static bool PlainDateTime_withPlainTime(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_withPlainTime>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.withPlainDate ( plainDateLike ) + */ +static bool PlainDateTime_withPlainDate(JSContext* cx, const CallArgs& args) { + auto* temporalDateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto time = ToPlainTime(temporalDateTime); + Rooted<CalendarValue> calendar(cx, temporalDateTime->calendar()); + + // Step 3. + Rooted<PlainDateWithCalendar> plainDate(cx); + if (!ToTemporalDate(cx, args.get(0), &plainDate)) { + return false; + } + auto date = plainDate.date(); + + // Step 4. + if (!ConsolidateCalendars(cx, calendar, plainDate.calendar(), &calendar)) { + return false; + } + + // Step 5. + auto* obj = CreateTemporalDateTime(cx, {date, time}, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.withPlainDate ( plainDateLike ) + */ +static bool PlainDateTime_withPlainDate(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_withPlainDate>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.withCalendar ( calendar ) + */ +static bool PlainDateTime_withCalendar(JSContext* cx, const CallArgs& args) { + auto* temporalDateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto dateTime = ToPlainDateTime(temporalDateTime); + + // Step 3. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // Step 4. + auto* result = CreateTemporalDateTime(cx, dateTime, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.withCalendar ( calendar ) + */ +static bool PlainDateTime_withCalendar(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_withCalendar>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainDateTime_add(JSContext* cx, const CallArgs& args) { + // Step 3. + return AddDurationToOrSubtractDurationFromPlainDateTime( + cx, PlainDateTimeDuration::Add, args); +} + +/** + * Temporal.PlainDateTime.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainDateTime_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_add>(cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.subtract ( temporalDurationLike [ , options + * ] ) + */ +static bool PlainDateTime_subtract(JSContext* cx, const CallArgs& args) { + // Step 3. + return AddDurationToOrSubtractDurationFromPlainDateTime( + cx, PlainDateTimeDuration::Subtract, args); +} + +/** + * Temporal.PlainDateTime.prototype.subtract ( temporalDurationLike [ , options + * ] ) + */ +static bool PlainDateTime_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_subtract>(cx, + args); +} + +/** + * Temporal.PlainDateTime.prototype.until ( other [ , options ] ) + */ +static bool PlainDateTime_until(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainDateTime(cx, TemporalDifference::Until, args); +} + +/** + * Temporal.PlainDateTime.prototype.until ( other [ , options ] ) + */ +static bool PlainDateTime_until(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_until>(cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.since ( other [ , options ] ) + */ +static bool PlainDateTime_since(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainDateTime(cx, TemporalDifference::Since, args); +} + +/** + * Temporal.PlainDateTime.prototype.since ( other [ , options ] ) + */ +static bool PlainDateTime_since(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_since>(cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.round ( roundTo ) + */ +static bool PlainDateTime_round(JSContext* cx, const CallArgs& args) { + auto* temporalDateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto dateTime = ToPlainDateTime(temporalDateTime); + Rooted<CalendarValue> calendar(cx, temporalDateTime->calendar()); + + // Steps 3-12. + auto smallestUnit = TemporalUnit::Auto; + auto roundingMode = TemporalRoundingMode::HalfExpand; + auto roundingIncrement = Increment{1}; + if (args.get(0).isString()) { + // Step 4. (Not applicable in our implementation.) + + // Step 9. + Rooted<JSString*> paramString(cx, args[0].toString()); + if (!GetTemporalUnit(cx, paramString, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::DayTime, &smallestUnit)) { + return false; + } + + MOZ_ASSERT(TemporalUnit::Day <= smallestUnit && + smallestUnit <= TemporalUnit::Nanosecond); + + // Steps 6-8 and 10-12. (Implicit) + } else { + // Steps 3 and 5. + Rooted<JSObject*> roundTo( + cx, RequireObjectArg(cx, "roundTo", "round", args.get(0))); + if (!roundTo) { + return false; + } + + // Steps 6-7. + if (!ToTemporalRoundingIncrement(cx, roundTo, &roundingIncrement)) { + return false; + } + + // Step 8. + if (!ToTemporalRoundingMode(cx, roundTo, &roundingMode)) { + return false; + } + + // Step 9. + if (!GetTemporalUnit(cx, roundTo, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::DayTime, &smallestUnit)) { + return false; + } + + if (smallestUnit == TemporalUnit::Auto) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_OPTION, "smallestUnit"); + return false; + } + + MOZ_ASSERT(TemporalUnit::Day <= smallestUnit && + smallestUnit <= TemporalUnit::Nanosecond); + + // Steps 10-11. + auto maximum = Increment{1}; + bool inclusive = true; + if (smallestUnit > TemporalUnit::Day) { + maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit); + inclusive = false; + } + + // Step 12. + if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, + inclusive)) { + return false; + } + } + + // Step 13. + if (smallestUnit == TemporalUnit::Nanosecond && + roundingIncrement == Increment{1}) { + auto* obj = CreateTemporalDateTime(cx, dateTime, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 14. + auto result = + RoundISODateTime(dateTime, roundingIncrement, smallestUnit, roundingMode); + + // Step 15. + auto* obj = CreateTemporalDateTime(cx, result, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.round ( roundTo ) + */ +static bool PlainDateTime_round(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_round>(cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.equals ( other ) + */ +static bool PlainDateTime_equals(JSContext* cx, const CallArgs& args) { + auto* temporalDateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto dateTime = ToPlainDateTime(temporalDateTime); + Rooted<CalendarValue> calendar(cx, temporalDateTime->calendar()); + + // Step 3. + Rooted<PlainDateTimeWithCalendar> other(cx); + if (!::ToTemporalDateTime(cx, args.get(0), &other)) { + return false; + } + + // Steps 4-13. + bool equals = dateTime == other.dateTime(); + if (equals && !CalendarEquals(cx, calendar, other.calendar(), &equals)) { + return false; + } + + args.rval().setBoolean(equals); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.equals ( other ) + */ +static bool PlainDateTime_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_equals>(cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.toString ( [ options ] ) + */ +static bool PlainDateTime_toString(JSContext* cx, const CallArgs& args) { + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto dt = ToPlainDateTime(dateTime); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + SecondsStringPrecision precision = {Precision::Auto(), + TemporalUnit::Nanosecond, Increment{1}}; + auto roundingMode = TemporalRoundingMode::Trunc; + auto showCalendar = CalendarOption::Auto; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Steps 4-5. + if (!ToCalendarNameOption(cx, options, &showCalendar)) { + return false; + } + + // Step 6. + auto digits = Precision::Auto(); + if (!ToFractionalSecondDigits(cx, options, &digits)) { + return false; + } + + // Step 7. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 8. + auto smallestUnit = TemporalUnit::Auto; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + // Step 9. + if (smallestUnit == TemporalUnit::Hour) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_OPTION, "hour", + "smallestUnit"); + return false; + } + + // Step 10. + precision = ToSecondsStringPrecision(smallestUnit, digits); + } + + // Step 11. + auto result = + RoundISODateTime(dt, precision.increment, precision.unit, roundingMode); + + // Step 12. + JSString* str = ::TemporalDateTimeToString(cx, result, calendar, + precision.precision, showCalendar); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toString ( [ options ] ) + */ +static bool PlainDateTime_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toString>(cx, + args); +} + +/** + * Temporal.PlainDateTime.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainDateTime_toLocaleString(JSContext* cx, const CallArgs& args) { + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto dt = ToPlainDateTime(dateTime); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 3. + JSString* str = ::TemporalDateTimeToString( + cx, dt, calendar, Precision::Auto(), CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainDateTime_toLocaleString(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toLocaleString>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.toJSON ( ) + */ +static bool PlainDateTime_toJSON(JSContext* cx, const CallArgs& args) { + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto dt = ToPlainDateTime(dateTime); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 3. + JSString* str = ::TemporalDateTimeToString( + cx, dt, calendar, Precision::Auto(), CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toJSON ( ) + */ +static bool PlainDateTime_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toJSON>(cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.valueOf ( ) + */ +static bool PlainDateTime_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "PlainDateTime", "primitive type"); + return false; +} + +/** + * Temporal.PlainDateTime.prototype.getISOFields ( ) + */ +static bool PlainDateTime_getISOFields(JSContext* cx, const CallArgs& args) { + auto* temporalDateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + auto dateTime = ToPlainDateTime(temporalDateTime); + auto calendar = temporalDateTime->calendar(); + + // Step 3. + Rooted<IdValueVector> fields(cx, IdValueVector(cx)); + + // Step 4. + if (!fields.emplaceBack(NameToId(cx->names().calendar), calendar.toValue())) { + return false; + } + + // Step 5. + if (!fields.emplaceBack(NameToId(cx->names().isoDay), + Int32Value(dateTime.date.day))) { + return false; + } + + // Step 6. + if (!fields.emplaceBack(NameToId(cx->names().isoHour), + Int32Value(dateTime.time.hour))) { + return false; + } + + // Step 7. + if (!fields.emplaceBack(NameToId(cx->names().isoMicrosecond), + Int32Value(dateTime.time.microsecond))) { + return false; + } + + // Step 8. + if (!fields.emplaceBack(NameToId(cx->names().isoMillisecond), + Int32Value(dateTime.time.millisecond))) { + return false; + } + + // Step 9. + if (!fields.emplaceBack(NameToId(cx->names().isoMinute), + Int32Value(dateTime.time.minute))) { + return false; + } + + // Step 10. + if (!fields.emplaceBack(NameToId(cx->names().isoMonth), + Int32Value(dateTime.date.month))) { + return false; + } + + // Step 11. + if (!fields.emplaceBack(NameToId(cx->names().isoNanosecond), + Int32Value(dateTime.time.nanosecond))) { + return false; + } + + // Step 12. + if (!fields.emplaceBack(NameToId(cx->names().isoSecond), + Int32Value(dateTime.time.second))) { + return false; + } + + // Step 13. + if (!fields.emplaceBack(NameToId(cx->names().isoYear), + Int32Value(dateTime.date.year))) { + return false; + } + + // Step 14. + auto* obj = + NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.getISOFields ( ) + */ +static bool PlainDateTime_getISOFields(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_getISOFields>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.getCalendar ( ) + */ +static bool PlainDateTime_getCalendar(JSContext* cx, const CallArgs& args) { + auto* temporalDateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + Rooted<CalendarValue> calendar(cx, temporalDateTime->calendar()); + + // Step 3. + auto* obj = ToTemporalCalendarObject(cx, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.getCalendar ( ) + */ +static bool PlainDateTime_getCalendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_getCalendar>(cx, + args); +} + +/** + * Temporal.PlainDateTime.prototype.toZonedDateTime ( temporalTimeZoneLike [ , + * options ] ) + */ +static bool PlainDateTime_toZonedDateTime(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 3. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + auto disambiguation = TemporalDisambiguation::Compatible; + if (args.hasDefined(1)) { + // Step 4. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toZonedDateTime", args[1])); + if (!options) { + return false; + } + + // Step 5. + if (!ToTemporalDisambiguation(cx, options, &disambiguation)) { + return false; + } + } + + // Steps 6-7. + Instant instant; + if (!GetInstantFor(cx, timeZone, dateTime, disambiguation, &instant)) { + return false; + } + + // Step 8. + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toZonedDateTime ( temporalTimeZoneLike [ , + * options ] ) + */ +static bool PlainDateTime_toZonedDateTime(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toZonedDateTime>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.toPlainDate ( ) + */ +static bool PlainDateTime_toPlainDate(JSContext* cx, const CallArgs& args) { + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + Rooted<CalendarValue> calendar(cx, dateTime->calendar()); + + // Step 3. + auto* obj = CreateTemporalDate(cx, ToPlainDate(dateTime), calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toPlainDate ( ) + */ +static bool PlainDateTime_toPlainDate(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toPlainDate>(cx, + args); +} + +/** + * Temporal.PlainDateTime.prototype.toPlainYearMonth ( ) + */ +static bool PlainDateTime_toPlainYearMonth(JSContext* cx, + const CallArgs& args) { + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendarValue(cx, dateTime->calendar()); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return false; + } + + // Step 4. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 4. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, dateTime, fieldNames)); + if (!fields) { + return false; + } + + // Step 5. + auto obj = CalendarYearMonthFromFields(cx, calendar, fields); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toPlainYearMonth ( ) + */ +static bool PlainDateTime_toPlainYearMonth(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toPlainYearMonth>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.toPlainMonthDay ( ) + */ +static bool PlainDateTime_toPlainMonthDay(JSContext* cx, const CallArgs& args) { + Rooted<PlainDateTimeObject*> dateTime( + cx, &args.thisv().toObject().as<PlainDateTimeObject>()); + Rooted<CalendarValue> calendarValue(cx, dateTime->calendar()); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::MonthDayFromFields, + }, + &calendar)) { + return false; + } + + // Step 4. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::MonthCode}, + &fieldNames)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, dateTime, fieldNames)); + if (!fields) { + return false; + } + + // Steps 6-7. + auto obj = CalendarMonthDayFromFields(cx, calendar, fields); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toPlainMonthDay ( ) + */ +static bool PlainDateTime_toPlainMonthDay(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toPlainMonthDay>( + cx, args); +} + +/** + * Temporal.PlainDateTime.prototype.toPlainTime ( ) + */ +static bool PlainDateTime_toPlainTime(JSContext* cx, const CallArgs& args) { + auto* dateTime = &args.thisv().toObject().as<PlainDateTimeObject>(); + + // Step 3. + auto* obj = CreateTemporalTime(cx, ToPlainTime(dateTime)); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainDateTime.prototype.toPlainTime ( ) + */ +static bool PlainDateTime_toPlainTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainDateTime, PlainDateTime_toPlainTime>(cx, + args); +} + +const JSClass PlainDateTimeObject::class_ = { + "Temporal.PlainDateTime", + JSCLASS_HAS_RESERVED_SLOTS(PlainDateTimeObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_PlainDateTime), + JS_NULL_CLASS_OPS, + &PlainDateTimeObject::classSpec_, +}; + +const JSClass& PlainDateTimeObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec PlainDateTime_methods[] = { + JS_FN("from", PlainDateTime_from, 1, 0), + JS_FN("compare", PlainDateTime_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec PlainDateTime_prototype_methods[] = { + JS_FN("with", PlainDateTime_with, 1, 0), + JS_FN("withPlainTime", PlainDateTime_withPlainTime, 0, 0), + JS_FN("withPlainDate", PlainDateTime_withPlainDate, 1, 0), + JS_FN("withCalendar", PlainDateTime_withCalendar, 1, 0), + JS_FN("add", PlainDateTime_add, 1, 0), + JS_FN("subtract", PlainDateTime_subtract, 1, 0), + JS_FN("until", PlainDateTime_until, 1, 0), + JS_FN("since", PlainDateTime_since, 1, 0), + JS_FN("round", PlainDateTime_round, 1, 0), + JS_FN("equals", PlainDateTime_equals, 1, 0), + JS_FN("toString", PlainDateTime_toString, 0, 0), + JS_FN("toLocaleString", PlainDateTime_toLocaleString, 0, 0), + JS_FN("toJSON", PlainDateTime_toJSON, 0, 0), + JS_FN("valueOf", PlainDateTime_valueOf, 0, 0), + JS_FN("toZonedDateTime", PlainDateTime_toZonedDateTime, 1, 0), + JS_FN("toPlainDate", PlainDateTime_toPlainDate, 0, 0), + JS_FN("toPlainYearMonth", PlainDateTime_toPlainYearMonth, 0, 0), + JS_FN("toPlainMonthDay", PlainDateTime_toPlainMonthDay, 0, 0), + JS_FN("toPlainTime", PlainDateTime_toPlainTime, 0, 0), + JS_FN("getISOFields", PlainDateTime_getISOFields, 0, 0), + JS_FN("getCalendar", PlainDateTime_getCalendar, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec PlainDateTime_prototype_properties[] = { + JS_PSG("calendarId", PlainDateTime_calendarId, 0), + JS_PSG("year", PlainDateTime_year, 0), + JS_PSG("month", PlainDateTime_month, 0), + JS_PSG("monthCode", PlainDateTime_monthCode, 0), + JS_PSG("day", PlainDateTime_day, 0), + JS_PSG("hour", PlainDateTime_hour, 0), + JS_PSG("minute", PlainDateTime_minute, 0), + JS_PSG("second", PlainDateTime_second, 0), + JS_PSG("millisecond", PlainDateTime_millisecond, 0), + JS_PSG("microsecond", PlainDateTime_microsecond, 0), + JS_PSG("nanosecond", PlainDateTime_nanosecond, 0), + JS_PSG("dayOfWeek", PlainDateTime_dayOfWeek, 0), + JS_PSG("dayOfYear", PlainDateTime_dayOfYear, 0), + JS_PSG("weekOfYear", PlainDateTime_weekOfYear, 0), + JS_PSG("yearOfWeek", PlainDateTime_yearOfWeek, 0), + JS_PSG("daysInWeek", PlainDateTime_daysInWeek, 0), + JS_PSG("daysInMonth", PlainDateTime_daysInMonth, 0), + JS_PSG("daysInYear", PlainDateTime_daysInYear, 0), + JS_PSG("monthsInYear", PlainDateTime_monthsInYear, 0), + JS_PSG("inLeapYear", PlainDateTime_inLeapYear, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.PlainDateTime", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec PlainDateTimeObject::classSpec_ = { + GenericCreateConstructor<PlainDateTimeConstructor, 3, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PlainDateTimeObject>, + PlainDateTime_methods, + nullptr, + PlainDateTime_prototype_methods, + PlainDateTime_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/PlainDateTime.h b/js/src/builtin/temporal/PlainDateTime.h new file mode 100644 index 0000000000..3546fca903 --- /dev/null +++ b/js/src/builtin/temporal/PlainDateTime.h @@ -0,0 +1,263 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_PlainDateTime_h +#define builtin_temporal_PlainDateTime_h + +#include "mozilla/Assertions.h" + +#include <stdint.h> + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/Wrapped.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +class JS_PUBLIC_API JSTracer; + +namespace js { +struct ClassSpec; +class PlainObject; +} // namespace js + +namespace js::temporal { + +class PlainDateTimeObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + // TODO: Consider compacting fields to reduce object size. + // + // See also PlainDateObject and PlainTimeObject. + + static constexpr uint32_t ISO_YEAR_SLOT = 0; + static constexpr uint32_t ISO_MONTH_SLOT = 1; + static constexpr uint32_t ISO_DAY_SLOT = 2; + static constexpr uint32_t ISO_HOUR_SLOT = 3; + static constexpr uint32_t ISO_MINUTE_SLOT = 4; + static constexpr uint32_t ISO_SECOND_SLOT = 5; + static constexpr uint32_t ISO_MILLISECOND_SLOT = 6; + static constexpr uint32_t ISO_MICROSECOND_SLOT = 7; + static constexpr uint32_t ISO_NANOSECOND_SLOT = 8; + static constexpr uint32_t CALENDAR_SLOT = 9; + static constexpr uint32_t SLOT_COUNT = 10; + + int32_t isoYear() const { return getFixedSlot(ISO_YEAR_SLOT).toInt32(); } + + int32_t isoMonth() const { return getFixedSlot(ISO_MONTH_SLOT).toInt32(); } + + int32_t isoDay() const { return getFixedSlot(ISO_DAY_SLOT).toInt32(); } + + int32_t isoHour() const { return getFixedSlot(ISO_HOUR_SLOT).toInt32(); } + + int32_t isoMinute() const { return getFixedSlot(ISO_MINUTE_SLOT).toInt32(); } + + int32_t isoSecond() const { return getFixedSlot(ISO_SECOND_SLOT).toInt32(); } + + int32_t isoMillisecond() const { + return getFixedSlot(ISO_MILLISECOND_SLOT).toInt32(); + } + + int32_t isoMicrosecond() const { + return getFixedSlot(ISO_MICROSECOND_SLOT).toInt32(); + } + + int32_t isoNanosecond() const { + return getFixedSlot(ISO_NANOSECOND_SLOT).toInt32(); + } + + CalendarValue calendar() const { + return CalendarValue(getFixedSlot(CALENDAR_SLOT)); + } + + private: + static const ClassSpec classSpec_; +}; + +/** + * Extract the date fields from the PlainDateTime object. + */ +inline PlainDate ToPlainDate(const PlainDateTimeObject* dateTime) { + return {dateTime->isoYear(), dateTime->isoMonth(), dateTime->isoDay()}; +} + +/** + * Extract the time fields from the PlainDateTime object. + */ +inline PlainTime ToPlainTime(const PlainDateTimeObject* dateTime) { + return {dateTime->isoHour(), dateTime->isoMinute(), + dateTime->isoSecond(), dateTime->isoMillisecond(), + dateTime->isoMicrosecond(), dateTime->isoNanosecond()}; +} + +/** + * Extract the date-time fields from the PlainDateTime object. + */ +inline PlainDateTime ToPlainDateTime(const PlainDateTimeObject* dateTime) { + return {ToPlainDate(dateTime), ToPlainTime(dateTime)}; +} + +enum class TemporalUnit; + +#ifdef DEBUG +/** + * IsValidISODateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond ) + */ +bool IsValidISODateTime(const PlainDateTime& dateTime); +#endif + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +bool ISODateTimeWithinLimits(const PlainDateTime& dateTime); + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +bool ISODateTimeWithinLimits(const PlainDate& date); + +/** + * ISODateTimeWithinLimits ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond ) + */ +bool ISODateTimeWithinLimits(double year, double month, double day); + +/** + * CreateTemporalDateTime ( isoYear, isoMonth, isoDay, hour, minute, second, + * millisecond, microsecond, nanosecond, calendar [ , newTarget ] ) + */ +PlainDateTimeObject* CreateTemporalDateTime(JSContext* cx, + const PlainDateTime& dateTime, + JS::Handle<CalendarValue> calendar); + +/** + * ToTemporalDateTime ( item [ , options ] ) + */ +Wrapped<PlainDateTimeObject*> ToTemporalDateTime(JSContext* cx, + JS::Handle<JS::Value> item); + +/** + * ToTemporalDateTime ( item [ , options ] ) + */ +bool ToTemporalDateTime(JSContext* cx, JS::Handle<JS::Value> item, + PlainDateTime* result); + +/** + * InterpretTemporalDateTimeFields ( calendarRec, fields, options ) + */ +bool InterpretTemporalDateTimeFields(JSContext* cx, + JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields, + JS::Handle<PlainObject*> options, + PlainDateTime* result); + +/** + * InterpretTemporalDateTimeFields ( calendarRec, fields, options ) + */ +bool InterpretTemporalDateTimeFields(JSContext* cx, + JS::Handle<CalendarRecord> calendar, + JS::Handle<PlainObject*> fields, + PlainDateTime* result); + +/** + * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, + * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) + */ +bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, + const PlainDateTime& two, + JS::Handle<CalendarRecord> calendar, + TemporalUnit largestUnit, Duration* result); + +/** + * DifferenceISODateTime ( y1, mon1, d1, h1, min1, s1, ms1, mus1, ns1, y2, mon2, + * d2, h2, min2, s2, ms2, mus2, ns2, calendarRec, largestUnit, options ) + */ +bool DifferenceISODateTime(JSContext* cx, const PlainDateTime& one, + const PlainDateTime& two, + JS::Handle<CalendarRecord> calendar, + TemporalUnit largestUnit, + JS::Handle<PlainObject*> options, Duration* result); + +class PlainDateTimeWithCalendar { + PlainDateTime dateTime_; + CalendarValue calendar_; + + public: + PlainDateTimeWithCalendar() = default; + + PlainDateTimeWithCalendar(const PlainDateTime& dateTime, + const CalendarValue& calendar) + : dateTime_(dateTime), calendar_(calendar) { + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + } + + explicit PlainDateTimeWithCalendar(const PlainDateTimeObject* dateTime) + : PlainDateTimeWithCalendar(ToPlainDateTime(dateTime), + dateTime->calendar()) {} + + const auto& dateTime() const { return dateTime_; } + const auto& date() const { return dateTime_.date; } + const auto& time() const { return dateTime_.time; } + const auto& calendar() const { return calendar_; } + + // Allow implicit conversion to a calendar-less PlainDateTime. + operator const PlainDateTime&() const { return dateTime(); } + + void trace(JSTracer* trc) { calendar_.trace(trc); } + + const auto* calendarDoNotUse() const { return &calendar_; } +}; + +/** + * Extract the date-time fields from the PlainDateTimeWithCalendar object. + */ +inline const auto& ToPlainDateTime(const PlainDateTimeWithCalendar& dateTime) { + return dateTime.dateTime(); +} + +/** + * CreateTemporalDateTime ( isoYear, isoMonth, isoDay, hour, minute, second, + * millisecond, microsecond, nanosecond, calendar [ , newTarget ] ) + */ +bool CreateTemporalDateTime( + JSContext* cx, const PlainDateTime& dateTime, + JS::Handle<CalendarValue> calendar, + JS::MutableHandle<PlainDateTimeWithCalendar> result); + +} /* namespace js::temporal */ + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<temporal::PlainDateTimeWithCalendar, Wrapper> { + const auto& container() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + const auto& dateTime() const { return container().dateTime(); } + const auto& date() const { return container().date(); } + const auto& time() const { return container().time(); } + + auto calendar() const { + return JS::Handle<temporal::CalendarValue>::fromMarkedLocation( + container().calendarDoNotUse()); + } + + // Allow implicit conversion to a calendar-less PlainDateTime. + operator const temporal::PlainDateTime&() const { return dateTime(); } +}; + +} // namespace js + +#endif /* builtin_temporal_PlainDateTime_h */ diff --git a/js/src/builtin/temporal/PlainMonthDay.cpp b/js/src/builtin/temporal/PlainMonthDay.cpp new file mode 100644 index 0000000000..f97b7ad68c --- /dev/null +++ b/js/src/builtin/temporal/PlainMonthDay.cpp @@ -0,0 +1,1000 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/PlainMonthDay.h" + +#include "mozilla/Assertions.h" + +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.h" +#include "builtin/temporal/TemporalParser.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/ToString.h" +#include "builtin/temporal/Wrapped.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "ds/IdValuePair.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCVector.h" +#include "js/Id.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/BytecodeUtil.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.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 IsPlainMonthDay(Handle<Value> v) { + return v.isObject() && v.toObject().is<PlainMonthDayObject>(); +} + +/** + * CreateTemporalMonthDay ( isoMonth, isoDay, calendar, referenceISOYear [ , + * newTarget ] ) + */ +static PlainMonthDayObject* CreateTemporalMonthDay( + JSContext* cx, const CallArgs& args, double isoYear, double isoMonth, + double isoDay, Handle<CalendarValue> calendar) { + MOZ_ASSERT(IsInteger(isoYear)); + MOZ_ASSERT(IsInteger(isoMonth)); + MOZ_ASSERT(IsInteger(isoDay)); + + // Step 1. + if (!ThrowIfInvalidISODate(cx, isoYear, isoMonth, isoDay)) { + return nullptr; + } + + // Step 2. + if (!ISODateTimeWithinLimits(isoYear, isoMonth, isoDay)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_MONTH_DAY_INVALID); + return nullptr; + } + + // Steps 3-4. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PlainMonthDay, + &proto)) { + return nullptr; + } + + auto* obj = NewObjectWithClassProto<PlainMonthDayObject>(cx, proto); + if (!obj) { + return nullptr; + } + + // Step 5. + obj->setFixedSlot(PlainMonthDayObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 6. + obj->setFixedSlot(PlainMonthDayObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 7. + obj->setFixedSlot(PlainMonthDayObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 8. + obj->setFixedSlot(PlainMonthDayObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 9. + return obj; +} + +/** + * CreateTemporalMonthDay ( isoMonth, isoDay, calendar, referenceISOYear [ , + * newTarget ] ) + */ +PlainMonthDayObject* js::temporal::CreateTemporalMonthDay( + JSContext* cx, const PlainDate& date, Handle<CalendarValue> calendar) { + auto& [isoYear, isoMonth, isoDay] = date; + + // Step 1. + if (!ThrowIfInvalidISODate(cx, date)) { + return nullptr; + } + + // Step 2. + if (!ISODateTimeWithinLimits(date)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_MONTH_DAY_INVALID); + return nullptr; + } + + // Steps 3-4. + auto* obj = NewBuiltinClassInstance<PlainMonthDayObject>(cx); + if (!obj) { + return nullptr; + } + + // Step 5. + obj->setFixedSlot(PlainMonthDayObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 6. + obj->setFixedSlot(PlainMonthDayObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 7. + obj->setFixedSlot(PlainMonthDayObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 8. + obj->setFixedSlot(PlainMonthDayObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 9. + return obj; +} + +template <typename T, typename... Ts> +static bool ToTemporalCalendarForMonthDay(JSContext* cx, + Handle<JSObject*> object, + MutableHandle<CalendarValue> result) { + if (auto* unwrapped = object->maybeUnwrapIf<T>()) { + result.set(unwrapped->calendar()); + return result.wrap(cx); + } + + if constexpr (sizeof...(Ts) > 0) { + return ToTemporalCalendarForMonthDay<Ts...>(cx, object, result); + } + + result.set(CalendarValue()); + return true; +} + +/** + * ToTemporalMonthDay ( item [ , options ] ) + */ +static Wrapped<PlainMonthDayObject*> ToTemporalMonthDay( + JSContext* cx, Handle<Value> item, + Handle<JSObject*> maybeOptions = nullptr) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainObject*> maybeResolvedOptions(cx); + if (maybeOptions) { + maybeResolvedOptions = SnapshotOwnProperties(cx, maybeOptions); + if (!maybeResolvedOptions) { + return nullptr; + } + } + + // Step 3. + if (item.isObject()) { + Rooted<JSObject*> itemObj(cx, &item.toObject()); + + // Step 3.a. + if (itemObj->canUnwrapAs<PlainMonthDayObject>()) { + return itemObj; + } + + // Steps 3.b-c. + Rooted<CalendarValue> calendarValue(cx); + if (!::ToTemporalCalendarForMonthDay<PlainDateObject, PlainDateTimeObject, + PlainYearMonthObject, + ZonedDateTimeObject>(cx, itemObj, + &calendarValue)) { + return nullptr; + } + if (!calendarValue) { + // Step 3.c.i. + Rooted<Value> calendarLike(cx); + if (!GetProperty(cx, itemObj, itemObj, cx->names().calendar, + &calendarLike)) { + return nullptr; + } + + // Step 3.c.ii. + if (!ToTemporalCalendarWithISODefault(cx, calendarLike, &calendarValue)) { + return nullptr; + } + } + + // Step 3.d. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::MonthDayFromFields, + }, + &calendar)) { + return nullptr; + } + + // Step 3.e. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return nullptr; + } + + // Step 3.f. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, itemObj, fieldNames)); + if (!fields) { + return nullptr; + } + + // Step 3.g. + if (maybeResolvedOptions) { + return js::temporal::CalendarMonthDayFromFields(cx, calendar, fields, + maybeResolvedOptions); + } + return js::temporal::CalendarMonthDayFromFields(cx, calendar, fields); + } + + // Step 4. + if (!item.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, item, + nullptr, "not a string"); + return nullptr; + } + Rooted<JSString*> string(cx, item.toString()); + + // Step 5. + PlainDate result; + bool hasYear; + Rooted<JSString*> calendarString(cx); + if (!ParseTemporalMonthDayString(cx, string, &result, &hasYear, + &calendarString)) { + return nullptr; + } + + // Steps 6-9. + Rooted<CalendarValue> calendarValue(cx, CalendarValue(cx->names().iso8601)); + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendarValue)) { + return nullptr; + } + } + + // Step 10. + if (maybeResolvedOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return nullptr; + } + } + + // Step 11. + if (!hasYear) { + // Step 11.a. + MOZ_ASSERT(calendarValue.isString() && + EqualStrings(calendarValue.toString(), cx->names().iso8601)); + + // Step 11.b. + constexpr int32_t referenceISOYear = 1972; + + // Step 11.a. + return CreateTemporalMonthDay( + cx, {referenceISOYear, result.month, result.day}, calendarValue); + } + + // Step 12. + Rooted<PlainMonthDayObject*> obj( + cx, CreateTemporalMonthDay(cx, result, calendarValue)); + if (!obj) { + return nullptr; + } + + // FIXME: spec bug - missing call to CreateCalendarMethodsRecord + + // Step 13. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::MonthDayFromFields, + }, + &calendar)) { + return nullptr; + } + + // Steps 14-15. + return CalendarMonthDayFromFields(cx, calendar, obj); +} + +/** + * ToTemporalMonthDay ( item [ , options ] ) + */ +static bool ToTemporalMonthDay(JSContext* cx, Handle<Value> item, + PlainDate* result, + MutableHandle<CalendarValue> calendar) { + auto* obj = ToTemporalMonthDay(cx, item).unwrapOrNull(); + if (!obj) { + return false; + } + + *result = ToPlainDate(obj); + calendar.set(obj->calendar()); + return calendar.wrap(cx); +} + +/** + * Temporal.PlainMonthDay ( isoMonth, isoDay [ , calendarLike [ , + * referenceISOYear ] ] ) + */ +static bool PlainMonthDayConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.PlainMonthDay")) { + return false; + } + + // Step 3. + double isoMonth; + if (!ToIntegerWithTruncation(cx, args.get(0), "month", &isoMonth)) { + return false; + } + + // Step 4. + double isoDay; + if (!ToIntegerWithTruncation(cx, args.get(1), "day", &isoDay)) { + return false; + } + + // Step 5. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendarWithISODefault(cx, args.get(2), &calendar)) { + return false; + } + + // Steps 2 and 6. + double isoYear = 1972; + if (args.hasDefined(3)) { + if (!ToIntegerWithTruncation(cx, args[3], "year", &isoYear)) { + return false; + } + } + + // Step 7. + auto* monthDay = + CreateTemporalMonthDay(cx, args, isoYear, isoMonth, isoDay, calendar); + if (!monthDay) { + return false; + } + + args.rval().setObject(*monthDay); + return true; +} + +/** + * Temporal.PlainMonthDay.from ( item [ , options ] ) + */ +static bool PlainMonthDay_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "from", args[1]); + if (!options) { + return false; + } + } + + // Step 2. + if (args.get(0).isObject()) { + JSObject* item = &args[0].toObject(); + + if (auto* monthDay = item->maybeUnwrapIf<PlainMonthDayObject>()) { + auto date = ToPlainDate(monthDay); + + Rooted<CalendarValue> calendar(cx, monthDay->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + if (options) { + // Step 2.a. + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, options, &ignored)) { + return false; + } + } + + // Step 2.b. + auto* obj = CreateTemporalMonthDay(cx, date, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + } + + // Step 3. + auto obj = ToTemporalMonthDay(cx, args.get(0), options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * get Temporal.PlainMonthDay.prototype.calendarId + */ +static bool PlainMonthDay_calendarId(JSContext* cx, const CallArgs& args) { + auto* monthDay = &args.thisv().toObject().as<PlainMonthDayObject>(); + + // Step 3. + Rooted<CalendarValue> calendar(cx, monthDay->calendar()); + auto* calendarId = ToTemporalCalendarIdentifier(cx, calendar); + if (!calendarId) { + return false; + } + + args.rval().setString(calendarId); + return true; +} + +/** + * get Temporal.PlainMonthDay.prototype.calendarId + */ +static bool PlainMonthDay_calendarId(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_calendarId>(cx, + args); +} + +/** + * get Temporal.PlainMonthDay.prototype.monthCode + */ +static bool PlainMonthDay_monthCode(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + Rooted<CalendarValue> calendar(cx, monthDay->calendar()); + + // Step 4. + return CalendarMonthCode(cx, calendar, monthDay, args.rval()); +} + +/** + * get Temporal.PlainMonthDay.prototype.monthCode + */ +static bool PlainMonthDay_monthCode(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_monthCode>(cx, + args); +} + +/** + * get Temporal.PlainMonthDay.prototype.day + */ +static bool PlainMonthDay_day(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + Rooted<CalendarValue> calendar(cx, monthDay->calendar()); + + // Step 4. + return CalendarDay(cx, calendar, monthDay, args.rval()); +} + +/** + * get Temporal.PlainMonthDay.prototype.day + */ +static bool PlainMonthDay_day(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_day>(cx, args); +} + +/** + * Temporal.PlainMonthDay.prototype.with ( temporalMonthDayLike [ , options ] ) + */ +static bool PlainMonthDay_with(JSContext* cx, const CallArgs& args) { + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + Rooted<CalendarValue> calendarValue(cx, monthDay->calendar()); + + // Step 3. + Rooted<JSObject*> temporalMonthDayLike( + cx, RequireObjectArg(cx, "temporalMonthDayLike", "with", args.get(0))); + if (!temporalMonthDayLike) { + return false; + } + + // Step 4. + if (!RejectTemporalLikeObject(cx, temporalMonthDayLike)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "with", args[1])); + if (!options) { + return false; + } + resolvedOptions = SnapshotOwnProperties(cx, options); + } else { + resolvedOptions = NewPlainObjectWithProto(cx, nullptr); + } + if (!resolvedOptions) { + return false; + } + + // Step 6. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::MergeFields, + CalendarMethod::MonthDayFromFields, + }, + &calendar)) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, monthDay, fieldNames)); + if (!fields) { + return false; + } + + // Step 9. + Rooted<PlainObject*> partialMonthDay( + cx, PreparePartialTemporalFields(cx, temporalMonthDayLike, fieldNames)); + if (!partialMonthDay) { + return false; + } + + // Step 10. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, partialMonthDay)); + if (!mergedFields) { + return false; + } + + // Step 11. + fields = PrepareTemporalFields(cx, mergedFields, fieldNames); + if (!fields) { + return false; + } + + // Step 12. + auto obj = js::temporal::CalendarMonthDayFromFields(cx, calendar, fields, + resolvedOptions); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.with ( temporalMonthDayLike [ , options ] ) + */ +static bool PlainMonthDay_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_with>(cx, args); +} + +/** + * Temporal.PlainMonthDay.prototype.equals ( other ) + */ +static bool PlainMonthDay_equals(JSContext* cx, const CallArgs& args) { + auto* monthDay = &args.thisv().toObject().as<PlainMonthDayObject>(); + auto date = ToPlainDate(monthDay); + Rooted<CalendarValue> calendar(cx, monthDay->calendar()); + + // Step 3. + PlainDate other; + Rooted<CalendarValue> otherCalendar(cx); + if (!ToTemporalMonthDay(cx, args.get(0), &other, &otherCalendar)) { + return false; + } + + // Steps 4-7. + bool equals = date == other; + if (equals && !CalendarEquals(cx, calendar, otherCalendar, &equals)) { + return false; + } + + args.rval().setBoolean(equals); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.equals ( other ) + */ +static bool PlainMonthDay_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_equals>(cx, args); +} + +/** + * Temporal.PlainMonthDay.prototype.toString ( [ options ] ) + */ +static bool PlainMonthDay_toString(JSContext* cx, const CallArgs& args) { + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + + auto showCalendar = CalendarOption::Auto; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Step 4. + if (!ToCalendarNameOption(cx, options, &showCalendar)) { + return false; + } + } + + // Step 5. + JSString* str = TemporalMonthDayToString(cx, monthDay, showCalendar); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.toString ( [ options ] ) + */ +static bool PlainMonthDay_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_toString>(cx, + args); +} + +/** + * Temporal.PlainMonthDay.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainMonthDay_toLocaleString(JSContext* cx, const CallArgs& args) { + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + + // Step 3. + JSString* str = TemporalMonthDayToString(cx, monthDay, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainMonthDay_toLocaleString(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_toLocaleString>( + cx, args); +} + +/** + * Temporal.PlainMonthDay.prototype.toJSON ( ) + */ +static bool PlainMonthDay_toJSON(JSContext* cx, const CallArgs& args) { + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + + // Step 3. + JSString* str = TemporalMonthDayToString(cx, monthDay, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.toJSON ( ) + */ +static bool PlainMonthDay_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_toJSON>(cx, args); +} + +/** + * Temporal.PlainMonthDay.prototype.valueOf ( ) + */ +static bool PlainMonthDay_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "PlainMonthDay", "primitive type"); + return false; +} + +/** + * Temporal.PlainMonthDay.prototype.toPlainDate ( item ) + */ +static bool PlainMonthDay_toPlainDate(JSContext* cx, const CallArgs& args) { + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + + // Step 3. + Rooted<JSObject*> item( + cx, RequireObjectArg(cx, "item", "toPlainDate", args.get(0))); + if (!item) { + return false; + } + + // Step 4. + Rooted<CalendarValue> calendarValue(cx, monthDay->calendar()); + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + CalendarMethod::MergeFields, + }, + &calendar)) { + return false; + } + + // Step 5. + JS::RootedVector<PropertyKey> receiverFieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::MonthCode}, + &receiverFieldNames)) { + return false; + } + + // Step 6. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, monthDay, receiverFieldNames)); + if (!fields) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> inputFieldNames(cx); + if (!CalendarFields(cx, calendar, {CalendarField::Year}, &inputFieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> inputFields( + cx, PrepareTemporalFields(cx, item, inputFieldNames)); + if (!inputFields) { + return false; + } + + // Step 9. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, inputFields)); + if (!mergedFields) { + return false; + } + + // Step 10. + JS::RootedVector<PropertyKey> concatenatedFieldNames(cx); + if (!ConcatTemporalFieldNames(receiverFieldNames, inputFieldNames, + concatenatedFieldNames.get())) { + return false; + } + + // Step 11. + Rooted<PlainObject*> mergedFromConcatenatedFields( + cx, PrepareTemporalFields(cx, mergedFields, concatenatedFieldNames)); + if (!mergedFromConcatenatedFields) { + return false; + } + + // Step 12. + Rooted<PlainObject*> options(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!options) { + return false; + } + + // Step 13. + Rooted<Value> overflow(cx, StringValue(cx->names().constrain)); + if (!DefineDataProperty(cx, options, cx->names().overflow, overflow)) { + return false; + } + + // Step 14. + auto obj = js::temporal::CalendarDateFromFields( + cx, calendar, mergedFromConcatenatedFields, options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.toPlainDate ( item ) + */ +static bool PlainMonthDay_toPlainDate(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_toPlainDate>(cx, + args); +} + +/** + * Temporal.PlainMonthDay.prototype.getISOFields ( ) + */ +static bool PlainMonthDay_getISOFields(JSContext* cx, const CallArgs& args) { + Rooted<PlainMonthDayObject*> monthDay( + cx, &args.thisv().toObject().as<PlainMonthDayObject>()); + + // Step 3. + Rooted<IdValueVector> fields(cx, IdValueVector(cx)); + + // Step 4. + if (!fields.emplaceBack(NameToId(cx->names().calendar), + monthDay->calendar().toValue())) { + return false; + } + + // Step 5. + if (!fields.emplaceBack(NameToId(cx->names().isoDay), + Int32Value(monthDay->isoDay()))) { + return false; + } + + // Step 6. + if (!fields.emplaceBack(NameToId(cx->names().isoMonth), + Int32Value(monthDay->isoMonth()))) { + return false; + } + + // Step 7. + if (!fields.emplaceBack(NameToId(cx->names().isoYear), + Int32Value(monthDay->isoYear()))) { + return false; + } + + // Step 8. + auto* obj = + NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.getISOFields ( ) + */ +static bool PlainMonthDay_getISOFields(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_getISOFields>( + cx, args); +} + +/** + * Temporal.PlainMonthDay.prototype.getCalendar ( ) + */ +static bool PlainMonthDay_getCalendar(JSContext* cx, const CallArgs& args) { + auto* monthDay = &args.thisv().toObject().as<PlainMonthDayObject>(); + Rooted<CalendarValue> calendar(cx, monthDay->calendar()); + + // Step 3. + auto* obj = ToTemporalCalendarObject(cx, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainMonthDay.prototype.getCalendar ( ) + */ +static bool PlainMonthDay_getCalendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainMonthDay, PlainMonthDay_getCalendar>(cx, + args); +} + +const JSClass PlainMonthDayObject::class_ = { + "Temporal.PlainMonthDay", + JSCLASS_HAS_RESERVED_SLOTS(PlainMonthDayObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_PlainMonthDay), + JS_NULL_CLASS_OPS, + &PlainMonthDayObject::classSpec_, +}; + +const JSClass& PlainMonthDayObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec PlainMonthDay_methods[] = { + JS_FN("from", PlainMonthDay_from, 1, 0), + JS_FS_END, +}; + +static const JSFunctionSpec PlainMonthDay_prototype_methods[] = { + JS_FN("with", PlainMonthDay_with, 1, 0), + JS_FN("equals", PlainMonthDay_equals, 1, 0), + JS_FN("toString", PlainMonthDay_toString, 0, 0), + JS_FN("toLocaleString", PlainMonthDay_toLocaleString, 0, 0), + JS_FN("toJSON", PlainMonthDay_toJSON, 0, 0), + JS_FN("valueOf", PlainMonthDay_valueOf, 0, 0), + JS_FN("toPlainDate", PlainMonthDay_toPlainDate, 1, 0), + JS_FN("getISOFields", PlainMonthDay_getISOFields, 0, 0), + JS_FN("getCalendar", PlainMonthDay_getCalendar, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec PlainMonthDay_prototype_properties[] = { + JS_PSG("calendarId", PlainMonthDay_calendarId, 0), + JS_PSG("monthCode", PlainMonthDay_monthCode, 0), + JS_PSG("day", PlainMonthDay_day, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.PlainMonthDay", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec PlainMonthDayObject::classSpec_ = { + GenericCreateConstructor<PlainMonthDayConstructor, 2, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PlainMonthDayObject>, + PlainMonthDay_methods, + nullptr, + PlainMonthDay_prototype_methods, + PlainMonthDay_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/PlainMonthDay.h b/js/src/builtin/temporal/PlainMonthDay.h new file mode 100644 index 0000000000..1a48513387 --- /dev/null +++ b/js/src/builtin/temporal/PlainMonthDay.h @@ -0,0 +1,66 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_PlainMonthDay_h +#define builtin_temporal_PlainMonthDay_h + +#include <stdint.h> + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/TemporalTypes.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +namespace js { +struct ClassSpec; +} + +namespace js::temporal { + +class PlainMonthDayObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t ISO_YEAR_SLOT = 0; + static constexpr uint32_t ISO_MONTH_SLOT = 1; + static constexpr uint32_t ISO_DAY_SLOT = 2; + static constexpr uint32_t CALENDAR_SLOT = 3; + static constexpr uint32_t SLOT_COUNT = 4; + + int32_t isoYear() const { return getFixedSlot(ISO_YEAR_SLOT).toInt32(); } + + int32_t isoMonth() const { return getFixedSlot(ISO_MONTH_SLOT).toInt32(); } + + int32_t isoDay() const { return getFixedSlot(ISO_DAY_SLOT).toInt32(); } + + CalendarValue calendar() const { + return CalendarValue(getFixedSlot(CALENDAR_SLOT)); + } + + private: + static const ClassSpec classSpec_; +}; + +/** + * Extract the date fields from the PlainMonthDay object. + */ +inline PlainDate ToPlainDate(const PlainMonthDayObject* monthDay) { + return {monthDay->isoYear(), monthDay->isoMonth(), monthDay->isoDay()}; +} + +/** + * CreateTemporalMonthDay ( isoMonth, isoDay, calendar, referenceISOYear [ , + * newTarget ] ) + */ +PlainMonthDayObject* CreateTemporalMonthDay(JSContext* cx, + const PlainDate& date, + JS::Handle<CalendarValue> calendar); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_PlainMonthDay_h */ diff --git a/js/src/builtin/temporal/PlainTime.cpp b/js/src/builtin/temporal/PlainTime.cpp new file mode 100644 index 0000000000..9501b5853b --- /dev/null +++ b/js/src/builtin/temporal/PlainTime.cpp @@ -0,0 +1,2641 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/PlainTime.h" + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/Maybe.h" + +#include <algorithm> +#include <cmath> +#include <cstdlib> +#include <type_traits> +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/Instant.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainDateTime.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/ToString.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "ds/IdValuePair.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/Value.h" +#include "vm/BigIntType.h" +#include "vm/BytecodeUtil.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.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 IsPlainTime(Handle<Value> v) { + return v.isObject() && v.toObject().is<PlainTimeObject>(); +} + +#ifdef DEBUG +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +template <typename T> +static bool IsValidTime(T hour, T minute, T second, T millisecond, + T microsecond, T nanosecond) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(hour)); + MOZ_ASSERT(IsInteger(minute)); + MOZ_ASSERT(IsInteger(second)); + MOZ_ASSERT(IsInteger(millisecond)); + MOZ_ASSERT(IsInteger(microsecond)); + MOZ_ASSERT(IsInteger(nanosecond)); + + // Step 2. + if (hour < 0 || hour > 23) { + return false; + } + + // Step 3. + if (minute < 0 || minute > 59) { + return false; + } + + // Step 4. + if (second < 0 || second > 59) { + return false; + } + + // Step 5. + if (millisecond < 0 || millisecond > 999) { + return false; + } + + // Step 6. + if (microsecond < 0 || microsecond > 999) { + return false; + } + + // Step 7. + if (nanosecond < 0 || nanosecond > 999) { + return false; + } + + // Step 8. + return true; +} + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool js::temporal::IsValidTime(const PlainTime& time) { + auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + return ::IsValidTime(hour, minute, second, millisecond, microsecond, + nanosecond); +} + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool js::temporal::IsValidTime(double hour, double minute, double second, + double millisecond, double microsecond, + double nanosecond) { + return ::IsValidTime(hour, minute, second, millisecond, microsecond, + nanosecond); +} +#endif + +static void ReportInvalidTimeValue(JSContext* cx, const char* name, int32_t min, + int32_t max, double num) { + Int32ToCStringBuf minCbuf; + const char* minStr = Int32ToCString(&minCbuf, min); + + Int32ToCStringBuf maxCbuf; + const char* maxStr = Int32ToCString(&maxCbuf, max); + + ToCStringBuf numCbuf; + const char* numStr = NumberToCString(&numCbuf, num); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_TIME_INVALID_VALUE, name, + minStr, maxStr, numStr); +} + +template <typename T> +static inline bool ThrowIfInvalidTimeValue(JSContext* cx, const char* name, + int32_t min, int32_t max, T num) { + if (min <= num && num <= max) { + return true; + } + ReportInvalidTimeValue(cx, name, min, max, num); + return false; +} + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +template <typename T> +static bool ThrowIfInvalidTime(JSContext* cx, T hour, T minute, T second, + T millisecond, T microsecond, T nanosecond) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(hour)); + MOZ_ASSERT(IsInteger(minute)); + MOZ_ASSERT(IsInteger(second)); + MOZ_ASSERT(IsInteger(millisecond)); + MOZ_ASSERT(IsInteger(microsecond)); + MOZ_ASSERT(IsInteger(nanosecond)); + + // Step 2. + if (!ThrowIfInvalidTimeValue(cx, "hour", 0, 23, hour)) { + return false; + } + + // Step 3. + if (!ThrowIfInvalidTimeValue(cx, "minute", 0, 59, minute)) { + return false; + } + + // Step 4. + if (!ThrowIfInvalidTimeValue(cx, "second", 0, 59, second)) { + return false; + } + + // Step 5. + if (!ThrowIfInvalidTimeValue(cx, "millisecond", 0, 999, millisecond)) { + return false; + } + + // Step 6. + if (!ThrowIfInvalidTimeValue(cx, "microsecond", 0, 999, microsecond)) { + return false; + } + + // Step 7. + if (!ThrowIfInvalidTimeValue(cx, "nanosecond", 0, 999, nanosecond)) { + return false; + } + + // Step 8. + return true; +} + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool js::temporal::ThrowIfInvalidTime(JSContext* cx, const PlainTime& time) { + auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + return ::ThrowIfInvalidTime(cx, hour, minute, second, millisecond, + microsecond, nanosecond); +} + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool js::temporal::ThrowIfInvalidTime(JSContext* cx, double hour, double minute, + double second, double millisecond, + double microsecond, double nanosecond) { + return ::ThrowIfInvalidTime(cx, hour, minute, second, millisecond, + microsecond, nanosecond); +} + +/** + * ConstrainTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +static PlainTime ConstrainTime(double hour, double minute, double second, + double millisecond, double microsecond, + double nanosecond) { + // Step 1. + MOZ_ASSERT(IsInteger(hour)); + MOZ_ASSERT(IsInteger(minute)); + MOZ_ASSERT(IsInteger(second)); + MOZ_ASSERT(IsInteger(millisecond)); + MOZ_ASSERT(IsInteger(microsecond)); + MOZ_ASSERT(IsInteger(nanosecond)); + + // Steps 2-8. + return { + int32_t(std::clamp(hour, 0.0, 23.0)), + int32_t(std::clamp(minute, 0.0, 59.0)), + int32_t(std::clamp(second, 0.0, 59.0)), + int32_t(std::clamp(millisecond, 0.0, 999.0)), + int32_t(std::clamp(microsecond, 0.0, 999.0)), + int32_t(std::clamp(nanosecond, 0.0, 999.0)), + }; +} + +/** + * RegulateTime ( hour, minute, second, millisecond, microsecond, nanosecond, + * overflow ) + */ +bool js::temporal::RegulateTime(JSContext* cx, const TimeRecord& time, + TemporalOverflow overflow, PlainTime* result) { + auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + + // Step 1. + MOZ_ASSERT(IsInteger(hour)); + MOZ_ASSERT(IsInteger(minute)); + MOZ_ASSERT(IsInteger(second)); + MOZ_ASSERT(IsInteger(millisecond)); + MOZ_ASSERT(IsInteger(microsecond)); + MOZ_ASSERT(IsInteger(nanosecond)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + if (overflow == TemporalOverflow::Constrain) { + *result = ConstrainTime(hour, minute, second, millisecond, microsecond, + nanosecond); + return true; + } + + // Step 4.a. + MOZ_ASSERT(overflow == TemporalOverflow::Reject); + + // Step 4.b. + if (!ThrowIfInvalidTime(cx, hour, minute, second, millisecond, microsecond, + nanosecond)) { + return false; + } + + // Step 4.c. + *result = { + int32_t(hour), int32_t(minute), int32_t(second), + int32_t(millisecond), int32_t(microsecond), int32_t(nanosecond), + }; + return true; +} + +/** + * CreateTemporalTime ( hour, minute, second, millisecond, microsecond, + * nanosecond [ , newTarget ] ) + */ +static PlainTimeObject* CreateTemporalTime(JSContext* cx, const CallArgs& args, + double hour, double minute, + double second, double millisecond, + double microsecond, + double nanosecond) { + MOZ_ASSERT(IsInteger(hour)); + MOZ_ASSERT(IsInteger(minute)); + MOZ_ASSERT(IsInteger(second)); + MOZ_ASSERT(IsInteger(millisecond)); + MOZ_ASSERT(IsInteger(microsecond)); + MOZ_ASSERT(IsInteger(nanosecond)); + + // Step 1. + if (!ThrowIfInvalidTime(cx, hour, minute, second, millisecond, microsecond, + nanosecond)) { + return nullptr; + } + + // Steps 2-3. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PlainTime, + &proto)) { + return nullptr; + } + + auto* object = NewObjectWithClassProto<PlainTimeObject>(cx, proto); + if (!object) { + return nullptr; + } + + // Step 4. + object->setFixedSlot(PlainTimeObject::ISO_HOUR_SLOT, Int32Value(hour)); + + // Step 5. + object->setFixedSlot(PlainTimeObject::ISO_MINUTE_SLOT, Int32Value(minute)); + + // Step 6. + object->setFixedSlot(PlainTimeObject::ISO_SECOND_SLOT, Int32Value(second)); + + // Step 7. + object->setFixedSlot(PlainTimeObject::ISO_MILLISECOND_SLOT, + Int32Value(millisecond)); + + // Step 8. + object->setFixedSlot(PlainTimeObject::ISO_MICROSECOND_SLOT, + Int32Value(microsecond)); + + // Step 9. + object->setFixedSlot(PlainTimeObject::ISO_NANOSECOND_SLOT, + Int32Value(nanosecond)); + + // Step 10. + return object; +} + +/** + * CreateTemporalTime ( hour, minute, second, millisecond, microsecond, + * nanosecond [ , newTarget ] ) + */ +PlainTimeObject* js::temporal::CreateTemporalTime(JSContext* cx, + const PlainTime& time) { + auto& [hour, minute, second, millisecond, microsecond, nanosecond] = time; + + // Step 1. + if (!ThrowIfInvalidTime(cx, time)) { + return nullptr; + } + + // Steps 2-3. + auto* object = NewBuiltinClassInstance<PlainTimeObject>(cx); + if (!object) { + return nullptr; + } + + // Step 4. + object->setFixedSlot(PlainTimeObject::ISO_HOUR_SLOT, Int32Value(hour)); + + // Step 5. + object->setFixedSlot(PlainTimeObject::ISO_MINUTE_SLOT, Int32Value(minute)); + + // Step 6. + object->setFixedSlot(PlainTimeObject::ISO_SECOND_SLOT, Int32Value(second)); + + // Step 7. + object->setFixedSlot(PlainTimeObject::ISO_MILLISECOND_SLOT, + Int32Value(millisecond)); + + // Step 8. + object->setFixedSlot(PlainTimeObject::ISO_MICROSECOND_SLOT, + Int32Value(microsecond)); + + // Step 9. + object->setFixedSlot(PlainTimeObject::ISO_NANOSECOND_SLOT, + Int32Value(nanosecond)); + + // Step 10. + return object; +} + +/** + * CreateTimeDurationRecord ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds ) + */ +static TimeDuration CreateTimeDurationRecord(double days, int32_t hours, + int32_t minutes, int32_t seconds, + int32_t milliseconds, + int32_t microseconds, + int32_t nanoseconds) { + // Step 1. + MOZ_ASSERT(IsValidDuration({0, 0, 0, days, double(hours), double(minutes), + double(seconds), double(microseconds), + double(nanoseconds)})); + + // Step 2. + return { + days, + double(hours), + double(minutes), + double(seconds), + double(milliseconds), + double(microseconds), + double(nanoseconds), + }; +} + +/** + * DurationSign ( years, months, weeks, days, hours, minutes, seconds, + * milliseconds, microseconds, nanoseconds ) + */ +static int32_t DurationSign(int32_t hours, int32_t minutes, int32_t seconds, + int32_t milliseconds, int32_t microseconds, + int32_t nanoseconds) { + // Step 1. (Loop unrolled) + if (hours) { + return hours > 0 ? 1 : -1; + } + if (minutes) { + return minutes > 0 ? 1 : -1; + } + if (seconds) { + return seconds > 0 ? 1 : -1; + } + if (milliseconds) { + return milliseconds > 0 ? 1 : -1; + } + if (microseconds) { + return microseconds > 0 ? 1 : -1; + } + if (nanoseconds) { + return nanoseconds > 0 ? 1 : -1; + } + + // Step 2. + return 0; +} + +/** + * BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +template <typename IntT> +static BalancedTime BalanceTime(IntT hour, IntT minute, IntT second, + IntT millisecond, IntT microsecond, + IntT nanosecond) { + // Combined floor'ed division and modulo operation. + auto divmod = [](IntT dividend, int32_t divisor, int32_t* remainder) { + MOZ_ASSERT(divisor > 0); + + IntT quotient = dividend / divisor; + *remainder = dividend % divisor; + + // The remainder is negative, add the divisor and simulate a floor instead + // of trunc division. + if (*remainder < 0) { + *remainder += divisor; + quotient -= 1; + } + + return quotient; + }; + + PlainTime time = {}; + + // Steps 1-2. + microsecond += divmod(nanosecond, 1000, &time.nanosecond); + + // Steps 3-4. + millisecond += divmod(microsecond, 1000, &time.microsecond); + + // Steps 5-6. + second += divmod(millisecond, 1000, &time.millisecond); + + // Steps 7-8. + minute += divmod(second, 60, &time.second); + + // Steps 9-10. + hour += divmod(minute, 60, &time.minute); + + // Steps 11-12. + int32_t days = divmod(hour, 24, &time.hour); + + // Step 13. + MOZ_ASSERT(IsValidTime(time)); + return {days, time}; +} + +/** + * BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +static BalancedTime BalanceTime(int32_t hour, int32_t minute, int32_t second, + int32_t millisecond, int32_t microsecond, + int32_t nanosecond) { + MOZ_ASSERT(-24 < hour && hour < 2 * 24); + MOZ_ASSERT(-60 < minute && minute < 2 * 60); + MOZ_ASSERT(-60 < second && second < 2 * 60); + MOZ_ASSERT(-1000 < millisecond && millisecond < 2 * 1000); + MOZ_ASSERT(-1000 < microsecond && microsecond < 2 * 1000); + MOZ_ASSERT(-1000 < nanosecond && nanosecond < 2 * 1000); + + return BalanceTime<int32_t>(hour, minute, second, millisecond, microsecond, + nanosecond); +} + +/** + * BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +BalancedTime js::temporal::BalanceTime(const PlainTime& time, + int64_t nanoseconds) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(std::abs(nanoseconds) <= 2 * ToNanoseconds(TemporalUnit::Day)); + + return ::BalanceTime<int64_t>(time.hour, time.minute, time.second, + time.millisecond, time.microsecond, + time.nanosecond + nanoseconds); +} + +/** + * DifferenceTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, ns2 ) + */ +TimeDuration js::temporal::DifferenceTime(const PlainTime& time1, + const PlainTime& time2) { + MOZ_ASSERT(IsValidTime(time1)); + MOZ_ASSERT(IsValidTime(time2)); + + // Step 1. + int32_t hours = time2.hour - time1.hour; + + // Step 2. + int32_t minutes = time2.minute - time1.minute; + + // Step 3. + int32_t seconds = time2.second - time1.second; + + // Step 4. + int32_t milliseconds = time2.millisecond - time1.millisecond; + + // Step 5. + int32_t microseconds = time2.microsecond - time1.microsecond; + + // Step 6. + int32_t nanoseconds = time2.nanosecond - time1.nanosecond; + + // Step 7. + int32_t sign = ::DurationSign(hours, minutes, seconds, milliseconds, + microseconds, nanoseconds); + + // Step 8. + auto balanced = ::BalanceTime(hours * sign, minutes * sign, seconds * sign, + milliseconds * sign, microseconds * sign, + nanoseconds * sign); + + // Step 9. + MOZ_ASSERT(balanced.days == 0); + + // Step 10. + return CreateTimeDurationRecord( + 0, balanced.time.hour * sign, balanced.time.minute * sign, + balanced.time.second * sign, balanced.time.millisecond * sign, + balanced.time.microsecond * sign, balanced.time.nanosecond * sign); +} + +/** + * ToTemporalTime ( item [ , overflow ] ) + */ +static bool ToTemporalTime(JSContext* cx, Handle<Value> item, + TemporalOverflow overflow, PlainTime* result) { + // Steps 1-2. (Not applicable in our implementation.) + + // Steps 3-4. + if (item.isObject()) { + // Step 3. + Rooted<JSObject*> itemObj(cx, &item.toObject()); + + // Step 3.a. + if (auto* time = itemObj->maybeUnwrapIf<PlainTimeObject>()) { + *result = ToPlainTime(time); + return true; + } + + // Step 3.b. + if (auto* zonedDateTime = itemObj->maybeUnwrapIf<ZonedDateTimeObject>()) { + auto epochInstant = ToInstant(zonedDateTime); + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + + if (!timeZone.wrap(cx)) { + return false; + } + + // Steps 3.b.i-iii. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, epochInstant, &dateTime)) { + return false; + } + + // Step 3.b.iv. + *result = dateTime.time; + return true; + } + + // Step 3.c. + if (auto* dateTime = itemObj->maybeUnwrapIf<PlainDateTimeObject>()) { + *result = ToPlainTime(dateTime); + return true; + } + + // Step 3.d. + TimeRecord timeResult; + if (!ToTemporalTimeRecord(cx, itemObj, &timeResult)) { + return false; + } + + // Step 3.e. + if (!RegulateTime(cx, timeResult, overflow, result)) { + return false; + } + } else { + // Step 4. + + // Step 4.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 4.b. + if (!ParseTemporalTimeString(cx, string, result)) { + return false; + } + + // Step 4.c. + MOZ_ASSERT(IsValidTime(*result)); + } + + // Step 5. + return true; +} + +/** + * ToTemporalTime ( item [ , overflow ] ) + */ +static PlainTimeObject* ToTemporalTime(JSContext* cx, Handle<Value> item, + TemporalOverflow overflow) { + PlainTime time; + if (!ToTemporalTime(cx, item, overflow, &time)) { + return nullptr; + } + MOZ_ASSERT(IsValidTime(time)); + + return CreateTemporalTime(cx, time); +} + +/** + * ToTemporalTime ( item [ , overflow ] ) + */ +bool js::temporal::ToTemporalTime(JSContext* cx, Handle<Value> item, + PlainTime* result) { + return ToTemporalTime(cx, item, TemporalOverflow::Constrain, result); +} + +/** + * TotalDurationNanoseconds ( hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds ) + */ +static int64_t TotalDurationNanoseconds(const Duration& duration) { + // This function is only called from BalanceTime. The difference between two + // plain times can't exceed the number of nanoseconds in a day. + MOZ_ASSERT(IsValidDuration(duration)); + MOZ_ASSERT(std::abs(duration.hours) <= 24); + MOZ_ASSERT(std::abs(duration.minutes) <= 60); + MOZ_ASSERT(std::abs(duration.seconds) <= 60); + MOZ_ASSERT(std::abs(duration.milliseconds) <= 1000); + MOZ_ASSERT(std::abs(duration.microseconds) <= 1000); + MOZ_ASSERT(std::abs(duration.nanoseconds) <= 1000); + + // Step 1. + auto minutes = int64_t(duration.minutes) + int64_t(duration.hours) * 60; + + // Step 2. + auto seconds = int64_t(duration.seconds) + minutes * 60; + + // Step 3. + auto milliseconds = int64_t(duration.milliseconds) + seconds * 1000; + + // Step 4. + auto microseconds = int64_t(duration.microseconds) + milliseconds * 1000; + + // Steps 5. + return int64_t(duration.nanoseconds) + microseconds * 1000; +} + +/** + * BalanceTimeDuration ( days, hours, minutes, seconds, milliseconds, + * microseconds, nanoseconds, largestUnit [ , relativeTo ] ) + */ +static Duration BalanceTimeDuration(const Duration& duration, + TemporalUnit largestUnit) { + MOZ_ASSERT(IsValidDuration(duration)); + MOZ_ASSERT(largestUnit > TemporalUnit::Day); + + // We only handle time components here. + MOZ_ASSERT(duration.years == 0); + MOZ_ASSERT(duration.months == 0); + MOZ_ASSERT(duration.weeks == 0); + MOZ_ASSERT(duration.days == 0); + + // Step 1. (Not applicable) + + // Step 2. + int64_t nanoseconds = TotalDurationNanoseconds(duration); + MOZ_ASSERT(std::abs(nanoseconds) <= ToNanoseconds(TemporalUnit::Day)); + + // Steps 3-4. (Not applicable) + + // Step 5. + int64_t hours = 0; + int64_t minutes = 0; + int64_t seconds = 0; + int64_t milliseconds = 0; + int64_t microseconds = 0; + + // Step 6. + int32_t sign = nanoseconds < 0 ? -1 : +1; + + // Step 7. + nanoseconds = std::abs(nanoseconds); + + // Steps 8-13. + switch (largestUnit) { + case TemporalUnit::Auto: + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Day: + MOZ_CRASH("Unexpected temporal unit"); + + case TemporalUnit::Hour: { + // Step 8. + + // 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; + + // Step 8.g. + minutes = seconds / 60; + + // Step 8.h. + seconds = seconds % 60; + + // Step 8.i. + hours = minutes / 60; + + // Step 8.j. + minutes = minutes % 60; + + break; + } + case TemporalUnit::Minute: { + // Step 9. + + // Step 9.a. + microseconds = nanoseconds / 1000; + + // Step 9.b. + nanoseconds = nanoseconds % 1000; + + // Step 9.c. + milliseconds = microseconds / 1000; + + // Step 9.d. + microseconds = microseconds % 1000; + + // Step 9.e. + seconds = milliseconds / 1000; + + // Step 9.f. + milliseconds = milliseconds % 1000; + + // Step 9.g. + minutes = seconds / 60; + + // Step 9.h. + seconds = seconds % 60; + + break; + } + case TemporalUnit::Second: { + // Step 10. + + // Step 10.a. + microseconds = nanoseconds / 1000; + + // Step 10.b. + nanoseconds = nanoseconds % 1000; + + // Step 10.c. + milliseconds = microseconds / 1000; + + // Step 10.d. + microseconds = microseconds % 1000; + + // Step 10.e. + seconds = milliseconds / 1000; + + // Step 10.f. + milliseconds = milliseconds % 1000; + + break; + } + case TemporalUnit::Millisecond: { + // Step 11. + + // Step 11.a. + microseconds = nanoseconds / 1000; + + // Step 11.b. + nanoseconds = nanoseconds % 1000; + + // Step 11.c. + milliseconds = microseconds / 1000; + + // Step 11.d. + microseconds = microseconds % 1000; + + break; + } + case TemporalUnit::Microsecond: { + // Step 12. + + // Step 12.a. + microseconds = nanoseconds / 1000; + + // Step 12.b. + nanoseconds = nanoseconds % 1000; + + break; + } + case TemporalUnit::Nanosecond: { + // Step 13. + break; + } + } + + // Step 14. + return { + 0, + 0, + 0, + 0, + double(hours * sign), + double(minutes * sign), + double(seconds * sign), + double(milliseconds * sign), + double(microseconds * sign), + double(nanoseconds * sign), + }; +} + +/** + * CompareTemporalTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, + * ns2 ) + */ +int32_t js::temporal::CompareTemporalTime(const PlainTime& one, + const PlainTime& two) { + // Steps 1-2. + if (int32_t diff = one.hour - two.hour) { + return diff < 0 ? -1 : 1; + } + + // Steps 3-4. + if (int32_t diff = one.minute - two.minute) { + return diff < 0 ? -1 : 1; + } + + // Steps 5-6. + if (int32_t diff = one.second - two.second) { + return diff < 0 ? -1 : 1; + } + + // Steps 7-8. + if (int32_t diff = one.millisecond - two.millisecond) { + return diff < 0 ? -1 : 1; + } + + // Steps 9-10. + if (int32_t diff = one.microsecond - two.microsecond) { + return diff < 0 ? -1 : 1; + } + + // Steps 11-12. + if (int32_t diff = one.nanosecond - two.nanosecond) { + return diff < 0 ? -1 : 1; + } + + // Step 13. + return 0; +} + +/** + * ToTemporalTimeRecord ( temporalTimeLike [ , completeness ] ) + */ +static bool ToTemporalTimeRecord(JSContext* cx, + Handle<JSObject*> temporalTimeLike, + TimeRecord* result) { + // Steps 1 and 3-4. (Not applicable in our implementation.) + + // Step 2. (Inlined call to PrepareTemporalFields.) + // PrepareTemporalFields, step 1. (Not applicable in our implementation.) + + // PrepareTemporalFields, step 2. + bool any = false; + + // PrepareTemporalFields, steps 3-4. (Loop unrolled) + Rooted<Value> value(cx); + auto getTimeProperty = [&](Handle<PropertyName*> property, const char* name, + double* num) { + // Step 4.a. + if (!GetProperty(cx, temporalTimeLike, temporalTimeLike, property, + &value)) { + return false; + } + + // Step 4.b. + if (!value.isUndefined()) { + // Step 4.b.i. + any = true; + + // Step 4.b.ii.2. + if (!ToIntegerWithTruncation(cx, value, name, num)) { + return false; + } + } + return true; + }; + + if (!getTimeProperty(cx->names().hour, "hour", &result->hour)) { + return false; + } + if (!getTimeProperty(cx->names().microsecond, "microsecond", + &result->microsecond)) { + return false; + } + if (!getTimeProperty(cx->names().millisecond, "millisecond", + &result->millisecond)) { + return false; + } + if (!getTimeProperty(cx->names().minute, "minute", &result->minute)) { + return false; + } + if (!getTimeProperty(cx->names().nanosecond, "nanosecond", + &result->nanosecond)) { + return false; + } + if (!getTimeProperty(cx->names().second, "second", &result->second)) { + return false; + } + + // PrepareTemporalFields, step 5. + if (!any) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_TIME_MISSING_UNIT); + return false; + } + + // Steps 5-16. (Performed implicitly in our implementation.) + + // Step 17. + return true; +} + +/** + * ToTemporalTimeRecord ( temporalTimeLike [ , completeness ] ) + */ +bool js::temporal::ToTemporalTimeRecord(JSContext* cx, + Handle<JSObject*> temporalTimeLike, + TimeRecord* result) { + // Step 3.a. (Set all fields to zero.) + *result = {}; + + // Steps 1-2 and 4-17. + return ::ToTemporalTimeRecord(cx, temporalTimeLike, result); +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +static int64_t RoundNumberToIncrement(int64_t x, TemporalUnit unit, + Increment increment, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(x >= 0); + MOZ_ASSERT(x < ToNanoseconds(TemporalUnit::Day)); + + MOZ_ASSERT(unit >= TemporalUnit::Day); + MOZ_ASSERT_IF(unit == TemporalUnit::Day, increment == Increment{1}); + MOZ_ASSERT_IF(unit > TemporalUnit::Day, + increment <= MaximumTemporalDurationRoundingIncrement(unit)); + + int64_t divisor = ToNanoseconds(unit) * increment.value(); + MOZ_ASSERT(divisor > 0); + MOZ_ASSERT(divisor <= ToNanoseconds(TemporalUnit::Day)); + + // Division by one has no remainder. + if (divisor == 1) { + MOZ_ASSERT(increment == Increment{1}); + return x; + } + + // Steps 1-8. + int64_t rounded = Divide(x, divisor, roundingMode); + + // Step 9. + mozilla::CheckedInt64 result = rounded; + result *= increment.value(); + + MOZ_ASSERT(result.isValid(), "can't overflow when inputs are all in range"); + + return result.value(); +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +static int64_t RoundNumberToIncrement(int64_t x, int64_t divisor, + Increment increment, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(x >= 0); + MOZ_ASSERT(x < ToNanoseconds(TemporalUnit::Day)); + MOZ_ASSERT(divisor > 0); + MOZ_ASSERT(increment == Increment{1}, "Rounding increment for 'day' is 1"); + + // Steps 1-2. (Not applicable in our implementation) + + // Steps 3-8. + return Divide(x, divisor, roundingMode); +} + +static int64_t TimeToNanos(const PlainTime& time) { + // No overflow possible because the input is a valid time. + MOZ_ASSERT(IsValidTime(time)); + + int64_t hour = time.hour; + int64_t minute = time.minute; + int64_t second = time.second; + int64_t millisecond = time.millisecond; + int64_t microsecond = time.microsecond; + int64_t nanosecond = time.nanosecond; + + int64_t millis = ((hour * 60 + minute) * 60 + second) * 1000 + millisecond; + return (millis * 1000 + microsecond) * 1000 + nanosecond; +} + +/** + * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, + * increment, unit, roundingMode [ , dayLengthNs ] ) + */ +RoundedTime js::temporal::RoundTime(const PlainTime& time, Increment increment, + TemporalUnit unit, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(unit >= TemporalUnit::Day); + MOZ_ASSERT_IF(unit > TemporalUnit::Day, + increment <= MaximumTemporalDurationRoundingIncrement(unit)); + MOZ_ASSERT_IF(unit == TemporalUnit::Day, increment == Increment{1}); + + int32_t days = 0; + auto [hour, minute, second, millisecond, microsecond, nanosecond] = time; + + // Take the same approach as used in RoundDuration() to perform exact + // mathematical operations without possible loss of precision. + + // Steps 1-8. + PlainTime quantity; + int32_t* result; + switch (unit) { + case TemporalUnit::Day: + quantity = time; + result = &days; + break; + case TemporalUnit::Hour: + quantity = time; + result = &hour; + minute = 0; + second = 0; + millisecond = 0; + microsecond = 0; + nanosecond = 0; + break; + case TemporalUnit::Minute: + quantity = {0, minute, second, millisecond, microsecond, nanosecond}; + result = &minute; + second = 0; + millisecond = 0; + microsecond = 0; + nanosecond = 0; + break; + case TemporalUnit::Second: + quantity = {0, 0, second, millisecond, microsecond, nanosecond}; + result = &second; + millisecond = 0; + microsecond = 0; + nanosecond = 0; + break; + case TemporalUnit::Millisecond: + quantity = {0, 0, 0, millisecond, microsecond, nanosecond}; + result = &millisecond; + microsecond = 0; + nanosecond = 0; + break; + case TemporalUnit::Microsecond: + quantity = {0, 0, 0, 0, microsecond, nanosecond}; + result = µsecond; + nanosecond = 0; + break; + case TemporalUnit::Nanosecond: + quantity = {0, 0, 0, 0, 0, nanosecond}; + result = &nanosecond; + break; + + case TemporalUnit::Auto: + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + MOZ_CRASH("unexpected temporal unit"); + } + + // Step 9. + int64_t r = ::RoundNumberToIncrement(TimeToNanos(quantity), unit, increment, + roundingMode); + MOZ_ASSERT(r == int64_t(int32_t(r)), + "no overflow possible due to limited range of arguments"); + *result = r; + + // Step 10. + if (unit == TemporalUnit::Day) { + return {int64_t(days), {0, 0, 0, 0, 0, 0}}; + } + + // Steps 11-17. + auto balanced = + ::BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond); + return {int64_t(balanced.days), balanced.time}; +} + +/** + * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, + * increment, unit, roundingMode [ , dayLengthNs ] ) + */ +RoundedTime js::temporal::RoundTime(const PlainTime& time, Increment increment, + TemporalUnit unit, + TemporalRoundingMode roundingMode, + const InstantSpan& dayLengthNs) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + MOZ_ASSERT(dayLengthNs > (InstantSpan{})); + + if (unit != TemporalUnit::Day) { + return RoundTime(time, increment, unit, roundingMode); + } + + // Step 1. (Not applicable) + + // Step 2. + int64_t quantity = TimeToNanos(time); + MOZ_ASSERT(quantity < ToNanoseconds(TemporalUnit::Day)); + + // Steps 3-8. (Not applicable) + + // Step 9. + int64_t divisor; + if (auto checkedDiv = dayLengthNs.toNanoseconds(); checkedDiv.isValid()) { + divisor = checkedDiv.value(); + } else { + // When the divisor is too large, the expression `quantity / divisor` is a + // value near zero. Substitute |divisor| with an equivalent expression. + // Choose |86'400'000'000'000| which will give a similar result because + // |quantity| is guaranteed to be lower than |86'400'000'000'000|. + divisor = ToNanoseconds(TemporalUnit::Day); + } + MOZ_ASSERT(divisor > 0); + + int64_t result = + ::RoundNumberToIncrement(quantity, divisor, increment, roundingMode); + + // Step 10. + return {result, {0, 0, 0, 0, 0, 0}}; +} + +/** + * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, hours, + * minutes, seconds, milliseconds, microseconds, nanoseconds ) + */ +static PlainTime AddTime(const PlainTime& time, const Duration& duration) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(IsValidDuration(duration)); + + // Balance the duration so we don't have to worry about imprecise Number + // computations below. + + // Use either int64_t or int32_t below. Assert the total combined amount of + // the units can be expressed in either int64_t or int32_t. + static_assert(1 * UnitsPerDay(TemporalUnit::Nanosecond) > INT32_MAX, + "total combined nanoseconds per day"); + static_assert(2 * UnitsPerDay(TemporalUnit::Microsecond) > INT32_MAX, + "total combined microseconds per day"); + static_assert(3 * UnitsPerDay(TemporalUnit::Millisecond) <= INT32_MAX, + "total combined milliseconds per day"); + static_assert(4 * UnitsPerDay(TemporalUnit::Second) <= INT32_MAX, + "total combined seconds per day"); + static_assert(5 * UnitsPerDay(TemporalUnit::Minute) <= INT32_MAX, + "total combined minutes per day"); + static_assert(6 * UnitsPerDay(TemporalUnit::Hour) <= INT32_MAX, + "total combined hours per day"); + + // We ignore the days overflow in this function, therefore it's possible + // to restrict each unit to units-per-day. + int64_t nanoseconds = int64_t( + std::fmod(duration.nanoseconds, UnitsPerDay(TemporalUnit::Nanosecond))); + int64_t microseconds = int64_t( + std::fmod(duration.microseconds, UnitsPerDay(TemporalUnit::Microsecond))); + int32_t milliseconds = int32_t( + std::fmod(duration.milliseconds, UnitsPerDay(TemporalUnit::Millisecond))); + int32_t seconds = + int32_t(std::fmod(duration.seconds, UnitsPerDay(TemporalUnit::Second))); + int32_t minutes = + int32_t(std::fmod(duration.minutes, UnitsPerDay(TemporalUnit::Minute))); + int32_t hours = + int32_t(std::fmod(duration.hours, UnitsPerDay(TemporalUnit::Hour))); + + // Each unit is now less than the units-per-day. + MOZ_ASSERT(std::abs(nanoseconds) < UnitsPerDay(TemporalUnit::Nanosecond)); + MOZ_ASSERT(std::abs(microseconds) < UnitsPerDay(TemporalUnit::Microsecond)); + MOZ_ASSERT(std::abs(milliseconds) < UnitsPerDay(TemporalUnit::Millisecond)); + MOZ_ASSERT(std::abs(seconds) < UnitsPerDay(TemporalUnit::Second)); + MOZ_ASSERT(std::abs(minutes) < UnitsPerDay(TemporalUnit::Minute)); + MOZ_ASSERT(std::abs(hours) < UnitsPerDay(TemporalUnit::Hour)); + + microseconds += nanoseconds / 1000; + nanoseconds %= 1000; + MOZ_ASSERT(microseconds < 2 * UnitsPerDay(TemporalUnit::Microsecond)); + + milliseconds += microseconds / 1000; + microseconds %= 1000; + MOZ_ASSERT(milliseconds < 3 * UnitsPerDay(TemporalUnit::Millisecond)); + + seconds += milliseconds / 1000; + milliseconds %= 1000; + MOZ_ASSERT(seconds < 4 * UnitsPerDay(TemporalUnit::Second)); + + minutes += seconds / 60; + seconds %= 60; + MOZ_ASSERT(minutes < 5 * UnitsPerDay(TemporalUnit::Minute)); + + hours += minutes / 60; + minutes %= 60; + MOZ_ASSERT(hours < 6 * UnitsPerDay(TemporalUnit::Hour)); + + hours %= 24; + + MOZ_ASSERT(std::abs(hours) <= 23); + MOZ_ASSERT(std::abs(minutes) <= 59); + MOZ_ASSERT(std::abs(seconds) <= 59); + MOZ_ASSERT(std::abs(milliseconds) <= 999); + MOZ_ASSERT(std::abs(microseconds) <= 999); + MOZ_ASSERT(std::abs(nanoseconds) <= 999); + + // Step 1. + int32_t hour = time.hour + hours; + + // Step 2. + int32_t minute = time.minute + minutes; + + // Step 3. + int32_t second = time.second + seconds; + + // Step 4. + int32_t millisecond = time.millisecond + milliseconds; + + // Step 5. + int32_t microsecond = time.microsecond + int32_t(microseconds); + + // Step 6. + int32_t nanosecond = time.nanosecond + int32_t(nanoseconds); + + // Step 7. + auto balanced = + ::BalanceTime(hour, minute, second, millisecond, microsecond, nanosecond); + return balanced.time; +} + +static BigInt* FloorDiv(JSContext* cx, Handle<BigInt*> dividend, + int32_t divisor) { + MOZ_ASSERT(divisor > 0); + + Rooted<BigInt*> div(cx, BigInt::createFromInt64(cx, divisor)); + if (!div) { + return nullptr; + } + + Rooted<BigInt*> quotient(cx); + Rooted<BigInt*> remainder(cx); + if (!BigInt::divmod(cx, dividend, div, "ient, &remainder)) { + return nullptr; + } + if (remainder->isNegative()) { + return BigInt::dec(cx, quotient); + } + return quotient; +} + +static bool AddTimeDaysSlow(JSContext* cx, const PlainTime& time, + const Duration& duration, double* result) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(IsValidDuration(duration)); + + Rooted<BigInt*> days(cx, BigInt::createFromDouble(cx, duration.days)); + if (!days) { + return false; + } + + Rooted<BigInt*> hours(cx, BigInt::createFromDouble(cx, duration.hours)); + if (!hours) { + return false; + } + + Rooted<BigInt*> minutes(cx, BigInt::createFromDouble(cx, duration.minutes)); + if (!minutes) { + return false; + } + + Rooted<BigInt*> seconds(cx, BigInt::createFromDouble(cx, duration.seconds)); + if (!seconds) { + return false; + } + + Rooted<BigInt*> milliseconds( + cx, BigInt::createFromDouble(cx, duration.milliseconds)); + if (!milliseconds) { + return false; + } + + Rooted<BigInt*> microseconds( + cx, BigInt::createFromDouble(cx, duration.microseconds)); + if (!microseconds) { + return false; + } + + Rooted<BigInt*> nanoseconds( + cx, BigInt::createFromDouble(cx, duration.nanoseconds)); + if (!nanoseconds) { + return false; + } + + auto addWithInt32 = [cx](Handle<BigInt*> left, int32_t right) -> BigInt* { + Rooted<BigInt*> rightBigInt(cx, BigInt::createFromInt64(cx, right)); + if (!rightBigInt) { + return nullptr; + } + return BigInt::add(cx, left, rightBigInt); + }; + + // Step 1. + Rooted<BigInt*> hour(cx, addWithInt32(hours, time.hour)); + if (!hour) { + return false; + } + + // Step 2. + Rooted<BigInt*> minute(cx, addWithInt32(minutes, time.minute)); + if (!minute) { + return false; + } + + // Step 3. + Rooted<BigInt*> second(cx, addWithInt32(seconds, time.second)); + if (!second) { + return false; + } + + // Step 4. + Rooted<BigInt*> millisecond(cx, addWithInt32(milliseconds, time.millisecond)); + if (!millisecond) { + return false; + } + + // Step 5. + Rooted<BigInt*> microsecond(cx, addWithInt32(microseconds, time.microsecond)); + if (!microsecond) { + return false; + } + + // Step 6. + Rooted<BigInt*> nanosecond(cx, addWithInt32(nanoseconds, time.nanosecond)); + if (!nanosecond) { + return false; + } + + // Step 7. (Inlined BalanceTime) + + auto addFloorDiv = [cx](Handle<BigInt*> left, Handle<BigInt*> right, + int32_t divisor) -> BigInt* { + Rooted<BigInt*> quotient(cx, FloorDiv(cx, right, divisor)); + if (!quotient) { + return nullptr; + } + return BigInt::add(cx, left, quotient); + }; + + // BalanceTime, steps 1-2. + microsecond = addFloorDiv(microsecond, nanosecond, 1000); + if (!microsecond) { + return false; + } + + // BalanceTime, steps 3-4. + millisecond = addFloorDiv(millisecond, microsecond, 1000); + if (!millisecond) { + return false; + } + + // BalanceTime, steps 5-6. + second = addFloorDiv(second, millisecond, 1000); + if (!second) { + return false; + } + + // BalanceTime, steps 7-8. + minute = addFloorDiv(minute, second, 60); + if (!minute) { + return false; + } + + // BalanceTime, steps 9-10. + hour = addFloorDiv(hour, minute, 60); + if (!hour) { + return false; + } + + // BalanceTime, steps 11-13. + days = addFloorDiv(days, hour, 24); + if (!days) { + return false; + } + + // The days number is used as the input for a duration. Throw if the BigInt + // when converted to a Number can't be represented in a duration. + double daysNumber = BigInt::numberValue(days); + if (!ThrowIfInvalidDuration(cx, {0, 0, 0, daysNumber})) { + return false; + } + MOZ_ASSERT(IsInteger(daysNumber)); + + *result = daysNumber; + return true; +} + +static mozilla::Maybe<int64_t> AddTimeDays(const PlainTime& time, + const Duration& duration) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(IsValidDuration(duration)); + + int64_t days; + if (!mozilla::NumberEqualsInt64(duration.days, &days)) { + return mozilla::Nothing(); + } + + int64_t hours; + if (!mozilla::NumberEqualsInt64(duration.hours, &hours)) { + return mozilla::Nothing(); + } + + int64_t minutes; + if (!mozilla::NumberEqualsInt64(duration.minutes, &minutes)) { + return mozilla::Nothing(); + } + + int64_t seconds; + if (!mozilla::NumberEqualsInt64(duration.seconds, &seconds)) { + return mozilla::Nothing(); + } + + int64_t milliseconds; + if (!mozilla::NumberEqualsInt64(duration.milliseconds, &milliseconds)) { + return mozilla::Nothing(); + } + + int64_t microseconds; + if (!mozilla::NumberEqualsInt64(duration.microseconds, µseconds)) { + return mozilla::Nothing(); + } + + int64_t nanoseconds; + if (!mozilla::NumberEqualsInt64(duration.nanoseconds, &nanoseconds)) { + return mozilla::Nothing(); + } + + // Step 1. + auto hour = mozilla::CheckedInt64(time.hour) + hours; + if (!hour.isValid()) { + return mozilla::Nothing(); + } + + // Step 2. + auto minute = mozilla::CheckedInt64(time.minute) + minutes; + if (!minute.isValid()) { + return mozilla::Nothing(); + } + + // Step 3. + auto second = mozilla::CheckedInt64(time.second) + seconds; + if (!second.isValid()) { + return mozilla::Nothing(); + } + + // Step 4. + auto millisecond = mozilla::CheckedInt64(time.millisecond) + milliseconds; + if (!millisecond.isValid()) { + return mozilla::Nothing(); + } + + // Step 5. + auto microsecond = mozilla::CheckedInt64(time.microsecond) + microseconds; + if (!microsecond.isValid()) { + return mozilla::Nothing(); + } + + // Step 6. + auto nanosecond = mozilla::CheckedInt64(time.nanosecond) + nanoseconds; + if (!nanosecond.isValid()) { + return mozilla::Nothing(); + } + + // Step 7. (Inlined BalanceTime) + + // BalanceTime, steps 1-2. + microsecond += FloorDiv(nanosecond.value(), 1000); + if (!microsecond.isValid()) { + return mozilla::Nothing(); + } + + // BalanceTime, steps 3-4. + millisecond += FloorDiv(microsecond.value(), 1000); + if (!millisecond.isValid()) { + return mozilla::Nothing(); + } + + // BalanceTime, steps 5-6. + second += FloorDiv(millisecond.value(), 1000); + if (!second.isValid()) { + return mozilla::Nothing(); + } + + // BalanceTime, steps 7-8. + minute += FloorDiv(second.value(), 60); + if (!minute.isValid()) { + return mozilla::Nothing(); + } + + // BalanceTime, steps 9-10. + hour += FloorDiv(minute.value(), 60); + if (!hour.isValid()) { + return mozilla::Nothing(); + } + + // BalanceTime, steps 11-13. + auto result = mozilla::CheckedInt64(days) + FloorDiv(hour.value(), 24); + if (!result.isValid()) { + return mozilla::Nothing(); + } + return mozilla::Some(result.value()); +} + +static bool AddTimeDays(JSContext* cx, const PlainTime& time, + const Duration& duration, double* result) { + // Fast-path when we can perform the whole computation with int64 values. + if (auto days = AddTimeDays(time, duration)) { + *result = *days; + return true; + } + return AddTimeDaysSlow(cx, time, duration, result); +} + +/** + * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, hours, + * minutes, seconds, milliseconds, microseconds, nanoseconds ) + */ +bool js::temporal::AddTime(JSContext* cx, const PlainTime& time, + const Duration& duration, PlainTime* result, + double* daysResult) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(IsValidDuration(duration)); + + // Steps 1-7. + auto balanced = ::AddTime(time, duration); + + // Compute |days| separately to ensure no loss of precision occurs. + // + // The single caller of this |AddTime| function also needs to compute the + // addition of |duration.days| and the balanced days. Perform this addition + // here, so we don't need to pass around BigInt values for exact mathematical + // results. + double days; + if (!AddTimeDays(cx, time, duration, &days)) { + return false; + } + MOZ_ASSERT(IsInteger(days)); + + *result = balanced; + *daysResult = days; + return true; +} + +/** + * DifferenceTemporalPlainTime ( operation, temporalTime, other, options ) + */ +static bool DifferenceTemporalPlainTime(JSContext* cx, + TemporalDifference operation, + const CallArgs& args) { + auto temporalTime = + ToPlainTime(&args.thisv().toObject().as<PlainTimeObject>()); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + PlainTime other; + if (!ToTemporalTime(cx, args.get(0), &other)) { + return false; + } + + // Steps 3-4. + DifferenceSettings settings; + if (args.hasDefined(1)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", ToName(operation), args[1])); + if (!options) { + return false; + } + + // Step 3. + Rooted<PlainObject*> resolvedOptions(cx, + SnapshotOwnProperties(cx, options)); + if (!resolvedOptions) { + return false; + } + + // Step 4. + if (!GetDifferenceSettings( + cx, operation, resolvedOptions, TemporalUnitGroup::Time, + TemporalUnit::Nanosecond, TemporalUnit::Hour, &settings)) { + return false; + } + } else { + // Steps 3-4. + settings = { + TemporalUnit::Nanosecond, + TemporalUnit::Hour, + TemporalRoundingMode::Trunc, + Increment{1}, + }; + } + + // Step 5. + auto diff = DifferenceTime(temporalTime, other); + MOZ_ASSERT(diff.days == 0); + + // Step 6. + auto roundedDuration = diff.toDuration(); + if (settings.smallestUnit != TemporalUnit::Nanosecond || + settings.roundingIncrement != Increment{1}) { + // Steps 6.a-b. + if (!RoundDuration(cx, roundedDuration.time(), settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + &roundedDuration)) { + return false; + } + } + + // Step 7. + auto balancedDuration = + BalanceTimeDuration(roundedDuration, settings.largestUnit); + + // Step 8. + if (operation == TemporalDifference::Since) { + balancedDuration = balancedDuration.negate(); + } + + auto* result = CreateTemporalDuration(cx, balancedDuration); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +enum class PlainTimeDuration { Add, Subtract }; + +/** + * AddDurationToOrSubtractDurationFromPlainTime ( operation, temporalTime, + * temporalDurationLike ) + */ +static bool AddDurationToOrSubtractDurationFromPlainTime( + JSContext* cx, PlainTimeDuration operation, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Duration duration; + if (!ToTemporalDurationRecord(cx, args.get(0), &duration)) { + return false; + } + + // Step 3. + if (operation == PlainTimeDuration::Subtract) { + duration = duration.negate(); + } + auto result = AddTime(time, duration); + + // Step 4. + MOZ_ASSERT(IsValidTime(result)); + + // Step 5. + auto* obj = CreateTemporalTime(cx, result); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainTime ( [ hour [ , minute [ , second [ , millisecond [ , + * microsecond [ , nanosecond ] ] ] ] ] ] ) + */ +static bool PlainTimeConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.PlainTime")) { + return false; + } + + // Step 2. + double hour = 0; + if (args.hasDefined(0)) { + if (!ToIntegerWithTruncation(cx, args[0], "hour", &hour)) { + return false; + } + } + + // Step 3. + double minute = 0; + if (args.hasDefined(1)) { + if (!ToIntegerWithTruncation(cx, args[1], "minute", &minute)) { + return false; + } + } + + // Step 4. + double second = 0; + if (args.hasDefined(2)) { + if (!ToIntegerWithTruncation(cx, args[2], "second", &second)) { + return false; + } + } + + // Step 5. + double millisecond = 0; + if (args.hasDefined(3)) { + if (!ToIntegerWithTruncation(cx, args[3], "millisecond", &millisecond)) { + return false; + } + } + + // Step 6. + double microsecond = 0; + if (args.hasDefined(4)) { + if (!ToIntegerWithTruncation(cx, args[4], "microsecond", µsecond)) { + return false; + } + } + + // Step 7. + double nanosecond = 0; + if (args.hasDefined(5)) { + if (!ToIntegerWithTruncation(cx, args[5], "nanosecond", &nanosecond)) { + return false; + } + } + + // Step 8. + auto* temporalTime = CreateTemporalTime(cx, args, hour, minute, second, + millisecond, microsecond, nanosecond); + if (!temporalTime) { + return false; + } + + args.rval().setObject(*temporalTime); + return true; +} + +/** + * Temporal.PlainTime.from ( item [ , options ] ) + */ +static bool PlainTime_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Not applicable) + + auto overflow = TemporalOverflow::Constrain; + if (args.hasDefined(1)) { + // Step 2. + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "from", args[1])); + if (!options) { + return false; + } + + // Step 3. + if (!ToTemporalOverflow(cx, options, &overflow)) { + return false; + } + } + + // Steps 4-5. + auto result = ToTemporalTime(cx, args.get(0), overflow); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainTime.compare ( one, two ) + */ +static bool PlainTime_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + PlainTime one; + if (!ToTemporalTime(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + PlainTime two; + if (!ToTemporalTime(cx, args.get(1), &two)) { + return false; + } + + // Step 3. + args.rval().setInt32(CompareTemporalTime(one, two)); + return true; +} + +/** + * get Temporal.PlainTime.prototype.hour + */ +static bool PlainTime_hour(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + args.rval().setInt32(temporalTime->isoHour()); + return true; +} + +/** + * get Temporal.PlainTime.prototype.hour + */ +static bool PlainTime_hour(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_hour>(cx, args); +} + +/** + * get Temporal.PlainTime.prototype.minute + */ +static bool PlainTime_minute(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + args.rval().setInt32(temporalTime->isoMinute()); + return true; +} + +/** + * get Temporal.PlainTime.prototype.minute + */ +static bool PlainTime_minute(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_minute>(cx, args); +} + +/** + * get Temporal.PlainTime.prototype.second + */ +static bool PlainTime_second(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + args.rval().setInt32(temporalTime->isoSecond()); + return true; +} + +/** + * get Temporal.PlainTime.prototype.second + */ +static bool PlainTime_second(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_second>(cx, args); +} + +/** + * get Temporal.PlainTime.prototype.millisecond + */ +static bool PlainTime_millisecond(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + args.rval().setInt32(temporalTime->isoMillisecond()); + return true; +} + +/** + * get Temporal.PlainTime.prototype.millisecond + */ +static bool PlainTime_millisecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_millisecond>(cx, args); +} + +/** + * get Temporal.PlainTime.prototype.microsecond + */ +static bool PlainTime_microsecond(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + args.rval().setInt32(temporalTime->isoMicrosecond()); + return true; +} + +/** + * get Temporal.PlainTime.prototype.microsecond + */ +static bool PlainTime_microsecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_microsecond>(cx, args); +} + +/** + * get Temporal.PlainTime.prototype.nanosecond + */ +static bool PlainTime_nanosecond(JSContext* cx, const CallArgs& args) { + // Step 3. + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + args.rval().setInt32(temporalTime->isoNanosecond()); + return true; +} + +/** + * get Temporal.PlainTime.prototype.nanosecond + */ +static bool PlainTime_nanosecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_nanosecond>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.add ( temporalDurationLike ) + */ +static bool PlainTime_add(JSContext* cx, const CallArgs& args) { + // Step 3. + return AddDurationToOrSubtractDurationFromPlainTime( + cx, PlainTimeDuration::Add, args); +} + +/** + * Temporal.PlainTime.prototype.add ( temporalDurationLike ) + */ +static bool PlainTime_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_add>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.subtract ( temporalDurationLike ) + */ +static bool PlainTime_subtract(JSContext* cx, const CallArgs& args) { + // Step 3. + return AddDurationToOrSubtractDurationFromPlainTime( + cx, PlainTimeDuration::Subtract, args); +} + +/** + * Temporal.PlainTime.prototype.subtract ( temporalDurationLike ) + */ +static bool PlainTime_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_subtract>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.with ( temporalTimeLike [ , options ] ) + */ +static bool PlainTime_with(JSContext* cx, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + // Step 3. + Rooted<JSObject*> temporalTimeLike( + cx, RequireObjectArg(cx, "temporalTimeLike", "with", args.get(0))); + if (!temporalTimeLike) { + return false; + } + + // Step 4. + if (!RejectTemporalLikeObject(cx, temporalTimeLike)) { + return false; + } + + auto overflow = TemporalOverflow::Constrain; + if (args.hasDefined(1)) { + // Step 5. + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "with", args[1])); + if (!options) { + return false; + } + + // Step 6. + if (!ToTemporalOverflow(cx, options, &overflow)) { + return false; + } + } + + // Steps 7-19. + TimeRecord partialTime = { + double(time.hour), double(time.minute), + double(time.second), double(time.millisecond), + double(time.microsecond), double(time.nanosecond), + }; + if (!::ToTemporalTimeRecord(cx, temporalTimeLike, &partialTime)) { + return false; + } + + // Step 20. + PlainTime result; + if (!RegulateTime(cx, partialTime, overflow, &result)) { + return false; + } + + // Step 21. + auto* obj = CreateTemporalTime(cx, result); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainTime.prototype.with ( temporalTimeLike [ , options ] ) + */ +static bool PlainTime_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_with>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.until ( other [ , options ] ) + */ +static bool PlainTime_until(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainTime(cx, TemporalDifference::Until, args); +} + +/** + * Temporal.PlainTime.prototype.until ( other [ , options ] ) + */ +static bool PlainTime_until(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_until>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.since ( other [ , options ] ) + */ +static bool PlainTime_since(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainTime(cx, TemporalDifference::Since, args); +} + +/** + * Temporal.PlainTime.prototype.since ( other [ , options ] ) + */ +static bool PlainTime_since(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_since>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.round ( roundTo ) + */ +static bool PlainTime_round(JSContext* cx, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + // Steps 3-12. + auto smallestUnit = TemporalUnit::Auto; + auto roundingMode = TemporalRoundingMode::HalfExpand; + auto roundingIncrement = Increment{1}; + if (args.get(0).isString()) { + // Step 4. (Not applicable in our implementation.) + + // Step 9. + Rooted<JSString*> paramString(cx, args[0].toString()); + if (!GetTemporalUnit(cx, paramString, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + // Steps 6-8 and 10-12. (Implicit) + } else { + // Steps 3 and 5. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "roundTo", "round", args.get(0))); + if (!options) { + return false; + } + + // Steps 6-7. + if (!ToTemporalRoundingIncrement(cx, options, &roundingIncrement)) { + return false; + } + + // Step 8. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 9. + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + if (smallestUnit == TemporalUnit::Auto) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_OPTION, "smallestUnit"); + return false; + } + + // Steps 10-11. + auto maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit); + + // Step 12. + if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, + false)) { + return false; + } + } + + // Step 13. + auto result = RoundTime(time, roundingIncrement, smallestUnit, roundingMode); + + // Step 14. + auto* obj = CreateTemporalTime(cx, result.time); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainTime.prototype.round ( roundTo ) + */ +static bool PlainTime_round(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_round>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.equals ( other ) + */ +static bool PlainTime_equals(JSContext* cx, const CallArgs& args) { + auto temporalTime = + ToPlainTime(&args.thisv().toObject().as<PlainTimeObject>()); + + // Step 3. + PlainTime other; + if (!ToTemporalTime(cx, args.get(0), &other)) { + return false; + } + + // Steps 4-10. + args.rval().setBoolean(temporalTime == other); + return true; +} + +/** + * Temporal.PlainTime.prototype.equals ( other ) + */ +static bool PlainTime_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_equals>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.toPlainDateTime ( temporalDate ) + */ +static bool PlainTime_toPlainDateTime(JSContext* cx, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + // Step 3. + Rooted<PlainDateWithCalendar> plainDate(cx); + if (!ToTemporalDate(cx, args.get(0), &plainDate)) { + return false; + } + auto date = plainDate.date(); + auto calendar = plainDate.calendar(); + + // Step 4. + auto* result = CreateTemporalDateTime(cx, {date, time}, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainTime.prototype.toPlainDateTime ( temporalDate ) + */ +static bool PlainTime_toPlainDateTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_toPlainDateTime>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.toZonedDateTime ( item ) + * + * |item| is an options object with `plainDate` and `timeZone` properties. + */ +static bool PlainTime_toZonedDateTime(JSContext* cx, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + // Step 3. + Rooted<JSObject*> itemObj( + cx, RequireObjectArg(cx, "item", "toZonedDateTime", args.get(0))); + if (!itemObj) { + return false; + } + + // Step 4. + Rooted<Value> temporalDateLike(cx); + if (!GetProperty(cx, itemObj, args[0], cx->names().plainDate, + &temporalDateLike)) { + return false; + } + + // Step 5. + if (temporalDateLike.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_PROPERTY, "plainDate"); + return false; + } + + // Step 6. + Rooted<PlainDateWithCalendar> plainDate(cx); + if (!ToTemporalDate(cx, temporalDateLike, &plainDate)) { + return false; + } + auto date = plainDate.date(); + auto calendar = plainDate.calendar(); + + // Step 7. + Rooted<Value> temporalTimeZoneLike(cx); + if (!GetProperty(cx, itemObj, itemObj, cx->names().timeZone, + &temporalTimeZoneLike)) { + return false; + } + + // Step 8. + if (temporalTimeZoneLike.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_PROPERTY, "timeZone"); + return false; + } + + // Step 9. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZone(cx, temporalTimeZoneLike, &timeZone)) { + return false; + } + + // Step 10. + Rooted<PlainDateTimeWithCalendar> temporalDateTime(cx); + if (!CreateTemporalDateTime(cx, {date, time}, calendar, &temporalDateTime)) { + return false; + } + + // Steps 11-12. + Instant instant; + if (!GetInstantFor(cx, timeZone, temporalDateTime, + TemporalDisambiguation::Compatible, &instant)) { + return false; + } + + // Step 13. + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.PlainTime.prototype.toZonedDateTime ( item ) + */ +static bool PlainTime_toZonedDateTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_toZonedDateTime>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.getISOFields ( ) + */ +static bool PlainTime_getISOFields(JSContext* cx, const CallArgs& args) { + Rooted<PlainTimeObject*> temporalTime( + cx, &args.thisv().toObject().as<PlainTimeObject>()); + auto time = ToPlainTime(temporalTime); + + // Step 3. + Rooted<IdValueVector> fields(cx, IdValueVector(cx)); + + // Step 4. + if (!fields.emplaceBack(NameToId(cx->names().isoHour), + Int32Value(time.hour))) { + return false; + } + + // Step 5. + if (!fields.emplaceBack(NameToId(cx->names().isoMicrosecond), + Int32Value(time.microsecond))) { + return false; + } + + // Step 6. + if (!fields.emplaceBack(NameToId(cx->names().isoMillisecond), + Int32Value(time.millisecond))) { + return false; + } + + // Step 7. + if (!fields.emplaceBack(NameToId(cx->names().isoMinute), + Int32Value(time.minute))) { + return false; + } + + // Step 8. + if (!fields.emplaceBack(NameToId(cx->names().isoNanosecond), + Int32Value(time.nanosecond))) { + return false; + } + + // Step 9. + if (!fields.emplaceBack(NameToId(cx->names().isoSecond), + Int32Value(time.second))) { + return false; + } + + // Step 10. + auto* obj = + NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainTime.prototype.getISOFields ( ) + */ +static bool PlainTime_getISOFields(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_getISOFields>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.toString ( [ options ] ) + */ +static bool PlainTime_toString(JSContext* cx, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + SecondsStringPrecision precision = {Precision::Auto(), + TemporalUnit::Nanosecond, Increment{1}}; + auto roundingMode = TemporalRoundingMode::Trunc; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Steps 4-5. + auto digits = Precision::Auto(); + if (!ToFractionalSecondDigits(cx, options, &digits)) { + return false; + } + + // Step 6. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 7. + auto smallestUnit = TemporalUnit::Auto; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + // Step 8. + if (smallestUnit == TemporalUnit::Hour) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_OPTION, "hour", + "smallestUnit"); + return false; + } + + // Step 9. + precision = ToSecondsStringPrecision(smallestUnit, digits); + } + + // Step 10. + auto roundedTime = + RoundTime(time, precision.increment, precision.unit, roundingMode); + + // Step 11. + JSString* str = + TemporalTimeToString(cx, roundedTime.time, precision.precision); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainTime.prototype.toString ( [ options ] ) + */ +static bool PlainTime_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_toString>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainTime_toLocaleString(JSContext* cx, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + // Step 3. + JSString* str = TemporalTimeToString(cx, time, Precision::Auto()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainTime.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool PlainTime_toLocaleString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_toLocaleString>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.toJSON ( ) + */ +static bool PlainTime_toJSON(JSContext* cx, const CallArgs& args) { + auto* temporalTime = &args.thisv().toObject().as<PlainTimeObject>(); + auto time = ToPlainTime(temporalTime); + + // Step 3. + JSString* str = TemporalTimeToString(cx, time, Precision::Auto()); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainTime.prototype.toJSON ( ) + */ +static bool PlainTime_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainTime, PlainTime_toJSON>(cx, args); +} + +/** + * Temporal.PlainTime.prototype.valueOf ( ) + */ +static bool PlainTime_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "PlainTime", "primitive type"); + return false; +} + +const JSClass PlainTimeObject::class_ = { + "Temporal.PlainTime", + JSCLASS_HAS_RESERVED_SLOTS(PlainTimeObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_PlainTime), + JS_NULL_CLASS_OPS, + &PlainTimeObject::classSpec_, +}; + +const JSClass& PlainTimeObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec PlainTime_methods[] = { + JS_FN("from", PlainTime_from, 1, 0), + JS_FN("compare", PlainTime_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec PlainTime_prototype_methods[] = { + JS_FN("add", PlainTime_add, 1, 0), + JS_FN("subtract", PlainTime_subtract, 1, 0), + JS_FN("with", PlainTime_with, 1, 0), + JS_FN("until", PlainTime_until, 1, 0), + JS_FN("since", PlainTime_since, 1, 0), + JS_FN("round", PlainTime_round, 1, 0), + JS_FN("equals", PlainTime_equals, 1, 0), + JS_FN("toPlainDateTime", PlainTime_toPlainDateTime, 1, 0), + JS_FN("toZonedDateTime", PlainTime_toZonedDateTime, 1, 0), + JS_FN("getISOFields", PlainTime_getISOFields, 0, 0), + JS_FN("toString", PlainTime_toString, 0, 0), + JS_FN("toLocaleString", PlainTime_toLocaleString, 0, 0), + JS_FN("toJSON", PlainTime_toJSON, 0, 0), + JS_FN("valueOf", PlainTime_valueOf, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec PlainTime_prototype_properties[] = { + JS_PSG("hour", PlainTime_hour, 0), + JS_PSG("minute", PlainTime_minute, 0), + JS_PSG("second", PlainTime_second, 0), + JS_PSG("millisecond", PlainTime_millisecond, 0), + JS_PSG("microsecond", PlainTime_microsecond, 0), + JS_PSG("nanosecond", PlainTime_nanosecond, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.PlainTime", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec PlainTimeObject::classSpec_ = { + GenericCreateConstructor<PlainTimeConstructor, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PlainTimeObject>, + PlainTime_methods, + nullptr, + PlainTime_prototype_methods, + PlainTime_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/PlainTime.h b/js/src/builtin/temporal/PlainTime.h new file mode 100644 index 0000000000..b6da469913 --- /dev/null +++ b/js/src/builtin/temporal/PlainTime.h @@ -0,0 +1,187 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_PlainTime_h +#define builtin_temporal_PlainTime_h + +#include <stdint.h> + +#include "builtin/temporal/TemporalTypes.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +namespace js { +struct ClassSpec; +} + +namespace js::temporal { + +class PlainTimeObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + // TODO: Consider compacting fields to reduce object size. + // + // ceil(log2(24)) + 2 * ceil(log2(60)) + 3 * ceil(log2(1000)) = 47 bits are + // needed to store a time value in a single int64. 47 bits can be stored as + // raw bits in a JS::Value. + + static constexpr uint32_t ISO_HOUR_SLOT = 0; + static constexpr uint32_t ISO_MINUTE_SLOT = 1; + static constexpr uint32_t ISO_SECOND_SLOT = 2; + static constexpr uint32_t ISO_MILLISECOND_SLOT = 3; + static constexpr uint32_t ISO_MICROSECOND_SLOT = 4; + static constexpr uint32_t ISO_NANOSECOND_SLOT = 5; + static constexpr uint32_t SLOT_COUNT = 6; + + int32_t isoHour() const { return getFixedSlot(ISO_HOUR_SLOT).toInt32(); } + + int32_t isoMinute() const { return getFixedSlot(ISO_MINUTE_SLOT).toInt32(); } + + int32_t isoSecond() const { return getFixedSlot(ISO_SECOND_SLOT).toInt32(); } + + int32_t isoMillisecond() const { + return getFixedSlot(ISO_MILLISECOND_SLOT).toInt32(); + } + + int32_t isoMicrosecond() const { + return getFixedSlot(ISO_MICROSECOND_SLOT).toInt32(); + } + + int32_t isoNanosecond() const { + return getFixedSlot(ISO_NANOSECOND_SLOT).toInt32(); + } + + private: + static const ClassSpec classSpec_; +}; + +/** + * Extract the time fields from the PlainTime object. + */ +inline PlainTime ToPlainTime(const PlainTimeObject* time) { + return {time->isoHour(), time->isoMinute(), + time->isoSecond(), time->isoMillisecond(), + time->isoMicrosecond(), time->isoNanosecond()}; +} + +class Increment; +enum class TemporalOverflow; +enum class TemporalRoundingMode; +enum class TemporalUnit; + +#ifdef DEBUG +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool IsValidTime(const PlainTime& time); + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool IsValidTime(double hour, double minute, double second, double millisecond, + double microsecond, double nanosecond); +#endif + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool ThrowIfInvalidTime(JSContext* cx, const PlainTime& time); + +/** + * IsValidTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +bool ThrowIfInvalidTime(JSContext* cx, double hour, double minute, + double second, double millisecond, double microsecond, + double nanosecond); + +/** + * CreateTemporalTime ( hour, minute, second, millisecond, microsecond, + * nanosecond [ , newTarget ] ) + */ +PlainTimeObject* CreateTemporalTime(JSContext* cx, const PlainTime& time); + +/** + * ToTemporalTime ( item [ , overflow ] ) + */ +bool ToTemporalTime(JSContext* cx, JS::Handle<JS::Value> item, + PlainTime* result); + +/** + * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, hours, + * minutes, seconds, milliseconds, microseconds, nanoseconds ) + */ +bool AddTime(JSContext* cx, const PlainTime& time, const Duration& duration, + PlainTime* result, double* daysResult); + +/** + * DifferenceTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, ns2 ) + */ +TimeDuration DifferenceTime(const PlainTime& time1, const PlainTime& time2); + +struct TimeRecord final { + double hour = 0; + double minute = 0; + double second = 0; + double millisecond = 0; + double microsecond = 0; + double nanosecond = 0; +}; + +/** + * ToTemporalTimeRecord ( temporalTimeLike [ , completeness ] ) + */ +bool ToTemporalTimeRecord(JSContext* cx, JS::Handle<JSObject*> temporalTimeLike, + TimeRecord* result); + +/** + * RegulateTime ( hour, minute, second, millisecond, microsecond, nanosecond, + * overflow ) + */ +bool RegulateTime(JSContext* cx, const TimeRecord& time, + TemporalOverflow overflow, PlainTime* result); + +/** + * CompareTemporalTime ( h1, min1, s1, ms1, mus1, ns1, h2, min2, s2, ms2, mus2, + * ns2 ) + */ +int32_t CompareTemporalTime(const PlainTime& one, const PlainTime& two); + +struct BalancedTime final { + int32_t days = 0; + PlainTime time; +}; + +/** + * BalanceTime ( hour, minute, second, millisecond, microsecond, nanosecond ) + */ +BalancedTime BalanceTime(const PlainTime& time, int64_t nanoseconds); + +struct RoundedTime final { + int64_t days = 0; + PlainTime time; +}; + +/** + * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, + * increment, unit, roundingMode [ , dayLengthNs ] ) + */ +RoundedTime RoundTime(const PlainTime& time, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode); + +/** + * RoundTime ( hour, minute, second, millisecond, microsecond, nanosecond, + * increment, unit, roundingMode [ , dayLengthNs ] ) + */ +RoundedTime RoundTime(const PlainTime& time, Increment increment, + TemporalUnit unit, TemporalRoundingMode roundingMode, + const InstantSpan& dayLengthNs); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_PlainTime_h */ diff --git a/js/src/builtin/temporal/PlainYearMonth.cpp b/js/src/builtin/temporal/PlainYearMonth.cpp new file mode 100644 index 0000000000..a4e2f8f9e4 --- /dev/null +++ b/js/src/builtin/temporal/PlainYearMonth.cpp @@ -0,0 +1,1642 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/PlainYearMonth.h" + +#include "mozilla/Assertions.h" + +#include <type_traits> +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.h" +#include "builtin/temporal/TemporalParser.h" +#include "builtin/temporal/TemporalRoundingMode.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/TemporalUnit.h" +#include "builtin/temporal/ToString.h" +#include "builtin/temporal/Wrapped.h" +#include "ds/IdValuePair.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCVector.h" +#include "js/Id.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.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" + +using namespace js; +using namespace js::temporal; + +static inline bool IsPlainYearMonth(Handle<Value> v) { + return v.isObject() && v.toObject().is<PlainYearMonthObject>(); +} + +/** + * ISOYearMonthWithinLimits ( year, month ) + */ +template <typename T> +static bool ISOYearMonthWithinLimits(T year, int32_t month) { + static_assert(std::is_same_v<T, int32_t> || std::is_same_v<T, double>); + + // Step 1. + MOZ_ASSERT(IsInteger(year)); + MOZ_ASSERT(1 <= month && month <= 12); + + // Step 2. + if (year < -271821 || year > 275760) { + return false; + } + + // Step 3. + if (year == -271821 && month < 4) { + return false; + } + + // Step 4. + if (year == 275760 && month > 9) { + return false; + } + + // Step 5. + return true; +} + +/** + * CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , + * newTarget ] ) + */ +static PlainYearMonthObject* CreateTemporalYearMonth( + JSContext* cx, const CallArgs& args, double isoYear, double isoMonth, + double isoDay, Handle<CalendarValue> calendar) { + MOZ_ASSERT(IsInteger(isoYear)); + MOZ_ASSERT(IsInteger(isoMonth)); + MOZ_ASSERT(IsInteger(isoDay)); + + // Step 1. + if (!ThrowIfInvalidISODate(cx, isoYear, isoMonth, isoDay)) { + return nullptr; + } + + // FIXME: spec issue - Consider calling ISODateTimeWithinLimits to include + // testing |referenceISODay|? + + // Step 2. + if (!ISOYearMonthWithinLimits(isoYear, isoMonth)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID); + return nullptr; + } + + // Steps 3-4. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PlainYearMonth, + &proto)) { + return nullptr; + } + + auto* obj = NewObjectWithClassProto<PlainYearMonthObject>(cx, proto); + if (!obj) { + return nullptr; + } + + // Step 5. + obj->setFixedSlot(PlainYearMonthObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 6. + obj->setFixedSlot(PlainYearMonthObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 7. + obj->setFixedSlot(PlainYearMonthObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 8. + obj->setFixedSlot(PlainYearMonthObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 9. + return obj; +} + +/** + * CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , + * newTarget ] ) + */ +PlainYearMonthObject* js::temporal::CreateTemporalYearMonth( + JSContext* cx, const PlainDate& date, Handle<CalendarValue> calendar) { + auto& [isoYear, isoMonth, isoDay] = date; + + // Step 1. + if (!ThrowIfInvalidISODate(cx, date)) { + return nullptr; + } + + // FIXME: spec issue - Consider calling ISODateTimeWithinLimits to include + // testing |referenceISODay|? + + // Step 2. + if (!ISOYearMonthWithinLimits(isoYear, isoMonth)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PLAIN_YEAR_MONTH_INVALID); + return nullptr; + } + + // Steps 3-4. + auto* obj = NewBuiltinClassInstance<PlainYearMonthObject>(cx); + if (!obj) { + return nullptr; + } + + // Step 5. + obj->setFixedSlot(PlainYearMonthObject::ISO_YEAR_SLOT, Int32Value(isoYear)); + + // Step 6. + obj->setFixedSlot(PlainYearMonthObject::ISO_MONTH_SLOT, Int32Value(isoMonth)); + + // Step 7. + obj->setFixedSlot(PlainYearMonthObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 8. + obj->setFixedSlot(PlainYearMonthObject::ISO_DAY_SLOT, Int32Value(isoDay)); + + // Step 9. + return obj; +} + +/** + * ToTemporalYearMonth ( item [ , options ] ) + */ +static Wrapped<PlainYearMonthObject*> ToTemporalYearMonth( + JSContext* cx, Handle<Value> item, + Handle<JSObject*> maybeOptions = nullptr) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainObject*> maybeResolvedOptions(cx); + if (maybeOptions) { + maybeResolvedOptions = SnapshotOwnProperties(cx, maybeOptions); + if (!maybeResolvedOptions) { + return nullptr; + } + } + + // Step 3. + if (item.isObject()) { + Rooted<JSObject*> itemObj(cx, &item.toObject()); + + // Step 3.a. + if (itemObj->canUnwrapAs<PlainYearMonthObject>()) { + return itemObj; + } + + // Step 3.b. + Rooted<CalendarValue> calendarValue(cx); + if (!GetTemporalCalendarWithISODefault(cx, itemObj, &calendarValue)) { + return nullptr; + } + + // Step 3.c. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return nullptr; + } + + // Step 3.d. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Month, CalendarField::MonthCode, + CalendarField::Year}, + &fieldNames)) { + return nullptr; + } + + // Step 3.e. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, itemObj, fieldNames)); + if (!fields) { + return nullptr; + } + + // Step 3.f. + if (maybeResolvedOptions) { + return CalendarYearMonthFromFields(cx, calendar, fields, + maybeResolvedOptions); + } + return CalendarYearMonthFromFields(cx, calendar, fields); + } + + // Step 4. + if (!item.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, item, + nullptr, "not a string"); + return nullptr; + } + Rooted<JSString*> string(cx, item.toString()); + + // Step 5. + PlainDate result; + Rooted<JSString*> calendarString(cx); + if (!ParseTemporalYearMonthString(cx, string, &result, &calendarString)) { + return nullptr; + } + + // Steps 6-9. + Rooted<CalendarValue> calendarValue(cx, CalendarValue(cx->names().iso8601)); + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendarValue)) { + return nullptr; + } + } + + // Step 10. + if (maybeResolvedOptions) { + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return nullptr; + } + } + + // Step 11. + Rooted<PlainYearMonthObject*> obj( + cx, CreateTemporalYearMonth(cx, result, calendarValue)); + if (!obj) { + return nullptr; + } + + // Step 12. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return nullptr; + } + + // FIXME: spec issue - reorder note to appear directly before + // CalendarYearMonthFromFields + + // Steps 13-14. + return CalendarYearMonthFromFields(cx, calendar, obj); +} + +/** + * ToTemporalYearMonth ( item [ , options ] ) + */ +static bool ToTemporalYearMonth(JSContext* cx, Handle<Value> item, + PlainDate* result) { + auto obj = ToTemporalYearMonth(cx, item); + if (!obj) { + return false; + } + + *result = ToPlainDate(&obj.unwrap()); + return true; +} + +/** + * ToTemporalYearMonth ( item [ , options ] ) + */ +static bool ToTemporalYearMonth(JSContext* cx, Handle<Value> item, + PlainDate* result, + MutableHandle<CalendarValue> calendar) { + auto* obj = ToTemporalYearMonth(cx, item).unwrapOrNull(); + if (!obj) { + return false; + } + + *result = ToPlainDate(obj); + calendar.set(obj->calendar()); + return calendar.wrap(cx); +} + +/** + * DifferenceTemporalPlainYearMonth ( operation, yearMonth, other, options ) + */ +static bool DifferenceTemporalPlainYearMonth(JSContext* cx, + TemporalDifference operation, + const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + auto otherYearMonth = ToTemporalYearMonth(cx, args.get(0)); + if (!otherYearMonth) { + return false; + } + auto* unwrappedOtherYearMonth = &otherYearMonth.unwrap(); + auto otherYearMonthDate = ToPlainDate(unwrappedOtherYearMonth); + + Rooted<Wrapped<PlainYearMonthObject*>> other(cx, otherYearMonth); + Rooted<CalendarValue> otherCalendar(cx, unwrappedOtherYearMonth->calendar()); + if (!otherCalendar.wrap(cx)) { + return false; + } + + // Step 3. + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + if (!CalendarEqualsOrThrow(cx, calendar, otherCalendar)) { + return false; + } + + // Steps 5-6. + DifferenceSettings settings; + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", ToName(operation), args[1])); + if (!options) { + return false; + } + + // Step 5. + resolvedOptions = SnapshotOwnProperties(cx, options); + if (!resolvedOptions) { + return false; + } + + // Step 6. + if (!GetDifferenceSettings(cx, operation, resolvedOptions, + TemporalUnitGroup::Date, TemporalUnit::Month, + TemporalUnit::Month, TemporalUnit::Year, + &settings)) { + return false; + } + } else { + // Steps 5-6. + settings = { + TemporalUnit::Month, + TemporalUnit::Year, + TemporalRoundingMode::Trunc, + Increment{1}, + }; + } + + // Step 7. + if (ToPlainDate(yearMonth) == otherYearMonthDate) { + auto* obj = CreateTemporalDuration(cx, {}); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 8. + // FIXME: spec issue - duplicate CreateDataPropertyOrThrow for "largestUnit". + + // Step 9. + Rooted<CalendarRecord> calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::DateAdd, + CalendarMethod::DateFromFields, + CalendarMethod::DateUntil, + CalendarMethod::Fields, + }, + &calendarRec)) { + return false; + } + + // Step 10. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendarRec, + {CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 11. + Rooted<PlainObject*> thisFields( + cx, PrepareTemporalFields(cx, yearMonth, fieldNames)); + if (!thisFields) { + return false; + } + + // Step 12. + Value one = Int32Value(1); + auto handleOne = Handle<Value>::fromMarkedLocation(&one); + if (!DefineDataProperty(cx, thisFields, cx->names().day, handleOne)) { + return false; + } + + // Step 13. + Rooted<Wrapped<PlainDateObject*>> thisDate( + cx, CalendarDateFromFields(cx, calendarRec, thisFields)); + if (!thisDate) { + return false; + } + + // Step 14. + Rooted<PlainObject*> otherFields( + cx, PrepareTemporalFields(cx, other, fieldNames)); + if (!otherFields) { + return false; + } + + // Step 15. + if (!DefineDataProperty(cx, otherFields, cx->names().day, handleOne)) { + return false; + } + + // Step 16. + Rooted<Wrapped<PlainDateObject*>> otherDate( + cx, CalendarDateFromFields(cx, calendarRec, otherFields)); + if (!otherDate) { + return false; + } + + // Steps 17-18. + Duration result; + if (resolvedOptions) { + // Step 17. + Rooted<Value> largestUnitValue( + cx, StringValue(TemporalUnitToString(cx, settings.largestUnit))); + if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit, + largestUnitValue)) { + return false; + } + + // Step 18. + if (!CalendarDateUntil(cx, calendarRec, thisDate, otherDate, + resolvedOptions, &result)) { + return false; + } + } else { + // Steps 17-18. + if (!CalendarDateUntil(cx, calendarRec, thisDate, otherDate, + settings.largestUnit, &result)) { + return false; + } + } + + // We only care about years and months here, all other fields are set to zero. + Duration duration = {result.years, result.months}; + + // Step 19. + if (settings.smallestUnit != TemporalUnit::Month || + settings.roundingIncrement != Increment{1}) { + // Steps 19.a-b. + Duration roundResult; + if (!RoundDuration(cx, duration, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, thisDate, + calendarRec, &roundResult)) { + return false; + } + + // Step 19.c. + auto toBalance = Duration{roundResult.years, roundResult.months}; + DateDuration balanceResult; + if (!temporal::BalanceDateDurationRelative( + cx, toBalance, settings.largestUnit, settings.smallestUnit, + thisDate, calendarRec, &balanceResult)) { + return false; + } + duration = balanceResult.toDuration(); + } + + // Step 20. + if (operation == TemporalDifference::Since) { + duration = duration.negate(); + } + + auto* obj = CreateTemporalDuration(cx, {duration.years, duration.months}); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +enum class PlainYearMonthDuration { Add, Subtract }; + +/** + * AddDurationToOrSubtractDurationFromPlainYearMonth ( operation, yearMonth, + * temporalDurationLike, options ) + */ +static bool AddDurationToOrSubtractDurationFromPlainYearMonth( + JSContext* cx, PlainYearMonthDuration operation, const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + + // Step 1. + Duration duration; + if (!ToTemporalDurationRecord(cx, args.get(0), &duration)) { + return false; + } + + // Step 2. + if (operation == PlainYearMonthDuration::Subtract) { + duration = duration.negate(); + } + + // Step 3. + TimeDuration balanceResult; + if (!BalanceTimeDuration(cx, duration, TemporalUnit::Day, &balanceResult)) { + return false; + } + + // Step 4. + int32_t sign = DurationSign( + {duration.years, duration.months, duration.weeks, balanceResult.days}); + + // Step 5. + Rooted<CalendarValue> calendarValue(cx, yearMonth->calendar()); + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateAdd, + CalendarMethod::DateFromFields, + CalendarMethod::Day, + CalendarMethod::Fields, + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return false; + }; + + // Step 6. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 7. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, yearMonth, fieldNames)); + if (!fields) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fieldsCopy(cx, SnapshotOwnProperties(cx, fields)); + if (!fieldsCopy) { + return false; + } + + // Step 9. + Value one = Int32Value(1); + auto handleOne = Handle<Value>::fromMarkedLocation(&one); + if (!DefineDataProperty(cx, fields, cx->names().day, handleOne)) { + return false; + } + + // Step 10. + Rooted<Wrapped<PlainDateObject*>> intermediateDate( + cx, CalendarDateFromFields(cx, calendar, fields)); + if (!intermediateDate) { + return false; + } + + // Steps 11-12. + Rooted<Wrapped<PlainDateObject*>> date(cx); + if (sign < 0) { + // |intermediateDate| is initialized to the first day of |yearMonth|'s + // month. Compute the last day of |yearMonth|'s month by first adding one + // month and then subtracting one day. + // + // This is roughly equivalent to these calls: + // + // js> var ym = new Temporal.PlainYearMonth(2023, 1); + // js> ym.toPlainDate({day: 1}).add({months: 1}).subtract({days: 1}).day + // 31 + // + // For many calendars this is equivalent to `ym.daysInMonth`, except when + // some days are skipped, for example consider the Julian-to-Gregorian + // calendar transition. + + // Step 11.a. + Duration oneMonthDuration = {0, 1}; + + // Step 11.b. + Rooted<Wrapped<PlainDateObject*>> nextMonth( + cx, CalendarDateAdd(cx, calendar, intermediateDate, oneMonthDuration)); + if (!nextMonth) { + return false; + } + + auto* unwrappedNextMonth = nextMonth.unwrap(cx); + if (!unwrappedNextMonth) { + return false; + } + auto nextMonthDate = ToPlainDate(unwrappedNextMonth); + + // Step 11.c. + PlainDate endOfMonthISO; + if (!AddISODate(cx, nextMonthDate, {0, 0, 0, -1}, + TemporalOverflow::Constrain, &endOfMonthISO)) { + return false; + } + + // Step 11.d. + Rooted<PlainDateWithCalendar> endOfMonth(cx); + if (!CreateTemporalDate(cx, endOfMonthISO, calendar.receiver(), + &endOfMonth)) { + return false; + } + + // Step 11.e. + Rooted<Value> day(cx); + if (!CalendarDay(cx, calendar, endOfMonth.date(), &day)) { + return false; + } + + // Step 11.f. + if (!DefineDataProperty(cx, fieldsCopy, cx->names().day, day)) { + return false; + } + + // Step 11.g. + date = CalendarDateFromFields(cx, calendar, fieldsCopy); + if (!date) { + return false; + } + } else { + // Step 12.a. + date = intermediateDate; + } + + // Step 13. + Duration durationToAdd = {duration.years, duration.months, duration.weeks, + balanceResult.days}; + + // FIXME: spec issue - GetOptionsObject should be called after + // ToTemporalDurationRecord to validate the input type before performing any + // other user-visible operations. + // https://github.com/tc39/proposal-temporal/issues/2721 + + // Step 14. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + const char* name = + operation == PlainYearMonthDuration::Add ? "add" : "subtract"; + options = RequireObjectArg(cx, "options", name, args[1]); + } else { + // TODO: Avoid creating an options object if not necessary. + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { + return false; + } + + // Step 15. + Rooted<PlainObject*> optionsCopy(cx, SnapshotOwnProperties(cx, options)); + if (!optionsCopy) { + return false; + } + + // Step 16. + Rooted<Wrapped<PlainDateObject*>> addedDate( + cx, AddDate(cx, calendar, date, durationToAdd, options)); + if (!addedDate) { + return false; + } + + // Step 17. + Rooted<PlainObject*> addedDateFields( + cx, PrepareTemporalFields(cx, addedDate, fieldNames)); + if (!addedDateFields) { + return false; + } + + // Step 18. + auto obj = + CalendarYearMonthFromFields(cx, calendar, addedDateFields, optionsCopy); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainYearMonth ( isoYear, isoMonth [ , calendarLike [ , + * referenceISODay ] ] ) + */ +static bool PlainYearMonthConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.PlainYearMonth")) { + return false; + } + + // Step 3. + double isoYear; + if (!ToIntegerWithTruncation(cx, args.get(0), "year", &isoYear)) { + return false; + } + + // Step 4. + double isoMonth; + if (!ToIntegerWithTruncation(cx, args.get(1), "month", &isoMonth)) { + return false; + } + + // Step 5. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendarWithISODefault(cx, args.get(2), &calendar)) { + return false; + } + + // Steps 2 and 6. + double isoDay = 1; + if (args.hasDefined(3)) { + if (!ToIntegerWithTruncation(cx, args[3], "day", &isoDay)) { + return false; + } + } + + // Step 7. + auto* yearMonth = + CreateTemporalYearMonth(cx, args, isoYear, isoMonth, isoDay, calendar); + if (!yearMonth) { + return false; + } + + args.rval().setObject(*yearMonth); + return true; +} + +/** + * Temporal.PlainYearMonth.from ( item [ , options ] ) + */ +static bool PlainYearMonth_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "from", args[1]); + if (!options) { + return false; + } + } + + // Step 2. + if (args.get(0).isObject()) { + JSObject* item = &args[0].toObject(); + + if (auto* yearMonth = item->maybeUnwrapIf<PlainYearMonthObject>()) { + auto date = ToPlainDate(yearMonth); + + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + if (!calendar.wrap(cx)) { + return false; + } + + if (options) { + // Step 2.a. + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, options, &ignored)) { + return false; + } + } + + // Step 2.b. + auto* obj = CreateTemporalYearMonth(cx, date, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + } + + // Step 3. + auto obj = ToTemporalYearMonth(cx, args.get(0), options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainYearMonth.compare ( one, two ) + */ +static bool PlainYearMonth_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + PlainDate one; + if (!ToTemporalYearMonth(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + PlainDate two; + if (!ToTemporalYearMonth(cx, args.get(1), &two)) { + return false; + } + + // Step 3. + args.rval().setInt32(CompareISODate(one, two)); + return true; +} + +/** + * get Temporal.PlainYearMonth.prototype.calendarId + */ +static bool PlainYearMonth_calendarId(JSContext* cx, const CallArgs& args) { + auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>(); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 3. + auto* calendarId = ToTemporalCalendarIdentifier(cx, calendar); + if (!calendarId) { + return false; + } + + args.rval().setString(calendarId); + return true; +} + +/** + * get Temporal.PlainYearMonth.prototype.calendarId + */ +static bool PlainYearMonth_calendarId(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_calendarId>( + cx, args); +} + +/** + * get Temporal.PlainYearMonth.prototype.year + */ +static bool PlainYearMonth_year(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + return CalendarYear(cx, calendar, yearMonth, args.rval()); +} + +/** + * get Temporal.PlainYearMonth.prototype.year + */ +static bool PlainYearMonth_year(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_year>(cx, args); +} + +/** + * get Temporal.PlainYearMonth.prototype.month + */ +static bool PlainYearMonth_month(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + return CalendarMonth(cx, calendar, yearMonth, args.rval()); +} + +/** + * get Temporal.PlainYearMonth.prototype.month + */ +static bool PlainYearMonth_month(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_month>(cx, args); +} + +/** + * get Temporal.PlainYearMonth.prototype.monthCode + */ +static bool PlainYearMonth_monthCode(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + return CalendarMonthCode(cx, calendar, yearMonth, args.rval()); +} + +/** + * get Temporal.PlainYearMonth.prototype.monthCode + */ +static bool PlainYearMonth_monthCode(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_monthCode>(cx, + args); +} + +/** + * get Temporal.PlainYearMonth.prototype.daysInYear + */ +static bool PlainYearMonth_daysInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + return CalendarDaysInYear(cx, calendar, yearMonth, args.rval()); +} + +/** + * get Temporal.PlainYearMonth.prototype.daysInYear + */ +static bool PlainYearMonth_daysInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_daysInYear>( + cx, args); +} + +/** + * get Temporal.PlainYearMonth.prototype.daysInMonth + */ +static bool PlainYearMonth_daysInMonth(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + return CalendarDaysInMonth(cx, calendar, yearMonth, args.rval()); +} + +/** + * get Temporal.PlainYearMonth.prototype.daysInMonth + */ +static bool PlainYearMonth_daysInMonth(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_daysInMonth>( + cx, args); +} + +/** + * get Temporal.PlainYearMonth.prototype.monthsInYear + */ +static bool PlainYearMonth_monthsInYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + return CalendarMonthsInYear(cx, calendar, yearMonth, args.rval()); +} + +/** + * get Temporal.PlainYearMonth.prototype.monthsInYear + */ +static bool PlainYearMonth_monthsInYear(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_monthsInYear>( + cx, args); +} + +/** + * get Temporal.PlainYearMonth.prototype.inLeapYear + */ +static bool PlainYearMonth_inLeapYear(JSContext* cx, const CallArgs& args) { + // Step 3. + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 4. + return CalendarInLeapYear(cx, calendar, yearMonth, args.rval()); +} + +/** + * get Temporal.PlainYearMonth.prototype.inLeapYear + */ +static bool PlainYearMonth_inLeapYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_inLeapYear>( + cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.with ( temporalYearMonthLike [ , options ] + * ) + */ +static bool PlainYearMonth_with(JSContext* cx, const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + Rooted<CalendarValue> calendarValue(cx, yearMonth->calendar()); + + // Step 3. + Rooted<JSObject*> temporalYearMonthLike( + cx, RequireObjectArg(cx, "temporalYearMonthLike", "with", args.get(0))); + if (!temporalYearMonthLike) { + return false; + } + + // Step 4. + if (!RejectTemporalLikeObject(cx, temporalYearMonthLike)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "with", args[1])); + if (!options) { + return false; + } + resolvedOptions = SnapshotOwnProperties(cx, options); + } else { + resolvedOptions = NewPlainObjectWithProto(cx, nullptr); + } + if (!resolvedOptions) { + return false; + } + + // Step 6. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::Fields, + CalendarMethod::MergeFields, + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields( + cx, calendar, + {CalendarField::Month, CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, yearMonth, fieldNames)); + if (!fields) { + return false; + } + + // Step 9. + Rooted<PlainObject*> partialYearMonth( + cx, PreparePartialTemporalFields(cx, temporalYearMonthLike, fieldNames)); + if (!partialYearMonth) { + return false; + } + + // Step 10. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, partialYearMonth)); + if (!mergedFields) { + return false; + } + + // Step 11. + fields = PrepareTemporalFields(cx, mergedFields, fieldNames); + if (!fields) { + return false; + } + + // Step 12. + auto obj = CalendarYearMonthFromFields(cx, calendar, fields, resolvedOptions); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.with ( temporalYearMonthLike [ , options ] + * ) + */ +static bool PlainYearMonth_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_with>(cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainYearMonth_add(JSContext* cx, const CallArgs& args) { + // Step 3. + return AddDurationToOrSubtractDurationFromPlainYearMonth( + cx, PlainYearMonthDuration::Add, args); +} + +/** + * Temporal.PlainYearMonth.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool PlainYearMonth_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_add>(cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.subtract ( temporalDurationLike [ , options + * ] ) + */ +static bool PlainYearMonth_subtract(JSContext* cx, const CallArgs& args) { + // Step 3. + return AddDurationToOrSubtractDurationFromPlainYearMonth( + cx, PlainYearMonthDuration::Subtract, args); +} + +/** + * Temporal.PlainYearMonth.prototype.subtract ( temporalDurationLike [ , options + * ] ) + */ +static bool PlainYearMonth_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_subtract>(cx, + args); +} + +/** + * Temporal.PlainYearMonth.prototype.until ( other [ , options ] ) + */ +static bool PlainYearMonth_until(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainYearMonth(cx, TemporalDifference::Until, args); +} + +/** + * Temporal.PlainYearMonth.prototype.until ( other [ , options ] ) + */ +static bool PlainYearMonth_until(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_until>(cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.since ( other [ , options ] ) + */ +static bool PlainYearMonth_since(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalPlainYearMonth(cx, TemporalDifference::Since, args); +} + +/** + * Temporal.PlainYearMonth.prototype.since ( other [ , options ] ) + */ +static bool PlainYearMonth_since(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_since>(cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.equals ( other ) + */ +static bool PlainYearMonth_equals(JSContext* cx, const CallArgs& args) { + auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>(); + auto date = ToPlainDate(yearMonth); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 3. + PlainDate other; + Rooted<CalendarValue> otherCalendar(cx); + if (!ToTemporalYearMonth(cx, args.get(0), &other, &otherCalendar)) { + return false; + } + + // Steps 4-7. + bool equals = date == other; + if (equals && !CalendarEquals(cx, calendar, otherCalendar, &equals)) { + return false; + } + + args.rval().setBoolean(equals); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.equals ( other ) + */ +static bool PlainYearMonth_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_equals>(cx, + args); +} + +/** + * Temporal.PlainYearMonth.prototype.toString ( [ options ] ) + */ +static bool PlainYearMonth_toString(JSContext* cx, const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + + auto showCalendar = CalendarOption::Auto; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Step 4. + if (!ToCalendarNameOption(cx, options, &showCalendar)) { + return false; + } + } + + // Step 5. + JSString* str = TemporalYearMonthToString(cx, yearMonth, showCalendar); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.toString ( [ options ] ) + */ +static bool PlainYearMonth_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toString>(cx, + args); +} + +/** + * Temporal.PlainYearMonth.prototype.toLocaleString ( [ locales [ , options ] ] + * ) + */ +static bool PlainYearMonth_toLocaleString(JSContext* cx, const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + + // Step 3. + JSString* str = + TemporalYearMonthToString(cx, yearMonth, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.toLocaleString ( [ locales [ , options ] ] + * ) + */ +static bool PlainYearMonth_toLocaleString(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toLocaleString>( + cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.toJSON ( ) + */ +static bool PlainYearMonth_toJSON(JSContext* cx, const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + + // Step 3. + JSString* str = + TemporalYearMonthToString(cx, yearMonth, CalendarOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.toJSON ( ) + */ +static bool PlainYearMonth_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toJSON>(cx, + args); +} + +/** + * Temporal.PlainYearMonth.prototype.valueOf ( ) + */ +static bool PlainYearMonth_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "PlainYearMonth", "primitive type"); + return false; +} + +/** + * Temporal.PlainYearMonth.prototype.toPlainDate ( item ) + */ +static bool PlainYearMonth_toPlainDate(JSContext* cx, const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + + // Step 3. + Rooted<JSObject*> item( + cx, RequireObjectArg(cx, "item", "toPlainDate", args.get(0))); + if (!item) { + return false; + } + + // Step 4. + Rooted<CalendarValue> calendarValue(cx, yearMonth->calendar()); + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, calendarValue, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + CalendarMethod::MergeFields, + }, + &calendar)) { + return false; + } + + // Step 5. + JS::RootedVector<PropertyKey> receiverFieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::MonthCode, CalendarField::Year}, + &receiverFieldNames)) { + return false; + } + + // Step 6. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, yearMonth, receiverFieldNames)); + if (!fields) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> inputFieldNames(cx); + if (!CalendarFields(cx, calendar, {CalendarField::Day}, &inputFieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> inputFields( + cx, PrepareTemporalFields(cx, item, inputFieldNames)); + if (!inputFields) { + return false; + } + + // Step 9. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, inputFields)); + if (!mergedFields) { + return false; + } + + // Step 10. + JS::RootedVector<PropertyKey> concatenatedFieldNames(cx); + if (!ConcatTemporalFieldNames(receiverFieldNames, inputFieldNames, + concatenatedFieldNames.get())) { + return false; + } + + // Step 11. + Rooted<PlainObject*> mergedFromConcatenatedFields( + cx, PrepareTemporalFields(cx, mergedFields, concatenatedFieldNames)); + if (!mergedFromConcatenatedFields) { + return false; + } + + // Step 12. + Rooted<PlainObject*> options(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!options) { + return false; + } + + // Step 13. + Rooted<Value> overflow(cx, StringValue(cx->names().constrain)); + if (!DefineDataProperty(cx, options, cx->names().overflow, overflow)) { + return false; + } + + // Step 14. + auto obj = CalendarDateFromFields(cx, calendar, mergedFromConcatenatedFields, + options); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.toPlainDate ( item ) + */ +static bool PlainYearMonth_toPlainDate(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_toPlainDate>( + cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.getISOFields ( ) + */ +static bool PlainYearMonth_getISOFields(JSContext* cx, const CallArgs& args) { + Rooted<PlainYearMonthObject*> yearMonth( + cx, &args.thisv().toObject().as<PlainYearMonthObject>()); + + // Step 3. + Rooted<IdValueVector> fields(cx, IdValueVector(cx)); + + // Step 4. + if (!fields.emplaceBack(NameToId(cx->names().calendar), + yearMonth->calendar().toValue())) { + return false; + } + + // Step 5. + if (!fields.emplaceBack(NameToId(cx->names().isoDay), + Int32Value(yearMonth->isoDay()))) { + return false; + } + + // Step 6. + if (!fields.emplaceBack(NameToId(cx->names().isoMonth), + Int32Value(yearMonth->isoMonth()))) { + return false; + } + + // Step 7. + if (!fields.emplaceBack(NameToId(cx->names().isoYear), + Int32Value(yearMonth->isoYear()))) { + return false; + } + + // Step 8. + auto* obj = + NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.getISOFields ( ) + */ +static bool PlainYearMonth_getISOFields(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_getISOFields>( + cx, args); +} + +/** + * Temporal.PlainYearMonth.prototype.getCalendar ( ) + */ +static bool PlainYearMonth_getCalendar(JSContext* cx, const CallArgs& args) { + auto* yearMonth = &args.thisv().toObject().as<PlainYearMonthObject>(); + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + + // Step 3. + auto* obj = ToTemporalCalendarObject(cx, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.PlainYearMonth.prototype.getCalendar ( ) + */ +static bool PlainYearMonth_getCalendar(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsPlainYearMonth, PlainYearMonth_getCalendar>( + cx, args); +} + +const JSClass PlainYearMonthObject::class_ = { + "Temporal.PlainYearMonth", + JSCLASS_HAS_RESERVED_SLOTS(PlainYearMonthObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_PlainYearMonth), + JS_NULL_CLASS_OPS, + &PlainYearMonthObject::classSpec_, +}; + +const JSClass& PlainYearMonthObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec PlainYearMonth_methods[] = { + JS_FN("from", PlainYearMonth_from, 1, 0), + JS_FN("compare", PlainYearMonth_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec PlainYearMonth_prototype_methods[] = { + JS_FN("with", PlainYearMonth_with, 1, 0), + JS_FN("add", PlainYearMonth_add, 1, 0), + JS_FN("subtract", PlainYearMonth_subtract, 1, 0), + JS_FN("until", PlainYearMonth_until, 1, 0), + JS_FN("since", PlainYearMonth_since, 1, 0), + JS_FN("equals", PlainYearMonth_equals, 1, 0), + JS_FN("toString", PlainYearMonth_toString, 0, 0), + JS_FN("toLocaleString", PlainYearMonth_toLocaleString, 0, 0), + JS_FN("toJSON", PlainYearMonth_toJSON, 0, 0), + JS_FN("valueOf", PlainYearMonth_valueOf, 0, 0), + JS_FN("toPlainDate", PlainYearMonth_toPlainDate, 1, 0), + JS_FN("getISOFields", PlainYearMonth_getISOFields, 0, 0), + JS_FN("getCalendar", PlainYearMonth_getCalendar, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec PlainYearMonth_prototype_properties[] = { + JS_PSG("calendarId", PlainYearMonth_calendarId, 0), + JS_PSG("year", PlainYearMonth_year, 0), + JS_PSG("month", PlainYearMonth_month, 0), + JS_PSG("monthCode", PlainYearMonth_monthCode, 0), + JS_PSG("daysInYear", PlainYearMonth_daysInYear, 0), + JS_PSG("daysInMonth", PlainYearMonth_daysInMonth, 0), + JS_PSG("monthsInYear", PlainYearMonth_monthsInYear, 0), + JS_PSG("inLeapYear", PlainYearMonth_inLeapYear, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.PlainYearMonth", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec PlainYearMonthObject::classSpec_ = { + GenericCreateConstructor<PlainYearMonthConstructor, 2, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<PlainYearMonthObject>, + PlainYearMonth_methods, + nullptr, + PlainYearMonth_prototype_methods, + PlainYearMonth_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/PlainYearMonth.h b/js/src/builtin/temporal/PlainYearMonth.h new file mode 100644 index 0000000000..e9adda9b08 --- /dev/null +++ b/js/src/builtin/temporal/PlainYearMonth.h @@ -0,0 +1,65 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_PlainYearMonth_h +#define builtin_temporal_PlainYearMonth_h + +#include <stdint.h> + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/TemporalTypes.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +namespace js { +struct ClassSpec; +} + +namespace js::temporal { + +class PlainYearMonthObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t ISO_YEAR_SLOT = 0; + static constexpr uint32_t ISO_MONTH_SLOT = 1; + static constexpr uint32_t ISO_DAY_SLOT = 2; + static constexpr uint32_t CALENDAR_SLOT = 3; + static constexpr uint32_t SLOT_COUNT = 4; + + int32_t isoYear() const { return getFixedSlot(ISO_YEAR_SLOT).toInt32(); } + + int32_t isoMonth() const { return getFixedSlot(ISO_MONTH_SLOT).toInt32(); } + + int32_t isoDay() const { return getFixedSlot(ISO_DAY_SLOT).toInt32(); } + + CalendarValue calendar() const { + return CalendarValue(getFixedSlot(CALENDAR_SLOT)); + } + + private: + static const ClassSpec classSpec_; +}; + +/** + * Extract the date fields from the PlainYearMonth object. + */ +inline PlainDate ToPlainDate(const PlainYearMonthObject* yearMonth) { + return {yearMonth->isoYear(), yearMonth->isoMonth(), yearMonth->isoDay()}; +} + +/** + * CreateTemporalYearMonth ( isoYear, isoMonth, calendar, referenceISODay [ , + * newTarget ] ) + */ +PlainYearMonthObject* CreateTemporalYearMonth( + JSContext* cx, const PlainDate& date, JS::Handle<CalendarValue> calendar); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_PlainYearMonth_h */ diff --git a/js/src/builtin/temporal/Temporal.cpp b/js/src/builtin/temporal/Temporal.cpp new file mode 100644 index 0000000000..3960a2832d --- /dev/null +++ b/js/src/builtin/temporal/Temporal.cpp @@ -0,0 +1,1850 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/Temporal.h" + +#include "mozilla/CheckedInt.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" + +#include <algorithm> +#include <cmath> +#include <cstdlib> +#include <initializer_list> +#include <iterator> +#include <stdint.h> +#include <string_view> +#include <utility> + +#include "jsfriendapi.h" +#include "jsnum.h" +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Instant.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/PlainMonthDay.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/TemporalRoundingMode.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/TemporalUnit.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "gc/Barrier.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/String.h" +#include "js/Value.h" +#include "vm/BigIntType.h" +#include "vm/BytecodeUtil.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtomState.h" +#include "vm/JSAtomUtils.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/ObjectOperations.h" +#include "vm/PIC.h" +#include "vm/PlainObject.h" +#include "vm/Realm.h" +#include "vm/StringType.h" + +#include "vm/JSObject-inl.h" +#include "vm/ObjectOperations-inl.h" + +using namespace js; +using namespace js::temporal; + +/** + * GetOption ( options, property, type, values, default ) + * + * GetOption specialization when `type=string`. Default value handling must + * happen in the caller, so we don't provide the `default` parameter here. + */ +static bool GetStringOption(JSContext* cx, Handle<JSObject*> options, + Handle<PropertyName*> property, + MutableHandle<JSString*> string) { + // Step 1. + Rooted<Value> value(cx); + if (!GetProperty(cx, options, options, property, &value)) { + return false; + } + + // Step 2. (Caller should fill in the fallback.) + if (value.isUndefined()) { + return true; + } + + // Steps 3-4. (Not applicable when type=string) + + // Step 5. + string.set(JS::ToString(cx, value)); + if (!string) { + return false; + } + + // Step 6. (Not applicable in our implementation) + + // Step 7. + return true; +} + +/** + * GetOption ( options, property, type, values, default ) + */ +static bool GetNumberOption(JSContext* cx, Handle<JSObject*> options, + Handle<PropertyName*> property, double* number) { + // Step 1. + Rooted<Value> value(cx); + if (!GetProperty(cx, options, options, property, &value)) { + return false; + } + + // Step 2. (Caller should fill in the fallback.) + if (value.isUndefined()) { + return true; + } + + // Steps 3 and 5. (Not applicable in our implementation) + + // Step 4.a. + if (!JS::ToNumber(cx, value, number)) { + return false; + } + + // Step 4.b. (Caller must check for NaN values.) + + // Step 7. (Not applicable in our implementation) + + // Step 8. + return true; +} + +/** + * ToTemporalRoundingIncrement ( normalizedOptions, dividend, inclusive ) + */ +bool js::temporal::ToTemporalRoundingIncrement(JSContext* cx, + Handle<JSObject*> options, + Increment* increment) { + // Step 1. + double number = 1; + if (!GetNumberOption(cx, options, cx->names().roundingIncrement, &number)) { + return false; + } + + // Step 3. (Reordered) + number = std::trunc(number); + + // Steps 2 and 4. + if (!std::isfinite(number) || number < 1 || number > 1'000'000'000) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, number); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "roundingIncrement", + numStr); + return false; + } + + *increment = Increment{uint32_t(number)}; + return true; +} + +/** + * ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ) + */ +bool js::temporal::ValidateTemporalRoundingIncrement(JSContext* cx, + Increment increment, + int64_t dividend, + bool inclusive) { + MOZ_ASSERT(dividend > 0); + MOZ_ASSERT_IF(!inclusive, dividend > 1); + + // Steps 1-2. + int64_t maximum = inclusive ? dividend : dividend - 1; + + // Steps 3-4. + if (increment.value() > maximum || dividend % increment.value() != 0) { + Int32ToCStringBuf cbuf; + const char* numStr = Int32ToCString(&cbuf, increment.value()); + + // TODO: Better error message could be helpful. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "roundingIncrement", + numStr); + return false; + } + + // Step 5. + return true; +} + +PropertyName* js::temporal::TemporalUnitToString(JSContext* cx, + TemporalUnit unit) { + switch (unit) { + case TemporalUnit::Auto: + break; + case TemporalUnit::Year: + return cx->names().year; + case TemporalUnit::Month: + return cx->names().month; + case TemporalUnit::Week: + return cx->names().week; + case TemporalUnit::Day: + return cx->names().day; + case TemporalUnit::Hour: + return cx->names().hour; + case TemporalUnit::Minute: + return cx->names().minute; + case TemporalUnit::Second: + return cx->names().second; + case TemporalUnit::Millisecond: + return cx->names().millisecond; + case TemporalUnit::Microsecond: + return cx->names().microsecond; + case TemporalUnit::Nanosecond: + return cx->names().nanosecond; + } + MOZ_CRASH("invalid temporal unit"); +} + +static Handle<PropertyName*> ToPropertyName(JSContext* cx, + TemporalUnitKey key) { + switch (key) { + case TemporalUnitKey::SmallestUnit: + return cx->names().smallestUnit; + case TemporalUnitKey::LargestUnit: + return cx->names().largestUnit; + case TemporalUnitKey::Unit: + return cx->names().unit; + } + MOZ_CRASH("invalid temporal unit group"); +} + +static const char* ToCString(TemporalUnitKey key) { + switch (key) { + case TemporalUnitKey::SmallestUnit: + return "smallestUnit"; + case TemporalUnitKey::LargestUnit: + return "largestUnit"; + case TemporalUnitKey::Unit: + return "unit"; + } + MOZ_CRASH("invalid temporal unit group"); +} + +static bool ToTemporalUnit(JSContext* cx, JSLinearString* str, + TemporalUnitKey key, TemporalUnit* unit) { + struct UnitMap { + std::string_view name; + TemporalUnit unit; + }; + + static constexpr UnitMap mapping[] = { + {"year", TemporalUnit::Year}, + {"years", TemporalUnit::Year}, + {"month", TemporalUnit::Month}, + {"months", TemporalUnit::Month}, + {"week", TemporalUnit::Week}, + {"weeks", TemporalUnit::Week}, + {"day", TemporalUnit::Day}, + {"days", TemporalUnit::Day}, + {"hour", TemporalUnit::Hour}, + {"hours", TemporalUnit::Hour}, + {"minute", TemporalUnit::Minute}, + {"minutes", TemporalUnit::Minute}, + {"second", TemporalUnit::Second}, + {"seconds", TemporalUnit::Second}, + {"millisecond", TemporalUnit::Millisecond}, + {"milliseconds", TemporalUnit::Millisecond}, + {"microsecond", TemporalUnit::Microsecond}, + {"microseconds", TemporalUnit::Microsecond}, + {"nanosecond", TemporalUnit::Nanosecond}, + {"nanoseconds", TemporalUnit::Nanosecond}, + }; + + // Compute the length of the longest name. + constexpr size_t maxNameLength = + std::max_element(std::begin(mapping), std::end(mapping), + [](const auto& x, const auto& y) { + return x.name.length() < y.name.length(); + }) + ->name.length(); + + // Twenty StringEqualsLiteral calls for each possible combination seems a bit + // expensive, so let's instead copy the input name into a char array and rely + // on the compiler to generate optimized code for the comparisons. + + size_t length = str->length(); + if (length <= maxNameLength && StringIsAscii(str)) { + char chars[maxNameLength] = {}; + JS::LossyCopyLinearStringChars(chars, str, length); + + for (const auto& m : mapping) { + if (m.name == std::string_view(chars, length)) { + *unit = m.unit; + return true; + } + } + } + + if (auto chars = QuoteString(cx, str, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, ToCString(key), + chars.get()); + } + return false; +} + +static std::pair<TemporalUnit, TemporalUnit> AllowedValues( + TemporalUnitGroup unitGroup) { + switch (unitGroup) { + case TemporalUnitGroup::Date: + return {TemporalUnit::Year, TemporalUnit::Day}; + case TemporalUnitGroup::Time: + return {TemporalUnit::Hour, TemporalUnit::Nanosecond}; + case TemporalUnitGroup::DateTime: + return {TemporalUnit::Year, TemporalUnit::Nanosecond}; + case TemporalUnitGroup::DayTime: + return {TemporalUnit::Day, TemporalUnit::Nanosecond}; + } + MOZ_CRASH("invalid temporal unit group"); +} + +/** + * GetTemporalUnit ( normalizedOptions, key, unitGroup, default [ , extraValues + * ] ) + */ +bool js::temporal::GetTemporalUnit(JSContext* cx, Handle<JSObject*> options, + TemporalUnitKey key, + TemporalUnitGroup unitGroup, + TemporalUnit* unit) { + // Steps 1-8. (Not applicable in our implementation.) + + // Step 9. + Rooted<JSString*> value(cx); + if (!GetStringOption(cx, options, ToPropertyName(cx, key), &value)) { + return false; + } + + // Caller should fill in the fallback. + if (!value) { + return true; + } + + return GetTemporalUnit(cx, value, key, unitGroup, unit); +} + +/** + * GetTemporalUnit ( normalizedOptions, key, unitGroup, default [ , extraValues + * ] ) + */ +bool js::temporal::GetTemporalUnit(JSContext* cx, Handle<JSString*> value, + TemporalUnitKey key, + TemporalUnitGroup unitGroup, + TemporalUnit* unit) { + // Steps 1-9. (Not applicable in our implementation.) + + // Step 10. (Handled in caller.) + + Rooted<JSLinearString*> linear(cx, value->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Caller should fill in the fallback. + if (key == TemporalUnitKey::LargestUnit) { + if (StringEqualsLiteral(linear, "auto")) { + return true; + } + } + + // Step 11. + if (!ToTemporalUnit(cx, linear, key, unit)) { + return false; + } + + auto allowedValues = AllowedValues(unitGroup); + if (*unit < allowedValues.first || *unit > allowedValues.second) { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, ToCString(key), + chars.get()); + } + return false; + } + + return true; +} + +/** + * ToTemporalRoundingMode ( normalizedOptions, fallback ) + */ +bool js::temporal::ToTemporalRoundingMode(JSContext* cx, + Handle<JSObject*> options, + TemporalRoundingMode* mode) { + // Step 1. + Rooted<JSString*> string(cx); + if (!GetStringOption(cx, options, cx->names().roundingMode, &string)) { + return false; + } + + // Caller should fill in the fallback. + if (!string) { + return true; + } + + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsLiteral(linear, "ceil")) { + *mode = TemporalRoundingMode::Ceil; + } else if (StringEqualsLiteral(linear, "floor")) { + *mode = TemporalRoundingMode::Floor; + } else if (StringEqualsLiteral(linear, "expand")) { + *mode = TemporalRoundingMode::Expand; + } else if (StringEqualsLiteral(linear, "trunc")) { + *mode = TemporalRoundingMode::Trunc; + } else if (StringEqualsLiteral(linear, "halfCeil")) { + *mode = TemporalRoundingMode::HalfCeil; + } else if (StringEqualsLiteral(linear, "halfFloor")) { + *mode = TemporalRoundingMode::HalfFloor; + } else if (StringEqualsLiteral(linear, "halfExpand")) { + *mode = TemporalRoundingMode::HalfExpand; + } else if (StringEqualsLiteral(linear, "halfTrunc")) { + *mode = TemporalRoundingMode::HalfTrunc; + } else if (StringEqualsLiteral(linear, "halfEven")) { + *mode = TemporalRoundingMode::HalfEven; + } else { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "roundingMode", + chars.get()); + } + return false; + } + return true; +} + +static BigInt* Divide(JSContext* cx, Handle<BigInt*> dividend, int64_t divisor, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(divisor > 0); + + Rooted<BigInt*> div(cx, BigInt::createFromInt64(cx, divisor)); + if (!div) { + return nullptr; + } + + Rooted<BigInt*> quotient(cx); + Rooted<BigInt*> remainder(cx); + if (!BigInt::divmod(cx, dividend, div, "ient, &remainder)) { + return nullptr; + } + + // No rounding needed when the remainder is zero. + if (remainder->isZero()) { + return quotient; + } + + switch (roundingMode) { + case TemporalRoundingMode::Ceil: { + if (!remainder->isNegative()) { + return BigInt::inc(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::Floor: { + if (remainder->isNegative()) { + return BigInt::dec(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::Trunc: + // BigInt division truncates. + return quotient; + case TemporalRoundingMode::Expand: { + if (!remainder->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + case TemporalRoundingMode::HalfCeil: { + int64_t rem; + MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); + + if (!remainder->isNegative()) { + if (uint64_t(std::abs(rem)) * 2 >= uint64_t(divisor)) { + return BigInt::inc(cx, quotient); + } + } else { + if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { + return BigInt::dec(cx, quotient); + } + } + return quotient; + } + case TemporalRoundingMode::HalfFloor: { + int64_t rem; + MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); + + if (remainder->isNegative()) { + if (uint64_t(std::abs(rem)) * 2 >= uint64_t(divisor)) { + return BigInt::dec(cx, quotient); + } + } else { + if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { + return BigInt::inc(cx, quotient); + } + } + return quotient; + } + case TemporalRoundingMode::HalfExpand: { + int64_t rem; + MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); + + if (uint64_t(std::abs(rem)) * 2 >= uint64_t(divisor)) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::HalfTrunc: { + int64_t rem; + MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); + + if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::HalfEven: { + int64_t rem; + MOZ_ALWAYS_TRUE(BigInt::isInt64(remainder, &rem)); + + if (uint64_t(std::abs(rem)) * 2 == uint64_t(divisor)) { + bool isOdd = !quotient->isZero() && (quotient->digit(0) & 1) == 1; + if (isOdd) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + } + if (uint64_t(std::abs(rem)) * 2 > uint64_t(divisor)) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + return quotient; + } + } + + MOZ_CRASH("invalid rounding mode"); +} + +static BigInt* Divide(JSContext* cx, Handle<BigInt*> dividend, + Handle<BigInt*> divisor, + TemporalRoundingMode roundingMode) { + MOZ_ASSERT(!divisor->isNegative()); + MOZ_ASSERT(!divisor->isZero()); + + Rooted<BigInt*> quotient(cx); + Rooted<BigInt*> remainder(cx); + if (!BigInt::divmod(cx, dividend, divisor, "ient, &remainder)) { + return nullptr; + } + + // No rounding needed when the remainder is zero. + if (remainder->isZero()) { + return quotient; + } + + switch (roundingMode) { + case TemporalRoundingMode::Ceil: { + if (!remainder->isNegative()) { + return BigInt::inc(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::Floor: { + if (remainder->isNegative()) { + return BigInt::dec(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::Trunc: + // BigInt division truncates. + return quotient; + case TemporalRoundingMode::Expand: { + if (!remainder->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + case TemporalRoundingMode::HalfCeil: { + BigInt* rem = BigInt::add(cx, remainder, remainder); + if (!rem) { + return nullptr; + } + + if (!remainder->isNegative()) { + if (BigInt::absoluteCompare(rem, divisor) >= 0) { + return BigInt::inc(cx, quotient); + } + } else { + if (BigInt::absoluteCompare(rem, divisor) > 0) { + return BigInt::dec(cx, quotient); + } + } + return quotient; + } + case TemporalRoundingMode::HalfFloor: { + BigInt* rem = BigInt::add(cx, remainder, remainder); + if (!rem) { + return nullptr; + } + + if (remainder->isNegative()) { + if (BigInt::absoluteCompare(rem, divisor) >= 0) { + return BigInt::dec(cx, quotient); + } + } else { + if (BigInt::absoluteCompare(rem, divisor) > 0) { + return BigInt::inc(cx, quotient); + } + } + return quotient; + } + case TemporalRoundingMode::HalfExpand: { + BigInt* rem = BigInt::add(cx, remainder, remainder); + if (!rem) { + return nullptr; + } + + if (BigInt::absoluteCompare(rem, divisor) >= 0) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::HalfTrunc: { + BigInt* rem = BigInt::add(cx, remainder, remainder); + if (!rem) { + return nullptr; + } + + if (BigInt::absoluteCompare(rem, divisor) > 0) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + return quotient; + } + case TemporalRoundingMode::HalfEven: { + BigInt* rem = BigInt::add(cx, remainder, remainder); + if (!rem) { + return nullptr; + } + + if (BigInt::absoluteCompare(rem, divisor) == 0) { + bool isOdd = !quotient->isZero() && (quotient->digit(0) & 1) == 1; + if (isOdd) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + } + if (BigInt::absoluteCompare(rem, divisor) > 0) { + if (!dividend->isNegative()) { + return BigInt::inc(cx, quotient); + } + return BigInt::dec(cx, quotient); + } + return quotient; + } + } + + MOZ_CRASH("invalid rounding mode"); +} + +static BigInt* RoundNumberToIncrementSlow(JSContext* cx, Handle<BigInt*> x, + int64_t divisor, int64_t increment, + TemporalRoundingMode roundingMode) { + // Steps 1-8. + Rooted<BigInt*> rounded(cx, Divide(cx, x, divisor, roundingMode)); + if (!rounded) { + return nullptr; + } + + // We can skip the next step when |increment=1|. + if (increment == 1) { + return rounded; + } + + // Step 9. + Rooted<BigInt*> inc(cx, BigInt::createFromInt64(cx, increment)); + if (!inc) { + return nullptr; + } + return BigInt::mul(cx, rounded, inc); +} + +static BigInt* RoundNumberToIncrementSlow(JSContext* cx, Handle<BigInt*> x, + int64_t increment, + TemporalRoundingMode roundingMode) { + return RoundNumberToIncrementSlow(cx, x, increment, increment, roundingMode); +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool js::temporal::RoundNumberToIncrement(JSContext* cx, const Instant& x, + int64_t increment, + TemporalRoundingMode roundingMode, + Instant* result) { + MOZ_ASSERT(temporal::IsValidEpochInstant(x)); + MOZ_ASSERT(increment > 0); + MOZ_ASSERT(increment <= ToNanoseconds(TemporalUnit::Day)); + + // Fast path for the default case. + if (increment == 1) { + *result = x; + return true; + } + + // Dividing zero is always zero. + if (x == Instant{}) { + *result = x; + return true; + } + + // Fast-path when we can perform the whole computation with int64 values. + if (auto num = x.toNanoseconds(); MOZ_LIKELY(num.isValid())) { + // Steps 1-8. + int64_t rounded = Divide(num.value(), increment, roundingMode); + + // Step 9. + mozilla::CheckedInt64 checked = rounded; + checked *= increment; + if (MOZ_LIKELY(checked.isValid())) { + *result = Instant::fromNanoseconds(checked.value()); + return true; + } + } + + Rooted<BigInt*> bi(cx, ToEpochNanoseconds(cx, x)); + if (!bi) { + return false; + } + + auto* rounded = RoundNumberToIncrementSlow(cx, bi, increment, roundingMode); + if (!rounded) { + return false; + } + + *result = ToInstant(rounded); + return true; +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool js::temporal::RoundNumberToIncrement(JSContext* cx, int64_t numerator, + TemporalUnit unit, + Increment increment, + TemporalRoundingMode roundingMode, + double* result) { + MOZ_ASSERT(unit >= TemporalUnit::Day); + MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); + + // Take the slow path when the increment is too large. + if (MOZ_UNLIKELY(increment > Increment{100'000})) { + Rooted<BigInt*> bi(cx, BigInt::createFromInt64(cx, numerator)); + if (!bi) { + return false; + } + + Rooted<BigInt*> denominator( + cx, BigInt::createFromInt64(cx, ToNanoseconds(unit))); + if (!denominator) { + return false; + } + + return RoundNumberToIncrement(cx, bi, denominator, increment, roundingMode, + result); + } + + int64_t divisor = ToNanoseconds(unit) * increment.value(); + MOZ_ASSERT(divisor > 0); + MOZ_ASSERT(divisor <= 8'640'000'000'000'000'000); + + // Division by one has no remainder. + if (divisor == 1) { + MOZ_ASSERT(increment == Increment{1}); + *result = double(numerator); + return true; + } + + // Steps 1-8. + int64_t rounded = Divide(numerator, divisor, roundingMode); + + // Step 9. + mozilla::CheckedInt64 checked = rounded; + checked *= increment.value(); + if (checked.isValid()) { + *result = double(checked.value()); + return true; + } + + Rooted<BigInt*> bi(cx, BigInt::createFromInt64(cx, numerator)); + if (!bi) { + return false; + } + return RoundNumberToIncrement(cx, bi, unit, increment, roundingMode, result); +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool js::temporal::RoundNumberToIncrement( + JSContext* cx, Handle<BigInt*> numerator, TemporalUnit unit, + Increment increment, TemporalRoundingMode roundingMode, double* result) { + MOZ_ASSERT(unit >= TemporalUnit::Day); + MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); + + // Take the slow path when the increment is too large. + if (MOZ_UNLIKELY(increment > Increment{100'000})) { + Rooted<BigInt*> denominator( + cx, BigInt::createFromInt64(cx, ToNanoseconds(unit))); + if (!denominator) { + return false; + } + + return RoundNumberToIncrement(cx, numerator, denominator, increment, + roundingMode, result); + } + + int64_t divisor = ToNanoseconds(unit) * increment.value(); + MOZ_ASSERT(divisor > 0); + MOZ_ASSERT(divisor <= 8'640'000'000'000'000'000); + + // Division by one has no remainder. + if (divisor == 1) { + MOZ_ASSERT(increment == Increment{1}); + *result = BigInt::numberValue(numerator); + return true; + } + + // Dividing zero is always zero. + if (numerator->isZero()) { + *result = 0; + return true; + } + + // All callers are already in the slow path, so we don't need to fast-path the + // case when |x| can be represented by an int64 value. + + // Steps 1-9. + auto* rounded = RoundNumberToIncrementSlow(cx, numerator, divisor, + increment.value(), roundingMode); + if (!rounded) { + return false; + } + + *result = BigInt::numberValue(rounded); + return true; +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool js::temporal::RoundNumberToIncrement(JSContext* cx, int64_t numerator, + int64_t denominator, + Increment increment, + TemporalRoundingMode roundingMode, + double* result) { + MOZ_ASSERT(denominator > 0); + MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); + + // Dividing zero is always zero. + if (numerator == 0) { + *result = 0; + return true; + } + + // We don't have to adjust the divisor when |increment=1|. + if (increment == Increment{1}) { + int64_t divisor = denominator; + int64_t rounded = Divide(numerator, divisor, roundingMode); + + *result = double(rounded); + return true; + } + + auto divisor = mozilla::CheckedInt64(denominator) * increment.value(); + if (MOZ_LIKELY(divisor.isValid())) { + MOZ_ASSERT(divisor.value() > 0); + + // Steps 1-8. + int64_t rounded = Divide(numerator, divisor.value(), roundingMode); + + // Step 9. + auto adjusted = mozilla::CheckedInt64(rounded) * increment.value(); + if (MOZ_LIKELY(adjusted.isValid())) { + *result = double(adjusted.value()); + return true; + } + } + + // Slow path on overflow. + + Rooted<BigInt*> bi(cx, BigInt::createFromInt64(cx, numerator)); + if (!bi) { + return false; + } + + Rooted<BigInt*> denom(cx, BigInt::createFromInt64(cx, denominator)); + if (!denom) { + return false; + } + + return RoundNumberToIncrement(cx, bi, denom, increment, roundingMode, result); +} + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool js::temporal::RoundNumberToIncrement( + JSContext* cx, Handle<BigInt*> numerator, Handle<BigInt*> denominator, + Increment increment, TemporalRoundingMode roundingMode, double* result) { + MOZ_ASSERT(!denominator->isNegative()); + MOZ_ASSERT(!denominator->isZero()); + MOZ_ASSERT(Increment::min() <= increment && increment <= Increment::max()); + + // Dividing zero is always zero. + if (numerator->isZero()) { + *result = 0; + return true; + } + + // We don't have to adjust the divisor when |increment=1|. + if (increment == Increment{1}) { + auto divisor = denominator; + + auto* rounded = Divide(cx, numerator, divisor, roundingMode); + if (!rounded) { + return false; + } + + *result = BigInt::numberValue(rounded); + return true; + } + + Rooted<BigInt*> inc(cx, BigInt::createFromUint64(cx, increment.value())); + if (!inc) { + return false; + } + + Rooted<BigInt*> divisor(cx, BigInt::mul(cx, denominator, inc)); + if (!divisor) { + return false; + } + MOZ_ASSERT(!divisor->isNegative()); + MOZ_ASSERT(!divisor->isZero()); + + // Steps 1-8. + Rooted<BigInt*> rounded(cx, Divide(cx, numerator, divisor, roundingMode)); + if (!rounded) { + return false; + } + + // Step 9. + auto* adjusted = BigInt::mul(cx, rounded, inc); + if (!adjusted) { + return false; + } + + *result = BigInt::numberValue(adjusted); + return true; +} + +/** + * ToCalendarNameOption ( normalizedOptions ) + */ +bool js::temporal::ToCalendarNameOption(JSContext* cx, + Handle<JSObject*> options, + CalendarOption* result) { + // Step 1. + Rooted<JSString*> calendarName(cx); + if (!GetStringOption(cx, options, cx->names().calendarName, &calendarName)) { + return false; + } + + // Caller should fill in the fallback. + if (!calendarName) { + return true; + } + + JSLinearString* linear = calendarName->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsLiteral(linear, "auto")) { + *result = CalendarOption::Auto; + } else if (StringEqualsLiteral(linear, "always")) { + *result = CalendarOption::Always; + } else if (StringEqualsLiteral(linear, "never")) { + *result = CalendarOption::Never; + } else if (StringEqualsLiteral(linear, "critical")) { + *result = CalendarOption::Critical; + } else { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "calendarName", + chars.get()); + } + return false; + } + return true; +} + +/** + * ToFractionalSecondDigits ( normalizedOptions ) + */ +bool js::temporal::ToFractionalSecondDigits(JSContext* cx, + Handle<JSObject*> options, + Precision* precision) { + // Step 1. + Rooted<Value> digitsValue(cx); + if (!GetProperty(cx, options, options, cx->names().fractionalSecondDigits, + &digitsValue)) { + return false; + } + + // Step 2. + if (digitsValue.isUndefined()) { + *precision = Precision::Auto(); + return true; + } + + // Step 3. + if (!digitsValue.isNumber()) { + // Step 3.a. + JSString* string = JS::ToString(cx, digitsValue); + if (!string) { + return false; + } + + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return false; + } + + if (!StringEqualsLiteral(linear, "auto")) { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, + "fractionalSecondDigits", chars.get()); + } + return false; + } + + // Step 3.b. + *precision = Precision::Auto(); + return true; + } + + // Step 4. + double digitCount = digitsValue.toNumber(); + if (!std::isfinite(digitCount)) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, digitCount); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, + "fractionalSecondDigits", numStr); + return false; + } + + // Step 5. + digitCount = std::floor(digitCount); + + // Step 6. + if (digitCount < 0 || digitCount > 9) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, digitCount); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, + "fractionalSecondDigits", numStr); + return false; + } + + // Step 7. + *precision = Precision{uint8_t(digitCount)}; + return true; +} + +/** + * ToSecondsStringPrecisionRecord ( smallestUnit, fractionalDigitCount ) + */ +SecondsStringPrecision js::temporal::ToSecondsStringPrecision( + TemporalUnit smallestUnit, Precision fractionalDigitCount) { + MOZ_ASSERT(smallestUnit == TemporalUnit::Auto || + smallestUnit >= TemporalUnit::Minute); + MOZ_ASSERT(fractionalDigitCount == Precision::Auto() || + fractionalDigitCount.value() <= 9); + + // Steps 1-5. + switch (smallestUnit) { + // Step 1. + case TemporalUnit::Minute: + return {Precision::Minute(), TemporalUnit::Minute, Increment{1}}; + + // Step 2. + case TemporalUnit::Second: + return {Precision{0}, TemporalUnit::Second, Increment{1}}; + + // Step 3. + case TemporalUnit::Millisecond: + return {Precision{3}, TemporalUnit::Millisecond, Increment{1}}; + + // Step 4. + case TemporalUnit::Microsecond: + return {Precision{6}, TemporalUnit::Microsecond, Increment{1}}; + + // Step 5. + case TemporalUnit::Nanosecond: + return {Precision{9}, TemporalUnit::Nanosecond, Increment{1}}; + + case TemporalUnit::Auto: + break; + + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Day: + case TemporalUnit::Hour: + MOZ_CRASH("Unexpected temporal unit"); + } + + // Step 6. (Not applicable in our implementation.) + + // Step 7. + if (fractionalDigitCount == Precision::Auto()) { + return {Precision::Auto(), TemporalUnit::Nanosecond, Increment{1}}; + } + + static constexpr Increment increments[] = { + Increment{1}, + Increment{10}, + Increment{100}, + }; + + uint8_t digitCount = fractionalDigitCount.value(); + + // Step 8. + if (digitCount == 0) { + return {Precision{0}, TemporalUnit::Second, Increment{1}}; + } + + // Step 9. + if (digitCount <= 3) { + return {fractionalDigitCount, TemporalUnit::Millisecond, + increments[3 - digitCount]}; + } + + // Step 10. + if (digitCount <= 6) { + return {fractionalDigitCount, TemporalUnit::Microsecond, + increments[6 - digitCount]}; + } + + // Step 11. + MOZ_ASSERT(digitCount <= 9); + + // Step 12. + return {fractionalDigitCount, TemporalUnit::Nanosecond, + increments[9 - digitCount]}; +} + +/** + * ToTemporalOverflow ( normalizedOptions ) + */ +bool js::temporal::ToTemporalOverflow(JSContext* cx, Handle<JSObject*> options, + TemporalOverflow* result) { + // Step 1. + Rooted<JSString*> overflow(cx); + if (!GetStringOption(cx, options, cx->names().overflow, &overflow)) { + return false; + } + + // Caller should fill in the fallback. + if (!overflow) { + return true; + } + + JSLinearString* linear = overflow->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsLiteral(linear, "constrain")) { + *result = TemporalOverflow::Constrain; + } else if (StringEqualsLiteral(linear, "reject")) { + *result = TemporalOverflow::Reject; + } else { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "overflow", + chars.get()); + } + return false; + } + return true; +} + +/** + * ToTemporalDisambiguation ( options ) + */ +bool js::temporal::ToTemporalDisambiguation( + JSContext* cx, Handle<JSObject*> options, + TemporalDisambiguation* disambiguation) { + // Step 1. (Not applicable) + + // Step 2. + Rooted<JSString*> string(cx); + if (!GetStringOption(cx, options, cx->names().disambiguation, &string)) { + return false; + } + + // Caller should fill in the fallback. + if (!string) { + return true; + } + + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsLiteral(linear, "compatible")) { + *disambiguation = TemporalDisambiguation::Compatible; + } else if (StringEqualsLiteral(linear, "earlier")) { + *disambiguation = TemporalDisambiguation::Earlier; + } else if (StringEqualsLiteral(linear, "later")) { + *disambiguation = TemporalDisambiguation::Later; + } else if (StringEqualsLiteral(linear, "reject")) { + *disambiguation = TemporalDisambiguation::Reject; + } else { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "disambiguation", + chars.get()); + } + return false; + } + return true; +} + +/** + * ToTemporalOffset ( options, fallback ) + */ +bool js::temporal::ToTemporalOffset(JSContext* cx, Handle<JSObject*> options, + TemporalOffset* offset) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<JSString*> string(cx); + if (!GetStringOption(cx, options, cx->names().offset, &string)) { + return false; + } + + // Caller should fill in the fallback. + if (!string) { + return true; + } + + JSLinearString* linear = string->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsLiteral(linear, "prefer")) { + *offset = TemporalOffset::Prefer; + } else if (StringEqualsLiteral(linear, "use")) { + *offset = TemporalOffset::Use; + } else if (StringEqualsLiteral(linear, "ignore")) { + *offset = TemporalOffset::Ignore; + } else if (StringEqualsLiteral(linear, "reject")) { + *offset = TemporalOffset::Reject; + } else { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "offset", + chars.get()); + } + return false; + } + return true; +} + +/** + * ToTimeZoneNameOption ( normalizedOptions ) + */ +bool js::temporal::ToTimeZoneNameOption(JSContext* cx, + Handle<JSObject*> options, + TimeZoneNameOption* result) { + // Step 1. + Rooted<JSString*> timeZoneName(cx); + if (!GetStringOption(cx, options, cx->names().timeZoneName, &timeZoneName)) { + return false; + } + + // Caller should fill in the fallback. + if (!timeZoneName) { + return true; + } + + JSLinearString* linear = timeZoneName->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsLiteral(linear, "auto")) { + *result = TimeZoneNameOption::Auto; + } else if (StringEqualsLiteral(linear, "never")) { + *result = TimeZoneNameOption::Never; + } else if (StringEqualsLiteral(linear, "critical")) { + *result = TimeZoneNameOption::Critical; + } else { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "timeZoneName", + chars.get()); + } + return false; + } + return true; +} + +/** + * ToShowOffsetOption ( normalizedOptions ) + */ +bool js::temporal::ToShowOffsetOption(JSContext* cx, Handle<JSObject*> options, + ShowOffsetOption* result) { + // FIXME: spec issue - should be renamed to ToOffsetOption to match the other + // operations ToCalendarNameOption and ToTimeZoneNameOption. + // + // https://github.com/tc39/proposal-temporal/issues/2441 + + // Step 1. + Rooted<JSString*> offset(cx); + if (!GetStringOption(cx, options, cx->names().offset, &offset)) { + return false; + } + + // Caller should fill in the fallback. + if (!offset) { + return true; + } + + JSLinearString* linear = offset->ensureLinear(cx); + if (!linear) { + return false; + } + + if (StringEqualsLiteral(linear, "auto")) { + *result = ShowOffsetOption::Auto; + } else if (StringEqualsLiteral(linear, "never")) { + *result = ShowOffsetOption::Never; + } else { + if (auto chars = QuoteString(cx, linear, '"')) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_INVALID_OPTION_VALUE, "offset", + chars.get()); + } + return false; + } + return true; +} + +template <typename T, typename... Ts> +static JSObject* MaybeUnwrapIf(JSObject* object) { + if (auto* unwrapped = object->maybeUnwrapIf<T>()) { + return unwrapped; + } + if constexpr (sizeof...(Ts) > 0) { + return MaybeUnwrapIf<Ts...>(object); + } + return nullptr; +} + +// FIXME: spec issue - "Reject" is exclusively used for Promise rejection. The +// existing `RejectPromise` abstract operation unconditionally rejects, whereas +// this operation conditionally rejects. +// https://github.com/tc39/proposal-temporal/issues/2534 + +/** + * RejectTemporalLikeObject ( object ) + */ +bool js::temporal::RejectTemporalLikeObject(JSContext* cx, + Handle<JSObject*> object) { + // Step 1. + if (auto* unwrapped = + MaybeUnwrapIf<PlainDateObject, PlainDateTimeObject, + PlainMonthDayObject, PlainTimeObject, + PlainYearMonthObject, ZonedDateTimeObject>(object)) { + Rooted<Value> value(cx, ObjectValue(*object)); + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, value, + nullptr, unwrapped->getClass()->name); + return false; + } + + Rooted<Value> property(cx); + + // Step 2. + if (!GetProperty(cx, object, object, cx->names().calendar, &property)) { + return false; + } + + // Step 3. + if (!property.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "calendar"); + return false; + } + + // Step 4. + if (!GetProperty(cx, object, object, cx->names().timeZone, &property)) { + return false; + } + + // Step 5. + if (!property.isUndefined()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_UNEXPECTED_PROPERTY, "timeZone"); + return false; + } + + // Step 6. + return true; +} + +/** + * ToPositiveIntegerWithTruncation ( argument ) + */ +bool js::temporal::ToPositiveIntegerWithTruncation(JSContext* cx, + Handle<Value> value, + const char* name, + double* result) { + // Step 1. + double number; + if (!ToIntegerWithTruncation(cx, value, name, &number)) { + return false; + } + + // Step 2. + if (number <= 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_NUMBER, name); + return false; + } + + // Step 3. + *result = number; + return true; +} + +/** + * ToIntegerWithTruncation ( argument ) + */ +bool js::temporal::ToIntegerWithTruncation(JSContext* cx, Handle<Value> value, + const char* name, double* result) { + // Step 1. + double number; + if (!JS::ToNumber(cx, value, &number)) { + return false; + } + + // Step 2. + if (!std::isfinite(number)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_INTEGER, name); + return false; + } + + // Step 3. + *result = std::trunc(number) + (+0.0); // Add zero to convert -0 to +0. + return true; +} + +/** + * GetMethod ( V, P ) + */ +JSObject* js::temporal::GetMethod(JSContext* cx, Handle<JSObject*> object, + Handle<PropertyName*> name) { + // Step 1. + Rooted<Value> value(cx); + if (!GetProperty(cx, object, object, name, &value)) { + return nullptr; + } + + // Steps 2-3. + if (!IsCallable(value)) { + if (auto chars = StringToNewUTF8CharsZ(cx, *name)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_PROPERTY_NOT_CALLABLE, chars.get()); + } + return nullptr; + } + + // Step 4. + return &value.toObject(); +} + +/** + * CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] ) + * + * Implementation when |excludedKeys| and |excludedValues| are both empty lists. + */ +bool js::temporal::CopyDataProperties(JSContext* cx, + Handle<PlainObject*> target, + Handle<JSObject*> source) { + // Optimization for the common case when |source| is a native object. + if (source->is<NativeObject>()) { + bool optimized = false; + if (!CopyDataPropertiesNative(cx, target, source.as<NativeObject>(), + nullptr, &optimized)) { + return false; + } + if (optimized) { + return true; + } + } + + // Step 1-2. (Not applicable) + + // Step 3. + JS::RootedVector<PropertyKey> keys(cx); + if (!GetPropertyKeys( + cx, source, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) { + return false; + } + + // Step 4. + Rooted<mozilla::Maybe<PropertyDescriptor>> desc(cx); + Rooted<Value> propValue(cx); + for (size_t i = 0; i < keys.length(); i++) { + Handle<PropertyKey> key = keys[i]; + + // Steps 4.a-b. (Not applicable) + + // Step 4.c.i. + if (!GetOwnPropertyDescriptor(cx, source, key, &desc)) { + return false; + } + + // Step 4.c.ii. + if (desc.isNothing() || !desc->enumerable()) { + continue; + } + + // Step 4.c.ii.1. + if (!GetProperty(cx, source, source, key, &propValue)) { + return false; + } + + // Step 4.c.ii.2. (Not applicable) + + // Step 4.c.ii.3. + if (!DefineDataProperty(cx, target, key, propValue)) { + return false; + } + } + + // Step 5. + return true; +} + +/** + * CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] ) + * + * Implementation when |excludedKeys| is an empty list and |excludedValues| is + * the list ยซundefinedยป. + */ +static bool CopyDataPropertiesIgnoreUndefined(JSContext* cx, + Handle<PlainObject*> target, + Handle<JSObject*> source) { + // Step 1-2. (Not applicable) + + // Step 3. + JS::RootedVector<PropertyKey> keys(cx); + if (!GetPropertyKeys( + cx, source, JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS, &keys)) { + return false; + } + + // Step 4. + Rooted<mozilla::Maybe<PropertyDescriptor>> desc(cx); + Rooted<Value> propValue(cx); + for (size_t i = 0; i < keys.length(); i++) { + Handle<PropertyKey> key = keys[i]; + + // Steps 4.a-b. (Not applicable) + + // Step 4.c.i. + if (!GetOwnPropertyDescriptor(cx, source, key, &desc)) { + return false; + } + + // Step 4.c.ii. + if (desc.isNothing() || !desc->enumerable()) { + continue; + } + + // Step 4.c.ii.1. + if (!GetProperty(cx, source, source, key, &propValue)) { + return false; + } + + // Step 4.c.ii.2. + if (propValue.isUndefined()) { + continue; + } + + // Step 4.c.ii.3. + if (!DefineDataProperty(cx, target, key, propValue)) { + return false; + } + } + + // Step 5. + return true; +} + +/** + * SnapshotOwnProperties ( source, proto [, excludedKeys [, excludedValues ] ] ) + */ +PlainObject* js::temporal::SnapshotOwnProperties(JSContext* cx, + Handle<JSObject*> source) { + // Step 1. + Rooted<PlainObject*> copy(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!copy) { + return nullptr; + } + + // Steps 2-4. + if (!CopyDataProperties(cx, copy, source)) { + return nullptr; + } + + // Step 3. + return copy; +} + +/** + * SnapshotOwnProperties ( source, proto [, excludedKeys [, excludedValues ] ] ) + * + * Implementation when |excludedKeys| is an empty list and |excludedValues| is + * the list ยซundefinedยป. + */ +PlainObject* js::temporal::SnapshotOwnPropertiesIgnoreUndefined( + JSContext* cx, Handle<JSObject*> source) { + // Step 1. + Rooted<PlainObject*> copy(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!copy) { + return nullptr; + } + + // Steps 2-4. + if (!CopyDataPropertiesIgnoreUndefined(cx, copy, source)) { + return nullptr; + } + + // Step 3. + return copy; +} + +/** + * GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, + * fallbackSmallestUnit, smallestLargestDefaultUnit ) + */ +bool js::temporal::GetDifferenceSettings( + JSContext* cx, TemporalDifference operation, Handle<PlainObject*> options, + TemporalUnitGroup unitGroup, TemporalUnit smallestAllowedUnit, + TemporalUnit fallbackSmallestUnit, TemporalUnit smallestLargestDefaultUnit, + DifferenceSettings* result) { + // Steps 1-2. + auto largestUnit = TemporalUnit::Auto; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::LargestUnit, unitGroup, + &largestUnit)) { + return false; + } + + // Step 3. + if (largestUnit > smallestAllowedUnit) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_OPTION, + TemporalUnitToString(largestUnit), "largestUnit"); + return false; + } + + // Step 4. + auto roundingIncrement = Increment{1}; + if (!ToTemporalRoundingIncrement(cx, options, &roundingIncrement)) { + return false; + } + + // Step 5. + auto roundingMode = TemporalRoundingMode::Trunc; + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 6. + if (operation == TemporalDifference::Since) { + roundingMode = NegateTemporalRoundingMode(roundingMode); + } + + // Step 7. + auto smallestUnit = fallbackSmallestUnit; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, unitGroup, + &smallestUnit)) { + return false; + } + + // Step 8. + if (smallestUnit > smallestAllowedUnit) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_INVALID_UNIT_OPTION, + TemporalUnitToString(smallestUnit), "smallestUnit"); + return false; + } + + // Step 9. (Inlined call to LargerOfTwoTemporalUnits) + auto defaultLargestUnit = std::min(smallestLargestDefaultUnit, smallestUnit); + + // Step 10. + if (largestUnit == TemporalUnit::Auto) { + largestUnit = defaultLargestUnit; + } + + // Step 11. + if (largestUnit > smallestUnit) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_RANGE); + return false; + } + + // Steps 12-13. + if (smallestUnit > TemporalUnit::Day) { + // Step 12. + auto maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit); + + // Step 13. + if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, + false)) { + return false; + } + } + + // Step 14. + *result = {smallestUnit, largestUnit, roundingMode, roundingIncrement}; + return true; +} + +bool temporal::IsArrayIterationSane(JSContext* cx, bool* result) { + auto* stubChain = ForOfPIC::getOrCreate(cx); + if (!stubChain) { + return false; + } + return stubChain->tryOptimizeArray(cx, result); +} + +static JSObject* CreateTemporalObject(JSContext* cx, JSProtoKey key) { + Rooted<JSObject*> proto(cx, &cx->global()->getObjectPrototype()); + + // The |Temporal| object is just a plain object with some "static" data + // properties and some constructor properties. + return NewTenuredObjectWithGivenProto<TemporalObject>(cx, proto); +} + +/** + * Initializes the Temporal Object and its standard built-in properties. + */ +static bool TemporalClassFinish(JSContext* cx, Handle<JSObject*> temporal, + Handle<JSObject*> proto) { + Rooted<PropertyKey> ctorId(cx); + Rooted<Value> ctorValue(cx); + auto defineProperty = [&](JSProtoKey protoKey, Handle<PropertyName*> name) { + JSObject* ctor = GlobalObject::getOrCreateConstructor(cx, protoKey); + if (!ctor) { + return false; + } + + ctorId = NameToId(name); + ctorValue.setObject(*ctor); + return DefineDataProperty(cx, temporal, ctorId, ctorValue, 0); + }; + + // Add the constructor properties. + for (const auto& protoKey : + {JSProto_Calendar, JSProto_Duration, JSProto_Instant, JSProto_PlainDate, + JSProto_PlainDateTime, JSProto_PlainMonthDay, JSProto_PlainTime, + JSProto_PlainYearMonth, JSProto_TimeZone, JSProto_ZonedDateTime}) { + if (!defineProperty(protoKey, ClassName(protoKey, cx))) { + return false; + } + } + + // ClassName(JSProto_TemporalNow) returns "TemporalNow", so we need to handle + // it separately. + if (!defineProperty(JSProto_TemporalNow, cx->names().Now)) { + return false; + } + + return true; +} + +const JSClass TemporalObject::class_ = { + "Temporal", + JSCLASS_HAS_CACHED_PROTO(JSProto_Temporal), + JS_NULL_CLASS_OPS, + &TemporalObject::classSpec_, +}; + +static const JSPropertySpec Temporal_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Temporal", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec TemporalObject::classSpec_ = { + CreateTemporalObject, nullptr, nullptr, + Temporal_properties, nullptr, nullptr, + TemporalClassFinish, +}; diff --git a/js/src/builtin/temporal/Temporal.h b/js/src/builtin/temporal/Temporal.h new file mode 100644 index 0000000000..3d014bfaa7 --- /dev/null +++ b/js/src/builtin/temporal/Temporal.h @@ -0,0 +1,397 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_Temporal_h +#define builtin_temporal_Temporal_h + +#include "mozilla/Assertions.h" + +#include <stdint.h> + +#include "jstypes.h" + +#include "builtin/temporal/TemporalRoundingMode.h" +#include "builtin/temporal/TemporalUnit.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "vm/NativeObject.h" + +namespace js { +struct ClassSpec; +class PlainObject; +class PropertyName; +} // namespace js + +namespace js::temporal { + +class TemporalObject : public NativeObject { + public: + static const JSClass class_; + + private: + static const ClassSpec classSpec_; +}; + +struct Instant; +struct PlainTime; + +/** + * Rounding increment, which is an integer in the range [1, 1'000'000'000]. + * + * Temporal units are rounded to a multiple of the specified increment value. + */ +class Increment final { + uint32_t value_; + + public: + constexpr explicit Increment(uint32_t value) : value_(value) { + MOZ_ASSERT(1 <= value && value <= 1'000'000'000); + } + + /** + * Minimum allowed rounding increment. + */ + static constexpr auto min() { return Increment{1}; } + + /** + * Maximum allowed rounding increment. + */ + static constexpr auto max() { return Increment{1'000'000'000}; } + + /** + * The rounding increment's value. + */ + uint32_t value() const { return value_; } + + bool operator==(const Increment& other) const { + return value_ == other.value_; + } + + bool operator<(const Increment& other) const { return value_ < other.value_; } + + // Other operators are implemented in terms of operator== and operator<. + bool operator!=(const Increment& other) const { return !(*this == other); } + bool operator>(const Increment& other) const { return other < *this; } + bool operator<=(const Increment& other) const { return !(other < *this); } + bool operator>=(const Increment& other) const { return !(*this < other); } +}; + +/** + * ToTemporalRoundingIncrement ( normalizedOptions, dividend, inclusive ) + */ +bool ToTemporalRoundingIncrement(JSContext* cx, JS::Handle<JSObject*> options, + Increment* increment); + +/** + * ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ) + */ +bool ValidateTemporalRoundingIncrement(JSContext* cx, Increment increment, + int64_t dividend, bool inclusive); + +/** + * ValidateTemporalRoundingIncrement ( increment, dividend, inclusive ) + */ +inline bool ValidateTemporalRoundingIncrement(JSContext* cx, + Increment increment, + Increment dividend, + bool inclusive) { + return ValidateTemporalRoundingIncrement(cx, increment, dividend.value(), + inclusive); +} + +/** + * MaximumTemporalDurationRoundingIncrement ( unit ) + */ +constexpr Increment MaximumTemporalDurationRoundingIncrement( + TemporalUnit unit) { + // Step 1. (Not applicable in our implementation.) + MOZ_ASSERT(unit > TemporalUnit::Day); + + // Step 2. + if (unit == TemporalUnit::Hour) { + return Increment{24}; + } + + // Step 3. + if (unit <= TemporalUnit::Second) { + return Increment{60}; + } + + // Steps 4-5. + return Increment{1000}; +} + +PropertyName* TemporalUnitToString(JSContext* cx, TemporalUnit unit); + +enum class TemporalUnitGroup { + // Allow date units: "year", "month", "week", "day". + Date, + + // Allow time units: "hour", "minute", "second", "milli-/micro-/nanoseconds". + Time, + + // Allow date and time units. + DateTime, + + // Allow "day" and time units. + DayTime, +}; + +enum class TemporalUnitKey { + SmallestUnit, + LargestUnit, + Unit, +}; + +/** + * GetTemporalUnit ( normalizedOptions, key, unitGroup, default [ , extraValues + * ] ) + */ +bool GetTemporalUnit(JSContext* cx, JS::Handle<JSObject*> options, + TemporalUnitKey key, TemporalUnitGroup unitGroup, + TemporalUnit* unit); + +/** + * GetTemporalUnit ( normalizedOptions, key, unitGroup, default [ , extraValues + * ] ) + */ +bool GetTemporalUnit(JSContext* cx, JS::Handle<JSString*> value, + TemporalUnitKey key, TemporalUnitGroup unitGroup, + TemporalUnit* unit); + +/** + * ToTemporalRoundingMode ( normalizedOptions, fallback ) + */ +bool ToTemporalRoundingMode(JSContext* cx, JS::Handle<JSObject*> options, + TemporalRoundingMode* mode); + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool RoundNumberToIncrement(JSContext* cx, const Instant& x, int64_t increment, + TemporalRoundingMode roundingMode, Instant* result); + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool RoundNumberToIncrement(JSContext* cx, int64_t numerator, TemporalUnit unit, + Increment increment, + TemporalRoundingMode roundingMode, double* result); + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool RoundNumberToIncrement(JSContext* cx, JS::Handle<JS::BigInt*> numerator, + TemporalUnit unit, Increment increment, + TemporalRoundingMode roundingMode, double* result); + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool RoundNumberToIncrement(JSContext* cx, int64_t numerator, + int64_t denominator, Increment increment, + TemporalRoundingMode roundingMode, double* result); + +/** + * RoundNumberToIncrement ( x, increment, roundingMode ) + */ +bool RoundNumberToIncrement(JSContext* cx, JS::Handle<JS::BigInt*> numerator, + JS::Handle<JS::BigInt*> denominator, + Increment increment, + TemporalRoundingMode roundingMode, double* result); + +enum class CalendarOption { Auto, Always, Never, Critical }; + +/** + * ToCalendarNameOption ( normalizedOptions ) + */ +bool ToCalendarNameOption(JSContext* cx, JS::Handle<JSObject*> options, + CalendarOption* result); + +/** + * Precision when displaying fractional seconds. + */ +class Precision final { + int8_t value_; + + enum class Tag {}; + constexpr Precision(int8_t value, Tag) : value_(value) {} + + public: + constexpr explicit Precision(uint8_t value) : value_(value) { + MOZ_ASSERT(value < 10); + } + + bool operator==(const Precision& other) const { + return value_ == other.value_; + } + + bool operator!=(const Precision& other) const { return !(*this == other); } + + /** + * Return the number of fractional second digits. + */ + uint8_t value() const { + MOZ_ASSERT(value_ >= 0, "auto and minute precision don't have a value"); + return uint8_t(value_); + } + + /** + * Limit the precision to trim off any trailing zeros. + */ + static constexpr Precision Auto() { return {-1, Tag{}}; } + + /** + * Limit the precision to minutes, i.e. don't display seconds and sub-seconds. + */ + static constexpr Precision Minute() { return {-2, Tag{}}; } +}; + +/** + * ToFractionalSecondDigits ( normalizedOptions ) + */ +bool ToFractionalSecondDigits(JSContext* cx, JS::Handle<JSObject*> options, + Precision* precision); + +struct SecondsStringPrecision final { + Precision precision = Precision{0}; + TemporalUnit unit = TemporalUnit::Auto; + Increment increment = Increment{1}; +}; + +/** + * ToSecondsStringPrecisionRecord ( smallestUnit, fractionalDigitCount ) + */ +SecondsStringPrecision ToSecondsStringPrecision(TemporalUnit smallestUnit, + Precision fractionalDigitCount); + +enum class TemporalOverflow { Constrain, Reject }; + +/** + * ToTemporalOverflow ( normalizedOptions ) + */ +bool ToTemporalOverflow(JSContext* cx, JS::Handle<JSObject*> options, + TemporalOverflow* result); + +enum class TemporalDisambiguation { Compatible, Earlier, Later, Reject }; + +/** + * ToTemporalDisambiguation ( options ) + */ +bool ToTemporalDisambiguation(JSContext* cx, JS::Handle<JSObject*> options, + TemporalDisambiguation* disambiguation); + +enum class TemporalOffset { Prefer, Use, Ignore, Reject }; + +/** + * ToTemporalOffset ( options, fallback ) + */ +bool ToTemporalOffset(JSContext* cx, JS::Handle<JSObject*> options, + TemporalOffset* offset); + +enum class TimeZoneNameOption { Auto, Never, Critical }; + +bool ToTimeZoneNameOption(JSContext* cx, JS::Handle<JSObject*> options, + TimeZoneNameOption* result); + +enum class ShowOffsetOption { Auto, Never }; + +/** + * ToShowOffsetOption ( normalizedOptions ) + */ +bool ToShowOffsetOption(JSContext* cx, JS::Handle<JSObject*> options, + ShowOffsetOption* result); + +/** + * RejectTemporalLikeObject ( object ) + */ +bool RejectTemporalLikeObject(JSContext* cx, JS::Handle<JSObject*> object); + +/** + * ToPositiveIntegerWithTruncation ( argument ) + */ +bool ToPositiveIntegerWithTruncation(JSContext* cx, JS::Handle<JS::Value> value, + const char* name, double* result); + +/** + * ToIntegerWithTruncation ( argument ) + */ +bool ToIntegerWithTruncation(JSContext* cx, JS::Handle<JS::Value> value, + const char* name, double* result); + +/** + * GetMethod ( V, P ) + */ +JSObject* GetMethod(JSContext* cx, JS::Handle<JSObject*> object, + JS::Handle<PropertyName*> name); + +/** + * SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] + * ) + */ +PlainObject* SnapshotOwnProperties(JSContext* cx, JS::Handle<JSObject*> source); + +/** + * SnapshotOwnProperties ( source, proto [ , excludedKeys [ , excludedValues ] ] + * ) + */ +PlainObject* SnapshotOwnPropertiesIgnoreUndefined(JSContext* cx, + JS::Handle<JSObject*> source); + +/** + * CopyDataProperties ( target, source, excludedKeys [ , excludedValues ] ) + */ +bool CopyDataProperties(JSContext* cx, JS::Handle<PlainObject*> target, + JS::Handle<JSObject*> source); + +enum class TemporalDifference { Since, Until }; + +inline const char* ToName(TemporalDifference difference) { + return difference == TemporalDifference::Since ? "since" : "until"; +} + +struct DifferenceSettings final { + TemporalUnit smallestUnit = TemporalUnit::Auto; + TemporalUnit largestUnit = TemporalUnit::Auto; + TemporalRoundingMode roundingMode = TemporalRoundingMode::Trunc; + Increment roundingIncrement = Increment{1}; +}; + +/** + * GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, + * fallbackSmallestUnit, smallestLargestDefaultUnit ) + */ +bool GetDifferenceSettings(JSContext* cx, TemporalDifference operation, + JS::Handle<PlainObject*> options, + TemporalUnitGroup unitGroup, + TemporalUnit smallestAllowedUnit, + TemporalUnit fallbackSmallestUnit, + TemporalUnit smallestLargestDefaultUnit, + DifferenceSettings* result); + +/** + * GetDifferenceSettings ( operation, options, unitGroup, disallowedUnits, + * fallbackSmallestUnit, smallestLargestDefaultUnit ) + */ +inline bool GetDifferenceSettings(JSContext* cx, TemporalDifference operation, + JS::Handle<PlainObject*> options, + TemporalUnitGroup unitGroup, + TemporalUnit fallbackSmallestUnit, + TemporalUnit smallestLargestDefaultUnit, + DifferenceSettings* result) { + return GetDifferenceSettings(cx, operation, options, unitGroup, + TemporalUnit::Nanosecond, fallbackSmallestUnit, + smallestLargestDefaultUnit, result); +} + +/** + * Sets |result| to `true` when array iteration is still in its initial state. + */ +bool IsArrayIterationSane(JSContext* cx, bool* result); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_Temporal_h */ diff --git a/js/src/builtin/temporal/TemporalFields.cpp b/js/src/builtin/temporal/TemporalFields.cpp new file mode 100644 index 0000000000..9ac2e44639 --- /dev/null +++ b/js/src/builtin/temporal/TemporalFields.cpp @@ -0,0 +1,939 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/TemporalFields.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" +#include "mozilla/Range.h" +#include "mozilla/RangedPtr.h" + +#include <algorithm> +#include <cstring> +#include <iterator> +#include <stdint.h> +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Temporal.h" +#include "ds/Sort.h" +#include "gc/Barrier.h" +#include "gc/Tracer.h" +#include "js/AllocPolicy.h" +#include "js/ComparisonOperators.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCVector.h" +#include "js/Id.h" +#include "js/Printer.h" +#include "js/RootingAPI.h" +#include "js/TracingAPI.h" +#include "js/TypeDecls.h" +#include "js/Utility.h" +#include "js/Value.h" +#include "util/Text.h" +#include "vm/BytecodeUtil.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" +#include "vm/StringType.h" +#include "vm/SymbolType.h" + +#include "vm/JSAtomUtils-inl.h" +#include "vm/ObjectOperations-inl.h" + +using namespace js; +using namespace js::temporal; + +void TemporalFields::trace(JSTracer* trc) { + TraceNullableRoot(trc, &monthCode, "TemporalFields::monthCode"); + TraceNullableRoot(trc, &offset, "TemporalFields::offset"); + TraceNullableRoot(trc, &era, "TemporalFields::era"); + TraceRoot(trc, &timeZone, "TemporalFields::timeZone"); +} + +static PropertyName* ToPropertyName(JSContext* cx, TemporalField field) { + switch (field) { + case TemporalField::Year: + return cx->names().year; + case TemporalField::Month: + return cx->names().month; + case TemporalField::MonthCode: + return cx->names().monthCode; + case TemporalField::Day: + return cx->names().day; + case TemporalField::Hour: + return cx->names().hour; + case TemporalField::Minute: + return cx->names().minute; + case TemporalField::Second: + return cx->names().second; + case TemporalField::Millisecond: + return cx->names().millisecond; + case TemporalField::Microsecond: + return cx->names().microsecond; + case TemporalField::Nanosecond: + return cx->names().nanosecond; + case TemporalField::Offset: + return cx->names().offset; + case TemporalField::Era: + return cx->names().era; + case TemporalField::EraYear: + return cx->names().eraYear; + case TemporalField::TimeZone: + return cx->names().timeZone; + } + MOZ_CRASH("invalid temporal field name"); +} + +static const char* ToCString(TemporalField field) { + switch (field) { + case TemporalField::Year: + return "year"; + case TemporalField::Month: + return "month"; + case TemporalField::MonthCode: + return "monthCode"; + case TemporalField::Day: + return "day"; + case TemporalField::Hour: + return "hour"; + case TemporalField::Minute: + return "minute"; + case TemporalField::Second: + return "second"; + case TemporalField::Millisecond: + return "millisecond"; + case TemporalField::Microsecond: + return "microsecond"; + case TemporalField::Nanosecond: + return "nanosecond"; + case TemporalField::Offset: + return "offset"; + case TemporalField::Era: + return "era"; + case TemporalField::EraYear: + return "eraYear"; + case TemporalField::TimeZone: + return "timeZone"; + } + MOZ_CRASH("invalid temporal field name"); +} + +static JS::UniqueChars QuoteString(JSContext* cx, const char* str) { + Sprinter sprinter(cx); + if (!sprinter.init()) { + return nullptr; + } + mozilla::Range range(reinterpret_cast<const Latin1Char*>(str), + std::strlen(str)); + QuoteString<QuoteTarget::String>(&sprinter, range); + return sprinter.release(); +} + +static JS::UniqueChars QuoteString(JSContext* cx, PropertyKey key) { + if (key.isString()) { + return QuoteString(cx, key.toString()); + } + + if (key.isInt()) { + Int32ToCStringBuf buf; + size_t length; + const char* str = Int32ToCString(&buf, key.toInt(), &length); + return DuplicateString(cx, str, length); + } + + MOZ_ASSERT(key.isSymbol()); + return QuoteString(cx, key.toSymbol()->description()); +} + +static mozilla::Maybe<TemporalField> ToTemporalField(JSContext* cx, + PropertyKey property) { + static constexpr TemporalField fieldNames[] = { + TemporalField::Year, TemporalField::Month, + TemporalField::MonthCode, TemporalField::Day, + TemporalField::Hour, TemporalField::Minute, + TemporalField::Second, TemporalField::Millisecond, + TemporalField::Microsecond, TemporalField::Nanosecond, + TemporalField::Offset, TemporalField::Era, + TemporalField::EraYear, TemporalField::TimeZone, + }; + + for (const auto& fieldName : fieldNames) { + auto* name = ToPropertyName(cx, fieldName); + if (property.isAtom(name)) { + return mozilla::Some(fieldName); + } + } + return mozilla::Nothing(); +} + +static JSString* ToPrimitiveAndRequireString(JSContext* cx, + Handle<Value> value) { + Rooted<Value> primitive(cx, value); + if (!ToPrimitive(cx, JSTYPE_STRING, &primitive)) { + return nullptr; + } + if (!primitive.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, primitive, + nullptr, "not a string"); + return nullptr; + } + return primitive.toString(); +} + +static Value TemporalFieldDefaultValue(TemporalField field) { + switch (field) { + case TemporalField::Year: + case TemporalField::Month: + case TemporalField::MonthCode: + case TemporalField::Day: + case TemporalField::Offset: + case TemporalField::Era: + case TemporalField::EraYear: + case TemporalField::TimeZone: + return UndefinedValue(); + case TemporalField::Hour: + case TemporalField::Minute: + case TemporalField::Second: + case TemporalField::Millisecond: + case TemporalField::Microsecond: + case TemporalField::Nanosecond: + return Int32Value(0); + } + MOZ_CRASH("invalid temporal field name"); +} + +static bool TemporalFieldConvertValue(JSContext* cx, TemporalField field, + MutableHandle<Value> value) { + auto* name = ToCString(field); + switch (field) { + case TemporalField::Year: + case TemporalField::Hour: + case TemporalField::Minute: + case TemporalField::Second: + case TemporalField::Millisecond: + case TemporalField::Microsecond: + case TemporalField::Nanosecond: + case TemporalField::EraYear: { + double num; + if (!ToIntegerWithTruncation(cx, value, name, &num)) { + return false; + } + value.setNumber(num); + return true; + } + + case TemporalField::Month: + case TemporalField::Day: { + double num; + if (!ToPositiveIntegerWithTruncation(cx, value, name, &num)) { + return false; + } + value.setNumber(num); + return true; + } + + case TemporalField::MonthCode: + case TemporalField::Offset: + case TemporalField::Era: { + JSString* str = ToPrimitiveAndRequireString(cx, value); + if (!str) { + return false; + } + value.setString(str); + return true; + } + + case TemporalField::TimeZone: + // NB: timeZone has no conversion function. + return true; + } + MOZ_CRASH("invalid temporal field name"); +} + +static int32_t ComparePropertyKey(PropertyKey x, PropertyKey y) { + MOZ_ASSERT(x.isAtom() || x.isInt()); + MOZ_ASSERT(y.isAtom() || y.isInt()); + + if (MOZ_LIKELY(x.isAtom() && y.isAtom())) { + return CompareStrings(x.toAtom(), y.toAtom()); + } + + if (x.isInt() && y.isInt()) { + return x.toInt() - y.toInt(); + } + + uint32_t index = uint32_t(x.isInt() ? x.toInt() : y.toInt()); + JSAtom* str = x.isAtom() ? x.toAtom() : y.toAtom(); + + char16_t buf[UINT32_CHAR_BUFFER_LENGTH]; + mozilla::RangedPtr<char16_t> end(std::end(buf), buf, std::end(buf)); + mozilla::RangedPtr<char16_t> start = BackfillIndexInCharBuffer(index, end); + + int32_t result = CompareChars(start.get(), end - start, str); + return x.isInt() ? result : -result; +} + +#ifdef DEBUG +static bool IsSorted(std::initializer_list<TemporalField> fieldNames) { + return std::is_sorted(fieldNames.begin(), fieldNames.end(), + [](auto x, auto y) { + auto* a = ToCString(x); + auto* b = ToCString(y); + return std::strcmp(a, b) < 0; + }); +} + +static bool IsSorted(const TemporalFieldNames& fieldNames) { + return std::is_sorted( + fieldNames.begin(), fieldNames.end(), + [](auto x, auto y) { return ComparePropertyKey(x, y) < 0; }); +} +#endif + +// clang-format off +// +// TODO: |fields| is often a built-in Temporal type, so we likely want to +// optimise for this case. +// +// Consider the case when PlainDate.prototype.toPlainMonthDay is called. The +// following steps are applied: +// +// 1. CalendarFields(calendar, ยซ"day", "monthCode"ยป) is called to retrieve the +// relevant calendar fields. For (most?) built-in calendars this will just +// return the input list ยซ"day", "monthCode"ยป. +// 2. PrepareTemporalFields(plainDate, ยซ"day", "monthCode"ยป) is called. This +// will access the properties `plainDate.day` and `plainDate.monthCode`. +// a. `plainDate.day` will call CalendarDay(calendar, plainDate). +// b. For built-in calendars, this will simply access `plainDate.[[IsoDay]]`. +// c. `plainDate.monthCode` will call CalendarMonthCode(calendar, plainDate). +// d. For built-in calendars, ISOMonthCode(plainDate.[[IsoMonth]]) is called. +// 3. CalendarMonthDayFromFields(calendar, {day, monthCode}) is called. +// 4. For built-in calendars, this calls PrepareTemporalFields({day, monthCode}, +// ยซ"day", "month", "monthCode", "year"ยป, ยซ"day"ยป). +// 5. The previous PrepareTemporalFields call is a no-op and returns {day, monthCode}. +// 6. Then ISOMonthDayFromFields({day, monthCode}, "constrain") gets called. +// 7. ResolveISOMonth(monthCode) is called to parse the just created `monthCode`. +// 8. RegulateISODate(referenceISOYear, month, day, "constrain") is called. +// 9. Finally CreateTemporalMonthDay is called to create the PlainMonthDay instance. +// +// All these steps could be simplified to just: +// 1. CreateTemporalMonthDay(referenceISOYear, plainDate.[[IsoMonth]], plainDate.[[IsoDay]]). +// +// When the following conditions are true: +// 1. The `plainDate` is a Temporal.PlainDate instance and has no overridden methods. +// 2. The `calendar` is a Temporal.Calendar instance and has no overridden methods. +// 3. Temporal.PlainDate.prototype and Temporal.Calendar.prototype are in their initial state. +// 4. Array iteration is still in its initial state. (Required by CalendarFields) +// +// PlainDate_toPlainMonthDay has an example implementation for this optimisation. +// +// clang-format on + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields ) + */ +bool js::temporal::PrepareTemporalFields( + JSContext* cx, Handle<JSObject*> fields, + std::initializer_list<TemporalField> fieldNames, + std::initializer_list<TemporalField> requiredFields, + MutableHandle<TemporalFields> result) { + // Steps 1-3. (Not applicable in our implementation.) + + // Step 4. (|fieldNames| is sorted in our implementation.) + MOZ_ASSERT(IsSorted(fieldNames)); + + // Step 5. (The list doesn't contain duplicates in our implementation.) + MOZ_ASSERT(std::adjacent_find(fieldNames.begin(), fieldNames.end()) == + fieldNames.end()); + + // |requiredFields| is sorted and doesn't contain any duplicate elements. + MOZ_ASSERT(IsSorted(requiredFields)); + MOZ_ASSERT(std::adjacent_find(requiredFields.begin(), requiredFields.end()) == + requiredFields.end()); + + // Step 6. + Rooted<Value> value(cx); + for (auto fieldName : fieldNames) { + auto* property = ToPropertyName(cx, fieldName); + auto* cstr = ToCString(fieldName); + + // Step 6.a. (Not applicable in our implementation.) + + // Step 6.b.i. + if (!GetProperty(cx, fields, fields, property, &value)) { + return false; + } + + // Steps 6.b.ii-iii. + if (!value.isUndefined()) { + // Step 6.b.ii.1. (Not applicable in our implementation.) + + // Steps 6.b.ii.2-3. + switch (fieldName) { + case TemporalField::Year: + if (!ToIntegerWithTruncation(cx, value, cstr, &result.year())) { + return false; + } + break; + case TemporalField::Month: + if (!ToPositiveIntegerWithTruncation(cx, value, cstr, + &result.month())) { + return false; + } + break; + case TemporalField::MonthCode: { + JSString* str = ToPrimitiveAndRequireString(cx, value); + if (!str) { + return false; + } + result.monthCode().set(str); + break; + } + case TemporalField::Day: + if (!ToPositiveIntegerWithTruncation(cx, value, cstr, + &result.day())) { + return false; + } + break; + case TemporalField::Hour: + if (!ToIntegerWithTruncation(cx, value, cstr, &result.hour())) { + return false; + } + break; + case TemporalField::Minute: + if (!ToIntegerWithTruncation(cx, value, cstr, &result.minute())) { + return false; + } + break; + case TemporalField::Second: + if (!ToIntegerWithTruncation(cx, value, cstr, &result.second())) { + return false; + } + break; + case TemporalField::Millisecond: + if (!ToIntegerWithTruncation(cx, value, cstr, + &result.millisecond())) { + return false; + } + break; + case TemporalField::Microsecond: + if (!ToIntegerWithTruncation(cx, value, cstr, + &result.microsecond())) { + return false; + } + break; + case TemporalField::Nanosecond: + if (!ToIntegerWithTruncation(cx, value, cstr, &result.nanosecond())) { + return false; + } + break; + case TemporalField::Offset: { + JSString* str = ToPrimitiveAndRequireString(cx, value); + if (!str) { + return false; + } + result.offset().set(str); + break; + } + case TemporalField::Era: { + JSString* str = ToPrimitiveAndRequireString(cx, value); + if (!str) { + return false; + } + result.era().set(str); + break; + } + case TemporalField::EraYear: + if (!ToIntegerWithTruncation(cx, value, cstr, &result.eraYear())) { + return false; + } + break; + case TemporalField::TimeZone: + // NB: TemporalField::TimeZone has no conversion function. + result.timeZone().set(value); + break; + } + } else { + // Step 6.b.iii.1. + if (std::find(requiredFields.begin(), requiredFields.end(), fieldName) != + requiredFields.end()) { + if (auto chars = QuoteString(cx, cstr)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_PROPERTY, + chars.get()); + } + return false; + } + + // `const` can be changed to `constexpr` when we switch to C++20. + const TemporalFields FallbackValues{}; + + // Steps 6.b.iii.2-3. + switch (fieldName) { + case TemporalField::Year: + result.year() = FallbackValues.year; + break; + case TemporalField::Month: + result.month() = FallbackValues.month; + break; + case TemporalField::MonthCode: + result.monthCode().set(FallbackValues.monthCode); + break; + case TemporalField::Day: + result.day() = FallbackValues.day; + break; + case TemporalField::Hour: + result.hour() = FallbackValues.hour; + break; + case TemporalField::Minute: + result.minute() = FallbackValues.minute; + break; + case TemporalField::Second: + result.second() = FallbackValues.second; + break; + case TemporalField::Millisecond: + result.millisecond() = FallbackValues.millisecond; + break; + case TemporalField::Microsecond: + result.microsecond() = FallbackValues.microsecond; + break; + case TemporalField::Nanosecond: + result.nanosecond() = FallbackValues.nanosecond; + break; + case TemporalField::Offset: + result.offset().set(FallbackValues.offset); + break; + case TemporalField::Era: + result.era().set(FallbackValues.era); + break; + case TemporalField::EraYear: + result.eraYear() = FallbackValues.eraYear; + break; + case TemporalField::TimeZone: + result.timeZone().set(FallbackValues.timeZone); + break; + } + } + + // Steps 6.c-d. (Not applicable in our implementation.) + } + + // Step 7. (Not applicable in our implementation.) + + // Step 8. + return true; +} + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields [ , + * duplicateBehaviour ] ) + */ +PlainObject* js::temporal::PrepareTemporalFields( + JSContext* cx, Handle<JSObject*> fields, + Handle<TemporalFieldNames> fieldNames) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainObject*> result(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!result) { + return nullptr; + } + + // Step 3. (Not applicable in our implementation.) + + // Step 4. (The list is already sorted in our implementation.) + MOZ_ASSERT(IsSorted(fieldNames)); + + // Step 5. (The list doesn't contain duplicates in our implementation.) + MOZ_ASSERT(std::adjacent_find(fieldNames.begin(), fieldNames.end()) == + fieldNames.end()); + + // Step 6. + Rooted<Value> value(cx); + for (size_t i = 0; i < fieldNames.length(); i++) { + Handle<PropertyKey> property = fieldNames[i]; + + // Step 6.a. + MOZ_ASSERT(property != NameToId(cx->names().constructor)); + MOZ_ASSERT(property != NameToId(cx->names().proto_)); + + // Step 6.b.i. + if (!GetProperty(cx, fields, fields, property, &value)) { + return nullptr; + } + + // Steps 6.b.ii-iii. + if (auto fieldName = ToTemporalField(cx, property)) { + if (!value.isUndefined()) { + // Step 6.b.ii.1. (Not applicable in our implementation.) + + // Step 6.b.ii.2. + if (!TemporalFieldConvertValue(cx, *fieldName, &value)) { + return nullptr; + } + } else { + // Step 6.b.iii.1. (Not applicable in our implementation.) + + // Step 6.b.iii.2. + value = TemporalFieldDefaultValue(*fieldName); + } + } + + // Steps 6.b.ii.3 and 6.b.iii.3. + if (!DefineDataProperty(cx, result, property, value)) { + return nullptr; + } + + // Steps 6.c-d. (Not applicable in our implementation.) + } + + // Step 7. (Not applicable in our implementation.) + + // Step 8. + return result; +} + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields [ , + * duplicateBehaviour ] ) + */ +PlainObject* js::temporal::PrepareTemporalFields( + JSContext* cx, Handle<JSObject*> fields, + Handle<TemporalFieldNames> fieldNames, + std::initializer_list<TemporalField> requiredFields) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainObject*> result(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!result) { + return nullptr; + } + + // Step 3. (Not applicable in our implementation.) + + // Step 4. (The list is already sorted in our implementation.) + MOZ_ASSERT(IsSorted(fieldNames)); + + // Step 5. (The list doesn't contain duplicates in our implementation.) + MOZ_ASSERT(std::adjacent_find(fieldNames.begin(), fieldNames.end()) == + fieldNames.end()); + + // |requiredFields| is sorted and doesn't include any duplicate elements. + MOZ_ASSERT(IsSorted(requiredFields)); + MOZ_ASSERT(std::adjacent_find(requiredFields.begin(), requiredFields.end()) == + requiredFields.end()); + + // Step 6. + Rooted<Value> value(cx); + for (size_t i = 0; i < fieldNames.length(); i++) { + Handle<PropertyKey> property = fieldNames[i]; + + // Step 6.a. + MOZ_ASSERT(property != NameToId(cx->names().constructor)); + MOZ_ASSERT(property != NameToId(cx->names().proto_)); + + // Step 6.b.i. + if (!GetProperty(cx, fields, fields, property, &value)) { + return nullptr; + } + + // Steps 6.b.ii-iii. + if (auto fieldName = ToTemporalField(cx, property)) { + if (!value.isUndefined()) { + // Step 6.b.ii.1. (Not applicable in our implementation.) + + // Step 6.b.ii.2. + if (!TemporalFieldConvertValue(cx, *fieldName, &value)) { + return nullptr; + } + } else { + // Step 6.b.iii.1. + if (std::find(requiredFields.begin(), requiredFields.end(), + *fieldName) != requiredFields.end()) { + if (auto chars = QuoteString(cx, property.toString())) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_PROPERTY, + chars.get()); + } + return nullptr; + } + + // Step 6.b.iii.2. + value = TemporalFieldDefaultValue(*fieldName); + } + } + + // Steps 6.b.ii.3 and 6.b.iii.3. + if (!DefineDataProperty(cx, result, property, value)) { + return nullptr; + } + + // Steps 6.c-d. (Not applicable in our implementation.) + } + + // Step 7. (Not applicable in our implementation.) + + // Step 8. + return result; +} + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields [ , + * duplicateBehaviour ] ) + */ +PlainObject* js::temporal::PreparePartialTemporalFields( + JSContext* cx, Handle<JSObject*> fields, + Handle<TemporalFieldNames> fieldNames) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<PlainObject*> result(cx, NewPlainObjectWithProto(cx, nullptr)); + if (!result) { + return nullptr; + } + + // Step 3. + bool any = false; + + // Step 4. (The list is already sorted in our implementation.) + MOZ_ASSERT(IsSorted(fieldNames)); + + // Step 5. (The list doesn't contain duplicates in our implementation.) + MOZ_ASSERT(std::adjacent_find(fieldNames.begin(), fieldNames.end()) == + fieldNames.end()); + + // Step 6. + Rooted<Value> value(cx); + for (size_t i = 0; i < fieldNames.length(); i++) { + Handle<PropertyKey> property = fieldNames[i]; + + // Step 6.a. + MOZ_ASSERT(property != NameToId(cx->names().constructor)); + MOZ_ASSERT(property != NameToId(cx->names().proto_)); + + // Step 6.b.i. + if (!GetProperty(cx, fields, fields, property, &value)) { + return nullptr; + } + + // Steps 6.b.ii-iii. + if (!value.isUndefined()) { + // Step 6.b.ii.1. + any = true; + + // Step 6.b.ii.2. + if (auto fieldName = ToTemporalField(cx, property)) { + if (!TemporalFieldConvertValue(cx, *fieldName, &value)) { + return nullptr; + } + } + + // Steps 6.b.ii.3. + if (!DefineDataProperty(cx, result, property, value)) { + return nullptr; + } + } else { + // Step 6.b.iii. (Not applicable in our implementation.) + } + + // Steps 6.c-d. (Not applicable in our implementation.) + } + + // Step 7. + if (!any) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_TEMPORAL_FIELDS); + return nullptr; + } + + // Step 8. + return result; +} + +/** + * Performs list-concatenation, removes any duplicates, and sorts the result. + */ +bool js::temporal::ConcatTemporalFieldNames( + const TemporalFieldNames& receiverFieldNames, + const TemporalFieldNames& inputFieldNames, + TemporalFieldNames& concatenatedFieldNames) { + MOZ_ASSERT(IsSorted(receiverFieldNames)); + MOZ_ASSERT(IsSorted(inputFieldNames)); + MOZ_ASSERT(concatenatedFieldNames.empty()); + + auto appendUnique = [&](auto key) { + if (concatenatedFieldNames.empty() || + concatenatedFieldNames.back() != key) { + return concatenatedFieldNames.append(key); + } + return true; + }; + + size_t i = 0; + size_t j = 0; + + // Append the names from |receiverFieldNames| and |inputFieldNames|. + while (i < receiverFieldNames.length() && j < inputFieldNames.length()) { + auto x = receiverFieldNames[i]; + auto y = inputFieldNames[j]; + + PropertyKey z; + if (ComparePropertyKey(x, y) <= 0) { + z = x; + i++; + } else { + z = y; + j++; + } + if (!appendUnique(z)) { + return false; + } + } + + // Append the remaining names from |receiverFieldNames|. + while (i < receiverFieldNames.length()) { + if (!appendUnique(receiverFieldNames[i++])) { + return false; + } + } + + // Append the remaining names from |inputFieldNames|. + while (j < inputFieldNames.length()) { + if (!appendUnique(inputFieldNames[j++])) { + return false; + } + } + + return true; +} + +bool js::temporal::AppendSorted( + JSContext* cx, TemporalFieldNames& fieldNames, + std::initializer_list<TemporalField> additionalNames) { + // |fieldNames| is sorted and doesn't include any duplicates + MOZ_ASSERT(IsSorted(fieldNames)); + MOZ_ASSERT(std::adjacent_find(fieldNames.begin(), fieldNames.end()) == + fieldNames.end()); + + // |additionalNames| is non-empty, sorted, and doesn't include any duplicates. + MOZ_ASSERT(additionalNames.size() > 0); + MOZ_ASSERT(IsSorted(additionalNames)); + MOZ_ASSERT( + std::adjacent_find(additionalNames.begin(), additionalNames.end()) == + additionalNames.end()); + + // Allocate space for entries from |additionalNames|. + if (!fieldNames.growBy(additionalNames.size())) { + return false; + } + + auto* left = std::prev(fieldNames.end(), additionalNames.size()); + auto* right = additionalNames.end(); + auto* out = fieldNames.end(); + + // Write backwards into the newly allocated space. + while (left != fieldNames.begin() && right != additionalNames.begin()) { + MOZ_ASSERT(out != fieldNames.begin()); + auto x = *std::prev(left); + auto y = NameToId(ToPropertyName(cx, *std::prev(right))); + + int32_t r = ComparePropertyKey(x, y); + + // Reject duplicates per PrepareTemporalFields, step 6.c. + if (r == 0) { + if (auto chars = QuoteString(cx, x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DUPLICATE_PROPERTY, + chars.get()); + } + return false; + } + + // Insert the lexicographically greater key. + PropertyKey z; + if (r > 0) { + z = x; + left--; + } else { + z = y; + right--; + } + *--out = z; + } + + // Avoid unnecessary copying if possible. + if (left == out) { + MOZ_ASSERT(right == additionalNames.begin()); + return true; + } + + // Prepend the remaining names from |fieldNames|. + while (left != fieldNames.begin()) { + MOZ_ASSERT(out != fieldNames.begin()); + *--out = *--left; + } + + // Prepend the remaining names from |additionalNames|. + while (right != additionalNames.begin()) { + MOZ_ASSERT(out != fieldNames.begin()); + *--out = NameToId(ToPropertyName(cx, *--right)); + } + + // All field names were written into the result list. + MOZ_ASSERT(out == fieldNames.begin()); + + return true; +} + +bool js::temporal::SortTemporalFieldNames(JSContext* cx, + TemporalFieldNames& fieldNames) { + // Create scratch space for MergeSort(). + TemporalFieldNames scratch(cx); + if (!scratch.resize(fieldNames.length())) { + return false; + } + + // Sort all field names in alphabetical order. + auto comparator = [](const auto& x, const auto& y, bool* lessOrEqual) { + *lessOrEqual = ComparePropertyKey(x, y) <= 0; + return true; + }; + MOZ_ALWAYS_TRUE(MergeSort(fieldNames.begin(), fieldNames.length(), + scratch.begin(), comparator)); + + for (size_t i = 0; i < fieldNames.length(); i++) { + auto property = fieldNames[i]; + + // Reject "constructor" and "__proto__" per PrepareTemporalFields, step 6.a. + if (property == NameToId(cx->names().constructor) || + property == NameToId(cx->names().proto_)) { + if (auto chars = QuoteString(cx, property)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_PROPERTY, chars.get()); + } + return false; + } + + // Reject duplicates per PrepareTemporalFields, step 6.c. + if (i > 0 && property == fieldNames[i - 1]) { + if (auto chars = QuoteString(cx, property)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_DUPLICATE_PROPERTY, + chars.get()); + } + return false; + } + } + + return true; +} diff --git a/js/src/builtin/temporal/TemporalFields.h b/js/src/builtin/temporal/TemporalFields.h new file mode 100644 index 0000000000..1af3631169 --- /dev/null +++ b/js/src/builtin/temporal/TemporalFields.h @@ -0,0 +1,192 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_TemporalFields_h +#define builtin_temporal_TemporalFields_h + +#include "mozilla/FloatingPoint.h" + +#include <initializer_list> + +#include "jstypes.h" + +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" + +class JS_PUBLIC_API JSTracer; + +namespace js { +class PlainObject; +} + +namespace js::temporal { +enum class TemporalField { + Year, + Month, + MonthCode, + Day, + Hour, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond, + Offset, + Era, + EraYear, + TimeZone, +}; + +// Default values are specified in Table 15 [1]. `undefined` is replaced with +// an appropriate value based on the type, for example `double` fields use +// NaN whereas pointer fields use nullptr. +// +// [1] <https://tc39.es/proposal-temporal/#table-temporal-field-requirements> +struct TemporalFields final { + double year = mozilla::UnspecifiedNaN<double>(); + double month = mozilla::UnspecifiedNaN<double>(); + JSString* monthCode = nullptr; + double day = mozilla::UnspecifiedNaN<double>(); + double hour = 0; + double minute = 0; + double second = 0; + double millisecond = 0; + double microsecond = 0; + double nanosecond = 0; + JSString* offset = nullptr; + JSString* era = nullptr; + double eraYear = mozilla::UnspecifiedNaN<double>(); + JS::Value timeZone = JS::UndefinedValue(); + + TemporalFields() = default; + + void trace(JSTracer* trc); +}; +} // namespace js::temporal + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<temporal::TemporalFields, Wrapper> { + const temporal::TemporalFields& fields() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + double year() const { return fields().year; } + double month() const { return fields().month; } + double day() const { return fields().day; } + double hour() const { return fields().hour; } + double minute() const { return fields().minute; } + double second() const { return fields().second; } + double millisecond() const { return fields().millisecond; } + double microsecond() const { return fields().microsecond; } + double nanosecond() const { return fields().nanosecond; } + double eraYear() const { return fields().eraYear; } + + JS::Handle<JSString*> monthCode() const { + return JS::Handle<JSString*>::fromMarkedLocation(&fields().monthCode); + } + JS::Handle<JSString*> offset() const { + return JS::Handle<JSString*>::fromMarkedLocation(&fields().offset); + } + JS::Handle<JSString*> era() const { + return JS::Handle<JSString*>::fromMarkedLocation(&fields().era); + } + JS::Handle<JS::Value> timeZone() const { + return JS::Handle<JS::Value>::fromMarkedLocation(&fields().timeZone); + } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<temporal::TemporalFields, Wrapper> + : public WrappedPtrOperations<temporal::TemporalFields, Wrapper> { + temporal::TemporalFields& fields() { + return static_cast<Wrapper*>(this)->get(); + } + + public: + double& year() { return fields().year; } + double& month() { return fields().month; } + double& day() { return fields().day; } + double& hour() { return fields().hour; } + double& minute() { return fields().minute; } + double& second() { return fields().second; } + double& millisecond() { return fields().millisecond; } + double& microsecond() { return fields().microsecond; } + double& nanosecond() { return fields().nanosecond; } + double& eraYear() { return fields().eraYear; } + + JS::MutableHandle<JSString*> monthCode() { + return JS::MutableHandle<JSString*>::fromMarkedLocation( + &fields().monthCode); + } + JS::MutableHandle<JSString*> offset() { + return JS::MutableHandle<JSString*>::fromMarkedLocation(&fields().offset); + } + JS::MutableHandle<JSString*> era() { + return JS::MutableHandle<JSString*>::fromMarkedLocation(&fields().era); + } + JS::MutableHandle<JS::Value> timeZone() { + return JS::MutableHandle<JS::Value>::fromMarkedLocation(&fields().timeZone); + } +}; + +} // namespace js + +namespace js::temporal { + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields [ , + * duplicateBehaviour ] ) + */ +bool PrepareTemporalFields(JSContext* cx, JS::Handle<JSObject*> fields, + std::initializer_list<TemporalField> fieldNames, + std::initializer_list<TemporalField> requiredFields, + JS::MutableHandle<TemporalFields> result); + +using TemporalFieldNames = JS::StackGCVector<JS::PropertyKey>; + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields [ , + * duplicateBehaviour ] ) + */ +PlainObject* PrepareTemporalFields(JSContext* cx, JS::Handle<JSObject*> fields, + JS::Handle<TemporalFieldNames> fieldNames); + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields [ , + * duplicateBehaviour ] ) + */ +PlainObject* PrepareTemporalFields( + JSContext* cx, JS::Handle<JSObject*> fields, + JS::Handle<TemporalFieldNames> fieldNames, + std::initializer_list<TemporalField> requiredFields); + +/** + * PrepareTemporalFields ( fields, fieldNames, requiredFields [ , + * duplicateBehaviour ] ) + */ +PlainObject* PreparePartialTemporalFields( + JSContext* cx, JS::Handle<JSObject*> fields, + JS::Handle<TemporalFieldNames> fieldNames); + +[[nodiscard]] bool ConcatTemporalFieldNames( + const TemporalFieldNames& receiverFieldNames, + const TemporalFieldNames& inputFieldNames, + TemporalFieldNames& concatenatedFieldNames); + +[[nodiscard]] bool AppendSorted( + JSContext* cx, TemporalFieldNames& fieldNames, + std::initializer_list<TemporalField> additionalNames); + +[[nodiscard]] bool SortTemporalFieldNames(JSContext* cx, + TemporalFieldNames& fieldNames); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_TemporalFields_h */ diff --git a/js/src/builtin/temporal/TemporalNow.cpp b/js/src/builtin/temporal/TemporalNow.cpp new file mode 100644 index 0000000000..adc84361af --- /dev/null +++ b/js/src/builtin/temporal/TemporalNow.cpp @@ -0,0 +1,539 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/TemporalNow.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Result.h" + +#include <cstdlib> +#include <stdint.h> +#include <string_view> +#include <utility> + +#include "jsdate.h" +#include "jspubtd.h" +#include "jstypes.h" +#include "NamespaceImports.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Instant.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/TemporalParser.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/TimeZone.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "gc/Barrier.h" +#include "gc/GCEnum.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/Class.h" +#include "js/Date.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "vm/DateTime.h" +#include "vm/GlobalObject.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/Realm.h" +#include "vm/StringType.h" + +#include "vm/JSObject-inl.h" + +using namespace js; +using namespace js::temporal; + +static bool SystemTimeZoneOffset(JSContext* cx, int32_t* offset) { + auto rawOffset = + DateTimeInfo::getRawOffsetMs(DateTimeInfo::forceUTC(cx->realm())); + if (rawOffset.isErr()) { + intl::ReportInternalError(cx); + return false; + } + + *offset = rawOffset.unwrap(); + return true; +} + +/** + * 6.4.3 DefaultTimeZone () + * + * Returns the IANA time zone name for the host environment's current time zone. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + */ +static JSString* SystemTimeZoneIdentifier(JSContext* cx) { + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> formatBuffer(cx); + auto result = DateTimeInfo::timeZoneId(DateTimeInfo::forceUTC(cx->realm()), + formatBuffer); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + Rooted<JSString*> timeZone(cx, formatBuffer.toString(cx)); + if (!timeZone) { + return nullptr; + } + + Rooted<JSAtom*> validTimeZone(cx); + if (!IsValidTimeZoneName(cx, timeZone, &validTimeZone)) { + return nullptr; + } + if (validTimeZone) { + return CanonicalizeTimeZoneName(cx, validTimeZone); + } + + // See DateTimeFormat.js for the JS implementation. + // TODO: Move the JS implementation into C++. + + // Before defaulting to "UTC", try to represent the system time zone using + // the Etc/GMT + offset format. This format only accepts full hour offsets. + int32_t offset; + if (!SystemTimeZoneOffset(cx, &offset)) { + return nullptr; + } + + constexpr int32_t msPerHour = 60 * 60 * 1000; + int32_t offsetHours = std::abs(offset / msPerHour); + int32_t offsetHoursFraction = offset % msPerHour; + if (offsetHoursFraction == 0 && offsetHours < 24) { + // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset + // means a location west of GMT. + constexpr std::string_view etcGMT = "Etc/GMT"; + + char offsetString[etcGMT.length() + 3]; + + size_t n = etcGMT.copy(offsetString, etcGMT.length()); + offsetString[n++] = offset < 0 ? '+' : '-'; + if (offsetHours >= 10) { + offsetString[n++] = '0' + (offsetHours / 10); + } + offsetString[n++] = '0' + (offsetHours % 10); + + MOZ_ASSERT(n == etcGMT.length() + 2 || n == etcGMT.length() + 3); + + timeZone = NewStringCopyN<CanGC>(cx, offsetString, n); + if (!timeZone) { + return nullptr; + } + + // Check if the fallback is valid. + if (!IsValidTimeZoneName(cx, timeZone, &validTimeZone)) { + return nullptr; + } + if (validTimeZone) { + return CanonicalizeTimeZoneName(cx, validTimeZone); + } + } + + // Fallback to "UTC" if everything else fails. + return cx->names().UTC; +} + +static BuiltinTimeZoneObject* SystemTimeZoneObject(JSContext* cx) { + Rooted<JSString*> timeZoneIdentifier(cx, SystemTimeZoneIdentifier(cx)); + if (!timeZoneIdentifier) { + return nullptr; + } + + return CreateTemporalTimeZone(cx, timeZoneIdentifier); +} + +/** + * SystemUTCEpochNanoseconds ( ) + */ +static bool SystemUTCEpochNanoseconds(JSContext* cx, Instant* result) { + // Step 1. + JS::ClippedTime nowMillis = DateNow(cx); + MOZ_ASSERT(nowMillis.isValid()); + + // Step 2. + MOZ_ASSERT(nowMillis.toDouble() >= js::StartOfTime); + MOZ_ASSERT(nowMillis.toDouble() <= js::EndOfTime); + + // Step 3. + *result = Instant::fromMilliseconds(int64_t(nowMillis.toDouble())); + return true; +} + +/** + * SystemInstant ( ) + */ +static bool SystemInstant(JSContext* cx, Instant* result) { + // Steps 1-2. + return SystemUTCEpochNanoseconds(cx, result); +} + +/** + * SystemInstant ( ) + */ +static InstantObject* SystemInstant(JSContext* cx) { + // Step 1. + Instant instant; + if (!SystemUTCEpochNanoseconds(cx, &instant)) { + return nullptr; + } + + // Step 2. + return CreateTemporalInstant(cx, instant); +} + +/** + * SystemDateTime ( temporalTimeZoneLike, calendarLike ) + * SystemZonedDateTime ( temporalTimeZoneLike, calendarLike ) + */ +static bool ToTemporalTimeZoneOrSystemTimeZone( + JSContext* cx, Handle<Value> temporalTimeZoneLike, + MutableHandle<TimeZoneValue> timeZone) { + // Step 1. + if (temporalTimeZoneLike.isUndefined()) { + auto* timeZoneObj = SystemTimeZoneObject(cx); + if (!timeZoneObj) { + return false; + } + timeZone.set(TimeZoneValue(timeZoneObj)); + return true; + } + + // Step 2. + return ToTemporalTimeZone(cx, temporalTimeZoneLike, timeZone); +} + +/** + * SystemDateTime ( temporalTimeZoneLike, calendarLike ) + */ +static bool SystemDateTime(JSContext* cx, Handle<TimeZoneValue> timeZone, + PlainDateTime* dateTime) { + // SystemDateTime, step 4. + Instant instant; + if (!SystemInstant(cx, &instant)) { + return false; + } + + // SystemDateTime, steps 5-6. + return GetPlainDateTimeFor(cx, timeZone, instant, dateTime); +} + +/** + * Temporal.Now.timeZoneId ( ) + */ +static bool Temporal_Now_timeZoneId(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + auto* result = SystemTimeZoneIdentifier(cx); + if (!result) { + return false; + } + + args.rval().setString(result); + return true; +} + +/** + * Temporal.Now.instant ( ) + */ +static bool Temporal_Now_instant(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + auto* result = SystemInstant(cx); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Now.plainDateTime ( calendar [ , temporalTimeZoneLike ] ) + */ +static bool Temporal_Now_plainDateTime(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Inlined call to SystemDateTime) + + // SystemDateTime, steps 1-2. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZoneOrSystemTimeZone(cx, args.get(1), &timeZone)) { + return false; + } + + // SystemDateTime, step 3. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // SystemDateTime, steps 4-5. + PlainDateTime dateTime; + if (!SystemDateTime(cx, timeZone, &dateTime)) { + return false; + } + + auto* result = CreateTemporalDateTime(cx, dateTime, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Now.plainDateTimeISO ( [ temporalTimeZoneLike ] ) + */ +static bool Temporal_Now_plainDateTimeISO(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Inlined call to SystemDateTime) + + // SystemDateTime, steps 1-2. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZoneOrSystemTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // SystemDateTime, step 3. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + + // SystemDateTime, steps 4-5. + PlainDateTime dateTime; + if (!SystemDateTime(cx, timeZone, &dateTime)) { + return false; + } + + auto* result = CreateTemporalDateTime(cx, dateTime, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Now.zonedDateTime ( calendar [ , temporalTimeZoneLike ] ) + */ +static bool Temporal_Now_zonedDateTime(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Inlined call to SystemZonedDateTime) + + // SystemZonedDateTime, steps 1-2. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZoneOrSystemTimeZone(cx, args.get(1), &timeZone)) { + return false; + } + + // SystemZonedDateTime, step 3. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // SystemZonedDateTime, step 4. + Instant instant; + if (!SystemUTCEpochNanoseconds(cx, &instant)) { + return false; + } + + // SystemZonedDateTime, step 5. + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Now.zonedDateTimeISO ( [ temporalTimeZoneLike ] ) + */ +static bool Temporal_Now_zonedDateTimeISO(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Inlined call to SystemZonedDateTime) + + // SystemZonedDateTime, steps 1-2. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZoneOrSystemTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // SystemZonedDateTime, step 3. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + + // SystemZonedDateTime, step 4. + Instant instant; + if (!SystemUTCEpochNanoseconds(cx, &instant)) { + return false; + } + + // SystemZonedDateTime, step 5. + auto* result = CreateTemporalZonedDateTime(cx, instant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Now.plainDate ( calendar [ , temporalTimeZoneLike ] ) + */ +static bool Temporal_Now_plainDate(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Inlined call to SystemDateTime) + + // SystemDateTime, steps 1-2. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZoneOrSystemTimeZone(cx, args.get(1), &timeZone)) { + return false; + } + + // SystemDateTime, step 3. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // SystemDateTime, steps 4-5. + PlainDateTime dateTime; + if (!SystemDateTime(cx, timeZone, &dateTime)) { + return false; + } + + // Step 2. + auto* result = CreateTemporalDate(cx, dateTime.date, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Now.plainDateISO ( [ temporalTimeZoneLike ] ) + */ +static bool Temporal_Now_plainDateISO(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Inlined call to SystemDateTime) + + // SystemDateTime, steps 1-2. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZoneOrSystemTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // SystemDateTime, step 3. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + + // SystemDateTime, steps 4-5. + PlainDateTime dateTime; + if (!SystemDateTime(cx, timeZone, &dateTime)) { + return false; + } + + // Step 2. + auto* result = CreateTemporalDate(cx, dateTime.date, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.Now.plainTimeISO ( [ temporalTimeZoneLike ] ) + */ +static bool Temporal_Now_plainTimeISO(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. (Inlined call to SystemDateTime) + + // SystemDateTime, steps 1-2. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZoneOrSystemTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // SystemDateTime, step 3. (Not applicable) + + // SystemDateTime, steps 4-5. + PlainDateTime dateTime; + if (!SystemDateTime(cx, timeZone, &dateTime)) { + return false; + } + + // Step 2. + auto* result = CreateTemporalTime(cx, dateTime.time); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +const JSClass TemporalNowObject::class_ = { + "Temporal.Now", + JSCLASS_HAS_CACHED_PROTO(JSProto_TemporalNow), + JS_NULL_CLASS_OPS, + &TemporalNowObject::classSpec_, +}; + +static const JSFunctionSpec TemporalNow_methods[] = { + JS_FN("timeZoneId", Temporal_Now_timeZoneId, 0, 0), + JS_FN("instant", Temporal_Now_instant, 0, 0), + JS_FN("plainDateTime", Temporal_Now_plainDateTime, 1, 0), + JS_FN("plainDateTimeISO", Temporal_Now_plainDateTimeISO, 0, 0), + JS_FN("zonedDateTime", Temporal_Now_zonedDateTime, 1, 0), + JS_FN("zonedDateTimeISO", Temporal_Now_zonedDateTimeISO, 0, 0), + JS_FN("plainDate", Temporal_Now_plainDate, 1, 0), + JS_FN("plainDateISO", Temporal_Now_plainDateISO, 0, 0), + JS_FN("plainTimeISO", Temporal_Now_plainTimeISO, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec TemporalNow_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Temporal.Now", JSPROP_READONLY), + JS_PS_END, +}; + +static JSObject* CreateTemporalNowObject(JSContext* cx, JSProtoKey key) { + Rooted<JSObject*> proto(cx, &cx->global()->getObjectPrototype()); + return NewTenuredObjectWithGivenProto(cx, &TemporalNowObject::class_, proto); +} + +const ClassSpec TemporalNowObject::classSpec_ = { + CreateTemporalNowObject, + nullptr, + TemporalNow_methods, + TemporalNow_properties, + nullptr, + nullptr, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/TemporalNow.h b/js/src/builtin/temporal/TemporalNow.h new file mode 100644 index 0000000000..52ff68b8ce --- /dev/null +++ b/js/src/builtin/temporal/TemporalNow.h @@ -0,0 +1,30 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_TemporalNow_h +#define builtin_temporal_TemporalNow_h + +#include "vm/NativeObject.h" + +struct JSClass; + +namespace js { +struct ClassSpec; +} + +namespace js::temporal { + +class TemporalNowObject : public NativeObject { + public: + static const JSClass class_; + + private: + static const ClassSpec classSpec_; +}; + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_TemporalNow_h */ diff --git a/js/src/builtin/temporal/TemporalParser.cpp b/js/src/builtin/temporal/TemporalParser.cpp new file mode 100644 index 0000000000..c117e63b31 --- /dev/null +++ b/js/src/builtin/temporal/TemporalParser.cpp @@ -0,0 +1,3464 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/TemporalParser.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/Maybe.h" +#include "mozilla/Range.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/TextUtils.h" + +#include <algorithm> +#include <cstdlib> +#include <initializer_list> +#include <iterator> +#include <limits> +#include <stdint.h> +#include <string_view> +#include <type_traits> +#include <utility> + +#include "jsnum.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/TemporalUnit.h" +#include "gc/Barrier.h" +#include "gc/Tracer.h" +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/GCAPI.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "util/Text.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/StringType.h" + +using namespace js; +using namespace js::temporal; + +// TODO: Better error message for empty strings? +// TODO: Add string input to error message? +// TODO: Better error messages, for example display current character? +// https://bugzilla.mozilla.org/show_bug.cgi?id=1839676 + +struct StringName final { + // Start position and length of this name. + size_t start = 0; + size_t length = 0; + + bool present() const { return length > 0; } +}; + +static JSLinearString* ToString(JSContext* cx, JSString* string, + const StringName& name) { + MOZ_ASSERT(name.present()); + return NewDependentString(cx, string, name.start, name.length); +} + +template <typename CharT> +bool EqualCharIgnoreCaseAscii(CharT c1, char c2) { + if constexpr (sizeof(CharT) > sizeof(char)) { + if (!mozilla::IsAscii(c1)) { + return false; + } + } + + static constexpr auto toLower = 0x20; + static_assert('a' - 'A' == toLower); + + // Convert both characters to lower case before the comparison. + char c = c1; + if (mozilla::IsAsciiUppercaseAlpha(c1)) { + c = c + toLower; + } + char d = c2; + if (mozilla::IsAsciiUppercaseAlpha(c2)) { + d = d + toLower; + } + return c == d; +} + +using CalendarName = StringName; +using AnnotationKey = StringName; +using AnnotationValue = StringName; +using TimeZoneName = StringName; + +struct Annotation final { + AnnotationKey key; + AnnotationValue value; + bool critical = false; +}; + +struct TimeSpec final { + PlainTime time; +}; + +struct TimeZoneUTCOffset final { + // ยฑ1 for time zones with an offset, otherwise 0. + int32_t sign = 0; + + // An integer in the range [0, 23]. + int32_t hour = 0; + + // An integer in the range [0, 59]. + int32_t minute = 0; +}; + +struct DateTimeUTCOffset final { + // ยฑ1 for time zones with an offset, otherwise 0. + int32_t sign = 0; + + // An integer in the range [0, 23]. + int32_t hour = 0; + + // An integer in the range [0, 59]. + int32_t minute = 0; + + // An integer in the range [0, 59]. + int32_t second = 0; + + // An integer in the range [0, 999'999]. + int32_t fractionalPart = 0; + + // Time zone with sub-minute precision. + bool subMinutePrecision = false; + + // Convert to a TimeZoneUTCOffset. + TimeZoneUTCOffset toTimeZoneUTCOffset() const { + MOZ_ASSERT(!subMinutePrecision, "unexpected sub-minute precision"); + return {sign, hour, minute}; + } +}; + +/** + * ParseDateTimeUTCOffset ( offsetString ) + */ +static int64_t ParseDateTimeUTCOffset(const DateTimeUTCOffset& offset) { + constexpr int64_t nanoPerSec = 1'000'000'000; + + MOZ_ASSERT(offset.sign == -1 || offset.sign == +1); + MOZ_ASSERT(0 <= offset.hour && offset.hour < 24); + MOZ_ASSERT(0 <= offset.minute && offset.minute < 60); + MOZ_ASSERT(0 <= offset.second && offset.second < 60); + MOZ_ASSERT(0 <= offset.fractionalPart && offset.fractionalPart < nanoPerSec); + + // sign ร (((hours ร 60 + minutes) ร 60 + seconds) ร 10^9 + nanoseconds). + int64_t seconds = (offset.hour * 60 + offset.minute) * 60 + offset.second; + int64_t nanos = (seconds * nanoPerSec) + offset.fractionalPart; + int64_t result = offset.sign * nanos; + + MOZ_ASSERT(std::abs(result) < ToNanoseconds(TemporalUnit::Day), + "time zone offset is less than 24:00 hours"); + + return result; +} + +static int32_t ParseTimeZoneOffset(const TimeZoneUTCOffset& offset) { + MOZ_ASSERT(offset.sign == -1 || offset.sign == +1); + MOZ_ASSERT(0 <= offset.hour && offset.hour < 24); + MOZ_ASSERT(0 <= offset.minute && offset.minute < 60); + + // sign ร (hour ร 60 + minute). + int32_t result = offset.sign * (offset.hour * 60 + offset.minute); + + MOZ_ASSERT(std::abs(result) < UnitsPerDay(TemporalUnit::Minute), + "time zone offset is less than 24:00 hours"); + + return result; +} + +/** + * Struct to hold time zone annotations. + */ +struct TimeZoneAnnotation final { + // Time zone offset. + TimeZoneUTCOffset offset; + + // Time zone name. + TimeZoneName name; + + /** + * Returns true iff the time zone has an offset part, e.g. "+01:00". + */ + bool hasOffset() const { return offset.sign != 0; } + + /** + * Returns true iff the time zone has an IANA name, e.g. "Asia/Tokyo". + */ + bool hasName() const { return name.present(); } +}; + +/** + * Struct to hold any time zone parts of a parsed string. + */ +struct TimeZoneString final { + // Date-time UTC offset. + DateTimeUTCOffset offset; + + // Time zone annotation; + TimeZoneAnnotation annotation; + + // UTC time zone. + bool utc = false; + + static auto from(DateTimeUTCOffset offset) { + TimeZoneString timeZone{}; + timeZone.offset = offset; + return timeZone; + } + + static auto from(TimeZoneUTCOffset offset) { + TimeZoneString timeZone{}; + timeZone.annotation.offset = offset; + return timeZone; + } + + static auto from(TimeZoneName name) { + TimeZoneString timeZone{}; + timeZone.annotation.name = name; + return timeZone; + } + + static auto UTC() { + TimeZoneString timeZone{}; + timeZone.utc = true; + return timeZone; + } + + /** + * Returns true iff the time zone has an offset part, e.g. "+01:00". + */ + bool hasOffset() const { return offset.sign != 0; } + + /** + * Returns true iff the time zone has an annotation. + */ + bool hasAnnotation() const { + return annotation.hasName() || annotation.hasOffset(); + } + + /** + * Returns true iff the time zone uses the "Z" abbrevation to denote UTC time. + */ + bool isUTC() const { return utc; } +}; + +/** + * Struct to hold the parsed date, time, time zone, and calendar components. + */ +struct ZonedDateTimeString final { + PlainDate date; + PlainTime time; + TimeZoneString timeZone; + CalendarName calendar; +}; + +template <typename CharT> +static bool IsISO8601Calendar(mozilla::Span<const CharT> calendar) { + static constexpr std::string_view iso8601 = "iso8601"; + + if (calendar.size() != iso8601.length()) { + return false; + } + + for (size_t i = 0; i < iso8601.length(); i++) { + if (!EqualCharIgnoreCaseAscii(calendar[i], iso8601[i])) { + return false; + } + } + return true; +} + +static constexpr int32_t AbsentYear = INT32_MAX; + +/** + * ParseISODateTime ( isoString ) + */ +static bool ParseISODateTime(JSContext* cx, const ZonedDateTimeString& parsed, + PlainDateTime* result) { + // Steps 1-6, 8, 10-13 (Not applicable here). + + PlainDateTime dateTime = {parsed.date, parsed.time}; + + // NOTE: ToIntegerOrInfinity("") is 0. + if (dateTime.date.year == AbsentYear) { + dateTime.date.year = 0; + } + + // Step 7. + if (dateTime.date.month == 0) { + dateTime.date.month = 1; + } + + // Step 9. + if (dateTime.date.day == 0) { + dateTime.date.day = 1; + } + + // Step 14. + if (dateTime.time.second == 60) { + dateTime.time.second = 59; + } + + // ParseISODateTime, steps 15-16 (Not applicable in our implementation). + + // Call ThrowIfInvalidISODate to report an error if |days| exceeds the number + // of days in the month. All other values are already in-bounds. + MOZ_ASSERT(std::abs(dateTime.date.year) <= 999'999); + MOZ_ASSERT(1 <= dateTime.date.month && dateTime.date.month <= 12); + MOZ_ASSERT(1 <= dateTime.date.day && dateTime.date.day <= 31); + + // ParseISODateTime, step 17. + if (!ThrowIfInvalidISODate(cx, dateTime.date)) { + return false; + } + + // ParseISODateTime, step 18. + MOZ_ASSERT(IsValidTime(dateTime.time)); + + // Steps 19-25. (Handled in caller.) + + *result = dateTime; + return true; +} + +static bool ParseTimeZoneAnnotation(JSContext* cx, + const TimeZoneAnnotation& annotation, + JSLinearString* linear, + MutableHandle<ParsedTimeZone> result) { + MOZ_ASSERT(annotation.hasOffset() || annotation.hasName()); + + if (annotation.hasOffset()) { + int32_t offset = ParseTimeZoneOffset(annotation.offset); + result.set(ParsedTimeZone::fromOffset(offset)); + return true; + } + + auto* str = ToString(cx, linear, annotation.name); + if (!str) { + return false; + } + result.set(ParsedTimeZone::fromName(str)); + return true; +} + +/** + * Struct for the parsed duration components. + */ +struct TemporalDurationString final { + // A non-negative integer or +Infinity. + double years = 0; + + // A non-negative integer or +Infinity. + double months = 0; + + // A non-negative integer or +Infinity. + double weeks = 0; + + // A non-negative integer or +Infinity. + double days = 0; + + // A non-negative integer or +Infinity. + double hours = 0; + + // A non-negative integer or +Infinity. + double minutes = 0; + + // A non-negative integer or +Infinity. + double seconds = 0; + + // An integer in the range [0, 999'999]. + int32_t hoursFraction = 0; + + // An integer in the range [0, 999'999]. + int32_t minutesFraction = 0; + + // An integer in the range [0, 999'999]. + int32_t secondsFraction = 0; + + // ยฑ1 when an offset is present, otherwise 0. + int32_t sign = 0; +}; + +class ParserError final { + JSErrNum error_ = JSMSG_NOT_AN_ERROR; + + public: + constexpr MOZ_IMPLICIT ParserError(JSErrNum error) : error_(error) {} + + constexpr JSErrNum error() const { return error_; } + + constexpr operator JSErrNum() const { return error(); } +}; + +namespace mozilla::detail { +// Zero is used for tagging, so it mustn't be an error. +static_assert(static_cast<JSErrNum>(0) == JSMSG_NOT_AN_ERROR); + +// Ensure efficient packing of the error type. +template <> +struct UnusedZero<::ParserError> { + private: + using Error = ::ParserError; + using ErrorKind = JSErrNum; + + public: + using StorageType = std::underlying_type_t<ErrorKind>; + + static constexpr bool value = true; + static constexpr StorageType nullValue = 0; + + static constexpr Error Inspect(const StorageType& aValue) { + return Error(static_cast<ErrorKind>(aValue)); + } + static constexpr Error Unwrap(StorageType aValue) { + return Error(static_cast<ErrorKind>(aValue)); + } + static constexpr StorageType Store(Error aValue) { + return static_cast<StorageType>(aValue.error()); + } +}; +} // namespace mozilla::detail + +static_assert(mozilla::Result<ZonedDateTimeString, ParserError>::Strategy != + mozilla::detail::PackingStrategy::Variant); + +template <typename CharT> +class StringReader final { + mozilla::Span<const CharT> string_; + + // Current position in the string. + size_t index_ = 0; + + public: + explicit StringReader(mozilla::Span<const CharT> string) : string_(string) {} + + /** + * Returns the input string. + */ + mozilla::Span<const CharT> string() const { return string_; } + + /** + * Returns a substring of the input string. + */ + mozilla::Span<const CharT> substring(const StringName& name) const { + MOZ_ASSERT(name.present()); + return string_.Subspan(name.start, name.length); + } + + /** + * Returns the current parse position. + */ + size_t index() const { return index_; } + + /** + * Returns the length of the input string- + */ + size_t length() const { return string_.size(); } + + /** + * Returns true iff the whole string has been parsed. + */ + bool atEnd() const { return index() == length(); } + + /** + * Reset the parser to a previous parse position. + */ + void reset(size_t index = 0) { + MOZ_ASSERT(index <= length()); + index_ = index; + } + + /** + * Returns true if at least `amount` characters can be read from the current + * parse position. + */ + bool hasMore(size_t amount) const { return index() + amount <= length(); } + + /** + * Advances the parse position by `amount` characters. + */ + void advance(size_t amount) { + MOZ_ASSERT(hasMore(amount)); + index_ += amount; + } + + /** + * Returns the character at the current parse position. + */ + CharT current() const { return string()[index()]; } + + /** + * Returns the character at the next parse position. + */ + CharT next() const { return string()[index() + 1]; } + + /** + * Returns the character at position `index`. + */ + CharT at(size_t index) const { return string()[index]; } +}; + +template <typename CharT> +class TemporalParser final { + StringReader<CharT> reader_; + + /** + * Read an unlimited amount of decimal digits, returning `Nothing` if no + * digits were read. + */ + mozilla::Maybe<double> digits(JSContext* cx); + + /** + * Read exactly `length` digits, returning `Nothing` on failure. + */ + mozilla::Maybe<int32_t> digits(size_t length) { + MOZ_ASSERT(length > 0, "can't read zero digits"); + MOZ_ASSERT(length <= std::numeric_limits<int32_t>::digits10, + "can't read more than digits10 digits without overflow"); + + if (!reader_.hasMore(length)) { + return mozilla::Nothing(); + } + int32_t num = 0; + size_t index = reader_.index(); + for (size_t i = 0; i < length; i++) { + auto ch = reader_.at(index + i); + if (!mozilla::IsAsciiDigit(ch)) { + return mozilla::Nothing(); + } + num = num * 10 + AsciiDigitToNumber(ch); + } + reader_.advance(length); + return mozilla::Some(num); + } + + // TimeFractionalPart : + // Digit{1, 9} + // + // Fraction : + // DecimalSeparator TimeFractionalPart + mozilla::Maybe<int32_t> fraction() { + if (!reader_.hasMore(2)) { + return mozilla::Nothing(); + } + if (!hasDecimalSeparator() || !mozilla::IsAsciiDigit(reader_.next())) { + return mozilla::Nothing(); + } + + // Consume the decimal separator. + MOZ_ALWAYS_TRUE(decimalSeparator()); + + // Maximal nine fractional digits are supported. + constexpr size_t maxFractions = 9; + + // Read up to |maxFractions| digits. + int32_t num = 0; + size_t index = reader_.index(); + size_t i = 0; + for (; i < std::min(reader_.length() - index, maxFractions); i++) { + CharT ch = reader_.at(index + i); + if (!mozilla::IsAsciiDigit(ch)) { + break; + } + num = num * 10 + AsciiDigitToNumber(ch); + } + + // Skip past the read digits. + reader_.advance(i); + + // Normalize the fraction to |maxFractions| digits. + for (; i < maxFractions; i++) { + num *= 10; + } + return mozilla::Some(num); + } + + /** + * Returns true iff the current character is `ch`. + */ + bool hasCharacter(CharT ch) const { + return reader_.hasMore(1) && reader_.current() == ch; + } + + /** + * Consumes the current character if it's equal to `ch` and then returns + * `true`. Otherwise returns `false`. + */ + bool character(CharT ch) { + if (!hasCharacter(ch)) { + return false; + } + reader_.advance(1); + return true; + } + + /** + * Consumes the next characters if they're equal to `str` and then returns + * `true`. Otherwise returns `false`. + */ + template <size_t N> + bool string(const char (&str)[N]) { + static_assert(N > 2, "use character() for one element strings"); + + if (!reader_.hasMore(N - 1)) { + return false; + } + size_t index = reader_.index(); + for (size_t i = 0; i < N - 1; i++) { + if (reader_.at(index + i) != str[i]) { + return false; + } + } + reader_.advance(N - 1); + return true; + } + + /** + * Returns true if the next two characters are ASCII alphabetic characters. + */ + bool hasTwoAsciiAlpha() { + if (!reader_.hasMore(2)) { + return false; + } + size_t index = reader_.index(); + return mozilla::IsAsciiAlpha(reader_.at(index)) && + mozilla::IsAsciiAlpha(reader_.at(index + 1)); + } + + /** + * Returns true iff the current character is one of `chars`. + */ + bool hasOneOf(std::initializer_list<char16_t> chars) const { + if (!reader_.hasMore(1)) { + return false; + } + auto ch = reader_.current(); + return std::find(chars.begin(), chars.end(), ch) != chars.end(); + } + + /** + * Consumes the current character if it's in `chars` and then returns `true`. + * Otherwise returns `false`. + */ + bool oneOf(std::initializer_list<char16_t> chars) { + if (!hasOneOf(chars)) { + return false; + } + reader_.advance(1); + return true; + } + + /** + * Consumes the current character if it matches the predicate and then returns + * `true`. Otherwise returns `false`. + */ + template <typename Predicate> + bool matches(Predicate&& predicate) { + if (!reader_.hasMore(1)) { + return false; + } + + CharT ch = reader_.current(); + if (!predicate(ch)) { + return false; + } + + reader_.advance(1); + return true; + } + + // Sign : + // ASCIISign + // U+2212 + // + // ASCIISign : one of + // + - + bool hasSign() const { return hasOneOf({'+', '-', 0x2212}); } + + /** + * Consumes the current character, which must be a sign character, and returns + * its numeric value. + */ + int32_t sign() { + MOZ_ASSERT(hasSign()); + int32_t plus = hasCharacter('+'); + reader_.advance(1); + return plus ? 1 : -1; + } + + // DecimalSeparator : one of + // . , + bool hasDecimalSeparator() const { return hasOneOf({'.', ','}); } + + bool decimalSeparator() { return oneOf({'.', ','}); } + + // DaysDesignator : one of + // D d + bool daysDesignator() { return oneOf({'D', 'd'}); } + + // HoursDesignator : one of + // H h + bool hoursDesignator() { return oneOf({'H', 'h'}); } + + // MinutesDesignator : one of + // M m + bool minutesDesignator() { return oneOf({'M', 'm'}); } + + // MonthsDesignator : one of + // M m + bool monthsDesignator() { return oneOf({'M', 'm'}); } + + // DurationDesignator : one of + // P p + bool durationDesignator() { return oneOf({'P', 'p'}); } + + // SecondsDesignator : one of + // S s + bool secondsDesignator() { return oneOf({'S', 's'}); } + + // DateTimeSeparator : + // <SP> + // T + // t + bool dateTimeSeparator() { return oneOf({' ', 'T', 't'}); } + + // TimeDesignator : one of + // T t + bool hasTimeDesignator() const { return hasOneOf({'T', 't'}); } + + bool timeDesignator() { return oneOf({'T', 't'}); } + + // WeeksDesignator : one of + // W w + bool weeksDesignator() { return oneOf({'W', 'w'}); } + + // YearsDesignator : one of + // Y y + bool yearsDesignator() { return oneOf({'Y', 'y'}); } + + // UTCDesignator : one of + // Z z + bool utcDesignator() { return oneOf({'Z', 'z'}); } + + // TZLeadingChar : + // Alpha + // . + // _ + bool tzLeadingChar() { + return matches([](auto ch) { + return mozilla::IsAsciiAlpha(ch) || ch == '.' || ch == '_'; + }); + } + + // TZChar : + // TZLeadingChar + // DecimalDigit + // - + // + + bool tzChar() { + return matches([](auto ch) { + return mozilla::IsAsciiAlphanumeric(ch) || ch == '.' || ch == '_' || + ch == '-' || ch == '+'; + }); + } + + // AnnotationCriticalFlag : + // ! + bool annotationCriticalFlag() { return character('!'); } + + // AKeyLeadingChar : + // LowercaseAlpha + // _ + bool aKeyLeadingChar() { + return matches([](auto ch) { + return mozilla::IsAsciiLowercaseAlpha(ch) || ch == '_'; + }); + } + + // AKeyChar : + // AKeyLeadingChar + // DecimalDigit + // - + bool aKeyChar() { + return matches([](auto ch) { + return mozilla::IsAsciiLowercaseAlpha(ch) || mozilla::IsAsciiDigit(ch) || + ch == '-' || ch == '_'; + }); + } + + // AnnotationValueComponent : + // Alpha AnnotationValueComponent? + // DecimalDigit AnnotationValueComponent? + bool annotationValueComponent() { + size_t index = reader_.index(); + size_t i = 0; + for (; index + i < reader_.length(); i++) { + auto ch = reader_.at(index + i); + if (!mozilla::IsAsciiAlphanumeric(ch)) { + break; + } + } + if (i == 0) { + return false; + } + reader_.advance(i); + return true; + } + + template <typename T> + static constexpr bool inBounds(const T& x, const T& min, const T& max) { + return min <= x && x <= max; + } + + mozilla::Result<ZonedDateTimeString, ParserError> dateTime(); + + mozilla::Result<PlainDate, ParserError> date(); + + mozilla::Result<PlainDate, ParserError> dateSpecYearMonth(); + + mozilla::Result<PlainDate, ParserError> dateSpecMonthDay(); + + mozilla::Result<PlainDate, ParserError> validMonthDay(); + + mozilla::Result<PlainTime, ParserError> timeSpec(); + + // Return true when |Annotation| can start at the current position. + bool hasAnnotationStart() const { return hasCharacter('['); } + + // Return true when |TimeZoneAnnotation| can start at the current position. + bool hasTimeZoneAnnotationStart() const { + if (!hasCharacter('[')) { + return false; + } + + // Ensure no '=' is found before the closing ']', otherwise the opening '[' + // may actually start an |Annotation| instead of a |TimeZoneAnnotation|. + for (size_t i = reader_.index() + 1; i < reader_.length(); i++) { + CharT ch = reader_.at(i); + if (ch == '=') { + return false; + } + if (ch == ']') { + break; + } + } + return true; + } + + // Return true when |DateTimeUTCOffset| can start at the current position. + bool hasDateTimeUTCOffsetStart() { + return hasOneOf({'Z', 'z', '+', '-', 0x2212}); + } + + mozilla::Result<TimeZoneString, ParserError> dateTimeUTCOffset(); + + mozilla::Result<DateTimeUTCOffset, ParserError> utcOffsetSubMinutePrecision(); + + mozilla::Result<TimeZoneUTCOffset, ParserError> timeZoneUTCOffsetName(); + + mozilla::Result<TimeZoneAnnotation, ParserError> timeZoneIdentifier(); + + mozilla::Result<TimeZoneAnnotation, ParserError> timeZoneAnnotation(); + + mozilla::Result<TimeZoneName, ParserError> timeZoneIANAName(); + + mozilla::Result<AnnotationKey, ParserError> annotationKey(); + mozilla::Result<AnnotationValue, ParserError> annotationValue(); + mozilla::Result<Annotation, ParserError> annotation(); + mozilla::Result<CalendarName, ParserError> annotations(); + + mozilla::Result<ZonedDateTimeString, ParserError> annotatedTime(); + + mozilla::Result<ZonedDateTimeString, ParserError> annotatedDateTime(); + + mozilla::Result<ZonedDateTimeString, ParserError> + annotatedDateTimeTimeRequired(); + + mozilla::Result<ZonedDateTimeString, ParserError> annotatedYearMonth(); + + mozilla::Result<ZonedDateTimeString, ParserError> annotatedMonthDay(); + + public: + explicit TemporalParser(mozilla::Span<const CharT> str) : reader_(str) {} + + mozilla::Result<ZonedDateTimeString, ParserError> + parseTemporalInstantString(); + + mozilla::Result<ZonedDateTimeString, ParserError> + parseTemporalTimeZoneString(); + + mozilla::Result<TimeZoneAnnotation, ParserError> parseTimeZoneIdentifier(); + + mozilla::Result<TimeZoneUTCOffset, ParserError> parseTimeZoneOffsetString(); + + mozilla::Result<DateTimeUTCOffset, ParserError> parseDateTimeUTCOffset(); + + mozilla::Result<TemporalDurationString, ParserError> + parseTemporalDurationString(JSContext* cx); + + mozilla::Result<ZonedDateTimeString, ParserError> + parseTemporalCalendarString(); + + mozilla::Result<ZonedDateTimeString, ParserError> parseTemporalTimeString(); + + mozilla::Result<ZonedDateTimeString, ParserError> + parseTemporalMonthDayString(); + + mozilla::Result<ZonedDateTimeString, ParserError> + parseTemporalYearMonthString(); + + mozilla::Result<ZonedDateTimeString, ParserError> + parseTemporalDateTimeString(); + + mozilla::Result<ZonedDateTimeString, ParserError> + parseTemporalZonedDateTimeString(); +}; + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::dateTime() { + // DateTime : + // Date + // Date DateTimeSeparator TimeSpec DateTimeUTCOffset? + ZonedDateTimeString result = {}; + + auto dt = date(); + if (dt.isErr()) { + return dt.propagateErr(); + } + result.date = dt.unwrap(); + + if (dateTimeSeparator()) { + auto time = timeSpec(); + if (time.isErr()) { + return time.propagateErr(); + } + result.time = time.unwrap(); + + if (hasDateTimeUTCOffsetStart()) { + auto tz = dateTimeUTCOffset(); + if (tz.isErr()) { + return tz.propagateErr(); + } + result.timeZone = tz.unwrap(); + } + } + + return result; +} + +template <typename CharT> +mozilla::Result<PlainDate, ParserError> TemporalParser<CharT>::date() { + // Date : + // DateYear - DateMonth - DateDay + // DateYear DateMonth DateDay + PlainDate result = {}; + + // DateYear : + // DecimalDigit{4} + // Sign DecimalDigit{6} + if (auto year = digits(4)) { + result.year = year.value(); + } else if (hasSign()) { + int32_t yearSign = sign(); + if (auto year = digits(6)) { + result.year = yearSign * year.value(); + if (yearSign < 0 && result.year == 0) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_NEGATIVE_ZERO_YEAR); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_EXTENDED_YEAR); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_YEAR); + } + + // Optional: - + character('-'); + + // DateMonth : + // 0 NonzeroDigit + // 10 + // 11 + // 12 + if (auto month = digits(2)) { + result.month = month.value(); + if (!inBounds(result.month, 1, 12)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); + } + + // Optional: - + character('-'); + + // DateDay : + // 0 NonzeroDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 30 + // 31 + if (auto day = digits(2)) { + result.day = day.value(); + if (!inBounds(result.day, 1, 31)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DAY); + } + + return result; +} + +template <typename CharT> +mozilla::Result<PlainTime, ParserError> TemporalParser<CharT>::timeSpec() { + // TimeSpec : + // TimeHour + // TimeHour : TimeMinute + // TimeHour TimeMinute + // TimeHour : TimeMinute : TimeSecond TimeFraction? + // TimeHour TimeMinute TimeSecond TimeFraction? + PlainTime result = {}; + + // TimeHour : + // Hour[+Padded] + // + // Hour[Padded] : + // [~Padded] DecimalDigit + // [~Padded] 0 DecimalDigit + // 1 DecimalDigit + // 20 + // 21 + // 22 + // 23 + if (auto hour = digits(2)) { + result.hour = hour.value(); + if (!inBounds(result.hour, 0, 23)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_HOUR); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); + } + + // Optional: : + bool needsMinutes = character(':'); + + // TimeMinute : + // MinuteSecond + // + // MinuteSecond : + // 0 DecimalDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 3 DecimalDigit + // 4 DecimalDigit + // 5 DecimalDigit + if (auto minute = digits(2)) { + result.minute = minute.value(); + if (!inBounds(result.minute, 0, 59)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MINUTE); + } + + // Optional: : + bool needsSeconds = needsMinutes && character(':'); + + // TimeSecond : + // MinuteSecond + // 60 + if (auto second = digits(2)) { + result.second = second.value(); + if (!inBounds(result.second, 0, 60)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_LEAPSECOND); + } + + // TimeFraction : + // Fraction + if (auto f = fraction()) { + int32_t fractionalPart = f.value(); + result.millisecond = fractionalPart / 1'000'000; + result.microsecond = (fractionalPart % 1'000'000) / 1'000; + result.nanosecond = fractionalPart % 1'000; + } + } else if (needsSeconds) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_SECOND); + } + } else if (needsMinutes) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MINUTE); + } + + return result; +} + +template <typename CharT> +mozilla::Result<TimeZoneString, ParserError> +TemporalParser<CharT>::dateTimeUTCOffset() { + // DateTimeUTCOffset : + // UTCDesignator + // UTCOffsetSubMinutePrecision + + if (utcDesignator()) { + return TimeZoneString::UTC(); + } + + if (hasSign()) { + auto offset = utcOffsetSubMinutePrecision(); + if (offset.isErr()) { + return offset.propagateErr(); + } + return TimeZoneString::from(offset.unwrap()); + } + + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE); +} + +template <typename CharT> +mozilla::Result<TimeZoneUTCOffset, ParserError> +TemporalParser<CharT>::timeZoneUTCOffsetName() { + // TimeZoneUTCOffsetName : + // UTCOffsetMinutePrecision + // + // UTCOffsetMinutePrecision : + // Sign Hour[+Padded] + // Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond + // Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond + + TimeZoneUTCOffset result = {}; + + if (!hasSign()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE_SIGN); + } + result.sign = sign(); + + // Hour[Padded] : + // [~Padded] DecimalDigit + // [+Padded] 0 DecimalDigit + // 1 DecimalDigit + // 20 + // 21 + // 22 + // 23 + if (auto hour = digits(2)) { + result.hour = hour.value(); + if (!inBounds(result.hour, 0, 23)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_HOUR); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); + } + + // TimeSeparator[Extended] : + // [+Extended] : + // [~Extended] [empty] + bool needsMinutes = character(':'); + + // MinuteSecond : + // 0 DecimalDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 3 DecimalDigit + // 4 DecimalDigit + // 5 DecimalDigit + if (auto minute = digits(2)) { + result.minute = minute.value(); + if (!inBounds(result.minute, 0, 59)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MINUTE); + } + + if (hasCharacter(':')) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_SUBMINUTE_TIMEZONE); + } + } else if (needsMinutes) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MINUTE); + } + + return result; +} + +template <typename CharT> +mozilla::Result<DateTimeUTCOffset, ParserError> +TemporalParser<CharT>::utcOffsetSubMinutePrecision() { + // clang-format off + // + // UTCOffsetSubMinutePrecision : + // UTCOffsetMinutePrecision + // UTCOffsetWithSubMinuteComponents[+Extended] + // UTCOffsetWithSubMinuteComponents[~Extended] + // + // UTCOffsetMinutePrecision : + // Sign Hour[+Padded] + // Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond + // Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond + // + // UTCOffsetWithSubMinuteComponents[Extended] : + // Sign Hour[+Padded] TimeSeparator[?Extended] MinuteSecond TimeSeparator[?Extended] MinuteSecond Fraction? + // + // clang-format on + + DateTimeUTCOffset result = {}; + + if (!hasSign()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE_SIGN); + } + result.sign = sign(); + + // Hour[Padded] : + // [~Padded] DecimalDigit + // [+Padded] 0 DecimalDigit + // 1 DecimalDigit + // 20 + // 21 + // 22 + // 23 + if (auto hour = digits(2)) { + result.hour = hour.value(); + if (!inBounds(result.hour, 0, 23)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_HOUR); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_HOUR); + } + + // TimeSeparator[Extended] : + // [+Extended] : + // [~Extended] [empty] + bool needsMinutes = character(':'); + + // MinuteSecond : + // 0 DecimalDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 3 DecimalDigit + // 4 DecimalDigit + // 5 DecimalDigit + if (auto minute = digits(2)) { + result.minute = minute.value(); + if (!inBounds(result.minute, 0, 59)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MINUTE); + } + + // TimeSeparator[Extended] : + // [+Extended] : + // [~Extended] [empty] + bool needsSeconds = needsMinutes && character(':'); + + // MinuteSecond : + // 0 DecimalDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 3 DecimalDigit + // 4 DecimalDigit + // 5 DecimalDigit + if (auto second = digits(2)) { + result.second = second.value(); + if (!inBounds(result.second, 0, 59)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_SECOND); + } + + if (auto fractionalPart = fraction()) { + result.fractionalPart = fractionalPart.value(); + } + + result.subMinutePrecision = true; + } else if (needsSeconds) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_SECOND); + } + } else if (needsMinutes) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MINUTE); + } + + return result; +} + +template <typename CharT> +mozilla::Result<TimeZoneAnnotation, ParserError> +TemporalParser<CharT>::timeZoneIdentifier() { + // TimeZoneIdentifier : + // TimeZoneUTCOffsetName + // TimeZoneIANAName + + TimeZoneAnnotation result = {}; + if (hasSign()) { + auto offset = timeZoneUTCOffsetName(); + if (offset.isErr()) { + return offset.propagateErr(); + } + result.offset = offset.unwrap(); + } else { + auto name = timeZoneIANAName(); + if (name.isErr()) { + return name.propagateErr(); + } + result.name = name.unwrap(); + } + + return result; +} + +template <typename CharT> +mozilla::Result<TimeZoneAnnotation, ParserError> +TemporalParser<CharT>::timeZoneAnnotation() { + // TimeZoneAnnotation : + // [ AnnotationCriticalFlag? TimeZoneIdentifier ] + + if (!character('[')) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_BEFORE_TIMEZONE); + } + + // Skip over the optional critical flag. + annotationCriticalFlag(); + + auto result = timeZoneIdentifier(); + if (result.isErr()) { + return result.propagateErr(); + } + + if (!character(']')) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_AFTER_TIMEZONE); + } + + return result; +} + +template <typename CharT> +mozilla::Result<TimeZoneName, ParserError> +TemporalParser<CharT>::timeZoneIANAName() { + // TimeZoneIANAName : + // TimeZoneIANANameComponent + // TimeZoneIANAName / TimeZoneIANANameComponent + // + // TimeZoneIANANameComponent : + // TZLeadingChar + // TimeZoneIANANameComponent TZChar + + size_t start = reader_.index(); + + do { + if (!tzLeadingChar()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE_NAME); + } + + // Optionally followed by a sequence of |TZChar|. + while (tzChar()) { + } + } while (character('/')); + + return TimeZoneName{start, reader_.index() - start}; +} + +template <typename CharT> +mozilla::Maybe<double> TemporalParser<CharT>::digits(JSContext* cx) { + auto span = reader_.string().Subspan(reader_.index()); + + // GetPrefixInteger can't fail when integer separator handling is disabled. + const CharT* endp = nullptr; + double num; + MOZ_ALWAYS_TRUE(GetPrefixInteger(span.data(), span.data() + span.size(), 10, + IntegerSeparatorHandling::None, &endp, + &num)); + + size_t len = endp - span.data(); + if (len == 0) { + return mozilla::Nothing(); + } + reader_.advance(len); + return mozilla::Some(num); +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalInstantString() { + // Initialize all fields to zero. + ZonedDateTimeString result = {}; + + // clang-format off + // + // TemporalInstantString : + // Date DateTimeSeparator TimeSpec DateTimeUTCOffset TimeZoneAnnotation? Annotations? + // + // clang-format on + + auto dt = date(); + if (dt.isErr()) { + return dt.propagateErr(); + } + result.date = dt.unwrap(); + + if (!dateTimeSeparator()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DATE_TIME_SEPARATOR); + } + + auto time = timeSpec(); + if (time.isErr()) { + return time.propagateErr(); + } + result.time = time.unwrap(); + + auto tz = dateTimeUTCOffset(); + if (tz.isErr()) { + return tz.propagateErr(); + } + result.timeZone = tz.unwrap(); + + if (hasTimeZoneAnnotationStart()) { + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + } + + if (hasAnnotationStart()) { + if (auto cal = annotations(); cal.isErr()) { + return cal.propagateErr(); + } + } + + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + + return result; +} + +/** + * ParseTemporalInstantString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalInstantString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalInstantString(); +} + +/** + * ParseTemporalInstantString ( isoString ) + */ +static auto ParseTemporalInstantString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalInstantString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalInstantString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalInstantString ( isoString ) + */ +bool js::temporal::ParseTemporalInstantString(JSContext* cx, + Handle<JSString*> str, + PlainDateTime* result, + int64_t* offset) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Step 1. + auto parseResult = ::ParseTemporalInstantString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + // Step 2. + if (!ParseISODateTime(cx, parsed, result)) { + return false; + } + + // Steps 3-4. + if (parsed.timeZone.hasOffset()) { + *offset = ParseDateTimeUTCOffset(parsed.timeZone.offset); + } else { + MOZ_ASSERT(parsed.timeZone.isUTC()); + *offset = 0; + } + return true; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalTimeZoneString() { + // TimeZoneIdentifier : + // TimeZoneUTCOffsetName + // TimeZoneIANAName + + if (hasSign()) { + if (auto offset = timeZoneUTCOffsetName(); + offset.isOk() && reader_.atEnd()) { + ZonedDateTimeString result = {}; + result.timeZone = TimeZoneString::from(offset.unwrap()); + return result; + } + } else { + if (auto name = timeZoneIANAName(); name.isOk() && reader_.atEnd()) { + ZonedDateTimeString result = {}; + result.timeZone = TimeZoneString::from(name.unwrap()); + return result; + } + } + + // Try all five parse goals from ParseISODateTime in order. + // + // TemporalDateTimeString + // TemporalInstantString + // TemporalTimeString + // TemporalMonthDayString + // TemporalYearMonthString + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalDateTimeString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalInstantString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalTimeString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalMonthDayString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalYearMonthString(); dt.isOk()) { + return dt.unwrap(); + } else { + return dt.propagateErr(); + } +} + +/** + * ParseTemporalTimeZoneString ( timeZoneString ) + */ +template <typename CharT> +static auto ParseTemporalTimeZoneString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalTimeZoneString(); +} + +/** + * ParseTemporalTimeZoneString ( timeZoneString ) + */ +static auto ParseTemporalTimeZoneString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalTimeZoneString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalTimeZoneString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalTimeZoneString ( timeZoneString ) + */ +bool js::temporal::ParseTemporalTimeZoneString( + JSContext* cx, Handle<JSString*> str, + MutableHandle<ParsedTimeZone> result) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-4. + auto parseResult = ::ParseTemporalTimeZoneString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + const auto& timeZone = parsed.timeZone; + + // Step 3. + PlainDateTime unused; + if (!ParseISODateTime(cx, parsed, &unused)) { + return false; + } + + if (timeZone.hasAnnotation()) { + // Case 1: 19700101T00:00Z[+02:00] + // Case 2: 19700101T00:00+00:00[+02:00] + // Case 3: 19700101T00:00[+02:00] + // Case 4: 19700101T00:00Z[Europe/Berlin] + // Case 5: 19700101T00:00+00:00[Europe/Berlin] + // Case 6: 19700101T00:00[Europe/Berlin] + + if (!ParseTimeZoneAnnotation(cx, timeZone.annotation, linear, result)) { + return false; + } + } else if (timeZone.isUTC()) { + result.set(ParsedTimeZone::fromName(cx->names().UTC)); + } else if (timeZone.hasOffset()) { + // ToTemporalTimeZoneSlotValue, step 7. + // + // Error reporting for sub-minute precision moved here. + if (timeZone.offset.subMinutePrecision) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PARSER_INVALID_SUBMINUTE_TIMEZONE); + return false; + } + + int32_t offset = ParseTimeZoneOffset(timeZone.offset.toTimeZoneUTCOffset()); + result.set(ParsedTimeZone::fromOffset(offset)); + } else { + // Step 5. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PARSER_MISSING_TIMEZONE); + return false; + } + + // Step 6. + return true; +} + +template <typename CharT> +mozilla::Result<TimeZoneAnnotation, ParserError> +TemporalParser<CharT>::parseTimeZoneIdentifier() { + auto result = timeZoneIdentifier(); + if (result.isErr()) { + return result.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + return result; +} + +/** + * ParseTimeZoneIdentifier ( identifier ) + */ +template <typename CharT> +static auto ParseTimeZoneIdentifier(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTimeZoneIdentifier(); +} + +/** + * ParseTimeZoneIdentifier ( identifier ) + */ +static auto ParseTimeZoneIdentifier(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTimeZoneIdentifier<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTimeZoneIdentifier<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTimeZoneIdentifier ( identifier ) + */ +bool js::temporal::ParseTimeZoneIdentifier( + JSContext* cx, Handle<JSString*> str, + MutableHandle<ParsedTimeZone> result) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-2. + auto parseResult = ::ParseTimeZoneIdentifier(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + auto timeZone = parseResult.unwrap(); + + // Steps 3-4. + return ParseTimeZoneAnnotation(cx, timeZone, linear, result); +} + +template <typename CharT> +mozilla::Result<TimeZoneUTCOffset, ParserError> +TemporalParser<CharT>::parseTimeZoneOffsetString() { + auto offset = timeZoneUTCOffsetName(); + if (offset.isErr()) { + return offset.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + return offset.unwrap(); +} + +/** + * ParseTimeZoneOffsetString ( isoString ) + */ +template <typename CharT> +static auto ParseTimeZoneOffsetString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTimeZoneOffsetString(); +} + +/** + * ParseTimeZoneOffsetString ( isoString ) + */ +static auto ParseTimeZoneOffsetString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTimeZoneOffsetString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTimeZoneOffsetString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTimeZoneOffsetString ( isoString ) + */ +bool js::temporal::ParseTimeZoneOffsetString(JSContext* cx, + Handle<JSString*> str, + int32_t* result) { + // Step 1. (Not applicable in our implementation.) + + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Step 2. + auto parseResult = ::ParseTimeZoneOffsetString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + + // Steps 3-13. + *result = ParseTimeZoneOffset(parseResult.unwrap()); + return true; +} + +template <typename CharT> +mozilla::Result<DateTimeUTCOffset, ParserError> +TemporalParser<CharT>::parseDateTimeUTCOffset() { + auto offset = utcOffsetSubMinutePrecision(); + if (offset.isErr()) { + return offset.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + return offset.unwrap(); +} + +/** + * ParseDateTimeUTCOffset ( offsetString ) + */ +template <typename CharT> +static auto ParseDateTimeUTCOffset(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseDateTimeUTCOffset(); +} + +/** + * ParseDateTimeUTCOffset ( offsetString ) + */ +static auto ParseDateTimeUTCOffset(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseDateTimeUTCOffset<Latin1Char>(str->latin1Range(nogc)); + } + return ParseDateTimeUTCOffset<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseDateTimeUTCOffset ( offsetString ) + */ +bool js::temporal::ParseDateTimeUTCOffset(JSContext* cx, Handle<JSString*> str, + int64_t* result) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-2. + auto parseResult = ::ParseDateTimeUTCOffset(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + + // Steps 3-21. + *result = ParseDateTimeUTCOffset(parseResult.unwrap()); + return true; +} + +template <typename CharT> +mozilla::Result<TemporalDurationString, ParserError> +TemporalParser<CharT>::parseTemporalDurationString(JSContext* cx) { + // Initialize all fields to zero. + TemporalDurationString result = {}; + + // TemporalDurationString : + // Duration + // + // Duration : + // Sign? DurationDesignator DurationDate + // Sign? DurationDesignator DurationTime + + if (hasSign()) { + result.sign = sign(); + } + + if (!durationDesignator()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DESIGNATOR); + } + + // DurationDate : + // DurationYearsPart DurationTime? + // DurationMonthsPart DurationTime? + // DurationWeeksPart DurationTime? + // DurationDaysPart DurationTime? + + do { + double num; + if (hasTimeDesignator()) { + break; + } + if (auto d = digits(cx); !d) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); + } else { + num = *d; + } + + // DurationYearsPart : + // DurationYears YearsDesignator DurationMonthsPart + // DurationYears YearsDesignator DurationWeeksPart + // DurationYears YearsDesignator DurationDaysPart? + // + // DurationYears : + // DecimalDigits[~Sep] + if (yearsDesignator()) { + result.years = num; + if (reader_.atEnd()) { + return result; + } + if (hasTimeDesignator()) { + break; + } + if (auto d = digits(cx); !d) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); + } else { + num = *d; + } + } + + // DurationMonthsPart : + // DurationMonths MonthsDesignator DurationWeeksPart + // DurationMonths MonthsDesignator DurationDaysPart? + // + // DurationMonths : + // DecimalDigits[~Sep] + if (monthsDesignator()) { + result.months = num; + if (reader_.atEnd()) { + return result; + } + if (hasTimeDesignator()) { + break; + } + if (auto d = digits(cx); !d) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); + } else { + num = *d; + } + } + + // DurationWeeksPart : + // DurationWeeks WeeksDesignator DurationDaysPart? + // + // DurationWeeks : + // DecimalDigits[~Sep] + if (weeksDesignator()) { + result.weeks = num; + if (reader_.atEnd()) { + return result; + } + if (hasTimeDesignator()) { + break; + } + if (auto d = digits(cx); !d) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); + } else { + num = *d; + } + } + + // DurationDaysPart : + // DurationDays DaysDesignator + // + // DurationDays : + // DecimalDigits[~Sep] + if (daysDesignator()) { + result.days = num; + if (reader_.atEnd()) { + return result; + } + if (hasTimeDesignator()) { + break; + } + } + + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } while (false); + + // DurationTime : + // DurationTimeDesignator DurationHoursPart + // DurationTimeDesignator DurationMinutesPart + // DurationTimeDesignator DurationSecondsPart + if (!timeDesignator()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_TIME_DESIGNATOR); + } + + double num; + mozilla::Maybe<int32_t> frac; + auto digitsAndFraction = [&]() { + auto d = digits(cx); + if (!d) { + return false; + } + num = *d; + frac = fraction(); + return true; + }; + + if (!digitsAndFraction()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); + } + + // clang-format off + // + // DurationHoursPart : + // DurationWholeHours DurationHoursFraction HoursDesignator + // DurationWholeHours HoursDesignator DurationMinutesPart + // DurationWholeHours HoursDesignator DurationSecondsPart? + // + // DurationWholeHours : + // DecimalDigits[~Sep] + // + // DurationHoursFraction : + // TimeFraction + // + // TimeFraction : + // Fraction + // + // clang-format on + bool hasHoursFraction = false; + if (hoursDesignator()) { + hasHoursFraction = bool(frac); + result.hours = num; + result.hoursFraction = frac.valueOr(0); + if (reader_.atEnd()) { + return result; + } + if (!digitsAndFraction()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); + } + } + + // clang-format off + // + // DurationMinutesPart : + // DurationWholeMinutes DurationMinutesFraction MinutesDesignator + // DurationWholeMinutes MinutesDesignator DurationSecondsPart? + // + // DurationWholeMinutes : + // DecimalDigits[~Sep] + // + // DurationMinutesFraction : + // TimeFraction + // + // TimeFraction : + // Fraction + // + // clang-format on + bool hasMinutesFraction = false; + if (minutesDesignator()) { + if (hasHoursFraction) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DURATION_MINUTES); + } + hasMinutesFraction = bool(frac); + result.minutes = num; + result.minutesFraction = frac.valueOr(0); + if (reader_.atEnd()) { + return result; + } + if (!digitsAndFraction()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DURATION_DIGITS); + } + } + + // DurationSecondsPart : + // DurationWholeSeconds DurationSecondsFraction? SecondsDesignator + // + // DurationWholeSeconds : + // DecimalDigits[~Sep] + // + // DurationSecondsFraction : + // TimeFraction + // + // TimeFraction : + // Fraction + if (secondsDesignator()) { + if (hasHoursFraction || hasMinutesFraction) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DURATION_SECONDS); + } + result.seconds = num; + result.secondsFraction = frac.valueOr(0); + if (reader_.atEnd()) { + return result; + } + } + + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); +} + +/** + * ParseTemporalDurationString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalDurationString(JSContext* cx, + mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalDurationString(cx); +} + +/** + * ParseTemporalDurationString ( isoString ) + */ +static auto ParseTemporalDurationString(JSContext* cx, + Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalDurationString<Latin1Char>(cx, str->latin1Range(nogc)); + } + return ParseTemporalDurationString<char16_t>(cx, str->twoByteRange(nogc)); +} + +/** + * ParseTemporalDurationString ( isoString ) + */ +bool js::temporal::ParseTemporalDurationString(JSContext* cx, + Handle<JSString*> str, + Duration* result) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-3. + auto parseResult = ::ParseTemporalDurationString(cx, linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + TemporalDurationString parsed = parseResult.unwrap(); + + // Steps 4-8. + double years = parsed.years; + double months = parsed.months; + double weeks = parsed.weeks; + double days = parsed.days; + double hours = parsed.hours; + + // Steps 9-17. + double minutes, seconds, milliseconds, microseconds, nanoseconds; + if (parsed.hoursFraction) { + MOZ_ASSERT(parsed.hoursFraction > 0); + MOZ_ASSERT(parsed.hoursFraction < 1'000'000'000); + + // Step 9.a. + MOZ_ASSERT(parsed.minutes == 0); + MOZ_ASSERT(parsed.minutesFraction == 0); + MOZ_ASSERT(parsed.seconds == 0); + MOZ_ASSERT(parsed.secondsFraction == 0); + + // Steps 9.b-d. + int64_t h = int64_t(parsed.hoursFraction) * 60; + minutes = h / 1'000'000'000; + + // Steps 13 and 15-17. + int64_t min = (h % 1'000'000'000) * 60; + seconds = min / 1'000'000'000; + milliseconds = (min % 1'000'000'000) / 1'000'000; + microseconds = (min % 1'000'000) / 1'000; + nanoseconds = (min % 1'000); + } + + // Step 11. + else if (parsed.minutesFraction) { + MOZ_ASSERT(parsed.minutesFraction > 0); + MOZ_ASSERT(parsed.minutesFraction < 1'000'000'000); + + // Step 11.a. + MOZ_ASSERT(parsed.seconds == 0); + MOZ_ASSERT(parsed.secondsFraction == 0); + + // Step 10. + minutes = parsed.minutes; + + // Steps 11.b-d and 15-17. + int64_t min = int64_t(parsed.minutesFraction) * 60; + seconds = min / 1'000'000'000; + milliseconds = (min % 1'000'000'000) / 1'000'000; + microseconds = (min % 1'000'000) / 1'000; + nanoseconds = (min % 1'000); + } + + // Step 14. + else if (parsed.secondsFraction) { + MOZ_ASSERT(parsed.secondsFraction > 0); + MOZ_ASSERT(parsed.secondsFraction < 1'000'000'000); + + // Step 10. + minutes = parsed.minutes; + + // Step 12. + seconds = parsed.seconds; + + // Steps 14, 16-17 + milliseconds = (parsed.secondsFraction / 1'000'000); + microseconds = ((parsed.secondsFraction % 1'000'000) / 1'000); + nanoseconds = (parsed.secondsFraction % 1'000); + } else { + // Step 10. + minutes = parsed.minutes; + + // Step 12. + seconds = parsed.seconds; + + // Steps 15-17 + milliseconds = 0; + microseconds = 0; + nanoseconds = 0; + } + + // Steps 18-19. + int32_t factor = parsed.sign ? parsed.sign : 1; + MOZ_ASSERT(factor == -1 || factor == 1); + + // Step 20. + *result = { + (years * factor) + (+0.0), (months * factor) + (+0.0), + (weeks * factor) + (+0.0), (days * factor) + (+0.0), + (hours * factor) + (+0.0), (minutes * factor) + (+0.0), + (seconds * factor) + (+0.0), (milliseconds * factor) + (+0.0), + (microseconds * factor) + (+0.0), (nanoseconds * factor) + (+0.0), + }; + if (!ThrowIfInvalidDuration(cx, *result)) { + return false; + } + return true; +} + +template <typename CharT> +mozilla::Result<AnnotationKey, ParserError> +TemporalParser<CharT>::annotationKey() { + // AnnotationKey : + // AKeyLeadingChar + // AnnotationKey AKeyChar + + size_t start = reader_.index(); + + if (!aKeyLeadingChar()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_ANNOTATION_KEY); + } + + // Optionally followed by a sequence of |AKeyChar|. + while (aKeyChar()) { + } + + return AnnotationKey{start, reader_.index() - start}; +} + +template <typename CharT> +mozilla::Result<AnnotationValue, ParserError> +TemporalParser<CharT>::annotationValue() { + // AnnotationValue : + // AnnotationValueComponent + // AnnotationValueComponent - AnnotationValue + + size_t start = reader_.index(); + + do { + if (!annotationValueComponent()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_ANNOTATION_VALUE); + } + } while (character('-')); + + return AnnotationValue{start, reader_.index() - start}; +} + +template <typename CharT> +mozilla::Result<Annotation, ParserError> TemporalParser<CharT>::annotation() { + // Annotation : + // [ AnnotationCriticalFlag? AnnotationKey = AnnotationValue ] + + if (!character('[')) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_BEFORE_ANNOTATION); + } + + bool critical = annotationCriticalFlag(); + + auto key = annotationKey(); + if (key.isErr()) { + return key.propagateErr(); + } + + if (!character('=')) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_ASSIGNMENT_IN_ANNOTATION); + } + + auto value = annotationValue(); + if (value.isErr()) { + return value.propagateErr(); + } + + if (!character(']')) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_BRACKET_AFTER_ANNOTATION); + } + + return Annotation{key.unwrap(), value.unwrap(), critical}; +} + +template <typename CharT> +mozilla::Result<CalendarName, ParserError> +TemporalParser<CharT>::annotations() { + // Annotations: + // Annotation Annotations? + + MOZ_ASSERT(hasAnnotationStart()); + + CalendarName calendar; + bool calendarWasCritical = false; + while (hasAnnotationStart()) { + auto anno = annotation(); + if (anno.isErr()) { + return anno.propagateErr(); + } + auto [key, value, critical] = anno.unwrap(); + + // FIXME: spec issue - ignore case for "[u-ca=" to match BCP47? + // https://github.com/tc39/proposal-temporal/issues/2524 + + static constexpr std::string_view ca = "u-ca"; + + auto keySpan = reader_.substring(key); + if (keySpan.size() == ca.length() && + std::equal(ca.begin(), ca.end(), keySpan.data())) { + if (!calendar.present()) { + calendar = value; + calendarWasCritical = critical; + } else if (critical || calendarWasCritical) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_CRITICAL_ANNOTATION); + } + } else if (critical) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_CRITICAL_ANNOTATION); + } + } + return calendar; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::annotatedTime() { + // clang-format off + // + // AnnotatedTime : + // TimeDesignator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? + // TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation? Annotations? + // + // clang-format on + + if (timeDesignator()) { + ZonedDateTimeString result = {}; + + auto time = timeSpec(); + if (time.isErr()) { + return time.propagateErr(); + } + result.time = time.unwrap(); + + if (hasDateTimeUTCOffsetStart()) { + auto tz = dateTimeUTCOffset(); + if (tz.isErr()) { + return tz.propagateErr(); + } + result.timeZone = tz.unwrap(); + } + + if (hasTimeZoneAnnotationStart()) { + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + } + + if (hasAnnotationStart()) { + auto cal = annotations(); + if (cal.isErr()) { + return cal.propagateErr(); + } + result.calendar = cal.unwrap(); + } + + return result; + } + + // clang-format off + // + // TimeSpecWithOptionalOffsetNotAmbiguous : + // TimeSpec DateTimeUTCOffset? but not one of ValidMonthDay or DateSpecYearMonth + // + // clang-format on + + size_t start = reader_.index(); + + ZonedDateTimeString result = {}; + + auto time = timeSpec(); + if (time.isErr()) { + return time.propagateErr(); + } + result.time = time.unwrap(); + + if (hasDateTimeUTCOffsetStart()) { + auto tz = dateTimeUTCOffset(); + if (tz.isErr()) { + return tz.propagateErr(); + } + result.timeZone = tz.unwrap(); + } + + size_t end = reader_.index(); + + // Reset and check if the input can also be parsed as ValidMonthDay. + reader_.reset(start); + + if (validMonthDay().isOk()) { + if (reader_.index() == end) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_AMBIGUOUS_TIME_MONTH_DAY); + } + } + + // Reset and check if the input can also be parsed as DateSpecYearMonth. + reader_.reset(start); + + if (dateSpecYearMonth().isOk()) { + if (reader_.index() == end) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_AMBIGUOUS_TIME_YEAR_MONTH); + } + } + + // Input can neither be parsed as ValidMonthDay nor DateSpecYearMonth. + reader_.reset(end); + + if (hasTimeZoneAnnotationStart()) { + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + } + + if (hasAnnotationStart()) { + auto cal = annotations(); + if (cal.isErr()) { + return cal.propagateErr(); + } + result.calendar = cal.unwrap(); + } + + return result; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::annotatedDateTime() { + // AnnotatedDateTime[Zoned] : + // [~Zoned] DateTime TimeZoneAnnotation? Annotations? + // [+Zoned] DateTime TimeZoneAnnotation Annotations? + + auto dt = dateTime(); + if (dt.isErr()) { + return dt.propagateErr(); + } + auto result = dt.unwrap(); + + if (hasTimeZoneAnnotationStart()) { + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + } + + if (hasAnnotationStart()) { + auto cal = annotations(); + if (cal.isErr()) { + return cal.propagateErr(); + } + result.calendar = cal.unwrap(); + } + + return result; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::annotatedDateTimeTimeRequired() { + // clang-format off + // + // AnnotatedDateTimeTimeRequired : + // Date DateTimeSeparator TimeSpec DateTimeUTCOffset? TimeZoneAnnotation? Annotations? + // + // clang-format on + + ZonedDateTimeString result = {}; + + auto dt = date(); + if (dt.isErr()) { + return dt.propagateErr(); + } + result.date = dt.unwrap(); + + if (!dateTimeSeparator()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DATE_TIME_SEPARATOR); + } + + auto time = timeSpec(); + if (time.isErr()) { + return time.propagateErr(); + } + result.time = time.unwrap(); + + if (hasDateTimeUTCOffsetStart()) { + auto tz = dateTimeUTCOffset(); + if (tz.isErr()) { + return tz.propagateErr(); + } + result.timeZone = tz.unwrap(); + } + + if (hasTimeZoneAnnotationStart()) { + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + } + + if (hasAnnotationStart()) { + auto cal = annotations(); + if (cal.isErr()) { + return cal.propagateErr(); + } + result.calendar = cal.unwrap(); + } + + return result; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::annotatedYearMonth() { + // AnnotatedYearMonth : + // DateSpecYearMonth TimeZoneAnnotation? Annotations? + + ZonedDateTimeString result = {}; + + auto yearMonth = dateSpecYearMonth(); + if (yearMonth.isErr()) { + return yearMonth.propagateErr(); + } + result.date = yearMonth.unwrap(); + + if (hasTimeZoneAnnotationStart()) { + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + } + + if (hasAnnotationStart()) { + auto cal = annotations(); + if (cal.isErr()) { + return cal.propagateErr(); + } + result.calendar = cal.unwrap(); + } + + return result; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::annotatedMonthDay() { + // AnnotatedMonthDay : + // DateSpecMonthDay TimeZoneAnnotation? Annotations? + + ZonedDateTimeString result = {}; + + auto monthDay = dateSpecMonthDay(); + if (monthDay.isErr()) { + return monthDay.propagateErr(); + } + result.date = monthDay.unwrap(); + + if (hasTimeZoneAnnotationStart()) { + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + } + + if (hasAnnotationStart()) { + auto cal = annotations(); + if (cal.isErr()) { + return cal.propagateErr(); + } + result.calendar = cal.unwrap(); + } + + return result; +} + +template <typename CharT> +mozilla::Result<PlainDate, ParserError> +TemporalParser<CharT>::dateSpecYearMonth() { + // DateSpecYearMonth : + // DateYear -? DateMonth + PlainDate result = {}; + + // DateYear : + // DecimalDigit{4} + // Sign DecimalDigit{6} + if (auto year = digits(4)) { + result.year = year.value(); + } else if (hasSign()) { + int32_t yearSign = sign(); + if (auto year = digits(6)) { + result.year = yearSign * year.value(); + if (yearSign < 0 && result.year == 0) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_NEGATIVE_ZERO_YEAR); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_EXTENDED_YEAR); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_YEAR); + } + + character('-'); + + // DateMonth : + // 0 NonzeroDigit + // 10 + // 11 + // 12 + if (auto month = digits(2)) { + result.month = month.value(); + if (!inBounds(result.month, 1, 12)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); + } + + // Absent days default to 1, cf. ParseISODateTime. + result.day = 1; + + return result; +} + +template <typename CharT> +mozilla::Result<PlainDate, ParserError> +TemporalParser<CharT>::dateSpecMonthDay() { + // DateSpecMonthDay : + // -- DateMonth -? DateDay + // DateMonth -? DateDay + PlainDate result = {}; + + string("--"); + + result.year = AbsentYear; + + // DateMonth : + // 0 NonzeroDigit + // 10 + // 11 + // 12 + if (auto month = digits(2)) { + result.month = month.value(); + if (!inBounds(result.month, 1, 12)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); + } + + character('-'); + + // DateDay : + // 0 NonzeroDigit + // 1 DecimalDigit + // 2 DecimalDigit + // 30 + // 31 + if (auto day = digits(2)) { + result.day = day.value(); + if (!inBounds(result.day, 1, 31)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DAY); + } + + return result; +} + +template <typename CharT> +mozilla::Result<PlainDate, ParserError> TemporalParser<CharT>::validMonthDay() { + // ValidMonthDay : + // DateMonth -? 0 NonZeroDigit + // DateMonth -? 1 DecimalDigit + // DateMonth -? 2 DecimalDigit + // DateMonth -? 30 but not one of 0230 or 02-30 + // DateMonthWithThirtyOneDays -? 31 + // + // DateMonthWithThirtyOneDays : one of + // 01 03 05 07 08 10 12 + + PlainDate result = {}; + + // DateMonth : + // 0 NonzeroDigit + // 10 + // 11 + // 12 + if (auto month = digits(2)) { + result.month = month.value(); + if (!inBounds(result.month, 1, 12)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_MONTH); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_MONTH); + } + + character('-'); + + if (auto day = digits(2)) { + result.day = day.value(); + if (!inBounds(result.day, 1, 31)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); + } + } else { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MISSING_DAY); + } + + if (result.month == 2 && result.day > 29) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); + } + + if (result.day > 30) { + MOZ_ASSERT(result.day == 31); + + static constexpr int32_t monthsWithThirtyOneDays[] = { + 1, 3, 5, 7, 8, 10, 12, + }; + + if (std::find(std::begin(monthsWithThirtyOneDays), + std::end(monthsWithThirtyOneDays), + result.month) == std::end(monthsWithThirtyOneDays)) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_INVALID_DAY); + } + } + + return result; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalCalendarString() { + // Handle the common case of a standalone calendar name first. + // + // All valid calendar names start with two alphabetic characters and none of + // the ParseISODateTime parse goals can start with two alphabetic characters. + // TemporalTimeString can start with 'T', so we can't only check the first + // character. + if (hasTwoAsciiAlpha()) { + auto cal = annotationValue(); + if (cal.isErr()) { + return cal.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + + ZonedDateTimeString result = {}; + result.calendar = cal.unwrap(); + return result; + } + + // Try all five parse goals from ParseISODateTime in order. + // + // TemporalDateTimeString + // TemporalInstantString + // TemporalTimeString + // TemporalZonedDateTimeString + // TemporalMonthDayString + // TemporalYearMonthString + + if (auto dt = parseTemporalDateTimeString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalInstantString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalTimeString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalMonthDayString(); dt.isOk()) { + return dt.unwrap(); + } + + // Restart parsing from the start of the string. + reader_.reset(); + + if (auto dt = parseTemporalYearMonthString(); dt.isOk()) { + return dt.unwrap(); + } else { + return dt.propagateErr(); + } +} + +/** + * ParseTemporalCalendarString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalCalendarString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalCalendarString(); +} + +/** + * ParseTemporalCalendarString ( isoString ) + */ +static auto ParseTemporalCalendarString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalCalendarString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalCalendarString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalCalendarString ( isoString ) + */ +JSLinearString* js::temporal::ParseTemporalCalendarString( + JSContext* cx, Handle<JSString*> str) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return nullptr; + } + + // Steps 1-3. + auto parseResult = ::ParseTemporalCalendarString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return nullptr; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + PlainDateTime unused; + if (!ParseISODateTime(cx, parsed, &unused)) { + return nullptr; + } + + // Step 2.b. + if (!parsed.calendar.present()) { + return cx->names().iso8601; + } + + // Steps 2.c and 3.c + return ToString(cx, linear, parsed.calendar); +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalTimeString() { + // TemporalTimeString : + // AnnotatedTime + // AnnotatedDateTimeTimeRequired + + if (auto time = annotatedTime(); time.isOk() && reader_.atEnd()) { + return time.unwrap(); + } + + // Reset and try the next option. + reader_.reset(); + + auto dt = annotatedDateTimeTimeRequired(); + if (dt.isErr()) { + return dt.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + return dt.unwrap(); +} + +/** + * ParseTemporalTimeString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalTimeString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalTimeString(); +} + +/** + * ParseTemporalTimeString ( isoString ) + */ +static auto ParseTemporalTimeString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalTimeString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalTimeString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalTimeString ( isoString ) + */ +bool js::temporal::ParseTemporalTimeString(JSContext* cx, Handle<JSString*> str, + PlainTime* result) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-2. + auto parseResult = ::ParseTemporalTimeString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + // Step 3. + if (parsed.timeZone.isUTC()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); + return false; + } + + // Step 4. + PlainDateTime dateTime; + if (!ParseISODateTime(cx, parsed, &dateTime)) { + return false; + } + *result = dateTime.time; + + // Step 5. + return true; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalMonthDayString() { + // TemporalMonthDayString : + // AnnotatedMonthDay + // AnnotatedDateTime[~Zoned] + + if (auto monthDay = annotatedMonthDay(); monthDay.isOk() && reader_.atEnd()) { + auto result = monthDay.unwrap(); + + // ParseISODateTime, step 3. + if (result.calendar.present() && + !IsISO8601Calendar(reader_.substring(result.calendar))) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_MONTH_DAY_CALENDAR_NOT_ISO8601); + } + return result; + } + + // Reset and try the next option. + reader_.reset(); + + auto dt = annotatedDateTime(); + if (dt.isErr()) { + return dt.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + return dt.unwrap(); +} + +/** + * ParseTemporalMonthDayString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalMonthDayString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalMonthDayString(); +} + +/** + * ParseTemporalMonthDayString ( isoString ) + */ +static auto ParseTemporalMonthDayString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalMonthDayString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalMonthDayString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalMonthDayString ( isoString ) + */ +bool js::temporal::ParseTemporalMonthDayString( + JSContext* cx, Handle<JSString*> str, PlainDate* result, bool* hasYear, + MutableHandle<JSString*> calendar) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-2 . + auto parseResult = ::ParseTemporalMonthDayString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + // Step 3. + if (parsed.timeZone.isUTC()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); + return false; + } + + // Step 4. + PlainDateTime dateTime; + if (!ParseISODateTime(cx, parsed, &dateTime)) { + return false; + } + *result = dateTime.date; + + // Steps 5-6. + *hasYear = parsed.date.year != AbsentYear; + + if (parsed.calendar.present()) { + calendar.set(ToString(cx, linear, parsed.calendar)); + if (!calendar) { + return false; + } + } + + // Step 7. + return true; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalYearMonthString() { + // TemporalYearMonthString : + // AnnotatedYearMonth + // AnnotatedDateTime[~Zoned] + + if (auto yearMonth = annotatedYearMonth(); + yearMonth.isOk() && reader_.atEnd()) { + auto result = yearMonth.unwrap(); + + // ParseISODateTime, step 3. + if (result.calendar.present() && + !IsISO8601Calendar(reader_.substring(result.calendar))) { + return mozilla::Err( + JSMSG_TEMPORAL_PARSER_YEAR_MONTH_CALENDAR_NOT_ISO8601); + } + return result; + } + + // Reset and try the next option. + reader_.reset(); + + auto dt = annotatedDateTime(); + if (dt.isErr()) { + return dt.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + return dt.unwrap(); +} + +/** + * ParseTemporalYearMonthString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalYearMonthString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalYearMonthString(); +} + +/** + * ParseTemporalYearMonthString ( isoString ) + */ +static auto ParseTemporalYearMonthString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalYearMonthString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalYearMonthString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalYearMonthString ( isoString ) + */ +bool js::temporal::ParseTemporalYearMonthString( + JSContext* cx, Handle<JSString*> str, PlainDate* result, + MutableHandle<JSString*> calendar) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-2. + auto parseResult = ::ParseTemporalYearMonthString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + // Step 3. + if (parsed.timeZone.isUTC()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); + return false; + } + + // Step 4. + PlainDateTime dateTime; + if (!ParseISODateTime(cx, parsed, &dateTime)) { + return false; + } + *result = dateTime.date; + + if (parsed.calendar.present()) { + calendar.set(ToString(cx, linear, parsed.calendar)); + if (!calendar) { + return false; + } + } + + // Step 5. + return true; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalDateTimeString() { + // TemporalDateTimeString[Zoned] : + // AnnotatedDateTime[?Zoned] + + auto dateTime = annotatedDateTime(); + if (dateTime.isErr()) { + return dateTime.propagateErr(); + } + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + return dateTime.unwrap(); +} + +/** + * ParseTemporalDateTimeString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalDateTimeString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalDateTimeString(); +} + +/** + * ParseTemporalDateTimeString ( isoString ) + */ +static auto ParseTemporalDateTimeString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalDateTimeString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalDateTimeString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalDateTimeString ( isoString ) + */ +bool js::temporal::ParseTemporalDateTimeString( + JSContext* cx, Handle<JSString*> str, PlainDateTime* result, + MutableHandle<JSString*> calendar) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-2. + auto parseResult = ::ParseTemporalDateTimeString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + // Step 3. + if (parsed.timeZone.isUTC()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR); + return false; + } + + // Step 4. + if (!ParseISODateTime(cx, parsed, result)) { + return false; + } + + if (parsed.calendar.present()) { + calendar.set(ToString(cx, linear, parsed.calendar)); + if (!calendar) { + return false; + } + } + + return true; +} + +/** + * ParseTemporalDateString ( isoString ) + */ +bool js::temporal::ParseTemporalDateString(JSContext* cx, Handle<JSString*> str, + PlainDate* result, + MutableHandle<JSString*> calendar) { + // Step 1. + PlainDateTime dateTime; + if (!ParseTemporalDateTimeString(cx, str, &dateTime, calendar)) { + return false; + } + + // Step 2. + *result = dateTime.date; + return true; +} + +template <typename CharT> +mozilla::Result<ZonedDateTimeString, ParserError> +TemporalParser<CharT>::parseTemporalZonedDateTimeString() { + // Parse goal: TemporalDateTimeString[+Zoned] + // + // TemporalDateTimeString[Zoned] : + // AnnotatedDateTime[?Zoned] + // + // AnnotatedDateTime[Zoned] : + // [~Zoned] DateTime TimeZoneAnnotation? Annotations? + // [+Zoned] DateTime TimeZoneAnnotation Annotations? + + auto dt = dateTime(); + if (dt.isErr()) { + return dt.propagateErr(); + } + auto result = dt.unwrap(); + + auto annotation = timeZoneAnnotation(); + if (annotation.isErr()) { + return annotation.propagateErr(); + } + result.timeZone.annotation = annotation.unwrap(); + + if (hasAnnotationStart()) { + auto cal = annotations(); + if (cal.isErr()) { + return cal.propagateErr(); + } + result.calendar = cal.unwrap(); + } + + if (!reader_.atEnd()) { + return mozilla::Err(JSMSG_TEMPORAL_PARSER_GARBAGE_AFTER_INPUT); + } + + return result; +} + +/** + * ParseTemporalZonedDateTimeString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalZonedDateTimeString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalZonedDateTimeString(); +} + +/** + * ParseTemporalZonedDateTimeString ( isoString ) + */ +static auto ParseTemporalZonedDateTimeString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalZonedDateTimeString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalZonedDateTimeString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalZonedDateTimeString ( isoString ) + */ +bool js::temporal::ParseTemporalZonedDateTimeString( + JSContext* cx, Handle<JSString*> str, PlainDateTime* dateTime, bool* isUTC, + bool* hasOffset, int64_t* timeZoneOffset, + MutableHandle<ParsedTimeZone> timeZoneName, + MutableHandle<JSString*> calendar) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Step 1. + auto parseResult = ::ParseTemporalZonedDateTimeString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + // Step 2. (ParseISODateTime, steps 1-18.) + if (!ParseISODateTime(cx, parsed, dateTime)) { + return false; + } + + // Step 2. (ParseISODateTime, steps 19-21.) + { + MOZ_ASSERT(parsed.timeZone.hasAnnotation()); + + // Case 1: 19700101T00:00Z[+02:00] + // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "+02:00" } + // + // Case 2: 19700101T00:00+02:00[+02:00] + // { [[Z]]: false, [[OffsetString]]: "+02:00", [[Name]]: "+02:00" } + // + // Case 3: 19700101[+02:00] + // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "+02:00" } + // + // Case 4: 19700101T00:00Z[Europe/Berlin] + // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } + // + // Case 5: 19700101T00:00+01:00[Europe/Berlin] + // { [[Z]]: false, [[OffsetString]]: "+01:00", [[Name]]: "Europe/Berlin" } + // + // Case 6: 19700101[Europe/Berlin] + // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } + + const auto& annotation = parsed.timeZone.annotation; + if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneName)) { + return false; + } + + if (parsed.timeZone.isUTC()) { + *isUTC = true; + *hasOffset = false; + *timeZoneOffset = 0; + } else if (parsed.timeZone.hasOffset()) { + *isUTC = false; + *hasOffset = true; + *timeZoneOffset = ParseDateTimeUTCOffset(parsed.timeZone.offset); + } else { + *isUTC = false; + *hasOffset = false; + *timeZoneOffset = 0; + } + } + + // Step 2. (ParseISODateTime, steps 23-24.) + if (parsed.calendar.present()) { + calendar.set(ToString(cx, linear, parsed.calendar)); + if (!calendar) { + return false; + } + } + + // Step 2. (ParseISODateTime, step 25.) + return true; +} + +/** + * ParseTemporalRelativeToString ( isoString ) + */ +template <typename CharT> +static auto ParseTemporalRelativeToString(mozilla::Span<const CharT> str) { + TemporalParser<CharT> parser(str); + return parser.parseTemporalDateTimeString(); +} + +/** + * ParseTemporalRelativeToString ( isoString ) + */ +static auto ParseTemporalRelativeToString(Handle<JSLinearString*> str) { + JS::AutoCheckCannotGC nogc; + if (str->hasLatin1Chars()) { + return ParseTemporalRelativeToString<Latin1Char>(str->latin1Range(nogc)); + } + return ParseTemporalRelativeToString<char16_t>(str->twoByteRange(nogc)); +} + +/** + * ParseTemporalRelativeToString ( isoString ) + */ +bool js::temporal::ParseTemporalRelativeToString( + JSContext* cx, Handle<JSString*> str, PlainDateTime* dateTime, bool* isUTC, + bool* hasOffset, int64_t* timeZoneOffset, + MutableHandle<ParsedTimeZone> timeZoneName, + MutableHandle<JSString*> calendar) { + Rooted<JSLinearString*> linear(cx, str->ensureLinear(cx)); + if (!linear) { + return false; + } + + // Steps 1-2. + auto parseResult = ::ParseTemporalRelativeToString(linear); + if (parseResult.isErr()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + parseResult.unwrapErr()); + return false; + } + ZonedDateTimeString parsed = parseResult.unwrap(); + + // Step 3. + if (parsed.timeZone.isUTC() && !parsed.timeZone.hasAnnotation()) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_PARSER_INVALID_UTC_DESIGNATOR_WITHOUT_NAME); + return false; + } + + // Step 4. (ParseISODateTime, steps 1-18.) + if (!ParseISODateTime(cx, parsed, dateTime)) { + return false; + } + + // Step 4. (ParseISODateTime, steps 19-22.) + if (parsed.timeZone.hasAnnotation()) { + // Case 1: 19700101Z[+02:00] + // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "+02:00" } + // + // Case 2: 19700101+00:00[+02:00] + // { [[Z]]: false, [[OffsetString]]: "+00:00", [[Name]]: "+02:00" } + // + // Case 3: 19700101[+02:00] + // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "+02:00" } + // + // Case 4: 19700101Z[Europe/Berlin] + // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } + // + // Case 5: 19700101+00:00[Europe/Berlin] + // { [[Z]]: false, [[OffsetString]]: "+00:00", [[Name]]: "Europe/Berlin" } + // + // Case 6: 19700101[Europe/Berlin] + // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } + + const auto& annotation = parsed.timeZone.annotation; + if (!ParseTimeZoneAnnotation(cx, annotation, linear, timeZoneName)) { + return false; + } + + if (parsed.timeZone.isUTC()) { + *isUTC = true; + *hasOffset = false; + *timeZoneOffset = 0; + } else if (parsed.timeZone.hasOffset()) { + *isUTC = false; + *hasOffset = true; + *timeZoneOffset = ParseDateTimeUTCOffset(parsed.timeZone.offset); + } else { + *isUTC = false; + *hasOffset = false; + *timeZoneOffset = 0; + } + } else { + // ToRelativeTemporalObject ignores any other time zone information when no + // bracketed time zone annotation is present. + + *isUTC = false; + *hasOffset = false; + *timeZoneOffset = 0; + timeZoneName.set(ParsedTimeZone{}); + } + + // Step 4. (ParseISODateTime, steps 23-24.) + if (parsed.calendar.present()) { + calendar.set(ToString(cx, linear, parsed.calendar)); + if (!calendar) { + return false; + } + } + + // Step 4. (Return) + return true; +} + +void js::temporal::ParsedTimeZone::trace(JSTracer* trc) { + TraceNullableRoot(trc, &name, "ParsedTimeZone::name"); +} diff --git a/js/src/builtin/temporal/TemporalParser.h b/js/src/builtin/temporal/TemporalParser.h new file mode 100644 index 0000000000..677a90b58d --- /dev/null +++ b/js/src/builtin/temporal/TemporalParser.h @@ -0,0 +1,166 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_TemporalParser_h +#define builtin_temporal_TemporalParser_h + +#include "mozilla/Assertions.h" + +#include <cstdlib> +#include <stdint.h> + +#include "builtin/temporal/TemporalUnit.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" + +class JSLinearString; +class JS_PUBLIC_API JSTracer; + +namespace js::temporal { + +struct Duration; +struct PlainDate; +struct PlainDateTime; +struct PlainTime; + +struct ParsedTimeZone { + JSLinearString* name = nullptr; + int32_t offset = INT32_MIN; + + void trace(JSTracer* trc); + + static ParsedTimeZone fromName(JSLinearString* name) { + MOZ_ASSERT(name); + return {name, 0}; + } + + static ParsedTimeZone fromOffset(int32_t offset) { + MOZ_ASSERT(std::abs(offset) < UnitsPerDay(TemporalUnit::Minute)); + return {nullptr, offset}; + } + + explicit operator bool() const { + return name != nullptr || offset != INT32_MIN; + } +}; + +/** + * ParseTemporalInstantString ( isoString ) + */ +bool ParseTemporalInstantString(JSContext* cx, JS::Handle<JSString*> str, + PlainDateTime* result, int64_t* offset); + +/** + * ParseTemporalTimeZoneString ( timeZoneString ) + */ +bool ParseTemporalTimeZoneString(JSContext* cx, JS::Handle<JSString*> str, + JS::MutableHandle<ParsedTimeZone> result); + +/** + * ParseTimeZoneIdentifier ( identifier ) + */ +bool ParseTimeZoneIdentifier(JSContext* cx, JS::Handle<JSString*> str, + JS::MutableHandle<ParsedTimeZone> result); + +/** + * ParseTimeZoneOffsetString ( isoString ) + */ +bool ParseTimeZoneOffsetString(JSContext* cx, JS::Handle<JSString*> str, + int32_t* result); + +/** + * ParseDateTimeUTCOffset ( offsetString ) + */ +bool ParseDateTimeUTCOffset(JSContext* cx, JS::Handle<JSString*> str, + int64_t* result); + +/** + * ParseTemporalDurationString ( isoString ) + */ +bool ParseTemporalDurationString(JSContext* cx, JS::Handle<JSString*> str, + Duration* result); + +/** + * ParseTemporalCalendarString ( isoString ) + */ +JSLinearString* ParseTemporalCalendarString(JSContext* cx, + JS::Handle<JSString*> str); + +/** + * ParseTemporalTimeString ( isoString ) + */ +bool ParseTemporalTimeString(JSContext* cx, JS::Handle<JSString*> str, + PlainTime* result); + +/** + * ParseTemporalDateString ( isoString ) + */ +bool ParseTemporalDateString(JSContext* cx, JS::Handle<JSString*> str, + PlainDate* result, + JS::MutableHandle<JSString*> calendar); + +/** + * ParseTemporalMonthDayString ( isoString ) + */ +bool ParseTemporalMonthDayString(JSContext* cx, JS::Handle<JSString*> str, + PlainDate* result, bool* hasYear, + JS::MutableHandle<JSString*> calendar); + +/** + * ParseTemporalYearMonthString ( isoString ) + */ +bool ParseTemporalYearMonthString(JSContext* cx, JS::Handle<JSString*> str, + PlainDate* result, + JS::MutableHandle<JSString*> calendar); + +/** + * ParseTemporalDateTimeString ( isoString ) + */ +bool ParseTemporalDateTimeString(JSContext* cx, JS::Handle<JSString*> str, + PlainDateTime* result, + JS::MutableHandle<JSString*> calendar); + +/** + * ParseTemporalZonedDateTimeString ( isoString ) + */ +bool ParseTemporalZonedDateTimeString( + JSContext* cx, JS::Handle<JSString*> str, PlainDateTime* dateTime, + bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, + JS::MutableHandle<ParsedTimeZone> timeZoneName, + JS::MutableHandle<JSString*> calendar); + +/** + * ParseTemporalRelativeToString ( isoString ) + */ +bool ParseTemporalRelativeToString( + JSContext* cx, JS::Handle<JSString*> str, PlainDateTime* dateTime, + bool* isUTC, bool* hasOffset, int64_t* timeZoneOffset, + JS::MutableHandle<ParsedTimeZone> timeZoneName, + JS::MutableHandle<JSString*> calendar); + +} /* namespace js::temporal */ + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<temporal::ParsedTimeZone, Wrapper> { + const auto& object() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + JS::Handle<JSLinearString*> name() const { + return JS::Handle<JSLinearString*>::fromMarkedLocation(&object().name); + } + + int32_t offset() const { return object().offset; } + + explicit operator bool() const { return bool(object()); } +}; + +} /* namespace js */ + +#endif /* builtin_temporal_TemporalParser_h */ diff --git a/js/src/builtin/temporal/TemporalRoundingMode.h b/js/src/builtin/temporal/TemporalRoundingMode.h new file mode 100644 index 0000000000..91ef758fc6 --- /dev/null +++ b/js/src/builtin/temporal/TemporalRoundingMode.h @@ -0,0 +1,433 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_TemporalRoundingMode_h +#define builtin_temporal_TemporalRoundingMode_h + +#include "mozilla/Assertions.h" + +#include <cmath> +#include <stdint.h> + +namespace js::temporal { + +// Overview of integer rounding modes is available at +// <https://en.wikipedia.org/wiki/Rounding#Rounding_to_integer>. +enum class TemporalRoundingMode { + // 1. Directed rounding to an integer. + + // Round toward positive infinity. + Ceil, + + // Round toward negative infinity. + Floor, + + // Round toward infinity or round away from zero. + Expand, + + // Round toward zero or round away from infinity. + Trunc, + + // 2. Rounding to the nearest integer. + + // Round half toward positive infinity. + HalfCeil, + + // Round half toward negative infinity. + HalfFloor, + + // Round half toward infinity or round half away from zero. + HalfExpand, + + // Round half toward zero or round half away from infinity. + HalfTrunc, + + // Round half to even. + HalfEven, +}; + +/** + * NegateTemporalRoundingMode ( roundingMode ) + */ +constexpr auto NegateTemporalRoundingMode(TemporalRoundingMode roundingMode) { + // Steps 1-5. + switch (roundingMode) { + case TemporalRoundingMode::Ceil: + return TemporalRoundingMode::Floor; + + case TemporalRoundingMode::Floor: + return TemporalRoundingMode::Ceil; + + case TemporalRoundingMode::HalfCeil: + return TemporalRoundingMode::HalfFloor; + + case TemporalRoundingMode::HalfFloor: + return TemporalRoundingMode::HalfCeil; + + case TemporalRoundingMode::Expand: + case TemporalRoundingMode::Trunc: + case TemporalRoundingMode::HalfExpand: + case TemporalRoundingMode::HalfTrunc: + case TemporalRoundingMode::HalfEven: + return roundingMode; + } + MOZ_CRASH("invalid rounding mode"); +} + +/** + * Adjust the rounding mode to round negative values in the same direction as + * positive values. + */ +constexpr auto ToPositiveRoundingMode(TemporalRoundingMode roundingMode) { + switch (roundingMode) { + case TemporalRoundingMode::Ceil: + case TemporalRoundingMode::Floor: + case TemporalRoundingMode::HalfCeil: + case TemporalRoundingMode::HalfFloor: + case TemporalRoundingMode::HalfEven: + // (Half-)Ceil/Floor round toward the same infinity for negative and + // positive values, so the rounding mode doesn't need to be adjusted. The + // same applies for half-even rounding. + return roundingMode; + + case TemporalRoundingMode::Expand: + // Expand rounds positive values toward +infinity, but negative values + // toward -infinity. Adjust the rounding mode to Ceil to round negative + // values in the same direction as positive values. + return TemporalRoundingMode::Ceil; + + case TemporalRoundingMode::Trunc: + // Truncation rounds positive values down toward zero, but negative values + // up toward zero. Adjust the rounding mode to Floor to round negative + // values in the same direction as positive values. + return TemporalRoundingMode::Floor; + + case TemporalRoundingMode::HalfExpand: + // Adjust the rounding mode to Half-Ceil, similar to the Expand case. + return TemporalRoundingMode::HalfCeil; + + case TemporalRoundingMode::HalfTrunc: + // Adjust the rounding mode to Half-Floor, similar to the Trunc case. + return TemporalRoundingMode::HalfFloor; + } + MOZ_CRASH("unexpected rounding mode"); +} + +// Temporal performs division on "mathematical values" [1] with implies using +// infinite precision. This rules out using IEE-754 floating point types like +// `double`. It also means we can't implement the algorithms from the +// specification verbatim, but instead have to translate them into equivalent +// operations. +// +// Throughout the following division functions, the divisor is required to be +// positive. This allows to simplify the implementation, because it ensures +// non-zero quotient and remainder values have the same sign as the dividend. +// +// [1] https://tc39.es/ecma262/#mathematical-value + +/** + * Compute ceiling division โdividend / divisorโ. The divisor must be a positive + * number. + */ +constexpr int64_t CeilDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // Ceiling division rounds the quotient toward positive infinity. When the + // quotient is negative, this is equivalent to rounding toward zero. See [1]. + // + // int64_t division truncates, so rounding toward zero for negative quotients + // is already covered. When there is a non-zero positive remainder, the + // quotient is positive and we have to increment it by one to implement + // rounding toward positive infinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder > 0) { + quotient += 1; + } + return quotient; +} + +/** + * Compute floor division โdividend / divisorโ. The divisor must be a positive + * number. + */ +constexpr int64_t FloorDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // Floor division rounds the quotient toward negative infinity. When the + // quotient is positive, this is equivalent to rounding toward zero. See [1]. + // + // int64_t division truncates, so rounding toward zero for positive quotients + // is already covered. When there is a non-zero negative remainder, the + // quotient is negative and we have to decrement it by one to implement + // rounding toward negative infinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder < 0) { + quotient -= 1; + } + return quotient; +} + +/** + * Compute "round toward infinity" division `dividend / divisor`. The divisor + * must be a positive number. + */ +constexpr int64_t ExpandDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // "Round toward infinity" division rounds positive quotients toward positive + // infinity and negative quotients toward negative infinity. See [1]. + // + // When there is a non-zero positive remainder, the quotient is positive and + // we have to increment it by one to implement rounding toward positive + // infinity. When there is a non-zero negative remainder, the quotient is + // negative and we have to decrement it by one to implement rounding toward + // negative infinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder > 0) { + quotient += 1; + } + if (remainder < 0) { + quotient -= 1; + } + return quotient; +} + +/** + * Compute truncating division `dividend / divisor`. The divisor must be a + * positive number. + */ +constexpr int64_t TruncDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // Truncating division rounds both positive and negative quotients toward + // zero, cf. [1]. + // + // int64_t division truncates, so rounding toward zero implicitly happens. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + return dividend / divisor; +} + +/** + * Compute "round half toward positive infinity" division `dividend / divisor`. + * The divisor must be a positive number. + */ +inline int64_t HalfCeilDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // "Round half toward positive infinity" division rounds the quotient toward + // positive infinity when the fractional part of the remainder is โฅ0.5. When + // the quotient is negative, this is equivalent to rounding toward zero + // instead of toward positive infinity. See [1]. + // + // When the remainder is a non-zero positive value, the quotient is positive, + // too. When additionally the fractional part of the remainder is โฅ0.5, we + // have to increment the quotient by one to implement rounding toward positive + // infinity. + // + // int64_t division truncates, so we implicitly round toward zero for negative + // quotients. When the absolute value of the fractional part of the remainder + // is >0.5, we should instead have rounded toward negative infinity, so we + // need to decrement the quotient by one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder > 0 && uint64_t(std::abs(remainder)) * 2 >= uint64_t(divisor)) { + quotient += 1; + } + if (remainder < 0 && uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { + quotient -= 1; + } + return quotient; +} + +/** + * Compute "round half toward negative infinity" division `dividend / divisor`. + * The divisor must be a positive number. + */ +inline int64_t HalfFloorDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // "Round half toward negative infinity" division rounds the quotient toward + // negative infinity when the fractional part of the remainder is โฅ0.5. When + // the quotient is positive, this is equivalent to rounding toward zero + // instead of toward negative infinity. See [1]. + // + // When the remainder is a non-zero negative value, the quotient is negative, + // too. When additionally the fractional part of the remainder is โฅ0.5, we + // have to decrement the quotient by one to implement rounding toward negative + // infinity. + // + // int64_t division truncates, so we implicitly round toward zero for positive + // quotients. When the absolute value of the fractional part of the remainder + // is >0.5, we should instead have rounded toward positive infinity, so we + // need to increment the quotient by one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (remainder < 0 && uint64_t(std::abs(remainder)) * 2 >= uint64_t(divisor)) { + quotient -= 1; + } + if (remainder > 0 && uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { + quotient += 1; + } + return quotient; +} + +/** + * Compute "round half toward infinity" division `dividend / divisor`. The + * divisor must be a positive number. + */ +inline int64_t HalfExpandDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // "Round half toward infinity" division rounds positive quotients whose + // remainder has a fractional part โฅ0.5 toward positive infinity. And negative + // quotients whose remainder has a fractional part โฅ0.5 toward negative + // infinity. See [1]. + // + // int64_t division truncates, which means it rounds toward zero, so we have + // to increment resp. decrement the quotient when the fractional part of the + // remainder is โฅ0.5 to round toward ยฑinfinity. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (uint64_t(std::abs(remainder)) * 2 >= uint64_t(divisor)) { + quotient += (dividend > 0) ? 1 : -1; + } + return quotient; +} + +/** + * Compute "round half toward zero" division `dividend / divisor`. The divisor + * must be a positive number. + */ +inline int64_t HalfTruncDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // "Round half toward zero" division rounds both positive and negative + // quotients whose remainder has a fractional part โค0.5 toward zero. See [1]. + // + // int64_t division truncates, so we implicitly round toward zero. When the + // fractional part of the remainder is >0.5, we should instead have rounded + // toward ยฑinfinity, so we need to increment resp. decrement the quotient by + // one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if (uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { + quotient += (dividend > 0) ? 1 : -1; + } + return quotient; +} + +/** + * Compute "round half to even" division `dividend / divisor`. The divisor must + * be a positive number. + */ +inline int64_t HalfEvenDiv(int64_t dividend, int64_t divisor) { + MOZ_ASSERT(divisor > 0, "negative divisor not supported"); + + // NB: Division and modulo operation are fused into a single machine code + // instruction by the compiler. + int64_t quotient = dividend / divisor; + int64_t remainder = dividend % divisor; + + // "Round half to even" division rounds both positive and negative quotients + // to the nearest even integer. See [1]. + // + // int64_t division truncates, so we implicitly round toward zero. When the + // fractional part of the remainder is 0.5 and the quotient is odd or when the + // fractional part of the remainder is >0.5, we should instead have rounded + // toward ยฑinfinity, so we need to increment resp. decrement the quotient by + // one. + // + // [1] + // https://tc39.es/proposal-temporal/#table-temporal-unsigned-rounding-modes + if ((quotient & 1) == 1 && + uint64_t(std::abs(remainder)) * 2 == uint64_t(divisor)) { + quotient += (dividend > 0) ? 1 : -1; + } + if (uint64_t(std::abs(remainder)) * 2 > uint64_t(divisor)) { + quotient += (dividend > 0) ? 1 : -1; + } + return quotient; +} + +/** + * Perform `dividend / divisor` and round the result according to the given + * rounding mode. + */ +inline int64_t Divide(int64_t dividend, int64_t divisor, + TemporalRoundingMode roundingMode) { + switch (roundingMode) { + case TemporalRoundingMode::Ceil: + return CeilDiv(dividend, divisor); + case TemporalRoundingMode::Floor: + return FloorDiv(dividend, divisor); + case TemporalRoundingMode::Expand: + return ExpandDiv(dividend, divisor); + case TemporalRoundingMode::Trunc: + return TruncDiv(dividend, divisor); + case TemporalRoundingMode::HalfCeil: + return HalfCeilDiv(dividend, divisor); + case TemporalRoundingMode::HalfFloor: + return HalfFloorDiv(dividend, divisor); + case TemporalRoundingMode::HalfExpand: + return HalfExpandDiv(dividend, divisor); + case TemporalRoundingMode::HalfTrunc: + return HalfTruncDiv(dividend, divisor); + case TemporalRoundingMode::HalfEven: + return HalfEvenDiv(dividend, divisor); + } + MOZ_CRASH("invalid rounding mode"); +} + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_TemporalRoundingMode_h */ diff --git a/js/src/builtin/temporal/TemporalTypes.h b/js/src/builtin/temporal/TemporalTypes.h new file mode 100644 index 0000000000..654fd65a3b --- /dev/null +++ b/js/src/builtin/temporal/TemporalTypes.h @@ -0,0 +1,546 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_TemporalTypes_h +#define builtin_temporal_TemporalTypes_h + +#include "mozilla/Assertions.h" +#include "mozilla/CheckedInt.h" + +#include <stdint.h> + +#include "builtin/temporal/TemporalUnit.h" + +namespace js::temporal { + +// Use __builtin_assume when available, otherwise fall back to +// __builtin_unreachable. +#if defined __has_builtin +# if __has_builtin(__builtin_assume) +# define JS_ASSUME(x) __builtin_assume(x) +# elif __has_builtin(__builtin_unreachable) +# define JS_ASSUME(x) \ + if (!(x)) __builtin_unreachable() +# else +# endif +#endif + +// Fallback to no-op if neither built-in is available. +#ifndef JS_ASSUME +# define JS_ASSUME(x) \ + do { \ + } while (false) +#endif + +/** + * Struct to represent a seconds and nanoseconds value. The nanoseconds value + * is normalized to the range [0, 999'999'999]. + */ +template <typename Derived> +struct SecondsAndNanoseconds { + // Seconds part in the range allowed by the derived class. + int64_t seconds = 0; + + // Nanoseconds part in the range [0, 999'999'999]. + int32_t nanoseconds = 0; + + constexpr bool operator==(const SecondsAndNanoseconds& other) const { + return seconds == other.seconds && nanoseconds == other.nanoseconds; + } + + constexpr bool operator<(const SecondsAndNanoseconds& other) const { + // The compiler can optimize expressions like |instant < Instant{}| to a + // single right-shift operation when we propagate the range of nanoseconds. + JS_ASSUME(nanoseconds >= 0); + JS_ASSUME(other.nanoseconds >= 0); + return (seconds < other.seconds) || + (seconds == other.seconds && nanoseconds < other.nanoseconds); + } + + // Other operators are implemented in terms of operator== and operator<. + constexpr bool operator!=(const SecondsAndNanoseconds& other) const { + return !(*this == other); + } + constexpr bool operator>(const SecondsAndNanoseconds& other) const { + return other < *this; + } + constexpr bool operator<=(const SecondsAndNanoseconds& other) const { + return !(other < *this); + } + constexpr bool operator>=(const SecondsAndNanoseconds& other) const { + return !(*this < other); + } + + protected: + template <typename T, typename U, class R = Derived> + static constexpr R add(const SecondsAndNanoseconds<T>& self, + const SecondsAndNanoseconds<U>& other) { + // The caller needs to make sure integer overflow won't happen. CheckedInt + // will assert on overflow and we intentionally don't try to recover from + // overflow in this method. + + mozilla::CheckedInt64 secs = self.seconds; + secs += other.seconds; + + mozilla::CheckedInt32 nanos = self.nanoseconds; + nanos += other.nanoseconds; + + if (nanos.value() >= 1'000'000'000) { + secs += 1; + nanos -= 1'000'000'000; + } + MOZ_ASSERT(0 <= nanos.value() && nanos.value() < 1'000'000'000); + + return {secs.value(), nanos.value()}; + } + + template <class T, class U, class R = Derived> + static constexpr R subtract(const SecondsAndNanoseconds<T>& self, + const SecondsAndNanoseconds<U>& other) { + // The caller needs to make sure integer underflow won't happen. CheckedInt + // will assert on underflow and we intentionally don't try to recover from + // underflow in this method. + + mozilla::CheckedInt64 secs = self.seconds; + secs -= other.seconds; + + mozilla::CheckedInt32 nanos = self.nanoseconds; + nanos -= other.nanoseconds; + + if (nanos.value() < 0) { + secs -= 1; + nanos += 1'000'000'000; + } + MOZ_ASSERT(0 <= nanos.value() && nanos.value() < 1'000'000'000); + + return {secs.value(), nanos.value()}; + } + + static constexpr Derived negate(const Derived& self) { + return subtract(Derived{}, self); + } + + public: + /** + * Return the absolute value. + */ + constexpr Derived abs() const { + int64_t sec = seconds; + int32_t nanos = nanoseconds; + if (sec < 0) { + if (nanos > 0) { + sec += 1; + nanos -= 1'000'000'000; + } + sec = -sec; + nanos = -nanos; + } + return {sec, nanos}; + } + + /** + * Return the seconds value, rounded towards zero. + */ + constexpr int64_t toSeconds() const { + int64_t sec = seconds; + int32_t nanos = nanoseconds; + if (sec < 0 && nanos > 0) { + sec += 1; + } + return sec; + } + + /** + * Return the milliseconds value, rounded towards zero. + */ + constexpr int64_t toMilliseconds() const { + int64_t sec = seconds; + int32_t nanos = nanoseconds; + if (sec < 0 && nanos > 0) { + sec += 1; + nanos -= 1'000'000'000; + } + return (sec * 1'000) + (nanos / 1'000'000); + } + + /** + * Return the microseconds value, rounded towards zero. + */ + constexpr int64_t toMicroseconds() const { + int64_t sec = seconds; + int32_t nanos = nanoseconds; + if (sec < 0 && nanos > 0) { + sec += 1; + nanos -= 1'000'000'000; + } + return (sec * 1'000'000) + (nanos / 1'000); + } + + /** + * Return the nanoseconds value. + * + * The returned nanoseconds amount can be invalid on overflow. The caller is + * responsible for handling the overflow case. + */ + constexpr mozilla::CheckedInt64 toNanoseconds() const { + mozilla::CheckedInt64 nanos = seconds; + nanos *= ToNanoseconds(TemporalUnit::Second); + nanos += nanoseconds; + return nanos; + } + + /** + * Create from a minutes value. + */ + static constexpr Derived fromMinutes(int64_t minutes) { + return {minutes * 60, 0}; + } + + /** + * Create from a seconds value. + */ + static constexpr Derived fromSeconds(int64_t seconds) { return {seconds, 0}; } + + /** + * Create from a milliseconds value. + */ + static constexpr Derived fromMilliseconds(int64_t milliseconds) { + int64_t seconds = milliseconds / 1'000; + int32_t millis = milliseconds % 1'000; + if (millis < 0) { + seconds -= 1; + millis += 1'000; + } + return {seconds, millis * 1'000'000}; + } + + /** + * Create from a microseconds value. + */ + static constexpr Derived fromMicroseconds(int64_t microseconds) { + int64_t seconds = microseconds / 1'000'000; + int32_t micros = microseconds % 1'000'000; + if (micros < 0) { + seconds -= 1; + micros += 1'000'000; + } + return {seconds, micros * 1'000}; + } + + /** + * Create from a nanoseconds value. + */ + static constexpr Derived fromNanoseconds(int64_t nanoseconds) { + int64_t seconds = nanoseconds / 1'000'000'000; + int32_t nanos = nanoseconds % 1'000'000'000; + if (nanos < 0) { + seconds -= 1; + nanos += 1'000'000'000; + } + return {seconds, nanos}; + } +}; + +#undef JS_ASSUME + +/** + * InstantSpan represents a span of time between two Instants, measured in + * nanoseconds. + */ +struct InstantSpan final : SecondsAndNanoseconds<InstantSpan> { + constexpr InstantSpan& operator+=(const InstantSpan& other) { + *this = add(*this, other); + return *this; + } + + constexpr InstantSpan& operator-=(const InstantSpan& other) { + *this = subtract(*this, other); + return *this; + } + + constexpr InstantSpan operator+(const InstantSpan& other) const { + return add(*this, other); + } + + constexpr InstantSpan operator-(const InstantSpan& other) const { + return subtract(*this, other); + } + + constexpr InstantSpan operator-() const { return negate(*this); } + + /** + * Returns the maximum instant span value. + */ + static constexpr InstantSpan max() { + // The limit is 2ร8.64 ร 10^21 nanoseconds, which is 2ร8.64 ร 10^12 seconds. + constexpr int64_t seconds = 2 * 8'640'000'000'000; + constexpr int64_t nanos = 0; + return {seconds, nanos}; + } + + /** + * Returns the minimum instant span value. + */ + static constexpr InstantSpan min() { return -max(); } +}; + +/** + * Instant represents a time since the epoch value, measured in nanoseconds. + * + * Instant supports a range of ยฑ8.64 ร 10^21 nanoseconds, covering ยฑ10^8 days + * in either direction relative to midnight at the beginning of 1 January 1970 + * UTC. The range also exactly matches the supported range of JavaScript Date + * objects. + * + * C++ doesn't provide a built-in type capable of storing an integer in the + * range ยฑ8.64 ร 10^21, therefore we need to create our own abstraction. This + * struct follows the design of `std::timespec` and splits the instant into a + * signed seconds part and an unsigned nanoseconds part. + */ +struct Instant final : SecondsAndNanoseconds<Instant> { + constexpr Instant& operator+=(const InstantSpan& other) { + *this = add(*this, other); + return *this; + } + + constexpr Instant& operator-=(const InstantSpan& other) { + *this = subtract(*this, other); + return *this; + } + + constexpr Instant operator+(const InstantSpan& other) const { + return add(*this, other); + } + + constexpr Instant operator-(const InstantSpan& other) const { + return subtract(*this, other); + } + + constexpr InstantSpan operator-(const Instant& other) const { + return subtract<Instant, Instant, InstantSpan>(*this, other); + } + + constexpr Instant operator-() const { return negate(*this); } + + /** + * Return this instant as microseconds from the start of the epoch. (Rounds + * towards negative infinity.) + */ + constexpr int64_t floorToMicroseconds() const { + return (seconds * 1'000'000) + (nanoseconds / 1'000); + } + + /** + * Return this instant as milliseconds from the start of the epoch. (Rounds + * towards negative infinity.) + */ + constexpr int64_t floorToMilliseconds() const { + return (seconds * 1'000) + (nanoseconds / 1'000'000); + } + + /** + * Return this instant as milliseconds from the start of the epoch. (Rounds + * towards positive infinity.) + */ + constexpr int64_t ceilToMilliseconds() const { + return floorToMilliseconds() + int64_t(nanoseconds % 1'000'000 != 0); + } + + /** + * Returns the maximum instant value. + */ + static constexpr Instant max() { + // The limit is 8.64 ร 10^21 nanoseconds, which is 8.64 ร 10^12 seconds. + constexpr int64_t seconds = 8'640'000'000'000; + constexpr int64_t nanos = 0; + return {seconds, nanos}; + } + + /** + * Returns the minimum instant value. + */ + static constexpr Instant min() { return -max(); } +}; + +/** + * Plain date represents a date in the ISO 8601 calendar. + */ +struct PlainDate final { + // [-271821, 275760] + // + // Dates are limited to the range of ยฑ100'000'000 days relative to midnight at + // the beginning of 1 January 1970 UTC. This limits valid years to [-271821, + // 275760]. + int32_t year = 0; + + // [1, 12] + int32_t month = 0; + + // [1, 31] + int32_t day = 0; + + bool operator==(const PlainDate& other) const { + return year == other.year && month == other.month && day == other.day; + } + + bool operator!=(const PlainDate& other) const { return !(*this == other); } +}; + +/** + * Plain time represents a time value on a 24-hour clock. Leap seconds aren't + * supported. + */ +struct PlainTime final { + // [0, 23] + int32_t hour = 0; + + // [0, 59] + int32_t minute = 0; + + // [0, 59] + int32_t second = 0; + + // [0, 999] + int32_t millisecond = 0; + + // [0, 999] + int32_t microsecond = 0; + + // [0, 999] + int32_t nanosecond = 0; + + bool operator==(const PlainTime& other) const { + return hour == other.hour && minute == other.minute && + second == other.second && millisecond == other.millisecond && + microsecond == other.microsecond && nanosecond == other.nanosecond; + } + + bool operator!=(const PlainTime& other) const { return !(*this == other); } +}; + +/** + * Plain date-time represents a date-time value in the ISO 8601 calendar. + */ +struct PlainDateTime final { + PlainDate date; + PlainTime time; + + bool operator==(const PlainDateTime& other) const { + return date == other.date && time == other.time; + } + + bool operator!=(const PlainDateTime& other) const { + return !(*this == other); + } +}; + +/** + * Duration represents the difference between dates or times. Each duration + * component is an integer and all components must have the same sign. + */ +struct Duration final { + double years = 0; + double months = 0; + double weeks = 0; + double days = 0; + double hours = 0; + double minutes = 0; + double seconds = 0; + double milliseconds = 0; + double microseconds = 0; + double nanoseconds = 0; + + bool operator==(const Duration& other) const { + return years == other.years && months == other.months && + weeks == other.weeks && days == other.days && hours == other.hours && + minutes == other.minutes && seconds == other.seconds && + milliseconds == other.milliseconds && + microseconds == other.microseconds && + nanoseconds == other.nanoseconds; + } + + bool operator!=(const Duration& other) const { return !(*this == other); } + + /** + * Return the date components of this duration. + */ + Duration date() const { return {years, months, weeks, days}; } + + /** + * Return the time components of this duration. + */ + Duration time() const { + return { + 0, + 0, + 0, + 0, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds, + }; + } + + /** + * Return a new duration with every component negated. + */ + Duration negate() const { + // Add zero to convert -0 to +0. + return { + -years + (+0.0), -months + (+0.0), -weeks + (+0.0), + -days + (+0.0), -hours + (+0.0), -minutes + (+0.0), + -seconds + (+0.0), -milliseconds + (+0.0), -microseconds + (+0.0), + -nanoseconds + (+0.0), + }; + } +}; + +/** + * Date duration represents the difference between dates. Each duration + * component is an integer and all components must have the same sign. + */ +struct DateDuration final { + double years = 0; + double months = 0; + double weeks = 0; + double days = 0; + + Duration toDuration() { return {years, months, weeks, days}; } +}; + +/** + * Time duration represents the difference between times. Each duration + * component is an integer and all components must have the same sign. + */ +struct TimeDuration final { + double days = 0; + double hours = 0; + double minutes = 0; + double seconds = 0; + double milliseconds = 0; + double microseconds = 0; + double nanoseconds = 0; + + Duration toDuration() { + return {0, + 0, + 0, + days, + hours, + minutes, + seconds, + milliseconds, + microseconds, + nanoseconds}; + } +}; + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_TemporalTypes_h */ diff --git a/js/src/builtin/temporal/TemporalUnit.h b/js/src/builtin/temporal/TemporalUnit.h new file mode 100644 index 0000000000..3c8801cb85 --- /dev/null +++ b/js/src/builtin/temporal/TemporalUnit.h @@ -0,0 +1,135 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_TemporalUnit_h +#define builtin_temporal_TemporalUnit_h + +#include "mozilla/Assertions.h" + +#include <stdint.h> + +namespace js::temporal { +enum class TemporalUnit { + Auto, + Year, + Month, + Week, + Day, + Hour, + Minute, + Second, + Millisecond, + Microsecond, + Nanosecond +}; + +constexpr int64_t ToNanoseconds(TemporalUnit unit) { + switch (unit) { + case TemporalUnit::Day: + return 86'400'000'000'000; + case TemporalUnit::Hour: + return 3'600'000'000'000; + case TemporalUnit::Minute: + return 60'000'000'000; + case TemporalUnit::Second: + return 1'000'000'000; + case TemporalUnit::Millisecond: + return 1'000'000; + case TemporalUnit::Microsecond: + return 1'000; + case TemporalUnit::Nanosecond: + return 1; + + case TemporalUnit::Auto: + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + break; + } + MOZ_CRASH("Unexpected temporal unit"); +} + +constexpr int64_t ToMilliseconds(TemporalUnit unit) { + switch (unit) { + case TemporalUnit::Day: + return 86'400'000; + case TemporalUnit::Hour: + return 3'600'000; + case TemporalUnit::Minute: + return 60'000; + case TemporalUnit::Second: + return 1'000; + case TemporalUnit::Millisecond: + return 1; + + case TemporalUnit::Auto: + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + case TemporalUnit::Microsecond: + case TemporalUnit::Nanosecond: + break; + } + MOZ_CRASH("Unexpected temporal unit"); +} + +constexpr int64_t UnitsPerDay(TemporalUnit unit) { + switch (unit) { + case TemporalUnit::Day: + return 1; + case TemporalUnit::Hour: + return 24; + case TemporalUnit::Minute: + return 1440; + case TemporalUnit::Second: + return 86'400; + case TemporalUnit::Millisecond: + return 86'400'000; + case TemporalUnit::Microsecond: + return 86'400'000'000; + case TemporalUnit::Nanosecond: + return 86'400'000'000'000; + + case TemporalUnit::Auto: + case TemporalUnit::Year: + case TemporalUnit::Month: + case TemporalUnit::Week: + break; + } + MOZ_CRASH("Unexpected temporal unit"); +} + +constexpr const char* TemporalUnitToString(TemporalUnit unit) { + switch (unit) { + case TemporalUnit::Auto: + return "auto"; + case TemporalUnit::Year: + return "year"; + case TemporalUnit::Month: + return "month"; + case TemporalUnit::Week: + return "week"; + case TemporalUnit::Day: + return "day"; + case TemporalUnit::Hour: + return "hour"; + case TemporalUnit::Minute: + return "minute"; + case TemporalUnit::Second: + return "second"; + case TemporalUnit::Millisecond: + return "millisecond"; + case TemporalUnit::Microsecond: + return "microsecond"; + case TemporalUnit::Nanosecond: + return "nanosecond"; + } + MOZ_CRASH("Unexpected temporal unit"); +} + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_TemporalUnit_h */ diff --git a/js/src/builtin/temporal/TimeZone.cpp b/js/src/builtin/temporal/TimeZone.cpp new file mode 100644 index 0000000000..ca7e1b9f11 --- /dev/null +++ b/js/src/builtin/temporal/TimeZone.cpp @@ -0,0 +1,2729 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/TimeZone.h" + +#include "mozilla/Array.h" +#include "mozilla/Assertions.h" +#include "mozilla/intl/TimeZone.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" +#include "mozilla/Range.h" +#include "mozilla/Result.h" +#include "mozilla/Span.h" +#include "mozilla/UniquePtr.h" + +#include <cmath> +#include <cstdlib> +#include <initializer_list> +#include <iterator> +#include <utility> + +#include "jsnum.h" +#include "jspubtd.h" +#include "jstypes.h" +#include "NamespaceImports.h" + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/FormatBuffer.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Instant.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/TemporalTypes.h" +#include "builtin/temporal/TemporalUnit.h" +#include "builtin/temporal/Wrapped.h" +#include "builtin/temporal/ZonedDateTime.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "gc/GCContext.h" +#include "gc/GCEnum.h" +#include "gc/Tracer.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/ComparisonOperators.h" +#include "js/Date.h" +#include "js/ErrorReport.h" +#include "js/ForOfIterator.h" +#include "js/friend/ErrorMessages.h" +#include "js/Printer.h" +#include "js/PropertyDescriptor.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/StableStringChars.h" +#include "threading/ProtectedData.h" +#include "vm/ArrayObject.h" +#include "vm/BytecodeUtil.h" +#include "vm/Compartment.h" +#include "vm/DateTime.h" +#include "vm/GlobalObject.h" +#include "vm/Interpreter.h" +#include "vm/JSAtomState.h" +#include "vm/JSContext.h" +#include "vm/JSObject.h" +#include "vm/PlainObject.h" +#include "vm/Runtime.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 IsTimeZone(Handle<Value> v) { + return v.isObject() && v.toObject().is<TimeZoneObject>(); +} + +void js::temporal::TimeZoneValue::trace(JSTracer* trc) { + TraceNullableRoot(trc, &object_, "TimeZoneValue::object"); +} + +void js::temporal::TimeZoneRecord::trace(JSTracer* trc) { + receiver_.trace(trc); + TraceNullableRoot(trc, &getOffsetNanosecondsFor_, + "TimeZoneMethods::getOffsetNanosecondsFor"); + TraceNullableRoot(trc, &getPossibleInstantsFor_, + "TimeZoneMethods::getPossibleInstantsFor"); +} + +static mozilla::UniquePtr<mozilla::intl::TimeZone> CreateIntlTimeZone( + JSContext* cx, JSString* identifier) { + JS::AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, identifier)) { + return nullptr; + } + + auto result = mozilla::intl::TimeZone::TryCreate( + mozilla::Some(stableChars.twoByteRange())); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + return result.unwrap(); +} + +static mozilla::intl::TimeZone* GetOrCreateIntlTimeZone( + JSContext* cx, Handle<TimeZoneObjectMaybeBuiltin*> timeZone) { + // Obtain a cached mozilla::intl::TimeZone object. + if (auto* tz = timeZone->getTimeZone()) { + return tz; + } + + auto* tz = CreateIntlTimeZone(cx, timeZone->identifier()).release(); + if (!tz) { + return nullptr; + } + timeZone->setTimeZone(tz); + + intl::AddICUCellMemory(timeZone, + TimeZoneObjectMaybeBuiltin::EstimatedMemoryUse); + return tz; +} + +/** + * IsValidTimeZoneName ( timeZone ) + * IsAvailableTimeZoneName ( timeZone ) + */ +bool js::temporal::IsValidTimeZoneName( + JSContext* cx, Handle<JSString*> timeZone, + MutableHandle<JSAtom*> validatedTimeZone) { + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + if (!sharedIntlData.validateTimeZoneName(cx, timeZone, validatedTimeZone)) { + return false; + } + + if (validatedTimeZone) { + cx->markAtom(validatedTimeZone); + } + return true; +} + +/** + * 6.5.2 CanonicalizeTimeZoneName ( timeZone ) + * + * Canonicalizes the given IANA time zone name. + * + * ES2024 Intl draft rev 74ca7099f103d143431b2ea422ae640c6f43e3e6 + */ +JSString* js::temporal::CanonicalizeTimeZoneName( + JSContext* cx, Handle<JSLinearString*> timeZone) { + // Step 1. (Not applicable, the input is already a valid IANA time zone.) +#ifdef DEBUG + MOZ_ASSERT(!StringEqualsLiteral(timeZone, "Etc/Unknown"), + "Invalid time zone"); + + Rooted<JSAtom*> checkTimeZone(cx); + if (!IsValidTimeZoneName(cx, timeZone, &checkTimeZone)) { + return nullptr; + } + MOZ_ASSERT(EqualStrings(timeZone, checkTimeZone), + "Time zone name not normalized"); +#endif + + // Step 2. + Rooted<JSLinearString*> ianaTimeZone(cx); + do { + intl::SharedIntlData& sharedIntlData = cx->runtime()->sharedIntlData.ref(); + + // Some time zone names are canonicalized differently by ICU -- handle + // those first: + Rooted<JSAtom*> canonicalTimeZone(cx); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA( + cx, timeZone, &canonicalTimeZone)) { + return nullptr; + } + + if (canonicalTimeZone) { + cx->markAtom(canonicalTimeZone); + ianaTimeZone = canonicalTimeZone; + break; + } + + JS::AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, timeZone)) { + return nullptr; + } + + intl::FormatBuffer<char16_t, intl::INITIAL_CHAR_BUFFER_SIZE> buffer(cx); + auto result = mozilla::intl::TimeZone::GetCanonicalTimeZoneID( + stableChars.twoByteRange(), buffer); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return nullptr; + } + + ianaTimeZone = buffer.toString(cx); + if (!ianaTimeZone) { + return nullptr; + } + } while (false); + +#ifdef DEBUG + MOZ_ASSERT(!StringEqualsLiteral(ianaTimeZone, "Etc/Unknown"), + "Invalid canonical time zone"); + + if (!IsValidTimeZoneName(cx, ianaTimeZone, &checkTimeZone)) { + return nullptr; + } + MOZ_ASSERT(EqualStrings(ianaTimeZone, checkTimeZone), + "Unsupported canonical time zone"); +#endif + + // Step 3. + if (StringEqualsLiteral(ianaTimeZone, "Etc/UTC") || + StringEqualsLiteral(ianaTimeZone, "Etc/GMT")) { + return cx->names().UTC; + } + + // We don't need to check against "GMT", because ICU uses the tzdata rearguard + // format, where "GMT" is a link to "Etc/GMT". + MOZ_ASSERT(!StringEqualsLiteral(ianaTimeZone, "GMT")); + + // Step 4. + return ianaTimeZone; +} + +/** + * IsValidTimeZoneName ( timeZone ) + * IsAvailableTimeZoneName ( timeZone ) + * CanonicalizeTimeZoneName ( timeZone ) + */ +JSString* js::temporal::ValidateAndCanonicalizeTimeZoneName( + JSContext* cx, Handle<JSString*> timeZone) { + Rooted<JSAtom*> validatedTimeZone(cx); + if (!IsValidTimeZoneName(cx, timeZone, &validatedTimeZone)) { + return nullptr; + } + + if (!validatedTimeZone) { + if (auto chars = QuoteString(cx, timeZone)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_INVALID_IDENTIFIER, + chars.get()); + } + return nullptr; + } + + return CanonicalizeTimeZoneName(cx, validatedTimeZone); +} + +class EpochInstantList final { + // GetNamedTimeZoneEpochNanoseconds can return up-to two elements. + static constexpr size_t MaxLength = 2; + + mozilla::Array<Instant, MaxLength> array_ = {}; + size_t length_ = 0; + + public: + EpochInstantList() = default; + + size_t length() const { return length_; } + + void append(const Instant& instant) { array_[length_++] = instant; } + + auto& operator[](size_t i) { return array_[i]; } + const auto& operator[](size_t i) const { return array_[i]; } + + auto begin() const { return array_.begin(); } + auto end() const { return array_.begin() + length_; } +}; + +/** + * GetNamedTimeZoneEpochNanoseconds ( timeZoneIdentifier, year, month, day, + * hour, minute, second, millisecond, microsecond, nanosecond ) + */ +static bool GetNamedTimeZoneEpochNanoseconds( + JSContext* cx, Handle<TimeZoneObjectMaybeBuiltin*> timeZone, + const PlainDateTime& dateTime, EpochInstantList& instants) { + MOZ_ASSERT(timeZone->offsetMinutes().isUndefined()); + MOZ_ASSERT(IsValidISODateTime(dateTime)); + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + MOZ_ASSERT(instants.length() == 0); + + // FIXME: spec issue - assert ISODateTimeWithinLimits instead of + // IsValidISODate + + int64_t ms = MakeDate(dateTime); + + auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); + if (!tz) { + return false; + } + + auto getOffset = [&](mozilla::intl::TimeZone::LocalOption skippedTime, + mozilla::intl::TimeZone::LocalOption repeatedTime, + int32_t* offset) { + auto result = tz->GetUTCOffsetMs(ms, skippedTime, repeatedTime); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + *offset = result.unwrap(); + MOZ_ASSERT(std::abs(*offset) < UnitsPerDay(TemporalUnit::Millisecond)); + + return true; + }; + + constexpr auto formerTime = mozilla::intl::TimeZone::LocalOption::Former; + constexpr auto latterTime = mozilla::intl::TimeZone::LocalOption::Latter; + + int32_t formerOffset; + if (!getOffset(formerTime, formerTime, &formerOffset)) { + return false; + } + + int32_t latterOffset; + if (!getOffset(latterTime, latterTime, &latterOffset)) { + return false; + } + + if (formerOffset == latterOffset) { + auto instant = GetUTCEpochNanoseconds( + dateTime, InstantSpan::fromMilliseconds(formerOffset)); + instants.append(instant); + return true; + } + + int32_t disambiguationOffset; + if (!getOffset(formerTime, latterTime, &disambiguationOffset)) { + return false; + } + + // Skipped time. + if (disambiguationOffset == formerOffset) { + return true; + } + + // Repeated time. + for (auto offset : {formerOffset, latterOffset}) { + auto instant = + GetUTCEpochNanoseconds(dateTime, InstantSpan::fromMilliseconds(offset)); + instants.append(instant); + } + + MOZ_ASSERT(instants.length() == 2); + + // Ensure the returned instants are sorted in numerical order. + if (instants[0] > instants[1]) { + std::swap(instants[0], instants[1]); + } + + return true; +} + +/** + * GetNamedTimeZoneOffsetNanoseconds ( timeZoneIdentifier, epochNanoseconds ) + */ +static bool GetNamedTimeZoneOffsetNanoseconds( + JSContext* cx, Handle<TimeZoneObjectMaybeBuiltin*> timeZone, + const Instant& epochInstant, int64_t* offset) { + MOZ_ASSERT(timeZone->offsetMinutes().isUndefined()); + + // Round down (floor) to the previous full milliseconds. + int64_t millis = epochInstant.floorToMilliseconds(); + + auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); + if (!tz) { + return false; + } + + auto result = tz->GetOffsetMs(millis); + if (result.isErr()) { + intl::ReportInternalError(cx, result.unwrapErr()); + return false; + } + + // FIXME: spec issue - should constrain the range to not exceed 24-hours. + // https://github.com/tc39/ecma262/issues/3101 + + int64_t nanoPerMs = 1'000'000; + *offset = result.unwrap() * nanoPerMs; + return true; +} + +/** + * GetNamedTimeZoneNextTransition ( timeZoneIdentifier, epochNanoseconds ) + */ +static bool GetNamedTimeZoneNextTransition(JSContext* cx, + Handle<TimeZoneObject*> timeZone, + const Instant& epochInstant, + mozilla::Maybe<Instant>* result) { + MOZ_ASSERT(timeZone->offsetMinutes().isUndefined()); + + // Round down (floor) to the previous full millisecond. + // + // IANA has experimental support for transitions at sub-second precision, but + // the default configuration doesn't enable it, therefore it's safe to round + // to milliseconds here. In addition to that, ICU also only supports + // transitions at millisecond precision. + int64_t millis = epochInstant.floorToMilliseconds(); + + auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); + if (!tz) { + return false; + } + + auto next = tz->GetNextTransition(millis); + if (next.isErr()) { + intl::ReportInternalError(cx, next.unwrapErr()); + return false; + } + + auto transition = next.unwrap(); + if (!transition) { + *result = mozilla::Nothing(); + return true; + } + + auto transitionInstant = Instant::fromMilliseconds(*transition); + if (!IsValidEpochInstant(transitionInstant)) { + *result = mozilla::Nothing(); + return true; + } + + *result = mozilla::Some(transitionInstant); + return true; +} + +/** + * GetNamedTimeZonePreviousTransition ( timeZoneIdentifier, epochNanoseconds ) + */ +static bool GetNamedTimeZonePreviousTransition( + JSContext* cx, Handle<TimeZoneObject*> timeZone, + const Instant& epochInstant, mozilla::Maybe<Instant>* result) { + MOZ_ASSERT(timeZone->offsetMinutes().isUndefined()); + + // Round up (ceil) to the next full millisecond. + // + // IANA has experimental support for transitions at sub-second precision, but + // the default configuration doesn't enable it, therefore it's safe to round + // to milliseconds here. In addition to that, ICU also only supports + // transitions at millisecond precision. + int64_t millis = epochInstant.ceilToMilliseconds(); + + auto* tz = GetOrCreateIntlTimeZone(cx, timeZone); + if (!tz) { + return false; + } + + auto previous = tz->GetPreviousTransition(millis); + if (previous.isErr()) { + intl::ReportInternalError(cx, previous.unwrapErr()); + return false; + } + + auto transition = previous.unwrap(); + if (!transition) { + *result = mozilla::Nothing(); + return true; + } + + auto transitionInstant = Instant::fromMilliseconds(*transition); + if (!IsValidEpochInstant(transitionInstant)) { + *result = mozilla::Nothing(); + return true; + } + + *result = mozilla::Some(transitionInstant); + return true; +} + +/** + * FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ) + */ +static JSString* FormatOffsetTimeZoneIdentifier(JSContext* cx, + int32_t offsetMinutes) { + MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute)); + + // Step 1. + char sign = offsetMinutes >= 0 ? '+' : '-'; + + // Step 2. + int32_t absoluteMinutes = std::abs(offsetMinutes); + + // Step 3. + int32_t hour = absoluteMinutes / 60; + + // Step 4. + int32_t minute = absoluteMinutes % 60; + + // Step 5. (Inlined FormatTimeString). + // + // Format: "sign hour{2} : minute{2}" + char result[] = { + sign, char('0' + (hour / 10)), char('0' + (hour % 10)), + ':', char('0' + (minute / 10)), char('0' + (minute % 10)), + }; + + // Step 6. + return NewStringCopyN<CanGC>(cx, result, std::size(result)); +} + +/** + * CreateTemporalTimeZone ( identifier [ , newTarget ] ) + */ +static TimeZoneObject* CreateTemporalTimeZone(JSContext* cx, + const CallArgs& args, + Handle<JSString*> identifier, + Handle<Value> offsetMinutes) { + MOZ_ASSERT(offsetMinutes.isUndefined() || offsetMinutes.isInt32()); + MOZ_ASSERT_IF(offsetMinutes.isInt32(), std::abs(offsetMinutes.toInt32()) < + UnitsPerDay(TemporalUnit::Minute)); + + // Steps 1-2. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_TimeZone, &proto)) { + return nullptr; + } + + auto* timeZone = NewObjectWithClassProto<TimeZoneObject>(cx, proto); + if (!timeZone) { + return nullptr; + } + + // Step 4.a. (Not applicable in our implementation.) + + // Steps 3.a or 4.b. + timeZone->setFixedSlot(TimeZoneObject::IDENTIFIER_SLOT, + StringValue(identifier)); + + // Step 3.b or 4.c. + timeZone->setFixedSlot(TimeZoneObject::OFFSET_MINUTES_SLOT, offsetMinutes); + + // Step 5. + return timeZone; +} + +static BuiltinTimeZoneObject* CreateBuiltinTimeZone( + JSContext* cx, Handle<JSString*> identifier) { + // TODO: Implement a built-in time zone object cache. + + auto* object = NewObjectWithGivenProto<BuiltinTimeZoneObject>(cx, nullptr); + if (!object) { + return nullptr; + } + + object->setFixedSlot(BuiltinTimeZoneObject::IDENTIFIER_SLOT, + StringValue(identifier)); + + object->setFixedSlot(BuiltinTimeZoneObject::OFFSET_MINUTES_SLOT, + UndefinedValue()); + + return object; +} + +static BuiltinTimeZoneObject* CreateBuiltinTimeZone(JSContext* cx, + int32_t offsetMinutes) { + // TODO: It's unclear if offset time zones should also be cached. Real world + // experience will tell if a cache should be added. + + MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute)); + + Rooted<JSString*> identifier( + cx, FormatOffsetTimeZoneIdentifier(cx, offsetMinutes)); + if (!identifier) { + return nullptr; + } + + auto* object = NewObjectWithGivenProto<BuiltinTimeZoneObject>(cx, nullptr); + if (!object) { + return nullptr; + } + + object->setFixedSlot(BuiltinTimeZoneObject::IDENTIFIER_SLOT, + StringValue(identifier)); + + object->setFixedSlot(BuiltinTimeZoneObject::OFFSET_MINUTES_SLOT, + Int32Value(offsetMinutes)); + + return object; +} + +/** + * CreateTemporalTimeZone ( identifier [ , newTarget ] ) + */ +static TimeZoneObject* CreateTemporalTimeZone( + JSContext* cx, Handle<BuiltinTimeZoneObject*> timeZone) { + // Steps 1-2. + auto* object = NewBuiltinClassInstance<TimeZoneObject>(cx); + if (!object) { + return nullptr; + } + + // Step 4.a. (Not applicable in our implementation.) + + // Steps 3.a or 4.b. + object->setFixedSlot( + TimeZoneObject::IDENTIFIER_SLOT, + timeZone->getFixedSlot(BuiltinTimeZoneObject::IDENTIFIER_SLOT)); + + // Step 3.b or 4.c. + object->setFixedSlot( + TimeZoneObject::OFFSET_MINUTES_SLOT, + timeZone->getFixedSlot(BuiltinTimeZoneObject::OFFSET_MINUTES_SLOT)); + + // Step 5. + return object; +} + +/** + * CreateTemporalTimeZone ( identifier [ , newTarget ] ) + */ +BuiltinTimeZoneObject* js::temporal::CreateTemporalTimeZone( + JSContext* cx, Handle<JSString*> identifier) { + return ::CreateBuiltinTimeZone(cx, identifier); +} + +/** + * ToTemporalTimeZoneSlotValue ( temporalTimeZoneLike ) + */ +bool js::temporal::ToTemporalTimeZone(JSContext* cx, + Handle<ParsedTimeZone> string, + MutableHandle<TimeZoneValue> result) { + // Steps 1-3. (Not applicable) + + // Steps 4-5. + if (string.name()) { + // Steps 4.a-c. (Not applicable in our implementation.) + + // Steps 4.d-e. + Rooted<JSString*> timeZoneName( + cx, ValidateAndCanonicalizeTimeZoneName(cx, string.name())); + if (!timeZoneName) { + return false; + } + + // Steps 4.f and 5. + auto* obj = ::CreateBuiltinTimeZone(cx, timeZoneName); + if (!obj) { + return false; + } + + result.set(TimeZoneValue(obj)); + return true; + } + + // Steps 4.b-c and 8. + auto* obj = ::CreateBuiltinTimeZone(cx, string.offset()); + if (!obj) { + return false; + } + + result.set(TimeZoneValue(obj)); + return true; +} + +/** + * ObjectImplementsTemporalTimeZoneProtocol ( object ) + */ +static bool ObjectImplementsTemporalTimeZoneProtocol(JSContext* cx, + Handle<JSObject*> object, + bool* result) { + // Step 1. (Not applicable in our implementation.) + MOZ_ASSERT(!object->canUnwrapAs<TimeZoneObject>(), + "TimeZone objects handled in the caller"); + + // Step 2. + for (auto key : { + &JSAtomState::getOffsetNanosecondsFor, + &JSAtomState::getPossibleInstantsFor, + &JSAtomState::id, + }) { + // Step 2.a. + bool has; + if (!HasProperty(cx, object, cx->names().*key, &has)) { + return false; + } + if (!has) { + *result = false; + return true; + } + } + + // Step 3. + *result = true; + return true; +} + +/** + * ToTemporalTimeZoneSlotValue ( temporalTimeZoneLike ) + */ +bool js::temporal::ToTemporalTimeZone(JSContext* cx, + Handle<Value> temporalTimeZoneLike, + MutableHandle<TimeZoneValue> result) { + // Step 1. + Rooted<Value> timeZoneLike(cx, temporalTimeZoneLike); + if (timeZoneLike.isObject()) { + Rooted<JSObject*> obj(cx, &timeZoneLike.toObject()); + + // Step 1.b. (Partial) + if (obj->canUnwrapAs<TimeZoneObject>()) { + result.set(TimeZoneValue(obj)); + return true; + } + + // Step 1.a. + if (auto* zonedDateTime = obj->maybeUnwrapIf<ZonedDateTimeObject>()) { + result.set(zonedDateTime->timeZone()); + return result.wrap(cx); + } + + // Step 1.b. + bool implementsTimeZoneProtocol; + if (!ObjectImplementsTemporalTimeZoneProtocol( + cx, obj, &implementsTimeZoneProtocol)) { + return false; + } + if (!implementsTimeZoneProtocol) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_OBJECT, + "Temporal.TimeZone", obj->getClass()->name); + return false; + } + + // Step 1.c. + result.set(TimeZoneValue(obj)); + return true; + } + + // Step 2. + if (!timeZoneLike.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, + timeZoneLike, nullptr, "not a string"); + return false; + } + Rooted<JSString*> identifier(cx, timeZoneLike.toString()); + + // Step 3. + Rooted<ParsedTimeZone> timeZoneName(cx); + if (!ParseTemporalTimeZoneString(cx, identifier, &timeZoneName)) { + return false; + } + + // Steps 4-8. + return ToTemporalTimeZone(cx, timeZoneName, result); +} + +/** + * ToTemporalTimeZoneObject ( timeZoneSlotValue ) + */ +JSObject* js::temporal::ToTemporalTimeZoneObject( + JSContext* cx, Handle<TimeZoneValue> timeZone) { + // Step 1. + if (timeZone.isObject()) { + return timeZone.toObject(); + } + + // Step 2. + return CreateTemporalTimeZone(cx, timeZone.toString()); +} + +/** + * ToTemporalTimeZoneIdentifier ( timeZoneSlotValue ) + */ +JSString* js::temporal::ToTemporalTimeZoneIdentifier( + JSContext* cx, Handle<TimeZoneValue> timeZone) { + // Step 1. + if (timeZone.isString()) { + // Step 1.a. (Not applicable in our implementation.) + + // Step 1.b. + return timeZone.toString()->identifier(); + } + + // Step 2. + Rooted<JSObject*> timeZoneObj(cx, timeZone.toObject()); + Rooted<Value> identifier(cx); + if (!GetProperty(cx, timeZoneObj, timeZoneObj, cx->names().id, &identifier)) { + return nullptr; + } + + // Step 3. + if (!identifier.isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, identifier, + nullptr, "not a string"); + return nullptr; + } + + // Step 4. + return identifier.toString(); +} + +static bool TimeZone_getOffsetNanosecondsFor(JSContext* cx, unsigned argc, + Value* vp); + +static bool TimeZone_getPossibleInstantsFor(JSContext* cx, unsigned argc, + Value* vp); + +/** + * TimeZoneMethodsRecordLookup ( timeZoneRec, methodName ) + */ +static bool TimeZoneMethodsRecordLookup(JSContext* cx, + MutableHandle<TimeZoneRecord> timeZone, + TimeZoneMethod methodName) { + // Step 1. (Not applicable in our implementation.) + + // Steps 2-4. + auto object = timeZone.receiver().toObject(); + + auto lookup = [&](Handle<PropertyName*> name, JSNative native, + MutableHandle<JSObject*> result) { + auto* method = GetMethod(cx, object, name); + if (!method) { + return false; + } + + // As an optimization we only store the method if the receiver is either + // a custom time zone object or if the method isn't the default, built-in + // time zone method. + if (!object->is<TimeZoneObject>() || !IsNativeFunction(method, native)) { + result.set(method); + } + return true; + }; + + switch (methodName) { + // Steps 2 and 4. + case TimeZoneMethod::GetOffsetNanosecondsFor: + return lookup(cx->names().getOffsetNanosecondsFor, + TimeZone_getOffsetNanosecondsFor, + timeZone.getOffsetNanosecondsFor()); + + // Steps 3 and 4. + case TimeZoneMethod::GetPossibleInstantsFor: + return lookup(cx->names().getPossibleInstantsFor, + TimeZone_getPossibleInstantsFor, + timeZone.getPossibleInstantsFor()); + } + + MOZ_CRASH("invalid time zone method"); +} + +/** + * CreateTimeZoneMethodsRecord ( timeZone, methods ) + */ +bool js::temporal::CreateTimeZoneMethodsRecord( + JSContext* cx, Handle<TimeZoneValue> timeZone, + mozilla::EnumSet<TimeZoneMethod> methods, + MutableHandle<TimeZoneRecord> result) { + MOZ_ASSERT(!methods.isEmpty()); + + // Step 1. + result.set(TimeZoneRecord{timeZone}); + +#ifdef DEBUG + // Remember the set of looked-up methods for assertions. + result.get().lookedUp() += methods; +#endif + + // Built-in time zones don't perform observable lookups. + if (timeZone.isString()) { + return true; + } + + // Step 2. + for (auto method : methods) { + if (!TimeZoneMethodsRecordLookup(cx, result, method)) { + return false; + } + } + + // Step 3. + return true; +} + +bool js::temporal::WrapTimeZoneValueObject(JSContext* cx, + MutableHandle<JSObject*> timeZone) { + // First handle the common case when |timeZone| is TimeZoneObjectMaybeBuiltin + // from the current compartment. + if (MOZ_LIKELY(timeZone->is<TimeZoneObjectMaybeBuiltin>() && + timeZone->compartment() == cx->compartment())) { + return true; + } + + // If it's not a built-in time zone, simply wrap the object into the current + // compartment. + auto* unwrappedTimeZone = timeZone->maybeUnwrapIf<BuiltinTimeZoneObject>(); + if (!unwrappedTimeZone) { + return cx->compartment()->wrap(cx, timeZone); + } + + // If this is a built-in time zone from a different compartment, create a + // fresh copy using the current compartment. + // + // We create a fresh copy, so we don't have to support the cross-compartment + // case, which makes detection of "string" time zones easier. + + const auto& offsetMinutes = unwrappedTimeZone->offsetMinutes(); + if (offsetMinutes.isInt32()) { + auto* obj = CreateBuiltinTimeZone(cx, offsetMinutes.toInt32()); + if (!obj) { + return false; + } + + timeZone.set(obj); + return true; + } + MOZ_ASSERT(offsetMinutes.isUndefined()); + + Rooted<JSString*> identifier(cx, unwrappedTimeZone->identifier()); + if (!cx->compartment()->wrap(cx, &identifier)) { + return false; + } + + auto* obj = ::CreateBuiltinTimeZone(cx, identifier); + if (!obj) { + return false; + } + + timeZone.set(obj); + return true; +} + +/** + * Temporal.TimeZone.prototype.getOffsetNanosecondsFor ( instant ) + */ +static bool BuiltinGetOffsetNanosecondsFor( + JSContext* cx, Handle<TimeZoneObjectMaybeBuiltin*> timeZone, + const Instant& instant, int64_t* offsetNanoseconds) { + // Steps 1-3. (Not applicable.) + + // Step 4. + if (timeZone->offsetMinutes().isInt32()) { + int32_t offset = timeZone->offsetMinutes().toInt32(); + MOZ_ASSERT(std::abs(offset) < UnitsPerDay(TemporalUnit::Minute)); + + *offsetNanoseconds = int64_t(offset) * ToNanoseconds(TemporalUnit::Minute); + return true; + } + MOZ_ASSERT(timeZone->offsetMinutes().isUndefined()); + + // Step 5. + int64_t offset; + if (!GetNamedTimeZoneOffsetNanoseconds(cx, timeZone, instant, &offset)) { + return false; + } + MOZ_ASSERT(std::abs(offset) < ToNanoseconds(TemporalUnit::Day)); + + *offsetNanoseconds = offset; + return true; +} + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +static bool GetOffsetNanosecondsForSlow(JSContext* cx, + Handle<TimeZoneRecord> timeZone, + Handle<Wrapped<InstantObject*>> instant, + int64_t* offsetNanoseconds) { + // Step 1. (Inlined call to TimeZoneMethodsRecordCall) + Rooted<Value> fval(cx, ObjectValue(*timeZone.getOffsetNanosecondsFor())); + auto thisv = timeZone.receiver().toObject(); + Rooted<Value> instantVal(cx, ObjectValue(*instant)); + Rooted<Value> rval(cx); + if (!Call(cx, fval, thisv, instantVal, &rval)) { + return false; + } + + // Step 2. (Not applicable) + + // Step 3. + if (!rval.isNumber()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, rval, + nullptr, "not a number"); + return false; + } + + // Steps 4-6. + double num = rval.toNumber(); + if (!IsInteger(num) || std::abs(num) >= ToNanoseconds(TemporalUnit::Day)) { + ToCStringBuf cbuf; + const char* numStr = NumberToCString(&cbuf, num); + + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_NANOS_RANGE, numStr); + return false; + } + + // Step 7. + *offsetNanoseconds = int64_t(num); + return true; +} + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool js::temporal::GetOffsetNanosecondsFor( + JSContext* cx, Handle<TimeZoneRecord> timeZone, + Handle<Wrapped<InstantObject*>> instant, int64_t* offsetNanoseconds) { + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Step 2. (Reordered) + auto getOffsetNanosecondsFor = timeZone.getOffsetNanosecondsFor(); + if (!getOffsetNanosecondsFor) { + auto* unwrapped = instant.unwrap(cx); + if (!unwrapped) { + return false; + } + auto instant = ToInstant(unwrapped); + auto builtin = timeZone.receiver().toTimeZoneObjectMaybeBuiltin(); + + return BuiltinGetOffsetNanosecondsFor(cx, builtin, instant, + offsetNanoseconds); + } + + // Steps 1 and 3-7. + return ::GetOffsetNanosecondsForSlow(cx, timeZone, instant, + offsetNanoseconds); +} + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool js::temporal::GetOffsetNanosecondsFor( + JSContext* cx, Handle<TimeZoneValue> timeZone, + Handle<Wrapped<InstantObject*>> instant, int64_t* offsetNanoseconds) { + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + }, + &timeZoneRec)) { + return false; + } + + return GetOffsetNanosecondsFor(cx, timeZoneRec, instant, offsetNanoseconds); +} + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool js::temporal::GetOffsetNanosecondsFor(JSContext* cx, + Handle<TimeZoneRecord> timeZone, + const Instant& instant, + int64_t* offsetNanoseconds) { + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Step 2. (Reordered) + auto getOffsetNanosecondsFor = timeZone.getOffsetNanosecondsFor(); + if (!getOffsetNanosecondsFor) { + auto builtin = timeZone.receiver().toTimeZoneObjectMaybeBuiltin(); + return BuiltinGetOffsetNanosecondsFor(cx, builtin, instant, + offsetNanoseconds); + } + + // Steps 1 and 3-7. + Rooted<InstantObject*> obj(cx, CreateTemporalInstant(cx, instant)); + if (!obj) { + return false; + } + return ::GetOffsetNanosecondsForSlow(cx, timeZone, obj, offsetNanoseconds); +} + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool js::temporal::GetOffsetNanosecondsFor(JSContext* cx, + Handle<TimeZoneValue> timeZone, + const Instant& instant, + int64_t* offsetNanoseconds) { + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + }, + &timeZoneRec)) { + return false; + } + + return GetOffsetNanosecondsFor(cx, timeZoneRec, instant, offsetNanoseconds); +} + +/** + * FormatUTCOffsetNanoseconds ( offsetNanoseconds ) + */ +JSString* js::temporal::FormatUTCOffsetNanoseconds(JSContext* cx, + int64_t offsetNanoseconds) { + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Step 1. + char sign = offsetNanoseconds >= 0 ? '+' : '-'; + + // Step 2. + int64_t absoluteNanoseconds = std::abs(offsetNanoseconds); + + // Step 6. (Reordered) + int32_t subSecondNanoseconds = int32_t(absoluteNanoseconds % 1'000'000'000); + + // Step 5. (Reordered) + int32_t quotient = int32_t(absoluteNanoseconds / 1'000'000'000); + int32_t second = quotient % 60; + + // Step 4. (Reordered) + quotient /= 60; + int32_t minute = quotient % 60; + + // Step 3. + int32_t hour = quotient / 60; + MOZ_ASSERT(hour < 24, "time zone offset mustn't exceed 24-hours"); + + // Format: "sign hour{2} : minute{2} : second{2} . fractional{9}" + constexpr size_t maxLength = 1 + 2 + 1 + 2 + 1 + 2 + 1 + 9; + char result[maxLength]; + + size_t n = 0; + + // Steps 7-8. (Inlined FormatTimeString). + result[n++] = sign; + result[n++] = '0' + (hour / 10); + result[n++] = '0' + (hour % 10); + result[n++] = ':'; + result[n++] = '0' + (minute / 10); + result[n++] = '0' + (minute % 10); + + if (second != 0 || subSecondNanoseconds != 0) { + result[n++] = ':'; + result[n++] = '0' + (second / 10); + result[n++] = '0' + (second % 10); + + if (uint32_t fractional = subSecondNanoseconds) { + result[n++] = '.'; + + uint32_t k = 100'000'000; + do { + result[n++] = '0' + (fractional / k); + fractional %= k; + k /= 10; + } while (fractional); + } + } + + MOZ_ASSERT(n <= maxLength); + + // Step 9. + return NewStringCopyN<CanGC>(cx, result, n); +} + +/** + * GetOffsetStringFor ( timeZoneRec, instant ) + */ +JSString* js::temporal::GetOffsetStringFor(JSContext* cx, + Handle<TimeZoneValue> timeZone, + const Instant& instant) { + // Step 1. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return nullptr; + } + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Step 2. + return FormatUTCOffsetNanoseconds(cx, offsetNanoseconds); +} + +/** + * GetOffsetStringFor ( timeZoneRec, instant ) + */ +JSString* js::temporal::GetOffsetStringFor( + JSContext* cx, Handle<TimeZoneRecord> timeZone, + Handle<Wrapped<InstantObject*>> instant) { + // Step 1. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return nullptr; + } + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Step 2. + return FormatUTCOffsetNanoseconds(cx, offsetNanoseconds); +} + +/** + * TimeZoneEquals ( one, two ) + */ +bool js::temporal::TimeZoneEquals(JSContext* cx, Handle<JSString*> one, + Handle<JSString*> two, bool* equals) { + // Steps 1-3. (Not applicable) + + // Step 4. + if (!EqualStrings(cx, one, two, equals)) { + return false; + } + if (*equals) { + return true; + } + + // Step 5. + Rooted<ParsedTimeZone> timeZoneOne(cx); + if (!ParseTimeZoneIdentifier(cx, one, &timeZoneOne)) { + return false; + } + + // Step 6. + Rooted<ParsedTimeZone> timeZoneTwo(cx); + if (!ParseTimeZoneIdentifier(cx, two, &timeZoneTwo)) { + return false; + } + + // Step 7. + if (timeZoneOne.name() && timeZoneTwo.name()) { + // Step 7.a. + Rooted<JSAtom*> validTimeZoneOne(cx); + if (!IsValidTimeZoneName(cx, timeZoneOne.name(), &validTimeZoneOne)) { + return false; + } + if (!validTimeZoneOne) { + *equals = false; + return true; + } + + // Step 7.b. + Rooted<JSAtom*> validTimeZoneTwo(cx); + if (!IsValidTimeZoneName(cx, timeZoneTwo.name(), &validTimeZoneTwo)) { + return false; + } + if (!validTimeZoneTwo) { + *equals = false; + return true; + } + + // Step 7.c and 9. + Rooted<JSString*> canonicalOne( + cx, CanonicalizeTimeZoneName(cx, validTimeZoneOne)); + if (!canonicalOne) { + return false; + } + + JSString* canonicalTwo = CanonicalizeTimeZoneName(cx, validTimeZoneTwo); + if (!canonicalTwo) { + return false; + } + + return EqualStrings(cx, canonicalOne, canonicalTwo, equals); + } + + // Step 8.a. + if (!timeZoneOne.name() && !timeZoneTwo.name()) { + *equals = (timeZoneOne.offset() == timeZoneTwo.offset()); + return true; + } + + // Step 9. + *equals = false; + return true; +} + +/** + * TimeZoneEquals ( one, two ) + */ +bool js::temporal::TimeZoneEquals(JSContext* cx, Handle<TimeZoneValue> one, + Handle<TimeZoneValue> two, bool* equals) { + // Step 1. + if (one.isObject() && two.isObject() && one.toObject() == two.toObject()) { + *equals = true; + return true; + } + + // Step 2. + Rooted<JSString*> timeZoneOne(cx, ToTemporalTimeZoneIdentifier(cx, one)); + if (!timeZoneOne) { + return false; + } + + // Step 3. + Rooted<JSString*> timeZoneTwo(cx, ToTemporalTimeZoneIdentifier(cx, two)); + if (!timeZoneTwo) { + return false; + } + + // Steps 4-9. + return TimeZoneEquals(cx, timeZoneOne, timeZoneTwo, equals); +} + +// ES2019 draft rev 0ceb728a1adbffe42b26972a6541fd7f398b1557 +// 5.2.5 Mathematical Operations +static inline double PositiveModulo(double dividend, double divisor) { + MOZ_ASSERT(divisor > 0); + MOZ_ASSERT(std::isfinite(divisor)); + + double result = std::fmod(dividend, divisor); + if (result < 0) { + result += divisor; + } + return result + (+0.0); +} + +/* ES5 15.9.1.10. */ +static double HourFromTime(double t) { + return PositiveModulo(std::floor(t / msPerHour), HoursPerDay); +} + +static double MinFromTime(double t) { + return PositiveModulo(std::floor(t / msPerMinute), MinutesPerHour); +} + +static double SecFromTime(double t) { + return PositiveModulo(std::floor(t / msPerSecond), SecondsPerMinute); +} + +static double msFromTime(double t) { return PositiveModulo(t, msPerSecond); } + +/** + * GetISOPartsFromEpoch ( epochNanoseconds ) + */ +static PlainDateTime GetISOPartsFromEpoch(const Instant& instant) { + // TODO: YearFromTime/MonthFromTime/DayFromTime recompute the same values + // multiple times. Consider adding a new function avoids this. + + // Step 1. + MOZ_ASSERT(IsValidEpochInstant(instant)); + + // Step 2. + int32_t remainderNs = instant.nanoseconds % 1'000'000; + + // Step 3. + int64_t epochMilliseconds = instant.floorToMilliseconds(); + + // Step 4. + int32_t year = JS::YearFromTime(epochMilliseconds); + + // Step 5. + int32_t month = JS::MonthFromTime(epochMilliseconds) + 1; + + // Step 6. + int32_t day = JS::DayFromTime(epochMilliseconds); + + // Step 7. + int32_t hour = HourFromTime(epochMilliseconds); + + // Step 8. + int32_t minute = MinFromTime(epochMilliseconds); + + // Step 9. + int32_t second = SecFromTime(epochMilliseconds); + + // Step 10. + int32_t millisecond = msFromTime(epochMilliseconds); + + // Step 11. + int32_t microsecond = remainderNs / 1000; + + // Step 12. + int32_t nanosecond = remainderNs % 1000; + + // Step 13. + PlainDateTime result = { + {year, month, day}, + {hour, minute, second, millisecond, microsecond, nanosecond}}; + + // Always valid when the epoch nanoseconds are within the representable limit. + MOZ_ASSERT(IsValidISODateTime(result)); + MOZ_ASSERT(ISODateTimeWithinLimits(result)); + + return result; +} + +/** + * BalanceISODateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond ) + */ +static PlainDateTime BalanceISODateTime(const PlainDateTime& dateTime, + int64_t nanoseconds) { + MOZ_ASSERT(IsValidISODateTime(dateTime)); + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + MOZ_ASSERT(std::abs(nanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + auto& [date, time] = dateTime; + + // Step 1. + auto balancedTime = BalanceTime(time, nanoseconds); + MOZ_ASSERT(-1 <= balancedTime.days && balancedTime.days <= 1); + + // Step 2. + auto balancedDate = + BalanceISODate(date.year, date.month, date.day + balancedTime.days); + + // Step 3. + return {balancedDate, balancedTime.time}; +} + +/** + * GetPlainDateTimeFor ( timeZoneRec, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +static PlainDateTimeObject* GetPlainDateTimeFor( + JSContext* cx, Handle<TimeZoneValue> timeZone, + Handle<Wrapped<InstantObject*>> instant, Handle<CalendarValue> calendar) { + // Step 1. (Not applicable in our implementation.) + + // Steps 2-3. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return nullptr; + } + + // Step 4. + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + auto* unwrappedInstant = instant.unwrap(cx); + if (!unwrappedInstant) { + return nullptr; + } + + // Steps 5-7. + auto dateTime = + GetPlainDateTimeFor(ToInstant(unwrappedInstant), offsetNanoseconds); + + // FIXME: spec issue - CreateTemporalDateTime is infallible + // https://github.com/tc39/proposal-temporal/issues/2523 + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + return CreateTemporalDateTime(cx, dateTime, calendar); +} + +/** + * GetPlainDateTimeFor ( timeZoneRec, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +PlainDateTime js::temporal::GetPlainDateTimeFor(const Instant& instant, + int64_t offsetNanoseconds) { + // Steps 1-3. (Not applicable) + + // Step 4. + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // TODO: Steps 5-6 can be combined into a single operation to improve perf. + + // Step 5. + PlainDateTime dateTime = GetISOPartsFromEpoch(instant); + + // Step 6. + auto balanced = BalanceISODateTime(dateTime, offsetNanoseconds); + + // FIXME: spec issue - CreateTemporalDateTime is infallible + // https://github.com/tc39/proposal-temporal/issues/2523 + MOZ_ASSERT(ISODateTimeWithinLimits(balanced)); + + // Step 7. + return balanced; +} + +/** + * GetPlainDateTimeFor ( timeZone, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +bool js::temporal::GetPlainDateTimeFor(JSContext* cx, + Handle<TimeZoneRecord> timeZone, + const Instant& instant, + PlainDateTime* result) { + MOZ_ASSERT(IsValidEpochInstant(instant)); + + // Step 1. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Steps 2-3. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return false; + } + + // Step 4. + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Steps 5-7. + *result = GetPlainDateTimeFor(instant, offsetNanoseconds); + return true; +} + +/** + * GetPlainDateTimeFor ( timeZone, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +bool js::temporal::GetPlainDateTimeFor(JSContext* cx, + Handle<TimeZoneValue> timeZone, + const Instant& instant, + PlainDateTime* result) { + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + }, + &timeZoneRec)) { + return false; + } + + return GetPlainDateTimeFor(cx, timeZoneRec, instant, result); +} + +/** + * GetPlainDateTimeFor ( timeZone, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +PlainDateTimeObject* js::temporal::GetPlainDateTimeFor( + JSContext* cx, Handle<TimeZoneValue> timeZone, const Instant& instant, + Handle<CalendarValue> calendar) { + // Steps 1-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, timeZone, instant, &dateTime)) { + return nullptr; + } + + // FIXME: spec issue - CreateTemporalDateTime is infallible + // https://github.com/tc39/proposal-temporal/issues/2523 + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + // Step 7. + return CreateTemporalDateTime(cx, dateTime, calendar); +} + +/** + * GetPlainDateTimeFor ( timeZone, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +PlainDateTimeObject* js::temporal::GetPlainDateTimeFor( + JSContext* cx, const Instant& instant, Handle<CalendarValue> calendar, + int64_t offsetNanoseconds) { + MOZ_ASSERT(IsValidEpochInstant(instant)); + + // Steps 1-6. + auto dateTime = GetPlainDateTimeFor(instant, offsetNanoseconds); + + // FIXME: spec issue - CreateTemporalDateTime is infallible + // https://github.com/tc39/proposal-temporal/issues/2523 + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + // Step 7. + return CreateTemporalDateTime(cx, dateTime, calendar); +} + +/** + * Temporal.TimeZone.prototype.getPossibleInstantsFor ( dateTime ) + */ +static bool BuiltinGetPossibleInstantsFor( + JSContext* cx, Handle<TimeZoneObjectMaybeBuiltin*> timeZone, + const PlainDateTime& dateTime, EpochInstantList& possibleInstants) { + MOZ_ASSERT(possibleInstants.length() == 0); + + // Steps 1-3. (Not applicable) + + // Step 4. + if (timeZone->offsetMinutes().isInt32()) { + int32_t offsetMin = timeZone->offsetMinutes().toInt32(); + MOZ_ASSERT(std::abs(offsetMin) < UnitsPerDay(TemporalUnit::Minute)); + + // Step 4.a. + auto epochInstant = + GetUTCEpochNanoseconds(dateTime, InstantSpan::fromMinutes(offsetMin)); + + // Step 4.b. + possibleInstants.append(epochInstant); + } else { + // Step 5. + if (!GetNamedTimeZoneEpochNanoseconds(cx, timeZone, dateTime, + possibleInstants)) { + return false; + } + } + + MOZ_ASSERT(possibleInstants.length() <= 2); + + // Step 7.b. + for (const auto& epochInstant : possibleInstants) { + if (!IsValidEpochInstant(epochInstant)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + } + + // Steps 6-8. (Handled in the caller). + return true; +} + +static bool BuiltinGetPossibleInstantsFor( + JSContext* cx, Handle<TimeZoneObjectMaybeBuiltin*> timeZone, + const PlainDateTime& dateTime, MutableHandle<InstantVector> list) { + // Temporal.TimeZone.prototype.getInstantFor, step 4. + EpochInstantList possibleInstants; + if (!BuiltinGetPossibleInstantsFor(cx, timeZone, dateTime, + possibleInstants)) { + return false; + } + + // Temporal.TimeZone.prototype.getInstantFor, step 7. + for (const auto& possibleInstant : possibleInstants) { + auto* instant = CreateTemporalInstant(cx, possibleInstant); + if (!instant) { + return false; + } + + if (!list.append(instant)) { + return false; + } + } + return true; +} + +/** + * GetPossibleInstantsFor ( timeZoneRec, dateTime ) + */ +static bool GetPossibleInstantsForSlow( + JSContext* cx, Handle<TimeZoneRecord> timeZone, + Handle<Wrapped<PlainDateTimeObject*>> dateTime, + MutableHandle<InstantVector> list) { + // Step 1. (Inlined call to TimeZoneMethodsRecordCall) + Rooted<Value> fval(cx, ObjectValue(*timeZone.getPossibleInstantsFor())); + auto thisv = timeZone.receiver().toObject(); + Rooted<Value> arg(cx, ObjectValue(*dateTime)); + Rooted<Value> rval(cx); + if (!Call(cx, fval, thisv, arg, &rval)) { + return false; + } + + // Step 2. (Not applicable) + + // Step 3. + JS::ForOfIterator iterator(cx); + if (!iterator.init(rval)) { + return false; + } + + // Step 4. (Not applicable in our implementation.) + + // Steps 5-6. + Rooted<Value> nextValue(cx); + while (true) { + // Steps 6.a and 6.b.i. + bool done; + if (!iterator.next(&nextValue, &done)) { + return false; + } + if (done) { + break; + } + + // Steps 6.b.ii. + if (nextValue.isObject()) { + JSObject* obj = &nextValue.toObject(); + if (obj->canUnwrapAs<InstantObject>()) { + // Step 6.b.iii. + if (!list.append(obj)) { + return false; + } + continue; + } + } + + // Step 6.b.ii.1. + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_IGNORE_STACK, nextValue, + nullptr, "not an instant"); + + // Step 6.b.ii.2. + iterator.closeThrow(); + return false; + } + + // Step 7. + return true; +} + +/** + * GetPossibleInstantsFor ( timeZoneRec, dateTime ) + */ +static bool GetPossibleInstantsFor( + JSContext* cx, Handle<TimeZoneRecord> timeZone, + Handle<Wrapped<PlainDateTimeObject*>> dateTimeObj, + const PlainDateTime& dateTime, MutableHandle<InstantVector> list) { + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + // Step 2. (Reordered) + auto getPossibleInstantsFor = timeZone.getPossibleInstantsFor(); + if (!getPossibleInstantsFor) { + bool arrayIterationSane; + if (timeZone.receiver().isString()) { + // "String" time zones don't perform observable array iteration. + arrayIterationSane = true; + } else { + // "Object" time zones need to ensure array iteration is still sane. + if (!IsArrayIterationSane(cx, &arrayIterationSane)) { + return false; + } + } + + if (arrayIterationSane) { + auto builtin = timeZone.receiver().toTimeZoneObjectMaybeBuiltin(); + return BuiltinGetPossibleInstantsFor(cx, builtin, dateTime, list); + } + } + + // Steps 1 and 3-7. + return GetPossibleInstantsForSlow(cx, timeZone, dateTimeObj, list); +} + +/** + * GetPossibleInstantsFor ( timeZoneRec, dateTime ) + */ +bool js::temporal::GetPossibleInstantsFor( + JSContext* cx, Handle<TimeZoneRecord> timeZone, + Handle<PlainDateTimeWithCalendar> dateTime, + MutableHandle<InstantVector> list) { + // Step 2. (Reordered) + auto getPossibleInstantsFor = timeZone.getPossibleInstantsFor(); + if (!getPossibleInstantsFor) { + bool arrayIterationSane; + if (timeZone.receiver().isString()) { + // "String" time zones don't perform observable array iteration. + arrayIterationSane = true; + } else { + // "Object" time zones need to ensure array iteration is still sane. + if (!IsArrayIterationSane(cx, &arrayIterationSane)) { + return false; + } + } + + if (arrayIterationSane) { + auto builtin = timeZone.receiver().toTimeZoneObjectMaybeBuiltin(); + return BuiltinGetPossibleInstantsFor(cx, builtin, + ToPlainDateTime(dateTime), list); + } + } + + Rooted<PlainDateTimeObject*> dateTimeObj( + cx, CreateTemporalDateTime(cx, ToPlainDateTime(dateTime), + dateTime.calendar())); + if (!dateTimeObj) { + return false; + } + + // Steps 1 and 3-7. + return GetPossibleInstantsForSlow(cx, timeZone, dateTimeObj, list); +} + +/** + * AddTime ( hour, minute, second, millisecond, microsecond, nanosecond, hours, + * minutes, seconds, milliseconds, microseconds, nanoseconds ) + */ +static auto AddTime(const PlainTime& time, int64_t nanoseconds) { + MOZ_ASSERT(IsValidTime(time)); + MOZ_ASSERT(std::abs(nanoseconds) <= 2 * ToNanoseconds(TemporalUnit::Day)); + + // Steps 1-7. + return BalanceTime(time, nanoseconds); +} + +/** + * DisambiguatePossibleInstants ( possibleInstants, timeZoneRec, dateTime, + * disambiguation ) + */ +bool js::temporal::DisambiguatePossibleInstants( + JSContext* cx, Handle<InstantVector> possibleInstants, + Handle<TimeZoneRecord> timeZone, const PlainDateTime& dateTime, + TemporalDisambiguation disambiguation, + MutableHandle<Wrapped<InstantObject*>> result) { + // Step 1. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + // Step 2. + MOZ_ASSERT_IF(possibleInstants.empty() && + disambiguation != TemporalDisambiguation::Reject, + TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Steps 3-4. + if (possibleInstants.length() == 1) { + result.set(possibleInstants[0]); + return true; + } + + // Steps 5-6. + if (!possibleInstants.empty()) { + // Step 5.a. + if (disambiguation == TemporalDisambiguation::Earlier || + disambiguation == TemporalDisambiguation::Compatible) { + result.set(possibleInstants[0]); + return true; + } + + // Step 5.b. + if (disambiguation == TemporalDisambiguation::Later) { + size_t last = possibleInstants.length() - 1; + result.set(possibleInstants[last]); + return true; + } + + // Step 5.c. + MOZ_ASSERT(disambiguation == TemporalDisambiguation::Reject); + + // Step 5.d. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS); + return false; + } + + // Step 7. + if (disambiguation == TemporalDisambiguation::Reject) { + // TODO: Improve error message to say the date was skipped. + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS); + return false; + } + + constexpr auto oneDay = + InstantSpan::fromNanoseconds(ToNanoseconds(TemporalUnit::Day)); + + // Step 8. + auto epochNanoseconds = GetUTCEpochNanoseconds(dateTime); + + // Steps 9 and 11. + auto dayBefore = epochNanoseconds - oneDay; + + // Step 10. + if (!IsValidEpochInstant(dayBefore)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 12 and 14. + auto dayAfter = epochNanoseconds + oneDay; + + // Step 13. + if (!IsValidEpochInstant(dayAfter)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 15. + int64_t offsetBefore; + if (!GetOffsetNanosecondsFor(cx, timeZone, dayBefore, &offsetBefore)) { + return false; + } + MOZ_ASSERT(std::abs(offsetBefore) < ToNanoseconds(TemporalUnit::Day)); + + // Step 16. + int64_t offsetAfter; + if (!GetOffsetNanosecondsFor(cx, timeZone, dayAfter, &offsetAfter)) { + return false; + } + MOZ_ASSERT(std::abs(offsetAfter) < ToNanoseconds(TemporalUnit::Day)); + + // Step 17. + int64_t nanoseconds = offsetAfter - offsetBefore; + + // Step 18. + if (disambiguation == TemporalDisambiguation::Earlier) { + // Step 18.a. + auto earlierTime = ::AddTime(dateTime.time, -nanoseconds); + MOZ_ASSERT(std::abs(earlierTime.days) <= 2, + "subtracting nanoseconds is at most two days"); + + // Step 18.b. + PlainDate earlierDate; + if (!AddISODate(cx, dateTime.date, {0, 0, 0, double(earlierTime.days)}, + TemporalOverflow::Constrain, &earlierDate)) { + return false; + } + + // Step 18.c. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + Rooted<PlainDateTimeWithCalendar> earlierDateTime( + cx, + PlainDateTimeWithCalendar{{earlierDate, earlierTime.time}, calendar}); + + // Step 18.d. + Rooted<InstantVector> earlierInstants(cx, InstantVector(cx)); + if (!GetPossibleInstantsFor(cx, timeZone, earlierDateTime, + &earlierInstants)) { + return false; + } + + // Step 18.e. + if (earlierInstants.empty()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS); + return false; + } + + // Step 18.f. + result.set(earlierInstants[0]); + return true; + } + + // Step 19. + MOZ_ASSERT(disambiguation == TemporalDisambiguation::Compatible || + disambiguation == TemporalDisambiguation::Later); + + // Step 20. + auto laterTime = ::AddTime(dateTime.time, nanoseconds); + MOZ_ASSERT(std::abs(laterTime.days) <= 2, + "adding nanoseconds is at most two days"); + + // Step 21. + PlainDate laterDate; + if (!AddISODate(cx, dateTime.date, {0, 0, 0, double(laterTime.days)}, + TemporalOverflow::Constrain, &laterDate)) { + return false; + } + + // Step 22. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + Rooted<PlainDateTimeWithCalendar> laterDateTime( + cx, PlainDateTimeWithCalendar{{laterDate, laterTime.time}, calendar}); + + // Step 23. + Rooted<InstantVector> laterInstants(cx, InstantVector(cx)); + if (!GetPossibleInstantsFor(cx, timeZone, laterDateTime, &laterInstants)) { + return false; + } + + // Steps 24-25. + if (laterInstants.empty()) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_INSTANT_AMBIGUOUS); + return false; + } + + // Step 26. + size_t last = laterInstants.length() - 1; + result.set(laterInstants[last]); + return true; +} + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +static bool GetInstantFor(JSContext* cx, Handle<TimeZoneRecord> timeZone, + Handle<Wrapped<PlainDateTimeObject*>> dateTime, + TemporalDisambiguation disambiguation, + MutableHandle<Wrapped<InstantObject*>> result) { + // Step 1. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Step 2. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + auto* unwrappedDateTime = dateTime.unwrap(cx); + if (!unwrappedDateTime) { + return false; + } + auto plainDateTime = ToPlainDateTime(unwrappedDateTime); + + // Step 3. + Rooted<InstantVector> possibleInstants(cx, InstantVector(cx)); + if (!GetPossibleInstantsFor(cx, timeZone, dateTime, plainDateTime, + &possibleInstants)) { + return false; + } + + // Step 4. + return DisambiguatePossibleInstants(cx, possibleInstants, timeZone, + plainDateTime, disambiguation, result); +} + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +static bool GetInstantFor(JSContext* cx, Handle<TimeZoneValue> timeZone, + Handle<Wrapped<PlainDateTimeObject*>> dateTime, + TemporalDisambiguation disambiguation, + MutableHandle<Wrapped<InstantObject*>> result) { + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { + return false; + } + + return GetInstantFor(cx, timeZoneRec, dateTime, disambiguation, result); +} + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +bool js::temporal::GetInstantFor(JSContext* cx, Handle<TimeZoneValue> timeZone, + Handle<PlainDateTimeObject*> dateTime, + TemporalDisambiguation disambiguation, + Instant* result) { + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { + return false; + } + + Rooted<Wrapped<InstantObject*>> instant(cx); + if (!::GetInstantFor(cx, timeZoneRec, dateTime, disambiguation, &instant)) { + return false; + } + + auto* unwrappedInstant = instant.unwrap(cx); + if (!unwrappedInstant) { + return false; + } + + *result = ToInstant(unwrappedInstant); + return true; +} + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +bool js::temporal::GetInstantFor(JSContext* cx, Handle<TimeZoneRecord> timeZone, + Handle<PlainDateTimeWithCalendar> dateTime, + TemporalDisambiguation disambiguation, + Instant* result) { + // Step 1. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Step 2. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + // Step 3. + Rooted<InstantVector> possibleInstants(cx, InstantVector(cx)); + if (!GetPossibleInstantsFor(cx, timeZone, dateTime, &possibleInstants)) { + return false; + } + + // Step 4. + Rooted<Wrapped<InstantObject*>> instant(cx); + if (!DisambiguatePossibleInstants(cx, possibleInstants, timeZone, + ToPlainDateTime(dateTime), disambiguation, + &instant)) { + return false; + } + + auto* unwrappedInstant = instant.unwrap(cx); + if (!unwrappedInstant) { + return false; + } + + *result = ToInstant(unwrappedInstant); + return true; +} + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +bool js::temporal::GetInstantFor(JSContext* cx, Handle<TimeZoneValue> timeZone, + Handle<PlainDateTimeWithCalendar> dateTime, + TemporalDisambiguation disambiguation, + Instant* result) { + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { + return false; + } + + return GetInstantFor(cx, timeZoneRec, dateTime, disambiguation, result); +} + +/** + * IsOffsetTimeZoneIdentifier ( offsetString ) + * + * Return true if |offsetString| is the prefix of a time zone offset string. + * Time zone offset strings are be parsed through the |TimeZoneUTCOffsetName| + * production. + * + * TimeZoneUTCOffsetName : + * UTCOffsetMinutePrecision + * + * UTCOffsetMinutePrecision : + * Sign Hour[+Padded] + * Sign Hour[+Padded] TimeSeparator[+Extended] MinuteSecond + * Sign Hour[+Padded] TimeSeparator[~Extended] MinuteSecond + * + * Sign : + * ASCIISign + * U+2212 + * + * ASCIISign : one of + - + * + * NOTE: IANA time zone identifiers can't start with |Sign|. + */ +static bool IsOffsetTimeZoneIdentifierPrefix(JSLinearString* offsetString) { + // Empty string can't be the prefix of |TimeZoneUTCOffsetName|. + if (offsetString->empty()) { + return false; + } + + // Return true iff |offsetString| starts with |Sign|. + char16_t ch = offsetString->latin1OrTwoByteChar(0); + return ch == '+' || ch == '-' || ch == 0x2212; +} + +/** + * Temporal.TimeZone ( identifier ) + */ +static bool TimeZoneConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.TimeZone")) { + return false; + } + + // Step 2. + if (!args.requireAtLeast(cx, "Temporal.TimeZone", 1)) { + return false; + } + + if (!args[0].isString()) { + ReportValueError(cx, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK, args[0], + nullptr, "not a string"); + return false; + } + + Rooted<JSLinearString*> identifier(cx, args[0].toString()->ensureLinear(cx)); + if (!identifier) { + return false; + } + + Rooted<JSString*> canonical(cx); + Rooted<Value> offsetMinutes(cx); + if (IsOffsetTimeZoneIdentifierPrefix(identifier)) { + // Step 3. + int32_t minutes; + if (!ParseTimeZoneOffsetString(cx, identifier, &minutes)) { + return false; + } + MOZ_ASSERT(std::abs(minutes) < UnitsPerDay(TemporalUnit::Minute)); + + canonical = FormatOffsetTimeZoneIdentifier(cx, minutes); + if (!canonical) { + return false; + } + + offsetMinutes.setInt32(minutes); + } else { + // Step 4. + canonical = ValidateAndCanonicalizeTimeZoneName(cx, identifier); + if (!canonical) { + return false; + } + + offsetMinutes.setUndefined(); + } + + // Step 5. + auto* timeZone = CreateTemporalTimeZone(cx, args, canonical, offsetMinutes); + if (!timeZone) { + return false; + } + + args.rval().setObject(*timeZone); + return true; +} + +/** + * Temporal.TimeZone.from ( item ) + */ +static bool TimeZone_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // Step 2. + auto* obj = ToTemporalTimeZoneObject(cx, timeZone); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.TimeZone.prototype.equals ( timeZoneLike ) + */ +static bool TimeZone_equals(JSContext* cx, const CallArgs& args) { + Rooted<TimeZoneValue> timeZone(cx, &args.thisv().toObject()); + + // Step 3. + Rooted<TimeZoneValue> other(cx); + if (!ToTemporalTimeZone(cx, args.get(0), &other)) { + return false; + } + + // Step 4. + bool equals; + if (!TimeZoneEquals(cx, timeZone, other, &equals)) { + return false; + } + + args.rval().setBoolean(equals); + return true; +} + +/** + * Temporal.TimeZone.prototype.equals ( timeZoneLike ) + */ +static bool TimeZone_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_equals>(cx, args); +} + +/** + * Temporal.TimeZone.prototype.getOffsetNanosecondsFor ( instant ) + */ +static bool TimeZone_getOffsetNanosecondsFor(JSContext* cx, + const CallArgs& args) { + Rooted<TimeZoneObject*> timeZone( + cx, &args.thisv().toObject().as<TimeZoneObject>()); + + // Step 3. + Instant instant; + if (!ToTemporalInstant(cx, args.get(0), &instant)) { + return false; + } + + // Steps 4-5. + int64_t offset; + if (!BuiltinGetOffsetNanosecondsFor(cx, timeZone, instant, &offset)) { + return false; + } + + args.rval().setNumber(offset); + return true; +} + +/** + * Temporal.TimeZone.prototype.getOffsetNanosecondsFor ( instant ) + */ +static bool TimeZone_getOffsetNanosecondsFor(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_getOffsetNanosecondsFor>( + cx, args); +} + +/** + * Temporal.TimeZone.prototype.getOffsetStringFor ( instant ) + */ +static bool TimeZone_getOffsetStringFor(JSContext* cx, const CallArgs& args) { + Rooted<TimeZoneValue> timeZone(cx, &args.thisv().toObject()); + + // FIXME: spec issue - CreateTimeZoneMethodsRecord called before + // ToTemporalInstant whereas TimeZone.p.{getPlainDateTimeFor,getInstantFor} + // first convert the input arguments. + + // Step 3. + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + }, + &timeZoneRec)) { + return false; + } + + // Step 4. + Rooted<Wrapped<InstantObject*>> instant(cx, + ToTemporalInstant(cx, args.get(0))); + if (!instant) { + return false; + } + + // Step 5. + JSString* str = GetOffsetStringFor(cx, timeZoneRec, instant); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.TimeZone.prototype.getOffsetStringFor ( instant ) + */ +static bool TimeZone_getOffsetStringFor(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_getOffsetStringFor>(cx, + args); +} + +/** + * Temporal.TimeZone.prototype.getPlainDateTimeFor ( instant [, calendarLike ] ) + */ +static bool TimeZone_getPlainDateTimeFor(JSContext* cx, const CallArgs& args) { + Rooted<TimeZoneValue> timeZone(cx, &args.thisv().toObject()); + + // Step 3. + Rooted<Wrapped<InstantObject*>> instant(cx, + ToTemporalInstant(cx, args.get(0))); + if (!instant) { + return false; + } + + // Step 4. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendarWithISODefault(cx, args.get(1), &calendar)) { + return false; + } + + // Steps 5-6. + auto* result = GetPlainDateTimeFor(cx, timeZone, instant, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.TimeZone.prototype.getPlainDateTimeFor ( instant [, calendarLike ] ) + */ +static bool TimeZone_getPlainDateTimeFor(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_getPlainDateTimeFor>(cx, + args); +} + +/** + * Temporal.TimeZone.prototype.getInstantFor ( dateTime [ , options ] ) + */ +static bool TimeZone_getInstantFor(JSContext* cx, const CallArgs& args) { + Rooted<TimeZoneValue> timeZone(cx, &args.thisv().toObject()); + + // Step 3. + Rooted<Wrapped<PlainDateTimeObject*>> dateTime( + cx, ToTemporalDateTime(cx, args.get(0))); + if (!dateTime) { + return false; + } + + // Steps 4-5. + auto disambiguation = TemporalDisambiguation::Compatible; + if (args.hasDefined(1)) { + // Step 4. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "getInstantFor", args[1])); + if (!options) { + return false; + } + + // Step 5. + if (!ToTemporalDisambiguation(cx, options, &disambiguation)) { + return false; + } + } + + // Steps 6-7. + Rooted<Wrapped<InstantObject*>> result(cx); + if (!::GetInstantFor(cx, timeZone, dateTime, disambiguation, &result)) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.TimeZone.prototype.getInstantFor ( dateTime [ , options ] ) + */ +static bool TimeZone_getInstantFor(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_getInstantFor>(cx, args); +} + +/** + * Temporal.TimeZone.prototype.getPossibleInstantsFor ( dateTime ) + */ +static bool TimeZone_getPossibleInstantsFor(JSContext* cx, + const CallArgs& args) { + Rooted<TimeZoneObject*> timeZone( + cx, &args.thisv().toObject().as<TimeZoneObject>()); + + // Step 3. + PlainDateTime dateTime; + if (!ToTemporalDateTime(cx, args.get(0), &dateTime)) { + return false; + } + + // Steps 4-5. + EpochInstantList possibleInstants; + if (!BuiltinGetPossibleInstantsFor(cx, timeZone, dateTime, + possibleInstants)) { + return false; + } + + // Step 6. + size_t length = possibleInstants.length(); + Rooted<ArrayObject*> result(cx, NewDenseFullyAllocatedArray(cx, length)); + if (!result) { + return false; + } + result->ensureDenseInitializedLength(0, length); + + // Step 7. + for (size_t i = 0; i < length; i++) { + // Step 7.a. (Already performed in step 4 in our implementation.) + MOZ_ASSERT(IsValidEpochInstant(possibleInstants[i])); + + // Step 7.b. + auto* instant = CreateTemporalInstant(cx, possibleInstants[i]); + if (!instant) { + return false; + } + + // Step 7.c. + result->initDenseElement(i, ObjectValue(*instant)); + } + + // Step 8. + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.TimeZone.prototype.getPossibleInstantsFor ( dateTime ) + */ +static bool TimeZone_getPossibleInstantsFor(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_getPossibleInstantsFor>( + cx, args); +} + +/** + * Temporal.TimeZone.prototype.getNextTransition ( startingPoint ) + */ +static bool TimeZone_getNextTransition(JSContext* cx, const CallArgs& args) { + Rooted<TimeZoneObject*> timeZone( + cx, &args.thisv().toObject().as<TimeZoneObject>()); + + // Step 3. + Instant startingPoint; + if (!ToTemporalInstant(cx, args.get(0), &startingPoint)) { + return false; + } + + // Step 4. + if (!timeZone->offsetMinutes().isUndefined()) { + args.rval().setNull(); + return true; + } + + // Step 5. + mozilla::Maybe<Instant> transition; + if (!GetNamedTimeZoneNextTransition(cx, timeZone, startingPoint, + &transition)) { + return false; + } + + // Step 6. + if (!transition) { + args.rval().setNull(); + return true; + } + + // Step 7. + auto* instant = CreateTemporalInstant(cx, *transition); + if (!instant) { + return false; + } + + args.rval().setObject(*instant); + return true; +} + +/** + * Temporal.TimeZone.prototype.getNextTransition ( startingPoint ) + */ +static bool TimeZone_getNextTransition(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_getNextTransition>(cx, args); +} + +/** + * Temporal.TimeZone.prototype.getPreviousTransition ( startingPoint ) + */ +static bool TimeZone_getPreviousTransition(JSContext* cx, + const CallArgs& args) { + Rooted<TimeZoneObject*> timeZone( + cx, &args.thisv().toObject().as<TimeZoneObject>()); + + // Step 3. + Instant startingPoint; + if (!ToTemporalInstant(cx, args.get(0), &startingPoint)) { + return false; + } + + // Step 4. + if (!timeZone->offsetMinutes().isUndefined()) { + args.rval().setNull(); + return true; + } + + // Step 5. + mozilla::Maybe<Instant> transition; + if (!GetNamedTimeZonePreviousTransition(cx, timeZone, startingPoint, + &transition)) { + return false; + } + + // Step 6. + if (!transition) { + args.rval().setNull(); + return true; + } + + // Step 7. + auto* instant = CreateTemporalInstant(cx, *transition); + if (!instant) { + return false; + } + + args.rval().setObject(*instant); + return true; +} + +/** + * Temporal.TimeZone.prototype.getPreviousTransition ( startingPoint ) + */ +static bool TimeZone_getPreviousTransition(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_getPreviousTransition>(cx, + args); +} + +/** + * Temporal.TimeZone.prototype.toString ( ) + */ +static bool TimeZone_toString(JSContext* cx, const CallArgs& args) { + auto* timeZone = &args.thisv().toObject().as<TimeZoneObject>(); + + // Steps 3-4. + args.rval().setString(timeZone->identifier()); + return true; +} + +/** + * Temporal.TimeZone.prototype.toString ( ) + */ +static bool TimeZone_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_toString>(cx, args); +} + +/** + * Temporal.TimeZone.prototype.toJSON ( ) + */ +static bool TimeZone_toJSON(JSContext* cx, const CallArgs& args) { + auto* timeZone = &args.thisv().toObject().as<TimeZoneObject>(); + + // Steps 3-4. + args.rval().setString(timeZone->identifier()); + return true; +} + +/** + * Temporal.TimeZone.prototype.toJSON ( ) + */ +static bool TimeZone_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_toJSON>(cx, args); +} + +/** + * get Temporal.TimeZone.prototype.id + */ +static bool TimeZone_id(JSContext* cx, const CallArgs& args) { + auto* timeZone = &args.thisv().toObject().as<TimeZoneObject>(); + + // Steps 3-4. + args.rval().setString(timeZone->identifier()); + return true; +} + +/** + * get Temporal.TimeZone.prototype.id + */ +static bool TimeZone_id(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsTimeZone, TimeZone_id>(cx, args); +} + +void js::temporal::TimeZoneObjectMaybeBuiltin::finalize(JS::GCContext* gcx, + JSObject* obj) { + MOZ_ASSERT(gcx->onMainThread()); + + if (auto* timeZone = obj->as<TimeZoneObjectMaybeBuiltin>().getTimeZone()) { + intl::RemoveICUCellMemory(gcx, obj, TimeZoneObject::EstimatedMemoryUse); + delete timeZone; + } +} + +const JSClassOps TimeZoneObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + TimeZoneObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass TimeZoneObject::class_ = { + "Temporal.TimeZone", + JSCLASS_HAS_RESERVED_SLOTS(TimeZoneObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_TimeZone) | + JSCLASS_FOREGROUND_FINALIZE, + &TimeZoneObject::classOps_, + &TimeZoneObject::classSpec_, +}; + +const JSClass& TimeZoneObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec TimeZone_methods[] = { + JS_FN("from", TimeZone_from, 1, 0), + JS_FS_END, +}; + +static const JSFunctionSpec TimeZone_prototype_methods[] = { + JS_FN("equals", TimeZone_equals, 1, 0), + JS_FN("getOffsetNanosecondsFor", TimeZone_getOffsetNanosecondsFor, 1, 0), + JS_FN("getOffsetStringFor", TimeZone_getOffsetStringFor, 1, 0), + JS_FN("getPlainDateTimeFor", TimeZone_getPlainDateTimeFor, 1, 0), + JS_FN("getInstantFor", TimeZone_getInstantFor, 1, 0), + JS_FN("getPossibleInstantsFor", TimeZone_getPossibleInstantsFor, 1, 0), + JS_FN("getNextTransition", TimeZone_getNextTransition, 1, 0), + JS_FN("getPreviousTransition", TimeZone_getPreviousTransition, 1, 0), + JS_FN("toString", TimeZone_toString, 0, 0), + JS_FN("toJSON", TimeZone_toJSON, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec TimeZone_prototype_properties[] = { + JS_PSG("id", TimeZone_id, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.TimeZone", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec TimeZoneObject::classSpec_ = { + GenericCreateConstructor<TimeZoneConstructor, 1, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<TimeZoneObject>, + TimeZone_methods, + nullptr, + TimeZone_prototype_methods, + TimeZone_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; + +const JSClassOps BuiltinTimeZoneObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + BuiltinTimeZoneObject::finalize, // finalize + nullptr, // call + nullptr, // construct + nullptr, // trace +}; + +const JSClass BuiltinTimeZoneObject::class_ = { + "Temporal.BuiltinTimeZone", + JSCLASS_HAS_RESERVED_SLOTS(BuiltinTimeZoneObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &BuiltinTimeZoneObject::classOps_, +}; diff --git a/js/src/builtin/temporal/TimeZone.h b/js/src/builtin/temporal/TimeZone.h new file mode 100644 index 0000000000..f1d0bf3f1f --- /dev/null +++ b/js/src/builtin/temporal/TimeZone.h @@ -0,0 +1,625 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_TimeZone_h +#define builtin_temporal_TimeZone_h + +#include "mozilla/Assertions.h" +#include "mozilla/EnumSet.h" + +#include <stddef.h> +#include <stdint.h> + +#include "builtin/temporal/Wrapped.h" +#include "js/GCVector.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/JSObject.h" +#include "vm/NativeObject.h" + +class JSLinearString; +class JS_PUBLIC_API JSTracer; +struct JSClassOps; + +namespace js { +struct ClassSpec; +} + +namespace mozilla::intl { +class TimeZone; +} + +namespace js::temporal { + +class TimeZoneObjectMaybeBuiltin : public NativeObject { + public: + static constexpr uint32_t IDENTIFIER_SLOT = 0; + static constexpr uint32_t OFFSET_MINUTES_SLOT = 1; + static constexpr uint32_t INTL_TIMEZONE_SLOT = 2; + static constexpr uint32_t SLOT_COUNT = 3; + + // Estimated memory use for intl::TimeZone (see IcuMemoryUsage). + static constexpr size_t EstimatedMemoryUse = 6840; + + JSString* identifier() const { + return getFixedSlot(IDENTIFIER_SLOT).toString(); + } + + const auto& offsetMinutes() const { + return getFixedSlot(OFFSET_MINUTES_SLOT); + } + + mozilla::intl::TimeZone* getTimeZone() const { + const auto& slot = getFixedSlot(INTL_TIMEZONE_SLOT); + if (slot.isUndefined()) { + return nullptr; + } + return static_cast<mozilla::intl::TimeZone*>(slot.toPrivate()); + } + + void setTimeZone(mozilla::intl::TimeZone* timeZone) { + setFixedSlot(INTL_TIMEZONE_SLOT, JS::PrivateValue(timeZone)); + } + + protected: + static void finalize(JS::GCContext* gcx, JSObject* obj); +}; + +class TimeZoneObject : public TimeZoneObjectMaybeBuiltin { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + private: + static const JSClassOps classOps_; + static const ClassSpec classSpec_; +}; + +class BuiltinTimeZoneObject : public TimeZoneObjectMaybeBuiltin { + public: + static const JSClass class_; + + private: + static const JSClassOps classOps_; +}; + +} /* namespace js::temporal */ + +template <> +inline bool JSObject::is<js::temporal::TimeZoneObjectMaybeBuiltin>() const { + return is<js::temporal::TimeZoneObject>() || + is<js::temporal::BuiltinTimeZoneObject>(); +} + +namespace js::temporal { + +/** + * Temporal time zones can be either objects or strings. Objects are either + * instances of `Temporal.TimeZone` or user-defined time zones. Strings are + * either canonical time zone identifiers or time zone offset strings. + * + * Examples of valid Temporal time zones: + * - Any object + * - "UTC" + * - "America/New_York" + * - "+00:00" + * + * Examples of invalid Temporal time zones: + * - Number values + * - "utc" (wrong case) + * - "Etc/UTC" (canonical name is "UTC") + * - "+00" (missing minutes part) + * - "+00:00:00" (sub-minute precision) + * - "+00:00:01" (sub-minute precision) + * - "-00:00" (wrong sign for zero offset) + * + * String-valued Temporal time zones are an optimization to avoid allocating + * `Temporal.TimeZone` objects when creating `Temporal.ZonedDateTime` objects. + * For example `Temporal.ZonedDateTime.from("1970-01-01[UTC]")` doesn't require + * to allocate a fresh `Temporal.TimeZone` object for the "UTC" time zone. + * + * The specification creates new `Temporal.TimeZone` objects whenever any + * operation is performed on a string-valued Temporal time zone. This newly + * created object can't be accessed by the user and implementations are expected + * to optimize away the allocation. + * + * The following two implementation approaches are possible: + * + * 1. Represent string-valued time zones as JSStrings. Additionally keep a + * mapping from JSString to `mozilla::intl::TimeZone` to avoid repeatedly + * creating new `mozilla::intl::TimeZone` for time zone operations. Offset + * string time zones have to be special cased, because they don't use + * `mozilla::intl::TimeZone`. Either detect offset strings by checking the + * time zone identifier or store offset strings as the offset in minutes + * value to avoid reparsing the offset string again and again. + * 2. Represent string-valued time zones as `Temporal.TimeZone`-like objects. + * These internal `Temporal.TimeZone`-like objects must not be exposed to + * user-code. + * + * Option 2 is a bit easier to implement, so we use this approach for now. + */ +class TimeZoneValue final { + JSObject* object_ = nullptr; + + public: + /** + * Default initialize this TimeZoneValue. + */ + TimeZoneValue() = default; + + /** + * Initialize this TimeZoneValue with a "string" time zone object. + */ + explicit TimeZoneValue(BuiltinTimeZoneObject* timeZone) : object_(timeZone) { + MOZ_ASSERT(isString()); + } + + /** + * Initialize this TimeZoneValue with an "object" time zone object. + */ + explicit TimeZoneValue(JSObject* timeZone) : object_(timeZone) { + MOZ_ASSERT(isObject()); + } + + /** + * Initialize this TimeZoneValue from a slot Value, which must be either a + * "string" or "object" time zone object. + */ + explicit TimeZoneValue(const JS::Value& value) : object_(&value.toObject()) {} + + /** + * Return true if this TimeZoneValue is not null. + */ + explicit operator bool() const { return !!object_; } + + /** + * Return true if this TimeZoneValue is a "string" time zone. + */ + bool isString() const { + return object_ && object_->is<BuiltinTimeZoneObject>(); + } + + /** + * Return true if this TimeZoneValue is an "object" time zone. + */ + bool isObject() const { return object_ && !isString(); } + + /** + * Return true if this TimeZoneValue holds a TimeZoneObjectMaybeBuiltin. + */ + bool isTimeZoneObjectMaybeBuiltin() const { + return object_ && object_->is<TimeZoneObjectMaybeBuiltin>(); + } + + /** + * Return this "string" time zone. + */ + auto* toString() const { + MOZ_ASSERT(isString()); + return &object_->as<BuiltinTimeZoneObject>(); + } + + /** + * Return this "object" time zone. + */ + JSObject* toObject() const { + MOZ_ASSERT(isObject()); + return object_; + } + + /** + * Return the underlying object as a TimeZoneObjectMaybeBuiltin. + */ + auto* toTimeZoneObjectMaybeBuiltin() const { + MOZ_ASSERT(isTimeZoneObjectMaybeBuiltin()); + return &object_->as<TimeZoneObjectMaybeBuiltin>(); + } + + /** + * Return the Value representation of this TimeZoneValue. + */ + JS::Value toValue() const { + if (isString()) { + return JS::StringValue(toString()->identifier()); + } + + MOZ_ASSERT(object_); + return JS::ObjectValue(*object_); + } + + /** + * Return the slot Value representation of this TimeZoneValue. + */ + JS::Value toSlotValue() const { + MOZ_ASSERT(object_); + return JS::ObjectValue(*object_); + } + + // Helper methods for (Mutable)WrappedPtrOperations. + auto address() { return &object_; } + auto address() const { return &object_; } + + // Trace implementation. + void trace(JSTracer* trc); +}; + +enum class TimeZoneMethod { + GetOffsetNanosecondsFor, + GetPossibleInstantsFor, +}; + +class TimeZoneRecord { + TimeZoneValue receiver_; + + // Null unless non-builtin time zone methods are used. + JSObject* getOffsetNanosecondsFor_ = nullptr; + JSObject* getPossibleInstantsFor_ = nullptr; + +#ifdef DEBUG + mozilla::EnumSet<TimeZoneMethod> lookedUp_{}; +#endif + + public: + /** + * Default initialize this TimeZoneRecord. + */ + TimeZoneRecord() = default; + + explicit TimeZoneRecord(const TimeZoneValue& receiver) + : receiver_(receiver) {} + + const auto& receiver() const { return receiver_; } + auto* getOffsetNanosecondsFor() const { return getOffsetNanosecondsFor_; } + auto* getPossibleInstantsFor() const { return getPossibleInstantsFor_; } + +#ifdef DEBUG + auto& lookedUp() const { return lookedUp_; } + auto& lookedUp() { return lookedUp_; } +#endif + + // Helper methods for (Mutable)WrappedPtrOperations. + auto* receiverDoNotUse() const { return &receiver_; } + auto* getOffsetNanosecondsForDoNotUse() const { + return &getOffsetNanosecondsFor_; + } + auto* getOffsetNanosecondsForDoNotUse() { return &getOffsetNanosecondsFor_; } + auto* getPossibleInstantsForDoNotUse() const { + return &getPossibleInstantsFor_; + } + auto* getPossibleInstantsForDoNotUse() { return &getPossibleInstantsFor_; } + + // Trace implementation. + void trace(JSTracer* trc); +}; + +struct Instant; +struct ParsedTimeZone; +struct PlainDateTime; +class CalendarValue; +class InstantObject; +class PlainDateTimeObject; +class PlainDateTimeWithCalendar; +enum class TemporalDisambiguation; + +/** + * IsValidTimeZoneName ( timeZone ) + * IsAvailableTimeZoneName ( timeZone ) + */ +bool IsValidTimeZoneName(JSContext* cx, JS::Handle<JSString*> timeZone, + JS::MutableHandle<JSAtom*> validatedTimeZone); + +/** + * CanonicalizeTimeZoneName ( timeZone ) + */ +JSString* CanonicalizeTimeZoneName(JSContext* cx, + JS::Handle<JSLinearString*> timeZone); + +/** + * IsValidTimeZoneName ( timeZone ) + * IsAvailableTimeZoneName ( timeZone ) + * CanonicalizeTimeZoneName ( timeZone ) + */ +JSString* ValidateAndCanonicalizeTimeZoneName(JSContext* cx, + JS::Handle<JSString*> timeZone); + +/** + * CreateTemporalTimeZone ( identifier [ , newTarget ] ) + */ +BuiltinTimeZoneObject* CreateTemporalTimeZone(JSContext* cx, + JS::Handle<JSString*> identifier); + +/** + * ToTemporalTimeZoneSlotValue ( temporalTimeZoneLike ) + */ +bool ToTemporalTimeZone(JSContext* cx, + JS::Handle<JS::Value> temporalTimeZoneLike, + JS::MutableHandle<TimeZoneValue> result); + +/** + * ToTemporalTimeZoneSlotValue ( temporalTimeZoneLike ) + */ +bool ToTemporalTimeZone(JSContext* cx, JS::Handle<ParsedTimeZone> string, + JS::MutableHandle<TimeZoneValue> result); + +/** + * ToTemporalTimeZoneObject ( timeZoneSlotValue ) + */ +JSObject* ToTemporalTimeZoneObject(JSContext* cx, + JS::Handle<TimeZoneValue> timeZone); + +/** + * ToTemporalTimeZoneIdentifier ( timeZoneSlotValue ) + */ +JSString* ToTemporalTimeZoneIdentifier(JSContext* cx, + JS::Handle<TimeZoneValue> timeZone); + +/** + * TimeZoneEquals ( one, two ) + */ +bool TimeZoneEquals(JSContext* cx, JS::Handle<JSString*> one, + JS::Handle<JSString*> two, bool* equals); + +/** + * TimeZoneEquals ( one, two ) + */ +bool TimeZoneEquals(JSContext* cx, JS::Handle<TimeZoneValue> one, + JS::Handle<TimeZoneValue> two, bool* equals); + +/** + * GetPlainDateTimeFor ( timeZoneRec, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +PlainDateTimeObject* GetPlainDateTimeFor(JSContext* cx, + JS::Handle<TimeZoneValue> timeZone, + const Instant& instant, + JS::Handle<CalendarValue> calendar); + +/** + * GetPlainDateTimeFor ( timeZoneRec, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +PlainDateTimeObject* GetPlainDateTimeFor(JSContext* cx, const Instant& instant, + JS::Handle<CalendarValue> calendar, + int64_t offsetNanoseconds); + +/** + * GetPlainDateTimeFor ( timeZoneRec, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +PlainDateTime GetPlainDateTimeFor(const Instant& instant, + int64_t offsetNanoseconds); + +/** + * GetPlainDateTimeFor ( timeZoneRec, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +bool GetPlainDateTimeFor(JSContext* cx, JS::Handle<TimeZoneRecord> timeZone, + const Instant& instant, PlainDateTime* result); + +/** + * GetPlainDateTimeFor ( timeZoneRec, instant, calendar [ , + * precalculatedOffsetNanoseconds ] ) + */ +bool GetPlainDateTimeFor(JSContext* cx, JS::Handle<TimeZoneValue> timeZone, + const Instant& instant, PlainDateTime* result); + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +bool GetInstantFor(JSContext* cx, JS::Handle<TimeZoneValue> timeZone, + JS::Handle<PlainDateTimeObject*> dateTime, + TemporalDisambiguation disambiguation, Instant* result); + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +bool GetInstantFor(JSContext* cx, JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<PlainDateTimeWithCalendar> dateTime, + TemporalDisambiguation disambiguation, Instant* result); + +/** + * GetInstantFor ( timeZoneRec, dateTime, disambiguation ) + */ +bool GetInstantFor(JSContext* cx, JS::Handle<TimeZoneValue> timeZone, + JS::Handle<PlainDateTimeWithCalendar> dateTime, + TemporalDisambiguation disambiguation, Instant* result); + +/** + * FormatUTCOffsetNanoseconds ( offsetNanoseconds ) + */ +JSString* FormatUTCOffsetNanoseconds(JSContext* cx, int64_t offsetNanoseconds); + +/** + * GetOffsetStringFor ( timeZoneRec, instant ) + */ +JSString* GetOffsetStringFor(JSContext* cx, JS::Handle<TimeZoneValue> timeZone, + const Instant& instant); + +/** + * GetOffsetStringFor ( timeZoneRec, instant ) + */ +JSString* GetOffsetStringFor(JSContext* cx, JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<Wrapped<InstantObject*>> instant); + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool GetOffsetNanosecondsFor(JSContext* cx, JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<Wrapped<InstantObject*>> instant, + int64_t* offsetNanoseconds); + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool GetOffsetNanosecondsFor(JSContext* cx, JS::Handle<TimeZoneValue> timeZone, + JS::Handle<Wrapped<InstantObject*>> instant, + int64_t* offsetNanoseconds); + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool GetOffsetNanosecondsFor(JSContext* cx, JS::Handle<TimeZoneRecord> timeZone, + const Instant& instant, + int64_t* offsetNanoseconds); + +/** + * GetOffsetNanosecondsFor ( timeZoneRec, instant ) + */ +bool GetOffsetNanosecondsFor(JSContext* cx, JS::Handle<TimeZoneValue> timeZone, + const Instant& instant, + int64_t* offsetNanoseconds); + +using InstantVector = JS::StackGCVector<Wrapped<InstantObject*>>; + +/** + * GetPossibleInstantsFor ( timeZoneRec, dateTime ) + */ +bool GetPossibleInstantsFor(JSContext* cx, JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<PlainDateTimeWithCalendar> dateTime, + JS::MutableHandle<InstantVector> list); + +/** + * DisambiguatePossibleInstants ( possibleInstants, timeZoneRec, dateTime, + * disambiguation ) + */ +bool DisambiguatePossibleInstants( + JSContext* cx, JS::Handle<InstantVector> possibleInstants, + JS::Handle<TimeZoneRecord> timeZone, const PlainDateTime& dateTime, + TemporalDisambiguation disambiguation, + JS::MutableHandle<Wrapped<InstantObject*>> result); + +/** + * CreateTimeZoneMethodsRecord ( timeZone, methods ) + */ +bool CreateTimeZoneMethodsRecord(JSContext* cx, + JS::Handle<TimeZoneValue> timeZone, + mozilla::EnumSet<TimeZoneMethod> methods, + JS::MutableHandle<TimeZoneRecord> result); + +#ifdef DEBUG +/** + * TimeZoneMethodsRecordHasLookedUp ( timeZoneRec, methodName ) + */ +inline bool TimeZoneMethodsRecordHasLookedUp(const TimeZoneRecord& timeZone, + TimeZoneMethod methodName) { + // Steps 1-4. + return timeZone.lookedUp().contains(methodName); +} +#endif + +/** + * TimeZoneMethodsRecordIsBuiltin ( timeZoneRec ) + */ +inline bool TimeZoneMethodsRecordIsBuiltin(const TimeZoneRecord& timeZone) { + // Steps 1-2. + return timeZone.receiver().isString(); +} + +// Helper for MutableWrappedPtrOperations. +bool WrapTimeZoneValueObject(JSContext* cx, + JS::MutableHandle<JSObject*> timeZone); + +} /* namespace js::temporal */ + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<temporal::TimeZoneValue, Wrapper> { + const auto& container() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + explicit operator bool() const { return !!container(); } + + bool isString() const { return container().isString(); } + + bool isObject() const { return container().isObject(); } + + JS::Handle<temporal::BuiltinTimeZoneObject*> toString() const { + MOZ_ASSERT(container().isString()); + auto h = JS::Handle<JSObject*>::fromMarkedLocation(container().address()); + return h.template as<temporal::BuiltinTimeZoneObject>(); + } + + JS::Handle<JSObject*> toObject() const { + MOZ_ASSERT(container().isObject()); + return JS::Handle<JSObject*>::fromMarkedLocation(container().address()); + } + + JS::Handle<temporal::TimeZoneObjectMaybeBuiltin*> + toTimeZoneObjectMaybeBuiltin() const { + MOZ_ASSERT(container().isTimeZoneObjectMaybeBuiltin()); + auto h = JS::Handle<JSObject*>::fromMarkedLocation(container().address()); + return h.template as<temporal::TimeZoneObjectMaybeBuiltin>(); + } + + JS::Value toValue() const { return container().toValue(); } + + JS::Value toSlotValue() const { return container().toSlotValue(); } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<temporal::TimeZoneValue, Wrapper> + : public WrappedPtrOperations<temporal::TimeZoneValue, Wrapper> { + auto& container() { return static_cast<Wrapper*>(this)->get(); } + + public: + /** + * Wrap the time zone value into the current compartment. + */ + bool wrap(JSContext* cx) { + MOZ_ASSERT(container().isString() || container().isObject()); + auto mh = + JS::MutableHandle<JSObject*>::fromMarkedLocation(container().address()); + return temporal::WrapTimeZoneValueObject(cx, mh); + } +}; + +template <typename Wrapper> +class WrappedPtrOperations<temporal::TimeZoneRecord, Wrapper> { + const auto& container() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + JS::Handle<temporal::TimeZoneValue> receiver() const { + return JS::Handle<temporal::TimeZoneValue>::fromMarkedLocation( + container().receiverDoNotUse()); + } + + JS::Handle<JSObject*> getOffsetNanosecondsFor() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().getOffsetNanosecondsForDoNotUse()); + } + + JS::Handle<JSObject*> getPossibleInstantsFor() const { + return JS::Handle<JSObject*>::fromMarkedLocation( + container().getPossibleInstantsForDoNotUse()); + } +}; + +template <typename Wrapper> +class MutableWrappedPtrOperations<temporal::TimeZoneRecord, Wrapper> + : public WrappedPtrOperations<temporal::TimeZoneRecord, Wrapper> { + auto& container() { return static_cast<Wrapper*>(this)->get(); } + + public: + JS::MutableHandle<JSObject*> getOffsetNanosecondsFor() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().getOffsetNanosecondsForDoNotUse()); + } + + JS::MutableHandle<JSObject*> getPossibleInstantsFor() { + return JS::MutableHandle<JSObject*>::fromMarkedLocation( + container().getPossibleInstantsForDoNotUse()); + } +}; + +} /* namespace js */ + +#endif /* builtin_temporal_TimeZone_h */ diff --git a/js/src/builtin/temporal/ToString.cpp b/js/src/builtin/temporal/ToString.cpp new file mode 100644 index 0000000000..c789c5e95c --- /dev/null +++ b/js/src/builtin/temporal/ToString.cpp @@ -0,0 +1,679 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/ToString.h" + +#include "mozilla/Assertions.h" + +#include <cstdlib> +#include <stddef.h> +#include <stdint.h> +#include <type_traits> +#include <utility> + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Instant.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainMonthDay.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/Temporal.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/Policy.h" +#include "js/RootingAPI.h" +#include "util/StringBuffer.h" +#include "vm/StringType.h" + +using namespace js; +using namespace js::temporal; + +enum class TemporalStringFormat { + None, + Date, + Time, + DateTime, + YearMonth, + MonthDay, + ZonedDateTime, + Instant, +}; + +class TemporalStringBuilder { + JSStringBuilder sb_; + + TemporalStringFormat kind_ = TemporalStringFormat::None; + +#ifdef DEBUG + bool reserved_ = false; +#endif + + static constexpr size_t reserveAmount(TemporalStringFormat format) { + // Note: This doesn't reserve too much space, because the string builder + // already internally reserves space for 64 characters. + + constexpr size_t datePart = 1 + 6 + 1 + 2 + 1 + 2; // 13 + constexpr size_t timePart = 2 + 1 + 2 + 1 + 2 + 1 + 9; // 18 + constexpr size_t dateTimePart = datePart + 1 + timePart; // including 'T' + constexpr size_t timeZoneOffsetPart = 1 + 2 + 1 + 2; // 6 + + switch (format) { + case TemporalStringFormat::Date: + case TemporalStringFormat::YearMonth: + case TemporalStringFormat::MonthDay: + return datePart; + case TemporalStringFormat::Time: + return timePart; + case TemporalStringFormat::DateTime: + return dateTimePart; + case TemporalStringFormat::ZonedDateTime: + return dateTimePart + timeZoneOffsetPart; + case TemporalStringFormat::Instant: + return dateTimePart + timeZoneOffsetPart; + case TemporalStringFormat::None: + break; + } + MOZ_CRASH("invalid reserve amount"); + } + + public: + TemporalStringBuilder(JSContext* cx, TemporalStringFormat kind) + : sb_(cx), kind_(kind) { + MOZ_ASSERT(kind != TemporalStringFormat::None); + } + + bool reserve() { + MOZ_ASSERT(!reserved_); + + if (!sb_.reserve(reserveAmount(kind_))) { + return false; + } + +#ifdef DEBUG + reserved_ = true; +#endif + return true; + } + + void append(char value) { + MOZ_ASSERT(reserved_); + sb_.infallibleAppend(value); + } + + void appendTwoDigit(int32_t value) { + MOZ_ASSERT(0 <= value && value <= 99); + MOZ_ASSERT(reserved_); + + sb_.infallibleAppend(char('0' + (value / 10))); + sb_.infallibleAppend(char('0' + (value % 10))); + } + + void appendFourDigit(int32_t value) { + MOZ_ASSERT(0 <= value && value <= 9999); + MOZ_ASSERT(reserved_); + + sb_.infallibleAppend(char('0' + (value / 1000))); + sb_.infallibleAppend(char('0' + (value % 1000) / 100)); + sb_.infallibleAppend(char('0' + (value % 100) / 10)); + sb_.infallibleAppend(char('0' + (value % 10))); + } + + void appendSixDigit(int32_t value) { + MOZ_ASSERT(0 <= value && value <= 999999); + MOZ_ASSERT(reserved_); + + sb_.infallibleAppend(char('0' + (value / 100000))); + sb_.infallibleAppend(char('0' + (value % 100000) / 10000)); + sb_.infallibleAppend(char('0' + (value % 10000) / 1000)); + sb_.infallibleAppend(char('0' + (value % 1000) / 100)); + sb_.infallibleAppend(char('0' + (value % 100) / 10)); + sb_.infallibleAppend(char('0' + (value % 10))); + } + + void appendYear(int32_t year) { + if (0 <= year && year <= 9999) { + appendFourDigit(year); + } else { + append(year < 0 ? '-' : '+'); + appendSixDigit(std::abs(year)); + } + } + + auto* finishString() { return sb_.finishString(); } + + auto& builder() { return sb_; } +}; + +/** + * FormatFractionalSeconds ( subSecondNanoseconds, precision ) + */ +static void FormatFractionalSeconds(TemporalStringBuilder& 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; + } + + // Step 3. (Reordered) + result.append('.'); + + // Steps 1.b-c. + uint32_t k = 100'000'000; + do { + result.append(char('0' + (subSecondNanoseconds / k))); + subSecondNanoseconds %= k; + k /= 10; + } while (subSecondNanoseconds); + } else { + // Step 2.a. + uint8_t p = precision.value(); + if (p == 0) { + return; + } + + // Step 3. (Reordered) + result.append('.'); + + // Steps 2.b-c. + uint32_t k = 100'000'000; + for (uint8_t i = 0; i < p; i++) { + result.append(char('0' + (subSecondNanoseconds / k))); + subSecondNanoseconds %= k; + k /= 10; + } + } +} + +/** + * FormatTimeString ( hour, minute, second, subSecondNanoseconds, precision ) + */ +static void FormatTimeString(TemporalStringBuilder& result, + const PlainTime& time, Precision precision) { + // Step 1. + result.appendTwoDigit(time.hour); + + // Step 2. + result.append(':'); + result.appendTwoDigit(time.minute); + + // Steps 4-7. + if (precision != Precision::Minute()) { + result.append(':'); + result.appendTwoDigit(time.second); + + int32_t subSecondNanoseconds = time.millisecond * 1'000'000 + + time.microsecond * 1'000 + time.nanosecond; + FormatFractionalSeconds(result, subSecondNanoseconds, precision); + } +} + +static void FormatDateString(TemporalStringBuilder& result, + const PlainDate& date) { + result.appendYear(date.year); + result.append('-'); + result.appendTwoDigit(date.month); + result.append('-'); + result.appendTwoDigit(date.day); +} + +static void FormatDateTimeString(TemporalStringBuilder& result, + const PlainDateTime& dateTime, + Precision precision) { + FormatDateString(result, dateTime.date); + result.append('T'); + FormatTimeString(result, dateTime.time, precision); +} + +/** + * FormatOffsetTimeZoneIdentifier ( offsetMinutes [ , style ] ) + */ +static void FormatOffsetTimeZoneIdentifier(TemporalStringBuilder& result, + int32_t offsetMinutes) { + MOZ_ASSERT(std::abs(offsetMinutes) < UnitsPerDay(TemporalUnit::Minute), + "time zone offset mustn't exceed 24-hours"); + + // Step 1. + char sign = offsetMinutes >= 0 ? '+' : '-'; + + // Step 2. + int32_t absoluteMinutes = std::abs(offsetMinutes); + + // Step 3. + int32_t hours = absoluteMinutes / 60; + + // Step 4. + int32_t minutes = absoluteMinutes % 60; + + // Steps 5-6. (Inlined FormatTimeString) + result.append(sign); + result.appendTwoDigit(hours); + result.append(':'); + result.appendTwoDigit(minutes); +} + +// Returns |RoundNumberToIncrement(offsetNanoseconds, 60 ร 10^9, "halfExpand")| +// divided by |60 ร 10^9|. +static int32_t RoundNanosecondsToMinutes(int64_t offsetNanoseconds) { + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + constexpr int64_t increment = ToNanoseconds(TemporalUnit::Minute); + + int64_t quotient = offsetNanoseconds / increment; + int64_t remainder = offsetNanoseconds % increment; + if (std::abs(remainder * 2) >= increment) { + quotient += (offsetNanoseconds > 0 ? 1 : -1); + } + return quotient; +} + +/** + * FormatDateTimeUTCOffsetRounded ( offsetNanoseconds ) + */ +static void FormatDateTimeUTCOffsetRounded(TemporalStringBuilder& result, + int64_t offsetNanoseconds) { + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Steps 1-3. + int32_t offsetMinutes = RoundNanosecondsToMinutes(offsetNanoseconds); + + // Step 4. + FormatOffsetTimeZoneIdentifier(result, offsetMinutes); +} + +/** + * FormatCalendarAnnotation ( id, showCalendar ) + */ +static bool FormatCalendarAnnotation(TemporalStringBuilder& result, + JSLinearString* id, + CalendarOption showCalendar) { + switch (showCalendar) { + case CalendarOption::Never: + return true; + + case CalendarOption::Auto: { + if (StringEqualsLiteral(id, "iso8601")) { + return true; + } + [[fallthrough]]; + } + + case CalendarOption::Always: { + auto& sb = result.builder(); + return sb.append("[u-ca=") && sb.append(id) && sb.append(']'); + } + + case CalendarOption::Critical: { + auto& sb = result.builder(); + return sb.append("[!u-ca=") && sb.append(id) && sb.append(']'); + } + } + MOZ_CRASH("bad calendar option"); +} + +/** + * MaybeFormatCalendarAnnotation ( calendar, showCalendar ) + */ +static bool MaybeFormatCalendarAnnotation(JSContext* cx, + TemporalStringBuilder& result, + Handle<CalendarValue> calendar, + CalendarOption showCalendar) { + // Step 1. + if (showCalendar == CalendarOption::Never) { + return true; + } + + // Step 2. + JSString* calendarIdentifier = ToTemporalCalendarIdentifier(cx, calendar); + if (!calendarIdentifier) { + return false; + } + + JSLinearString* linearCalendarId = calendarIdentifier->ensureLinear(cx); + if (!linearCalendarId) { + return false; + } + + // Step 3. + return FormatCalendarAnnotation(result, linearCalendarId, showCalendar); +} + +static bool FormatTimeZoneAnnotation(TemporalStringBuilder& result, + JSLinearString* id, + TimeZoneNameOption showTimeZone) { + switch (showTimeZone) { + case TimeZoneNameOption::Never: + return true; + + case TimeZoneNameOption::Auto: { + auto& sb = result.builder(); + return sb.append("[") && sb.append(id) && sb.append(']'); + } + + case TimeZoneNameOption::Critical: { + auto& sb = result.builder(); + return sb.append("[!") && sb.append(id) && sb.append(']'); + } + } + MOZ_CRASH("bad time zone option"); +} + +static bool MaybeFormatTimeZoneAnnotation(JSContext* cx, + TemporalStringBuilder& result, + Handle<TimeZoneValue> timeZone, + TimeZoneNameOption showTimeZone) { + if (showTimeZone == TimeZoneNameOption::Never) { + return true; + } + + JSString* timeZoneIdentifier = ToTemporalTimeZoneIdentifier(cx, timeZone); + if (!timeZoneIdentifier) { + return false; + } + + JSLinearString* linearTimeZoneId = timeZoneIdentifier->ensureLinear(cx); + if (!linearTimeZoneId) { + return false; + } + + return FormatTimeZoneAnnotation(result, linearTimeZoneId, showTimeZone); +} + +/** + * TemporalInstantToString ( instant, timeZone, precision ) + */ +JSString* js::temporal::TemporalInstantToString(JSContext* cx, + Handle<InstantObject*> instant, + Handle<TimeZoneValue> timeZone, + Precision precision) { + TemporalStringBuilder result(cx, TemporalStringFormat::Instant); + if (!result.reserve()) { + return nullptr; + } + + // Steps 1-2. (Not applicable in our implementation.) + + // Steps 3-6. + int64_t offsetNanoseconds = 0; + if (timeZone) { + // Steps 3-4. (Not applicable) + + // Steps 5-6. + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return nullptr; + } + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + } + + // Step 7. + auto dateTime = GetPlainDateTimeFor(ToInstant(instant), offsetNanoseconds); + + // Step 8. (Inlined TemporalDateTimeToString) + FormatDateTimeString(result, dateTime, precision); + + // Steps 9-10. + Rooted<JSString*> timeZoneString(cx); + if (!timeZone) { + // Step 9.a. + result.append('Z'); + } else { + // Step 10.a. + FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds); + } + + // Step 11. + return result.finishString(); +} + +/** + * TemporalDateToString ( temporalDate, showCalendar ) + */ +JSString* js::temporal::TemporalDateToString( + JSContext* cx, Handle<PlainDateObject*> temporalDate, + CalendarOption showCalendar) { + auto date = ToPlainDate(temporalDate); + + // Steps 1-2. (Not applicable in our implementation.) + + TemporalStringBuilder result(cx, TemporalStringFormat::Date); + if (!result.reserve()) { + return nullptr; + } + + // Steps 3-5. + FormatDateString(result, date); + + // Step 6. + Rooted<CalendarValue> calendar(cx, temporalDate->calendar()); + if (!MaybeFormatCalendarAnnotation(cx, result, calendar, showCalendar)) { + return nullptr; + } + + // Step 7. + return result.finishString(); +} + +/** + * TemporalDateTimeToString ( isoYear, isoMonth, isoDay, hour, minute, second, + * millisecond, microsecond, nanosecond, calendar, precision, showCalendar ) + */ +JSString* js::temporal::TemporalDateTimeToString(JSContext* cx, + const PlainDateTime& dateTime, + Handle<CalendarValue> calendar, + Precision precision, + CalendarOption showCalendar) { + TemporalStringBuilder result(cx, TemporalStringFormat::DateTime); + if (!result.reserve()) { + return nullptr; + } + + // Step 1. (Not applicable in our implementation.) + + // Steps 2-6. + FormatDateTimeString(result, dateTime, precision); + + // Step 7. + if (!MaybeFormatCalendarAnnotation(cx, result, calendar, showCalendar)) { + return nullptr; + } + + // Step 8. + return result.finishString(); +} + +/** + * TemporalTimeToString ( hour, minute, second, millisecond, microsecond, + * nanosecond, precision ) + */ +JSString* js::temporal::TemporalTimeToString(JSContext* cx, + const PlainTime& time, + Precision precision) { + // Step 1. (Not applicable in our implementation.) + + TemporalStringBuilder result(cx, TemporalStringFormat::Time); + if (!result.reserve()) { + return nullptr; + } + + // Steps 2-3. + FormatTimeString(result, time, precision); + + return result.finishString(); +} + +/** + * TemporalMonthDayToString ( monthDay, showCalendar ) + */ +JSString* js::temporal::TemporalMonthDayToString( + JSContext* cx, Handle<PlainMonthDayObject*> monthDay, + CalendarOption showCalendar) { + // Steps 1-2. (Not applicable in our implementation.) + + TemporalStringBuilder result(cx, TemporalStringFormat::MonthDay); + if (!result.reserve()) { + return nullptr; + } + + // Step 6. (Reordered) + Rooted<CalendarValue> calendar(cx, monthDay->calendar()); + JSString* str = ToTemporalCalendarIdentifier(cx, calendar); + if (!str) { + return nullptr; + } + + Rooted<JSLinearString*> calendarIdentifier(cx, str->ensureLinear(cx)); + if (!calendarIdentifier) { + return nullptr; + } + + // Steps 3-5 and 7. + auto date = ToPlainDate(monthDay); + if (showCalendar == CalendarOption::Always || + showCalendar == CalendarOption::Critical || + !StringEqualsLiteral(calendarIdentifier, "iso8601")) { + // FIXME: spec issue - don't print "year" part when showCalendar is "never". + // + // ```js + // let cal = new Proxy({id: "cal"}, {has(t, pk) { return true; }}); + // let pmd = new Temporal.PlainMonthDay(8, 1, cal); + // pmd.toString({calendarName: "never"}) + // ``` + + FormatDateString(result, date); + } else { + result.appendTwoDigit(date.month); + result.append('-'); + result.appendTwoDigit(date.day); + } + + // Steps 8-9. + if (!FormatCalendarAnnotation(result, calendarIdentifier, showCalendar)) { + return nullptr; + } + + // Step 10. + return result.finishString(); +} + +/** + * TemporalYearMonthToString ( yearMonth, showCalendar ) + */ +JSString* js::temporal::TemporalYearMonthToString( + JSContext* cx, Handle<PlainYearMonthObject*> yearMonth, + CalendarOption showCalendar) { + // Steps 1-2. (Not applicable in our implementation.) + + TemporalStringBuilder result(cx, TemporalStringFormat::YearMonth); + if (!result.reserve()) { + return nullptr; + } + + // Step 6. (Reordered) + Rooted<CalendarValue> calendar(cx, yearMonth->calendar()); + JSString* str = ToTemporalCalendarIdentifier(cx, calendar); + if (!str) { + return nullptr; + } + + Rooted<JSLinearString*> calendarIdentifier(cx, str->ensureLinear(cx)); + if (!calendarIdentifier) { + return nullptr; + } + + // Steps 3-5 and 7. + auto date = ToPlainDate(yearMonth); + if (showCalendar == CalendarOption::Always || + showCalendar == CalendarOption::Critical || + !StringEqualsLiteral(calendarIdentifier, "iso8601")) { + // FIXME: spec issue - don't print "day" part when showCalendar is "never". + // + // ```js + // let cal = new Proxy({id: "cal"}, {has(t, pk) { return true; }}); + // let pym = new Temporal.PlainYearMonth(2023, 8, cal); + // pym.toString({calendarName: "never"}) + // ``` + + FormatDateString(result, date); + } else { + result.appendYear(date.year); + result.append('-'); + result.appendTwoDigit(date.month); + } + + // Steps 8-9. + if (!FormatCalendarAnnotation(result, calendarIdentifier, showCalendar)) { + return nullptr; + } + + // Step 10. + return result.finishString(); +} + +/** + * TemporalZonedDateTimeToString ( zonedDateTime, precision, showCalendar, + * showTimeZone, showOffset [ , increment, unit, roundingMode ] ) + */ +JSString* js::temporal::TemporalZonedDateTimeToString( + JSContext* cx, Handle<ZonedDateTime> zonedDateTime, Precision precision, + CalendarOption showCalendar, TimeZoneNameOption showTimeZone, + ShowOffsetOption showOffset, Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode) { + TemporalStringBuilder result(cx, TemporalStringFormat::ZonedDateTime); + if (!result.reserve()) { + return nullptr; + } + + // Steps 1-3. (Not applicable in our implementation.) + + // Step 4. + Instant ns; + if (!RoundTemporalInstant(cx, zonedDateTime.instant(), increment, unit, + roundingMode, &ns)) { + return nullptr; + } + + // Step 5. + auto timeZone = zonedDateTime.timeZone(); + + // Steps 6-8. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, ns, &offsetNanoseconds)) { + return nullptr; + } + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Step 9. + auto temporalDateTime = GetPlainDateTimeFor(ns, offsetNanoseconds); + + // Step 10. (Inlined TemporalDateTimeToString) + FormatDateTimeString(result, temporalDateTime, precision); + + // Steps 11-12. + if (showOffset != ShowOffsetOption::Never) { + FormatDateTimeUTCOffsetRounded(result, offsetNanoseconds); + } + + // Steps 13-14. + if (!MaybeFormatTimeZoneAnnotation(cx, result, timeZone, showTimeZone)) { + return nullptr; + } + + // Step 15. + if (!MaybeFormatCalendarAnnotation(cx, result, zonedDateTime.calendar(), + showCalendar)) { + return nullptr; + } + + // Step 16. + return result.finishString(); +} diff --git a/js/src/builtin/temporal/ToString.h b/js/src/builtin/temporal/ToString.h new file mode 100644 index 0000000000..7560ea8ca2 --- /dev/null +++ b/js/src/builtin/temporal/ToString.h @@ -0,0 +1,86 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_ToString_h +#define builtin_temporal_ToString_h + +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalRoundingMode.h" +#include "builtin/temporal/TemporalUnit.h" +#include "js/TypeDecls.h" + +namespace js::temporal { + +class CalendarValue; +class InstantObject; +class PlainDateObject; +class PlainMonthDayObject; +class PlainYearMonthObject; +class TimeZoneValue; +class ZonedDateTime; + +struct PlainDateTime; +struct PlainTime; + +/** + * TemporalInstantToString ( instant, timeZone, precision ) + */ +JSString* TemporalInstantToString(JSContext* cx, + JS::Handle<InstantObject*> instant, + JS::Handle<TimeZoneValue> timeZone, + Precision precision); + +/** + * TemporalDateToString ( temporalDate, showCalendar ) + */ +JSString* TemporalDateToString(JSContext* cx, + JS::Handle<PlainDateObject*> temporalDate, + CalendarOption showCalendar); + +/** + * TemporalDateTimeToString ( isoYear, isoMonth, isoDay, hour, minute, second, + * millisecond, microsecond, nanosecond, calendar, precision, showCalendar ) + */ +JSString* TemporalDateTimeToString(JSContext* cx, const PlainDateTime& dateTime, + JS::Handle<CalendarValue> calendar, + Precision precision, + CalendarOption showCalendar); + +/** + * TemporalTimeToString ( hour, minute, second, millisecond, microsecond, + * nanosecond, precision ) + */ +JSString* TemporalTimeToString(JSContext* cx, const PlainTime& time, + Precision precision); + +/** + * TemporalMonthDayToString ( monthDay, showCalendar ) + */ +JSString* TemporalMonthDayToString(JSContext* cx, + JS::Handle<PlainMonthDayObject*> monthDay, + CalendarOption showCalendar); + +/** + * TemporalYearMonthToString ( yearMonth, showCalendar ) + */ +JSString* TemporalYearMonthToString(JSContext* cx, + JS::Handle<PlainYearMonthObject*> yearMonth, + CalendarOption showCalendar); + +/** + * TemporalZonedDateTimeToString ( zonedDateTime, precision, showCalendar, + * showTimeZone, showOffset [ , increment, unit, roundingMode ] ) + */ +JSString* TemporalZonedDateTimeToString( + JSContext* cx, JS::Handle<ZonedDateTime> zonedDateTime, Precision precision, + CalendarOption showCalendar, TimeZoneNameOption showTimeZone, + ShowOffsetOption showOffset, Increment increment = Increment{1}, + TemporalUnit unit = TemporalUnit::Nanosecond, + TemporalRoundingMode roundingMode = TemporalRoundingMode::Trunc); + +} /* namespace js::temporal */ + +#endif /* builtin_temporal_ToString_h */ diff --git a/js/src/builtin/temporal/Wrapped.cpp b/js/src/builtin/temporal/Wrapped.cpp new file mode 100644 index 0000000000..ca0ec9f8b1 --- /dev/null +++ b/js/src/builtin/temporal/Wrapped.cpp @@ -0,0 +1,22 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/Wrapped.h" + +#include "jsfriendapi.h" + +#include "js/ErrorReport.h" +#include "js/friend/ErrorMessages.h" +#include "js/Wrapper.h" + +void js::temporal::ReportDeadWrapperOrAccessDenied(JSContext* cx, + JSObject* obj) { + if (JS_IsDeadWrapper(obj)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DEAD_OBJECT); + } else { + ReportAccessDenied(cx); + } +} diff --git a/js/src/builtin/temporal/Wrapped.h b/js/src/builtin/temporal/Wrapped.h new file mode 100644 index 0000000000..904a9b3ac9 --- /dev/null +++ b/js/src/builtin/temporal/Wrapped.h @@ -0,0 +1,169 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_Wrapped_h +#define builtin_temporal_Wrapped_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" + +#include <type_traits> + +#include "gc/Tracer.h" +#include "js/RootingAPI.h" +#include "vm/JSObject.h" +#include "vm/NativeObject.h" + +namespace js::temporal { + +/** + * Type to represent possibly wrapped objects from a different compartment. + * + * This can be used to represent specific JSObject sub-classes in return types + * without having to pass unwrapped objects around. + */ +template <class T> +class MOZ_STACK_CLASS Wrapped final { + static_assert(std::is_pointer_v<T>); + static_assert(std::is_convertible_v<T, NativeObject*>); + + using U = std::remove_pointer_t<T>; + + JSObject* ptr_ = nullptr; + + public: + Wrapped() = default; + + MOZ_IMPLICIT Wrapped(decltype(nullptr)) : ptr_(nullptr) {} + + MOZ_IMPLICIT Wrapped(T ptr) : ptr_(ptr) { + // No assertion needed when the object already has the correct type. + } + + MOZ_IMPLICIT Wrapped(JSObject* ptr) : ptr_(ptr) { + // Ensure the caller passed a valid pointer. + MOZ_ASSERT_IF(ptr_, ptr_->canUnwrapAs<U>()); + } + + template <typename S> + MOZ_IMPLICIT Wrapped( + const JS::Rooted<S>& root, + std::enable_if_t<std::is_convertible_v<S, T>, int> dummy = 0) + : Wrapped(root.get()) {} + + MOZ_IMPLICIT Wrapped(const JS::Rooted<JSObject*>& root) + : Wrapped(root.get()) {} + + template <typename S> + MOZ_IMPLICIT Wrapped( + const JS::Handle<S>& root, + std::enable_if_t<std::is_convertible_v<S, T>, int> dummy = 0) + : Wrapped(root.get()) {} + + MOZ_IMPLICIT Wrapped(const JS::Handle<JSObject*>& root) + : Wrapped(root.get()) {} + + template <typename S> + MOZ_IMPLICIT Wrapped( + const JS::MutableHandle<S>& root, + std::enable_if_t<std::is_convertible_v<S, T>, int> dummy = 0) + : Wrapped(root.get()) {} + + MOZ_IMPLICIT Wrapped(const JS::MutableHandle<JSObject*>& root) + : Wrapped(root.get()) {} + + Wrapped& operator=(decltype(nullptr)) { + ptr_ = nullptr; + return *this; + } + + Wrapped& operator=(T ptr) { + ptr_ = ptr; + return *this; + } + + explicit operator bool() const { return !!ptr_; } + + JSObject* operator->() const { return ptr_; } + + JSObject& operator*() const { return *ptr_; } + + JSObject* get() const { return ptr_; } + + operator JSObject*() const { return get(); } + + auto address() const { return &ptr_; } + + U& unwrap() const { + MOZ_ASSERT(ptr_); + + // Direct unwrap because the constructor already verified the object can be + // unwrapped. + // + // We use JSObject::unwrapAs() instead of JSObject::maybeUnwrapIf(), because + // this is an unrooted Wrapped, so hazard analysis will ensure that no + // wrappers have been invalidated, because wrapper invalidation generally + // only happens in the same case as GC. + // + // Rooted Wrapped are accessed through their WrappedPtrOperations + // specialization, which uses JSObject::maybeUnwrapIf() to handle the + // wrapper invalidation case correctly. + return ptr_->unwrapAs<U>(); + } + + U* unwrapOrNull() const { + // Direct unwrap because the constructor already verified the object can be + // unwrapped. + // + // See Wrapped::unwrap() for why we don't call maybeUnwrapIf() here. + return ptr_ ? &ptr_->unwrapAs<U>() : nullptr; + } + + void trace(JSTracer* trc) { TraceNullableRoot(trc, &ptr_, "Wrapped::ptr_"); } +}; + +void ReportDeadWrapperOrAccessDenied(JSContext* cx, JSObject* obj); + +} /* namespace js::temporal */ + +namespace js { +template <typename T, typename Container> +class WrappedPtrOperations<temporal::Wrapped<T>, Container> { + using U = std::remove_pointer_t<T>; + + const auto& wrapped() const { + return static_cast<const Container*>(this)->get(); + } + + public: + explicit operator bool() const { return !!wrapped(); } + + JSObject* operator->() const { return wrapped().get(); } + + JSObject& operator*() const { return *wrapped().get(); } + + JS::Handle<JSObject*> object() const { + return JS::Handle<JSObject*>::fromMarkedLocation(wrapped().address()); + } + + operator JS::Handle<JSObject*>() const { return object(); } + + [[nodiscard]] U* unwrap(JSContext* cx) const { + JSObject* obj = wrapped().get(); + + // Call JSObject::maybeUnwrapIf() instead of JSObject::unwrapAs() in case + // |obj| is an invalidated wrapper. + if (auto* unwrapped = obj->maybeUnwrapIf<U>()) { + return unwrapped; + } + + temporal::ReportDeadWrapperOrAccessDenied(cx, obj); + return nullptr; + } +}; +} // namespace js + +#endif /* builtin_temporal_Wrapped_h */ diff --git a/js/src/builtin/temporal/ZonedDateTime.cpp b/js/src/builtin/temporal/ZonedDateTime.cpp new file mode 100644 index 0000000000..690ff223b1 --- /dev/null +++ b/js/src/builtin/temporal/ZonedDateTime.cpp @@ -0,0 +1,4124 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "builtin/temporal/ZonedDateTime.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Maybe.h" + +#include <cstdlib> +#include <utility> + +#include "jspubtd.h" +#include "NamespaceImports.h" + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Duration.h" +#include "builtin/temporal/Instant.h" +#include "builtin/temporal/PlainDate.h" +#include "builtin/temporal/PlainDateTime.h" +#include "builtin/temporal/PlainMonthDay.h" +#include "builtin/temporal/PlainTime.h" +#include "builtin/temporal/PlainYearMonth.h" +#include "builtin/temporal/Temporal.h" +#include "builtin/temporal/TemporalFields.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/ToString.h" +#include "builtin/temporal/Wrapped.h" +#include "ds/IdValuePair.h" +#include "gc/AllocKind.h" +#include "gc/Barrier.h" +#include "js/AllocPolicy.h" +#include "js/CallArgs.h" +#include "js/CallNonGenericMethod.h" +#include "js/Class.h" +#include "js/ComparisonOperators.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/TracingAPI.h" +#include "js/Value.h" +#include "vm/BigIntType.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/JSContext-inl.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 IsZonedDateTime(Handle<Value> v) { + return v.isObject() && v.toObject().is<ZonedDateTimeObject>(); +} + +// Returns |RoundNumberToIncrement(offsetNanoseconds, 60 ร 10^9, "halfExpand")|. +static int64_t RoundNanosecondsToMinutesIncrement(int64_t offsetNanoseconds) { + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + constexpr int64_t increment = ToNanoseconds(TemporalUnit::Minute); + + int64_t quotient = offsetNanoseconds / increment; + int64_t remainder = offsetNanoseconds % increment; + if (std::abs(remainder * 2) >= increment) { + quotient += (offsetNanoseconds > 0 ? 1 : -1); + } + return quotient * increment; +} + +/** + * InterpretISODateTimeOffset ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond, offsetBehaviour, offsetNanoseconds, + * timeZoneRec, disambiguation, offsetOption, matchBehaviour ) + */ +bool js::temporal::InterpretISODateTimeOffset( + JSContext* cx, const PlainDateTime& dateTime, + OffsetBehaviour offsetBehaviour, int64_t offsetNanoseconds, + Handle<TimeZoneRecord> timeZone, TemporalDisambiguation disambiguation, + TemporalOffset offsetOption, MatchBehaviour matchBehaviour, + Instant* result) { + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Step 1. + MOZ_ASSERT(IsValidISODate(dateTime.date)); + + // Step 2. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Step 3. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + // Step 4. + Rooted<CalendarValue> calendar(cx, CalendarValue(cx->names().iso8601)); + Rooted<PlainDateTimeWithCalendar> temporalDateTime(cx); + if (!CreateTemporalDateTime(cx, dateTime, calendar, &temporalDateTime)) { + return false; + } + + // Step 5. + if (offsetBehaviour == OffsetBehaviour::Wall || + offsetOption == TemporalOffset::Ignore) { + // Steps 5.a-b. + return GetInstantFor(cx, timeZone, temporalDateTime, disambiguation, + result); + } + + // Step 6. + if (offsetBehaviour == OffsetBehaviour::Exact || + offsetOption == TemporalOffset::Use) { + // Step 6.a. + auto epochNanoseconds = GetUTCEpochNanoseconds( + dateTime, InstantSpan::fromNanoseconds(offsetNanoseconds)); + + // Step 6.b. + if (!IsValidEpochInstant(epochNanoseconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 6.c. + *result = epochNanoseconds; + return true; + } + + // Step 7. + MOZ_ASSERT(offsetBehaviour == OffsetBehaviour::Option); + + // Step 8. + MOZ_ASSERT(offsetOption == TemporalOffset::Prefer || + offsetOption == TemporalOffset::Reject); + + // FIXME: spec issue - duplicate assertion + + // Step 9. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + // Step 10. + Rooted<InstantVector> possibleInstants(cx, InstantVector(cx)); + if (!GetPossibleInstantsFor(cx, timeZone, temporalDateTime, + &possibleInstants)) { + return false; + } + + // Step 11. + if (!possibleInstants.empty()) { + // Step 11.a. + Rooted<Wrapped<InstantObject*>> candidate(cx); + for (size_t i = 0; i < possibleInstants.length(); i++) { + candidate = possibleInstants[i]; + + // Step 11.a.i. + int64_t candidateNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, candidate, + &candidateNanoseconds)) { + return false; + } + MOZ_ASSERT(std::abs(candidateNanoseconds) < + ToNanoseconds(TemporalUnit::Day)); + + // Step 11.a.ii. + if (candidateNanoseconds == offsetNanoseconds) { + auto* unwrapped = candidate.unwrap(cx); + if (!unwrapped) { + return false; + } + + *result = ToInstant(unwrapped); + return true; + } + + // Step 11.a.iii. + if (matchBehaviour == MatchBehaviour::MatchMinutes) { + // Step 11.a.iii.1. + int64_t roundedCandidateNanoseconds = + RoundNanosecondsToMinutesIncrement(candidateNanoseconds); + + // Step 11.a.iii.2. + if (roundedCandidateNanoseconds == offsetNanoseconds) { + auto* unwrapped = candidate.unwrap(cx); + if (!unwrapped) { + return false; + } + + // Step 11.a.iii.2.a. + *result = ToInstant(unwrapped); + return true; + } + } + } + } + + // Step 12. + if (offsetOption == TemporalOffset::Reject) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_NO_TIME_FOUND); + return false; + } + + // Step 13. + Rooted<Wrapped<InstantObject*>> instant(cx); + if (!DisambiguatePossibleInstants(cx, possibleInstants, timeZone, + ToPlainDateTime(temporalDateTime), + disambiguation, &instant)) { + return false; + } + + auto* unwrappedInstant = instant.unwrap(cx); + if (!unwrappedInstant) { + return false; + } + + // Step 14. + *result = ToInstant(unwrappedInstant); + return true; +} + +/** + * ToTemporalZonedDateTime ( item [ , options ] ) + */ +static bool ToTemporalZonedDateTime(JSContext* cx, Handle<Value> item, + Handle<JSObject*> maybeOptions, + MutableHandle<ZonedDateTime> result) { + // Step 1. (Not applicable in our implementation) + + // Step 2. + Rooted<PlainObject*> maybeResolvedOptions(cx); + if (maybeOptions) { + maybeResolvedOptions = SnapshotOwnProperties(cx, maybeOptions); + if (!maybeResolvedOptions) { + return false; + } + } + + // Step 3. + auto offsetBehaviour = OffsetBehaviour::Option; + + // Step 4. + auto matchBehaviour = MatchBehaviour::MatchExactly; + + // Step 7. (Reordered) + int64_t offsetNanoseconds = 0; + + // Step 5. + Rooted<CalendarValue> calendar(cx); + Rooted<TimeZoneValue> timeZone(cx); + PlainDateTime dateTime; + auto disambiguation = TemporalDisambiguation::Compatible; + auto offsetOption = TemporalOffset::Reject; + if (item.isObject()) { + Rooted<JSObject*> itemObj(cx, &item.toObject()); + + // Step 5.a. + if (auto* zonedDateTime = itemObj->maybeUnwrapIf<ZonedDateTimeObject>()) { + auto instant = ToInstant(zonedDateTime); + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar()); + + if (!timeZone.wrap(cx)) { + return false; + } + if (!calendar.wrap(cx)) { + return false; + } + + result.set(ZonedDateTime{instant, timeZone, calendar}); + return true; + } + + // Step 5.b. + if (!GetTemporalCalendarWithISODefault(cx, itemObj, &calendar)) { + return false; + } + + // Step 5.c. + Rooted<CalendarRecord> calendarRec(cx); + if (!CreateCalendarMethodsRecord(cx, calendar, + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + }, + &calendarRec)) { + return false; + } + + // Step 5.d. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendarRec, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 5.e. + if (!AppendSorted(cx, fieldNames.get(), + { + TemporalField::Hour, + TemporalField::Microsecond, + TemporalField::Millisecond, + TemporalField::Minute, + TemporalField::Nanosecond, + TemporalField::Offset, + TemporalField::Second, + TemporalField::TimeZone, + })) { + return false; + } + + // Step 5.f. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, itemObj, fieldNames, + {TemporalField::TimeZone})); + if (!fields) { + return false; + } + + // Step 5.g. + Rooted<Value> timeZoneValue(cx); + if (!GetProperty(cx, fields, fields, cx->names().timeZone, + &timeZoneValue)) { + return false; + } + + // Step 5.h. + if (!ToTemporalTimeZone(cx, timeZoneValue, &timeZone)) { + return false; + } + + // Step 5.i. + Rooted<Value> offsetValue(cx); + if (!GetProperty(cx, fields, fields, cx->names().offset, &offsetValue)) { + return false; + } + + // Step 5.j. + MOZ_ASSERT(offsetValue.isString() || offsetValue.isUndefined()); + + // Step 5.k. + Rooted<JSString*> offsetString(cx); + if (offsetValue.isString()) { + offsetString = offsetValue.toString(); + } else { + offsetBehaviour = OffsetBehaviour::Wall; + } + + if (maybeResolvedOptions) { + // Steps 5.l-m. + if (!ToTemporalDisambiguation(cx, maybeResolvedOptions, + &disambiguation)) { + return false; + } + + // Step 5.n. + if (!ToTemporalOffset(cx, maybeResolvedOptions, &offsetOption)) { + return false; + } + + // Step 5.o. + if (!InterpretTemporalDateTimeFields(cx, calendarRec, fields, + maybeResolvedOptions, &dateTime)) { + return false; + } + } else { + // Steps 5.l-n. (Not applicable) + + // Step 5.o. + if (!InterpretTemporalDateTimeFields(cx, calendarRec, fields, + &dateTime)) { + return false; + } + } + + // Step 8. + if (offsetBehaviour == OffsetBehaviour::Option) { + if (!ParseDateTimeUTCOffset(cx, offsetString, &offsetNanoseconds)) { + return false; + } + } + } else { + // Step 6.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()); + + // Case 1: 19700101Z[+02:00] + // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "+02:00" } + // + // Case 2: 19700101+00:00[+02:00] + // { [[Z]]: false, [[OffsetString]]: "+00:00", [[Name]]: "+02:00" } + // + // Case 3: 19700101[+02:00] + // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "+02:00" } + // + // Case 4: 19700101Z[Europe/Berlin] + // { [[Z]]: true, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } + // + // Case 5: 19700101+00:00[Europe/Berlin] + // { [[Z]]: false, [[OffsetString]]: "+00:00", [[Name]]: "Europe/Berlin" } + // + // Case 6: 19700101[Europe/Berlin] + // { [[Z]]: false, [[OffsetString]]: undefined, [[Name]]: "Europe/Berlin" } + + // Steps 6.b-c. + bool isUTC; + bool hasOffset; + int64_t timeZoneOffset; + Rooted<ParsedTimeZone> timeZoneString(cx); + Rooted<JSString*> calendarString(cx); + if (!ParseTemporalZonedDateTimeString(cx, string, &dateTime, &isUTC, + &hasOffset, &timeZoneOffset, + &timeZoneString, &calendarString)) { + return false; + } + + // Step 6.d. + MOZ_ASSERT(timeZoneString); + + // Step 6.e. + if (!ToTemporalTimeZone(cx, timeZoneString, &timeZone)) { + return false; + } + + // Step 6.f. (Not applicable in our implementation.) + + // Step 6.g. + if (isUTC) { + offsetBehaviour = OffsetBehaviour::Exact; + } + + // Step 6.h. + else if (!hasOffset) { + offsetBehaviour = OffsetBehaviour::Wall; + } + + // Steps 6.i-l. + if (calendarString) { + if (!ToBuiltinCalendar(cx, calendarString, &calendar)) { + return false; + } + } else { + calendar.set(CalendarValue(cx->names().iso8601)); + } + + // Step 6.m. + matchBehaviour = MatchBehaviour::MatchMinutes; + + if (maybeResolvedOptions) { + // Step 6.n. + if (!ToTemporalDisambiguation(cx, maybeResolvedOptions, + &disambiguation)) { + return false; + } + + // Step 6.o. + if (!ToTemporalOffset(cx, maybeResolvedOptions, &offsetOption)) { + return false; + } + + // Step 6.p. + TemporalOverflow ignored; + if (!ToTemporalOverflow(cx, maybeResolvedOptions, &ignored)) { + return false; + } + } + + // Step 8. + if (offsetBehaviour == OffsetBehaviour::Option) { + MOZ_ASSERT(hasOffset); + offsetNanoseconds = timeZoneOffset; + } + } + + // Step 9. + Rooted<TimeZoneRecord> timeZoneRec(cx); + if (!CreateTimeZoneMethodsRecord(cx, timeZone, + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZoneRec)) { + return false; + } + + // Step 10. + Instant epochNanoseconds; + if (!InterpretISODateTimeOffset( + cx, dateTime, offsetBehaviour, offsetNanoseconds, timeZoneRec, + disambiguation, offsetOption, matchBehaviour, &epochNanoseconds)) { + return false; + } + + // Step 11. + result.set(ZonedDateTime{epochNanoseconds, timeZone, calendar}); + return true; +} + +/** + * ToTemporalZonedDateTime ( item [ , options ] ) + */ +static bool ToTemporalZonedDateTime(JSContext* cx, Handle<Value> item, + MutableHandle<ZonedDateTime> result) { + return ToTemporalZonedDateTime(cx, item, nullptr, result); +} + +/** + * ToTemporalZonedDateTime ( item [ , options ] ) + */ +static ZonedDateTimeObject* ToTemporalZonedDateTime( + JSContext* cx, Handle<Value> item, Handle<JSObject*> maybeOptions) { + Rooted<ZonedDateTime> result(cx); + if (!ToTemporalZonedDateTime(cx, item, maybeOptions, &result)) { + return nullptr; + } + return CreateTemporalZonedDateTime(cx, result.instant(), result.timeZone(), + result.calendar()); +} + +/** + * CreateTemporalZonedDateTime ( epochNanoseconds, timeZone, calendar [ , + * newTarget ] ) + */ +static ZonedDateTimeObject* CreateTemporalZonedDateTime( + JSContext* cx, const CallArgs& args, Handle<BigInt*> epochNanoseconds, + Handle<TimeZoneValue> timeZone, Handle<CalendarValue> calendar) { + // Step 1. + MOZ_ASSERT(IsValidEpochNanoseconds(epochNanoseconds)); + + // Steps 3-4. + Rooted<JSObject*> proto(cx); + if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_ZonedDateTime, + &proto)) { + return nullptr; + } + + auto* obj = NewObjectWithClassProto<ZonedDateTimeObject>(cx, proto); + if (!obj) { + return nullptr; + } + + // Step 4. + auto instant = ToInstant(epochNanoseconds); + obj->setFixedSlot(ZonedDateTimeObject::SECONDS_SLOT, + NumberValue(instant.seconds)); + obj->setFixedSlot(ZonedDateTimeObject::NANOSECONDS_SLOT, + Int32Value(instant.nanoseconds)); + + // Step 5. + obj->setFixedSlot(ZonedDateTimeObject::TIMEZONE_SLOT, timeZone.toSlotValue()); + + // Step 6. + obj->setFixedSlot(ZonedDateTimeObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 7. + return obj; +} + +/** + * CreateTemporalZonedDateTime ( epochNanoseconds, timeZone, calendar [ , + * newTarget ] ) + */ +ZonedDateTimeObject* js::temporal::CreateTemporalZonedDateTime( + JSContext* cx, const Instant& instant, Handle<TimeZoneValue> timeZone, + Handle<CalendarValue> calendar) { + // Step 1. + MOZ_ASSERT(IsValidEpochInstant(instant)); + + // Steps 2-3. + auto* obj = NewBuiltinClassInstance<ZonedDateTimeObject>(cx); + if (!obj) { + return nullptr; + } + + // Step 4. + obj->setFixedSlot(ZonedDateTimeObject::SECONDS_SLOT, + NumberValue(instant.seconds)); + obj->setFixedSlot(ZonedDateTimeObject::NANOSECONDS_SLOT, + Int32Value(instant.nanoseconds)); + + // Step 5. + obj->setFixedSlot(ZonedDateTimeObject::TIMEZONE_SLOT, timeZone.toSlotValue()); + + // Step 6. + obj->setFixedSlot(ZonedDateTimeObject::CALENDAR_SLOT, calendar.toValue()); + + // Step 7. + return obj; +} + +struct PlainDateTimeAndInstant { + PlainDateTime dateTime; + Instant instant; +}; + +/** + * AddDaysToZonedDateTime ( instant, dateTime, timeZoneRec, calendar, days [ , + * overflow ] ) + */ +static bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, + const PlainDateTime& dateTime, + Handle<TimeZoneRecord> timeZone, + Handle<CalendarValue> calendar, double days, + TemporalOverflow overflow, + PlainDateTimeAndInstant* result) { + // Step 1. (Not applicable in our implementation.) + + // Step 2. (Not applicable) + + // Step 3. + if (days == 0) { + *result = {dateTime, instant}; + return true; + } + + // Step 4. + PlainDate addedDate; + if (!AddISODate(cx, dateTime.date, {0, 0, 0, days}, overflow, &addedDate)) { + return false; + } + + // Step 5. + Rooted<PlainDateTimeWithCalendar> dateTimeResult(cx); + if (!CreateTemporalDateTime(cx, {addedDate, dateTime.time}, calendar, + &dateTimeResult)) { + return false; + } + + // Step 6. + Instant instantResult; + if (!GetInstantFor(cx, timeZone, dateTimeResult, + TemporalDisambiguation::Compatible, &instantResult)) { + return false; + } + + // Step 7. + *result = {ToPlainDateTime(dateTimeResult), instantResult}; + return true; +} + +/** + * AddDaysToZonedDateTime ( instant, dateTime, timeZoneRec, calendar, days [ , + * overflow ] ) + */ +bool js::temporal::AddDaysToZonedDateTime( + JSContext* cx, const Instant& instant, const PlainDateTime& dateTime, + Handle<TimeZoneRecord> timeZone, Handle<CalendarValue> calendar, + double days, TemporalOverflow overflow, Instant* result) { + // Steps 1-7. + PlainDateTimeAndInstant dateTimeAndInstant; + if (!::AddDaysToZonedDateTime(cx, instant, dateTime, timeZone, calendar, days, + overflow, &dateTimeAndInstant)) { + return false; + } + + *result = dateTimeAndInstant.instant; + return true; +} + +/** + * AddDaysToZonedDateTime ( instant, dateTime, timeZoneRec, calendar, days [ , + * overflow ] ) + */ +bool js::temporal::AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, + const PlainDateTime& dateTime, + Handle<TimeZoneRecord> timeZone, + Handle<CalendarValue> calendar, + double days, Instant* result) { + // Step 2. + auto overflow = TemporalOverflow::Constrain; + + // Steps 1 and 3-7. + return AddDaysToZonedDateTime(cx, instant, dateTime, timeZone, calendar, days, + overflow, result); +} + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds + * [ , precalculatedPlainDateTime [ , options ] ] ) + */ +static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, + Handle<TimeZoneRecord> timeZone, + Handle<CalendarRecord> calendar, + const Duration& duration, + mozilla::Maybe<const PlainDateTime&> dateTime, + Handle<JSObject*> maybeOptions, Instant* result) { + MOZ_ASSERT(IsValidEpochInstant(epochNanoseconds)); + MOZ_ASSERT(IsValidDuration(duration.date())); + MOZ_ASSERT(IsValidDuration(duration.time())); + + // Step 1. + MOZ_ASSERT(TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetPossibleInstantsFor)); + + // Steps 2-3. + MOZ_ASSERT_IF(!dateTime, + TimeZoneMethodsRecordHasLookedUp( + timeZone, TimeZoneMethod::GetOffsetNanosecondsFor)); + + // Steps 4-5. (Not applicable in our implementation) + + // Step 6. + if (duration.years == 0 && duration.months == 0 && duration.weeks == 0 && + duration.days == 0) { + // Step 6.a. + return AddInstant(cx, epochNanoseconds, duration, result); + } + + // Step 7. (Not applicable in our implementation) + + // Steps 8-9. + PlainDateTime temporalDateTime; + if (dateTime) { + // Step 8.a. + temporalDateTime = *dateTime; + } else { + // Step 9.a. + if (!GetPlainDateTimeFor(cx, timeZone, epochNanoseconds, + &temporalDateTime)) { + return false; + } + } + auto& [date, time] = temporalDateTime; + + // Step 10. + if (duration.years == 0 && duration.months == 0 && duration.weeks == 0) { + // Step 10.a. + auto overflow = TemporalOverflow::Constrain; + if (maybeOptions) { + if (!ToTemporalOverflow(cx, maybeOptions, &overflow)) { + return false; + } + } + + // Step 10.b. + Instant intermediate; + if (!AddDaysToZonedDateTime(cx, epochNanoseconds, temporalDateTime, + timeZone, calendar.receiver(), duration.days, + overflow, &intermediate)) { + return false; + } + + // Step 10.c. + return AddInstant(cx, intermediate, duration.time(), result); + } + + // Step 11. + MOZ_ASSERT( + CalendarMethodsRecordHasLookedUp(calendar, CalendarMethod::DateAdd)); + + // Step 12. + const auto& datePart = date; + + // Step 13. + auto dateDuration = duration.date(); + + // Step 14. + PlainDate addedDate; + if (maybeOptions) { + if (!CalendarDateAdd(cx, calendar, datePart, dateDuration, maybeOptions, + &addedDate)) { + return false; + } + } else { + if (!CalendarDateAdd(cx, calendar, datePart, dateDuration, &addedDate)) { + return false; + } + } + + // Step 15. + Rooted<PlainDateTimeWithCalendar> intermediateDateTime(cx); + if (!CreateTemporalDateTime(cx, {addedDate, time}, calendar.receiver(), + &intermediateDateTime)) { + return false; + } + + // Step 16. + Instant intermediateInstant; + if (!GetInstantFor(cx, timeZone, intermediateDateTime, + TemporalDisambiguation::Compatible, + &intermediateInstant)) { + return false; + } + + // Step 17. + return AddInstant(cx, intermediateInstant, duration.time(), result); +} + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds + * [ , precalculatedPlainDateTime [ , options ] ] ) + */ +static bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, + Handle<TimeZoneRecord> timeZone, + Handle<CalendarRecord> calendar, + const Duration& duration, + Handle<JSObject*> maybeOptions, Instant* result) { + return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, duration, + mozilla::Nothing(), maybeOptions, result); +} + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds + * [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool js::temporal::AddZonedDateTime(JSContext* cx, + const Instant& epochNanoseconds, + Handle<TimeZoneRecord> timeZone, + Handle<CalendarRecord> calendar, + const Duration& duration, Instant* result) { + return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, duration, + mozilla::Nothing(), nullptr, result); +} + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds + * [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool js::temporal::AddZonedDateTime( + JSContext* cx, const Instant& epochNanoseconds, + Handle<TimeZoneRecord> timeZone, Handle<CalendarRecord> calendar, + const Duration& duration, const PlainDateTime& dateTime, Instant* result) { + return ::AddZonedDateTime(cx, epochNanoseconds, timeZone, calendar, duration, + mozilla::SomeRef(dateTime), nullptr, result); +} + +double js::temporal::NanosecondsAndDays::daysNumber() const { + if (days) { + return BigInt::numberValue(days); + } + return double(daysInt); +} + +void js::temporal::NanosecondsAndDays::trace(JSTracer* trc) { + if (days) { + TraceRoot(trc, &days, "NanosecondsAndDays::days"); + } +} + +/** + * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * precalculatedPlainDateTime ] ) + */ +static bool NanosecondsToDays( + JSContext* cx, const InstantSpan& nanoseconds, + Handle<ZonedDateTime> zonedRelativeTo, Handle<TimeZoneRecord> timeZone, + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, + MutableHandle<NanosecondsAndDays> result) { + MOZ_ASSERT(IsValidInstantSpan(nanoseconds)); + + // Step 1. + if (nanoseconds == InstantSpan{}) { + result.set(NanosecondsAndDays::from( + int64_t(0), InstantSpan{}, + InstantSpan::fromNanoseconds(ToNanoseconds(TemporalUnit::Day)))); + return true; + } + + // Step 2. + int32_t sign = nanoseconds < InstantSpan{} ? -1 : 1; + + // Step 3. + auto startNs = zonedRelativeTo.instant(); + auto calendar = zonedRelativeTo.calendar(); + + // Step 5. + // + // NB: This addition can't overflow, because we've checked that |nanoseconds| + // can be represented as an InstantSpan value. + auto endNs = startNs + nanoseconds; + + // Step 6. + if (!IsValidEpochInstant(endNs)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Steps 4 and 8. + PlainDateTime startDateTime; + if (!precalculatedPlainDateTime) { + if (!GetPlainDateTimeFor(cx, timeZone, startNs, &startDateTime)) { + return false; + } + } else { + startDateTime = *precalculatedPlainDateTime; + } + + // Steps 7 and 9. + PlainDateTime endDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, endNs, &endDateTime)) { + return false; + } + + // Steps 10-11. (Not applicable in our implementation.) + + // Step 12. + // + // Overflows in step 21 can be safely ignored, because they take too long to + // happen for int64. + int64_t days = DaysUntil(startDateTime.date, endDateTime.date); + + // Step 13. + int32_t timeSign = CompareTemporalTime(startDateTime.time, endDateTime.time); + + // Steps 14-15. + if (days > 0 && timeSign > 0) { + days -= 1; + } else if (days < 0 && timeSign < 0) { + days += 1; + } + + // Step 16. + PlainDateTimeAndInstant relativeResult; + if (!::AddDaysToZonedDateTime(cx, startNs, startDateTime, timeZone, calendar, + days, TemporalOverflow::Constrain, + &relativeResult)) { + return false; + } + MOZ_ASSERT(IsValidISODateTime(relativeResult.dateTime)); + MOZ_ASSERT(IsValidEpochInstant(relativeResult.instant)); + + // Step 17. + if (sign > 0) { + // Step 17.a. + while (days > 0 && relativeResult.instant > endNs) { + // This loop can iterate indefinitely when given a specially crafted + // time zone object, so we need to check for interrupts. + if (!CheckForInterrupt(cx)) { + return false; + } + + // Step 17.a.i. + days -= 1; + + // Step 17.a.ii. + if (!::AddDaysToZonedDateTime(cx, startNs, startDateTime, timeZone, + calendar, days, TemporalOverflow::Constrain, + &relativeResult)) { + return false; + } + MOZ_ASSERT(IsValidISODateTime(relativeResult.dateTime)); + MOZ_ASSERT(IsValidEpochInstant(relativeResult.instant)); + } + + MOZ_ASSERT_IF(days > 0, relativeResult.instant <= endNs); + } + + MOZ_ASSERT_IF(days == 0, relativeResult.instant == startNs); + + // Step 18. + auto ns = endNs - relativeResult.instant; + MOZ_ASSERT(IsValidInstantSpan(ns)); + + // Steps 19-21. + InstantSpan dayLengthNs{}; + while (true) { + // This loop can iterate indefinitely when given a specially crafted time + // zone object, so we need to check for interrupts. + if (!CheckForInterrupt(cx)) { + return false; + } + + // Step 21.a. + PlainDateTimeAndInstant oneDayFarther; + if (!::AddDaysToZonedDateTime( + cx, relativeResult.instant, relativeResult.dateTime, timeZone, + calendar, sign, TemporalOverflow::Constrain, &oneDayFarther)) { + return false; + } + MOZ_ASSERT(IsValidISODateTime(oneDayFarther.dateTime)); + MOZ_ASSERT(IsValidEpochInstant(oneDayFarther.instant)); + + // Step 21.b. + dayLengthNs = oneDayFarther.instant - relativeResult.instant; + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + + // clang-format off + // + // First iteration: + // + // ns = endNs - relativeResult.instant + // dayLengthNs = oneDayFarther.instant - relativeResult.instant + // diff = ns - dayLengthNs + // = (endNs - relativeResult.instant) - (oneDayFarther.instant - relativeResult.instant) + // = endNs - relativeResult.instant - oneDayFarther.instant + relativeResult.instant + // = endNs - oneDayFarther.instant + // + // Second iteration: + // + // ns = diff' + // = endNs - oneDayFarther.instant' + // relativeResult.instant = oneDayFarther.instant' + // dayLengthNs = oneDayFarther.instant - relativeResult.instant + // = oneDayFarther.instant - oneDayFarther.instant' + // diff = ns - dayLengthNs + // = (endNs - oneDayFarther.instant') - (oneDayFarther.instant - oneDayFarther.instant') + // = endNs - oneDayFarther.instant' - oneDayFarther.instant + oneDayFarther.instant' + // = endNs - oneDayFarther.instant + // + // Where |diff'| and |oneDayFarther.instant'| denote the variables from the + // previous iteration. + // + // This repeats for all following iterations. + // + // |endNs| and |oneDayFarther.instant| are both valid epoch instant values, + // so the difference is a valid epoch instant difference value, too. + // + // clang-format on + + // Step 21.c. + auto diff = ns - dayLengthNs; + MOZ_ASSERT(IsValidInstantSpan(diff)); + MOZ_ASSERT(diff == (endNs - oneDayFarther.instant)); + + if (diff == InstantSpan{} || ((diff < InstantSpan{}) == (sign < 0))) { + // Step 21.c.i. + ns = diff; + + // Step 21.c.ii. + relativeResult = oneDayFarther; + + // Step 21.c.iii. + days += sign; + } else { + // Step 21.d. + break; + } + } + + // Step 22. + if (days < 0 && sign > 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCORRECT_SIGN, + "days"); + return false; + } + + // Step 23. + if (days > 0 && sign < 0) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCORRECT_SIGN, + "days"); + return false; + } + + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + MOZ_ASSERT(IsValidInstantSpan(ns)); + + // FIXME: spec issue - rewrite steps 24-25 as: + // + // If sign = -1, then + // If nanoseconds > 0, throw a RangeError. + // Else, + // Assert: nanoseconds โฅ 0. + // + // https://github.com/tc39/proposal-temporal/issues/2530 + + // Steps 24-25. + if (sign < 0) { + if (ns > InstantSpan{}) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_INCORRECT_SIGN, + "nanoseconds"); + return false; + } + } else { + MOZ_ASSERT(ns >= InstantSpan{}); + } + + // Step 26. + MOZ_ASSERT(ns.abs() < dayLengthNs.abs()); + + // Step 27. + result.set(NanosecondsAndDays::from(days, ns, dayLengthNs.abs())); + return true; +} + +/** + * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * precalculatedPlainDateTime ] ) + */ +bool js::temporal::NanosecondsToDays(JSContext* cx, + const InstantSpan& nanoseconds, + Handle<ZonedDateTime> zonedRelativeTo, + Handle<TimeZoneRecord> timeZone, + MutableHandle<NanosecondsAndDays> result) { + return ::NanosecondsToDays(cx, nanoseconds, zonedRelativeTo, timeZone, + mozilla::Nothing(), result); +} + +/** + * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * precalculatedPlainDateTime ] ) + */ +bool js::temporal::NanosecondsToDays( + JSContext* cx, const InstantSpan& nanoseconds, + Handle<ZonedDateTime> zonedRelativeTo, Handle<TimeZoneRecord> timeZone, + const PlainDateTime& precalculatedPlainDateTime, + MutableHandle<NanosecondsAndDays> result) { + return ::NanosecondsToDays(cx, nanoseconds, zonedRelativeTo, timeZone, + mozilla::SomeRef(precalculatedPlainDateTime), + result); +} + +/** + * DifferenceZonedDateTime ( ns1, ns2, timeZoneRec, calendarRec, largestUnit, + * options, precalculatedPlainDateTime ) + */ +static bool DifferenceZonedDateTime( + JSContext* cx, const Instant& ns1, const Instant& ns2, + Handle<TimeZoneRecord> timeZone, Handle<CalendarRecord> calendar, + TemporalUnit largestUnit, Handle<PlainObject*> maybeOptions, + mozilla::Maybe<const PlainDateTime&> precalculatedPlainDateTime, + Duration* result) { + MOZ_ASSERT(IsValidEpochInstant(ns1)); + MOZ_ASSERT(IsValidEpochInstant(ns2)); + + // Steps 1. + if (ns1 == ns2) { + *result = {}; + return true; + } + + // Steps 2-3. + PlainDateTime startDateTime; + if (!precalculatedPlainDateTime) { + // Steps 2.a-b. + if (!GetPlainDateTimeFor(cx, timeZone, ns1, &startDateTime)) { + return false; + } + } else { + startDateTime = *precalculatedPlainDateTime; + } + + // Steps 4-5. + PlainDateTime endDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, ns2, &endDateTime)) { + return false; + } + + // Step 6. + Duration dateDifference; + if (maybeOptions) { + if (!DifferenceISODateTime(cx, startDateTime, endDateTime, calendar, + largestUnit, maybeOptions, &dateDifference)) { + return false; + } + } else { + if (!DifferenceISODateTime(cx, startDateTime, endDateTime, calendar, + largestUnit, &dateDifference)) { + return false; + } + } + + // Step 7. + Instant intermediateNs; + if (!AddZonedDateTime(cx, ns1, timeZone, calendar, + { + dateDifference.years, + dateDifference.months, + dateDifference.weeks, + }, + startDateTime, &intermediateNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(intermediateNs)); + + // Step 8. + auto timeRemainder = ns2 - intermediateNs; + MOZ_ASSERT(IsValidInstantSpan(timeRemainder)); + + // Step 9. + Rooted<ZonedDateTime> intermediate( + cx, + ZonedDateTime{intermediateNs, timeZone.receiver(), calendar.receiver()}); + + // Step 10. + Rooted<NanosecondsAndDays> nanosAndDays(cx); + if (!NanosecondsToDays(cx, timeRemainder, intermediate, timeZone, + &nanosAndDays)) { + return false; + } + + // Step 11. + TimeDuration timeDifference; + if (!BalanceTimeDuration(cx, nanosAndDays.nanoseconds(), TemporalUnit::Hour, + &timeDifference)) { + return false; + } + + // Step 12. + *result = { + dateDifference.years, dateDifference.months, + dateDifference.weeks, nanosAndDays.daysNumber(), + timeDifference.hours, timeDifference.minutes, + timeDifference.seconds, timeDifference.milliseconds, + timeDifference.microseconds, timeDifference.nanoseconds, + }; + MOZ_ASSERT(IsValidDuration(*result)); + return true; +} + +/** + * DifferenceZonedDateTime ( ns1, ns2, timeZoneRec, calendarRec, largestUnit, + * options, precalculatedPlainDateTime ) + */ +bool js::temporal::DifferenceZonedDateTime( + JSContext* cx, const Instant& ns1, const Instant& ns2, + Handle<TimeZoneRecord> timeZone, Handle<CalendarRecord> calendar, + TemporalUnit largestUnit, const PlainDateTime& precalculatedPlainDateTime, + Duration* result) { + return ::DifferenceZonedDateTime( + cx, ns1, ns2, timeZone, calendar, largestUnit, nullptr, + mozilla::SomeRef(precalculatedPlainDateTime), result); +} + +/** + * TimeZoneEquals ( one, two ) + */ +static bool TimeZoneEqualsOrThrow(JSContext* cx, Handle<TimeZoneValue> one, + Handle<TimeZoneValue> two) { + // Step 1. + if (one.isObject() && two.isObject() && one.toObject() == two.toObject()) { + return true; + } + + // Step 2. + Rooted<JSString*> timeZoneOne(cx, ToTemporalTimeZoneIdentifier(cx, one)); + if (!timeZoneOne) { + return false; + } + + // Step 3. + Rooted<JSString*> timeZoneTwo(cx, ToTemporalTimeZoneIdentifier(cx, two)); + if (!timeZoneTwo) { + return false; + } + + // Steps 4-9. + bool equals; + if (!TimeZoneEquals(cx, timeZoneOne, timeZoneTwo, &equals)) { + return false; + } + if (equals) { + return true; + } + + // Throw an error when the time zone identifiers don't match. Used when + // unequal time zones throw a RangeError. + if (auto charsOne = QuoteString(cx, timeZoneOne)) { + if (auto charsTwo = QuoteString(cx, timeZoneTwo)) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_TIMEZONE_INCOMPATIBLE, + charsOne.get(), charsTwo.get()); + } + } + return false; +} + +/** + * RoundISODateTime ( year, month, day, hour, minute, second, millisecond, + * microsecond, nanosecond, increment, unit, roundingMode [ , dayLength ] ) + */ +static bool RoundISODateTime(JSContext* cx, const PlainDateTime& dateTime, + Increment increment, TemporalUnit unit, + TemporalRoundingMode roundingMode, + const InstantSpan& dayLength, + PlainDateTime* result) { + MOZ_ASSERT(IsValidInstantSpan(dayLength)); + MOZ_ASSERT(dayLength > (InstantSpan{})); + + const auto& [date, time] = dateTime; + + // Step 1. + MOZ_ASSERT(IsValidISODateTime(dateTime)); + MOZ_ASSERT(ISODateTimeWithinLimits(dateTime)); + + // Step 2. (Not applicable in our implementation.) + + // Step 3. + auto roundedTime = RoundTime(time, increment, unit, roundingMode, dayLength); + + // |dayLength| can be as small as 1, so the number of rounded days can be as + // large as the number of nanoseconds in |time|. + MOZ_ASSERT(0 <= roundedTime.days && + roundedTime.days < ToNanoseconds(TemporalUnit::Day)); + + // Step 4. + PlainDate balanceResult; + if (!BalanceISODate(cx, date.year, date.month, + int64_t(date.day) + roundedTime.days, &balanceResult)) { + return false; + } + + // Step 5. + *result = {balanceResult, roundedTime.time}; + return true; +} + +/** + * DifferenceTemporalZonedDateTime ( operation, zonedDateTime, other, options ) + */ +static bool DifferenceTemporalZonedDateTime(JSContext* cx, + TemporalDifference operation, + const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Rooted<ZonedDateTime> other(cx); + if (!ToTemporalZonedDateTime(cx, args.get(0), &other)) { + return false; + } + + // Step 3. + if (!CalendarEqualsOrThrow(cx, zonedDateTime.calendar(), other.calendar())) { + return false; + } + + // Steps 4-5. + Rooted<PlainObject*> resolvedOptions(cx); + DifferenceSettings settings; + if (args.hasDefined(1)) { + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", ToName(operation), args[1])); + if (!options) { + return false; + } + + // Step 4. + resolvedOptions = SnapshotOwnProperties(cx, options); + if (!resolvedOptions) { + return false; + } + + // Step 5. + if (!GetDifferenceSettings( + cx, operation, resolvedOptions, TemporalUnitGroup::DateTime, + TemporalUnit::Nanosecond, TemporalUnit::Hour, &settings)) { + return false; + } + } else { + // Steps 4-5. + settings = { + TemporalUnit::Nanosecond, + TemporalUnit::Hour, + TemporalRoundingMode::Trunc, + Increment{1}, + }; + } + + // Step 6. + if (settings.largestUnit > TemporalUnit::Day) { + MOZ_ASSERT(settings.smallestUnit >= settings.largestUnit); + + // Step 6.a. + Duration difference; + if (!DifferenceInstant(cx, zonedDateTime.instant(), other.instant(), + settings.roundingIncrement, settings.smallestUnit, + settings.largestUnit, settings.roundingMode, + &difference)) { + return false; + } + + // Step 6.b. + if (operation == TemporalDifference::Since) { + difference = difference.negate(); + } + + auto* result = CreateTemporalDuration(cx, difference); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; + } + + // FIXME: spec issue - move this step next to the calendar validation? + // https://github.com/tc39/proposal-temporal/issues/2533 + + // Step 7. + if (!TimeZoneEqualsOrThrow(cx, zonedDateTime.timeZone(), other.timeZone())) { + return false; + } + + // Step 8. + if (zonedDateTime.instant() == other.instant()) { + auto* obj = CreateTemporalDuration(cx, {}); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Step 9. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Step 10. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), + { + CalendarMethod::DateAdd, + CalendarMethod::DateUntil, + }, + &calendar)) { + return false; + } + + // Steps 11-12. + PlainDateTime precalculatedPlainDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, zonedDateTime.instant(), + &precalculatedPlainDateTime)) { + return false; + } + + // Step 13. + Rooted<PlainDateObject*> plainRelativeTo( + cx, CreateTemporalDate(cx, precalculatedPlainDateTime.date, + calendar.receiver())); + if (!plainRelativeTo) { + return false; + } + + // Step 14. + if (resolvedOptions) { + Rooted<Value> largestUnitValue( + cx, StringValue(TemporalUnitToString(cx, settings.largestUnit))); + if (!DefineDataProperty(cx, resolvedOptions, cx->names().largestUnit, + largestUnitValue)) { + return false; + } + } + + // Step 15. + Duration difference; + if (!::DifferenceZonedDateTime( + cx, zonedDateTime.instant(), other.instant(), timeZone, calendar, + settings.largestUnit, resolvedOptions, + mozilla::SomeRef<const PlainDateTime>(precalculatedPlainDateTime), + &difference)) { + return false; + } + + // Step 16. + bool roundingGranularityIsNoop = + settings.smallestUnit == TemporalUnit::Nanosecond && + settings.roundingIncrement == Increment{1}; + + // Step 17. + if (roundingGranularityIsNoop) { + if (operation == TemporalDifference::Since) { + difference = difference.negate(); + } + + auto* obj = CreateTemporalDuration(cx, difference); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; + } + + // Steps 18-19. + Duration roundResult; + if (!RoundDuration(cx, difference, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + plainRelativeTo, calendar, zonedDateTime, timeZone, + precalculatedPlainDateTime, &roundResult)) { + return false; + } + + // Step 20. + Duration adjustResult; + if (!AdjustRoundedDurationDays(cx, roundResult, settings.roundingIncrement, + settings.smallestUnit, settings.roundingMode, + zonedDateTime, calendar, timeZone, + precalculatedPlainDateTime, &adjustResult)) { + return false; + } + + // Step 21. + DateDuration balanceResult; + if (!temporal::BalanceDateDurationRelative( + cx, adjustResult.date(), settings.largestUnit, settings.smallestUnit, + plainRelativeTo, calendar, &balanceResult)) { + return false; + } + + // Step 22. + auto result = Duration{ + balanceResult.years, balanceResult.months, + balanceResult.weeks, balanceResult.days, + adjustResult.hours, adjustResult.minutes, + adjustResult.seconds, adjustResult.milliseconds, + adjustResult.microseconds, adjustResult.nanoseconds, + }; + if (operation == TemporalDifference::Since) { + result = result.negate(); + } + + auto* obj = CreateTemporalDuration(cx, result); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +enum class ZonedDateTimeDuration { Add, Subtract }; + +/** + * AddDurationToOrSubtractDurationFromZonedDateTime ( operation, zonedDateTime, + * temporalDurationLike, options ) + */ +static bool AddDurationToOrSubtractDurationFromZonedDateTime( + JSContext* cx, ZonedDateTimeDuration operation, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, &args.thisv().toObject().as<ZonedDateTimeObject>()); + + // Step 1. (Not applicable in our implementation.) + + // Step 2. + Duration duration; + if (!ToTemporalDurationRecord(cx, args.get(0), &duration)) { + return false; + } + + // Step 3. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + const char* name = + operation == ZonedDateTimeDuration::Add ? "add" : "subtract"; + options = RequireObjectArg(cx, "options", name, args[1]); + } else { + options = NewPlainObjectWithProto(cx, nullptr); + } + if (!options) { + return false; + } + + // Step 4. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Step 5. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), + { + CalendarMethod::DateAdd, + }, + &calendar)) { + return false; + } + + // Step 6. + if (operation == ZonedDateTimeDuration::Subtract) { + duration = duration.negate(); + } + + Instant resultInstant; + if (!::AddZonedDateTime(cx, zonedDateTime.instant(), timeZone, calendar, + duration, options, &resultInstant)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(resultInstant)); + + // Step 7. + auto* result = CreateTemporalZonedDateTime( + cx, resultInstant, timeZone.receiver(), calendar.receiver()); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime ( epochNanoseconds, timeZoneLike [ , calendarLike ] ) + */ +static bool ZonedDateTimeConstructor(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Temporal.ZonedDateTime")) { + return false; + } + + // Step 2. + Rooted<BigInt*> epochNanoseconds(cx, js::ToBigInt(cx, args.get(0))); + if (!epochNanoseconds) { + return false; + } + + // Step 3. + if (!IsValidEpochNanoseconds(epochNanoseconds)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INSTANT_INVALID); + return false; + } + + // Step 4. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZone(cx, args.get(1), &timeZone)) { + return false; + } + + // Step 5. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendarWithISODefault(cx, args.get(2), &calendar)) { + return false; + } + + // Step 6. + auto* obj = CreateTemporalZonedDateTime(cx, args, epochNanoseconds, timeZone, + calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.ZonedDateTime.from ( item [ , options ] ) + */ +static bool ZonedDateTime_from(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<JSObject*> options(cx); + if (args.hasDefined(1)) { + options = RequireObjectArg(cx, "options", "from", args[1]); + if (!options) { + return false; + } + } + + // Step 2. + if (args.get(0).isObject()) { + JSObject* item = &args[0].toObject(); + if (auto* zonedDateTime = item->maybeUnwrapIf<ZonedDateTimeObject>()) { + auto epochInstant = ToInstant(zonedDateTime); + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar()); + + if (!timeZone.wrap(cx)) { + return false; + } + if (!calendar.wrap(cx)) { + return false; + } + + if (options) { + // Steps 2.a-b. + TemporalDisambiguation ignoredDisambiguation; + if (!ToTemporalDisambiguation(cx, options, &ignoredDisambiguation)) { + return false; + } + + // Step 2.c. + TemporalOffset ignoredOffset; + if (!ToTemporalOffset(cx, options, &ignoredOffset)) { + return false; + } + + // Step 2.d. + TemporalOverflow ignoredOverflow; + if (!ToTemporalOverflow(cx, options, &ignoredOverflow)) { + return false; + } + } + + // Step 2.e. + auto* result = + CreateTemporalZonedDateTime(cx, epochInstant, timeZone, calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; + } + } + + // Step 3. + auto* result = ToTemporalZonedDateTime(cx, args.get(0), options); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.compare ( one, two ) + */ +static bool ZonedDateTime_compare(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + Rooted<ZonedDateTime> one(cx); + if (!ToTemporalZonedDateTime(cx, args.get(0), &one)) { + return false; + } + + // Step 2. + Rooted<ZonedDateTime> two(cx); + if (!ToTemporalZonedDateTime(cx, args.get(1), &two)) { + return false; + } + + // Step 3. + auto oneNs = one.instant(); + auto twoNs = two.instant(); + args.rval().setInt32(oneNs > twoNs ? 1 : oneNs < twoNs ? -1 : 0); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.calendarId + */ +static bool ZonedDateTime_calendarId(JSContext* cx, const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + + // Step 3. + Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar()); + auto* calendarId = ToTemporalCalendarIdentifier(cx, calendar); + if (!calendarId) { + return false; + } + + args.rval().setString(calendarId); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.calendarId + */ +static bool ZonedDateTime_calendarId(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_calendarId>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.timeZoneId + */ +static bool ZonedDateTime_timeZoneId(JSContext* cx, const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + + // Step 3. + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + auto* timeZoneId = ToTemporalTimeZoneIdentifier(cx, timeZone); + if (!timeZoneId) { + return false; + } + + args.rval().setString(timeZoneId); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.timeZoneId + */ +static bool ZonedDateTime_timeZoneId(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_timeZoneId>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.year + */ +static bool ZonedDateTime_year(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarYear(cx, zonedDateTime.calendar(), dateTime, args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.year + */ +static bool ZonedDateTime_year(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_year>(cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.month + */ +static bool ZonedDateTime_month(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarMonth(cx, zonedDateTime.calendar(), dateTime, args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.month + */ +static bool ZonedDateTime_month(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_month>(cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.monthCode + */ +static bool ZonedDateTime_monthCode(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarMonthCode(cx, zonedDateTime.calendar(), dateTime, args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.monthCode + */ +static bool ZonedDateTime_monthCode(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_monthCode>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.day + */ +static bool ZonedDateTime_day(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 4. (Reordered) + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), + { + CalendarMethod::Day, + }, + &calendar)) { + return false; + } + + // Steps 3 and 5-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarDay(cx, calendar, dateTime, args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.day + */ +static bool ZonedDateTime_day(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_day>(cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.hour + */ +static bool ZonedDateTime_hour(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + args.rval().setInt32(dateTime.time.hour); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.hour + */ +static bool ZonedDateTime_hour(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_hour>(cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.minute + */ +static bool ZonedDateTime_minute(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + args.rval().setInt32(dateTime.time.minute); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.minute + */ +static bool ZonedDateTime_minute(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_minute>(cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.second + */ +static bool ZonedDateTime_second(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + args.rval().setInt32(dateTime.time.second); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.second + */ +static bool ZonedDateTime_second(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_second>(cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.millisecond + */ +static bool ZonedDateTime_millisecond(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + args.rval().setInt32(dateTime.time.millisecond); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.millisecond + */ +static bool ZonedDateTime_millisecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_millisecond>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.microsecond + */ +static bool ZonedDateTime_microsecond(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + args.rval().setInt32(dateTime.time.microsecond); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.microsecond + */ +static bool ZonedDateTime_microsecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_microsecond>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.nanosecond + */ +static bool ZonedDateTime_nanosecond(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + args.rval().setInt32(dateTime.time.nanosecond); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.nanosecond + */ +static bool ZonedDateTime_nanosecond(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_nanosecond>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.epochSeconds + */ +static bool ZonedDateTime_epochSeconds(JSContext* cx, const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + + // Step 3. + auto instant = ToInstant(zonedDateTime); + + // Steps 4-5. + args.rval().setNumber(instant.seconds); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.epochSeconds + */ +static bool ZonedDateTime_epochSeconds(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_epochSeconds>( + cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.epochMilliseconds + */ +static bool ZonedDateTime_epochMilliseconds(JSContext* cx, + const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + + // Step 3. + auto instant = ToInstant(zonedDateTime); + + // Steps 4-5. + args.rval().setNumber(instant.floorToMilliseconds()); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.epochMilliseconds + */ +static bool ZonedDateTime_epochMilliseconds(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_epochMilliseconds>( + cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.epochMicroseconds + */ +static bool ZonedDateTime_epochMicroseconds(JSContext* cx, + const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + + // Step 3. + auto instant = ToInstant(zonedDateTime); + + // Step 4. + auto* microseconds = + BigInt::createFromInt64(cx, instant.floorToMicroseconds()); + if (!microseconds) { + return false; + } + + // Step 5. + args.rval().setBigInt(microseconds); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.epochMicroseconds + */ +static bool ZonedDateTime_epochMicroseconds(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_epochMicroseconds>( + cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.epochNanoseconds + */ +static bool ZonedDateTime_epochNanoseconds(JSContext* cx, + const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + + // Step 3. + auto* nanoseconds = ToEpochNanoseconds(cx, ToInstant(zonedDateTime)); + if (!nanoseconds) { + return false; + } + + args.rval().setBigInt(nanoseconds); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.epochNanoseconds + */ +static bool ZonedDateTime_epochNanoseconds(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_epochNanoseconds>( + cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.dayOfWeek + */ +static bool ZonedDateTime_dayOfWeek(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarDayOfWeek(cx, zonedDateTime.calendar(), dateTime, args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.dayOfWeek + */ +static bool ZonedDateTime_dayOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_dayOfWeek>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.dayOfYear + */ +static bool ZonedDateTime_dayOfYear(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarDayOfYear(cx, zonedDateTime.calendar(), dateTime, args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.dayOfYear + */ +static bool ZonedDateTime_dayOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_dayOfYear>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.weekOfYear + */ +static bool ZonedDateTime_weekOfYear(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarWeekOfYear(cx, zonedDateTime.calendar(), dateTime, + args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.weekOfYear + */ +static bool ZonedDateTime_weekOfYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_weekOfYear>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.yearOfWeek + */ +static bool ZonedDateTime_yearOfWeek(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarYearOfWeek(cx, zonedDateTime.calendar(), dateTime, + args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.yearOfWeek + */ +static bool ZonedDateTime_yearOfWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_yearOfWeek>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.hoursInDay + */ +static bool ZonedDateTime_hoursInDay(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Step 4. + auto instant = zonedDateTime.instant(); + + // Step 5. + PlainDateTime temporalDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, instant, &temporalDateTime)) { + return false; + } + + // Steps 6-8. + const auto& date = temporalDateTime.date; + Rooted<CalendarValue> isoCalendar(cx, CalendarValue(cx->names().iso8601)); + + // Step 9. + Rooted<PlainDateTimeWithCalendar> today(cx); + if (!CreateTemporalDateTime(cx, {date, {}}, isoCalendar, &today)) { + return false; + } + + // Step 10. + auto tomorrowFields = BalanceISODate(date.year, date.month, date.day + 1); + + // Step 11. + Rooted<PlainDateTimeWithCalendar> tomorrow(cx); + if (!CreateTemporalDateTime(cx, {tomorrowFields, {}}, isoCalendar, + &tomorrow)) { + return false; + } + + // Step 12. + Instant todayInstant; + if (!GetInstantFor(cx, timeZone, today, TemporalDisambiguation::Compatible, + &todayInstant)) { + return false; + } + + // Step 13. + Instant tomorrowInstant; + if (!GetInstantFor(cx, timeZone, tomorrow, TemporalDisambiguation::Compatible, + &tomorrowInstant)) { + return false; + } + + // Step 14. + auto diffNs = tomorrowInstant - todayInstant; + MOZ_ASSERT(IsValidInstantSpan(diffNs)); + + // Step 15. + constexpr int32_t secPerHour = 60 * 60; + constexpr int64_t nsPerSec = ToNanoseconds(TemporalUnit::Second); + constexpr double nsPerHour = ToNanoseconds(TemporalUnit::Hour); + + int64_t hours = diffNs.seconds / secPerHour; + int64_t seconds = diffNs.seconds % secPerHour; + int64_t nanoseconds = seconds * nsPerSec + diffNs.nanoseconds; + + double result = double(hours) + double(nanoseconds) / nsPerHour; + args.rval().setNumber(result); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.hoursInDay + */ +static bool ZonedDateTime_hoursInDay(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_hoursInDay>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.daysInWeek + */ +static bool ZonedDateTime_daysInWeek(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarDaysInWeek(cx, zonedDateTime.calendar(), dateTime, + args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.daysInWeek + */ +static bool ZonedDateTime_daysInWeek(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_daysInWeek>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.daysInMonth + */ +static bool ZonedDateTime_daysInMonth(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarDaysInMonth(cx, zonedDateTime.calendar(), dateTime, + args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.daysInMonth + */ +static bool ZonedDateTime_daysInMonth(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_daysInMonth>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.daysInYear + */ +static bool ZonedDateTime_daysInYear(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarDaysInYear(cx, zonedDateTime.calendar(), dateTime, + args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.daysInYear + */ +static bool ZonedDateTime_daysInYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_daysInYear>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.monthsInYear + */ +static bool ZonedDateTime_monthsInYear(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarMonthsInYear(cx, zonedDateTime.calendar(), dateTime, + args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.monthsInYear + */ +static bool ZonedDateTime_monthsInYear(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_monthsInYear>( + cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.inLeapYear + */ +static bool ZonedDateTime_inLeapYear(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime dateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &dateTime)) { + return false; + } + + // Step 7. + return CalendarInLeapYear(cx, zonedDateTime.calendar(), dateTime, + args.rval()); +} + +/** + * get Temporal.ZonedDateTime.prototype.inLeapYear + */ +static bool ZonedDateTime_inLeapYear(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_inLeapYear>(cx, + args); +} + +/** + * get Temporal.ZonedDateTime.prototype.offsetNanoseconds + */ +static bool ZonedDateTime_offsetNanoseconds(JSContext* cx, + const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + auto timeZone = zonedDateTime.timeZone(); + + // Step 4. + auto instant = zonedDateTime.instant(); + + // Step 5. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return false; + } + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + args.rval().setNumber(offsetNanoseconds); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.offsetNanoseconds + */ +static bool ZonedDateTime_offsetNanoseconds(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_offsetNanoseconds>( + cx, args); +} + +/** + * get Temporal.ZonedDateTime.prototype.offset + */ +static bool ZonedDateTime_offset(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + auto timeZone = zonedDateTime.timeZone(); + + // Step 4. + auto instant = zonedDateTime.instant(); + + // Step 5. + JSString* str = GetOffsetStringFor(cx, timeZone, instant); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * get Temporal.ZonedDateTime.prototype.offset + */ +static bool ZonedDateTime_offset(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_offset>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.with ( temporalZonedDateTimeLike [ , options + * ] ) + */ +static bool ZonedDateTime_with(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<JSObject*> temporalZonedDateTimeLike( + cx, + RequireObjectArg(cx, "temporalZonedDateTimeLike", "with", args.get(0))); + if (!temporalZonedDateTimeLike) { + return false; + } + + // Step 4. + if (!RejectTemporalLikeObject(cx, temporalZonedDateTimeLike)) { + return false; + } + + // Step 5. + Rooted<PlainObject*> resolvedOptions(cx); + if (args.hasDefined(1)) { + Rooted<JSObject*> options(cx, + RequireObjectArg(cx, "options", "with", args[1])); + if (!options) { + return false; + } + resolvedOptions = SnapshotOwnProperties(cx, options); + } else { + resolvedOptions = NewPlainObjectWithProto(cx, nullptr); + } + if (!resolvedOptions) { + return false; + } + + // Step 6. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), + { + CalendarMethod::DateFromFields, + CalendarMethod::Fields, + CalendarMethod::MergeFields, + }, + &calendar)) { + return false; + } + + // Step 7. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Step 8. + auto instant = zonedDateTime.instant(); + + // Step 9. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return false; + } + + // Step 10. + Rooted<PlainDateTimeObject*> dateTime( + cx, + GetPlainDateTimeFor(cx, instant, calendar.receiver(), offsetNanoseconds)); + if (!dateTime) { + return false; + } + + // Step 11. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::Month, + CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 12. + Rooted<PlainObject*> fields(cx, + PrepareTemporalFields(cx, dateTime, fieldNames)); + if (!fields) { + return false; + } + + // Steps 13-18. + struct TimeField { + using FieldName = ImmutableTenuredPtr<PropertyName*> JSAtomState::*; + + FieldName name; + int32_t value; + } timeFields[] = { + {&JSAtomState::hour, dateTime->isoHour()}, + {&JSAtomState::minute, dateTime->isoMinute()}, + {&JSAtomState::second, dateTime->isoSecond()}, + {&JSAtomState::millisecond, dateTime->isoMillisecond()}, + {&JSAtomState::microsecond, dateTime->isoMicrosecond()}, + {&JSAtomState::nanosecond, dateTime->isoNanosecond()}, + }; + + Rooted<Value> timeFieldValue(cx); + for (const auto& timeField : timeFields) { + Handle<PropertyName*> name = cx->names().*(timeField.name); + timeFieldValue.setInt32(timeField.value); + + if (!DefineDataProperty(cx, fields, name, timeFieldValue)) { + return false; + } + } + + // Step 19. + JSString* fieldsOffset = FormatUTCOffsetNanoseconds(cx, offsetNanoseconds); + if (!fieldsOffset) { + return false; + } + + timeFieldValue.setString(fieldsOffset); + if (!DefineDataProperty(cx, fields, cx->names().offset, timeFieldValue)) { + return false; + } + + // Step 20. + if (!AppendSorted(cx, fieldNames.get(), + { + TemporalField::Hour, + TemporalField::Microsecond, + TemporalField::Millisecond, + TemporalField::Minute, + TemporalField::Nanosecond, + TemporalField::Offset, + TemporalField::Second, + })) { + return false; + } + + // Step 21. + Rooted<PlainObject*> partialZonedDateTime( + cx, + PreparePartialTemporalFields(cx, temporalZonedDateTimeLike, fieldNames)); + if (!partialZonedDateTime) { + return false; + } + + // Step 22. + Rooted<JSObject*> mergedFields( + cx, CalendarMergeFields(cx, calendar, fields, partialZonedDateTime)); + if (!mergedFields) { + return false; + } + + // Step 23. + fields = PrepareTemporalFields(cx, mergedFields, fieldNames, + {TemporalField::Offset}); + if (!fields) { + return false; + } + + // Step 24-25. + auto disambiguation = TemporalDisambiguation::Compatible; + if (!ToTemporalDisambiguation(cx, resolvedOptions, &disambiguation)) { + return false; + } + + // Step 26. + auto offset = TemporalOffset::Prefer; + if (!ToTemporalOffset(cx, resolvedOptions, &offset)) { + return false; + } + + // Step 27. + PlainDateTime dateTimeResult; + if (!InterpretTemporalDateTimeFields(cx, calendar, fields, resolvedOptions, + &dateTimeResult)) { + return false; + } + + // Step 28. + Rooted<Value> offsetString(cx); + if (!GetProperty(cx, fields, fields, cx->names().offset, &offsetString)) { + return false; + } + + // Step 29. + MOZ_ASSERT(offsetString.isString()); + + // Step 30. + Rooted<JSString*> offsetStr(cx, offsetString.toString()); + int64_t newOffsetNanoseconds; + if (!ParseDateTimeUTCOffset(cx, offsetStr, &newOffsetNanoseconds)) { + return false; + } + + // Step 31. + Instant epochNanoseconds; + if (!InterpretISODateTimeOffset( + cx, dateTimeResult, OffsetBehaviour::Option, newOffsetNanoseconds, + timeZone, disambiguation, offset, MatchBehaviour::MatchExactly, + &epochNanoseconds)) { + return false; + } + + // Step 32. + auto* result = CreateTemporalZonedDateTime( + cx, epochNanoseconds, timeZone.receiver(), calendar.receiver()); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.with ( temporalZonedDateTimeLike [ , options + * ] ) + */ +static bool ZonedDateTime_with(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_with>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.withPlainTime ( [ plainTimeLike ] ) + */ +static bool ZonedDateTime_withPlainTime(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-4. + PlainTime time = {}; + if (args.hasDefined(0)) { + if (!ToTemporalTime(cx, args[0], &time)) { + return false; + } + } + + // Step 5. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Steps 6 and 8. + PlainDateTime plainDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, zonedDateTime.instant(), + &plainDateTime)) { + return false; + } + + // Step 7. + auto calendar = zonedDateTime.calendar(); + + // Step 9. + Rooted<PlainDateTimeWithCalendar> resultPlainDateTime(cx); + if (!CreateTemporalDateTime(cx, {plainDateTime.date, time}, calendar, + &resultPlainDateTime)) { + return false; + } + + // Step 10. + Instant instant; + if (!GetInstantFor(cx, timeZone, resultPlainDateTime, + TemporalDisambiguation::Compatible, &instant)) { + return false; + } + + // Step 11. + auto* result = + CreateTemporalZonedDateTime(cx, instant, timeZone.receiver(), calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.withPlainTime ( [ plainTimeLike ] ) + */ +static bool ZonedDateTime_withPlainTime(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_withPlainTime>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.withPlainDate ( plainDateLike ) + */ +static bool ZonedDateTime_withPlainDate(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<PlainDateWithCalendar> plainDate(cx); + if (!ToTemporalDate(cx, args.get(0), &plainDate)) { + return false; + } + auto date = plainDate.date(); + + // Step 4. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Steps 5-6. + PlainDateTime plainDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, zonedDateTime.instant(), + &plainDateTime)) { + return false; + } + + // Step 7. + Rooted<CalendarValue> calendar(cx); + if (!ConsolidateCalendars(cx, zonedDateTime.calendar(), plainDate.calendar(), + &calendar)) { + return false; + } + + // Step 8. + Rooted<PlainDateTimeWithCalendar> resultPlainDateTime(cx); + if (!CreateTemporalDateTime(cx, {date, plainDateTime.time}, calendar, + &resultPlainDateTime)) { + return false; + } + + // Step 9. + Instant instant; + if (!GetInstantFor(cx, timeZone, resultPlainDateTime, + TemporalDisambiguation::Compatible, &instant)) { + return false; + } + + // Step 10. + auto* result = + CreateTemporalZonedDateTime(cx, instant, timeZone.receiver(), calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.withPlainDate ( plainDateLike ) + */ +static bool ZonedDateTime_withPlainDate(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_withPlainDate>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.withTimeZone ( timeZoneLike ) + */ +static bool ZonedDateTime_withTimeZone(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<TimeZoneValue> timeZone(cx); + if (!ToTemporalTimeZone(cx, args.get(0), &timeZone)) { + return false; + } + + // Step 4. + auto* result = CreateTemporalZonedDateTime( + cx, zonedDateTime.instant(), timeZone, zonedDateTime.calendar()); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.withTimeZone ( timeZoneLike ) + */ +static bool ZonedDateTime_withTimeZone(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_withTimeZone>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.withCalendar ( calendarLike ) + */ +static bool ZonedDateTime_withCalendar(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<CalendarValue> calendar(cx); + if (!ToTemporalCalendar(cx, args.get(0), &calendar)) { + return false; + } + + // Step 4. + auto* result = CreateTemporalZonedDateTime( + cx, zonedDateTime.instant(), zonedDateTime.timeZone(), calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.withCalendar ( calendarLike ) + */ +static bool ZonedDateTime_withCalendar(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_withCalendar>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool ZonedDateTime_add(JSContext* cx, const CallArgs& args) { + return AddDurationToOrSubtractDurationFromZonedDateTime( + cx, ZonedDateTimeDuration::Add, args); +} + +/** + * Temporal.ZonedDateTime.prototype.add ( temporalDurationLike [ , options ] ) + */ +static bool ZonedDateTime_add(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_add>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.subtract ( temporalDurationLike [ , options + * ] ) + */ +static bool ZonedDateTime_subtract(JSContext* cx, const CallArgs& args) { + return AddDurationToOrSubtractDurationFromZonedDateTime( + cx, ZonedDateTimeDuration::Subtract, args); +} + +/** + * Temporal.ZonedDateTime.prototype.subtract ( temporalDurationLike [ , options + * ] ) + */ +static bool ZonedDateTime_subtract(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_subtract>(cx, + args); +} + +/** + * Temporal.ZonedDateTime.prototype.until ( other [ , options ] ) + */ +static bool ZonedDateTime_until(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalZonedDateTime(cx, TemporalDifference::Until, args); +} + +/** + * Temporal.ZonedDateTime.prototype.until ( other [ , options ] ) + */ +static bool ZonedDateTime_until(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_until>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.since ( other [ , options ] ) + */ +static bool ZonedDateTime_since(JSContext* cx, const CallArgs& args) { + // Step 3. + return DifferenceTemporalZonedDateTime(cx, TemporalDifference::Since, args); +} + +/** + * Temporal.ZonedDateTime.prototype.since ( other [ , options ] ) + */ +static bool ZonedDateTime_since(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_since>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.round ( roundTo ) + */ +static bool ZonedDateTime_round(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-12. + auto smallestUnit = TemporalUnit::Auto; + auto roundingMode = TemporalRoundingMode::HalfExpand; + auto roundingIncrement = Increment{1}; + if (args.get(0).isString()) { + // Step 4. (Not applicable in our implementation.) + + // Step 9. + Rooted<JSString*> paramString(cx, args[0].toString()); + if (!GetTemporalUnit(cx, paramString, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::DayTime, &smallestUnit)) { + return false; + } + + // Steps 6-8 and 10-12. (Implicit) + } else { + // Steps 3 and 5.a + Rooted<JSObject*> roundTo( + cx, RequireObjectArg(cx, "roundTo", "round", args.get(0))); + if (!roundTo) { + return false; + } + + // Steps 6-7. + if (!ToTemporalRoundingIncrement(cx, roundTo, &roundingIncrement)) { + return false; + } + + // Step 8. + if (!ToTemporalRoundingMode(cx, roundTo, &roundingMode)) { + return false; + } + + // Step 9. + if (!GetTemporalUnit(cx, roundTo, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::DayTime, &smallestUnit)) { + return false; + } + + if (smallestUnit == TemporalUnit::Auto) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_MISSING_OPTION, "smallestUnit"); + return false; + } + + MOZ_ASSERT(TemporalUnit::Day <= smallestUnit && + smallestUnit <= TemporalUnit::Nanosecond); + + // Steps 10-11. + auto maximum = Increment{1}; + bool inclusive = true; + if (smallestUnit > TemporalUnit::Day) { + maximum = MaximumTemporalDurationRoundingIncrement(smallestUnit); + inclusive = false; + } + + // Step 12. + if (!ValidateTemporalRoundingIncrement(cx, roundingIncrement, maximum, + inclusive)) { + return false; + } + } + + // Step 13. + if (smallestUnit == TemporalUnit::Nanosecond && + roundingIncrement == Increment{1}) { + // Step 13.a. + auto* result = CreateTemporalZonedDateTime(cx, zonedDateTime.instant(), + zonedDateTime.timeZone(), + zonedDateTime.calendar()); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; + } + + // Step 14. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Step 16. (Reordered) + auto calendar = zonedDateTime.calendar(); + + // Steps 15 and 17. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, zonedDateTime.instant(), + &offsetNanoseconds)) { + return false; + } + MOZ_ASSERT(std::abs(offsetNanoseconds) < ToNanoseconds(TemporalUnit::Day)); + + // Step 18. + auto temporalDateTime = + GetPlainDateTimeFor(zonedDateTime.instant(), offsetNanoseconds); + + // Step 19. + Rooted<CalendarValue> isoCalendar(cx, CalendarValue(cx->names().iso8601)); + Rooted<PlainDateTimeWithCalendar> dtStart(cx); + if (!CreateTemporalDateTime(cx, {temporalDateTime.date, {}}, isoCalendar, + &dtStart)) { + return false; + } + + // Steps 20-21. + Instant startNs; + if (!GetInstantFor(cx, timeZone, dtStart, TemporalDisambiguation::Compatible, + &startNs)) { + return false; + } + + // Step 22. + Instant endNs; + if (!AddDaysToZonedDateTime(cx, startNs, ToPlainDateTime(dtStart), timeZone, + calendar, 1, &endNs)) { + return false; + } + MOZ_ASSERT(IsValidEpochInstant(endNs)); + + // Step 23. + auto dayLengthNs = endNs - startNs; + MOZ_ASSERT(IsValidInstantSpan(dayLengthNs)); + + // Step 24. + if (dayLengthNs <= InstantSpan{}) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_ZONED_DATE_TIME_NON_POSITIVE_DAY_LENGTH); + return false; + } + + // Step 25. + PlainDateTime roundResult; + if (!RoundISODateTime(cx, temporalDateTime, roundingIncrement, smallestUnit, + roundingMode, dayLengthNs, &roundResult)) { + return false; + } + + // Step 26. + Instant epochNanoseconds; + if (!InterpretISODateTimeOffset( + cx, roundResult, OffsetBehaviour::Option, offsetNanoseconds, timeZone, + TemporalDisambiguation::Compatible, TemporalOffset::Prefer, + MatchBehaviour::MatchExactly, &epochNanoseconds)) { + return false; + } + + // Step 27. + auto* result = CreateTemporalZonedDateTime(cx, epochNanoseconds, + timeZone.receiver(), calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.round ( roundTo ) + */ +static bool ZonedDateTime_round(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_round>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.equals ( other ) + */ +static bool ZonedDateTime_equals(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<ZonedDateTime> other(cx); + if (!ToTemporalZonedDateTime(cx, args.get(0), &other)) { + return false; + } + + // Steps 4-6. + bool equals = zonedDateTime.instant() == other.instant(); + if (equals && !TimeZoneEquals(cx, zonedDateTime.timeZone(), other.timeZone(), + &equals)) { + return false; + } + if (equals && !CalendarEquals(cx, zonedDateTime.calendar(), other.calendar(), + &equals)) { + return false; + } + + args.rval().setBoolean(equals); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.equals ( other ) + */ +static bool ZonedDateTime_equals(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_equals>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.toString ( [ options ] ) + */ +static bool ZonedDateTime_toString(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + SecondsStringPrecision precision = {Precision::Auto(), + TemporalUnit::Nanosecond, Increment{1}}; + auto roundingMode = TemporalRoundingMode::Trunc; + auto showCalendar = CalendarOption::Auto; + auto showTimeZone = TimeZoneNameOption::Auto; + auto showOffset = ShowOffsetOption::Auto; + if (args.hasDefined(0)) { + // Step 3. + Rooted<JSObject*> options( + cx, RequireObjectArg(cx, "options", "toString", args[0])); + if (!options) { + return false; + } + + // Steps 4-5. + if (!ToCalendarNameOption(cx, options, &showCalendar)) { + return false; + } + + // Step 6. + auto digits = Precision::Auto(); + if (!ToFractionalSecondDigits(cx, options, &digits)) { + return false; + } + + // Step 7. + if (!ToShowOffsetOption(cx, options, &showOffset)) { + return false; + } + + // Step 8. + if (!ToTemporalRoundingMode(cx, options, &roundingMode)) { + return false; + } + + // Step 9. + auto smallestUnit = TemporalUnit::Auto; + if (!GetTemporalUnit(cx, options, TemporalUnitKey::SmallestUnit, + TemporalUnitGroup::Time, &smallestUnit)) { + return false; + } + + // Step 10. + if (smallestUnit == TemporalUnit::Hour) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, + JSMSG_TEMPORAL_INVALID_UNIT_OPTION, "hour", + "smallestUnit"); + return false; + } + + // Step 11. + if (!ToTimeZoneNameOption(cx, options, &showTimeZone)) { + return false; + } + + // Step 12. + precision = ToSecondsStringPrecision(smallestUnit, digits); + } + + // Step 13. + JSString* str = TemporalZonedDateTimeToString( + cx, zonedDateTime, precision.precision, showCalendar, showTimeZone, + showOffset, precision.increment, precision.unit, roundingMode); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toString ( [ options ] ) + */ +static bool ZonedDateTime_toString(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toString>(cx, + args); +} + +/** + * Temporal.ZonedDateTime.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool ZonedDateTime_toLocaleString(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + JSString* str = TemporalZonedDateTimeToString( + cx, zonedDateTime, Precision::Auto(), CalendarOption::Auto, + TimeZoneNameOption::Auto, ShowOffsetOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toLocaleString ( [ locales [ , options ] ] ) + */ +static bool ZonedDateTime_toLocaleString(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toLocaleString>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.toJSON ( ) + */ +static bool ZonedDateTime_toJSON(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + JSString* str = TemporalZonedDateTimeToString( + cx, zonedDateTime, Precision::Auto(), CalendarOption::Auto, + TimeZoneNameOption::Auto, ShowOffsetOption::Auto); + if (!str) { + return false; + } + + args.rval().setString(str); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toJSON ( ) + */ +static bool ZonedDateTime_toJSON(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toJSON>(cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.valueOf ( ) + */ +static bool ZonedDateTime_valueOf(JSContext* cx, unsigned argc, Value* vp) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_CANT_CONVERT_TO, + "ZonedDateTime", "primitive type"); + return false; +} + +/** + * Temporal.ZonedDateTime.prototype.startOfDay ( ) + */ +static bool ZonedDateTime_startOfDay(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<TimeZoneRecord> timeZone(cx); + if (!CreateTimeZoneMethodsRecord(cx, zonedDateTime.timeZone(), + { + TimeZoneMethod::GetOffsetNanosecondsFor, + TimeZoneMethod::GetPossibleInstantsFor, + }, + &timeZone)) { + return false; + } + + // Step 4. + auto calendar = zonedDateTime.calendar(); + + // Step 5. + auto instant = zonedDateTime.instant(); + + // Steps 5-6. + PlainDateTime temporalDateTime; + if (!GetPlainDateTimeFor(cx, timeZone, instant, &temporalDateTime)) { + return false; + } + + // Step 7. + Rooted<PlainDateTimeWithCalendar> startDateTime(cx); + if (!CreateTemporalDateTime(cx, {temporalDateTime.date, {}}, calendar, + &startDateTime)) { + return false; + } + + // Step 8. + Instant startInstant; + if (!GetInstantFor(cx, timeZone, startDateTime, + TemporalDisambiguation::Compatible, &startInstant)) { + return false; + } + + // Step 9. + auto* result = CreateTemporalZonedDateTime(cx, startInstant, + timeZone.receiver(), calendar); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.startOfDay ( ) + */ +static bool ZonedDateTime_startOfDay(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_startOfDay>(cx, + args); +} + +/** + * Temporal.ZonedDateTime.prototype.toInstant ( ) + */ +static bool ZonedDateTime_toInstant(JSContext* cx, const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + auto instant = ToInstant(zonedDateTime); + + // Step 3. + auto* result = CreateTemporalInstant(cx, instant); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toInstant ( ) + */ +static bool ZonedDateTime_toInstant(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toInstant>(cx, + args); +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainDate ( ) + */ +static bool ZonedDateTime_toPlainDate(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime temporalDateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &temporalDateTime)) { + return false; + } + + // Step 7. + auto* result = + CreateTemporalDate(cx, temporalDateTime.date, zonedDateTime.calendar()); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainDate ( ) + */ +static bool ZonedDateTime_toPlainDate(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toPlainDate>(cx, + args); +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainTime ( ) + */ +static bool ZonedDateTime_toPlainTime(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-6. + PlainDateTime temporalDateTime; + if (!GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), + zonedDateTime.instant(), &temporalDateTime)) { + return false; + } + + // Step 7. + auto* result = CreateTemporalTime(cx, temporalDateTime.time); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainTime ( ) + */ +static bool ZonedDateTime_toPlainTime(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toPlainTime>(cx, + args); +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainDateTime ( ) + */ +static bool ZonedDateTime_toPlainDateTime(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Steps 3-5. + auto* result = + GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), zonedDateTime.instant(), + zonedDateTime.calendar()); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainDateTime ( ) + */ +static bool ZonedDateTime_toPlainDateTime(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toPlainDateTime>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainYearMonth ( ) + */ +static bool ZonedDateTime_toPlainYearMonth(JSContext* cx, + const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), + { + CalendarMethod::Fields, + CalendarMethod::YearMonthFromFields, + }, + &calendar)) { + return false; + } + + // Steps 4-6. + Rooted<PlainDateTimeObject*> temporalDateTime( + cx, + GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), zonedDateTime.instant(), + zonedDateTime.calendar())); + if (!temporalDateTime) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::MonthCode, CalendarField::Year}, + &fieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDateTime, fieldNames)); + if (!fields) { + return false; + } + + // Steps 9-10. + auto result = CalendarYearMonthFromFields(cx, calendar, fields); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainYearMonth ( ) + */ +static bool ZonedDateTime_toPlainYearMonth(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toPlainYearMonth>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainMonthDay ( ) + */ +static bool ZonedDateTime_toPlainMonthDay(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<CalendarRecord> calendar(cx); + if (!CreateCalendarMethodsRecord(cx, zonedDateTime.calendar(), + { + CalendarMethod::Fields, + CalendarMethod::MonthDayFromFields, + }, + &calendar)) { + return false; + } + + // Steps 4-6. + Rooted<PlainDateTimeObject*> temporalDateTime( + cx, + GetPlainDateTimeFor(cx, zonedDateTime.timeZone(), zonedDateTime.instant(), + zonedDateTime.calendar())); + if (!temporalDateTime) { + return false; + } + + // Step 7. + JS::RootedVector<PropertyKey> fieldNames(cx); + if (!CalendarFields(cx, calendar, + {CalendarField::Day, CalendarField::MonthCode}, + &fieldNames)) { + return false; + } + + // Step 8. + Rooted<PlainObject*> fields( + cx, PrepareTemporalFields(cx, temporalDateTime, fieldNames)); + if (!fields) { + return false; + } + + // Steps 9-10. + auto result = CalendarMonthDayFromFields(cx, calendar, fields); + if (!result) { + return false; + } + + args.rval().setObject(*result); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.toPlainMonthDay ( ) + */ +static bool ZonedDateTime_toPlainMonthDay(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_toPlainMonthDay>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.getISOFields ( ) + */ +static bool ZonedDateTime_getISOFields(JSContext* cx, const CallArgs& args) { + Rooted<ZonedDateTime> zonedDateTime( + cx, ZonedDateTime{&args.thisv().toObject().as<ZonedDateTimeObject>()}); + + // Step 3. + Rooted<IdValueVector> fields(cx, IdValueVector(cx)); + + // Step 4. + auto instant = zonedDateTime.instant(); + + // Step 5. + auto calendar = zonedDateTime.calendar(); + + // Step 6. + auto timeZone = zonedDateTime.timeZone(); + + // Step 7. + int64_t offsetNanoseconds; + if (!GetOffsetNanosecondsFor(cx, timeZone, instant, &offsetNanoseconds)) { + return false; + } + + // Step 8. + auto temporalDateTime = GetPlainDateTimeFor(instant, offsetNanoseconds); + + // Step 9. + Rooted<JSString*> offset(cx, + FormatUTCOffsetNanoseconds(cx, offsetNanoseconds)); + if (!offset) { + return false; + } + + // Step 10. + if (!fields.emplaceBack(NameToId(cx->names().calendar), calendar.toValue())) { + return false; + } + + // Step 11. + if (!fields.emplaceBack(NameToId(cx->names().isoDay), + Int32Value(temporalDateTime.date.day))) { + return false; + } + + // Step 12. + if (!fields.emplaceBack(NameToId(cx->names().isoHour), + Int32Value(temporalDateTime.time.hour))) { + return false; + } + + // Step 13. + if (!fields.emplaceBack(NameToId(cx->names().isoMicrosecond), + Int32Value(temporalDateTime.time.microsecond))) { + return false; + } + + // Step 14. + if (!fields.emplaceBack(NameToId(cx->names().isoMillisecond), + Int32Value(temporalDateTime.time.millisecond))) { + return false; + } + + // Step 15. + if (!fields.emplaceBack(NameToId(cx->names().isoMinute), + Int32Value(temporalDateTime.time.minute))) { + return false; + } + + // Step 16. + if (!fields.emplaceBack(NameToId(cx->names().isoMonth), + Int32Value(temporalDateTime.date.month))) { + return false; + } + + // Step 17. + if (!fields.emplaceBack(NameToId(cx->names().isoNanosecond), + Int32Value(temporalDateTime.time.nanosecond))) { + return false; + } + + // Step 18. + if (!fields.emplaceBack(NameToId(cx->names().isoSecond), + Int32Value(temporalDateTime.time.second))) { + return false; + } + + // Step 19. + if (!fields.emplaceBack(NameToId(cx->names().isoYear), + Int32Value(temporalDateTime.date.year))) { + return false; + } + + // Step 20. + if (!fields.emplaceBack(NameToId(cx->names().offset), StringValue(offset))) { + return false; + } + + // Step 21. + if (!fields.emplaceBack(NameToId(cx->names().timeZone), timeZone.toValue())) { + return false; + } + + // Step 22. + auto* obj = + NewPlainObjectWithUniqueNames(cx, fields.begin(), fields.length()); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.getISOFields ( ) + */ +static bool ZonedDateTime_getISOFields(JSContext* cx, unsigned argc, + Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_getISOFields>( + cx, args); +} + +/** + * Temporal.ZonedDateTime.prototype.getCalendar ( ) + */ +static bool ZonedDateTime_getCalendar(JSContext* cx, const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + Rooted<CalendarValue> calendar(cx, zonedDateTime->calendar()); + + // Step 3. + auto* obj = ToTemporalCalendarObject(cx, calendar); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.getCalendar ( ) + */ +static bool ZonedDateTime_getCalendar(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_getCalendar>(cx, + args); +} + +/** + * Temporal.ZonedDateTime.prototype.getTimeZone ( ) + */ +static bool ZonedDateTime_getTimeZone(JSContext* cx, const CallArgs& args) { + auto* zonedDateTime = &args.thisv().toObject().as<ZonedDateTimeObject>(); + Rooted<TimeZoneValue> timeZone(cx, zonedDateTime->timeZone()); + + // Step 3. + auto* obj = ToTemporalTimeZoneObject(cx, timeZone); + if (!obj) { + return false; + } + + args.rval().setObject(*obj); + return true; +} + +/** + * Temporal.ZonedDateTime.prototype.getTimeZone ( ) + */ +static bool ZonedDateTime_getTimeZone(JSContext* cx, unsigned argc, Value* vp) { + // Steps 1-2. + CallArgs args = CallArgsFromVp(argc, vp); + return CallNonGenericMethod<IsZonedDateTime, ZonedDateTime_getTimeZone>(cx, + args); +} + +const JSClass ZonedDateTimeObject::class_ = { + "Temporal.ZonedDateTime", + JSCLASS_HAS_RESERVED_SLOTS(ZonedDateTimeObject::SLOT_COUNT) | + JSCLASS_HAS_CACHED_PROTO(JSProto_ZonedDateTime), + JS_NULL_CLASS_OPS, + &ZonedDateTimeObject::classSpec_, +}; + +const JSClass& ZonedDateTimeObject::protoClass_ = PlainObject::class_; + +static const JSFunctionSpec ZonedDateTime_methods[] = { + JS_FN("from", ZonedDateTime_from, 1, 0), + JS_FN("compare", ZonedDateTime_compare, 2, 0), + JS_FS_END, +}; + +static const JSFunctionSpec ZonedDateTime_prototype_methods[] = { + JS_FN("with", ZonedDateTime_with, 1, 0), + JS_FN("withPlainTime", ZonedDateTime_withPlainTime, 0, 0), + JS_FN("withPlainDate", ZonedDateTime_withPlainDate, 1, 0), + JS_FN("withTimeZone", ZonedDateTime_withTimeZone, 1, 0), + JS_FN("withCalendar", ZonedDateTime_withCalendar, 1, 0), + JS_FN("add", ZonedDateTime_add, 1, 0), + JS_FN("subtract", ZonedDateTime_subtract, 1, 0), + JS_FN("until", ZonedDateTime_until, 1, 0), + JS_FN("since", ZonedDateTime_since, 1, 0), + JS_FN("round", ZonedDateTime_round, 1, 0), + JS_FN("equals", ZonedDateTime_equals, 1, 0), + JS_FN("toString", ZonedDateTime_toString, 0, 0), + JS_FN("toLocaleString", ZonedDateTime_toLocaleString, 0, 0), + JS_FN("toJSON", ZonedDateTime_toJSON, 0, 0), + JS_FN("valueOf", ZonedDateTime_valueOf, 0, 0), + JS_FN("startOfDay", ZonedDateTime_startOfDay, 0, 0), + JS_FN("toInstant", ZonedDateTime_toInstant, 0, 0), + JS_FN("toPlainDate", ZonedDateTime_toPlainDate, 0, 0), + JS_FN("toPlainTime", ZonedDateTime_toPlainTime, 0, 0), + JS_FN("toPlainDateTime", ZonedDateTime_toPlainDateTime, 0, 0), + JS_FN("toPlainYearMonth", ZonedDateTime_toPlainYearMonth, 0, 0), + JS_FN("toPlainMonthDay", ZonedDateTime_toPlainMonthDay, 0, 0), + JS_FN("getISOFields", ZonedDateTime_getISOFields, 0, 0), + JS_FN("getCalendar", ZonedDateTime_getCalendar, 0, 0), + JS_FN("getTimeZone", ZonedDateTime_getTimeZone, 0, 0), + JS_FS_END, +}; + +static const JSPropertySpec ZonedDateTime_prototype_properties[] = { + JS_PSG("calendarId", ZonedDateTime_calendarId, 0), + JS_PSG("timeZoneId", ZonedDateTime_timeZoneId, 0), + JS_PSG("year", ZonedDateTime_year, 0), + JS_PSG("month", ZonedDateTime_month, 0), + JS_PSG("monthCode", ZonedDateTime_monthCode, 0), + JS_PSG("day", ZonedDateTime_day, 0), + JS_PSG("hour", ZonedDateTime_hour, 0), + JS_PSG("minute", ZonedDateTime_minute, 0), + JS_PSG("second", ZonedDateTime_second, 0), + JS_PSG("millisecond", ZonedDateTime_millisecond, 0), + JS_PSG("microsecond", ZonedDateTime_microsecond, 0), + JS_PSG("nanosecond", ZonedDateTime_nanosecond, 0), + JS_PSG("epochSeconds", ZonedDateTime_epochSeconds, 0), + JS_PSG("epochMilliseconds", ZonedDateTime_epochMilliseconds, 0), + JS_PSG("epochMicroseconds", ZonedDateTime_epochMicroseconds, 0), + JS_PSG("epochNanoseconds", ZonedDateTime_epochNanoseconds, 0), + JS_PSG("dayOfWeek", ZonedDateTime_dayOfWeek, 0), + JS_PSG("dayOfYear", ZonedDateTime_dayOfYear, 0), + JS_PSG("weekOfYear", ZonedDateTime_weekOfYear, 0), + JS_PSG("yearOfWeek", ZonedDateTime_yearOfWeek, 0), + JS_PSG("hoursInDay", ZonedDateTime_hoursInDay, 0), + JS_PSG("daysInWeek", ZonedDateTime_daysInWeek, 0), + JS_PSG("daysInMonth", ZonedDateTime_daysInMonth, 0), + JS_PSG("daysInYear", ZonedDateTime_daysInYear, 0), + JS_PSG("monthsInYear", ZonedDateTime_monthsInYear, 0), + JS_PSG("inLeapYear", ZonedDateTime_inLeapYear, 0), + JS_PSG("offsetNanoseconds", ZonedDateTime_offsetNanoseconds, 0), + JS_PSG("offset", ZonedDateTime_offset, 0), + JS_STRING_SYM_PS(toStringTag, "Temporal.ZonedDateTime", JSPROP_READONLY), + JS_PS_END, +}; + +const ClassSpec ZonedDateTimeObject::classSpec_ = { + GenericCreateConstructor<ZonedDateTimeConstructor, 2, + gc::AllocKind::FUNCTION>, + GenericCreatePrototype<ZonedDateTimeObject>, + ZonedDateTime_methods, + nullptr, + ZonedDateTime_prototype_methods, + ZonedDateTime_prototype_properties, + nullptr, + ClassSpec::DontDefineConstructor, +}; diff --git a/js/src/builtin/temporal/ZonedDateTime.h b/js/src/builtin/temporal/ZonedDateTime.h new file mode 100644 index 0000000000..73e3a3384f --- /dev/null +++ b/js/src/builtin/temporal/ZonedDateTime.h @@ -0,0 +1,280 @@ +/* -*- 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 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef builtin_temporal_ZonedDateTime_h +#define builtin_temporal_ZonedDateTime_h + +#include "mozilla/Assertions.h" + +#include <stdint.h> + +#include "builtin/temporal/Calendar.h" +#include "builtin/temporal/Instant.h" +#include "builtin/temporal/TemporalTypes.h" +#include "builtin/temporal/TimeZone.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Value.h" +#include "vm/NativeObject.h" + +class JS_PUBLIC_API JSTracer; + +namespace js { +struct ClassSpec; +} + +namespace js::temporal { + +class ZonedDateTimeObject : public NativeObject { + public: + static const JSClass class_; + static const JSClass& protoClass_; + + static constexpr uint32_t SECONDS_SLOT = 0; + static constexpr uint32_t NANOSECONDS_SLOT = 1; + static constexpr uint32_t TIMEZONE_SLOT = 2; + static constexpr uint32_t CALENDAR_SLOT = 3; + static constexpr uint32_t SLOT_COUNT = 4; + + int64_t seconds() const { + double seconds = getFixedSlot(SECONDS_SLOT).toNumber(); + MOZ_ASSERT(-8'640'000'000'000 <= seconds && seconds <= 8'640'000'000'000); + return int64_t(seconds); + } + + int32_t nanoseconds() const { + int32_t nanoseconds = getFixedSlot(NANOSECONDS_SLOT).toInt32(); + MOZ_ASSERT(0 <= nanoseconds && nanoseconds <= 999'999'999); + return nanoseconds; + } + + TimeZoneValue timeZone() const { + return TimeZoneValue(getFixedSlot(TIMEZONE_SLOT)); + } + + CalendarValue calendar() const { + return CalendarValue(getFixedSlot(CALENDAR_SLOT)); + } + + private: + static const ClassSpec classSpec_; +}; +/** + * Extract the instant fields from the ZonedDateTime object. + */ +inline Instant ToInstant(const ZonedDateTimeObject* zonedDateTime) { + return {zonedDateTime->seconds(), zonedDateTime->nanoseconds()}; +} + +class ZonedDateTime { + Instant instant_; + TimeZoneValue timeZone_; + CalendarValue calendar_; + + public: + ZonedDateTime() = default; + + ZonedDateTime(const Instant& instant, const TimeZoneValue& timeZone, + const CalendarValue& calendar) + : instant_(instant), timeZone_(timeZone), calendar_(calendar) { + MOZ_ASSERT(IsValidEpochInstant(instant)); + MOZ_ASSERT(timeZone); + MOZ_ASSERT(calendar); + } + + explicit ZonedDateTime(const ZonedDateTimeObject* obj) + : ZonedDateTime(ToInstant(obj), obj->timeZone(), obj->calendar()) {} + + const auto& instant() const { return instant_; } + + const auto& timeZone() const { return timeZone_; } + + const auto& calendar() const { return calendar_; } + + explicit operator bool() const { return !!timeZone_ && !!calendar_; } + + void trace(JSTracer* trc) { + timeZone_.trace(trc); + calendar_.trace(trc); + } + + const auto* timeZoneDoNotUse() const { return &timeZone_; } + const auto* calendarDoNotUse() const { return &calendar_; } +}; + +enum class TemporalDisambiguation; +enum class TemporalOffset; +enum class TemporalOverflow; +enum class TemporalUnit; + +/** + * CreateTemporalZonedDateTime ( epochNanoseconds, timeZone, calendar [ , + * newTarget ] ) + */ +ZonedDateTimeObject* CreateTemporalZonedDateTime( + JSContext* cx, const Instant& instant, JS::Handle<TimeZoneValue> timeZone, + JS::Handle<CalendarValue> calendar); + +/** + * AddDaysToZonedDateTime ( instant, dateTime, timeZoneRec, calendar, days [ , + * overflow ] ) + */ +bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, + const PlainDateTime& dateTime, + JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<CalendarValue> calendar, double days, + TemporalOverflow overflow, Instant* result); + +/** + * AddDaysToZonedDateTime ( instant, dateTime, timeZoneRec, calendar, days [ , + * overflow ] ) + */ +bool AddDaysToZonedDateTime(JSContext* cx, const Instant& instant, + const PlainDateTime& dateTime, + JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<CalendarValue> calendar, double days, + Instant* result); + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds + * [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, + JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<CalendarRecord> calendar, + const Duration& duration, Instant* result); + +/** + * AddZonedDateTime ( epochNanoseconds, timeZoneRec, calendarRec, years, months, + * weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds + * [ , precalculatedPlainDateTime [ , options ] ] ) + */ +bool AddZonedDateTime(JSContext* cx, const Instant& epochNanoseconds, + JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<CalendarRecord> calendar, + const Duration& duration, const PlainDateTime& dateTime, + Instant* result); + +/** + * DifferenceZonedDateTime ( ns1, ns2, timeZoneRec, calendarRec, largestUnit, + * options, precalculatedPlainDateTime ) + */ +bool DifferenceZonedDateTime(JSContext* cx, const Instant& ns1, + const Instant& ns2, + JS::Handle<TimeZoneRecord> timeZone, + JS::Handle<CalendarRecord> calendar, + TemporalUnit largestUnit, + const PlainDateTime& precalculatedPlainDateTime, + Duration* result); + +struct NanosecondsAndDays final { + JS::BigInt* days = nullptr; + int64_t daysInt = 0; + InstantSpan nanoseconds; + InstantSpan dayLength; + + double daysNumber() const; + + void trace(JSTracer* trc); + + static NanosecondsAndDays from(int64_t days, const InstantSpan& nanoseconds, + const InstantSpan& dayLength) { + return {nullptr, days, nanoseconds, dayLength}; + } + + static NanosecondsAndDays from(JS::BigInt* days, + const InstantSpan& nanoseconds, + const InstantSpan& dayLength) { + return {days, 0, nanoseconds, dayLength}; + } +}; + +/** + * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * precalculatedPlainDateTime ] ) + */ +bool NanosecondsToDays(JSContext* cx, const InstantSpan& nanoseconds, + JS::Handle<ZonedDateTime> zonedRelativeTo, + JS::Handle<TimeZoneRecord> timeZone, + JS::MutableHandle<NanosecondsAndDays> result); + +/** + * NanosecondsToDays ( nanoseconds, zonedRelativeTo, timeZoneRec [ , + * precalculatedPlainDateTime ] ) + */ +bool NanosecondsToDays(JSContext* cx, const InstantSpan& nanoseconds, + JS::Handle<ZonedDateTime> zonedRelativeTo, + JS::Handle<TimeZoneRecord> timeZone, + const PlainDateTime& precalculatedPlainDateTime, + JS::MutableHandle<NanosecondsAndDays> result); + +enum class OffsetBehaviour { Option, Exact, Wall }; + +enum class MatchBehaviour { MatchExactly, MatchMinutes }; + +/** + * InterpretISODateTimeOffset ( year, month, day, hour, minute, second, + * millisecond, microsecond, nanosecond, offsetBehaviour, offsetNanoseconds, + * timeZoneRec, disambiguation, offsetOption, matchBehaviour ) + */ +bool InterpretISODateTimeOffset(JSContext* cx, const PlainDateTime& dateTime, + OffsetBehaviour offsetBehaviour, + int64_t offsetNanoseconds, + JS::Handle<TimeZoneRecord> timeZone, + TemporalDisambiguation disambiguation, + TemporalOffset offsetOption, + MatchBehaviour matchBehaviour, Instant* result); + +} /* namespace js::temporal */ + +namespace js { + +template <typename Wrapper> +class WrappedPtrOperations<temporal::ZonedDateTime, Wrapper> { + const auto& container() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + explicit operator bool() const { return bool(container()); } + + const auto& instant() const { return container().instant(); } + + JS::Handle<temporal::TimeZoneValue> timeZone() const { + return JS::Handle<temporal::TimeZoneValue>::fromMarkedLocation( + container().timeZoneDoNotUse()); + } + + JS::Handle<temporal::CalendarValue> calendar() const { + return JS::Handle<temporal::CalendarValue>::fromMarkedLocation( + container().calendarDoNotUse()); + } +}; + +template <typename Wrapper> +class WrappedPtrOperations<temporal::NanosecondsAndDays, Wrapper> { + const auto& object() const { + return static_cast<const Wrapper*>(this)->get(); + } + + public: + double daysNumber() const { return object().daysNumber(); } + + JS::Handle<JS::BigInt*> days() const { + return JS::Handle<JS::BigInt*>::fromMarkedLocation(&object().days); + } + + int64_t daysInt() const { return object().daysInt; } + + temporal::InstantSpan nanoseconds() const { return object().nanoseconds; } + + temporal::InstantSpan dayLength() const { return object().dayLength; } +}; + +} /* namespace js */ + +#endif /* builtin_temporal_ZonedDateTime_h */ diff --git a/js/src/builtin/temporal/moz.build b/js/src/builtin/temporal/moz.build new file mode 100644 index 0000000000..ae3bb618ad --- /dev/null +++ b/js/src/builtin/temporal/moz.build @@ -0,0 +1,34 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +FINAL_LIBRARY = "js" + +# Includes should be relative to parent path +LOCAL_INCLUDES += ["!../..", "../.."] + +include("../../js-config.mozbuild") +include("../../js-cxxflags.mozbuild") + +if CONFIG["JS_HAS_TEMPORAL_API"]: + UNIFIED_SOURCES += [ + "Calendar.cpp", + "Duration.cpp", + "Instant.cpp", + "Int96.cpp", + "PlainDate.cpp", + "PlainDateTime.cpp", + "PlainMonthDay.cpp", + "PlainTime.cpp", + "PlainYearMonth.cpp", + "Temporal.cpp", + "TemporalFields.cpp", + "TemporalNow.cpp", + "TemporalParser.cpp", + "TimeZone.cpp", + "ToString.cpp", + "Wrapped.cpp", + "ZonedDateTime.cpp", + ] |