diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/src/builtin/intl/NumberFormat.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/builtin/intl/NumberFormat.cpp')
-rw-r--r-- | js/src/builtin/intl/NumberFormat.cpp | 1467 |
1 files changed, 1467 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..dd01c9cfc0 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -0,0 +1,1467 @@ +/* -*- 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/UniquePtr.h" + +#include <algorithm> +#include <cstring> +#include <iterator> +#include <stddef.h> +#include <stdint.h> +#include <string> +#include <type_traits> + +#include "builtin/Array.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/LanguageTag.h" +#include "builtin/intl/MeasureUnitGenerated.h" +#include "builtin/intl/RelativeTimeFormat.h" +#include "builtin/intl/ScopedICUObject.h" +#include "ds/Sort.h" +#include "gc/FreeOp.h" +#include "js/CharacterEncoding.h" +#include "js/PropertySpec.h" +#include "js/RootingAPI.h" +#include "js/TypeDecls.h" +#include "js/Vector.h" +#include "unicode/udata.h" +#include "unicode/ufieldpositer.h" +#include "unicode/uformattedvalue.h" +#include "unicode/unum.h" +#include "unicode/unumberformatter.h" +#include "unicode/unumsys.h" +#include "unicode/ures.h" +#include "unicode/utypes.h" +#include "vm/BigIntType.h" +#include "vm/GlobalObject.h" +#include "vm/JSContext.h" +#include "vm/PlainObject.h" // js::PlainObject +#include "vm/SelfHosting.h" +#include "vm/Stack.h" +#include "vm/StringType.h" + +#include "vm/JSObject-inl.h" + +using namespace js; + +using mozilla::AssertedCast; +using mozilla::IsFinite; +using mozilla::IsNaN; +using mozilla::IsNegative; +using mozilla::SpecificNaN; + +using js::intl::CallICU; +using js::intl::DateTimeFormatOptions; +using js::intl::FieldType; +using js::intl::IcuLocale; + +const JSClassOps NumberFormatObject::classOps_ = { + nullptr, // addProperty + nullptr, // delProperty + nullptr, // enumerate + nullptr, // newEnumerate + nullptr, // resolve + nullptr, // mayResolve + NumberFormatObject::finalize, // finalize + nullptr, // call + nullptr, // hasInstance + 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), + 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) { + // 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(JSFreeOp* fop, JSObject* obj) { + MOZ_ASSERT(fop->onMainThread()); + + auto* numberFormat = &obj->as<NumberFormatObject>(); + UNumberFormatter* nf = numberFormat->getNumberFormatter(); + UFormattedNumber* formatted = numberFormat->getFormattedNumber(); + + if (nf) { + intl::RemoveICUCellMemory(fop, obj, NumberFormatObject::EstimatedMemoryUse); + + unumf_close(nf); + } + if (formatted) { + // UFormattedNumber memory tracked as part of UNumberFormatter. + + unumf_closeResult(formatted); + } +} + +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; + } + + UErrorCode status = U_ZERO_ERROR; + UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.get()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject<UNumberingSystem, unumsys_close> toClose(numbers); + + const char* name = unumsys_getName(numbers); + if (!name) { + intl::ReportInternalError(cx); + return false; + } + + JSString* jsname = NewStringCopyZ<CanGC>(cx, name); + if (!jsname) { + return false; + } + + args.rval().setString(jsname); + return true; +} + +#if DEBUG || MOZ_SYSTEM_ICU +class UResourceBundleDeleter { + public: + void operator()(UResourceBundle* aPtr) { ures_close(aPtr); } +}; + +using UniqueUResourceBundle = + mozilla::UniquePtr<UResourceBundle, UResourceBundleDeleter>; + +bool js::intl_availableMeasurementUnits(JSContext* cx, unsigned argc, + Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedObject measurementUnits( + cx, NewObjectWithGivenProto<PlainObject>(cx, nullptr)); + if (!measurementUnits) { + return false; + } + + // Lookup the available measurement units in the resource boundle of the root + // locale. + + static const char packageName[] = + U_ICUDATA_NAME U_TREE_SEPARATOR_STRING "unit"; + static const char rootLocale[] = ""; + + UErrorCode status = U_ZERO_ERROR; + UResourceBundle* rawRes = ures_open(packageName, rootLocale, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + UniqueUResourceBundle res(rawRes); + + UResourceBundle* rawUnits = + ures_getByKey(res.get(), "units", nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + UniqueUResourceBundle units(rawUnits); + + RootedAtom unitAtom(cx); + + int32_t unitsSize = ures_getSize(units.get()); + for (int32_t i = 0; i < unitsSize; i++) { + UResourceBundle* rawType = + ures_getByIndex(units.get(), i, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + UniqueUResourceBundle type(rawType); + + int32_t typeSize = ures_getSize(type.get()); + for (int32_t j = 0; j < typeSize; j++) { + UResourceBundle* rawSubtype = + ures_getByIndex(type.get(), j, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + UniqueUResourceBundle subtype(rawSubtype); + + const char* unitIdentifier = ures_getKey(subtype.get()); + + unitAtom = Atomize(cx, unitIdentifier, strlen(unitIdentifier)); + if (!unitAtom) { + return false; + } + if (!DefineDataProperty(cx, measurementUnits, unitAtom->asPropertyName(), + TrueHandleValue)) { + return false; + } + } + } + + args.rval().setObject(*measurementUnits); + return true; +} +#endif + +bool js::intl::NumberFormatterSkeleton::currency(JSLinearString* currency) { + MOZ_ASSERT(currency->length() == 3, + "IsWellFormedCurrencyCode permits only length-3 strings"); + + char16_t currencyChars[] = {currency->latin1OrTwoByteChar(0), + currency->latin1OrTwoByteChar(1), + currency->latin1OrTwoByteChar(2), '\0'}; + return append(u"currency/") && append(currencyChars) && append(' '); +} + +bool js::intl::NumberFormatterSkeleton::currencyDisplay( + CurrencyDisplay display) { + switch (display) { + case CurrencyDisplay::Code: + return appendToken(u"unit-width-iso-code"); + case CurrencyDisplay::Name: + return appendToken(u"unit-width-full-name"); + case CurrencyDisplay::Symbol: + // Default, no additional tokens needed. + return true; + case CurrencyDisplay::NarrowSymbol: + return appendToken(u"unit-width-narrow"); + } + MOZ_CRASH("unexpected currency display type"); +} + +static const MeasureUnit& FindSimpleMeasureUnit(const char* name) { + auto measureUnit = std::lower_bound( + std::begin(simpleMeasureUnits), std::end(simpleMeasureUnits), name, + [](const auto& measureUnit, const char* name) { + return strcmp(measureUnit.name, name) < 0; + }); + MOZ_ASSERT(measureUnit != std::end(simpleMeasureUnits), + "unexpected unit identifier: unit not found"); + MOZ_ASSERT(strcmp(measureUnit->name, name) == 0, + "unexpected unit identifier: wrong unit found"); + return *measureUnit; +} + +static constexpr size_t MaxUnitLength() { + size_t length = 0; + for (const auto& unit : simpleMeasureUnits) { + length = std::max(length, std::char_traits<char>::length(unit.name)); + } + return length * 2 + std::char_traits<char>::length("-per-"); +} + +bool js::intl::NumberFormatterSkeleton::unit(JSLinearString* unit) { + MOZ_RELEASE_ASSERT(unit->length() <= MaxUnitLength()); + + char unitChars[MaxUnitLength() + 1] = {}; + CopyChars(reinterpret_cast<Latin1Char*>(unitChars), *unit); + + auto appendUnit = [this](const MeasureUnit& unit) { + return append(unit.type, strlen(unit.type)) && append('-') && + append(unit.name, strlen(unit.name)); + }; + + // |unit| can be a compound unit identifier, separated by "-per-". + + static constexpr char separator[] = "-per-"; + if (char* p = strstr(unitChars, separator)) { + // Split into two strings. + p[0] = '\0'; + + auto& numerator = FindSimpleMeasureUnit(unitChars); + if (!append(u"measure-unit/") || !appendUnit(numerator) || !append(' ')) { + return false; + } + + auto& denominator = FindSimpleMeasureUnit(p + strlen(separator)); + if (!append(u"per-measure-unit/") || !appendUnit(denominator) || + !append(' ')) { + return false; + } + } else { + auto& simple = FindSimpleMeasureUnit(unitChars); + if (!append(u"measure-unit/") || !appendUnit(simple) || !append(' ')) { + return false; + } + } + return true; +} + +bool js::intl::NumberFormatterSkeleton::unitDisplay(UnitDisplay display) { + switch (display) { + case UnitDisplay::Short: + return appendToken(u"unit-width-short"); + case UnitDisplay::Narrow: + return appendToken(u"unit-width-narrow"); + case UnitDisplay::Long: + return appendToken(u"unit-width-full-name"); + } + MOZ_CRASH("unexpected unit display type"); +} + +bool js::intl::NumberFormatterSkeleton::percent() { + return appendToken(u"percent scale/100"); +} + +bool js::intl::NumberFormatterSkeleton::fractionDigits(uint32_t min, + uint32_t max) { + // Note: |min| can be zero here. + MOZ_ASSERT(min <= max); + return append('.') && appendN('0', min) && appendN('#', max - min) && + append(' '); +} + +bool js::intl::NumberFormatterSkeleton::integerWidth(uint32_t min) { + MOZ_ASSERT(min > 0); + return append(u"integer-width/+") && appendN('0', min) && append(' '); +} + +bool js::intl::NumberFormatterSkeleton::significantDigits(uint32_t min, + uint32_t max) { + MOZ_ASSERT(min > 0); + MOZ_ASSERT(min <= max); + return appendN('@', min) && appendN('#', max - min) && append(' '); +} + +bool js::intl::NumberFormatterSkeleton::useGrouping(bool on) { + return on || appendToken(u"group-off"); +} + +bool js::intl::NumberFormatterSkeleton::notation(Notation style) { + switch (style) { + case Notation::Standard: + // Default, no additional tokens needed. + return true; + case Notation::Scientific: + return appendToken(u"scientific"); + case Notation::Engineering: + return appendToken(u"engineering"); + case Notation::CompactShort: + return appendToken(u"compact-short"); + case Notation::CompactLong: + return appendToken(u"compact-long"); + } + MOZ_CRASH("unexpected notation style"); +} + +bool js::intl::NumberFormatterSkeleton::signDisplay(SignDisplay display) { + switch (display) { + case SignDisplay::Auto: + // Default, no additional tokens needed. + return true; + case SignDisplay::Always: + return appendToken(u"sign-always"); + case SignDisplay::Never: + return appendToken(u"sign-never"); + case SignDisplay::ExceptZero: + return appendToken(u"sign-except-zero"); + case SignDisplay::Accounting: + return appendToken(u"sign-accounting"); + case SignDisplay::AccountingAlways: + return appendToken(u"sign-accounting-always"); + case SignDisplay::AccountingExceptZero: + return appendToken(u"sign-accounting-except-zero"); + } + MOZ_CRASH("unexpected sign display type"); +} + +bool js::intl::NumberFormatterSkeleton::roundingModeHalfUp() { + return appendToken(u"rounding-mode-half-up"); +} + +UNumberFormatter* js::intl::NumberFormatterSkeleton::toFormatter( + JSContext* cx, const char* locale) { + UErrorCode status = U_ZERO_ERROR; + UNumberFormatter* nf = unumf_openForSkeletonAndLocale( + vector_.begin(), vector_.length(), locale, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + return nf; +} + +/** + * Returns a new UNumberFormatter with the locale and number formatting options + * of the given NumberFormat. + */ +static UNumberFormatter* NewUNumberFormatter( + JSContext* cx, Handle<NumberFormatObject*> numberFormat) { + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, numberFormat)); + if (!internals) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { + return nullptr; + } + + // ICU expects numberingSystem as a Unicode locale extensions on locale. + + intl::LanguageTag tag(cx); + { + JSLinearString* locale = value.toString()->ensureLinear(cx); + if (!locale) { + return nullptr; + } + + if (!intl::LanguageTagParser::parse(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; + } + + UniqueChars locale = tag.toStringZ(cx); + if (!locale) { + return nullptr; + } + + intl::NumberFormatterSkeleton skeleton(cx); + + if (!GetProperty(cx, internals, internals, cx->names().style, &value)) { + return nullptr; + } + + bool accountingSign = false; + { + JSLinearString* style = value.toString()->ensureLinear(cx); + if (!style) { + return nullptr; + } + + if (StringEqualsLiteral(style, "currency")) { + if (!GetProperty(cx, internals, internals, cx->names().currency, + &value)) { + return nullptr; + } + JSLinearString* currency = value.toString()->ensureLinear(cx); + if (!currency) { + return nullptr; + } + + if (!skeleton.currency(currency)) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, + &value)) { + return nullptr; + } + JSLinearString* currencyDisplay = value.toString()->ensureLinear(cx); + if (!currencyDisplay) { + return nullptr; + } + + using CurrencyDisplay = intl::NumberFormatterSkeleton::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 (!skeleton.currencyDisplay(display)) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().currencySign, + &value)) { + return nullptr; + } + JSLinearString* currencySign = value.toString()->ensureLinear(cx); + if (!currencySign) { + return nullptr; + } + + if (StringEqualsLiteral(currencySign, "accounting")) { + accountingSign = true; + } else { + MOZ_ASSERT(StringEqualsLiteral(currencySign, "standard")); + } + } else if (StringEqualsLiteral(style, "percent")) { + if (!skeleton.percent()) { + return nullptr; + } + } else if (StringEqualsLiteral(style, "unit")) { + if (!GetProperty(cx, internals, internals, cx->names().unit, &value)) { + return nullptr; + } + JSLinearString* unit = value.toString()->ensureLinear(cx); + if (!unit) { + return nullptr; + } + + if (!skeleton.unit(unit)) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().unitDisplay, + &value)) { + return nullptr; + } + JSLinearString* unitDisplay = value.toString()->ensureLinear(cx); + if (!unitDisplay) { + return nullptr; + } + + using UnitDisplay = intl::NumberFormatterSkeleton::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; + } + + if (!skeleton.unitDisplay(display)) { + return nullptr; + } + } else { + MOZ_ASSERT(StringEqualsLiteral(style, "decimal")); + } + } + + bool hasMinimumSignificantDigits; + if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits, + &hasMinimumSignificantDigits)) { + return nullptr; + } + + if (hasMinimumSignificantDigits) { + if (!GetProperty(cx, internals, internals, + cx->names().minimumSignificantDigits, &value)) { + return nullptr; + } + uint32_t minimumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumSignificantDigits, &value)) { + return nullptr; + } + uint32_t maximumSignificantDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!skeleton.significantDigits(minimumSignificantDigits, + maximumSignificantDigits)) { + return nullptr; + } + } + + bool hasMinimumFractionDigits; + if (!HasProperty(cx, internals, cx->names().minimumFractionDigits, + &hasMinimumFractionDigits)) { + return nullptr; + } + + if (hasMinimumFractionDigits) { + if (!GetProperty(cx, internals, internals, + cx->names().minimumFractionDigits, &value)) { + return nullptr; + } + uint32_t minimumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!GetProperty(cx, internals, internals, + cx->names().maximumFractionDigits, &value)) { + return nullptr; + } + uint32_t maximumFractionDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!skeleton.fractionDigits(minimumFractionDigits, + maximumFractionDigits)) { + return nullptr; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, + &value)) { + return nullptr; + } + uint32_t minimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32()); + + if (!skeleton.integerWidth(minimumIntegerDigits)) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) { + return nullptr; + } + if (!skeleton.useGrouping(value.toBoolean())) { + return nullptr; + } + + if (!GetProperty(cx, internals, internals, cx->names().notation, &value)) { + return nullptr; + } + + { + JSLinearString* notation = value.toString()->ensureLinear(cx); + if (!notation) { + return nullptr; + } + + using Notation = intl::NumberFormatterSkeleton::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 nullptr; + } + + JSLinearString* compactDisplay = value.toString()->ensureLinear(cx); + if (!compactDisplay) { + return nullptr; + } + + if (StringEqualsLiteral(compactDisplay, "short")) { + style = Notation::CompactShort; + } else { + MOZ_ASSERT(StringEqualsLiteral(compactDisplay, "long")); + style = Notation::CompactLong; + } + } + + if (!skeleton.notation(style)) { + return nullptr; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().signDisplay, &value)) { + return nullptr; + } + + { + JSLinearString* signDisplay = value.toString()->ensureLinear(cx); + if (!signDisplay) { + return nullptr; + } + + using SignDisplay = intl::NumberFormatterSkeleton::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 { + MOZ_ASSERT(StringEqualsLiteral(signDisplay, "exceptZero")); + if (accountingSign) { + display = SignDisplay::AccountingExceptZero; + } else { + display = SignDisplay::ExceptZero; + } + } + + if (!skeleton.signDisplay(display)) { + return nullptr; + } + } + + if (!skeleton.roundingModeHalfUp()) { + return nullptr; + } + + return skeleton.toFormatter(cx, locale.get()); +} + +static UFormattedNumber* NewUFormattedNumber(JSContext* cx) { + UErrorCode status = U_ZERO_ERROR; + UFormattedNumber* formatted = unumf_openResult(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + return formatted; +} + +static const UFormattedValue* PartitionNumberPattern( + JSContext* cx, const UNumberFormatter* nf, UFormattedNumber* formatted, + HandleValue x) { + UErrorCode status = U_ZERO_ERROR; + if (x.isNumber()) { + double num = x.toNumber(); + + // ICU incorrectly formats NaN values with the sign bit set, as if they + // were negative. Replace all NaNs with a single pattern with sign bit + // unset ("positive", that is) until ICU is fixed. + if (MOZ_UNLIKELY(IsNaN(num))) { + num = SpecificNaN<double>(0, 1); + } + + unumf_formatDouble(nf, num, formatted, &status); + } else { + RootedBigInt bi(cx, x.toBigInt()); + + int64_t num; + if (BigInt::isInt64(bi, &num)) { + unumf_formatInt(nf, num, formatted, &status); + } else { + JSLinearString* str = BigInt::toString<CanGC>(cx, bi, 10); + if (!str) { + return nullptr; + } + MOZ_ASSERT(str->hasLatin1Chars()); + + // Tell the analysis the |unumf_formatDecimal| function can't GC. + JS::AutoSuppressGCAnalysis nogc; + + const char* chars = reinterpret_cast<const char*>(str->latin1Chars(nogc)); + unumf_formatDecimal(nf, chars, str->length(), formatted, &status); + } + } + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + const UFormattedValue* formattedValue = + unumf_resultAsValue(formatted, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + return formattedValue; +} + +static JSString* FormattedNumberToString( + JSContext* cx, const UFormattedValue* formattedValue) { + UErrorCode status = U_ZERO_ERROR; + int32_t strLength; + const char16_t* str = ufmtval_getString(formattedValue, &strLength, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + return NewStringCopyN<CanGC>(cx, str, AssertedCast<uint32_t>(strLength)); +} + +static bool FormatNumeric(JSContext* cx, const UNumberFormatter* nf, + UFormattedNumber* formatted, HandleValue x, + MutableHandleValue result) { + const UFormattedValue* formattedValue = + PartitionNumberPattern(cx, nf, formatted, x); + if (!formattedValue) { + return false; + } + + JSString* str = FormattedNumberToString(cx, formattedValue); + if (!str) { + return false; + } + + result.setString(str); + return true; +} + +enum class FormattingType { ForUnit, NotForUnit }; + +static FieldType GetFieldTypeForNumberField(UNumberFormatFields fieldName, + HandleValue x, + FormattingType formattingType) { + // See intl/icu/source/i18n/unicode/unum.h for a detailed field list. This + // list is deliberately exhaustive: cases might have to be added/removed if + // this code is compiled with a different ICU with more UNumberFormatFields + // enum initializers. Please guard such cases with appropriate ICU + // version-testing #ifdefs, should cross-version divergence occur. + switch (fieldName) { + case UNUM_INTEGER_FIELD: + if (x.isNumber()) { + double d = x.toNumber(); + if (IsNaN(d)) { + return &JSAtomState::nan; + } + if (!IsFinite(d)) { + return &JSAtomState::infinity; + } + } + return &JSAtomState::integer; + + case UNUM_GROUPING_SEPARATOR_FIELD: + return &JSAtomState::group; + + case UNUM_DECIMAL_SEPARATOR_FIELD: + return &JSAtomState::decimal; + + case UNUM_FRACTION_FIELD: + return &JSAtomState::fraction; + + case UNUM_SIGN_FIELD: { + // We coerce all NaNs to one with the sign bit unset, so all NaNs are + // positive in our implementation. + bool isNegative = x.isNumber() + ? !IsNaN(x.toNumber()) && IsNegative(x.toNumber()) + : x.toBigInt()->isNegative(); + return isNegative ? &JSAtomState::minusSign : &JSAtomState::plusSign; + } + + case UNUM_PERCENT_FIELD: + // Percent fields are returned as "unit" elements when the number + // formatter's style is "unit". + if (formattingType == FormattingType::ForUnit) { + return &JSAtomState::unit; + } + return &JSAtomState::percentSign; + + case UNUM_CURRENCY_FIELD: + return &JSAtomState::currency; + + case UNUM_PERMILL_FIELD: + MOZ_ASSERT_UNREACHABLE( + "unexpected permill field found, even though " + "we don't use any user-defined patterns that " + "would require a permill field"); + break; + + case UNUM_EXPONENT_SYMBOL_FIELD: + return &JSAtomState::exponentSeparator; + + case UNUM_EXPONENT_SIGN_FIELD: + return &JSAtomState::exponentMinusSign; + + case UNUM_EXPONENT_FIELD: + return &JSAtomState::exponentInteger; + + case UNUM_MEASURE_UNIT_FIELD: + return &JSAtomState::unit; + + case UNUM_COMPACT_FIELD: + return &JSAtomState::compact; + +#ifndef U_HIDE_DEPRECATED_API + case UNUM_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE( + "format field sentinel value returned by iterator!"); + break; +#endif + } + + MOZ_ASSERT_UNREACHABLE( + "unenumerated, undocumented format field returned by iterator"); + return nullptr; +} + +struct Field { + uint32_t begin; + uint32_t end; + FieldType type; + + // Needed for vector-resizing scratch space. + Field() = default; + + Field(uint32_t begin, uint32_t end, FieldType type) + : begin(begin), end(end), type(type) {} +}; + +class NumberFormatFields { + using FieldsVector = Vector<Field, 16>; + + FieldsVector fields_; + + public: + explicit NumberFormatFields(JSContext* cx) : fields_(cx) {} + + MOZ_MUST_USE bool append(FieldType type, int32_t begin, int32_t end); + + MOZ_MUST_USE ArrayObject* toArray(JSContext* cx, + JS::HandleString overallResult, + FieldType unitType); +}; + +bool NumberFormatFields::append(FieldType type, int32_t begin, int32_t end) { + MOZ_ASSERT(begin >= 0); + MOZ_ASSERT(end >= 0); + MOZ_ASSERT(begin < end, "erm, aren't fields always non-empty?"); + + return fields_.emplaceBack(uint32_t(begin), uint32_t(end), type); +} + +ArrayObject* NumberFormatFields::toArray(JSContext* cx, + HandleString overallResult, + FieldType unitType) { + // Merge sort the fields vector. Expand the vector to have scratch space for + // performing the sort. + size_t fieldsLen = fields_.length(); + if (!fields_.growByUninitialized(fieldsLen)) { + return nullptr; + } + + MOZ_ALWAYS_TRUE(MergeSort( + fields_.begin(), fieldsLen, fields_.begin() + fieldsLen, + [](const Field& left, const Field& right, bool* lessOrEqual) { + // Sort first by begin index, then to place + // enclosing fields before nested fields. + *lessOrEqual = left.begin < right.begin || + (left.begin == right.begin && left.end > right.end); + return true; + })); + + // Delete the elements in the scratch space. + fields_.shrinkBy(fieldsLen); + + // Then iterate over the sorted field list to generate a sequence of parts + // (what ECMA-402 actually exposes). A part is a maximal character sequence + // entirely within no field or a single most-nested field. + // + // Diagrams may be helpful to illustrate how fields map to parts. Consider + // formatting -19,766,580,028,249.41, the US national surplus (negative + // because it's actually a debt) on October 18, 2016. + // + // var options = + // { style: "currency", currency: "USD", currencyDisplay: "name" }; + // var usdFormatter = new Intl.NumberFormat("en-US", options); + // usdFormatter.format(-19766580028249.41); + // + // The formatted result is "-19,766,580,028,249.41 US dollars". ICU + // identifies these fields in the string: + // + // UNUM_GROUPING_SEPARATOR_FIELD + // | + // UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD + // | __________/| | + // | / | | | | + // "-19,766,580,028,249.41 US dollars" + // \________________/ |/ \_______/ + // | | | + // UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD + // | + // UNUM_FRACTION_FIELD + // + // These fields map to parts as follows: + // + // integer decimal + // _____|________ | + // / /| |\ |\ |\ | literal + // /| / | | \ | \ | \| | + // "-19,766,580,028,249.41 US dollars" + // | \___|___|___/ |/ \________/ + // | | | | + // | group | currency + // | | + // minusSign fraction + // + // The sign is a part. Each comma is a part, splitting the integer field + // into parts for trillions/billions/&c. digits. The decimal point is a + // part. Cents are a part. The space between cents and currency is a part + // (outside any field). Last, the currency field is a part. + // + // Because parts fully partition the formatted string, we only track the + // end of each part -- the beginning is implicitly the last part's end. + struct Part { + uint32_t end; + FieldType type; + }; + + class PartGenerator { + // The fields in order from start to end, then least to most nested. + const FieldsVector& fields; + + // Index of the current field, in |fields|, being considered to + // determine part boundaries. |lastEnd <= fields[index].begin| is an + // invariant. + size_t index; + + // The end index of the last part produced, always less than or equal + // to |limit|, strictly increasing. + uint32_t lastEnd; + + // The length of the overall formatted string. + const uint32_t limit; + + Vector<size_t, 4> enclosingFields; + + void popEnclosingFieldsEndingAt(uint32_t end) { + MOZ_ASSERT_IF(enclosingFields.length() > 0, + fields[enclosingFields.back()].end >= end); + + while (enclosingFields.length() > 0 && + fields[enclosingFields.back()].end == end) { + enclosingFields.popBack(); + } + } + + bool nextPartInternal(Part* part) { + size_t len = fields.length(); + MOZ_ASSERT(index <= len); + + // If we're out of fields, all that remains are part(s) consisting + // of trailing portions of enclosing fields, and maybe a final + // literal part. + if (index == len) { + if (enclosingFields.length() > 0) { + const auto& enclosing = fields[enclosingFields.popCopy()]; + part->end = enclosing.end; + part->type = enclosing.type; + + // If additional enclosing fields end where this part ends, + // pop them as well. + popEnclosingFieldsEndingAt(part->end); + } else { + part->end = limit; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise we still have a field to process. + const Field* current = &fields[index]; + MOZ_ASSERT(lastEnd <= current->begin); + MOZ_ASSERT(current->begin < current->end); + + // But first, deal with inter-field space. + if (lastEnd < current->begin) { + if (enclosingFields.length() > 0) { + // Space between fields, within an enclosing field, is part + // of that enclosing field, until the start of the current + // field or the end of the enclosing field, whichever is + // earlier. + const auto& enclosing = fields[enclosingFields.back()]; + part->end = std::min(enclosing.end, current->begin); + part->type = enclosing.type; + popEnclosingFieldsEndingAt(part->end); + } else { + // If there's no enclosing field, the space is a literal. + part->end = current->begin; + part->type = &JSAtomState::literal; + } + + return true; + } + + // Otherwise, the part spans a prefix of the current field. Find + // the most-nested field containing that prefix. + const Field* next; + do { + current = &fields[index]; + + // If the current field is last, the part extends to its end. + if (++index == len) { + part->end = current->end; + part->type = current->type; + return true; + } + + next = &fields[index]; + MOZ_ASSERT(current->begin <= next->begin); + MOZ_ASSERT(current->begin < next->end); + + // If the next field nests within the current field, push an + // enclosing field. (If there are no nested fields, don't + // bother pushing a field that'd be immediately popped.) + if (current->end > next->begin) { + if (!enclosingFields.append(index - 1)) { + return false; + } + } + + // Do so until the next field begins after this one. + } while (current->begin == next->begin); + + part->type = current->type; + + if (current->end <= next->begin) { + // The next field begins after the current field ends. Therefore + // the current part ends at the end of the current field. + part->end = current->end; + popEnclosingFieldsEndingAt(part->end); + } else { + // The current field encloses the next one. The current part + // ends where the next field/part will start. + part->end = next->begin; + } + + return true; + } + + public: + PartGenerator(JSContext* cx, const FieldsVector& vec, uint32_t limit) + : fields(vec), + index(0), + lastEnd(0), + limit(limit), + enclosingFields(cx) {} + + bool nextPart(bool* hasPart, Part* part) { + // There are no parts left if we've partitioned the entire string. + if (lastEnd == limit) { + MOZ_ASSERT(enclosingFields.length() == 0); + *hasPart = false; + return true; + } + + if (!nextPartInternal(part)) { + return false; + } + + *hasPart = true; + lastEnd = part->end; + return true; + } + }; + + // Finally, generate the result array. + size_t lastEndIndex = 0; + RootedObject singlePart(cx); + RootedValue propVal(cx); + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) { + return nullptr; + } + + PartGenerator gen(cx, fields_, overallResult->length()); + do { + bool hasPart; + Part part; + if (!gen.nextPart(&hasPart, &part)) { + return nullptr; + } + + if (!hasPart) { + break; + } + + FieldType type = part.type; + size_t endIndex = part.end; + + MOZ_ASSERT(lastEndIndex < endIndex); + + singlePart = NewBuiltinClassInstance<PlainObject>(cx); + if (!singlePart) { + return nullptr; + } + + propVal.setString(cx->names().*type); + if (!DefineDataProperty(cx, singlePart, cx->names().type, propVal)) { + return nullptr; + } + + JSLinearString* partSubstr = NewDependentString( + cx, overallResult, lastEndIndex, endIndex - lastEndIndex); + if (!partSubstr) { + return nullptr; + } + + propVal.setString(partSubstr); + if (!DefineDataProperty(cx, singlePart, cx->names().value, propVal)) { + return nullptr; + } + + if (unitType != nullptr && type != &JSAtomState::literal) { + propVal.setString(cx->names().*unitType); + if (!DefineDataProperty(cx, singlePart, cx->names().unit, propVal)) { + return nullptr; + } + } + + if (!NewbornArrayPush(cx, partsArray, ObjectValue(*singlePart))) { + return nullptr; + } + + lastEndIndex = endIndex; + } while (true); + + MOZ_ASSERT(lastEndIndex == overallResult->length(), + "result array must partition the entire string"); + + return partsArray; +} + +static bool FormattedNumberToParts(JSContext* cx, + const UFormattedValue* formattedValue, + HandleValue number, + FieldType relativeTimeUnit, + FormattingType formattingType, + MutableHandleValue result) { + MOZ_ASSERT(number.isNumeric()); + + RootedString overallResult(cx, FormattedNumberToString(cx, formattedValue)); + if (!overallResult) { + return false; + } + + UErrorCode status = U_ZERO_ERROR; + UConstrainedFieldPosition* fpos = ucfpos_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject<UConstrainedFieldPosition, ucfpos_close> toCloseFpos(fpos); + + // We're only interested in UFIELD_CATEGORY_NUMBER fields. + ucfpos_constrainCategory(fpos, UFIELD_CATEGORY_NUMBER, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + // Vacuum up fields in the overall formatted string. + + NumberFormatFields fields(cx); + + while (true) { + bool hasMore = ufmtval_nextPosition(formattedValue, fpos, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + if (!hasMore) { + break; + } + + int32_t field = ucfpos_getField(fpos, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + int32_t beginIndex, endIndex; + ucfpos_getIndexes(fpos, &beginIndex, &endIndex, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(field), + number, formattingType); + + if (!fields.append(type, beginIndex, endIndex)) { + return false; + } + } + + ArrayObject* array = fields.toArray(cx, overallResult, relativeTimeUnit); + if (!array) { + return false; + } + + result.setObject(*array); + return true; +} + +bool js::intl::FormattedRelativeTimeToParts( + JSContext* cx, const UFormattedValue* formattedValue, double timeValue, + FieldType relativeTimeUnit, MutableHandleValue result) { + Value tval = DoubleValue(timeValue); + return FormattedNumberToParts( + cx, formattedValue, HandleValue::fromMarkedLocation(&tval), + relativeTimeUnit, FormattingType::NotForUnit, result); +} + +static bool FormatNumericToParts(JSContext* cx, const UNumberFormatter* nf, + UFormattedNumber* formatted, HandleValue x, + FormattingType formattingType, + MutableHandleValue result) { + const UFormattedValue* formattedValue = + PartitionNumberPattern(cx, nf, formatted, x); + if (!formattedValue) { + return false; + } + + return FormattedNumberToParts(cx, formattedValue, x, nullptr, formattingType, + result); +} + +bool js::intl_FormatNumber(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].isNumeric()); + MOZ_ASSERT(args[2].isBoolean()); + MOZ_ASSERT(args[3].isBoolean()); + + Rooted<NumberFormatObject*> numberFormat( + cx, &args[0].toObject().as<NumberFormatObject>()); + + // Obtain a cached UNumberFormatter object. + UNumberFormatter* nf = numberFormat->getNumberFormatter(); + if (!nf) { + nf = NewUNumberFormatter(cx, numberFormat); + if (!nf) { + return false; + } + numberFormat->setNumberFormatter(nf); + + intl::AddICUCellMemory(numberFormat, + NumberFormatObject::EstimatedMemoryUse); + } + + // Obtain a cached UFormattedNumber object. + UFormattedNumber* formatted = numberFormat->getFormattedNumber(); + if (!formatted) { + formatted = NewUFormattedNumber(cx); + if (!formatted) { + return false; + } + numberFormat->setFormattedNumber(formatted); + + // UFormattedNumber memory tracked as part of UNumberFormatter. + } + + // Use the UNumberFormatter to actually format the number. + if (args[2].toBoolean()) { + FormattingType formattingType = args[3].toBoolean() + ? FormattingType::ForUnit + : FormattingType::NotForUnit; + return FormatNumericToParts(cx, nf, formatted, args[1], formattingType, + args.rval()); + } + + return FormatNumeric(cx, nf, formatted, args[1], args.rval()); +} |