/* 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.RelativeTimeFormat implementation. */ #include "builtin/intl/RelativeTimeFormat.h" #include "mozilla/Assertions.h" #include "mozilla/intl/RelativeTimeFormat.h" #include "mozilla/UsingEnum.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/FormatBuffer.h" #include "builtin/intl/LanguageTag.h" #include "builtin/intl/LocaleNegotiation.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/Packed.h" #include "builtin/intl/ParameterNegotiation.h" #include "gc/GCContext.h" #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/Printer.h" #include "js/PropertySpec.h" #include "vm/GlobalObject.h" #include "vm/JSContext.h" #include "vm/PlainObject.h" #include "vm/StringType.h" #include "vm/NativeObject-inl.h" using namespace js; using namespace js::intl; /**************** RelativeTimeFormat *****************/ const JSClassOps RelativeTimeFormatObject::classOps_ = { .finalize = RelativeTimeFormatObject::finalize, }; const JSClass RelativeTimeFormatObject::class_ = { "Intl.RelativeTimeFormat", JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) | JSCLASS_HAS_CACHED_PROTO(JSProto_RelativeTimeFormat) | JSCLASS_BACKGROUND_FINALIZE, &RelativeTimeFormatObject::classOps_, &RelativeTimeFormatObject::classSpec_, }; const JSClass& RelativeTimeFormatObject::protoClass_ = PlainObject::class_; static bool relativeTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc, Value* vp); static bool relativeTimeFormat_format(JSContext* cx, unsigned argc, Value* vp); static bool relativeTimeFormat_formatToParts(JSContext* cx, unsigned argc, Value* vp); static bool relativeTimeFormat_resolvedOptions(JSContext* cx, unsigned argc, Value* vp); static bool relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); args.rval().setString(cx->names().RelativeTimeFormat); return true; } static const JSFunctionSpec relativeTimeFormat_static_methods[] = { JS_FN("supportedLocalesOf", relativeTimeFormat_supportedLocalesOf, 1, 0), JS_FS_END, }; static const JSFunctionSpec relativeTimeFormat_methods[] = { JS_FN("resolvedOptions", relativeTimeFormat_resolvedOptions, 0, 0), JS_FN("format", relativeTimeFormat_format, 2, 0), JS_FN("formatToParts", relativeTimeFormat_formatToParts, 2, 0), JS_FN("toSource", relativeTimeFormat_toSource, 0, 0), JS_FS_END, }; static const JSPropertySpec relativeTimeFormat_properties[] = { JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), JS_PS_END, }; static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp); const ClassSpec RelativeTimeFormatObject::classSpec_ = { GenericCreateConstructor, GenericCreatePrototype, relativeTimeFormat_static_methods, nullptr, relativeTimeFormat_methods, relativeTimeFormat_properties, nullptr, ClassSpec::DontDefineConstructor, }; struct js::intl::RelativeTimeFormatOptions { using Style = mozilla::intl::RelativeTimeFormatOptions::Style; Style style = Style::Long; using Numeric = mozilla::intl::RelativeTimeFormatOptions::Numeric; Numeric numeric = Numeric::Always; }; struct PackedRelativeTimeFormatOptions { using RawValue = uint32_t; using StyleField = packed::EnumField; using NumericField = packed::EnumField; using PackedValue = packed::PackedValue; static auto pack(const RelativeTimeFormatOptions& options) { RawValue rawValue = StyleField::pack(options.style) | NumericField::pack(options.numeric); return PackedValue::toValue(rawValue); } static auto unpack(JS::Value value) { RawValue rawValue = PackedValue::fromValue(value); return RelativeTimeFormatOptions{ .style = StyleField::unpack(rawValue), .numeric = NumericField::unpack(rawValue), }; } }; RelativeTimeFormatOptions js::intl::RelativeTimeFormatObject::getOptions() const { const auto& slot = getFixedSlotTyped(OPTIONS); if (slot.isUndefined()) { return {}; } return PackedRelativeTimeFormatOptions::unpack(slot); } void js::intl::RelativeTimeFormatObject::setOptions( const RelativeTimeFormatOptions& options) { setFixedSlotTyped(OPTIONS, PackedRelativeTimeFormatOptions::pack(options)); } static constexpr std::string_view RelativeTimeStyleToString( RelativeTimeFormatOptions::Style style) { MOZ_USING_ENUM(RelativeTimeFormatOptions::Style, Long, Short, Narrow); switch (style) { case Long: return "long"; case Short: return "short"; case Narrow: return "narrow"; } MOZ_CRASH("invalid relative time format style"); } static constexpr std::string_view NumericToString( RelativeTimeFormatOptions::Numeric numeric) { MOZ_USING_ENUM(RelativeTimeFormatOptions::Numeric, Always, Auto); switch (numeric) { case Always: return "always"; case Auto: return "auto"; } MOZ_CRASH("invalid relative time format numeric"); } /** * Intl.RelativeTimeFormat ( [ locales [ , options ] ] ) */ static bool RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1. if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) { return false; } // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). Rooted proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_RelativeTimeFormat, &proto)) { return false; } Rooted relativeTimeFormat(cx); relativeTimeFormat = NewObjectWithClassProto(cx, proto); if (!relativeTimeFormat) { return false; } // Step 3. (Inlined ResolveOptions) // ResolveOptions, step 1. auto* requestedLocales = CanonicalizeLocaleList(cx, args.get(0)); if (!requestedLocales) { return false; } relativeTimeFormat->setRequestedLocales(requestedLocales); RelativeTimeFormatOptions rtfOptions{}; if (args.hasDefined(1)) { // ResolveOptions, steps 2-3. Rooted options(cx, JS::ToObject(cx, args[1])); 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 numberingSystem(cx); if (!GetUnicodeExtensionOption(cx, options, UnicodeExtensionKey::NumberingSystem, &numberingSystem)) { return false; } if (numberingSystem) { relativeTimeFormat->setNumberingSystem(numberingSystem); } // ResolveOptions, step 7. (Not applicable) // ResolveOptions, step 8. (Performed in ResolveRelativeTimeFormat) // ResolveOptions, step 9. (Return) // Step 4. (Not applicable when ResolveOptions is inlined.) // Steps 5-9. (Performed in ResolveLocale) // Steps 10-11. static constexpr auto styles = MapOptions( RelativeTimeFormatOptions::Style::Long, RelativeTimeFormatOptions::Style::Short, RelativeTimeFormatOptions::Style::Narrow); if (!GetStringOption(cx, options, cx->names().style, styles, RelativeTimeFormatOptions::Style::Long, &rtfOptions.style)) { return false; } // Steps 12-13. static constexpr auto numerics = MapOptions(RelativeTimeFormatOptions::Numeric::Always, RelativeTimeFormatOptions::Numeric::Auto); if (!GetStringOption(cx, options, cx->names().numeric, numerics, RelativeTimeFormatOptions::Numeric::Always, &rtfOptions.numeric)) { return false; } } relativeTimeFormat->setOptions(rtfOptions); // Steps 14-17. (Not applicable in our implementation.) // Step 18. args.rval().setObject(*relativeTimeFormat); return true; } void js::intl::RelativeTimeFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { auto* rtf = &obj->as(); if (auto* formatter = rtf->getRelativeTimeFormatter()) { RemoveICUCellMemory(gcx, obj, RelativeTimeFormatObject::EstimatedMemoryUse); // This was allocated using `new` in mozilla::intl::RelativeTimeFormat, // so we delete here. delete formatter; } } /** * Resolve the actual locale to finish initialization of the RelativeTimeFormat. */ static bool ResolveLocale( JSContext* cx, Handle relativeTimeFormat) { // Return if the locale was already resolved. if (relativeTimeFormat->isLocaleResolved()) { return true; } Rooted requestedLocales( cx, &relativeTimeFormat->getRequestedLocales()->as()); // %Intl.RelativeTimeFormat%.[[RelevantExtensionKeys]] is « "nu" ». mozilla::EnumSet relevantExtensionKeys{ UnicodeExtensionKey::NumberingSystem, }; // Initialize locale options from constructor arguments. Rooted localeOptions(cx); if (auto* nu = relativeTimeFormat->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::RelativeTimeFormat, requestedLocales, localeOptions, relevantExtensionKeys, localeData, &resolved)) { return false; } // Finish initialization by setting the actual locale and numbering system. auto* locale = resolved.toLocale(cx); if (!locale) { return false; } relativeTimeFormat->setLocale(locale); if (auto nu = resolved.extension(UnicodeExtensionKey::NumberingSystem)) { relativeTimeFormat->setNumberingSystem(nu); } else { relativeTimeFormat->setNumberingSystem(cx->names().default_); } MOZ_ASSERT(relativeTimeFormat->isLocaleResolved(), "locale successfully resolved"); return true; } static JSLinearString* ResolveNumberingSystem( JSContext* cx, Handle relativeTimeFormat) { MOZ_ASSERT(relativeTimeFormat->isLocaleResolved()); auto* numberingSystem = relativeTimeFormat->getNumberingSystem(); if (numberingSystem == cx->names().default_) { numberingSystem = DefaultNumberingSystem(cx, relativeTimeFormat->getLocale()); if (!numberingSystem) { return nullptr; } relativeTimeFormat->setNumberingSystem(numberingSystem); } return numberingSystem; } /** * Returns a new URelativeDateTimeFormatter with the locale and options of the * given RelativeTimeFormatObject. */ static mozilla::intl::RelativeTimeFormat* NewRelativeTimeFormatter( JSContext* cx, Handle relativeTimeFormat) { if (!ResolveLocale(cx, relativeTimeFormat)) { return nullptr; } auto rtfOptions = relativeTimeFormat->getOptions(); // ICU expects numberingSystem as a Unicode locale extensions on locale. // // We don't add any Unicode extension keywords when the default values can be // used, because ICU optimizes for this case. JS::RootedVector keywords(cx); auto* numberingSystem = relativeTimeFormat->getNumberingSystem(); if (numberingSystem != cx->names().default_) { if (!keywords.emplaceBack("nu", numberingSystem)) { return nullptr; } } Rooted localeStr(cx, relativeTimeFormat->getLocale()); auto locale = FormatLocale(cx, localeStr, keywords); if (!locale) { return nullptr; } mozilla::intl::RelativeTimeFormatOptions options = { .style = rtfOptions.style, .numeric = rtfOptions.numeric, }; auto result = mozilla::intl::RelativeTimeFormat::TryCreate(locale.get(), options); if (result.isErr()) { ReportInternalError(cx, result.unwrapErr()); return nullptr; } return result.unwrap().release(); } static mozilla::intl::RelativeTimeFormat* GetOrCreateRelativeTimeFormat( JSContext* cx, Handle relativeTimeFormat) { // Obtain a cached RelativeDateTimeFormatter object. auto* rtf = relativeTimeFormat->getRelativeTimeFormatter(); if (rtf) { return rtf; } rtf = NewRelativeTimeFormatter(cx, relativeTimeFormat); if (!rtf) { return nullptr; } relativeTimeFormat->setRelativeTimeFormatter(rtf); AddICUCellMemory(relativeTimeFormat, RelativeTimeFormatObject::EstimatedMemoryUse); return rtf; } /** * SingularRelativeTimeUnit ( unit ) */ static bool SingularRelativeTimeUnit( JSContext* cx, Handle string, mozilla::intl::RelativeTimeFormat::FormatUnit* result) { using FormatUnit = mozilla::intl::RelativeTimeFormat::FormatUnit; auto* unit = string->ensureLinear(cx); if (!unit) { return false; } // Steps 1-10. if (StringEqualsLiteral(unit, "second") || StringEqualsLiteral(unit, "seconds")) { *result = FormatUnit::Second; } else if (StringEqualsLiteral(unit, "minute") || StringEqualsLiteral(unit, "minutes")) { *result = FormatUnit::Minute; } else if (StringEqualsLiteral(unit, "hour") || StringEqualsLiteral(unit, "hours")) { *result = FormatUnit::Hour; } else if (StringEqualsLiteral(unit, "day") || StringEqualsLiteral(unit, "days")) { *result = FormatUnit::Day; } else if (StringEqualsLiteral(unit, "week") || StringEqualsLiteral(unit, "weeks")) { *result = FormatUnit::Week; } else if (StringEqualsLiteral(unit, "month") || StringEqualsLiteral(unit, "months")) { *result = FormatUnit::Month; } else if (StringEqualsLiteral(unit, "quarter") || StringEqualsLiteral(unit, "quarters")) { *result = FormatUnit::Quarter; } else if (StringEqualsLiteral(unit, "year") || StringEqualsLiteral(unit, "years")) { *result = FormatUnit::Year; } else { if (auto unitChars = QuoteString(cx, unit, '"')) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INVALID_OPTION_VALUE, "unit", unitChars.get()); } return false; } return true; } static auto ToNumberFormatUnit( mozilla::intl::RelativeTimeFormat::FormatUnit unit) { MOZ_USING_ENUM(mozilla::intl::RelativeTimeFormat::FormatUnit, Second, Minute, Hour, Day, Week, Month, Quarter, Year); switch (unit) { case Second: return NumberFormatUnit::Second; case Minute: return NumberFormatUnit::Minute; case Hour: return NumberFormatUnit::Hour; case Day: return NumberFormatUnit::Day; case Week: return NumberFormatUnit::Week; case Month: return NumberFormatUnit::Month; case Quarter: return NumberFormatUnit::Quarter; case Year: return NumberFormatUnit::Year; } MOZ_CRASH("invalid format unit"); } /** * FormatRelativeTime ( relativeTimeFormat, value, unit ) * FormatRelativeTimeToParts ( relativeTimeFormat, value, unit ) * PartitionRelativeTimePattern ( relativeTimeFormat, value, unit ) * * Returns a relative time as a string formatted according to the effective * locale and the formatting options of the given RelativeTimeFormat. */ static bool FormatRelativeTime( JSContext* cx, Handle relativeTimeFormat, double value, Handle unit, bool formatToParts, MutableHandle rvalue) { // PartitionRelativeTimePattern, step 1. if (!std::isfinite(value)) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat", formatToParts ? "formatToParts" : "format"); return false; } // PartitionRelativeTimePattern, step 2. mozilla::intl::RelativeTimeFormat::FormatUnit relTimeUnit; if (!SingularRelativeTimeUnit(cx, unit, &relTimeUnit)) { return false; } auto* rtf = GetOrCreateRelativeTimeFormat(cx, relativeTimeFormat); if (!rtf) { return false; } // PartitionRelativeTimePattern, steps 3-14. // FormatRelativeTimeToParts, steps 2-5. if (formatToParts) { mozilla::intl::NumberPartVector parts; auto result = rtf->formatToParts(value, relTimeUnit, parts); if (result.isErr()) { ReportInternalError(cx, result.unwrapErr()); return false; } Rooted str(cx, NewStringCopy(cx, result.unwrap())); if (!str) { return false; } auto numberUnit = ToNumberFormatUnit(relTimeUnit); return FormattedRelativeTimeToParts(cx, str, parts, numberUnit, rvalue); } // PartitionRelativeTimePattern, steps 3-14. // FormatRelativeTime, steps 2-4. FormatBuffer buffer(cx); auto result = rtf->format(value, relTimeUnit, buffer); if (result.isErr()) { ReportInternalError(cx, result.unwrapErr()); return false; } auto* str = buffer.toString(cx); if (!str) { return false; } rvalue.setString(str); return true; } static bool IsRelativeTimeFormat(Handle v) { return v.isObject() && v.toObject().is(); } /** * Intl.RelativeTimeFormat.prototype.format ( value, unit ) */ static bool relativeTimeFormat_format(JSContext* cx, const CallArgs& args) { Rooted relativeTimeFormat( cx, &args.thisv().toObject().as()); // Step 3. double value; if (!JS::ToNumber(cx, args.get(0), &value)) { return false; } // Step 4. Rooted unit(cx, JS::ToString(cx, args.get(1))); if (!unit) { return false; } // Step 5. return FormatRelativeTime(cx, relativeTimeFormat, value, unit, /* formatToParts= */ false, args.rval()); } /** * Intl.RelativeTimeFormat.prototype.format ( value, unit ) */ static bool relativeTimeFormat_format(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod( cx, args); } /** * Intl.RelativeTimeFormat.prototype.formatToParts ( value, unit ) */ static bool relativeTimeFormat_formatToParts(JSContext* cx, const CallArgs& args) { Rooted relativeTimeFormat( cx, &args.thisv().toObject().as()); // Step 3. double value; if (!JS::ToNumber(cx, args.get(0), &value)) { return false; } // Step 4. Rooted unit(cx, JS::ToString(cx, args.get(1))); if (!unit) { return false; } // Step 5. return FormatRelativeTime(cx, relativeTimeFormat, value, unit, /* formatToParts= */ true, args.rval()); } /** * Intl.RelativeTimeFormat.prototype.formatToParts ( value, unit ) */ static bool relativeTimeFormat_formatToParts(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } /** * Intl.RelativeTimeFormat.prototype.resolvedOptions ( ) */ static bool relativeTimeFormat_resolvedOptions(JSContext* cx, const CallArgs& args) { Rooted relativeTimeFormat( cx, &args.thisv().toObject().as()); if (!ResolveLocale(cx, relativeTimeFormat)) { return false; } auto rtfOptions = relativeTimeFormat->getOptions(); // Step 3. Rooted options(cx, cx); // Step 4. if (!options.emplaceBack(NameToId(cx->names().locale), StringValue(relativeTimeFormat->getLocale()))) { return false; } auto* style = NewStringCopy(cx, RelativeTimeStyleToString(rtfOptions.style)); if (!style) { return false; } if (!options.emplaceBack(NameToId(cx->names().style), StringValue(style))) { return false; } auto* numeric = NewStringCopy(cx, NumericToString(rtfOptions.numeric)); if (!numeric) { return false; } if (!options.emplaceBack(NameToId(cx->names().numeric), StringValue(numeric))) { return false; } auto* numberingSystem = ResolveNumberingSystem(cx, relativeTimeFormat); if (!numberingSystem) { return false; } if (!options.emplaceBack(NameToId(cx->names().numberingSystem), StringValue(numberingSystem))) { return false; } // Step 5. auto* result = NewPlainObjectWithUniqueNames(cx, options); if (!result) { return false; } args.rval().setObject(*result); return true; } /** * Intl.RelativeTimeFormat.prototype.resolvedOptions ( ) */ static bool relativeTimeFormat_resolvedOptions(JSContext* cx, unsigned argc, Value* vp) { // Steps 1-2. CallArgs args = CallArgsFromVp(argc, vp); return CallNonGenericMethod(cx, args); } /** * Intl.RelativeTimeFormat.supportedLocalesOf ( locales [ , options ] ) */ static bool relativeTimeFormat_supportedLocalesOf(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Steps 1-3. auto* array = SupportedLocalesOf(cx, AvailableLocaleKind::RelativeTimeFormat, args.get(0), args.get(1)); if (!array) { return false; } args.rval().setObject(*array); return true; }