mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-10-25 00:27:29 +09:00
Issue #2046 - Move Intl.NumberFormat functionality into builtin/intl/NumberFormat.*
This commit is contained in:
parent
eee13d2653
commit
e7565ff1ae
9 changed files with 1025 additions and 936 deletions
|
|
@ -23,21 +23,11 @@
|
||||||
#include "jsobj.h"
|
#include "jsobj.h"
|
||||||
|
|
||||||
#include "builtin/intl/CommonFunctions.h"
|
#include "builtin/intl/CommonFunctions.h"
|
||||||
|
#include "builtin/intl/ICUHeader.h"
|
||||||
|
#include "builtin/intl/NumberFormat.h"
|
||||||
#include "builtin/intl/ScopedICUObject.h"
|
#include "builtin/intl/ScopedICUObject.h"
|
||||||
#include "builtin/IntlTimeZoneData.h"
|
#include "builtin/IntlTimeZoneData.h"
|
||||||
#include "ds/Sort.h"
|
#include "ds/Sort.h"
|
||||||
#include "unicode/plurrule.h"
|
|
||||||
#include "unicode/ucal.h"
|
|
||||||
#include "unicode/ucol.h"
|
|
||||||
#include "unicode/udat.h"
|
|
||||||
#include "unicode/udatpg.h"
|
|
||||||
#include "unicode/udisplaycontext.h"
|
|
||||||
#include "unicode/uenum.h"
|
|
||||||
#include "unicode/unum.h"
|
|
||||||
#include "unicode/unumsys.h"
|
|
||||||
#include "unicode/upluralrules.h"
|
|
||||||
#include "unicode/ureldatefmt.h"
|
|
||||||
#include "unicode/ustring.h"
|
|
||||||
#include "vm/DateTime.h"
|
#include "vm/DateTime.h"
|
||||||
#include "vm/GlobalObject.h"
|
#include "vm/GlobalObject.h"
|
||||||
#include "vm/Interpreter.h"
|
#include "vm/Interpreter.h"
|
||||||
|
|
@ -53,7 +43,6 @@ using namespace js;
|
||||||
|
|
||||||
using mozilla::AssertedCast;
|
using mozilla::AssertedCast;
|
||||||
using mozilla::IsFinite;
|
using mozilla::IsFinite;
|
||||||
using mozilla::IsNaN;
|
|
||||||
using mozilla::IsNegativeZero;
|
using mozilla::IsNegativeZero;
|
||||||
using mozilla::PodCopy;
|
using mozilla::PodCopy;
|
||||||
|
|
||||||
|
|
@ -566,261 +555,6 @@ js::intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/******************** NumberFormat ********************/
|
|
||||||
|
|
||||||
static void numberFormat_finalize(FreeOp* fop, JSObject* obj);
|
|
||||||
|
|
||||||
static const uint32_t UNUMBER_FORMAT_SLOT = 0;
|
|
||||||
static const uint32_t NUMBER_FORMAT_SLOTS_COUNT = 1;
|
|
||||||
|
|
||||||
static const ClassOps NumberFormatClassOps = {
|
|
||||||
nullptr, /* addProperty */
|
|
||||||
nullptr, /* delProperty */
|
|
||||||
nullptr, /* getProperty */
|
|
||||||
nullptr, /* setProperty */
|
|
||||||
nullptr, /* enumerate */
|
|
||||||
nullptr, /* resolve */
|
|
||||||
nullptr, /* mayResolve */
|
|
||||||
numberFormat_finalize
|
|
||||||
};
|
|
||||||
|
|
||||||
static const Class NumberFormatClass = {
|
|
||||||
js_Object_str,
|
|
||||||
JSCLASS_HAS_RESERVED_SLOTS(NUMBER_FORMAT_SLOTS_COUNT) |
|
|
||||||
JSCLASS_FOREGROUND_FINALIZE,
|
|
||||||
&NumberFormatClassOps
|
|
||||||
};
|
|
||||||
|
|
||||||
#if JS_HAS_TOSOURCE
|
|
||||||
static bool
|
|
||||||
numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp)
|
|
||||||
{
|
|
||||||
CallArgs args = CallArgsFromVp(argc, vp);
|
|
||||||
args.rval().setString(cx->names().NumberFormat);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
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),
|
|
||||||
#if JS_HAS_TOSOURCE
|
|
||||||
JS_FN(js_toSource_str, numberFormat_toSource, 0, 0),
|
|
||||||
#endif
|
|
||||||
JS_FS_END
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 11.2.1 Intl.NumberFormat([ locales [, options]])
|
|
||||||
*
|
|
||||||
* ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
|
|
||||||
*/
|
|
||||||
static bool
|
|
||||||
NumberFormat(JSContext* cx, const CallArgs& args, bool construct)
|
|
||||||
{
|
|
||||||
RootedObject obj(cx);
|
|
||||||
|
|
||||||
// We're following ECMA-402 1st Edition when NumberFormat is called
|
|
||||||
// because of backward compatibility issues.
|
|
||||||
// See https://github.com/tc39/ecma402/issues/57
|
|
||||||
if (!construct) {
|
|
||||||
// ES Intl 1st ed., 11.1.2.1 step 3
|
|
||||||
JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global());
|
|
||||||
if (!intl)
|
|
||||||
return false;
|
|
||||||
RootedValue self(cx, args.thisv());
|
|
||||||
if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) {
|
|
||||||
// ES Intl 1st ed., 11.1.2.1 step 4
|
|
||||||
obj = ToObject(cx, self);
|
|
||||||
if (!obj)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// ES Intl 1st ed., 11.1.2.1 step 5
|
|
||||||
bool extensible;
|
|
||||||
if (!IsExtensible(cx, obj, &extensible))
|
|
||||||
return false;
|
|
||||||
if (!extensible)
|
|
||||||
return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE);
|
|
||||||
} else {
|
|
||||||
// ES Intl 1st ed., 11.1.2.1 step 3.a
|
|
||||||
construct = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (construct) {
|
|
||||||
// Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
|
|
||||||
RootedObject proto(cx);
|
|
||||||
if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!proto) {
|
|
||||||
proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global());
|
|
||||||
if (!proto)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
obj = NewObjectWithGivenProto(cx, &NumberFormatClass, proto);
|
|
||||||
if (!obj)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
obj->as<NativeObject>().setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nullptr));
|
|
||||||
}
|
|
||||||
|
|
||||||
RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue());
|
|
||||||
RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue());
|
|
||||||
|
|
||||||
// Step 3.
|
|
||||||
if (!intl::InitializeObject(cx, obj, cx->names().InitializeNumberFormat, locales, options))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
args.rval().setObject(*obj);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
numberFormat_finalize(FreeOp* fop, JSObject* obj)
|
|
||||||
{
|
|
||||||
MOZ_ASSERT(fop->onMainThread());
|
|
||||||
|
|
||||||
// This is-undefined check shouldn't be necessary, but for internal
|
|
||||||
// brokenness in object allocation code. For the moment, hack around it by
|
|
||||||
// explicitly guarding against the possibility of the reserved slot not
|
|
||||||
// containing a private. See bug 949220.
|
|
||||||
const Value& slot = obj->as<NativeObject>().getReservedSlot(UNUMBER_FORMAT_SLOT);
|
|
||||||
if (!slot.isUndefined()) {
|
|
||||||
if (UNumberFormat* nf = static_cast<UNumberFormat*>(slot.toPrivate()))
|
|
||||||
unum_close(nf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static JSObject*
|
|
||||||
CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle<GlobalObject*> global)
|
|
||||||
{
|
|
||||||
RootedFunction ctor(cx);
|
|
||||||
ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0);
|
|
||||||
if (!ctor)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global,
|
|
||||||
&NumberFormatClass));
|
|
||||||
if (!proto)
|
|
||||||
return nullptr;
|
|
||||||
proto->setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nullptr));
|
|
||||||
|
|
||||||
if (!LinkConstructorAndPrototype(cx, ctor, proto))
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
// 11.2.2
|
|
||||||
if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods))
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
// 11.3.2 and 11.3.3
|
|
||||||
if (!JS_DefineFunctions(cx, proto, numberFormat_methods))
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Install the getter for NumberFormat.prototype.format, which returns a
|
|
||||||
* bound formatting function for the specified NumberFormat object (suitable
|
|
||||||
* for passing to methods like Array.prototype.map).
|
|
||||||
*/
|
|
||||||
RootedValue getter(cx);
|
|
||||||
if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet,
|
|
||||||
&getter))
|
|
||||||
{
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue,
|
|
||||||
JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()),
|
|
||||||
nullptr, JSPROP_GETTER | JSPROP_SHARED))
|
|
||||||
{
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
RootedValue options(cx);
|
|
||||||
if (!intl::CreateDefaultOptions(cx, &options))
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
// 11.2.1 and 11.3
|
|
||||||
if (!intl::InitializeObject(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue,
|
|
||||||
options))
|
|
||||||
{
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8.1
|
|
||||||
RootedValue ctorValue(cx, ObjectValue(*ctor));
|
|
||||||
if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0))
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
return proto;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp)
|
|
||||||
{
|
|
||||||
CallArgs args = CallArgsFromVp(argc, vp);
|
|
||||||
MOZ_ASSERT(args.length() == 0);
|
|
||||||
|
|
||||||
RootedValue result(cx);
|
|
||||||
if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &result))
|
|
||||||
return false;
|
|
||||||
args.rval().set(result);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
|
|
||||||
JSAutoByteString locale(cx, args[0].toString());
|
|
||||||
if (!locale)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
UErrorCode status = U_ZERO_ERROR;
|
|
||||||
UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status);
|
|
||||||
if (U_FAILURE(status)) {
|
|
||||||
intl::ReportInternalError(cx);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ScopedICUObject<UNumberingSystem, unumsys_close> toClose(numbers);
|
|
||||||
|
|
||||||
const char* name = unumsys_getName(numbers);
|
|
||||||
RootedString jsname(cx, JS_NewStringCopyZ(cx, name));
|
|
||||||
if (!jsname)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
args.rval().setString(jsname);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* This creates new UNumberFormat with calculated digit formatting
|
* This creates new UNumberFormat with calculated digit formatting
|
||||||
|
|
@ -903,629 +637,6 @@ NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules)
|
||||||
return toClose.forget();
|
return toClose.forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new UNumberFormat with the locale and number formatting options
|
|
||||||
* of the given NumberFormat.
|
|
||||||
*/
|
|
||||||
static UNumberFormat*
|
|
||||||
NewUNumberFormat(JSContext* cx, HandleObject 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;
|
|
||||||
JSAutoByteString locale(cx, value.toString());
|
|
||||||
if (!locale)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
// UNumberFormat options with default values
|
|
||||||
UNumberFormatStyle uStyle = UNUM_DECIMAL;
|
|
||||||
const UChar* uCurrency = nullptr;
|
|
||||||
uint32_t uMinimumIntegerDigits = 1;
|
|
||||||
uint32_t uMinimumFractionDigits = 0;
|
|
||||||
uint32_t uMaximumFractionDigits = 3;
|
|
||||||
int32_t uMinimumSignificantDigits = -1;
|
|
||||||
int32_t uMaximumSignificantDigits = -1;
|
|
||||||
bool uUseGrouping = true;
|
|
||||||
|
|
||||||
// Sprinkle appropriate rooting flavor over things the GC might care about.
|
|
||||||
RootedString currency(cx);
|
|
||||||
AutoStableStringChars stableChars(cx);
|
|
||||||
|
|
||||||
// We don't need to look at numberingSystem - it can only be set via
|
|
||||||
// the Unicode locale extension and is therefore already set on locale.
|
|
||||||
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().style, &value))
|
|
||||||
return nullptr;
|
|
||||||
JSAutoByteString style(cx, value.toString());
|
|
||||||
if (!style)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
if (StringsAreEqual(style, "currency")) {
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().currency, &value))
|
|
||||||
return nullptr;
|
|
||||||
currency = value.toString();
|
|
||||||
MOZ_ASSERT(currency->length() == 3,
|
|
||||||
"IsWellFormedCurrencyCode permits only length-3 strings");
|
|
||||||
if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency))
|
|
||||||
return nullptr;
|
|
||||||
// uCurrency remains owned by stableChars.
|
|
||||||
uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get());
|
|
||||||
if (!uCurrency)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value))
|
|
||||||
return nullptr;
|
|
||||||
JSAutoByteString currencyDisplay(cx, value.toString());
|
|
||||||
if (!currencyDisplay)
|
|
||||||
return nullptr;
|
|
||||||
if (StringsAreEqual(currencyDisplay, "code")) {
|
|
||||||
uStyle = UNUM_CURRENCY_ISO;
|
|
||||||
} else if (StringsAreEqual(currencyDisplay, "symbol")) {
|
|
||||||
uStyle = UNUM_CURRENCY;
|
|
||||||
} else {
|
|
||||||
MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name"));
|
|
||||||
uStyle = UNUM_CURRENCY_PLURAL;
|
|
||||||
}
|
|
||||||
} else if (StringsAreEqual(style, "percent")) {
|
|
||||||
uStyle = UNUM_PERCENT;
|
|
||||||
} else {
|
|
||||||
MOZ_ASSERT(StringsAreEqual(style, "decimal"));
|
|
||||||
uStyle = UNUM_DECIMAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
RootedId id(cx, NameToId(cx->names().minimumSignificantDigits));
|
|
||||||
bool hasP;
|
|
||||||
if (!HasProperty(cx, internals, id, &hasP))
|
|
||||||
return nullptr;
|
|
||||||
if (hasP) {
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits,
|
|
||||||
&value))
|
|
||||||
return nullptr;
|
|
||||||
uMinimumSignificantDigits = value.toInt32();
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits,
|
|
||||||
&value))
|
|
||||||
return nullptr;
|
|
||||||
uMaximumSignificantDigits = value.toInt32();
|
|
||||||
} else {
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
|
|
||||||
&value))
|
|
||||||
return nullptr;
|
|
||||||
uMinimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits,
|
|
||||||
&value))
|
|
||||||
return nullptr;
|
|
||||||
uMinimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits,
|
|
||||||
&value))
|
|
||||||
return nullptr;
|
|
||||||
uMaximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value))
|
|
||||||
return nullptr;
|
|
||||||
uUseGrouping = value.toBoolean();
|
|
||||||
|
|
||||||
UErrorCode status = U_ZERO_ERROR;
|
|
||||||
UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status);
|
|
||||||
if (U_FAILURE(status)) {
|
|
||||||
intl::ReportInternalError(cx);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
ScopedICUObject<UNumberFormat, unum_close> toClose(nf);
|
|
||||||
|
|
||||||
if (uCurrency) {
|
|
||||||
unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status);
|
|
||||||
if (U_FAILURE(status)) {
|
|
||||||
intl::ReportInternalError(cx);
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (uMinimumSignificantDigits != -1) {
|
|
||||||
unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true);
|
|
||||||
unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits);
|
|
||||||
unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits);
|
|
||||||
} else {
|
|
||||||
unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits);
|
|
||||||
unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits);
|
|
||||||
unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits);
|
|
||||||
}
|
|
||||||
unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping);
|
|
||||||
unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP);
|
|
||||||
|
|
||||||
return toClose.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
using FormattedNumberChars = Vector<char16_t, INITIAL_CHAR_BUFFER_SIZE>;
|
|
||||||
|
|
||||||
static bool
|
|
||||||
PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x,
|
|
||||||
UFieldPositionIterator* fpositer, FormattedNumberChars& formattedChars)
|
|
||||||
{
|
|
||||||
// PartitionNumberPattern doesn't consider -0.0 to be negative.
|
|
||||||
if (IsNegativeZero(*x))
|
|
||||||
*x = 0.0;
|
|
||||||
|
|
||||||
return CallICU(cx, formattedChars, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) {
|
|
||||||
return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status);
|
|
||||||
}) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result)
|
|
||||||
{
|
|
||||||
// Passing null for |fpositer| will just not compute partition information,
|
|
||||||
// letting us common up all ICU number-formatting code.
|
|
||||||
FormattedNumberChars chars(cx);
|
|
||||||
if (!PartitionNumberPattern(cx, nf, &x, nullptr, chars))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
JSString* str = NewStringCopyN<CanGC>(cx, chars.begin(), chars.length());
|
|
||||||
if (!str)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
result.setString(str);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
using FieldType = ImmutablePropertyNamePtr JSAtomState::*;
|
|
||||||
|
|
||||||
static FieldType
|
|
||||||
GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d)
|
|
||||||
{
|
|
||||||
// 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 (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: {
|
|
||||||
MOZ_ASSERT(!IsNegativeZero(d),
|
|
||||||
"-0 should have been excluded by PartitionNumberPattern");
|
|
||||||
|
|
||||||
// Manual trawling through the ICU call graph appears to indicate that
|
|
||||||
// the basic formatting we request will never include a positive sign.
|
|
||||||
// But this analysis may be mistaken, so don't absolutely trust it.
|
|
||||||
return d < 0 ? &JSAtomState::minusSign : &JSAtomState::plusSign;
|
|
||||||
}
|
|
||||||
|
|
||||||
case UNUM_PERCENT_FIELD:
|
|
||||||
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:
|
|
||||||
case UNUM_EXPONENT_SIGN_FIELD:
|
|
||||||
case UNUM_EXPONENT_FIELD:
|
|
||||||
MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in "
|
|
||||||
"formatted number, even though UNUM_SCIENTIFIC "
|
|
||||||
"and scientific notation were never requested");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case UNUM_FIELD_COUNT:
|
|
||||||
MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by "
|
|
||||||
"iterator!");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned "
|
|
||||||
"by iterator");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result)
|
|
||||||
{
|
|
||||||
UErrorCode status = U_ZERO_ERROR;
|
|
||||||
|
|
||||||
UFieldPositionIterator* fpositer = ufieldpositer_open(&status);
|
|
||||||
if (U_FAILURE(status)) {
|
|
||||||
intl::ReportInternalError(cx);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
MOZ_ASSERT(fpositer);
|
|
||||||
ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose(fpositer);
|
|
||||||
|
|
||||||
FormattedNumberChars chars(cx);
|
|
||||||
if (!PartitionNumberPattern(cx, nf, &x, fpositer, chars))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
|
|
||||||
if (!partsArray)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
RootedString overallResult(cx, NewStringCopyN<CanGC>(cx, chars.begin(), chars.length()));
|
|
||||||
if (!overallResult)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// First, vacuum up fields in the overall formatted string.
|
|
||||||
|
|
||||||
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)
|
|
||||||
{}
|
|
||||||
};
|
|
||||||
|
|
||||||
using FieldsVector = Vector<Field, 16>;
|
|
||||||
FieldsVector fields(cx);
|
|
||||||
|
|
||||||
int32_t fieldInt, beginIndexInt, endIndexInt;
|
|
||||||
while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) {
|
|
||||||
MOZ_ASSERT(beginIndexInt >= 0);
|
|
||||||
MOZ_ASSERT(endIndexInt >= 0);
|
|
||||||
MOZ_ASSERT(beginIndexInt < endIndexInt,
|
|
||||||
"erm, aren't fields always non-empty?");
|
|
||||||
|
|
||||||
FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x);
|
|
||||||
if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second, merge sort the fields vector. Expand the vector to have scratch
|
|
||||||
// space for performing the sort.
|
|
||||||
size_t fieldsLen = fields.length();
|
|
||||||
if (!fields.resizeUninitialized(fieldsLen * 2))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Deallocate the scratch space.
|
|
||||||
if (!fields.resize(fieldsLen))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// Third, 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 -28,114,774,228,750.32, the US national surplus (negative
|
|
||||||
// because it's actually a debt) on March 31, 2021.
|
|
||||||
//
|
|
||||||
// var options =
|
|
||||||
// { style: "currency", currency: "USD", currencyDisplay: "name" };
|
|
||||||
// var usdFormatter = new Intl.NumberFormat("en-US", options);
|
|
||||||
// usdFormatter.format(-28114774228750.32);
|
|
||||||
//
|
|
||||||
// The formatted result is "-28,114,774,228,750.32 US dollars". ICU
|
|
||||||
// identifies these fields in the string:
|
|
||||||
//
|
|
||||||
// UNUM_GROUPING_SEPARATOR_FIELD
|
|
||||||
// |
|
|
||||||
// UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD
|
|
||||||
// | __________/| |
|
|
||||||
// | / | | | |
|
|
||||||
// "-28,114,774,228,750.32 US dollars"
|
|
||||||
// \________________/ |/ \_______/
|
|
||||||
// | | |
|
|
||||||
// UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD
|
|
||||||
// |
|
|
||||||
// UNUM_FRACTION_FIELD
|
|
||||||
//
|
|
||||||
// These fields map to parts as follows:
|
|
||||||
//
|
|
||||||
// integer decimal
|
|
||||||
// _____|________ |
|
|
||||||
// / /| |\ |\ |\ | literal
|
|
||||||
// /| / | | \ | \ | \| |
|
|
||||||
// "-28,114,774,228,750.32 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;
|
|
||||||
uint32_t partIndex = 0;
|
|
||||||
RootedObject singlePart(cx);
|
|
||||||
RootedValue propVal(cx);
|
|
||||||
|
|
||||||
PartGenerator gen(cx, fields, chars.length());
|
|
||||||
do {
|
|
||||||
bool hasPart;
|
|
||||||
Part part;
|
|
||||||
if (!gen.nextPart(&hasPart, &part))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!hasPart)
|
|
||||||
break;
|
|
||||||
|
|
||||||
FieldType type = part.type;
|
|
||||||
size_t endIndex = part.end;
|
|
||||||
|
|
||||||
MOZ_ASSERT(lastEndIndex < endIndex);
|
|
||||||
|
|
||||||
singlePart = NewBuiltinClassInstance<PlainObject>(cx);
|
|
||||||
if (!singlePart)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
propVal.setString(cx->names().*type);
|
|
||||||
if (!DefineProperty(cx, singlePart, cx->names().type, propVal))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
JSLinearString* partSubstr =
|
|
||||||
NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex);
|
|
||||||
if (!partSubstr)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
propVal.setString(partSubstr);
|
|
||||||
if (!DefineProperty(cx, singlePart, cx->names().value, propVal))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
propVal.setObject(*singlePart);
|
|
||||||
if (!DefineElement(cx, partsArray, partIndex, propVal))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
lastEndIndex = endIndex;
|
|
||||||
partIndex++;
|
|
||||||
} while (true);
|
|
||||||
|
|
||||||
MOZ_ASSERT(lastEndIndex == chars.length(),
|
|
||||||
"result array must partition the entire string");
|
|
||||||
|
|
||||||
result.setObject(*partsArray);
|
|
||||||
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());
|
|
||||||
MOZ_ASSERT(args[1].isNumber());
|
|
||||||
MOZ_ASSERT(args[2].isBoolean());
|
|
||||||
|
|
||||||
RootedObject numberFormat(cx, &args[0].toObject());
|
|
||||||
|
|
||||||
// Obtain a UNumberFormat object, cached if possible.
|
|
||||||
bool isNumberFormatInstance = numberFormat->getClass() == &NumberFormatClass;
|
|
||||||
UNumberFormat* nf;
|
|
||||||
if (isNumberFormatInstance) {
|
|
||||||
void* priv =
|
|
||||||
numberFormat->as<NativeObject>().getReservedSlot(UNUMBER_FORMAT_SLOT).toPrivate();
|
|
||||||
nf = static_cast<UNumberFormat*>(priv);
|
|
||||||
if (!nf) {
|
|
||||||
nf = NewUNumberFormat(cx, numberFormat);
|
|
||||||
if (!nf)
|
|
||||||
return false;
|
|
||||||
numberFormat->as<NativeObject>().setReservedSlot(UNUMBER_FORMAT_SLOT, PrivateValue(nf));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// There's no good place to cache the ICU number format for an object
|
|
||||||
// that has been initialized as a NumberFormat but is not a
|
|
||||||
// NumberFormat instance. One possibility might be to add a
|
|
||||||
// NumberFormat instance as an internal property to each such object.
|
|
||||||
nf = NewUNumberFormat(cx, numberFormat);
|
|
||||||
if (!nf)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the UNumberFormat to actually format the number.
|
|
||||||
double d = args[1].toNumber();
|
|
||||||
RootedValue result(cx);
|
|
||||||
|
|
||||||
bool success;
|
|
||||||
if (args[2].toBoolean()) {
|
|
||||||
success = intl_FormatNumberToParts(cx, nf, d, &result);
|
|
||||||
} else {
|
|
||||||
MOZ_ASSERT(!args[2].toBoolean(),
|
|
||||||
"shouldn't be doing formatToParts without an ICU that "
|
|
||||||
"supports it");
|
|
||||||
success = intl_FormatNumber(cx, nf, d, &result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isNumberFormatInstance)
|
|
||||||
unum_close(nf);
|
|
||||||
if (!success)
|
|
||||||
return false;
|
|
||||||
args.rval().set(result);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/******************** DateTimeFormat ********************/
|
/******************** DateTimeFormat ********************/
|
||||||
|
|
||||||
static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj);
|
static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj);
|
||||||
|
|
@ -2337,6 +1448,8 @@ intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using FieldType = ImmutablePropertyNamePtr JSAtomState::*;
|
||||||
|
|
||||||
static FieldType
|
static FieldType
|
||||||
GetFieldTypeForFormatField(UDateFormatField fieldName)
|
GetFieldTypeForFormatField(UDateFormatField fieldName)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -218,51 +218,6 @@ extern MOZ_MUST_USE bool
|
||||||
intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp);
|
intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp);
|
||||||
|
|
||||||
|
|
||||||
/******************** NumberFormat ********************/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a new instance of the standard built-in NumberFormat constructor.
|
|
||||||
* Self-hosted code cannot cache this constructor (as it does for others in
|
|
||||||
* Utilities.js) because it is initialized after self-hosted code is compiled.
|
|
||||||
*
|
|
||||||
* Usage: numberFormat = intl_NumberFormat(locales, options)
|
|
||||||
*/
|
|
||||||
extern MOZ_MUST_USE bool
|
|
||||||
intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object indicating the supported locales for number formatting
|
|
||||||
* by having a true-valued property for each such locale with the
|
|
||||||
* canonicalized language tag as the property name. The object has no
|
|
||||||
* prototype.
|
|
||||||
*
|
|
||||||
* Usage: availableLocales = intl_NumberFormat_availableLocales()
|
|
||||||
*/
|
|
||||||
extern MOZ_MUST_USE bool
|
|
||||||
intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the numbering system type identifier per Unicode
|
|
||||||
* Technical Standard 35, Unicode Locale Data Markup Language, for the
|
|
||||||
* default numbering system for the given locale.
|
|
||||||
*
|
|
||||||
* Usage: defaultNumberingSystem = intl_numberingSystem(locale)
|
|
||||||
*/
|
|
||||||
extern MOZ_MUST_USE bool
|
|
||||||
intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a string representing the number x according to the effective
|
|
||||||
* locale and the formatting options of the given NumberFormat.
|
|
||||||
*
|
|
||||||
* Spec: ECMAScript Internationalization API Specification, 11.3.2.
|
|
||||||
*
|
|
||||||
* Usage: formatted = intl_FormatNumber(numberFormat, x)
|
|
||||||
*/
|
|
||||||
extern MOZ_MUST_USE bool
|
|
||||||
intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp);
|
|
||||||
|
|
||||||
|
|
||||||
/******************** DateTimeFormat ********************/
|
/******************** DateTimeFormat ********************/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,8 @@
|
||||||
#define REGEXP_STRING_ITERATOR_FLAGS_SLOT 2
|
#define REGEXP_STRING_ITERATOR_FLAGS_SLOT 2
|
||||||
#define REGEXP_STRING_ITERATOR_DONE_SLOT 3
|
#define REGEXP_STRING_ITERATOR_DONE_SLOT 3
|
||||||
|
|
||||||
|
#define INTL_INTERNALS_OBJECT_SLOT 0
|
||||||
|
|
||||||
#define MODULE_OBJECT_ENVIRONMENT_SLOT 1
|
#define MODULE_OBJECT_ENVIRONMENT_SLOT 1
|
||||||
#define MODULE_OBJECT_STATUS_SLOT 3
|
#define MODULE_OBJECT_STATUS_SLOT 3
|
||||||
#define MODULE_OBJECT_EVALUATION_ERROR_SLOT 4
|
#define MODULE_OBJECT_EVALUATION_ERROR_SLOT 4
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "builtin/intl/ICUHeader.h"
|
||||||
#include "js/RootingAPI.h"
|
#include "js/RootingAPI.h"
|
||||||
#include "js/Vector.h"
|
#include "js/Vector.h"
|
||||||
#include "vm/String.h"
|
#include "vm/String.h"
|
||||||
|
|
|
||||||
23
js/src/builtin/intl/ICUHeader.h
Normal file
23
js/src/builtin/intl/ICUHeader.h
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||||
|
* vim: set ts=8 sts=4 et sw=4 tw=99:
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
#ifndef builtin_intl_ICUHeader_h
|
||||||
|
#define builtin_intl_ICUHeader_h
|
||||||
|
|
||||||
|
#include "unicode/plurrule.h"
|
||||||
|
#include "unicode/ucal.h"
|
||||||
|
#include "unicode/ucol.h"
|
||||||
|
#include "unicode/udat.h"
|
||||||
|
#include "unicode/udatpg.h"
|
||||||
|
#include "unicode/udisplaycontext.h"
|
||||||
|
#include "unicode/uenum.h"
|
||||||
|
#include "unicode/unum.h"
|
||||||
|
#include "unicode/unumsys.h"
|
||||||
|
#include "unicode/upluralrules.h"
|
||||||
|
#include "unicode/ureldatefmt.h"
|
||||||
|
#include "unicode/ustring.h"
|
||||||
|
|
||||||
|
#endif /* builtin_intl_ICUHeader_h */
|
||||||
904
js/src/builtin/intl/NumberFormat.cpp
Normal file
904
js/src/builtin/intl/NumberFormat.cpp
Normal file
|
|
@ -0,0 +1,904 @@
|
||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||||
|
* vim: set ts=8 sts=4 et sw=4 tw=99:
|
||||||
|
* 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/FloatingPoint.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "jscntxt.h"
|
||||||
|
|
||||||
|
#include "builtin/intl/CommonFunctions.h"
|
||||||
|
#include "builtin/intl/ICUHeader.h"
|
||||||
|
#include "builtin/intl/ScopedICUObject.h"
|
||||||
|
#include "ds/Sort.h"
|
||||||
|
#include "js/RootingAPI.h"
|
||||||
|
|
||||||
|
#include "js/TypeDecls.h"
|
||||||
|
#include "vm/SelfHosting.h"
|
||||||
|
#include "vm/Stack.h"
|
||||||
|
|
||||||
|
#include "jsobjinlines.h"
|
||||||
|
|
||||||
|
using namespace js;
|
||||||
|
|
||||||
|
using mozilla::AssertedCast;
|
||||||
|
using mozilla::IsFinite;
|
||||||
|
using mozilla::IsNaN;
|
||||||
|
using mozilla::IsNegativeZero;
|
||||||
|
using js::intl::CallICU;
|
||||||
|
using js::intl::GetAvailableLocales;
|
||||||
|
using js::intl::IcuLocale;
|
||||||
|
using js::intl::INITIAL_CHAR_BUFFER_SIZE;
|
||||||
|
using js::intl::StringsAreEqual;
|
||||||
|
|
||||||
|
/******************** NumberFormat ********************/
|
||||||
|
|
||||||
|
const ClassOps NumberFormatObject::classOps_ = {
|
||||||
|
nullptr, /* addProperty */
|
||||||
|
nullptr, /* delProperty */
|
||||||
|
nullptr, /* getProperty */
|
||||||
|
nullptr, /* setProperty */
|
||||||
|
nullptr, /* enumerate */
|
||||||
|
nullptr, /* resolve */
|
||||||
|
nullptr, /* mayResolve */
|
||||||
|
NumberFormatObject::finalize
|
||||||
|
};
|
||||||
|
|
||||||
|
const Class NumberFormatObject::class_ = {
|
||||||
|
js_Object_str,
|
||||||
|
JSCLASS_HAS_RESERVED_SLOTS(NumberFormatObject::SLOT_COUNT) |
|
||||||
|
JSCLASS_FOREGROUND_FINALIZE,
|
||||||
|
&NumberFormatObject::classOps_
|
||||||
|
};
|
||||||
|
|
||||||
|
#if JS_HAS_TOSOURCE
|
||||||
|
static bool
|
||||||
|
numberFormat_toSource(JSContext* cx, unsigned argc, Value* vp)
|
||||||
|
{
|
||||||
|
CallArgs args = CallArgsFromVp(argc, vp);
|
||||||
|
args.rval().setString(cx->names().NumberFormat);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
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),
|
||||||
|
#if JS_HAS_TOSOURCE
|
||||||
|
JS_FN(js_toSource_str, numberFormat_toSource, 0, 0),
|
||||||
|
#endif
|
||||||
|
JS_FS_END
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 11.2.1 Intl.NumberFormat([ locales [, options]])
|
||||||
|
*
|
||||||
|
* ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b
|
||||||
|
*/
|
||||||
|
static bool
|
||||||
|
NumberFormat(JSContext* cx, const CallArgs& args, bool construct)
|
||||||
|
{
|
||||||
|
RootedObject obj(cx);
|
||||||
|
|
||||||
|
// We're following ECMA-402 1st Edition when NumberFormat is called
|
||||||
|
// because of backward compatibility issues.
|
||||||
|
// See https://github.com/tc39/ecma402/issues/57
|
||||||
|
if (!construct) {
|
||||||
|
// ES Intl 1st ed., 11.1.2.1 step 3
|
||||||
|
JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global());
|
||||||
|
if (!intl)
|
||||||
|
return false;
|
||||||
|
RootedValue self(cx, args.thisv());
|
||||||
|
if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) {
|
||||||
|
// ES Intl 1st ed., 11.1.2.1 step 4
|
||||||
|
obj = ToObject(cx, self);
|
||||||
|
if (!obj)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// ES Intl 1st ed., 11.1.2.1 step 5
|
||||||
|
bool extensible;
|
||||||
|
if (!IsExtensible(cx, obj, &extensible))
|
||||||
|
return false;
|
||||||
|
if (!extensible)
|
||||||
|
return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE);
|
||||||
|
} else {
|
||||||
|
// ES Intl 1st ed., 11.1.2.1 step 3.a
|
||||||
|
construct = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (construct) {
|
||||||
|
// Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor).
|
||||||
|
RootedObject proto(cx);
|
||||||
|
if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!proto) {
|
||||||
|
proto = GlobalObject::getOrCreateNumberFormatPrototype(cx, cx->global());
|
||||||
|
if (!proto)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
obj = NewObjectWithGivenProto<NumberFormatObject>(cx, proto);
|
||||||
|
if (!obj)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
obj->as<NativeObject>().setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue());
|
||||||
|
obj->as<NativeObject>().setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr));
|
||||||
|
}
|
||||||
|
|
||||||
|
RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue());
|
||||||
|
RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue());
|
||||||
|
|
||||||
|
// Step 3.
|
||||||
|
if (!intl::InitializeObject(cx, obj, cx->names().InitializeNumberFormat, locales, options))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
args.rval().setObject(*obj);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(FreeOp* fop, JSObject* obj)
|
||||||
|
{
|
||||||
|
MOZ_ASSERT(fop->onMainThread());
|
||||||
|
|
||||||
|
// This is-undefined check shouldn't be necessary, but for internal
|
||||||
|
// brokenness in object allocation code. For the moment, hack around it by
|
||||||
|
// explicitly guarding against the possibility of the reserved slot not
|
||||||
|
// containing a private. See bug 949220.
|
||||||
|
const Value& slot = obj->as<NativeObject>().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT);
|
||||||
|
if (!slot.isUndefined()) {
|
||||||
|
if (UNumberFormat* nf = static_cast<UNumberFormat*>(slot.toPrivate()))
|
||||||
|
unum_close(nf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSObject*
|
||||||
|
js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle<GlobalObject*> global)
|
||||||
|
{
|
||||||
|
RootedFunction ctor(cx);
|
||||||
|
ctor = GlobalObject::createConstructor(cx, &NumberFormat, cx->names().NumberFormat, 0);
|
||||||
|
if (!ctor)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global,
|
||||||
|
&NumberFormatObject::class_));
|
||||||
|
if (!proto)
|
||||||
|
return nullptr;
|
||||||
|
proto->setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nullptr));
|
||||||
|
|
||||||
|
if (!LinkConstructorAndPrototype(cx, ctor, proto))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// 11.2.2
|
||||||
|
if (!JS_DefineFunctions(cx, ctor, numberFormat_static_methods))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// 11.3.2 and 11.3.3
|
||||||
|
if (!JS_DefineFunctions(cx, proto, numberFormat_methods))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Install the getter for NumberFormat.prototype.format, which returns a
|
||||||
|
* bound formatting function for the specified NumberFormat object (suitable
|
||||||
|
* for passing to methods like Array.prototype.map).
|
||||||
|
*/
|
||||||
|
RootedValue getter(cx);
|
||||||
|
if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().NumberFormatFormatGet,
|
||||||
|
&getter))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (!DefineProperty(cx, proto, cx->names().format, UndefinedHandleValue,
|
||||||
|
JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()),
|
||||||
|
nullptr, JSPROP_GETTER | JSPROP_SHARED))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootedValue options(cx);
|
||||||
|
if (!intl::CreateDefaultOptions(cx, &options))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// 11.2.1 and 11.3
|
||||||
|
if (!intl::InitializeObject(cx, proto, cx->names().InitializeNumberFormat, UndefinedHandleValue,
|
||||||
|
options))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8.1
|
||||||
|
RootedValue ctorValue(cx, ObjectValue(*ctor));
|
||||||
|
if (!DefineProperty(cx, Intl, cx->names().NumberFormat, ctorValue, nullptr, nullptr, 0))
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
return proto;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
js::intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp)
|
||||||
|
{
|
||||||
|
CallArgs args = CallArgsFromVp(argc, vp);
|
||||||
|
MOZ_ASSERT(args.length() == 0);
|
||||||
|
|
||||||
|
RootedValue result(cx);
|
||||||
|
if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &result))
|
||||||
|
return false;
|
||||||
|
args.rval().set(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
|
||||||
|
JSAutoByteString locale(cx, args[0].toString());
|
||||||
|
if (!locale)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
UErrorCode status = U_ZERO_ERROR;
|
||||||
|
UNumberingSystem* numbers = unumsys_open(IcuLocale(locale.ptr()), &status);
|
||||||
|
if (U_FAILURE(status)) {
|
||||||
|
intl::ReportInternalError(cx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedICUObject<UNumberingSystem, unumsys_close> toClose(numbers);
|
||||||
|
|
||||||
|
const char* name = unumsys_getName(numbers);
|
||||||
|
RootedString jsname(cx, JS_NewStringCopyZ(cx, name));
|
||||||
|
if (!jsname)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
args.rval().setString(jsname);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new UNumberFormat with the locale and number formatting options
|
||||||
|
* of the given NumberFormat.
|
||||||
|
*/
|
||||||
|
static UNumberFormat*
|
||||||
|
NewUNumberFormat(JSContext* cx, HandleObject 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;
|
||||||
|
JSAutoByteString locale(cx, value.toString());
|
||||||
|
if (!locale)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
// UNumberFormat options with default values
|
||||||
|
UNumberFormatStyle uStyle = UNUM_DECIMAL;
|
||||||
|
const UChar* uCurrency = nullptr;
|
||||||
|
uint32_t uMinimumIntegerDigits = 1;
|
||||||
|
uint32_t uMinimumFractionDigits = 0;
|
||||||
|
uint32_t uMaximumFractionDigits = 3;
|
||||||
|
int32_t uMinimumSignificantDigits = -1;
|
||||||
|
int32_t uMaximumSignificantDigits = -1;
|
||||||
|
bool uUseGrouping = true;
|
||||||
|
|
||||||
|
// Sprinkle appropriate rooting flavor over things the GC might care about.
|
||||||
|
RootedString currency(cx);
|
||||||
|
AutoStableStringChars stableChars(cx);
|
||||||
|
|
||||||
|
// We don't need to look at numberingSystem - it can only be set via
|
||||||
|
// the Unicode locale extension and is therefore already set on locale.
|
||||||
|
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().style, &value))
|
||||||
|
return nullptr;
|
||||||
|
JSAutoByteString style(cx, value.toString());
|
||||||
|
if (!style)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (StringsAreEqual(style, "currency")) {
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().currency, &value))
|
||||||
|
return nullptr;
|
||||||
|
currency = value.toString();
|
||||||
|
MOZ_ASSERT(currency->length() == 3,
|
||||||
|
"IsWellFormedCurrencyCode permits only length-3 strings");
|
||||||
|
if (!currency->ensureFlat(cx) || !stableChars.initTwoByte(cx, currency))
|
||||||
|
return nullptr;
|
||||||
|
// uCurrency remains owned by stableChars.
|
||||||
|
uCurrency = Char16ToUChar(stableChars.twoByteRange().begin().get());
|
||||||
|
if (!uCurrency)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().currencyDisplay, &value))
|
||||||
|
return nullptr;
|
||||||
|
JSAutoByteString currencyDisplay(cx, value.toString());
|
||||||
|
if (!currencyDisplay)
|
||||||
|
return nullptr;
|
||||||
|
if (StringsAreEqual(currencyDisplay, "code")) {
|
||||||
|
uStyle = UNUM_CURRENCY_ISO;
|
||||||
|
} else if (StringsAreEqual(currencyDisplay, "symbol")) {
|
||||||
|
uStyle = UNUM_CURRENCY;
|
||||||
|
} else {
|
||||||
|
MOZ_ASSERT(StringsAreEqual(currencyDisplay, "name"));
|
||||||
|
uStyle = UNUM_CURRENCY_PLURAL;
|
||||||
|
}
|
||||||
|
} else if (StringsAreEqual(style, "percent")) {
|
||||||
|
uStyle = UNUM_PERCENT;
|
||||||
|
} else {
|
||||||
|
MOZ_ASSERT(StringsAreEqual(style, "decimal"));
|
||||||
|
uStyle = UNUM_DECIMAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
RootedId id(cx, NameToId(cx->names().minimumSignificantDigits));
|
||||||
|
bool hasP;
|
||||||
|
if (!HasProperty(cx, internals, id, &hasP))
|
||||||
|
return nullptr;
|
||||||
|
if (hasP) {
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits,
|
||||||
|
&value))
|
||||||
|
return nullptr;
|
||||||
|
uMinimumSignificantDigits = value.toInt32();
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits,
|
||||||
|
&value))
|
||||||
|
return nullptr;
|
||||||
|
uMaximumSignificantDigits = value.toInt32();
|
||||||
|
} else {
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits,
|
||||||
|
&value))
|
||||||
|
return nullptr;
|
||||||
|
uMinimumIntegerDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits,
|
||||||
|
&value))
|
||||||
|
return nullptr;
|
||||||
|
uMinimumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits,
|
||||||
|
&value))
|
||||||
|
return nullptr;
|
||||||
|
uMaximumFractionDigits = AssertedCast<uint32_t>(value.toInt32());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value))
|
||||||
|
return nullptr;
|
||||||
|
uUseGrouping = value.toBoolean();
|
||||||
|
|
||||||
|
UErrorCode status = U_ZERO_ERROR;
|
||||||
|
UNumberFormat* nf = unum_open(uStyle, nullptr, 0, IcuLocale(locale.ptr()), nullptr, &status);
|
||||||
|
if (U_FAILURE(status)) {
|
||||||
|
intl::ReportInternalError(cx);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
ScopedICUObject<UNumberFormat, unum_close> toClose(nf);
|
||||||
|
|
||||||
|
if (uCurrency) {
|
||||||
|
unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status);
|
||||||
|
if (U_FAILURE(status)) {
|
||||||
|
intl::ReportInternalError(cx);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (uMinimumSignificantDigits != -1) {
|
||||||
|
unum_setAttribute(nf, UNUM_SIGNIFICANT_DIGITS_USED, true);
|
||||||
|
unum_setAttribute(nf, UNUM_MIN_SIGNIFICANT_DIGITS, uMinimumSignificantDigits);
|
||||||
|
unum_setAttribute(nf, UNUM_MAX_SIGNIFICANT_DIGITS, uMaximumSignificantDigits);
|
||||||
|
} else {
|
||||||
|
unum_setAttribute(nf, UNUM_MIN_INTEGER_DIGITS, uMinimumIntegerDigits);
|
||||||
|
unum_setAttribute(nf, UNUM_MIN_FRACTION_DIGITS, uMinimumFractionDigits);
|
||||||
|
unum_setAttribute(nf, UNUM_MAX_FRACTION_DIGITS, uMaximumFractionDigits);
|
||||||
|
}
|
||||||
|
unum_setAttribute(nf, UNUM_GROUPING_USED, uUseGrouping);
|
||||||
|
unum_setAttribute(nf, UNUM_ROUNDING_MODE, UNUM_ROUND_HALFUP);
|
||||||
|
|
||||||
|
return toClose.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
static JSString*
|
||||||
|
PartitionNumberPattern(JSContext* cx, UNumberFormat* nf, double* x,
|
||||||
|
UFieldPositionIterator* fpositer)
|
||||||
|
{
|
||||||
|
// PartitionNumberPattern doesn't consider -0.0 to be negative.
|
||||||
|
if (IsNegativeZero(*x))
|
||||||
|
*x = 0.0;
|
||||||
|
|
||||||
|
return CallICU(cx, [nf, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) {
|
||||||
|
return unum_formatDoubleForFields(nf, *x, chars, size, fpositer, status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
js::intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result)
|
||||||
|
{
|
||||||
|
// Passing null for |fpositer| will just not compute partition information,
|
||||||
|
// letting us common up all ICU number-formatting code.
|
||||||
|
JSString* str = PartitionNumberPattern(cx, nf, &x, nullptr);
|
||||||
|
if (!str)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
result.setString(str);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
using FieldType = ImmutablePropertyNamePtr JSAtomState::*;
|
||||||
|
|
||||||
|
static FieldType
|
||||||
|
GetFieldTypeForNumberField(UNumberFormatFields fieldName, double d)
|
||||||
|
{
|
||||||
|
// 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 (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: {
|
||||||
|
MOZ_ASSERT(!IsNegativeZero(d),
|
||||||
|
"-0 should have been excluded by PartitionNumberPattern");
|
||||||
|
|
||||||
|
// Manual trawling through the ICU call graph appears to indicate that
|
||||||
|
// the basic formatting we request will never include a positive sign.
|
||||||
|
// But this analysis may be mistaken, so don't absolutely trust it.
|
||||||
|
return d < 0 ? &JSAtomState::minusSign : &JSAtomState::plusSign;
|
||||||
|
}
|
||||||
|
|
||||||
|
case UNUM_PERCENT_FIELD:
|
||||||
|
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:
|
||||||
|
case UNUM_EXPONENT_SIGN_FIELD:
|
||||||
|
case UNUM_EXPONENT_FIELD:
|
||||||
|
MOZ_ASSERT_UNREACHABLE("exponent field unexpectedly found in "
|
||||||
|
"formatted number, even though UNUM_SCIENTIFIC "
|
||||||
|
"and scientific notation were never requested");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UNUM_FIELD_COUNT:
|
||||||
|
MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by "
|
||||||
|
"iterator!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned "
|
||||||
|
"by iterator");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
intl_FormatNumberToParts(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result)
|
||||||
|
{
|
||||||
|
UErrorCode status = U_ZERO_ERROR;
|
||||||
|
|
||||||
|
UFieldPositionIterator* fpositer = ufieldpositer_open(&status);
|
||||||
|
if (U_FAILURE(status)) {
|
||||||
|
intl::ReportInternalError(cx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MOZ_ASSERT(fpositer);
|
||||||
|
ScopedICUObject<UFieldPositionIterator, ufieldpositer_close> toClose(fpositer);
|
||||||
|
|
||||||
|
RootedString overallResult(cx, PartitionNumberPattern(cx, nf, &x, fpositer));
|
||||||
|
if (!overallResult)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx));
|
||||||
|
if (!partsArray)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// First, vacuum up fields in the overall formatted string.
|
||||||
|
|
||||||
|
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)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
using FieldsVector = Vector<Field, 16>;
|
||||||
|
FieldsVector fields(cx);
|
||||||
|
|
||||||
|
int32_t fieldInt, beginIndexInt, endIndexInt;
|
||||||
|
while ((fieldInt = ufieldpositer_next(fpositer, &beginIndexInt, &endIndexInt)) >= 0) {
|
||||||
|
MOZ_ASSERT(beginIndexInt >= 0);
|
||||||
|
MOZ_ASSERT(endIndexInt >= 0);
|
||||||
|
MOZ_ASSERT(beginIndexInt < endIndexInt,
|
||||||
|
"erm, aren't fields always non-empty?");
|
||||||
|
|
||||||
|
FieldType type = GetFieldTypeForNumberField(UNumberFormatFields(fieldInt), x);
|
||||||
|
if (!fields.emplaceBack(uint32_t(beginIndexInt), uint32_t(endIndexInt), type))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second, merge sort the fields vector. Expand the vector to have scratch
|
||||||
|
// space for performing the sort.
|
||||||
|
size_t fieldsLen = fields.length();
|
||||||
|
if (!fields.resizeUninitialized(fieldsLen * 2))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Deallocate the scratch space.
|
||||||
|
if (!fields.resize(fieldsLen))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Third, 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 -28,114,774,228,750.32, the US national surplus (negative
|
||||||
|
// because it's actually a debt) on March 31, 2021.
|
||||||
|
//
|
||||||
|
// var options =
|
||||||
|
// { style: "currency", currency: "USD", currencyDisplay: "name" };
|
||||||
|
// var usdFormatter = new Intl.NumberFormat("en-US", options);
|
||||||
|
// usdFormatter.format(-28114774228750.32);
|
||||||
|
//
|
||||||
|
// The formatted result is "-28,114,774,228,750.32 US dollars". ICU
|
||||||
|
// identifies these fields in the string:
|
||||||
|
//
|
||||||
|
// UNUM_GROUPING_SEPARATOR_FIELD
|
||||||
|
// |
|
||||||
|
// UNUM_SIGN_FIELD | UNUM_DECIMAL_SEPARATOR_FIELD
|
||||||
|
// | __________/| |
|
||||||
|
// | / | | | |
|
||||||
|
// "-28,114,774,228,750.32 US dollars"
|
||||||
|
// \________________/ |/ \_______/
|
||||||
|
// | | |
|
||||||
|
// UNUM_INTEGER_FIELD | UNUM_CURRENCY_FIELD
|
||||||
|
// |
|
||||||
|
// UNUM_FRACTION_FIELD
|
||||||
|
//
|
||||||
|
// These fields map to parts as follows:
|
||||||
|
//
|
||||||
|
// integer decimal
|
||||||
|
// _____|________ |
|
||||||
|
// / /| |\ |\ |\ | literal
|
||||||
|
// /| / | | \ | \ | \| |
|
||||||
|
// "-28,114,774,228,750.32 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;
|
||||||
|
uint32_t partIndex = 0;
|
||||||
|
RootedObject singlePart(cx);
|
||||||
|
RootedValue propVal(cx);
|
||||||
|
|
||||||
|
PartGenerator gen(cx, fields, overallResult->length());
|
||||||
|
do {
|
||||||
|
bool hasPart;
|
||||||
|
Part part;
|
||||||
|
if (!gen.nextPart(&hasPart, &part))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!hasPart)
|
||||||
|
break;
|
||||||
|
|
||||||
|
FieldType type = part.type;
|
||||||
|
size_t endIndex = part.end;
|
||||||
|
|
||||||
|
MOZ_ASSERT(lastEndIndex < endIndex);
|
||||||
|
|
||||||
|
singlePart = NewBuiltinClassInstance<PlainObject>(cx);
|
||||||
|
if (!singlePart)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
propVal.setString(cx->names().*type);
|
||||||
|
if (!DefineProperty(cx, singlePart, cx->names().type, propVal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
JSLinearString* partSubstr =
|
||||||
|
NewDependentString(cx, overallResult, lastEndIndex, endIndex - lastEndIndex);
|
||||||
|
if (!partSubstr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
propVal.setString(partSubstr);
|
||||||
|
if (!DefineProperty(cx, singlePart, cx->names().value, propVal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
propVal.setObject(*singlePart);
|
||||||
|
if (!DefineElement(cx, partsArray, partIndex, propVal))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
lastEndIndex = endIndex;
|
||||||
|
partIndex++;
|
||||||
|
} while (true);
|
||||||
|
|
||||||
|
MOZ_ASSERT(lastEndIndex == chars.length(),
|
||||||
|
"result array must partition the entire string");
|
||||||
|
|
||||||
|
result.setObject(*partsArray);
|
||||||
|
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());
|
||||||
|
MOZ_ASSERT(args[1].isNumber());
|
||||||
|
MOZ_ASSERT(args[2].isBoolean());
|
||||||
|
|
||||||
|
RootedObject numberFormat(cx, &args[0].toObject());
|
||||||
|
|
||||||
|
// Obtain a UNumberFormat object, cached if possible.
|
||||||
|
bool isNumberFormatInstance = numberFormat->getClass() == &NumberFormatObject::class_;
|
||||||
|
UNumberFormat* nf;
|
||||||
|
if (isNumberFormatInstance) {
|
||||||
|
void* priv =
|
||||||
|
numberFormat->as<NativeObject>().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate();
|
||||||
|
nf = static_cast<UNumberFormat*>(priv);
|
||||||
|
if (!nf) {
|
||||||
|
nf = NewUNumberFormat(cx, numberFormat);
|
||||||
|
if (!nf)
|
||||||
|
return false;
|
||||||
|
numberFormat->as<NativeObject>().setReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT, PrivateValue(nf));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// There's no good place to cache the ICU number format for an object
|
||||||
|
// that has been initialized as a NumberFormat but is not a
|
||||||
|
// NumberFormat instance. One possibility might be to add a
|
||||||
|
// NumberFormat instance as an internal property to each such object.
|
||||||
|
nf = NewUNumberFormat(cx, numberFormat);
|
||||||
|
if (!nf)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the UNumberFormat to actually format the number.
|
||||||
|
double d = args[1].toNumber();
|
||||||
|
RootedValue result(cx);
|
||||||
|
|
||||||
|
bool success;
|
||||||
|
if (args[2].toBoolean()) {
|
||||||
|
success = intl_FormatNumberToParts(cx, nf, d, &result);
|
||||||
|
} else {
|
||||||
|
MOZ_ASSERT(!args[2].toBoolean(),
|
||||||
|
"shouldn't be doing formatToParts without an ICU that "
|
||||||
|
"supports it");
|
||||||
|
success = js::intl_FormatNumber(cx, nf, d, &result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNumberFormatInstance)
|
||||||
|
unum_close(nf);
|
||||||
|
if (!success)
|
||||||
|
return false;
|
||||||
|
args.rval().set(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
89
js/src/builtin/intl/NumberFormat.h
Normal file
89
js/src/builtin/intl/NumberFormat.h
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
|
||||||
|
* vim: set ts=8 sts=4 et sw=4 tw=99:
|
||||||
|
* 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/. */
|
||||||
|
|
||||||
|
#ifndef builtin_intl_NumberFormat_h
|
||||||
|
#define builtin_intl_NumberFormat_h
|
||||||
|
|
||||||
|
#include "mozilla/Attributes.h"
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "unicode/unum.h" // for UNumberFormat
|
||||||
|
#include "builtin/SelfHostingDefines.h"
|
||||||
|
#include "js/Class.h"
|
||||||
|
#include "vm/NativeObject.h"
|
||||||
|
|
||||||
|
namespace js {
|
||||||
|
|
||||||
|
class FreeOp;
|
||||||
|
|
||||||
|
class NumberFormatObject : public NativeObject
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static const Class class_;
|
||||||
|
|
||||||
|
static constexpr uint32_t INTERNALS_SLOT = 0;
|
||||||
|
static constexpr uint32_t UNUMBER_FORMAT_SLOT = 1;
|
||||||
|
static constexpr uint32_t SLOT_COUNT = 2;
|
||||||
|
|
||||||
|
static_assert(INTERNALS_SLOT == INTL_INTERNALS_OBJECT_SLOT,
|
||||||
|
"INTERNALS_SLOT must match self-hosting define for internals object slot");
|
||||||
|
private:
|
||||||
|
static const ClassOps classOps_;
|
||||||
|
|
||||||
|
static void finalize(FreeOp* fop, JSObject* obj);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern JSObject*
|
||||||
|
CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle<GlobalObject*> global);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new instance of the standard built-in NumberFormat constructor.
|
||||||
|
* Self-hosted code cannot cache this constructor (as it does for others in
|
||||||
|
* Utilities.js) because it is initialized after self-hosted code is compiled.
|
||||||
|
*
|
||||||
|
* Usage: numberFormat = intl_NumberFormat(locales, options)
|
||||||
|
*/
|
||||||
|
extern MOZ_MUST_USE bool
|
||||||
|
intl_NumberFormat(JSContext* cx, unsigned argc, Value* vp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object indicating the supported locales for number formatting
|
||||||
|
* by having a true-valued property for each such locale with the
|
||||||
|
* canonicalized language tag as the property name. The object has no
|
||||||
|
* prototype.
|
||||||
|
*
|
||||||
|
* Usage: availableLocales = intl_NumberFormat_availableLocales()
|
||||||
|
*/
|
||||||
|
extern MOZ_MUST_USE bool
|
||||||
|
intl_NumberFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the numbering system type identifier per Unicode
|
||||||
|
* Technical Standard 35, Unicode Locale Data Markup Language, for the
|
||||||
|
* default numbering system for the given locale.
|
||||||
|
*
|
||||||
|
* Usage: defaultNumberingSystem = intl_numberingSystem(locale)
|
||||||
|
*/
|
||||||
|
extern MOZ_MUST_USE bool
|
||||||
|
intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string representing the number x according to the effective
|
||||||
|
* locale and the formatting options of the given NumberFormat.
|
||||||
|
*
|
||||||
|
* Spec: ECMAScript Internationalization API Specification, 11.3.2.
|
||||||
|
*
|
||||||
|
* Usage: formatted = intl_FormatNumber(numberFormat, x)
|
||||||
|
*/
|
||||||
|
extern MOZ_MUST_USE bool
|
||||||
|
intl_FormatNumber(JSContext* cx, unsigned argc, Value* vp);
|
||||||
|
extern MOZ_MUST_USE bool
|
||||||
|
intl_FormatNumber(JSContext* cx, UNumberFormat* nf, double x, MutableHandleValue result);
|
||||||
|
|
||||||
|
} // namespace js
|
||||||
|
|
||||||
|
#endif /* builtin_intl_NumberFormat_h */
|
||||||
|
|
@ -116,6 +116,7 @@ UNIFIED_SOURCES += [
|
||||||
'builtin/Eval.cpp',
|
'builtin/Eval.cpp',
|
||||||
'builtin/Intl.cpp',
|
'builtin/Intl.cpp',
|
||||||
'builtin/intl/CommonFunctions.cpp',
|
'builtin/intl/CommonFunctions.cpp',
|
||||||
|
'builtin/intl/NumberFormat.cpp',
|
||||||
'builtin/MapObject.cpp',
|
'builtin/MapObject.cpp',
|
||||||
'builtin/ModuleObject.cpp',
|
'builtin/ModuleObject.cpp',
|
||||||
'builtin/Object.cpp',
|
'builtin/Object.cpp',
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
#include "selfhosted.out.h"
|
#include "selfhosted.out.h"
|
||||||
|
|
||||||
#include "builtin/Intl.h"
|
#include "builtin/Intl.h"
|
||||||
|
#include "builtin/intl/NumberFormat.h"
|
||||||
#include "builtin/MapObject.h"
|
#include "builtin/MapObject.h"
|
||||||
#include "builtin/ModuleObject.h"
|
#include "builtin/ModuleObject.h"
|
||||||
#include "builtin/Object.h"
|
#include "builtin/Object.h"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue