/* -*- 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.NumberFormat implementation. */ #include "builtin/intl/NumberFormat.h" #include "mozilla/Assertions.h" #include "mozilla/Casting.h" #include "mozilla/FloatingPoint.h" #include "mozilla/intl/Locale.h" #include "mozilla/intl/MeasureUnit.h" #include "mozilla/intl/MeasureUnitGenerated.h" #include "mozilla/intl/NumberFormat.h" #include "mozilla/intl/NumberingSystem.h" #include "mozilla/intl/NumberRangeFormat.h" #include "mozilla/Span.h" #include "mozilla/TextUtils.h" #include "mozilla/UniquePtr.h" #include #include #include #include #include #include #include "builtin/Array.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/DecimalNumber.h" #include "builtin/intl/FormatBuffer.h" #include "builtin/intl/LanguageTag.h" #include "builtin/intl/RelativeTimeFormat.h" #include "gc/GCContext.h" #include "js/CharacterEncoding.h" #include "js/PropertySpec.h" #include "js/RootingAPI.h" #include "js/TypeDecls.h" #include "util/Text.h" #include "vm/BigIntType.h" #include "vm/GlobalObject.h" #include "vm/JSContext.h" #include "vm/PlainObject.h" // js::PlainObject #include "vm/StringType.h" #include "vm/WellKnownAtom.h" // js_*_str #include "vm/GeckoProfiler-inl.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" using namespace js; using mozilla::AssertedCast; using js::intl::DateTimeFormatOptions; using js::intl::FieldType; const JSClassOps NumberFormatObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve NumberFormatObject::finalize, // finalize nullptr, // call nullptr, // construct nullptr, // trace }; const JSClass NumberFormatObject::class_ = { "Intl.NumberFormat", JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) | JSCLASS_HAS_CACHED_PROTO(JSProto_NumberFormat) | JSCLASS_FOREGROUND_FINALIZE, &NumberFormatObject::classOps_, &NumberFormatObject::classSpec_}; const JSClass& NumberFormatObject::protoClass_ = PlainObject::class_; static bool numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); args.rval().setString(cx->names().NumberFormat); return true; } static const JSFunctionSpec numberFormat_static_methods[] = { JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_NumberFormat_supportedLocalesOf", 1, 0), JS_FS_END, }; static const JSFunctionSpec numberFormat_methods[] = { JS_SELF_HOSTED_FN("resolvedOptions", "Intl_NumberFormat_resolvedOptions", 0, 0), JS_SELF_HOSTED_FN("formatToParts", "Intl_NumberFormat_formatToParts", 1, 0), #ifdef NIGHTLY_BUILD JS_SELF_HOSTED_FN("formatRange", "Intl_NumberFormat_formatRange", 2, 0), JS_SELF_HOSTED_FN("formatRangeToParts", "Intl_NumberFormat_formatRangeToParts", 2, 0), #endif JS_FN(js_toSource_str, numberFormat_toSource, 0, 0), JS_FS_END, }; static const JSPropertySpec numberFormat_properties[] = { JS_SELF_HOSTED_GET("format", "$Intl_NumberFormat_format_get", 0), JS_STRING_SYM_PS(toStringTag, "Intl.NumberFormat", JSPROP_READONLY), JS_PS_END, }; static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp); const ClassSpec NumberFormatObject::classSpec_ = { GenericCreateConstructor, GenericCreatePrototype, numberFormat_static_methods, nullptr, numberFormat_methods, numberFormat_properties, nullptr, ClassSpec::DontDefineConstructor}; /** * 11.2.1 Intl.NumberFormat([ locales [, options]]) * * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b */ static bool NumberFormat(JSContext* cx, const CallArgs& args, bool construct) { AutoJSConstructorProfilerEntry pseudoFrame(cx, "Intl.NumberFormat"); // Step 1 (Handled by OrdinaryCreateFromConstructor fallback code). // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). RootedObject proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_NumberFormat, &proto)) { return false; } Rooted numberFormat(cx); numberFormat = NewObjectWithClassProto(cx, proto); if (!numberFormat) { return false; } RootedValue thisValue(cx, construct ? ObjectValue(*numberFormat) : args.thisv()); HandleValue locales = args.get(0); HandleValue options = args.get(1); // Step 3. return intl::LegacyInitializeObject( cx, numberFormat, cx->names().InitializeNumberFormat, thisValue, locales, options, DateTimeFormatOptions::Standard, args.rval()); } static bool NumberFormat(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); return NumberFormat(cx, args, args.isConstructing()); } bool js::intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 2); MOZ_ASSERT(!args.isConstructing()); // intl_NumberFormat is an intrinsic for self-hosted JavaScript, so it // cannot be used with "new", but it still has to be treated as a // constructor. return NumberFormat(cx, args, true); } void js::NumberFormatObject::finalize(JS::GCContext* gcx, JSObject* obj) { MOZ_ASSERT(gcx->onMainThread()); auto* numberFormat = &obj->as(); mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter(); mozilla::intl::NumberRangeFormat* nrf = numberFormat->getNumberRangeFormatter(); if (nf) { intl::RemoveICUCellMemory(gcx, obj, NumberFormatObject::EstimatedMemoryUse); // This was allocated using `new` in mozilla::intl::NumberFormat, so we // delete here. delete nf; } if (nrf) { intl::RemoveICUCellMemory(gcx, obj, EstimatedRangeFormatterMemoryUse); // This was allocated using `new` in mozilla::intl::NumberRangeFormat, so we // delete here. delete nrf; } } bool js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 1); MOZ_ASSERT(args[0].isString()); UniqueChars locale = intl::EncodeLocale(cx, args[0].toString()); if (!locale) { return false; } auto numberingSystem = mozilla::intl::NumberingSystem::TryCreate(locale.get()); if (numberingSystem.isErr()) { intl::ReportInternalError(cx, numberingSystem.unwrapErr()); return false; } auto name = numberingSystem.inspect()->GetName(); if (name.isErr()) { intl::ReportInternalError(cx, name.unwrapErr()); return false; } JSString* jsname = NewStringCopy(cx, name.unwrap()); if (!jsname) { return false; } args.rval().setString(jsname); return true; } #if DEBUG || MOZ_SYSTEM_ICU bool js::intl_availableMeasurementUnits(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 0); RootedObject measurementUnits(cx, NewPlainObjectWithProto(cx, nullptr)); if (!measurementUnits) { return false; } auto units = mozilla::intl::MeasureUnit::GetAvailable(); if (units.isErr()) { intl::ReportInternalError(cx, units.unwrapErr()); return false; } Rooted unitAtom(cx); for (auto unit : units.unwrap()) { if (unit.isErr()) { intl::ReportInternalError(cx); return false; } auto unitIdentifier = unit.unwrap(); unitAtom = Atomize(cx, unitIdentifier.data(), unitIdentifier.size()); if (!unitAtom) { return false; } if (!DefineDataProperty(cx, measurementUnits, unitAtom->asPropertyName(), TrueHandleValue)) { return false; } } args.rval().setObject(*measurementUnits); return true; } #endif static constexpr size_t MaxUnitLength() { size_t length = 0; for (const auto& unit : mozilla::intl::simpleMeasureUnits) { length = std::max(length, std::char_traits::length(unit.name)); } return length * 2 + std::char_traits::length("-per-"); } static UniqueChars NumberFormatLocale(JSContext* cx, HandleObject internals) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { return nullptr; } // ICU expects numberingSystem as a Unicode locale extensions on locale. mozilla::intl::Locale tag; { Rooted locale(cx, value.toString()->ensureLinear(cx)); if (!locale) { return nullptr; } if (!intl::ParseLocale(cx, locale, tag)) { return nullptr; } } JS::RootedVector keywords(cx); if (!GetProperty(cx, internals, internals, cx->names().numberingSystem, &value)) { return nullptr; } { JSLinearString* numberingSystem = value.toString()->ensureLinear(cx); if (!numberingSystem) { return nullptr; } if (!keywords.emplaceBack("nu", numberingSystem)) { return nullptr; } } // |ApplyUnicodeExtensionToTag| applies the new keywords to the front of // the Unicode extension subtag. We're then relying on ICU to follow RFC // 6067, which states that any trailing keywords using the same key // should be ignored. if (!intl::ApplyUnicodeExtensionToTag(cx, tag, keywords)) { return nullptr; } intl::FormatBuffer buffer(cx); if (auto result = tag.ToString(buffer); result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return nullptr; } return buffer.extractStringZ(); } struct NumberFormatOptions : public mozilla::intl::NumberRangeFormatOptions { static_assert(std::is_base_of_v); char currencyChars[3] = {}; char unitChars[MaxUnitLength()] = {}; }; static bool FillNumberFormatOptions(JSContext* cx, HandleObject internals, NumberFormatOptions& options) { RootedValue value(cx); if (!GetProperty(cx, internals, internals, cx->names().style, &value)) { return false; } bool accountingSign = false; { JSLinearString* style = value.toString()->ensureLinear(cx); if (!style) { return false; } if (StringEqualsLiteral(style, "currency")) { if (!GetProperty(cx, internals, internals, cx->names().currency, &value)) { return false; } JSLinearString* currency = value.toString()->ensureLinear(cx); if (!currency) { return false; } MOZ_RELEASE_ASSERT( currency->length() == 3, "IsWellFormedCurrencyCode permits only length-3 strings"); MOZ_ASSERT(StringIsAscii(currency), "IsWellFormedCurrencyCode permits only ASCII strings"); CopyChars(reinterpret_cast(options.currencyChars), *currency); if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value)) { return false; } JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx); if (!currencyDisplay) { return false; } using CurrencyDisplay = mozilla::intl::NumberFormatOptions::CurrencyDisplay; CurrencyDisplay display; if (StringEqualsLiteral(currencyDisplay, "code")) { display = CurrencyDisplay::Code; } else if (StringEqualsLiteral(currencyDisplay, "symbol")) { display = CurrencyDisplay::Symbol; } else if (StringEqualsLiteral(currencyDisplay, "narrowSymbol")) { display = CurrencyDisplay::NarrowSymbol; } else { MOZ_ASSERT(StringEqualsLiteral(currencyDisplay, "name")); display = CurrencyDisplay::Name; } if (!GetProperty(cx, internals, internals, cx->names().currencySign, &value)) { return false; } JSLinearString* currencySign = value.toString()->ensureLinear(cx); if (!currencySign) { return false; } if (StringEqualsLiteral(currencySign, "accounting")) { accountingSign = true; } else { MOZ_ASSERT(StringEqualsLiteral(currencySign, "standard")); } options.mCurrency = mozilla::Some( std::make_pair(std::string_view(options.currencyChars, 3), display)); } else if (StringEqualsLiteral(style, "percent")) { options.mPercent = true; } else if (StringEqualsLiteral(style, "unit")) { if (!GetProperty(cx, internals, internals, cx->names().unit, &value)) { return false; } JSLinearString* unit = value.toString()->ensureLinear(cx); if (!unit) { return false; } size_t unit_str_length = unit->length(); MOZ_ASSERT(StringIsAscii(unit)); MOZ_RELEASE_ASSERT(unit_str_length <= MaxUnitLength()); CopyChars(reinterpret_cast(options.unitChars), *unit); if (!GetProperty(cx, internals, internals, cx->names().unitDisplay, &value)) { return false; } JSLinearString* unitDisplay = value.toString()->ensureLinear(cx); if (!unitDisplay) { return false; } using UnitDisplay = mozilla::intl::NumberFormatOptions::UnitDisplay; UnitDisplay display; if (StringEqualsLiteral(unitDisplay, "short")) { display = UnitDisplay::Short; } else if (StringEqualsLiteral(unitDisplay, "narrow")) { display = UnitDisplay::Narrow; } else { MOZ_ASSERT(StringEqualsLiteral(unitDisplay, "long")); display = UnitDisplay::Long; } options.mUnit = mozilla::Some(std::make_pair( std::string_view(options.unitChars, unit_str_length), display)); } else { MOZ_ASSERT(StringEqualsLiteral(style, "decimal")); } } bool hasMinimumSignificantDigits; if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits, &hasMinimumSignificantDigits)) { return false; } if (hasMinimumSignificantDigits) { if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, &value)) { return false; } uint32_t minimumSignificantDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, &value)) { return false; } uint32_t maximumSignificantDigits = AssertedCast(value.toInt32()); options.mSignificantDigits = mozilla::Some( std::make_pair(minimumSignificantDigits, maximumSignificantDigits)); } bool hasMinimumFractionDigits; if (!HasProperty(cx, internals, cx->names().minimumFractionDigits, &hasMinimumFractionDigits)) { return false; } if (hasMinimumFractionDigits) { if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, &value)) { return false; } uint32_t minimumFractionDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, &value)) { return false; } uint32_t maximumFractionDigits = AssertedCast(value.toInt32()); options.mFractionDigits = mozilla::Some( std::make_pair(minimumFractionDigits, maximumFractionDigits)); } if (!GetProperty(cx, internals, internals, cx->names().roundingPriority, &value)) { return false; } { JSLinearString* roundingPriority = value.toString()->ensureLinear(cx); if (!roundingPriority) { return false; } using RoundingPriority = mozilla::intl::NumberFormatOptions::RoundingPriority; RoundingPriority priority; if (StringEqualsLiteral(roundingPriority, "auto")) { priority = RoundingPriority::Auto; } else if (StringEqualsLiteral(roundingPriority, "morePrecision")) { priority = RoundingPriority::MorePrecision; } else { MOZ_ASSERT(StringEqualsLiteral(roundingPriority, "lessPrecision")); priority = RoundingPriority::LessPrecision; } options.mRoundingPriority = priority; } if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, &value)) { return false; } options.mMinIntegerDigits = mozilla::Some(AssertedCast(value.toInt32())); if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) { return false; } if (value.isString()) { JSLinearString* useGrouping = value.toString()->ensureLinear(cx); if (!useGrouping) { return false; } using Grouping = mozilla::intl::NumberFormatOptions::Grouping; Grouping grouping; if (StringEqualsLiteral(useGrouping, "auto")) { grouping = Grouping::Auto; } else if (StringEqualsLiteral(useGrouping, "always")) { grouping = Grouping::Always; } else { MOZ_ASSERT(StringEqualsLiteral(useGrouping, "min2")); grouping = Grouping::Min2; } options.mGrouping = grouping; } else { MOZ_ASSERT(value.isBoolean()); #ifdef NIGHTLY_BUILD // The caller passes the string "always" instead of |true| when the // NumberFormat V3 spec is being used. MOZ_ASSERT(value.toBoolean() == false); #endif using Grouping = mozilla::intl::NumberFormatOptions::Grouping; Grouping grouping; if (value.toBoolean()) { grouping = Grouping::Auto; } else { grouping = Grouping::Never; } options.mGrouping = grouping; } if (!GetProperty(cx, internals, internals, cx->names().notation, &value)) { return false; } { JSLinearString* notation = value.toString()->ensureLinear(cx); if (!notation) { return false; } using Notation = mozilla::intl::NumberFormatOptions::Notation; Notation style; if (StringEqualsLiteral(notation, "standard")) { style = Notation::Standard; } else if (StringEqualsLiteral(notation, "scientific")) { style = Notation::Scientific; } else if (StringEqualsLiteral(notation, "engineering")) { style = Notation::Engineering; } else { MOZ_ASSERT(StringEqualsLiteral(notation, "compact")); if (!GetProperty(cx, internals, internals, cx->names().compactDisplay, &value)) { return false; } JSLinearString* compactDisplay = value.toString()->ensureLinear(cx); if (!compactDisplay) { return false; } if (StringEqualsLiteral(compactDisplay, "short")) { style = Notation::CompactShort; } else { MOZ_ASSERT(StringEqualsLiteral(compactDisplay, "long")); style = Notation::CompactLong; } } options.mNotation = style; } if (!GetProperty(cx, internals, internals, cx->names().signDisplay, &value)) { return false; } { JSLinearString* signDisplay = value.toString()->ensureLinear(cx); if (!signDisplay) { return false; } using SignDisplay = mozilla::intl::NumberFormatOptions::SignDisplay; SignDisplay display; if (StringEqualsLiteral(signDisplay, "auto")) { if (accountingSign) { display = SignDisplay::Accounting; } else { display = SignDisplay::Auto; } } else if (StringEqualsLiteral(signDisplay, "never")) { display = SignDisplay::Never; } else if (StringEqualsLiteral(signDisplay, "always")) { if (accountingSign) { display = SignDisplay::AccountingAlways; } else { display = SignDisplay::Always; } } else if (StringEqualsLiteral(signDisplay, "exceptZero")) { if (accountingSign) { display = SignDisplay::AccountingExceptZero; } else { display = SignDisplay::ExceptZero; } } else { MOZ_ASSERT(StringEqualsLiteral(signDisplay, "negative")); if (accountingSign) { display = SignDisplay::AccountingNegative; } else { display = SignDisplay::Negative; } } options.mSignDisplay = display; } if (!GetProperty(cx, internals, internals, cx->names().roundingIncrement, &value)) { return false; } options.mRoundingIncrement = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().roundingMode, &value)) { return false; } { JSLinearString* roundingMode = value.toString()->ensureLinear(cx); if (!roundingMode) { return false; } using RoundingMode = mozilla::intl::NumberFormatOptions::RoundingMode; RoundingMode rounding; if (StringEqualsLiteral(roundingMode, "halfExpand")) { // "halfExpand" is the default mode, so we handle it first. rounding = RoundingMode::HalfExpand; } else if (StringEqualsLiteral(roundingMode, "ceil")) { rounding = RoundingMode::Ceil; } else if (StringEqualsLiteral(roundingMode, "floor")) { rounding = RoundingMode::Floor; } else if (StringEqualsLiteral(roundingMode, "expand")) { rounding = RoundingMode::Expand; } else if (StringEqualsLiteral(roundingMode, "trunc")) { rounding = RoundingMode::Trunc; } else if (StringEqualsLiteral(roundingMode, "halfCeil")) { rounding = RoundingMode::HalfCeil; } else if (StringEqualsLiteral(roundingMode, "halfFloor")) { rounding = RoundingMode::HalfFloor; } else if (StringEqualsLiteral(roundingMode, "halfTrunc")) { rounding = RoundingMode::HalfTrunc; } else { MOZ_ASSERT(StringEqualsLiteral(roundingMode, "halfEven")); rounding = RoundingMode::HalfEven; } options.mRoundingMode = rounding; } if (!GetProperty(cx, internals, internals, cx->names().trailingZeroDisplay, &value)) { return false; } { JSLinearString* trailingZeroDisplay = value.toString()->ensureLinear(cx); if (!trailingZeroDisplay) { return false; } if (StringEqualsLiteral(trailingZeroDisplay, "auto")) { options.mStripTrailingZero = false; } else { MOZ_ASSERT(StringEqualsLiteral(trailingZeroDisplay, "stripIfInteger")); options.mStripTrailingZero = true; } } return true; } /** * Returns a new mozilla::intl::Number[Range]Format with the locale and number * formatting options of the given NumberFormat, or a nullptr if * initialization failed. */ template static Formatter* NewNumberFormat(JSContext* cx, Handle numberFormat) { RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); if (!internals) { return nullptr; } UniqueChars locale = NumberFormatLocale(cx, internals); if (!locale) { return nullptr; } NumberFormatOptions options; if (!FillNumberFormatOptions(cx, internals, options)) { return nullptr; } options.mRangeCollapse = NumberFormatOptions::RangeCollapse::Auto; options.mRangeIdentityFallback = NumberFormatOptions::RangeIdentityFallback::Approximately; mozilla::Result, mozilla::intl::ICUError> result = Formatter::TryCreate(locale.get(), options); if (result.isOk()) { return result.unwrap().release(); } intl::ReportInternalError(cx, result.unwrapErr()); return nullptr; } static mozilla::intl::NumberFormat* GetOrCreateNumberFormat( JSContext* cx, Handle numberFormat) { // Obtain a cached mozilla::intl::NumberFormat object. mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter(); if (nf) { return nf; } nf = NewNumberFormat(cx, numberFormat); if (!nf) { return nullptr; } numberFormat->setNumberFormatter(nf); intl::AddICUCellMemory(numberFormat, NumberFormatObject::EstimatedMemoryUse); return nf; } static mozilla::intl::NumberRangeFormat* GetOrCreateNumberRangeFormat( JSContext* cx, Handle numberFormat) { // Obtain a cached mozilla::intl::NumberRangeFormat object. mozilla::intl::NumberRangeFormat* nrf = numberFormat->getNumberRangeFormatter(); if (nrf) { return nrf; } nrf = NewNumberFormat(cx, numberFormat); if (!nrf) { return nullptr; } numberFormat->setNumberRangeFormatter(nrf); intl::AddICUCellMemory(numberFormat, NumberFormatObject::EstimatedRangeFormatterMemoryUse); return nrf; } static FieldType GetFieldTypeForNumberPartType( mozilla::intl::NumberPartType type) { switch (type) { case mozilla::intl::NumberPartType::ApproximatelySign: return &JSAtomState::approximatelySign; case mozilla::intl::NumberPartType::Compact: return &JSAtomState::compact; case mozilla::intl::NumberPartType::Currency: return &JSAtomState::currency; case mozilla::intl::NumberPartType::Decimal: return &JSAtomState::decimal; case mozilla::intl::NumberPartType::ExponentInteger: return &JSAtomState::exponentInteger; case mozilla::intl::NumberPartType::ExponentMinusSign: return &JSAtomState::exponentMinusSign; case mozilla::intl::NumberPartType::ExponentSeparator: return &JSAtomState::exponentSeparator; case mozilla::intl::NumberPartType::Fraction: return &JSAtomState::fraction; case mozilla::intl::NumberPartType::Group: return &JSAtomState::group; case mozilla::intl::NumberPartType::Infinity: return &JSAtomState::infinity; case mozilla::intl::NumberPartType::Integer: return &JSAtomState::integer; case mozilla::intl::NumberPartType::Literal: return &JSAtomState::literal; case mozilla::intl::NumberPartType::MinusSign: return &JSAtomState::minusSign; case mozilla::intl::NumberPartType::Nan: return &JSAtomState::nan; case mozilla::intl::NumberPartType::Percent: return &JSAtomState::percentSign; case mozilla::intl::NumberPartType::PlusSign: return &JSAtomState::plusSign; case mozilla::intl::NumberPartType::Unit: return &JSAtomState::unit; } MOZ_ASSERT_UNREACHABLE( "unenumerated, undocumented format field returned by iterator"); return nullptr; } static FieldType GetFieldTypeForNumberPartSource( mozilla::intl::NumberPartSource source) { switch (source) { case mozilla::intl::NumberPartSource::Shared: return &JSAtomState::shared; case mozilla::intl::NumberPartSource::Start: return &JSAtomState::startRange; case mozilla::intl::NumberPartSource::End: return &JSAtomState::endRange; } MOZ_CRASH("unexpected number part source"); } enum class DisplayNumberPartSource : bool { No, Yes }; static bool FormattedNumberToParts(JSContext* cx, HandleString str, const mozilla::intl::NumberPartVector& parts, DisplayNumberPartSource displaySource, FieldType unitType, MutableHandleValue result) { size_t lastEndIndex = 0; RootedObject singlePart(cx); RootedValue propVal(cx); Rooted partsArray( cx, NewDenseFullyAllocatedArray(cx, parts.length())); if (!partsArray) { return false; } partsArray->ensureDenseInitializedLength(0, parts.length()); size_t index = 0; for (const auto& part : parts) { FieldType type = GetFieldTypeForNumberPartType(part.type); size_t endIndex = part.endIndex; MOZ_ASSERT(lastEndIndex < endIndex); singlePart = NewPlainObject(cx); if (!singlePart) { return false; } propVal.setString(cx->names().*type); if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) { return false; } JSLinearString* partSubstr = NewDependentString(cx, str, lastEndIndex, endIndex - lastEndIndex); if (!partSubstr) { return false; } propVal.setString(partSubstr); if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) { return false; } if (displaySource == DisplayNumberPartSource::Yes) { FieldType source = GetFieldTypeForNumberPartSource(part.source); propVal.setString(cx->names().*source); if (!DefineDataProperty(cx, singlePart, cx->names().source, propVal)) { return false; } } if (unitType != nullptr && type != &JSAtomState::literal) { propVal.setString(cx->names().*unitType); if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) { return false; } } partsArray->initDenseElement(index++, ObjectValue(*singlePart)); lastEndIndex = endIndex; } MOZ_ASSERT(index == parts.length()); MOZ_ASSERT(lastEndIndex == str->length(), "result array must partition the entire string"); result.setObject(*partsArray); return true; } bool js::intl::FormattedRelativeTimeToParts( JSContext* cx, HandleString str, const mozilla::intl::NumberPartVector& parts, FieldType relativeTimeUnit, MutableHandleValue result) { return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No, relativeTimeUnit, result); } // Return true if the string starts with "0[bBoOxX]", possibly skipping over // leading whitespace. template static bool IsNonDecimalNumber(mozilla::Range chars) { const CharT* end = chars.begin().get() + chars.length(); const CharT* start = SkipSpace(chars.begin().get(), end); if (end - start >= 2 && start[0] == '0') { CharT ch = start[1]; return ch == 'b' || ch == 'B' || ch == 'o' || ch == 'O' || ch == 'x' || ch == 'X'; } return false; } static bool IsNonDecimalNumber(JSLinearString* str) { JS::AutoCheckCannotGC nogc; return str->hasLatin1Chars() ? IsNonDecimalNumber(str->latin1Range(nogc)) : IsNonDecimalNumber(str->twoByteRange(nogc)); } static bool ToIntlMathematicalValue(JSContext* cx, MutableHandleValue value) { if (!ToPrimitive(cx, JSTYPE_NUMBER, value)) { return false; } // Maximum exponent supported by ICU. Exponents larger than this value will // cause ICU to report an error. // See also "intl/icu/source/i18n/decContext.h". constexpr int32_t maximumExponent = 999'999'999; // We further limit the maximum positive exponent to avoid spending multiple // seconds or even minutes in ICU when formatting large numbers. constexpr int32_t maximumPositiveExponent = 9'999'999; // Compute the maximum BigInt digit length from the maximum positive exponent. // // BigInts are stored with base |2 ** BigInt::DigitBits|, so we have: // // |maximumPositiveExponent| * Log_DigitBase(10) // = |maximumPositiveExponent| * Log2(10) / Log2(2 ** BigInt::DigitBits) // = |maximumPositiveExponent| * Log2(10) / BigInt::DigitBits // = 33219277.626945525... / BigInt::DigitBits constexpr size_t maximumBigIntLength = 33219277.626945525 / BigInt::DigitBits; if (!value.isString()) { if (!ToNumeric(cx, value)) { return false; } if (value.isBigInt() && value.toBigInt()->digitLength() > maximumBigIntLength) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_EXPONENT_TOO_LARGE); return false; } return true; } JSLinearString* str = value.toString()->ensureLinear(cx); if (!str) { return false; } // Parse the string as a number. double number = LinearStringToNumber(str); bool exponentTooLarge = false; if (std::isnan(number)) { // Set to NaN if the input can't be parsed as a number. value.setNaN(); } else if (IsNonDecimalNumber(str)) { // ICU doesn't accept non-decimal numbers, so we have to convert the input // into a base-10 string. MOZ_ASSERT(!mozilla::IsNegative(number), "non-decimal numbers can't be negative"); if (number < DOUBLE_INTEGRAL_PRECISION_LIMIT) { // Fast-path if we can guarantee there was no loss of precision. value.setDouble(number); } else { // For the slow-path convert the string into a BigInt. // StringToBigInt can't fail (other than OOM) when StringToNumber already // succeeded. RootedString rooted(cx, str); BigInt* bi; JS_TRY_VAR_OR_RETURN_FALSE(cx, bi, StringToBigInt(cx, rooted)); MOZ_ASSERT(bi); if (bi->digitLength() > maximumBigIntLength) { exponentTooLarge = true; } else { value.setBigInt(bi); } } } else { JS::AutoCheckCannotGC nogc; if (auto decimal = intl::DecimalNumber::from(str, nogc)) { if (decimal->isZero()) { // Normalize positive/negative zero. MOZ_ASSERT(number == 0); value.setDouble(number); } else if (decimal->exponentTooLarge() || std::abs(decimal->exponent()) >= maximumExponent || decimal->exponent() > maximumPositiveExponent) { exponentTooLarge = true; } } else { // If we can't parse the string as a decimal, it must be ±Infinity. MOZ_ASSERT(std::isinf(number)); MOZ_ASSERT(StringFindPattern(str, cx->names().Infinity, 0) >= 0); value.setDouble(number); } } if (exponentTooLarge) { // Throw an error if the exponent is too large. JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_EXPONENT_TOO_LARGE); return false; } return true; } // Return the number part of the input by removing leading and trailing // whitespace. template static mozilla::Span NumberPart(const CharT* chars, size_t length) { const CharT* start = chars; const CharT* end = chars + length; start = SkipSpace(start, end); // |SkipSpace| only supports forward iteration, so inline the backwards // iteration here. MOZ_ASSERT(start <= end); while (end > start && unicode::IsSpace(end[-1])) { end--; } // The number part is a non-empty, ASCII-only substring. MOZ_ASSERT(start < end); MOZ_ASSERT(mozilla::IsAscii(mozilla::Span(start, end))); return {start, end}; } static bool NumberPart(JSContext* cx, JSLinearString* str, const JS::AutoCheckCannotGC& nogc, JS::UniqueChars& latin1, std::string_view& result) { if (str->hasLatin1Chars()) { auto span = NumberPart( reinterpret_cast(str->latin1Chars(nogc)), str->length()); result = {span.data(), span.size()}; return true; } auto span = NumberPart(str->twoByteChars(nogc), str->length()); latin1.reset(JS::LossyTwoByteCharsToNewLatin1CharsZ(cx, span).c_str()); if (!latin1) { return false; } result = {latin1.get(), span.size()}; return true; } bool js::intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 3); MOZ_ASSERT(args[0].isObject()); #ifndef NIGHTLY_BUILD MOZ_ASSERT(args[1].isNumeric()); #endif MOZ_ASSERT(args[2].isBoolean()); Rooted numberFormat( cx, &args[0].toObject().as()); RootedValue value(cx, args[1]); #ifdef NIGHTLY_BUILD if (!ToIntlMathematicalValue(cx, &value)) { return false; } #endif mozilla::intl::NumberFormat* nf = GetOrCreateNumberFormat(cx, numberFormat); if (!nf) { return false; } // Actually format the number using ICUError = mozilla::intl::ICUError; bool formatToParts = args[2].toBoolean(); mozilla::Result result = mozilla::Err(ICUError::InternalError); mozilla::intl::NumberPartVector parts; if (value.isNumber()) { double num = value.toNumber(); if (formatToParts) { result = nf->formatToParts(num, parts); } else { result = nf->format(num); } } else if (value.isBigInt()) { RootedBigInt bi(cx, value.toBigInt()); int64_t num; if (BigInt::isInt64(bi, &num)) { if (formatToParts) { result = nf->formatToParts(num, parts); } else { result = nf->format(num); } } else { JSLinearString* str = BigInt::toString(cx, bi, 10); if (!str) { return false; } MOZ_RELEASE_ASSERT(str->hasLatin1Chars()); JS::AutoCheckCannotGC nogc; const char* chars = reinterpret_cast(str->latin1Chars(nogc)); if (formatToParts) { result = nf->formatToParts(std::string_view(chars, str->length()), parts); } else { result = nf->format(std::string_view(chars, str->length())); } } } else { JSLinearString* str = value.toString()->ensureLinear(cx); if (!str) { return false; } JS::AutoCheckCannotGC nogc; // Two-byte strings have to be copied into a separate |char| buffer. JS::UniqueChars latin1; std::string_view sv; if (!NumberPart(cx, str, nogc, latin1, sv)) { return false; } if (formatToParts) { result = nf->formatToParts(sv, parts); } else { result = nf->format(sv); } } if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } RootedString str(cx, NewStringCopy(cx, result.unwrap())); if (!str) { return false; } if (formatToParts) { return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::No, nullptr, args.rval()); } args.rval().setString(str); return true; } static JSLinearString* ToLinearString(JSContext* cx, HandleValue val) { // Special case to preserve negative zero. if (val.isDouble() && mozilla::IsNegativeZero(val.toDouble())) { constexpr std::string_view negativeZero = "-0"; return NewStringCopy(cx, negativeZero); } JSString* str = ToString(cx, val); return str ? str->ensureLinear(cx) : nullptr; }; bool js::intl_FormatNumberRange(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 4); MOZ_ASSERT(args[0].isObject()); MOZ_ASSERT(!args[1].isUndefined()); MOZ_ASSERT(!args[2].isUndefined()); MOZ_ASSERT(args[3].isBoolean()); Rooted numberFormat( cx, &args[0].toObject().as()); bool formatToParts = args[3].toBoolean(); RootedValue start(cx, args[1]); if (!ToIntlMathematicalValue(cx, &start)) { return false; } RootedValue end(cx, args[2]); if (!ToIntlMathematicalValue(cx, &end)) { return false; } // PartitionNumberRangePattern, step 1. if (start.isDouble() && std::isnan(start.toDouble())) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "start", "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange"); return false; } if (end.isDouble() && std::isnan(end.toDouble())) { JS_ReportErrorNumberASCII( cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "end", "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange"); return false; } using NumberRangeFormat = mozilla::intl::NumberRangeFormat; NumberRangeFormat* nf = GetOrCreateNumberRangeFormat(cx, numberFormat); if (!nf) { return false; } auto valueRepresentableAsDouble = [](const Value& val, double* num) { if (val.isNumber()) { *num = val.toNumber(); return true; } if (val.isBigInt()) { int64_t i64; if (BigInt::isInt64(val.toBigInt(), &i64) && i64 < int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT) && i64 > -int64_t(DOUBLE_INTEGRAL_PRECISION_LIMIT)) { *num = double(i64); return true; } } return false; }; // Actually format the number range. using ICUError = mozilla::intl::ICUError; mozilla::Result result = mozilla::Err(ICUError::InternalError); mozilla::intl::NumberPartVector parts; double numStart, numEnd; if (valueRepresentableAsDouble(start, &numStart) && valueRepresentableAsDouble(end, &numEnd)) { if (formatToParts) { result = nf->formatToParts(numStart, numEnd, parts); } else { result = nf->format(numStart, numEnd); } } else { Rooted strStart(cx, ToLinearString(cx, start)); if (!strStart) { return false; } Rooted strEnd(cx, ToLinearString(cx, end)); if (!strEnd) { return false; } JS::AutoCheckCannotGC nogc; // Two-byte strings have to be copied into a separate |char| buffer. JS::UniqueChars latin1Start; JS::UniqueChars latin1End; std::string_view svStart; if (!NumberPart(cx, strStart, nogc, latin1Start, svStart)) { return false; } std::string_view svEnd; if (!NumberPart(cx, strEnd, nogc, latin1End, svEnd)) { return false; } if (formatToParts) { result = nf->formatToParts(svStart, svEnd, parts); } else { result = nf->format(svStart, svEnd); } } if (result.isErr()) { intl::ReportInternalError(cx, result.unwrapErr()); return false; } RootedString str(cx, NewStringCopy(cx, result.unwrap())); if (!str) { return false; } if (formatToParts) { return FormattedNumberToParts(cx, str, parts, DisplayNumberPartSource::Yes, nullptr, args.rval()); } args.rval().setString(str); return true; }