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/TimeZone.cpp | |
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/TimeZone.cpp')
-rw-r--r-- | js/src/builtin/temporal/TimeZone.cpp | 2729 |
1 files changed, 2729 insertions, 0 deletions
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_, +}; |