/* -*- 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/. */ /* Intl.DateTimeFormat implementation. */ #include "builtin/intl/DateTimeFormat.h" #include "mozilla/Assertions.h" #include "mozilla/intl/Calendar.h" #include "mozilla/intl/DateIntervalFormat.h" #include "mozilla/intl/DateTimeFormat.h" #include "mozilla/intl/DateTimePart.h" #include "mozilla/intl/Locale.h" #include "mozilla/intl/TimeZone.h" #include "mozilla/Span.h" #include "builtin/Array.h" #include "builtin/Date.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/FormatBuffer.h" #include "builtin/intl/LanguageTag.h" #include "builtin/intl/LocaleNegotiation.h" #include "builtin/intl/ParameterNegotiation.h" #include "builtin/intl/SharedIntlData.h" #include "builtin/intl/UsingEnum.h" #include "builtin/temporal/Calendar.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/TemporalParser.h" #include "builtin/temporal/TimeZone.h" #include "builtin/temporal/ZonedDateTime.h" #include "gc/GCContext.h" #include "js/Date.h" #include "js/experimental/Intl.h" // JS::AddMozDateTimeFormatConstructor #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/GCAPI.h" #include "js/PropertyAndElement.h" // JS_DefineFunctions, JS_DefineProperties #include "js/PropertySpec.h" #include "js/StableStringChars.h" #include "js/Wrapper.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" #include "vm/JSContext.h" #include "vm/PlainObject.h" // js::PlainObject #include "vm/Runtime.h" #include "vm/Warnings.h" #include "vm/GeckoProfiler-inl.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" using namespace js; using namespace js::intl; using namespace js::temporal; using JS::ClippedTime; using JS::TimeClip; const JSClassOps DateTimeFormatObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve DateTimeFormatObject::finalize, // finalize nullptr, // call nullptr, // construct nullptr, // trace }; const JSClass DateTimeFormatObject::class_ = { "Intl.DateTimeFormat", JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) | JSCLASS_HAS_CACHED_PROTO(JSProto_DateTimeFormat) | JSCLASS_BACKGROUND_FINALIZE, &DateTimeFormatObject::classOps_, &DateTimeFormatObject::classSpec_, }; const JSClass& DateTimeFormatObject::protoClass_ = PlainObject::class_; static bool dateTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc, Value* vp); static bool dateTimeFormat_format(JSContext* cx, unsigned argc, Value* vp); static bool dateTimeFormat_formatToParts(JSContext* cx, unsigned argc, Value* vp); static bool dateTimeFormat_formatRange(JSContext* cx, unsigned argc, Value* vp); static bool dateTimeFormat_formatRangeToParts(JSContext* cx, unsigned argc, Value* vp); static bool dateTimeFormat_resolvedOptions(JSContext* cx, unsigned argc, Value* vp); static bool dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); args.rval().setString(cx->names().DateTimeFormat); return true; } static const JSFunctionSpec dateTimeFormat_static_methods[] = { JS_FN("supportedLocalesOf", dateTimeFormat_supportedLocalesOf, 1, 0), JS_FS_END, }; static const JSFunctionSpec dateTimeFormat_methods[] = { JS_FN("resolvedOptions", dateTimeFormat_resolvedOptions, 0, 0), JS_FN("formatToParts", dateTimeFormat_formatToParts, 1, 0), JS_FN("formatRange", dateTimeFormat_formatRange, 2, 0), JS_FN("formatRangeToParts", dateTimeFormat_formatRangeToParts, 2, 0), JS_FN("toSource", dateTimeFormat_toSource, 0, 0), JS_FS_END, }; static const JSPropertySpec dateTimeFormat_properties[] = { JS_PSG("format", dateTimeFormat_format, 0), JS_STRING_SYM_PS(toStringTag, "Intl.DateTimeFormat", JSPROP_READONLY), JS_PS_END, }; static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); const ClassSpec DateTimeFormatObject::classSpec_ = { GenericCreateConstructor, GenericCreatePrototype, dateTimeFormat_static_methods, nullptr, dateTimeFormat_methods, dateTimeFormat_properties, nullptr, ClassSpec::DontDefineConstructor, }; static constexpr std::string_view HourCycleToString( DateTimeFormatOptions::HourCycle hourCycle) { #ifndef USING_ENUM using enum DateTimeFormatOptions::HourCycle; #else USING_ENUM(DateTimeFormatOptions::HourCycle, H11, H12, H23, H24); #endif switch (hourCycle) { case H11: return "h11"; case H12: return "h12"; case H23: return "h23"; case H24: return "h24"; } MOZ_CRASH("invalid date time format hour cycle"); } template static constexpr std::string_view DateOrTimeStyleToString( DateOrTimeStyle dateOrTimeStyle) { switch (dateOrTimeStyle) { case DateOrTimeStyle::Full: return "full"; case DateOrTimeStyle::Long: return "long"; case DateOrTimeStyle::Medium: return "medium"; case DateOrTimeStyle::Short: return "short"; } MOZ_CRASH("invalid date time format date or time style"); } static constexpr std::string_view DateStyleToString( DateTimeFormatOptions::DateStyle dateStyle) { return DateOrTimeStyleToString(dateStyle); } static constexpr std::string_view TimeStyleToString( DateTimeFormatOptions::TimeStyle timeStyle) { return DateOrTimeStyleToString(timeStyle); } template static constexpr std::string_view TextComponentToString( TextComponent textComponent) { switch (textComponent) { case TextComponent::Narrow: return "narrow"; case TextComponent::Short: return "short"; case TextComponent::Long: return "long"; } MOZ_CRASH("invalid date time format text component"); } template static constexpr std::string_view NumericComponentToString( NumericComponent numericComponent) { switch (numericComponent) { case NumericComponent::TwoDigit: return "2-digit"; case NumericComponent::Numeric: return "numeric"; } MOZ_CRASH("invalid date time format numeric component"); } static constexpr std::string_view WeekdayToString( DateTimeFormatOptions::Weekday weekday) { return TextComponentToString(weekday); } static constexpr std::string_view EraToString(DateTimeFormatOptions::Era era) { return TextComponentToString(era); } static constexpr std::string_view YearToString( DateTimeFormatOptions::Year year) { return NumericComponentToString(year); } static constexpr std::string_view MonthToString( DateTimeFormatOptions::Month month) { #ifndef USING_ENUM using enum DateTimeFormatOptions::Month; #else USING_ENUM(DateTimeFormatOptions::Month, TwoDigit, Numeric, Narrow, Short, Long); #endif switch (month) { case TwoDigit: return "2-digit"; case Numeric: return "numeric"; case Narrow: return "narrow"; case Short: return "short"; case Long: return "long"; } MOZ_CRASH("invalid date time format month"); } static constexpr std::string_view DayToString(DateTimeFormatOptions::Day day) { return NumericComponentToString(day); } static constexpr std::string_view DayPeriodToString( DateTimeFormatOptions::DayPeriod dayPeriod) { return TextComponentToString(dayPeriod); } static constexpr std::string_view HourToString( DateTimeFormatOptions::Hour hour) { return NumericComponentToString(hour); } static constexpr std::string_view MinuteToString( DateTimeFormatOptions::Minute minute) { return NumericComponentToString(minute); } static constexpr std::string_view SecondToString( DateTimeFormatOptions::Second second) { return NumericComponentToString(second); } static constexpr std::string_view TimeZoneNameToString( DateTimeFormatOptions::TimeZoneName timeZoneName) { #ifndef USING_ENUM using enum DateTimeFormatOptions::TimeZoneName; #else USING_ENUM(DateTimeFormatOptions::TimeZoneName, Short, Long, ShortOffset, LongOffset, ShortGeneric, LongGeneric); #endif switch (timeZoneName) { case Short: return "short"; case Long: return "long"; case ShortOffset: return "shortOffset"; case LongOffset: return "longOffset"; case ShortGeneric: return "shortGeneric"; case LongGeneric: return "longGeneric"; } MOZ_CRASH("invalid date time format time zone name"); } enum class FormatMatcher { Basic, BestFit }; static constexpr std::string_view FormatMatcherToString( FormatMatcher formatMatcher) { #ifndef USING_ENUM using enum FormatMatcher; #else USING_ENUM(FormatMatcher, Basic, BestFit); #endif switch (formatMatcher) { case Basic: return "basic"; case BestFit: return "best fit"; } MOZ_CRASH("invalid format matcher"); } enum class DateTimeFormatConstructorKind { Standard, EnableMozExtensions, }; /** * CreateDateTimeFormat ( newTarget, locales, options, required, defaults [ , * toLocaleStringTimeZone ] ) */ static bool CreateDateTimeFormat( JSContext* cx, Handle dateTimeFormat, Handle locales, Handle optionsValue, DateTimeFormatOptions::Required required, DateTimeFormatOptions::Defaults defaults, Handle toLocaleStringTimeZone = nullptr, DateTimeFormatConstructorKind dtfKind = DateTimeFormatConstructorKind::Standard) { // Step 1. (Performed in caller) // Steps 2-4. (Inlined ResolveOptions) // ResolveOptions, step 1. Rooted requestedLocales(cx, cx); if (!CanonicalizeLocaleList(cx, locales, &requestedLocales)) { return false; } Rooted requestedLocalesArray( cx, LocalesListToArray(cx, requestedLocales)); if (!requestedLocalesArray) { return false; } dateTimeFormat->setRequestedLocales(requestedLocalesArray); auto dtfOptions = cx->make_unique(); if (!dtfOptions) { return false; } dtfOptions->required = required; dtfOptions->defaults = defaults; if (!optionsValue.isUndefined()) { // ResolveOptions, steps 2-3. Rooted options(cx, JS::ToObject(cx, optionsValue)); if (!options) { return false; } // ResolveOptions, step 4. LocaleMatcher matcher; if (!GetLocaleMatcherOption(cx, options, &matcher)) { return false; } // ResolveOptions, step 5. // // This implementation only supports the "lookup" locale matcher, therefore // the "localeMatcher" option doesn't need to be stored. // ResolveOptions, step 6. Rooted calendar(cx); if (!GetUnicodeExtensionOption(cx, options, UnicodeExtensionKey::Calendar, &calendar)) { return false; } if (calendar) { dateTimeFormat->setCalendar(calendar); } Rooted numberingSystem(cx); if (!GetUnicodeExtensionOption(cx, options, UnicodeExtensionKey::NumberingSystem, &numberingSystem)) { return false; } if (numberingSystem) { dateTimeFormat->setNumberingSystem(numberingSystem); } if (!GetBooleanOption(cx, options, cx->names().hour12, &dtfOptions->hour12)) { return false; } static constexpr auto hourCycles = MapOptions(DateTimeFormatOptions::HourCycle::H11, DateTimeFormatOptions::HourCycle::H12, DateTimeFormatOptions::HourCycle::H23, DateTimeFormatOptions::HourCycle::H24); if (!GetStringOption(cx, options, cx->names().hourCycle, hourCycles, &dtfOptions->hourCycle)) { return false; } // ResolveOptions, step 7. if (dtfOptions->hour12.isSome()) { // The "hourCycle" option is ignored if "hour12" is also present. dtfOptions->hourCycle = mozilla::Nothing(); } // ResolveOptions, step 8. (Performed in ResolveLocale) // ResolveOptions, step 9. (Return) // Step 5. (Not applicable when ResolveOptions is inlined.) // Step 6-14. (Performed in ResolveLocale) // Step 15. Rooted timeZoneOption(cx); if (!GetProperty(cx, options, options, cx->names().timeZone, &timeZoneOption)) { return false; } // Steps 16-19. JSLinearString* timeZone = nullptr; if (timeZoneOption.isUndefined()) { // Step 16. if (toLocaleStringTimeZone) { timeZone = toLocaleStringTimeZone; } else { timeZone = cx->global()->globalIntlData().defaultTimeZone(cx); if (!timeZone) { return false; } } // Steps 18-19. (Not applicable in our implementation.) } else { // Step 17. if (toLocaleStringTimeZone) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_DATETIME_OPTION, "timeZone", "Temporal.ZonedDateTime.toLocaleString"); return false; } Rooted timeZoneStr(cx, JS::ToString(cx, timeZoneOption)); if (!timeZoneStr) { return false; } // Steps 18-19. timeZone = temporal::ToValidCanonicalTimeZoneIdentifier(cx, timeZoneStr); if (!timeZone) { return false; } } // Step 20. dateTimeFormat->setTimeZone(timeZone); if (dtfKind == DateTimeFormatConstructorKind::EnableMozExtensions) { Rooted patternOption(cx); if (!GetProperty(cx, options, options, cx->names().pattern, &patternOption)) { return false; } if (!patternOption.isUndefined()) { auto* pattern = JS::ToString(cx, patternOption); if (!pattern) { return false; } dateTimeFormat->setPattern(pattern); } } // Steps 21-22. (Not applicable in our implementation.) // Step 23. (Moved below) // Step 24. static constexpr auto weekdays = MapOptions(DateTimeFormatOptions::Weekday::Narrow, DateTimeFormatOptions::Weekday::Short, DateTimeFormatOptions::Weekday::Long); if (!GetStringOption(cx, options, cx->names().weekday, weekdays, &dtfOptions->weekday)) { return false; } static constexpr auto eras = MapOptions( DateTimeFormatOptions::Era::Narrow, DateTimeFormatOptions::Era::Short, DateTimeFormatOptions::Era::Long); if (!GetStringOption(cx, options, cx->names().era, eras, &dtfOptions->era)) { return false; } static constexpr auto years = MapOptions(DateTimeFormatOptions::Year::TwoDigit, DateTimeFormatOptions::Year::Numeric); if (!GetStringOption(cx, options, cx->names().year, years, &dtfOptions->year)) { return false; } static constexpr auto months = MapOptions(DateTimeFormatOptions::Month::TwoDigit, DateTimeFormatOptions::Month::Numeric, DateTimeFormatOptions::Month::Narrow, DateTimeFormatOptions::Month::Short, DateTimeFormatOptions::Month::Long); if (!GetStringOption(cx, options, cx->names().month, months, &dtfOptions->month)) { return false; } static constexpr auto days = MapOptions(DateTimeFormatOptions::Day::TwoDigit, DateTimeFormatOptions::Day::Numeric); if (!GetStringOption(cx, options, cx->names().day, days, &dtfOptions->day)) { return false; } static constexpr auto dayPeriods = MapOptions(DateTimeFormatOptions::DayPeriod::Narrow, DateTimeFormatOptions::DayPeriod::Short, DateTimeFormatOptions::DayPeriod::Long); if (!GetStringOption(cx, options, cx->names().dayPeriod, dayPeriods, &dtfOptions->dayPeriod)) { return false; } static constexpr auto hours = MapOptions(DateTimeFormatOptions::Hour::TwoDigit, DateTimeFormatOptions::Hour::Numeric); if (!GetStringOption(cx, options, cx->names().hour, hours, &dtfOptions->hour)) { return false; } static constexpr auto minutes = MapOptions(DateTimeFormatOptions::Minute::TwoDigit, DateTimeFormatOptions::Minute::Numeric); if (!GetStringOption(cx, options, cx->names().minute, minutes, &dtfOptions->minute)) { return false; } static constexpr auto seconds = MapOptions(DateTimeFormatOptions::Second::TwoDigit, DateTimeFormatOptions::Second::Numeric); if (!GetStringOption(cx, options, cx->names().second, seconds, &dtfOptions->second)) { return false; } mozilla::Maybe fractionalSecondDigits{}; if (!GetNumberOption(cx, options, cx->names().fractionalSecondDigits, 1, 3, &fractionalSecondDigits)) { return false; } dtfOptions->fractionalSecondDigits = fractionalSecondDigits; static constexpr auto timeZoneNames = MapOptions( DateTimeFormatOptions::TimeZoneName::Short, DateTimeFormatOptions::TimeZoneName::Long, DateTimeFormatOptions::TimeZoneName::ShortOffset, DateTimeFormatOptions::TimeZoneName::LongOffset, DateTimeFormatOptions::TimeZoneName::ShortGeneric, DateTimeFormatOptions::TimeZoneName::LongGeneric); if (!GetStringOption(cx, options, cx->names().timeZoneName, timeZoneNames, &dtfOptions->timeZoneName)) { return false; } // Step 25. // // This implementation only supports the "best fit" format matcher, // therefore the "formatMatcher" option doesn't need to be stored. // // See bug 852837. static constexpr auto formatMatchers = MapOptions( FormatMatcher::Basic, FormatMatcher::BestFit); FormatMatcher formatMatcher; if (!GetStringOption(cx, options, cx->names().formatMatcher, formatMatchers, FormatMatcher::BestFit, &formatMatcher)) { return false; } // Steps 26-27. static constexpr auto dateStyles = MapOptions(DateTimeFormatOptions::DateStyle::Full, DateTimeFormatOptions::DateStyle::Long, DateTimeFormatOptions::DateStyle::Medium, DateTimeFormatOptions::DateStyle::Short); if (!GetStringOption(cx, options, cx->names().dateStyle, dateStyles, &dtfOptions->dateStyle)) { return false; } // Steps 28-29. static constexpr auto timeStyles = MapOptions(DateTimeFormatOptions::TimeStyle::Full, DateTimeFormatOptions::TimeStyle::Long, DateTimeFormatOptions::TimeStyle::Medium, DateTimeFormatOptions::TimeStyle::Short); if (!GetStringOption(cx, options, cx->names().timeStyle, timeStyles, &dtfOptions->timeStyle)) { return false; } // Step 30. if (dtfOptions->dateStyle.isSome() || dtfOptions->timeStyle.isSome()) { // Step 23. const char* explicitFormatComponent = ([&]() -> const char* { if (dtfOptions->weekday.isSome()) { return "weekday"; } if (dtfOptions->era.isSome()) { return "era"; } if (dtfOptions->year.isSome()) { return "year"; } if (dtfOptions->month.isSome()) { return "month"; } if (dtfOptions->day.isSome()) { return "day"; } if (dtfOptions->dayPeriod.isSome()) { return "dayPeriod"; } if (dtfOptions->hour.isSome()) { return "hour"; } if (dtfOptions->minute.isSome()) { return "minute"; } if (dtfOptions->second.isSome()) { return "second"; } if (dtfOptions->fractionalSecondDigits.isSome()) { return "fractionalSecondDigits"; } if (dtfOptions->timeZoneName.isSome()) { return "timeZoneName"; } return nullptr; })(); // Step 30.a. if (explicitFormatComponent) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_INVALID_DATETIME_OPTION, explicitFormatComponent, dtfOptions->dateStyle.isSome() ? "dateStyle" : "timeStyle"); return false; } // Step 30.b. if (required == DateTimeFormatOptions::Required::Date && dtfOptions->timeStyle.isSome()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_DATETIME_STYLE, "timeStyle", "date"); return false; } // Step 30.c. if (required == DateTimeFormatOptions::Required::Time && dtfOptions->dateStyle.isSome()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_DATETIME_STYLE, "dateStyle", "time"); return false; } } // Steps 31-33. (See ResolveDateTimeFormat) } else { // Absent |options| object only requires to store the current time zone. // Step 16. JSLinearString* timeZone; if (toLocaleStringTimeZone) { timeZone = toLocaleStringTimeZone; } else { timeZone = cx->global()->globalIntlData().defaultTimeZone(cx); if (!timeZone) { return false; } } // Step 20. dateTimeFormat->setTimeZone(timeZone); } dateTimeFormat->setOptions(dtfOptions.release()); AddCellMemory(dateTimeFormat, sizeof(DateTimeFormatOptions), MemoryUse::IntlOptions); // Step 34. return true; } /** * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) * * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b */ static bool DateTimeFormat(JSContext* cx, const CallArgs& args, DateTimeFormatOptions::Required required, DateTimeFormatOptions::Defaults defaults, DateTimeFormatConstructorKind dtfKind) { AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.DateTimeFormat"); // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). JSProtoKey protoKey = dtfKind == DateTimeFormatConstructorKind::Standard ? JSProto_DateTimeFormat : JSProto_Null; Rooted proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, protoKey, &proto)) { return false; } Rooted dateTimeFormat(cx); dateTimeFormat = NewObjectWithClassProto(cx, proto); if (!dateTimeFormat) { return false; } // Step 2. if (!CreateDateTimeFormat(cx, dateTimeFormat, args.get(0), args.get(1), required, defaults, nullptr, dtfKind)) { return false; } // Step 3. if (dtfKind == DateTimeFormatConstructorKind::Standard) { return ChainLegacyIntlFormat(cx, JSProto_DateTimeFormat, args, dateTimeFormat); } // Step 4. args.rval().setObject(*dateTimeFormat); return true; } static bool DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); auto required = DateTimeFormatOptions::Required::Any; auto defaults = DateTimeFormatOptions::Defaults::Date; return DateTimeFormat(cx, args, required, defaults, DateTimeFormatConstructorKind::Standard); } static bool MozDateTimeFormat(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Don't allow to call mozIntl.DateTimeFormat as a function. That way we // don't need to worry how to handle the legacy initialization semantics // when applied on mozIntl.DateTimeFormat. if (!ThrowIfNotConstructing(cx, args, "mozIntl.DateTimeFormat")) { return false; } auto required = DateTimeFormatOptions::Required::Any; auto defaults = DateTimeFormatOptions::Defaults::Date; return DateTimeFormat(cx, args, required, defaults, DateTimeFormatConstructorKind::EnableMozExtensions); } static auto ToRequired(DateTimeFormatKind kind) { #ifndef USING_ENUM using enum DateTimeFormatOptions::Required; #else USING_ENUM(DateTimeFormatOptions::Required, Any, Date, Time); #endif switch (kind) { case DateTimeFormatKind::All: return Any; case DateTimeFormatKind::Date: return Date; case DateTimeFormatKind::Time: return Time; } MOZ_CRASH("invalid date time format kind"); } static auto ToDefaults(DateTimeFormatKind kind) { #ifndef USING_ENUM using enum DateTimeFormatOptions::Defaults; #else USING_ENUM(DateTimeFormatOptions::Defaults, All, Date, Time); #endif switch (kind) { case DateTimeFormatKind::All: return All; case DateTimeFormatKind::Date: return Date; case DateTimeFormatKind::Time: return Time; } MOZ_CRASH("invalid date time format kind"); } static DateTimeFormatObject* CreateDateTimeFormat( JSContext* cx, Handle locales, Handle options, Handle toLocaleStringTimeZone, DateTimeFormatKind kind) { Rooted dateTimeFormat( cx, NewBuiltinClassInstance(cx)); if (!dateTimeFormat) { return nullptr; } auto required = ToRequired(kind); auto defaults = ToDefaults(kind); if (!CreateDateTimeFormat(cx, dateTimeFormat, locales, options, required, defaults, toLocaleStringTimeZone, DateTimeFormatConstructorKind::Standard)) { return nullptr; } return dateTimeFormat; } DateTimeFormatObject* js::intl::CreateDateTimeFormat(JSContext* cx, Handle locales, Handle options, DateTimeFormatKind kind) { return CreateDateTimeFormat(cx, locales, options, nullptr, kind); } DateTimeFormatObject* js::intl::GetOrCreateDateTimeFormat( JSContext* cx, Handle locales, Handle options, DateTimeFormatKind kind) { // Try to use a cached instance when |locales| is either undefined or a // string, and |options| is undefined. if ((locales.isUndefined() || locales.isString()) && options.isUndefined()) { Rooted locale(cx); if (locales.isString()) { locale = locales.toString()->ensureLinear(cx); if (!locale) { return nullptr; } } return cx->global()->globalIntlData().getOrCreateDateTimeFormat(cx, kind, locale); } // Create a new Intl.DateTimeFormat instance. return CreateDateTimeFormat(cx, locales, options, nullptr, kind); } void js::intl::DateTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { auto* dateTimeFormat = &obj->as(); auto* df = dateTimeFormat->getDateFormat(); auto* dif = dateTimeFormat->getDateIntervalFormat(); if (auto* options = dateTimeFormat->getOptions()) { gcx->delete_(obj, options, MemoryUse::IntlOptions); } if (df) { RemoveICUCellMemory(gcx, obj, DateTimeFormatObject::UDateFormatEstimatedMemoryUse); delete df; } if (dif) { RemoveICUCellMemory( gcx, obj, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse); delete dif; } } bool JS::AddMozDateTimeFormatConstructor(JSContext* cx, JS::Handle intl) { Rooted ctor( cx, GlobalObject::createConstructor(cx, MozDateTimeFormat, cx->names().DateTimeFormat, 0)); if (!ctor) { return false; } Rooted proto( cx, GlobalObject::createBlankPrototype(cx, cx->global())); if (!proto) { return false; } if (!LinkConstructorAndPrototype(cx, ctor, proto)) { return false; } // 12.3.2 if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) { return false; } // 12.4.4 and 12.4.5 if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) { return false; } // 12.4.2 and 12.4.3 if (!JS_DefineProperties(cx, proto, dateTimeFormat_properties)) { return false; } Rooted ctorValue(cx, ObjectValue(*ctor)); return DefineDataProperty(cx, intl, cx->names().DateTimeFormat, ctorValue, 0); } /** * Resolve the actual locale to finish initialization of the DateTimeFormat. */ static bool ResolveLocale(JSContext* cx, Handle dateTimeFormat) { // Return if the locale was already resolved. if (dateTimeFormat->isLocaleResolved()) { return true; } auto* dtfOptions = dateTimeFormat->getOptions(); Rooted requestedLocales( cx, &dateTimeFormat->getRequestedLocales()->as()); // %Intl.DateTimeFormat%.[[RelevantExtensionKeys]] is « "ca", "hc", "nu" ». mozilla::EnumSet relevantExtensionKeys{ UnicodeExtensionKey::Calendar, UnicodeExtensionKey::HourCycle, UnicodeExtensionKey::NumberingSystem, }; // Initialize locale options from constructor arguments. Rooted localeOptions(cx); if (auto* ca = dateTimeFormat->getCalendar()) { localeOptions.setUnicodeExtension(UnicodeExtensionKey::Calendar, ca); } if (dtfOptions->hour12.isSome()) { // Explicitly opt-out of hourCycle if the hour12 option is present, because // the latter takes precedence over hourCycle. localeOptions.setUnicodeExtension(UnicodeExtensionKey::HourCycle, nullptr); } else { if (auto hourCycle = dtfOptions->hourCycle) { #ifndef USING_ENUM using enum DateTimeFormatOptions::HourCycle; #else USING_ENUM(DateTimeFormatOptions::HourCycle, H11, H12, H23, H24); #endif JSLinearString* hc; switch (*hourCycle) { case H11: hc = cx->names().h11; break; case H12: hc = cx->names().h12; break; case H23: hc = cx->names().h23; break; case H24: hc = cx->names().h24; break; } localeOptions.setUnicodeExtension(UnicodeExtensionKey::HourCycle, hc); } } if (auto nu = dateTimeFormat->getNumberingSystem()) { localeOptions.setUnicodeExtension(UnicodeExtensionKey::NumberingSystem, nu); } // Use the default locale data. auto localeData = LocaleData::Default; // Resolve the actual locale. Rooted resolved(cx); if (!ResolveLocale(cx, AvailableLocaleKind::DateTimeFormat, requestedLocales, localeOptions, relevantExtensionKeys, localeData, &resolved)) { return false; } // Finish initialization by setting the actual locale and extensions. // Changes from "Intl era and monthCode" proposal. // // https://tc39.es/proposal-intl-era-monthcode/#sec-createdatetimeformat auto ca = resolved.extension(UnicodeExtensionKey::Calendar); MOZ_ASSERT(ca, "resolved calendar is non-null"); if (StringEqualsLiteral(ca, "islamic")) { if (!WarnNumberASCII(cx, JSMSG_ISLAMIC_FALLBACK)) { return false; } // Fallback to "islamic-tbla" calendar. auto* str = NewStringCopyZ(cx, "islamic-tbla"); if (!str) { return false; } dateTimeFormat->setCalendar(str); } else if (StringEqualsLiteral(ca, "islamic-rgsa")) { // Fallback to "islamic-tbla" calendar for 147 uplift compatibility. // The above warning text isn't suitable, and per 2025-12 TG2 meeting // treatment as unknown is expected going forward (bug 2005702). auto* str = NewStringCopyZ(cx, "islamic-tbla"); if (!str) { return false; } dateTimeFormat->setCalendar(str); } else { dateTimeFormat->setCalendar(ca); } auto hc = resolved.extension(UnicodeExtensionKey::HourCycle); if (hc) { MOZ_ASSERT(dtfOptions->hour12.isNothing()); #ifndef USING_ENUM using enum DateTimeFormatOptions::HourCycle; #else USING_ENUM(DateTimeFormatOptions::HourCycle, H11, H12, H23, H24); #endif if (StringEqualsLiteral(hc, "h11")) { dtfOptions->hourCycle = mozilla::Some(H11); } else if (StringEqualsLiteral(hc, "h12")) { dtfOptions->hourCycle = mozilla::Some(H12); } else if (StringEqualsLiteral(hc, "h23")) { dtfOptions->hourCycle = mozilla::Some(H23); } else { MOZ_ASSERT(StringEqualsLiteral(hc, "h24")); dtfOptions->hourCycle = mozilla::Some(H24); } } auto nu = resolved.extension(UnicodeExtensionKey::NumberingSystem); MOZ_ASSERT(nu, "resolved numbering system is non-null"); dateTimeFormat->setNumberingSystem(nu); auto* locale = resolved.toLocale(cx); if (!locale) { return false; } dateTimeFormat->setLocale(locale); MOZ_ASSERT(dateTimeFormat->isLocaleResolved(), "locale successfully resolved"); return true; } enum class HourCycle { // 12 hour cycle, from 0 to 11. H11, // 12 hour cycle, from 1 to 12. H12, // 24 hour cycle, from 0 to 23. H23, // 24 hour cycle, from 1 to 24. H24 }; static UniqueChars DateTimeFormatLocale( JSContext* cx, Handle dateTimeFormat, mozilla::Maybe hourCycle = mozilla::Nothing()) { MOZ_ASSERT(dateTimeFormat->isLocaleResolved()); // ICU expects calendar, numberingSystem, and hourCycle as Unicode locale // extensions on locale. JS::RootedVector keywords(cx); if (!keywords.emplaceBack("ca", dateTimeFormat->getCalendar())) { return nullptr; } if (!keywords.emplaceBack("nu", dateTimeFormat->getNumberingSystem())) { return nullptr; } if (hourCycle) { #ifndef USING_ENUM using enum mozilla::intl::DateTimeFormat::HourCycle; #else USING_ENUM(mozilla::intl::DateTimeFormat::HourCycle, H11, H12, H23, H24); #endif JSAtom* hourCycleStr; switch (*hourCycle) { case H11: hourCycleStr = cx->names().h11; break; case H12: hourCycleStr = cx->names().h12; break; case H23: hourCycleStr = cx->names().h23; break; case H24: hourCycleStr = cx->names().h24; break; } if (!keywords.emplaceBack("hc", hourCycleStr)) { return nullptr; } } Rooted localeStr(cx, dateTimeFormat->getLocale()); return FormatLocale(cx, localeStr, keywords); } template static auto ToTextComponent(TextComponent value) { #ifndef USING_ENUM using enum mozilla::intl::DateTimeFormat::Text; #else USING_ENUM(mozilla::intl::DateTimeFormat::Text, Narrow, Short, Long); #endif switch (value) { case TextComponent::Narrow: return Narrow; case TextComponent::Short: return Short; case TextComponent::Long: return Long; } MOZ_CRASH("invalid text component"); } template static auto ToNumericComponent(NumericComponent value) { #ifndef USING_ENUM using enum mozilla::intl::DateTimeFormat::Numeric; #else USING_ENUM(mozilla::intl::DateTimeFormat::Numeric, TwoDigit, Numeric); #endif switch (value) { case NumericComponent::TwoDigit: return TwoDigit; case NumericComponent::Numeric: return Numeric; } MOZ_CRASH("invalid numeric component"); } static auto ToMonthComponent(DateTimeFormatOptions::Month value) { #ifndef USING_ENUM using enum mozilla::intl::DateTimeFormat::Month; #else USING_ENUM(mozilla::intl::DateTimeFormat::Month, TwoDigit, Numeric, Narrow, Short, Long); #endif switch (value) { case DateTimeFormatOptions::Month::TwoDigit: return TwoDigit; case DateTimeFormatOptions::Month::Numeric: return Numeric; case DateTimeFormatOptions::Month::Narrow: return Narrow; case DateTimeFormatOptions::Month::Short: return Short; case DateTimeFormatOptions::Month::Long: return Long; } MOZ_CRASH("invalid month component"); } static auto ToTimeZoneNameComponent(DateTimeFormatOptions::TimeZoneName value) { #ifndef USING_ENUM using enum mozilla::intl::DateTimeFormat::TimeZoneName; #else USING_ENUM(mozilla::intl::DateTimeFormat::TimeZoneName, Short, Long, ShortOffset, LongOffset, ShortGeneric, LongGeneric); #endif switch (value) { case DateTimeFormatOptions::TimeZoneName::Short: return Short; case DateTimeFormatOptions::TimeZoneName::Long: return Long; case DateTimeFormatOptions::TimeZoneName::ShortOffset: return ShortOffset; case DateTimeFormatOptions::TimeZoneName::LongOffset: return LongOffset; case DateTimeFormatOptions::TimeZoneName::ShortGeneric: return ShortGeneric; case DateTimeFormatOptions::TimeZoneName::LongGeneric: return LongGeneric; } MOZ_CRASH("invalid time zone name component"); } static auto ToHourCycleComponent(DateTimeFormatOptions::HourCycle value) { #ifndef USING_ENUM using enum mozilla::intl::DateTimeFormat::HourCycle; #else USING_ENUM(mozilla::intl::DateTimeFormat::HourCycle, H11, H12, H23, H24); #endif switch (value) { case DateTimeFormatOptions::HourCycle::H11: return H11; case DateTimeFormatOptions::HourCycle::H12: return H12; case DateTimeFormatOptions::HourCycle::H23: return H23; case DateTimeFormatOptions::HourCycle::H24: return H24; } MOZ_CRASH("invalid hour cycle component"); } template static auto ToDateTimeStyle(DateTimeStyle value) { #ifndef USING_ENUM using enum mozilla::intl::DateTimeFormat::Style; #else USING_ENUM(mozilla::intl::DateTimeFormat::Style, Full, Long, Medium, Short); #endif switch (value) { case DateTimeStyle::Full: return Full; case DateTimeStyle::Long: return Long; case DateTimeStyle::Medium: return Medium; case DateTimeStyle::Short: return Short; } MOZ_CRASH("invalid date/time style"); } enum class Required { Date, Time, YearMonth, MonthDay, Any }; enum class Defaults { Date, Time, YearMonth, MonthDay, ZonedDateTime, All }; enum class Inherit { All, Relevant }; struct DateTimeFormatArgs { Required required; Defaults defaults; Inherit inherit; }; /** * Get the "required" argument passed to CreateDateTimeFormat. */ static auto GetRequired(DateTimeFormatOptions::Required required) { #ifndef USING_ENUM using enum Required; #else USING_ENUM(Required, Date, Time, Any); #endif switch (required) { case DateTimeFormatOptions::Required::Date: return Date; case DateTimeFormatOptions::Required::Time: return Time; case DateTimeFormatOptions::Required::Any: return Any; } MOZ_CRASH("invalid date time format required"); } /** * Get the "defaults" argument passed to CreateDateTimeFormat. */ static auto GetDefaults(DateTimeFormatOptions::Defaults defaults) { #ifndef USING_ENUM using enum Defaults; #else USING_ENUM(Defaults, Date, Time, All); #endif switch (defaults) { case DateTimeFormatOptions::Defaults::Date: return Date; case DateTimeFormatOptions::Defaults::Time: return Time; case DateTimeFormatOptions::Defaults::All: return All; } MOZ_CRASH("invalid date time format defaults"); } /** * Compute the (required, defaults, inherit) arguments passed to * GetDateTimeFormat. */ static DateTimeFormatArgs GetDateTimeFormatArgs( const DateTimeFormatOptions& options, DateTimeValueKind kind) { switch (kind) { case DateTimeValueKind::Number: return {GetRequired(options.required), GetDefaults(options.defaults), Inherit::All}; case DateTimeValueKind::TemporalDate: return {Required::Date, Defaults::Date, Inherit::Relevant}; case DateTimeValueKind::TemporalTime: return {Required::Time, Defaults::Time, Inherit::Relevant}; case DateTimeValueKind::TemporalDateTime: return {Required::Any, Defaults::All, Inherit::Relevant}; case DateTimeValueKind::TemporalYearMonth: return {Required::YearMonth, Defaults::YearMonth, Inherit::Relevant}; case DateTimeValueKind::TemporalMonthDay: return {Required::MonthDay, Defaults::MonthDay, Inherit::Relevant}; case DateTimeValueKind::TemporalZonedDateTime: return {Required::Any, Defaults::ZonedDateTime, Inherit::All}; case DateTimeValueKind::TemporalInstant: return {Required::Any, Defaults::All, Inherit::All}; } MOZ_CRASH("invalid date-time value kind"); } enum class DateTimeField { Weekday, Era, Year, Month, Day, DayPeriod, Hour, Minute, Second, FractionalSecondDigits, }; /** * GetDateTimeFormat ( formats, matcher, options, required, defaults, inherit ) * * https://tc39.es/proposal-temporal/#sec-getdatetimeformat */ static mozilla::Maybe GetDateTimeFormat(const mozilla::intl::DateTimeFormat::ComponentsBag& options, Required required, Defaults defaults, Inherit inherit) { // Steps 1-5. mozilla::EnumSet requiredOptions; switch (required) { case Required::Date: requiredOptions = { DateTimeField::Weekday, DateTimeField::Year, DateTimeField::Month, DateTimeField::Day, }; break; case Required::Time: requiredOptions = { DateTimeField::DayPeriod, DateTimeField::Hour, DateTimeField::Minute, DateTimeField::Second, DateTimeField::FractionalSecondDigits, }; break; case Required::YearMonth: requiredOptions = { DateTimeField::Year, DateTimeField::Month, }; break; case Required::MonthDay: requiredOptions = { DateTimeField::Month, DateTimeField::Day, }; break; case Required::Any: requiredOptions = { DateTimeField::Weekday, DateTimeField::Year, DateTimeField::Month, DateTimeField::Day, DateTimeField::DayPeriod, DateTimeField::Hour, DateTimeField::Minute, DateTimeField::Second, DateTimeField::FractionalSecondDigits, }; break; } MOZ_ASSERT(!requiredOptions.contains(DateTimeField::Era), "standalone era not supported"); // Steps 6-10. mozilla::EnumSet defaultOptions; switch (defaults) { case Defaults::Date: defaultOptions = { DateTimeField::Year, DateTimeField::Month, DateTimeField::Day, }; break; case Defaults::Time: defaultOptions = { DateTimeField::Hour, DateTimeField::Minute, DateTimeField::Second, }; break; case Defaults::YearMonth: defaultOptions = { DateTimeField::Year, DateTimeField::Month, }; break; case Defaults::MonthDay: defaultOptions = { DateTimeField::Month, DateTimeField::Day, }; break; case Defaults::ZonedDateTime: case Defaults::All: defaultOptions = { DateTimeField::Year, DateTimeField::Month, DateTimeField::Day, DateTimeField::Hour, DateTimeField::Minute, DateTimeField::Second, }; break; } MOZ_ASSERT(!defaultOptions.contains(DateTimeField::Weekday)); MOZ_ASSERT(!defaultOptions.contains(DateTimeField::Era)); MOZ_ASSERT(!defaultOptions.contains(DateTimeField::DayPeriod)); MOZ_ASSERT(!defaultOptions.contains(DateTimeField::FractionalSecondDigits)); // Steps 11-12. mozilla::intl::DateTimeFormat::ComponentsBag formatOptions; if (inherit == Inherit::All) { // Step 11.a. formatOptions = options; } else { // Step 12.a. (Implicit) // Step 12.b. switch (required) { case Required::Date: case Required::YearMonth: case Required::Any: formatOptions.era = options.era; break; case Required::Time: case Required::MonthDay: // |era| option not applicable for these types. break; } // Step 12.c. switch (required) { case Required::Time: case Required::Any: formatOptions.hourCycle = options.hourCycle; formatOptions.hour12 = options.hour12; break; case Required::Date: case Required::YearMonth: case Required::MonthDay: // |hourCycle| and |hour12| options not applicable for these types. break; } } // Steps 13-14. bool anyPresent = options.weekday || options.year || options.month || options.day || options.dayPeriod || options.hour || options.minute || options.second || options.fractionalSecondDigits; // Step 15. bool needDefaults = true; // Step 16. (Loop unrolled) if (requiredOptions.contains(DateTimeField::Weekday) && options.weekday) { formatOptions.weekday = options.weekday; needDefaults = false; } if (requiredOptions.contains(DateTimeField::Year) && options.year) { formatOptions.year = options.year; needDefaults = false; } if (requiredOptions.contains(DateTimeField::Month) && options.month) { formatOptions.month = options.month; needDefaults = false; } if (requiredOptions.contains(DateTimeField::Day) && options.day) { formatOptions.day = options.day; needDefaults = false; } if (requiredOptions.contains(DateTimeField::DayPeriod) && options.dayPeriod) { formatOptions.dayPeriod = options.dayPeriod; needDefaults = false; } if (requiredOptions.contains(DateTimeField::Hour) && options.hour) { formatOptions.hour = options.hour; needDefaults = false; } if (requiredOptions.contains(DateTimeField::Minute) && options.minute) { formatOptions.minute = options.minute; needDefaults = false; } if (requiredOptions.contains(DateTimeField::Second) && options.second) { formatOptions.second = options.second; needDefaults = false; } if (requiredOptions.contains(DateTimeField::FractionalSecondDigits) && options.fractionalSecondDigits) { formatOptions.fractionalSecondDigits = options.fractionalSecondDigits; needDefaults = false; } // Step 17. if (needDefaults) { // Step 17.a. if (anyPresent && inherit == Inherit::Relevant) { return mozilla::Nothing(); } // Step 17.b. (Loop unrolled) auto numericOption = mozilla::Some(mozilla::intl::DateTimeFormat::Numeric::Numeric); if (defaultOptions.contains(DateTimeField::Year)) { formatOptions.year = numericOption; } if (defaultOptions.contains(DateTimeField::Month)) { formatOptions.month = mozilla::Some(mozilla::intl::DateTimeFormat::Month::Numeric); } if (defaultOptions.contains(DateTimeField::Day)) { formatOptions.day = numericOption; } if (defaultOptions.contains(DateTimeField::Hour)) { formatOptions.hour = numericOption; } if (defaultOptions.contains(DateTimeField::Minute)) { formatOptions.minute = numericOption; } if (defaultOptions.contains(DateTimeField::Second)) { formatOptions.second = numericOption; } // Step 17.c. if (defaults == Defaults::ZonedDateTime && !formatOptions.timeZoneName) { formatOptions.timeZoneName = mozilla::Some(mozilla::intl::DateTimeFormat::TimeZoneName::Short); } } // Steps 18-20. (Performed in caller). return mozilla::Some(formatOptions); } /** * AdjustDateTimeStyleFormat ( formats, baseFormat, matcher, allowedOptions ) * * https://tc39.es/proposal-temporal/#sec-adjustdatetimestyleformat */ static mozilla::Result AdjustDateTimeStyleFormat(mozilla::intl::DateTimeFormat* baseFormat, mozilla::EnumSet allowedOptions) { // Step 1. mozilla::intl::DateTimeFormat::ComponentsBag formatOptions; // Step 2. (Loop unrolled) auto result = baseFormat->ResolveComponents(); if (result.isErr()) { return result.propagateErr(); } auto options = result.unwrap(); if (allowedOptions.contains(DateTimeField::Era) && options.era) { formatOptions.era = options.era; } if (allowedOptions.contains(DateTimeField::Weekday) && options.weekday) { formatOptions.weekday = options.weekday; } if (allowedOptions.contains(DateTimeField::Year) && options.year) { formatOptions.year = options.year; } if (allowedOptions.contains(DateTimeField::Month) && options.month) { formatOptions.month = options.month; } if (allowedOptions.contains(DateTimeField::Day) && options.day) { formatOptions.day = options.day; } if (allowedOptions.contains(DateTimeField::DayPeriod) && options.dayPeriod) { formatOptions.dayPeriod = options.dayPeriod; } if (allowedOptions.contains(DateTimeField::Hour) && options.hour) { formatOptions.hour = options.hour; formatOptions.hourCycle = options.hourCycle; } if (allowedOptions.contains(DateTimeField::Minute) && options.minute) { formatOptions.minute = options.minute; } if (allowedOptions.contains(DateTimeField::Second) && options.second) { formatOptions.second = options.second; } if (allowedOptions.contains(DateTimeField::FractionalSecondDigits) && options.fractionalSecondDigits) { formatOptions.fractionalSecondDigits = options.fractionalSecondDigits; } // Steps 3-5. (Performed in caller) return formatOptions; } static const char* DateTimeValueKindToString(DateTimeValueKind kind) { switch (kind) { case DateTimeValueKind::Number: return "number"; case DateTimeValueKind::TemporalDate: return "Temporal.PlainDate"; case DateTimeValueKind::TemporalTime: return "Temporal.PlainTime"; case DateTimeValueKind::TemporalDateTime: return "Temporal.PlainDateTime"; case DateTimeValueKind::TemporalYearMonth: return "Temporal.PlainYearMonth"; case DateTimeValueKind::TemporalMonthDay: return "Temporal.PlainMonthDay"; case DateTimeValueKind::TemporalZonedDateTime: return "Temporal.ZonedDateTime"; case DateTimeValueKind::TemporalInstant: return "Temporal.Instant"; } MOZ_CRASH("invalid date-time value kind"); } class TimeZoneOffsetString { static constexpr std::u16string_view GMT = u"GMT"; // Time zone offset string format is "±hh:mm". static constexpr size_t offsetLength = 6; // ICU custom time zones are in the format "GMT±hh:mm". char16_t timeZone_[GMT.size() + offsetLength] = {}; TimeZoneOffsetString() = default; public: TimeZoneOffsetString(const TimeZoneOffsetString& other) { *this = other; } TimeZoneOffsetString& operator=(const TimeZoneOffsetString& other) { std::copy_n(other.timeZone_, std::size(timeZone_), timeZone_); return *this; } operator mozilla::Span() const { return mozilla::Span(timeZone_); } static TimeZoneOffsetString GMTZero() { static constexpr std::u16string_view gmtZero = u"GMT+00:00"; static_assert(gmtZero.length() == std::extent_v); TimeZoneOffsetString result{}; gmtZero.copy(result.timeZone_, gmtZero.length()); return result; } /** * |timeZone| is either a canonical IANA time zone identifier or a normalized * time zone offset string. */ static mozilla::Maybe from( const JSLinearString* timeZone) { MOZ_RELEASE_ASSERT(!timeZone->empty(), "time zone is a non-empty string"); // If the time zone string starts with either "+" or "-", it is a normalized // time zone offset string, because (canonical) IANA time zone identifiers // can't start with "+" or "-". char16_t timeZoneSign = timeZone->latin1OrTwoByteChar(0); MOZ_ASSERT(timeZoneSign != 0x2212, "Minus sign is normalized to Ascii minus"); if (timeZoneSign != '+' && timeZoneSign != '-') { return mozilla::Nothing(); } // Release assert because we don't want CopyChars to write out-of-bounds. MOZ_RELEASE_ASSERT(timeZone->length() == offsetLength); // ToValidCanonicalTimeZoneIdentifier normalizes offset strings to the // format "±hh:mm". MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(1))); MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(2))); MOZ_ASSERT(timeZone->latin1OrTwoByteChar(3) == ':'); MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(4))); MOZ_ASSERT(mozilla::IsAsciiDigit(timeZone->latin1OrTwoByteChar(5))); // ToValidCanonicalTimeZoneIdentifier verifies the offset is at most ±23:59. #ifdef DEBUG auto twoDigit = [&](size_t offset) { auto c1 = timeZone->latin1OrTwoByteChar(offset); auto c2 = timeZone->latin1OrTwoByteChar(offset + 1); return mozilla::AsciiAlphanumericToNumber(c1) * 10 + mozilla::AsciiAlphanumericToNumber(c2); }; int32_t hours = twoDigit(1); MOZ_ASSERT(0 <= hours && hours <= 23); int32_t minutes = twoDigit(4); MOZ_ASSERT(0 <= minutes && minutes <= 59); #endif TimeZoneOffsetString result{}; // Copy the string "GMT" followed by the offset string. size_t copied = GMT.copy(result.timeZone_, GMT.size()); CopyChars(result.timeZone_ + copied, *timeZone); return mozilla::Some(result); } }; class TimeZoneChars final { JS::AutoStableStringChars timeZone_; mozilla::Maybe timeZoneOffset_{}; mozilla::Maybe> maybeSpan() const { if (timeZone_.isTwoByte()) { return mozilla::Some(timeZone_.twoByteRange()); } if (timeZoneOffset_) { return timeZoneOffset_; } return mozilla::Nothing(); } public: explicit TimeZoneChars(JSContext* cx) : timeZone_(cx) {} operator mozilla::Span() const { return maybeSpan().valueOr(mozilla::Span{}); } operator mozilla::Maybe>() const { return maybeSpan(); } bool init(JSContext* cx, JSLinearString* timeZone) { auto timeZoneOffset = TimeZoneOffsetString::from(timeZone); if (timeZoneOffset) { timeZoneOffset_ = timeZoneOffset; return true; } return timeZone_.initTwoByte(cx, timeZone); } void setToOffset(const TimeZoneOffsetString& offset) { timeZoneOffset_ = mozilla::Some(offset); } }; static bool IsPlainDateTimeValue(DateTimeValueKind kind) { switch (kind) { case DateTimeValueKind::Number: case DateTimeValueKind::TemporalZonedDateTime: case DateTimeValueKind::TemporalInstant: return false; case DateTimeValueKind::TemporalDate: case DateTimeValueKind::TemporalTime: case DateTimeValueKind::TemporalDateTime: case DateTimeValueKind::TemporalYearMonth: case DateTimeValueKind::TemporalMonthDay: return true; } MOZ_CRASH("invalid date time value kind"); } /** * Returns a new mozilla::intl::DateTimeFormat with the locale and date-time * formatting options of the given DateTimeFormat. */ static mozilla::intl::DateTimeFormat* NewDateTimeFormat( JSContext* cx, Handle dateTimeFormat, DateTimeValueKind kind) { if (!ResolveLocale(cx, dateTimeFormat)) { return nullptr; } auto dtfOptions = *dateTimeFormat->getOptions(); auto locale = DateTimeFormatLocale(cx, dateTimeFormat); if (!locale) { return nullptr; } TimeZoneChars timeZone(cx); if (IsPlainDateTimeValue(kind)) { timeZone.setToOffset(TimeZoneOffsetString::GMTZero()); } else { if (!timeZone.init(cx, dateTimeFormat->getTimeZone())) { return nullptr; } } // This is a DateTimeFormat defined by a pattern option. This is internal // to Mozilla, and not part of the ECMA-402 API. if (auto* patternString = dateTimeFormat->getPattern()) { JS::AutoStableStringChars pattern(cx); if (!pattern.initTwoByte(cx, patternString)) { return nullptr; } auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromPattern( mozilla::MakeStringSpan(locale.get()), pattern.twoByteRange(), timeZone); if (dfResult.isErr()) { ReportInternalError(cx, dfResult.unwrapErr()); return nullptr; } return dfResult.unwrap().release(); } // This is a DateTimeFormat defined by a time style or date style. if (dtfOptions.dateStyle.isSome() || dtfOptions.timeStyle.isSome()) { switch (kind) { case DateTimeValueKind::TemporalDate: case DateTimeValueKind::TemporalYearMonth: case DateTimeValueKind::TemporalMonthDay: { if (dtfOptions.dateStyle.isNothing()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_FORMAT_OPTIONS, DateTimeValueKindToString(kind)); return nullptr; } break; } case DateTimeValueKind::TemporalTime: { if (dtfOptions.timeStyle.isNothing()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_FORMAT_OPTIONS, DateTimeValueKindToString(kind)); return nullptr; } break; } case DateTimeValueKind::Number: case DateTimeValueKind::TemporalDateTime: case DateTimeValueKind::TemporalZonedDateTime: case DateTimeValueKind::TemporalInstant: break; } static auto ToDateStyle = ToDateTimeStyle; static auto ToTimeStyle = ToDateTimeStyle; mozilla::intl::DateTimeFormat::StyleBag style = { .date = dtfOptions.dateStyle.map(ToDateStyle), .time = dtfOptions.timeStyle.map(ToTimeStyle), .hourCycle = dtfOptions.hourCycle.map(ToHourCycleComponent), .hour12 = dtfOptions.hour12, }; auto& sharedIntlData = cx->runtime()->sharedIntlData.ref(); auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); if (!dtpg) { return nullptr; } auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromStyle( mozilla::MakeStringSpan(locale.get()), style, dtpg, timeZone); if (dfResult.isErr()) { ReportInternalError(cx, dfResult.unwrapErr()); return nullptr; } auto df = dfResult.unwrap(); mozilla::EnumSet allowedOptions; switch (kind) { case DateTimeValueKind::TemporalDate: allowedOptions = { DateTimeField::Weekday, DateTimeField::Era, DateTimeField::Year, DateTimeField::Month, DateTimeField::Day, }; break; case DateTimeValueKind::TemporalTime: allowedOptions = { DateTimeField::DayPeriod, DateTimeField::Hour, DateTimeField::Minute, DateTimeField::Second, DateTimeField::FractionalSecondDigits, }; break; case DateTimeValueKind::TemporalDateTime: allowedOptions = { DateTimeField::Weekday, DateTimeField::Era, DateTimeField::Year, DateTimeField::Month, DateTimeField::Day, DateTimeField::DayPeriod, DateTimeField::Hour, DateTimeField::Minute, DateTimeField::Second, DateTimeField::FractionalSecondDigits, }; break; case DateTimeValueKind::TemporalYearMonth: allowedOptions = { DateTimeField::Era, DateTimeField::Year, DateTimeField::Month, }; break; case DateTimeValueKind::TemporalMonthDay: allowedOptions = { DateTimeField::Month, DateTimeField::Day, }; break; case DateTimeValueKind::Number: case DateTimeValueKind::TemporalZonedDateTime: case DateTimeValueKind::TemporalInstant: break; } if (allowedOptions.isEmpty()) { return df.release(); } auto adjusted = AdjustDateTimeStyleFormat(df.get(), allowedOptions); if (adjusted.isErr()) { ReportInternalError(cx, adjusted.unwrapErr()); return nullptr; } auto bag = adjusted.unwrap(); auto dfAdjustedResult = mozilla::intl::DateTimeFormat::TryCreateFromComponents( mozilla::MakeStringSpan(locale.get()), bag, dtpg, timeZone); if (dfAdjustedResult.isErr()) { ReportInternalError(cx, dfAdjustedResult.unwrapErr()); return nullptr; } return dfAdjustedResult.unwrap().release(); } static auto ToEra = ToTextComponent; static auto ToYear = ToNumericComponent; static auto ToDay = ToNumericComponent; static auto ToWeekday = ToTextComponent; static auto ToHour = ToNumericComponent; static auto ToMinute = ToNumericComponent; static auto ToSecond = ToNumericComponent; static auto ToDayPeriod = ToTextComponent; // This is a DateTimeFormat defined by a components bag. mozilla::intl::DateTimeFormat::ComponentsBag bag = { .era = dtfOptions.era.map(ToEra), .year = dtfOptions.year.map(ToYear), .month = dtfOptions.month.map(ToMonthComponent), .day = dtfOptions.day.map(ToDay), .weekday = dtfOptions.weekday.map(ToWeekday), .hour = dtfOptions.hour.map(ToHour), .minute = dtfOptions.minute.map(ToMinute), .second = dtfOptions.second.map(ToSecond), .timeZoneName = dtfOptions.timeZoneName.map(ToTimeZoneNameComponent), .hour12 = dtfOptions.hour12, .hourCycle = dtfOptions.hourCycle.map(ToHourCycleComponent), .dayPeriod = dtfOptions.dayPeriod.map(ToDayPeriod), .fractionalSecondDigits = dtfOptions.fractionalSecondDigits, }; auto [required, defaults, inherit] = GetDateTimeFormatArgs(dtfOptions, kind); auto resolvedBag = GetDateTimeFormat(bag, required, defaults, inherit); if (!resolvedBag) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_FORMAT_OPTIONS, DateTimeValueKindToString(kind)); return nullptr; } bag = *resolvedBag; auto& sharedIntlData = cx->runtime()->sharedIntlData.ref(); auto* dtpg = sharedIntlData.getDateTimePatternGenerator(cx, locale.get()); if (!dtpg) { return nullptr; } auto dfResult = mozilla::intl::DateTimeFormat::TryCreateFromComponents( mozilla::MakeStringSpan(locale.get()), bag, dtpg, timeZone); if (dfResult.isErr()) { ReportInternalError(cx, dfResult.unwrapErr()); return nullptr; } return dfResult.unwrap().release(); } void js::intl::DateTimeFormatObject::maybeClearCache(DateTimeValueKind kind) { if (getDateTimeValueKind() == kind) { return; } setDateTimeValueKind(kind); if (auto* df = getDateFormat()) { RemoveICUCellMemory(this, DateTimeFormatObject::UDateFormatEstimatedMemoryUse); delete df; setDateFormat(nullptr); } if (auto* dif = getDateIntervalFormat()) { RemoveICUCellMemory( this, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse); delete dif; setDateIntervalFormat(nullptr); } } static mozilla::intl::DateTimeFormat* GetOrCreateDateTimeFormat( JSContext* cx, Handle dateTimeFormat, DateTimeValueKind kind) { // Clear previously created formatters if their type doesn't match. dateTimeFormat->maybeClearCache(kind); // Obtain a cached mozilla::intl::DateTimeFormat object. if (auto* df = dateTimeFormat->getDateFormat()) { return df; } auto* df = NewDateTimeFormat(cx, dateTimeFormat, kind); if (!df) { return nullptr; } dateTimeFormat->setDateFormat(df); AddICUCellMemory(dateTimeFormat, DateTimeFormatObject::UDateFormatEstimatedMemoryUse); return df; } template static bool SetOptionsProperty(JSContext* cx, MutableHandle options, Handle name, mozilla::Maybe intlProp) { if (!intlProp) { return true; } auto* str = NewStringCopyZ( cx, mozilla::intl::DateTimeFormat::ToString(*intlProp)); if (!str) { return false; } return options.emplaceBack(NameToId(name), StringValue(str)); } enum class IncludeDateTimeFields : bool { No, Yes }; /** * Extracts the resolved components from a DateTimeFormat and applies them to * the object for resolved components. */ static bool ResolveDateTimeFormatComponents( JSContext* cx, Handle dateTimeFormat, MutableHandle options, IncludeDateTimeFields includeDateTimeFields) { auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, DateTimeValueKind::Number); if (!df) { return false; } auto result = df->ResolveComponents(); if (result.isErr()) { ReportInternalError(cx, result.unwrapErr()); return false; } auto components = result.unwrap(); // Map the resolved mozilla::intl::DateTimeFormat::ComponentsBag to the // options object as returned by DateTimeFormat.prototype.resolvedOptions. // // Resolved options must match the ordering as defined in: // https://tc39.es/ecma402/#sec-intl.datetimeformat.prototype.resolvedoptions if (!SetOptionsProperty(cx, options, cx->names().hourCycle, components.hourCycle)) { return false; } if (components.hour12) { if (!options.emplaceBack(NameToId(cx->names().hour12), BooleanValue(*components.hour12))) { return false; } } if (includeDateTimeFields == IncludeDateTimeFields::No) { // Do not include date time fields. return true; } if (!SetOptionsProperty(cx, options, cx->names().weekday, components.weekday)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().era, components.era)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().year, components.year)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().month, components.month)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().day, components.day)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().dayPeriod, components.dayPeriod)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().hour, components.hour)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().minute, components.minute)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().second, components.second)) { return false; } if (!SetOptionsProperty(cx, options, cx->names().timeZoneName, components.timeZoneName)) { return false; } if (components.fractionalSecondDigits) { if (!options.emplaceBack(NameToId(cx->names().fractionalSecondDigits), Int32Value(*components.fractionalSecondDigits))) { return false; } } return true; } /** * ToDateTimeFormattable ( value ) * * https://tc39.es/proposal-temporal/#sec-todatetimeformattable */ static auto ToDateTimeFormattable(const Value& value) { MOZ_ASSERT(!value.isUndefined()); // Step 1. (Inlined IsTemporalObject) if (value.isObject()) { auto* obj = CheckedUnwrapStatic(&value.toObject()); if (obj) { if (obj->is()) { return DateTimeValueKind::TemporalDate; } if (obj->is()) { return DateTimeValueKind::TemporalDateTime; } if (obj->is()) { return DateTimeValueKind::TemporalTime; } if (obj->is()) { return DateTimeValueKind::TemporalYearMonth; } if (obj->is()) { return DateTimeValueKind::TemporalMonthDay; } if (obj->is()) { return DateTimeValueKind::TemporalZonedDateTime; } if (obj->is()) { return DateTimeValueKind::TemporalInstant; } return DateTimeValueKind::Number; } } // Step 2. (ToNumber performed in caller) return DateTimeValueKind::Number; } /** * Ensure the calendar value is resolved. */ static bool ResolveCalendarValue(JSContext* cx, Handle dateTimeFormat) { if (dateTimeFormat->getCalendarValue()) { return true; } // Ensure the calendar option is resolved. if (!ResolveLocale(cx, dateTimeFormat)) { return false; } Rooted calendarString(cx, dateTimeFormat->getCalendar()); Rooted calendar(cx); if (!CanonicalizeCalendar(cx, calendarString, &calendar)) { return false; } dateTimeFormat->setCalendarValue(calendar); return true; } /** * HandleDateTimeTemporalDate ( dateTimeFormat, temporalDate ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaldate */ static bool HandleDateTimeTemporalDate( JSContext* cx, Handle dateTimeFormat, Handle unwrappedTemporalDate, ClippedTime* result) { auto isoDate = unwrappedTemporalDate->date(); auto calendarId = unwrappedTemporalDate->calendar().identifier(); if (!ResolveCalendarValue(cx, dateTimeFormat)) { return false; } Rooted calendar(cx, dateTimeFormat->getCalendarValue()); // Step 1. if (calendarId != CalendarId::ISO8601 && calendarId != calendar.identifier()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE, CalendarIdentifier(calendarId).data(), CalendarIdentifier(calendar).data()); return false; } // Step 2. auto isoDateTime = ISODateTime{isoDate, {12, 0, 0}}; // Step 3. auto epochNs = GetUTCEpochNanoseconds(isoDateTime); // Steps 4-5. (Performed in NewDateTimeFormat) // Step 6. int64_t milliseconds = epochNs.floorToMilliseconds(); *result = JS::TimeClip(double(milliseconds)); return true; } /** * HandleDateTimeTemporalYearMonth ( dateTimeFormat, temporalYearMonth ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalyearmonth */ static bool HandleDateTimeTemporalYearMonth( JSContext* cx, Handle dateTimeFormat, Handle unwrappedTemporalYearMonth, ClippedTime* result) { auto isoDate = unwrappedTemporalYearMonth->date(); auto calendarId = unwrappedTemporalYearMonth->calendar().identifier(); if (!ResolveCalendarValue(cx, dateTimeFormat)) { return false; } Rooted calendar(cx, dateTimeFormat->getCalendarValue()); // Step 1. if (calendarId != calendar.identifier()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE, CalendarIdentifier(calendarId).data(), CalendarIdentifier(calendar).data()); return false; } // Step 2. auto isoDateTime = ISODateTime{isoDate, {12, 0, 0}}; // Step 3. auto epochNs = GetUTCEpochNanoseconds(isoDateTime); // Steps 4-5. (Performed in NewDateTimeFormat) // Step 6. int64_t milliseconds = epochNs.floorToMilliseconds(); *result = JS::TimeClip(double(milliseconds)); return true; } /** * HandleDateTimeTemporalMonthDay ( dateTimeFormat, temporalMonthDay ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalmonthday */ static bool HandleDateTimeTemporalMonthDay( JSContext* cx, Handle dateTimeFormat, Handle unwrappedTemporalMonthDay, ClippedTime* result) { auto isoDate = unwrappedTemporalMonthDay->date(); auto calendarId = unwrappedTemporalMonthDay->calendar().identifier(); if (!ResolveCalendarValue(cx, dateTimeFormat)) { return false; } Rooted calendar(cx, dateTimeFormat->getCalendarValue()); // Step 1. if (calendarId != calendar.identifier()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE, CalendarIdentifier(calendarId).data(), CalendarIdentifier(calendar).data()); return false; } // Step 2. auto isoDateTime = ISODateTime{isoDate, {12, 0, 0}}; // Step 3. auto epochNs = GetUTCEpochNanoseconds(isoDateTime); // Steps 4-5. (Performed in NewDateTimeFormat) // Step 6. int64_t milliseconds = epochNs.floorToMilliseconds(); *result = JS::TimeClip(double(milliseconds)); return true; } /** * HandleDateTimeTemporalTime ( dateTimeFormat, temporalTime ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaltime */ static bool HandleDateTimeTemporalTime(PlainTimeObject* unwrappedTemporalTime, ClippedTime* result) { auto time = unwrappedTemporalTime->time(); // Steps 1-2. auto isoDateTime = ISODateTime{{1970, 1, 1}, time}; // Step 3. auto epochNs = GetUTCEpochNanoseconds(isoDateTime); // Steps 4-5. (Performed in NewDateTimeFormat) // Step 6. int64_t milliseconds = epochNs.floorToMilliseconds(); *result = JS::TimeClip(double(milliseconds)); return true; } /** * HandleDateTimeTemporalDateTime ( dateTimeFormat, dateTime ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaldatetime */ static bool HandleDateTimeTemporalDateTime( JSContext* cx, Handle dateTimeFormat, Handle unwrappedDateTime, ClippedTime* result) { auto isoDateTime = unwrappedDateTime->dateTime(); auto calendarId = unwrappedDateTime->calendar().identifier(); if (!ResolveCalendarValue(cx, dateTimeFormat)) { return false; } Rooted calendar(cx, dateTimeFormat->getCalendarValue()); // Step 1. if (calendarId != CalendarId::ISO8601 && calendarId != calendar.identifier()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE, CalendarIdentifier(calendarId).data(), CalendarIdentifier(calendar).data()); return false; } // Step 2. auto epochNs = GetUTCEpochNanoseconds(isoDateTime); // Step 3. (Performed in NewDateTimeFormat) // Step 4. int64_t milliseconds = epochNs.floorToMilliseconds(); *result = JS::TimeClip(double(milliseconds)); return true; } /** * HandleDateTimeTemporalInstant ( dateTimeFormat, instant ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalinstant */ static bool HandleDateTimeTemporalInstant(InstantObject* unwrappedInstant, ClippedTime* result) { // Step 1. (Performed in NewDateTimeFormat) // Step 2. auto epochNs = unwrappedInstant->epochNanoseconds(); int64_t milliseconds = epochNs.floorToMilliseconds(); *result = JS::TimeClip(double(milliseconds)); return true; } /** * Temporal.ZonedDateTime.prototype.toLocaleString ( [ locales [ , options ] ] ) */ static bool HandleDateTimeTemporalZonedDateTime( JSContext* cx, Handle dateTimeFormat, Handle unwrappedZonedDateTime, ClippedTime* result) { auto epochNs = unwrappedZonedDateTime->epochNanoseconds(); auto calendarId = unwrappedZonedDateTime->calendar().identifier(); if (!ResolveCalendarValue(cx, dateTimeFormat)) { return false; } Rooted calendar(cx, dateTimeFormat->getCalendarValue()); // Step 4. if (calendarId != CalendarId::ISO8601 && calendarId != calendar.identifier()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_TEMPORAL_CALENDAR_INCOMPATIBLE, CalendarIdentifier(calendarId).data(), CalendarIdentifier(calendar).data()); return false; } // Step 5. int64_t milliseconds = epochNs.floorToMilliseconds(); *result = JS::TimeClip(double(milliseconds)); return true; } /** * HandleDateTimeOthers ( dateTimeFormat, x ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimeothers */ static bool HandleDateTimeOthers(JSContext* cx, const char* method, double x, ClippedTime* result) { // Step 1. auto clipped = JS::TimeClip(x); // Step 2. if (!clipped.isValid()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "DateTimeFormat", method); return false; } // Step 4. (Performed in NewDateTimeFormat) // Steps 3 and 5. *result = clipped; return true; } /** * HandleDateTimeValue ( dateTimeFormat, x ) * * https://tc39.es/proposal-temporal/#sec-temporal-handledatetimevalue */ static bool HandleDateTimeValue(JSContext* cx, const char* method, Handle dateTimeFormat, JSObject* x, ClippedTime* result) { // Step 1. Rooted unwrapped(cx, CheckedUnwrapStatic(x)); if (!unwrapped) { ReportAccessDenied(cx); return false; } // Step 1.a. if (unwrapped->is()) { return HandleDateTimeTemporalDate(cx, dateTimeFormat, unwrapped.as(), result); } // Step 1.b. if (unwrapped->is()) { return HandleDateTimeTemporalYearMonth( cx, dateTimeFormat, unwrapped.as(), result); } // Step 1.c. if (unwrapped->is()) { return HandleDateTimeTemporalMonthDay( cx, dateTimeFormat, unwrapped.as(), result); } // Step 1.d. if (unwrapped->is()) { return HandleDateTimeTemporalTime(&unwrapped->as(), result); } // Step 1.e. if (unwrapped->is()) { return HandleDateTimeTemporalDateTime( cx, dateTimeFormat, unwrapped.as(), result); } // Step 1.f. if (unwrapped->is()) { return HandleDateTimeTemporalInstant(&unwrapped->as(), result); } // Step 1.g. MOZ_ASSERT(unwrapped->is()); // Step 1.h. JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNEXPECTED_TYPE, "object", unwrapped->getClass()->name); return false; } struct DateTimeValue { ClippedTime time; DateTimeValueKind kind; }; /** * DateTime Format Functions, steps 3-4 and 5 (partial). * Intl.DateTimeFormat.prototype.formatToParts, steps 3-4 and 5 (partial). */ static bool ToDateTimeValue(JSContext* cx, const char* method, Handle dateTimeFormat, Handle date, DateTimeValue* result) { // DateTime Format Functions, step 3. // Intl.DateTimeFormat.prototype.formatToParts, step 3. if (date.isUndefined()) { result->time = DateNow(cx); result->kind = DateTimeValueKind::Number; return true; } // DateTime Format Functions, step 4. // Intl.DTF.prototype.formatToParts, step 4. auto kind = ToDateTimeFormattable(date); result->kind = kind; // DateTime Format Functions, step 5. (Call to FormatDateTime) // - FormatDateTime, step 1. (Call to PartitionDateTimePattern) // -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue) // --- HandleDateTimeValue, step 1. // // Intl.DTF.prototype.formatToParts, step 5. (Call to FormatDateTimeToParts) // - FormatDateTimeToParts, step 1. (Call to PartitionDateTimePattern) // -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue) // --- HandleDateTimeValue, step 1. if (kind != DateTimeValueKind::Number) { MOZ_ASSERT(date.isObject()); return HandleDateTimeValue(cx, method, dateTimeFormat, &date.toObject(), &result->time); } // ToDateTimeFormattable, step 2. (Call to ToNumber) double num; if (!JS::ToNumber(cx, date, &num)) { return false; } // DateTime Format Functions, step 5. (Call to FormatDateTime) // - FormatDateTime, step 1. (Call to PartitionDateTimePattern) // -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue) // --- HandleDateTimeValue, step 2. (Call to HandleDateTimeOthers) // // Intl.DTF.prototype.formatToParts, step 5. (Call to FormatDateTimeToParts) // - FormatDateTimeToParts, step 1. (Call to PartitionDateTimePattern) // -- PartitionDateTimePattern, step 1. (Call to HandleDateTimeValue) // --- HandleDateTimeValue, step 2. (Call to HandleDateTimeOthers) return HandleDateTimeOthers(cx, method, num, &result->time); } /** * FormatDateTime ( dateTimeFormat, x ) * PartitionDateTimePattern ( dateTimeFormat, x ) * * Returns a String value representing x according to the effective locale and * the formatting options of the given DateTimeFormat. */ static bool FormatDateTime(JSContext* cx, const mozilla::intl::DateTimeFormat* df, ClippedTime x, MutableHandle result) { // FormatDateTime, step 1. (Inlined call to PartitionDateTimePattern) // PartitionDateTimePattern, steps 1-2. MOZ_ASSERT(x.isValid()); // PartitionDateTimePattern, steps 3-8. FormatBuffer buffer(cx); auto dfResult = df->TryFormat(x.toDouble(), buffer); if (dfResult.isErr()) { ReportInternalError(cx, dfResult.unwrapErr()); return false; } // FormatDateTime, steps 2-4. auto* str = buffer.toString(cx); if (!str) { return false; } result.setString(str); return true; } static bool FormatDateTime(JSContext* cx, Handle dateTimeFormat, const DateTimeValue& date, MutableHandle result) { auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, date.kind); if (!df) { return false; } return FormatDateTime(cx, df, date.time, result); } using FieldType = js::ImmutableTenuredPtr JSAtomState::*; static FieldType GetFieldTypeForPartType(mozilla::intl::DateTimePartType type) { switch (type) { case mozilla::intl::DateTimePartType::Literal: return &JSAtomState::literal; case mozilla::intl::DateTimePartType::Era: return &JSAtomState::era; case mozilla::intl::DateTimePartType::Year: return &JSAtomState::year; case mozilla::intl::DateTimePartType::YearName: return &JSAtomState::yearName; case mozilla::intl::DateTimePartType::RelatedYear: return &JSAtomState::relatedYear; case mozilla::intl::DateTimePartType::Month: return &JSAtomState::month; case mozilla::intl::DateTimePartType::Day: return &JSAtomState::day; case mozilla::intl::DateTimePartType::Hour: return &JSAtomState::hour; case mozilla::intl::DateTimePartType::Minute: return &JSAtomState::minute; case mozilla::intl::DateTimePartType::Second: return &JSAtomState::second; case mozilla::intl::DateTimePartType::Weekday: return &JSAtomState::weekday; case mozilla::intl::DateTimePartType::DayPeriod: return &JSAtomState::dayPeriod; case mozilla::intl::DateTimePartType::TimeZoneName: return &JSAtomState::timeZoneName; case mozilla::intl::DateTimePartType::FractionalSecondDigits: return &JSAtomState::fractionalSecond; case mozilla::intl::DateTimePartType::Unknown: return &JSAtomState::unknown; } MOZ_CRASH( "unenumerated, undocumented format field returned " "by iterator"); } static FieldType GetFieldTypeForPartSource( mozilla::intl::DateTimePartSource source) { switch (source) { case mozilla::intl::DateTimePartSource::Shared: return &JSAtomState::shared; case mozilla::intl::DateTimePartSource::StartRange: return &JSAtomState::startRange; case mozilla::intl::DateTimePartSource::EndRange: return &JSAtomState::endRange; } MOZ_CRASH( "unenumerated, undocumented format field returned " "by iterator"); } enum class DateTimeSource : bool { No, Yes }; /** * FormatDateTimeToParts ( dateTimeFormat, x ) * FormatDateTimeRangeToParts ( dateTimeFormat, x, y ) * * Create the part object for FormatDateTime{Range}ToParts. */ static PlainObject* CreateDateTimePart(JSContext* cx, const mozilla::intl::DateTimePart& part, Handle value, DateTimeSource dateTimeSource) { Rooted properties(cx, cx); FieldType typeName = GetFieldTypeForPartType(part.mType); if (!properties.emplaceBack(NameToId(cx->names().type), StringValue(cx->names().*typeName))) { return nullptr; } if (!properties.emplaceBack(NameToId(cx->names().value), StringValue(value))) { return nullptr; } if (dateTimeSource == DateTimeSource::Yes) { FieldType sourceName = GetFieldTypeForPartSource(part.mSource); if (!properties.emplaceBack(NameToId(cx->names().source), StringValue(cx->names().*sourceName))) { return nullptr; } } return NewPlainObjectWithUniqueNames(cx, properties); } /** * FormatDateTimeToParts ( dateTimeFormat, x ) * FormatDateTimeRangeToParts ( dateTimeFormat, x, y ) * * Create the parts array for FormatDateTime{Range}ToParts. */ static bool CreateDateTimePartArray( JSContext* cx, mozilla::Span formattedSpan, DateTimeSource dateTimeSource, const mozilla::intl::DateTimePartVector& parts, MutableHandle result) { Rooted overallResult(cx, NewStringCopy(cx, formattedSpan)); if (!overallResult) { return false; } Rooted partsArray( cx, NewDenseFullyAllocatedArray(cx, parts.length())); if (!partsArray) { return false; } partsArray->ensureDenseInitializedLength(0, parts.length()); if (overallResult->length() == 0) { // An empty string contains no parts, so avoid extra work below. result.setObject(*partsArray); return true; } Rooted value(cx); size_t index = 0; size_t beginIndex = 0; for (const auto& part : parts) { MOZ_ASSERT(part.mEndIndex > beginIndex); value = NewDependentString(cx, overallResult, beginIndex, part.mEndIndex - beginIndex); if (!value) { return false; } beginIndex = part.mEndIndex; auto* obj = CreateDateTimePart(cx, part, value, dateTimeSource); if (!obj) { return false; } partsArray->initDenseElement(index++, ObjectValue(*obj)); } MOZ_ASSERT(index == parts.length()); MOZ_ASSERT(beginIndex == formattedSpan.size()); result.setObject(*partsArray); return true; } static bool FormatToPartsDateTime(JSContext* cx, const mozilla::intl::DateTimeFormat* df, ClippedTime x, DateTimeSource dateTimeSource, MutableHandle result) { MOZ_ASSERT(x.isValid()); FormatBuffer buffer(cx); mozilla::intl::DateTimePartVector parts; auto r = df->TryFormatToParts(x.toDouble(), buffer, parts); if (r.isErr()) { ReportInternalError(cx, r.unwrapErr()); return false; } return CreateDateTimePartArray(cx, buffer, dateTimeSource, parts, result); } static bool FormatToPartsDateTime(JSContext* cx, Handle dateTimeFormat, const DateTimeValue& date, MutableHandle result) { auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, date.kind); if (!df) { return false; } return FormatToPartsDateTime(cx, df, date.time, DateTimeSource::No, result); } struct DateTimeRangeValue { ClippedTime start; ClippedTime end; DateTimeValueKind kind; }; /** * Intl.DateTimeFormat.prototype.formatRange, steps 3-5 and 6 (partial). * Intl.DateTimeFormat.prototype.formatRangeToParts, steps 3-5 and 6 (partial). */ static bool ToDateTimeRangeValue(JSContext* cx, const char* method, Handle dateTimeFormat, Handle start, Handle end, DateTimeRangeValue* result) { // Intl.DateTimeFormat.prototype.formatRange, step 3. // Intl.DateTimeFormat.prototype.formatRangeToParts, step 3. if (start.isUndefined() || end.isUndefined()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNDEFINED_DATE, start.isUndefined() ? "start" : "end", method); return false; } // Intl.DateTimeFormat.prototype.formatRange, step 4. // Intl.DateTimeFormat.prototype.formatRangeToParts, step 4. auto startKind = ToDateTimeFormattable(start); // Intl.DateTimeFormat.prototype.formatRange, step 5. // Intl.DateTimeFormat.prototype.formatRangeToParts, step 5. auto endKind = ToDateTimeFormattable(end); // ToDateTimeFormattable, step 2. (Call to ToNumber) double startNum; if (startKind == DateTimeValueKind::Number) { if (!JS::ToNumber(cx, start, &startNum)) { return false; } } // ToDateTimeFormattable, step 2. (Call to ToNumber) double endNum; if (endKind == DateTimeValueKind::Number) { if (!JS::ToNumber(cx, end, &endNum)) { return false; } } // clang-format off // // Intl.DTF.prototype.formatRange, step 6. (Call to FormatDateTimeRange) // - FormatDateTimeRange, step 1. (Call to PartitionDateTimeRangePattern) // // Intl.DTF.prototype.formatRangeToParts, step 6. (Call to FormatDateTimeRangeToParts) // - FormatDateTimeRangeToParts, step 1. (Call to PartitionDateTimeRangePattern) // // clang-format on // PartitionDateTimeRangePattern, step 1. if (startKind != endKind) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_EXPECTED_TYPE, method, DateTimeValueKindToString(startKind), DateTimeValueKindToString(endKind)); return false; } result->kind = startKind; // PartitionDateTimeRangePattern, steps 2-3. (Call to HandleDateTimeValue) // HandleDateTimeValue, step 1. if (startKind != DateTimeValueKind::Number) { MOZ_ASSERT(start.isObject()); MOZ_ASSERT(end.isObject()); return HandleDateTimeValue(cx, method, dateTimeFormat, &start.toObject(), &result->start) && HandleDateTimeValue(cx, method, dateTimeFormat, &end.toObject(), &result->end); } // HandleDateTimeValue, step 2. return HandleDateTimeOthers(cx, method, startNum, &result->start) && HandleDateTimeOthers(cx, method, endNum, &result->end); } bool js::intl::FormatDateTime(JSContext* cx, Handle dateTimeFormat, double millis, MutableHandle result) { auto x = JS::TimeClip(millis); MOZ_ASSERT(x.isValid()); return FormatDateTime(cx, dateTimeFormat, {x, DateTimeValueKind::Number}, result); } /** * Returns a new DateIntervalFormat with the locale and date-time formatting * options of the given DateTimeFormat. */ static mozilla::intl::DateIntervalFormat* NewDateIntervalFormat( JSContext* cx, Handle dateTimeFormat, mozilla::intl::DateTimeFormat& mozDtf, DateTimeValueKind kind) { if (!ResolveLocale(cx, dateTimeFormat)) { return nullptr; } FormatBuffer pattern(cx); auto result = mozDtf.GetPattern(pattern); if (result.isErr()) { ReportInternalError(cx, result.unwrapErr()); return nullptr; } // Determine the hour cycle used in the resolved pattern. auto hcPattern = mozilla::intl::DateTimeFormat::HourCycleFromPattern(pattern); auto locale = DateTimeFormatLocale(cx, dateTimeFormat, hcPattern); if (!locale) { return nullptr; } TimeZoneChars timeZone(cx); if (IsPlainDateTimeValue(kind)) { timeZone.setToOffset(TimeZoneOffsetString::GMTZero()); } else { if (!timeZone.init(cx, dateTimeFormat->getTimeZone())) { return nullptr; } } FormatBuffer skeleton(cx); auto skelResult = mozDtf.GetOriginalSkeleton(skeleton); if (skelResult.isErr()) { ReportInternalError(cx, skelResult.unwrapErr()); return nullptr; } auto dif = mozilla::intl::DateIntervalFormat::TryCreate( mozilla::MakeStringSpan(locale.get()), skeleton, timeZone); if (dif.isErr()) { ReportInternalError(cx, dif.unwrapErr()); return nullptr; } return dif.unwrap().release(); } static mozilla::intl::DateIntervalFormat* GetOrCreateDateIntervalFormat( JSContext* cx, Handle dateTimeFormat, mozilla::intl::DateTimeFormat& mozDtf, DateTimeValueKind kind) { dateTimeFormat->maybeClearCache(kind); // Obtain a cached DateIntervalFormat object. if (auto* dif = dateTimeFormat->getDateIntervalFormat()) { return dif; } auto* dif = NewDateIntervalFormat(cx, dateTimeFormat, mozDtf, kind); if (!dif) { return nullptr; } dateTimeFormat->setDateIntervalFormat(dif); AddICUCellMemory(dateTimeFormat, DateTimeFormatObject::UDateIntervalFormatEstimatedMemoryUse); return dif; } /** * PartitionDateTimeRangePattern ( dateTimeFormat, x, y ) */ static bool PartitionDateTimeRangePattern( JSContext* cx, const mozilla::intl::DateTimeFormat* df, const mozilla::intl::DateIntervalFormat* dif, mozilla::intl::AutoFormattedDateInterval& formatted, ClippedTime x, ClippedTime y, bool* equal) { MOZ_ASSERT(x.isValid()); MOZ_ASSERT(y.isValid()); auto result = dif->TryFormatDateTime(x.toDouble(), y.toDouble(), df, formatted, equal); if (result.isErr()) { ReportInternalError(cx, result.unwrapErr()); return false; } return true; } /** * FormatDateTimeRange( dateTimeFormat, x, y ) * * Returns a String value representing the range between x and y according to * the effective locale and the formatting options of the given DateTimeFormat. */ static bool FormatDateTimeRange(JSContext* cx, Handle dateTimeFormat, const DateTimeRangeValue& values, MutableHandle result) { auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, values.kind); if (!df) { return false; } auto* dif = GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df, values.kind); if (!dif) { return false; } mozilla::intl::AutoFormattedDateInterval formatted; if (!formatted.IsValid()) { ReportInternalError(cx, formatted.GetError()); return false; } bool equal; if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, values.start, values.end, &equal)) { return false; } // PartitionDateTimeRangePattern, step 12. if (equal) { return FormatDateTime(cx, df, values.start, result); } auto spanResult = formatted.ToSpan(); if (spanResult.isErr()) { ReportInternalError(cx, spanResult.unwrapErr()); return false; } auto* resultStr = NewStringCopy(cx, spanResult.unwrap()); if (!resultStr) { return false; } result.setString(resultStr); return true; } /** * FormatDateTimeRangeToParts ( dateTimeFormat, x, y ) */ static bool FormatDateTimeRangeToParts( JSContext* cx, Handle dateTimeFormat, const DateTimeRangeValue& values, MutableHandle result) { auto* df = GetOrCreateDateTimeFormat(cx, dateTimeFormat, values.kind); if (!df) { return false; } auto* dif = GetOrCreateDateIntervalFormat(cx, dateTimeFormat, *df, values.kind); if (!dif) { return false; } mozilla::intl::AutoFormattedDateInterval formatted; if (!formatted.IsValid()) { ReportInternalError(cx, formatted.GetError()); return false; } bool equal; if (!PartitionDateTimeRangePattern(cx, df, dif, formatted, values.start, values.end, &equal)) { return false; } // PartitionDateTimeRangePattern, step 12. if (equal) { return FormatToPartsDateTime(cx, df, values.start, DateTimeSource::Yes, result); } mozilla::intl::DateTimePartVector parts; auto r = dif->TryFormattedToParts(formatted, parts); if (r.isErr()) { ReportInternalError(cx, r.unwrapErr()); return false; } auto spanResult = formatted.ToSpan(); if (spanResult.isErr()) { ReportInternalError(cx, spanResult.unwrapErr()); return false; } return CreateDateTimePartArray(cx, spanResult.unwrap(), DateTimeSource::Yes, parts, result); } static bool IsDateTimeFormat(Handle v) { return v.isObject() && v.toObject().is(); } /** * UnwrapDateTimeFormat ( dtf ) */ static bool UnwrapDateTimeFormat(JSContext* cx, MutableHandle dtf) { // Step 1. (Error handling moved to caller) if (!dtf.isObject()) { return true; } auto* obj = &dtf.toObject(); if (obj->canUnwrapAs()) { return true; } Rooted format(cx, obj); return UnwrapLegacyIntlFormat(cx, JSProto_DateTimeFormat, format, dtf); } static constexpr uint32_t DateTimeFormatFunction_DateTimeFormat = 0; /** * DateTime Format Functions */ static bool DateTimeCompareFunction(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Steps 1-2. auto* compare = &args.callee().as(); auto dtfValue = compare->getExtendedSlot(DateTimeFormatFunction_DateTimeFormat); Rooted dateTimeFormat( cx, &dtfValue.toObject().as()); // Steps 3-4. DateTimeValue x; if (!ToDateTimeValue(cx, "format", dateTimeFormat, args.get(0), &x)) { return false; } // Step 5. return FormatDateTime(cx, dateTimeFormat, x, args.rval()); } /** * get Intl.DateTimeFormat.prototype.format */ static bool dateTimeFormat_format(JSContext* cx, const CallArgs& args) { Rooted dateTimeFormat( cx, &args.thisv().toObject().as()); // Step 4. auto* boundFormat = dateTimeFormat->getBoundFormat(); if (!boundFormat) { Handle funName = cx->names().empty_; auto* fn = NewNativeFunction(cx, DateTimeCompareFunction, 1, funName, gc::AllocKind::FUNCTION_EXTENDED, GenericObject); if (!fn) { return false; } fn->initExtendedSlot(DateTimeFormatFunction_DateTimeFormat, ObjectValue(*dateTimeFormat)); dateTimeFormat->setBoundFormat(fn); boundFormat = fn; } // Step 5. args.rval().setObject(*boundFormat); return true; } /** * get Intl.DateTimeFormat.prototype.format */ static bool dateTimeFormat_format(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-3. CallArgs args = CallArgsFromVp(argc, vp); if (!UnwrapDateTimeFormat(cx, args.mutableThisv())) { return false; } return CallNonGenericMethod(cx, args); } /** * Intl.DateTimeFormat.prototype.formatToParts ( date ) */ static bool dateTimeFormat_formatToParts(JSContext* cx, const CallArgs& args) { Rooted dateTimeFormat( cx, &args.thisv().toObject().as()); // Steps 3-4. DateTimeValue x; if (!ToDateTimeValue(cx, "formatRange", dateTimeFormat, args.get(0), &x)) { return false; } // Step 5. return FormatToPartsDateTime(cx, dateTimeFormat, x, args.rval()); } /** * Intl.DateTimeFormat.prototype.formatToParts ( date ) */ static bool dateTimeFormat_formatToParts(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod( cx, args); } /** * Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate ) */ static bool dateTimeFormat_formatRange(JSContext* cx, const CallArgs& args) { Rooted dateTimeFormat( cx, &args.thisv().toObject().as()); // Steps 3-5. DateTimeRangeValue values; if (!ToDateTimeRangeValue(cx, "formatRange", dateTimeFormat, args.get(0), args.get(1), &values)) { return false; } // Step 6. return FormatDateTimeRange(cx, dateTimeFormat, values, args.rval()); } /** * Intl.DateTimeFormat.prototype.formatRange ( startDate, endDate ) */ static bool dateTimeFormat_formatRange(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod( cx, args); } /** * Intl.DateTimeFormat.prototype.formatRangeToParts ( date ) */ static bool dateTimeFormat_formatRangeToParts(JSContext* cx, const CallArgs& args) { Rooted dateTimeFormat( cx, &args.thisv().toObject().as()); // Steps 3-5. DateTimeRangeValue values; if (!ToDateTimeRangeValue(cx, "formatRangeToParts", dateTimeFormat, args.get(0), args.get(1), &values)) { return false; } // Step 6. return FormatDateTimeRangeToParts(cx, dateTimeFormat, values, args.rval()); } /** * Intl.DateTimeFormat.prototype.formatRangeToParts ( date ) */ static bool dateTimeFormat_formatRangeToParts(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } /** * Intl.DateTimeFormat.prototype.resolvedOptions ( ) */ static bool dateTimeFormat_resolvedOptions(JSContext* cx, const CallArgs& args) { Rooted dateTimeFormat( cx, &args.thisv().toObject().as()); if (!ResolveLocale(cx, dateTimeFormat)) { return false; } auto dtfOptions = *dateTimeFormat->getOptions(); // Step 3. Rooted options(cx, cx); // Step 4. if (!options.emplaceBack(NameToId(cx->names().locale), StringValue(dateTimeFormat->getLocale()))) { return false; } if (!options.emplaceBack(NameToId(cx->names().calendar), StringValue(dateTimeFormat->getCalendar()))) { return false; } if (!options.emplaceBack(NameToId(cx->names().numberingSystem), StringValue(dateTimeFormat->getNumberingSystem()))) { return false; } if (!options.emplaceBack(NameToId(cx->names().timeZone), StringValue(dateTimeFormat->getTimeZone()))) { return false; } // The raw pattern option is only internal to Mozilla, and not part of the // ECMA-402 API. if (auto* pattern = dateTimeFormat->getPattern()) { if (!options.emplaceBack(NameToId(cx->names().pattern), StringValue(pattern))) { return false; } } bool hasDateStyle = dtfOptions.dateStyle.isSome(); bool hasTimeStyle = dtfOptions.timeStyle.isSome(); if (hasDateStyle || hasTimeStyle) { if (hasTimeStyle) { // timeStyle (unlike dateStyle) requires resolving the pattern to ensure // "hourCycle" and "hour12" properties are added to |result|. if (!ResolveDateTimeFormatComponents(cx, dateTimeFormat, &options, IncludeDateTimeFields::No)) { return false; } } if (hasDateStyle) { auto* dateStyle = NewStringCopy(cx, DateStyleToString(*dtfOptions.dateStyle)); if (!dateStyle) { return false; } if (!options.emplaceBack(NameToId(cx->names().dateStyle), StringValue(dateStyle))) { return false; } } if (hasTimeStyle) { auto* timeStyle = NewStringCopy(cx, TimeStyleToString(*dtfOptions.timeStyle)); if (!timeStyle) { return false; } if (!options.emplaceBack(NameToId(cx->names().timeStyle), StringValue(timeStyle))) { return false; } } } else { if (!ResolveDateTimeFormatComponents(cx, dateTimeFormat, &options, IncludeDateTimeFields::Yes)) { return false; } } // Step 6. auto* result = NewPlainObjectWithUniqueNames(cx, options); if (!result) { return false; } args.rval().setObject(*result); return true; } /** * Intl.DateTimeFormat.prototype.resolvedOptions ( ) */ static bool dateTimeFormat_resolvedOptions(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. CallArgs args = CallArgsFromVp(argc, vp); if (!UnwrapDateTimeFormat(cx, args.mutableThisv())) { return false; } return CallNonGenericMethod( cx, args); } /** * Intl.DateTimeFormat.supportedLocalesOf ( locales [ , options ] ) */ static bool dateTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Steps 1-3. auto* array = SupportedLocalesOf(cx, AvailableLocaleKind::DateTimeFormat, args.get(0), args.get(1)); if (!array) { return false; } args.rval().setObject(*array); return true; } bool js::intl::TemporalObjectToLocaleString( JSContext* cx, const CallArgs& args, DateTimeFormatKind formatKind, Handle toLocaleStringTimeZone) { Rooted thisValue(cx, &args.thisv().toObject()); auto kind = ToDateTimeFormattable(args.thisv()); MOZ_ASSERT(kind != DateTimeValueKind::Number); MOZ_ASSERT_IF(kind != DateTimeValueKind::TemporalZonedDateTime, toLocaleStringTimeZone == nullptr); MOZ_ASSERT_IF(kind == DateTimeValueKind::TemporalZonedDateTime, toLocaleStringTimeZone != nullptr); auto locales = args.get(0); auto options = args.get(1); Rooted dateTimeFormat(cx); if (kind != DateTimeValueKind::TemporalZonedDateTime) { dateTimeFormat = GetOrCreateDateTimeFormat(cx, locales, options, formatKind); } else { // Cache doesn't yet support Temporal.ZonedDateTime. dateTimeFormat = ::CreateDateTimeFormat(cx, locales, options, toLocaleStringTimeZone, formatKind); } if (!dateTimeFormat) { return false; } JS::ClippedTime x; if (kind == DateTimeValueKind::TemporalZonedDateTime) { auto zonedDateTime = thisValue.as(); if (!HandleDateTimeTemporalZonedDateTime(cx, dateTimeFormat, zonedDateTime, &x)) { return false; } } else { if (!HandleDateTimeValue(cx, "toLocaleString", dateTimeFormat, thisValue, &x)) { return false; } } return FormatDateTime(cx, dateTimeFormat, {x, kind}, args.rval()); }