Source code

Revision control

Other Tools

/* -*- 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/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 <cstring>
#include <iterator>
#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/MeasureUnitGenerated.h"
#include "builtin/intl/RelativeTimeFormat.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 "util/Text.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/WellKnownAtom.h" // js_*_str
#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, // 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),
#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) {
// 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>();
mozilla::intl::NumberFormat* nf = numberFormat->getNumberFormatter();
mozilla::intl::NumberRangeFormat* nrf =
numberFormat->getNumberRangeFormatter();
if (nf) {
intl::RemoveICUCellMemory(fop, obj, NumberFormatObject::EstimatedMemoryUse);
// This was allocated using `new` in mozilla::intl::NumberFormat, so we
// delete here.
delete nf;
}
if (nrf) {
intl::RemoveICUCellMemory(fop, 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;
}
RootedAtom 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 : 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;
{
RootedLinearString 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);
RootedArrayObject 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,
double* numberApproximation = nullptr) {
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;
}
// Call StringToNumber to validate the input can be parsed as a number.
double number;
if (!StringToNumber(cx, str, &number)) {
return false;
}
if (numberApproximation) {
*numberApproximation = number;
}
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;
};
static bool ValidateNumberRange(JSContext* cx, MutableHandleValue start,
double startApprox, MutableHandleValue end,
double endApprox, bool formatToParts) {
static auto isSpecificDouble = [](const Value& val, auto fn) {
return val.isDouble() && fn(val.toDouble());
};
static auto isNaN = [](const Value& val) {
return isSpecificDouble(val, mozilla::IsNaN<double>);
};
static auto isPositiveInfinity = [](const Value& val) {
return isSpecificDouble(
val, [](double num) { return num > 0 && mozilla::IsInfinite(num); });
};
static auto isNegativeInfinity = [](const Value& val) {
return isSpecificDouble(
val, [](double num) { return num < 0 && mozilla::IsInfinite(num); });
};
static auto isNegativeZero = [](const Value& val) {
return isSpecificDouble(val, mozilla::IsNegativeZero<double>);
};
static auto isMathematicalValue = [](const Value& val) {
// |ToIntlMathematicalValue()| normalizes non-finite values and negative
// zero to Double values, so any string is guaranteed to be a mathematical
// value at this point.
if (!val.isDouble()) {
return true;
}
double num = val.toDouble();
return mozilla::IsFinite(num) && !mozilla::IsNegativeZero(num);
};
static auto isPositiveOrZero = [](const Value& val, double approx) {
MOZ_ASSERT(isMathematicalValue(val));
if (val.isNumber()) {
return val.toNumber() >= 0;
}
if (val.isBigInt()) {
return !val.toBigInt()->isNegative();
}
return approx >= 0;
};
auto throwRangeError = [&]() {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_START_AFTER_END_NUMBER,
"NumberFormat", formatToParts ? "formatRangeToParts" : "formatRange");
return false;
};
// PartitionNumberRangePattern, step 1.
if (isNaN(start)) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "start",
formatToParts ? "formatRangeToParts" : "formatRange");
return false;
}
if (isNaN(end)) {
JS_ReportErrorNumberASCII(
cx, GetErrorMessage, nullptr, JSMSG_NAN_NUMBER_RANGE, "end",
formatToParts ? "formatRangeToParts" : "formatRange");
return false;
}
// Make sure |start| and |end| can be correctly classified.
MOZ_ASSERT(isMathematicalValue(start) || isNegativeZero(start) ||
isNegativeInfinity(start) || isPositiveInfinity(start));
MOZ_ASSERT(isMathematicalValue(end) || isNegativeZero(end) ||
isNegativeInfinity(end) || isPositiveInfinity(end));
// PartitionNumberRangePattern, step 2.
if (isMathematicalValue(start)) {
// PartitionNumberRangePattern, step 2.a.
if (isMathematicalValue(end)) {
if (!start.isString() && !end.isString()) {
MOZ_ASSERT(start.isNumeric() && end.isNumeric());
bool isLessThan;
if (!LessThan(cx, end, start, &isLessThan)) {
return false;
}
if (isLessThan) {
return throwRangeError();
}
} else {
// |startApprox| and |endApprox| are only initially computed for string
// numbers.
if (start.isNumber()) {
startApprox = start.toNumber();
} else if (start.isBigInt()) {
startApprox = BigInt::numberValue(start.toBigInt());
}
if (end.isNumber()) {
endApprox = end.toNumber();
} else if (end.isBigInt()) {
endApprox = BigInt::numberValue(end.toBigInt());
}
// If the approximation is smaller, the actual value is definitely
// smaller, too.
if (endApprox < startApprox) {
return throwRangeError();
}
// If both approximations are equal to each other, we have to perform
// more work.
if (endApprox == startApprox) {
RootedLinearString strStart(cx, ToLinearString(cx, start));
if (!strStart) {
return false;
}
RootedLinearString strEnd(cx, ToLinearString(cx, end));
if (!strEnd) {
return false;
}
bool endLessThanStart;
{
JS::AutoCheckCannotGC nogc;
auto decStart = intl::DecimalNumber::from(strStart, nogc);
MOZ_ASSERT(decStart);
auto decEnd = intl::DecimalNumber::from(strEnd, nogc);
MOZ_ASSERT(decEnd);
endLessThanStart = decEnd->compareTo(*decStart) < 0;
}
if (endLessThanStart) {
return throwRangeError();
}
// If either value is a string, we end up passing both values as
// strings to the formatter. So let's save the string representation
// here, because then we don't have to recompute them later on.
start.setString(strStart);
end.setString(strEnd);
}
}
}
// PartitionNumberRangePattern, step 2.b.
else if (isNegativeInfinity(end)) {
return throwRangeError();
}
// PartitionNumberRangePattern, step 2.c.
else if (isNegativeZero(end)) {
if (isPositiveOrZero(start, startApprox)) {
return throwRangeError();
}
}
// No range restrictions when the end is positive infinity.
else {
MOZ_ASSERT(isPositiveInfinity(end));
}
}
// PartitionNumberRangePattern, step 3.
else if (isPositiveInfinity(start)) {
// PartitionNumberRangePattern, steps 3.a-c.
if (!isPositiveInfinity(end)) {
return throwRangeError();
}
}
// PartitionNumberRangePattern, step 4.
else if (isNegativeZero(start)) {
// PartitionNumberRangePattern, step 4.a.
if (isMathematicalValue(end)) {
if (!isPositiveOrZero(end, endApprox)) {
return throwRangeError();
}
}
// PartitionNumberRangePattern, step 4.b.
else if (isNegativeInfinity(end)) {
return throwRangeError();
}
// No range restrictions when the end is negative zero or positive infinity.
else {
MOZ_ASSERT(isNegativeZero(end) || isPositiveInfinity(end));
}
}
// No range restrictions when the start is negative infinity.
else {
MOZ_ASSERT(isNegativeInfinity(start));
}
return true;
}
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]);
double startApprox = mozilla::UnspecifiedNaN<double>();
if (!ToIntlMathematicalValue(cx, &start, &startApprox)) {
return false;
}
RootedValue end(cx, args[2]);
double endApprox = mozilla::UnspecifiedNaN<double>();
if (!ToIntlMathematicalValue(cx, &end, &endApprox)) {
return false;
}
if (!ValidateNumberRange(cx, &start, startApprox, &end, endApprox,
formatToParts)) {
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);