diff options
Diffstat (limited to 'js/src/builtin/intl/NumberFormat.cpp')
-rw-r--r-- | js/src/builtin/intl/NumberFormat.cpp | 1374 |
1 files changed, 1374 insertions, 0 deletions
diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp new file mode 100644 index 0000000000..21b0db030e --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -0,0 +1,1374 @@ +/* -*- 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 <algorithm> +#include <stddef.h> +#include <stdint.h> +#include <string> +#include <string_view> +#include <type_traits> + +#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<NumberFormat, 0, gc::AllocKind::FUNCTION>, + GenericCreatePrototype<NumberFormatObject>, + 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<NumberFormatObject*> numberFormat(cx); + numberFormat = NewObjectWithClassProto<NumberFormatObject>(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<NumberFormatObject>(); + 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<CanGC>(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<JSAtom*> 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<char>::length(unit.name)); + } + return length * 2 + std::char_traits<char>::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<JSLinearString*> locale(cx, value.toString()->ensureLinear(cx)); + if (!locale) { + return nullptr; + } + + if (!intl::ParseLocale(cx, locale, tag)) { + return nullptr; + } + } + + JS::RootedVector<intl::UnicodeExtensionKeyword> 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<char> 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<mozilla::intl::NumberFormatOptions, + mozilla::intl::NumberRangeFormatOptions>); + + 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<Latin1Char*>(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<Latin1Char*>(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<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumSignificantDigits, &value)) { + return false; + } + uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(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<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumFractionDigits, &value)) { + return false; + } + uint32_t maximumFractionDigits = AssertedCast<uint32_t>(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<uint32_t>(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<uint32_t>(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 <class Formatter> +static Formatter* NewNumberFormat(JSContext* cx, + Handle<NumberFormatObject*> 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::UniquePtr<Formatter>, 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<NumberFormatObject*> numberFormat) { + // Obtain a cached mozilla::intl::NumberFormat object. + mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter(); + if (nf) { + return nf; + } + + nf = NewNumberFormat<mozilla::intl::NumberFormat>(cx, numberFormat); + if (!nf) { + return nullptr; + } + numberFormat->setNumberFormatter(nf); + + intl::AddICUCellMemory(numberFormat, NumberFormatObject::EstimatedMemoryUse); + return nf; +} + +static mozilla::intl::NumberRangeFormat* GetOrCreateNumberRangeFormat( + JSContext* cx, Handle<NumberFormatObject*> numberFormat) { + // Obtain a cached mozilla::intl::NumberRangeFormat object. + mozilla::intl::NumberRangeFormat* nrf = + numberFormat->getNumberRangeFormatter(); + if (nrf) { + return nrf; + } + + nrf = NewNumberFormat<mozilla::intl::NumberRangeFormat>(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<ArrayObject*> 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 <typename CharT> +static bool IsNonDecimalNumber(mozilla::Range<const CharT> 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 (mozilla::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(mozilla::IsInfinite(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 <typename CharT> +static mozilla::Span<const CharT> 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<const char*>(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<NumberFormatObject*> numberFormat( + cx, &args[0].toObject().as<NumberFormatObject>()); + + 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<std::u16string_view, ICUError> 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<CanGC>(cx, bi, 10); + if (!str) { + return false; + } + MOZ_RELEASE_ASSERT(str->hasLatin1Chars()); + + JS::AutoCheckCannotGC nogc; + + const char* chars = reinterpret_cast<const char*>(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<CanGC>(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<CanGC>(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<NumberFormatObject*> numberFormat( + cx, &args[0].toObject().as<NumberFormatObject>()); + 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() && mozilla::IsNaN(start.toDouble())) { + JS_ReportErrorNumberASCII( + cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "start", + "NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange"); + return false; + } + if (end.isDouble() && mozilla::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<std::u16string_view, ICUError> 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<JSLinearString*> strStart(cx, ToLinearString(cx, start)); + if (!strStart) { + return false; + } + + Rooted<JSLinearString*> 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<CanGC>(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; +} |