From 98180dd541327e4f9265902cd2990c7aebd4ef3a Mon Sep 17 00:00:00 2001 From: Fedor Date: Sun, 17 Jan 2021 16:10:59 +0200 Subject: [PATCH] Implement Intl.PluralRules API. --- config/check_spidermonkey_style.py | 2 + js/public/Class.h | 2 +- js/src/builtin/Intl.cpp | 486 ++++++++++++++++++++++++++++- js/src/builtin/Intl.h | 48 +++ js/src/builtin/Intl.js | 351 ++++++++++++++++++--- js/src/vm/CommonPropertyNames.h | 3 + js/src/vm/GlobalObject.h | 6 + js/src/vm/SelfHosting.cpp | 3 + 8 files changed, 834 insertions(+), 67 deletions(-) diff --git a/config/check_spidermonkey_style.py b/config/check_spidermonkey_style.py index cc8695cee..4667a1a2f 100644 --- a/config/check_spidermonkey_style.py +++ b/config/check_spidermonkey_style.py @@ -79,6 +79,7 @@ included_inclnames_to_ignore = set([ 'prtypes.h', # NSPR 'selfhosted.out.h', # generated in $OBJDIR 'shellmoduleloader.out.h', # generated in $OBJDIR + 'unicode/plurrule.h', # ICU 'unicode/timezone.h', # ICU 'unicode/ucal.h', # ICU 'unicode/uclean.h', # ICU @@ -90,6 +91,7 @@ included_inclnames_to_ignore = set([ 'unicode/unorm.h', # ICU 'unicode/unum.h', # ICU 'unicode/unumsys.h', # ICU + 'unicode/upluralrules.h', # ICU 'unicode/ustring.h', # ICU 'unicode/utypes.h', # ICU 'vtune/VTuneWrapper.h' # VTune diff --git a/js/public/Class.h b/js/public/Class.h index 9cb377892..094e4a7ed 100644 --- a/js/public/Class.h +++ b/js/public/Class.h @@ -778,7 +778,7 @@ struct JSClass { // application. #define JSCLASS_GLOBAL_APPLICATION_SLOTS 5 #define JSCLASS_GLOBAL_SLOT_COUNT \ - (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 45) + (JSCLASS_GLOBAL_APPLICATION_SLOTS + JSProto_LIMIT * 2 + 46) #define JSCLASS_GLOBAL_FLAGS_WITH_SLOTS(n) \ (JSCLASS_IS_GLOBAL | JSCLASS_HAS_RESERVED_SLOTS(JSCLASS_GLOBAL_SLOT_COUNT + (n))) #define JSCLASS_GLOBAL_FLAGS \ diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp index d231d7bec..622e773e0 100644 --- a/js/src/builtin/Intl.cpp +++ b/js/src/builtin/Intl.cpp @@ -10,6 +10,7 @@ #include "builtin/Intl.h" +#include "mozilla/Casting.h" #include "mozilla/PodOperations.h" #include "mozilla/Range.h" #include "mozilla/ScopeExit.h" @@ -22,6 +23,7 @@ #include "jsobj.h" #include "builtin/IntlTimeZoneData.h" +#include "unicode/plurrule.h" #include "unicode/ucal.h" #include "unicode/ucol.h" #include "unicode/udat.h" @@ -29,6 +31,7 @@ #include "unicode/uenum.h" #include "unicode/unum.h" #include "unicode/unumsys.h" +#include "unicode/upluralrules.h" #include "unicode/ustring.h" #include "vm/DateTime.h" #include "vm/GlobalObject.h" @@ -43,6 +46,7 @@ using namespace js; +using mozilla::AssertedCast; using mozilla::IsFinite; using mozilla::IsNegativeZero; using mozilla::MakeScopeExit; @@ -962,6 +966,89 @@ js::intl_numberingSystem(JSContext* cx, unsigned argc, Value* vp) return true; } +/** + * + * This creates new UNumberFormat with calculated digit formatting + * properties for PluralRules. + * + * This is similar to NewUNumberFormat but doesn't allow for currency or + * percent types. + * + */ +static UNumberFormat* +NewUNumberFormatForPluralRules(JSContext* cx, HandleObject pluralRules) +{ + RootedObject internals(cx, GetInternals(cx, pluralRules)); + if (!internals) + return nullptr; + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + uint32_t uMinimumIntegerDigits = 1; + uint32_t uMinimumFractionDigits = 0; + uint32_t uMaximumFractionDigits = 3; + int32_t uMinimumSignificantDigits = -1; + int32_t uMaximumSignificantDigits = -1; + + 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(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, + &value)) + return nullptr; + uMinimumFractionDigits = AssertedCast(value.toInt32()); + + if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, + &value)) + return nullptr; + uMaximumFractionDigits = AssertedCast(value.toInt32()); + } + + UErrorCode status = U_ZERO_ERROR; + UNumberFormat* nf = unum_open(UNUM_DECIMAL, nullptr, 0, icuLocale(locale.ptr()), nullptr, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return nullptr; + } + ScopedICUObject toClose(nf); + + 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); + } + + return toClose.forget(); +} + + /** * Returns a new UNumberFormat with the locale and number formatting options * of the given NumberFormat. @@ -1044,35 +1131,25 @@ NewUNumberFormat(JSContext* cx, HandleObject numberFormat) if (hasP) { if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, &value)) - { return nullptr; - } - uMinimumSignificantDigits = int32_t(value.toNumber()); + uMinimumSignificantDigits = value.toInt32(); if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, &value)) - { return nullptr; - } - uMaximumSignificantDigits = int32_t(value.toNumber()); + uMaximumSignificantDigits = value.toInt32(); } else { if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, &value)) - { return nullptr; - } - uMinimumIntegerDigits = int32_t(value.toNumber()); + uMinimumIntegerDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, &value)) - { return nullptr; - } - uMinimumFractionDigits = int32_t(value.toNumber()); + uMinimumFractionDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, &value)) - { return nullptr; - } - uMaximumFractionDigits = int32_t(value.toNumber()); + uMaximumFractionDigits = AssertedCast(value.toInt32()); } if (!GetProperty(cx, internals, internals, cx->names().useGrouping, &value)) @@ -2323,6 +2400,381 @@ js::intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp) return true; } +/**************** PluralRules *****************/ + +static void pluralRules_finalize(FreeOp* fop, JSObject* obj); + +static const uint32_t UPLURAL_RULES_SLOT = 0; +static const uint32_t PLURAL_RULES_SLOTS_COUNT = 1; + +static const ClassOps PluralRulesClassOps = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + pluralRules_finalize +}; + +static const Class PluralRulesClass = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(PLURAL_RULES_SLOTS_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &PluralRulesClassOps +}; + +#if JS_HAS_TOSOURCE +static bool +pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().PluralRules); + return true; +} +#endif + +static const JSFunctionSpec pluralRules_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_PluralRules_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec pluralRules_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_PluralRules_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("select", "Intl_PluralRules_select", 1, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * PluralRules constructor. + * Spec: ECMAScript 402 API, PluralRules, 1.1 + */ +static bool +PluralRules(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + if (!construct) { + JSObject* intl = GlobalObject::getOrCreateIntlObject(cx, cx->global()); + if (!intl) + return false; + RootedValue self(cx, args.thisv()); + if (!self.isUndefined() && (!self.isObject() || self.toObject() != *intl)) { + obj = ToObject(cx, self); + if (!obj) + return false; + + bool extensible; + if (!IsExtensible(cx, obj, &extensible)) + return false; + if (!extensible) + return Throw(cx, obj, JSMSG_OBJECT_NOT_EXTENSIBLE); + } else { + construct = true; + } + } + if (construct) { + RootedObject proto(cx, GlobalObject::getOrCreatePluralRulesPrototype(cx, cx->global())); + if (!proto) + return false; + obj = NewObjectWithGivenProto(cx, &PluralRulesClass, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(UPLURAL_RULES_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + if (!IntlInitialize(cx, obj, cx->names().InitializePluralRules, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +PluralRules(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return PluralRules(cx, args, args.isConstructing()); +} + +bool +js::intl_PluralRules(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + return PluralRules(cx, args, true); +} + +static void +pluralRules_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().getReservedSlot(UPLURAL_RULES_SLOT); + if (!slot.isUndefined()) { + if (UPluralRules* pr = static_cast(slot.toPrivate())) + uplrules_close(pr); + } +} + +static JSObject* +CreatePluralRulesPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = global->createConstructor(cx, &PluralRules, cx->names().PluralRules, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &PluralRulesClass)); + if (!proto) + return nullptr; + proto->setReservedSlot(UPLURAL_RULES_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + if (!JS_DefineFunctions(cx, ctor, pluralRules_static_methods)) + return nullptr; + + if (!JS_DefineFunctions(cx, proto, pluralRules_methods)) + return nullptr; + + RootedValue options(cx); + if (!CreateDefaultOptions(cx, &options)) + return nullptr; + + if (!IntlInitialize(cx, proto, cx->names().InitializePluralRules, UndefinedHandleValue, + options)) + { + return nullptr; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().PluralRules, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + // We're going to use ULocale availableLocales as per ICU recommendation: + // https://ssl.icu-project.org/trac/ticket/12756 + if (!intl_availableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +bool +js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + RootedObject pluralRules(cx, &args[0].toObject()); + + UNumberFormat* nf = NewUNumberFormatForPluralRules(cx, pluralRules); + if (!nf) + return false; + + ScopedICUObject closeNumberFormat(nf); + + RootedObject internals(cx, GetInternals(cx, pluralRules)); + if (!internals) + return false; + + RootedValue value(cx); + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return false; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return false; + + if (!GetProperty(cx, internals, internals, cx->names().type, &value)) + return false; + JSAutoByteString type(cx, value.toString()); + if (!type) + return false; + + double x = args[1].toNumber(); + + // We need a NumberFormat in order to format the number + // using the number formatting options (minimum/maximum*Digits) + // before we push the result to PluralRules + // + // This should be fixed in ICU 59 and we'll be able to switch to that + // API: http://bugs.icu-project.org/trac/ticket/12763 + // + RootedValue fmtNumValue(cx); + if (!intl_FormatNumber(cx, nf, x, &fmtNumValue)) + return false; + RootedString fmtNumValueString(cx, fmtNumValue.toString()); + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, fmtNumValueString)) + return false; + + const UChar* uFmtNumValue = Char16ToUChar(stableChars.twoByteRange().begin().get()); + + UErrorCode status = U_ZERO_ERROR; + + UFormattable* fmt = unum_parseToUFormattable(nf, nullptr, uFmtNumValue, + stableChars.twoByteRange().length(), 0, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + ScopedICUObject closeUFormattable(fmt); + + double y = ufmt_getDouble(fmt, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + UPluralType category; + + if (equal(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(equal(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType(icuLocale(locale.ptr()), category, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + ScopedICUObject closePluralRules(pr); + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + int size = uplrules_select(pr, y, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, &status); + if (status == U_BUFFER_OVERFLOW_ERROR) { + if (!chars.resize(size)) + return false; + status = U_ZERO_ERROR; + uplrules_select(pr, y, Char16ToUChar(chars.begin()), size, &status); + } + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + JSString* str = NewStringCopyN(cx, chars.begin(), size); + if (!str) + return false; + + args.rval().setString(str); + return true; +} + +bool +js::intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSAutoByteString type(cx, args[1].toString()); + if (!type) + return false; + + UErrorCode status = U_ZERO_ERROR; + + UPluralType category; + + if (equal(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(equal(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType( + icuLocale(locale.ptr()), + category, + &status + ); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + ScopedICUObject closePluralRules(pr); + + // We should get a C API for that in ICU 59 and switch to it + // https://ssl.icu-project.org/trac/ticket/12772 + // + icu::StringEnumeration* kwenum = + reinterpret_cast(pr)->getKeywords(status); + UEnumeration* ue = uenum_openFromStringEnumeration(kwenum, &status); + + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + ScopedICUObject closeEnum(ue); + + RootedObject res(cx, NewDenseEmptyArray(cx)); + if (!res) + return false; + + RootedValue element(cx); + uint32_t i = 0; + int32_t catSize; + const char* cat; + + do { + cat = uenum_next(ue, &catSize, &status); + if (U_FAILURE(status)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); + return false; + } + + if (!cat) + break; + + JSString* str = NewStringCopyN(cx, cat, catSize); + if (!str) + return false; + + element.setString(str); + if (!DefineElement(cx, res, i, element)) + return false; + i++; + } while (true); + + args.rval().setObject(*res); + return true; +} + bool js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) { @@ -2761,6 +3213,9 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) RootedObject numberFormatProto(cx, CreateNumberFormatPrototype(cx, intl, global)); if (!numberFormatProto) return false; + RootedObject pluralRulesProto(cx, CreatePluralRulesPrototype(cx, intl, global)); + if (!pluralRulesProto) + return false; // The |Intl| object is fully set up now, so define the global property. RootedValue intlValue(cx, ObjectValue(*intl)); @@ -2782,6 +3237,7 @@ GlobalObject::initIntlObject(JSContext* cx, Handle global) global->setReservedSlot(COLLATOR_PROTO, ObjectValue(*collatorProto)); global->setReservedSlot(DATE_TIME_FORMAT_PROTO, ObjectValue(*dateTimeFormatProto)); global->setReservedSlot(NUMBER_FORMAT_PROTO, ObjectValue(*numberFormatProto)); + global->setReservedSlot(PLURAL_RULES_PROTO, ObjectValue(*pluralRulesProto)); // Also cache |Intl| to implement spec language that conditions behavior // based on values being equal to "the standard built-in |Intl| object". diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h index 5384d9be1..fd1fc5da6 100644 --- a/js/src/builtin/Intl.h +++ b/js/src/builtin/Intl.h @@ -358,6 +358,54 @@ intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); extern MOZ_MUST_USE bool intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); +/******************** PluralRules ********************/ + +/** + * Returns a new PluralRules instance. + * 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: pluralRules = intl_PluralRules(locales, options) + */ +extern MOZ_MUST_USE bool +intl_PluralRules(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for plural rules + * 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_PluralRules_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_PluralRules_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a plural rule for the number x according to the effective + * locale and the formatting options of the given PluralRules. + * + * A plural rule is a grammatical category that expresses count distinctions + * (such as "one", "two", "few" etc.). + * + * Usage: rule = intl_SelectPluralRule(pluralRules, x) + */ +extern MOZ_MUST_USE bool +intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an array of plural rules categories for a given + * locale and type. + * + * Usage: categories = intl_GetPluralCategories(locale, type) + * + * Example: + * + * intl_getPluralCategories('pl', 'cardinal'); // ['one', 'few', 'many', 'other'] + */ +extern MOZ_MUST_USE bool +intl_GetPluralCategories(JSContext* cx, unsigned argc, Value* vp); + /** * Returns a plain object with calendar information for a single valid locale * (callers must perform this validation). The object will have these diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js index 6391c3e70..281b0f424 100644 --- a/js/src/builtin/Intl.js +++ b/js/src/builtin/Intl.js @@ -21,6 +21,9 @@ intl_availableCalendars: false, intl_patternForSkeleton: false, intl_FormatDateTime: false, + intl_SelectPluralRule: false, + intl_GetPluralCategories: false, + intl_GetCalendarInfo: false, */ /* @@ -857,6 +860,7 @@ function BestAvailableLocaleIgnoringDefault(availableLocales, locale) { return BestAvailableLocaleHelper(availableLocales, locale, false); } +var noRelevantExtensionKeys = []; /** * Compares a BCP 47 language priority list against the set of locales in @@ -1270,7 +1274,9 @@ function initializeIntlObject(obj) { function setLazyData(internals, type, lazyData) { assert(internals.type === "partial", "can't set lazy data for anything but a newborn"); - assert(type === "Collator" || type === "DateTimeFormat" || type == "NumberFormat", "bad type"); + assert(type === "Collator" || type === "DateTimeFormat" || + type == "NumberFormat" || type === "PluralRules", + "bad type"); assert(IsObject(lazyData), "non-object lazy data"); // Set in reverse order so that the .type change is a barrier. @@ -1320,7 +1326,9 @@ function isInitializedIntlObject(obj) { if (IsObject(internals)) { assert(callFunction(std_Object_hasOwnProperty, internals, "type"), "missing type"); var type = internals.type; - assert(type === "partial" || type === "Collator" || type === "DateTimeFormat" || type === "NumberFormat", "unexpected type"); + assert(type === "partial" || type === "Collator" || + type === "DateTimeFormat" || type === "NumberFormat" || type === "PluralRules", + "unexpected type"); assert(callFunction(std_Object_hasOwnProperty, internals, "lazyData"), "missing lazyData"); assert(callFunction(std_Object_hasOwnProperty, internals, "internalProps"), "missing internalProps"); } else { @@ -1377,6 +1385,8 @@ function getInternals(obj) internalProps = resolveCollatorInternals(lazyData) else if (type === "DateTimeFormat") internalProps = resolveDateTimeFormatInternals(lazyData) + else if (type === "PluralRules") + internalProps = resolvePluralRulesInternals(lazyData) else internalProps = resolveNumberFormatInternals(lazyData); setInternalProperties(internals, internalProps); @@ -1776,45 +1786,39 @@ function resolveNumberFormatInternals(lazyNumberFormatData) { // Step 6. var opt = lazyNumberFormatData.opt; - // Compute effective locale. - // Step 9. var NumberFormat = numberFormatInternalProperties; - // Step 10. + // Step 9. var localeData = NumberFormat.localeData; - // Step 11. + // Step 10. var r = ResolveLocale(callFunction(NumberFormat.availableLocales, NumberFormat), lazyNumberFormatData.requestedLocales, lazyNumberFormatData.opt, NumberFormat.relevantExtensionKeys, localeData); - // Steps 12-13. (Step 14 is not relevant to our implementation.) + // Steps 11-12. (Step 13 is not relevant to our implementation.) internalProps.locale = r.locale; internalProps.numberingSystem = r.nu; // Compute formatting options. - // Step 16. + // Step 15. var s = lazyNumberFormatData.style; internalProps.style = s; - // Steps 20, 22. + // Steps 19, 21. if (s === "currency") { internalProps.currency = lazyNumberFormatData.currency; internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay; } - // Step 24. internalProps.minimumIntegerDigits = lazyNumberFormatData.minimumIntegerDigits; - // Steps 27. internalProps.minimumFractionDigits = lazyNumberFormatData.minimumFractionDigits; - // Step 30. internalProps.maximumFractionDigits = lazyNumberFormatData.maximumFractionDigits; - // Step 33. if ("minimumSignificantDigits" in lazyNumberFormatData) { // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the // actual presence (versus undefined-ness) of these properties. @@ -1823,10 +1827,10 @@ function resolveNumberFormatInternals(lazyNumberFormatData) { internalProps.maximumSignificantDigits = lazyNumberFormatData.maximumSignificantDigits; } - // Step 35. + // Step 27. internalProps.useGrouping = lazyNumberFormatData.useGrouping; - // Step 42. + // Step 34. internalProps.boundFormat = undefined; // The caller is responsible for associating |internalProps| with the right @@ -1854,6 +1858,41 @@ function getNumberFormatInternals(obj, methodName) { return internalProps; } +/** + * Applies digit options used for number formatting onto the intl object. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.1. + */ +function SetNumberFormatDigitOptions(lazyData, options, mnfdDefault) { + // We skip Step 1 because we set the properties on a lazyData object. + + // Step 2-3. + assert(IsObject(options), "SetNumberFormatDigitOptions"); + assert(typeof mnfdDefault === "number", "SetNumberFormatDigitOptions"); + + // Steps 4-6. + const mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1); + const mnfd = GetNumberOption(options, "minimumFractionDigits", 0, 20, mnfdDefault); + const mxfd = GetNumberOption(options, "maximumFractionDigits", mnfd, 20); + + // Steps 7-8. + let mnsd = options.minimumSignificantDigits; + let mxsd = options.maximumSignificantDigits; + + // Steps 9-11. + lazyData.minimumIntegerDigits = mnid; + lazyData.minimumFractionDigits = mnfd; + lazyData.maximumFractionDigits = mxfd; + + // Step 12. + if (mnsd !== undefined || mxsd !== undefined) { + mnsd = GetNumberOption(options, "minimumSignificantDigits", 1, 21, 1); + mxsd = GetNumberOption(options, "maximumSignificantDigits", mnsd, 21, 21); + lazyData.minimumSignificantDigits = mnsd; + lazyData.maximumSignificantDigits = mxsd; + } +} + /** * Initializes an object as a NumberFormat. @@ -1903,7 +1942,7 @@ function InitializeNumberFormat(numberFormat, locales, options) { // } // // Note that lazy data is only installed as a final step of initialization, - // so every Collator lazy data object has *all* these properties, never a + // so every NumberFormat lazy data object has *all* these properties, never a // subset of them. var lazyNumberFormatData = std_Object_create(null); @@ -1933,11 +1972,11 @@ function InitializeNumberFormat(numberFormat, locales, options) { opt.localeMatcher = matcher; // Compute formatting options. - // Step 15. + // Step 14. var s = GetOption(options, "style", "string", ["decimal", "percent", "currency"], "decimal"); lazyNumberFormatData.style = s; - // Steps 17-20. + // Steps 16-19. var c = GetOption(options, "currency", "string", undefined, undefined); if (c !== undefined && !IsWellFormedCurrencyCode(c)) ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, c); @@ -1946,54 +1985,36 @@ function InitializeNumberFormat(numberFormat, locales, options) { if (c === undefined) ThrowTypeError(JSMSG_UNDEFINED_CURRENCY); - // Steps 20.a-c. + // Steps 19.a-c. c = toASCIIUpperCase(c); lazyNumberFormatData.currency = c; cDigits = CurrencyDigits(c); } - // Step 21. + // Step 20. var cd = GetOption(options, "currencyDisplay", "string", ["code", "symbol", "name"], "symbol"); if (s === "currency") lazyNumberFormatData.currencyDisplay = cd; - // Step 23. - var mnid = GetNumberOption(options, "minimumIntegerDigits", 1, 21, 1); - lazyNumberFormatData.minimumIntegerDigits = mnid; + // Steps 22-24. + SetNumberFormatDigitOptions(lazyNumberFormatData, options, s === "currency" ? cDigits: 0); - // Steps 25-26. - var mnfdDefault = (s === "currency") ? cDigits : 0; - var mnfd = GetNumberOption(options, "minimumFractionDigits", 0, 20, mnfdDefault); - lazyNumberFormatData.minimumFractionDigits = mnfd; - - // Steps 28-29. - var mxfdDefault; - if (s === "currency") - mxfdDefault = std_Math_max(mnfd, cDigits); - else if (s === "percent") - mxfdDefault = std_Math_max(mnfd, 0); - else - mxfdDefault = std_Math_max(mnfd, 3); - var mxfd = GetNumberOption(options, "maximumFractionDigits", mnfd, 20, mxfdDefault); - lazyNumberFormatData.maximumFractionDigits = mxfd; - - // Steps 31-32. - var mnsd = options.minimumSignificantDigits; - var mxsd = options.maximumSignificantDigits; - - // Step 33. - if (mnsd !== undefined || mxsd !== undefined) { - mnsd = GetNumberOption(options, "minimumSignificantDigits", 1, 21, 1); - mxsd = GetNumberOption(options, "maximumSignificantDigits", mnsd, 21, 21); - lazyNumberFormatData.minimumSignificantDigits = mnsd; - lazyNumberFormatData.maximumSignificantDigits = mxsd; + // Step 25. + if (lazyNumberFormatData.maximumFractionDigits === undefined) { + let mxfdDefault = s === "currency" + ? cDigits + : s === "percent" + ? 0 + : 3; + lazyNumberFormatData.maximumFractionDigits = + std_Math_max(lazyNumberFormatData.minimumFractionDigits, mxfdDefault); } - // Step 34. + // Step 26. var g = GetOption(options, "useGrouping", "boolean", undefined, true); lazyNumberFormatData.useGrouping = g; - // Step 43. + // Steps 35-36. // // We've done everything that must be done now: mark the lazy data as fully // computed and install it. @@ -2990,6 +3011,234 @@ function resolveICUPattern(pattern, result) { } } +/********** Intl.PluralRules **********/ + +/** + * PluralRules internal properties. + * + * Spec: ECMAScript 402 API, PluralRules, 1.3.3. + */ +var pluralRulesInternalProperties = { + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_PluralRules_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + } +}; + +/** + * Compute an internal properties object from |lazyPluralRulesData|. + */ +function resolvePluralRulesInternals(lazyPluralRulesData) { + assert(IsObject(lazyPluralRulesData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var requestedLocales = lazyPluralRulesData.requestedLocales; + + var PluralRules = pluralRulesInternalProperties; + + // Step 13. + const r = ResolveLocale(callFunction(PluralRules.availableLocales, PluralRules), + lazyPluralRulesData.requestedLocales, + lazyPluralRulesData.opt, + noRelevantExtensionKeys, undefined); + + // Step 14. + internalProps.locale = r.locale; + internalProps.type = lazyPluralRulesData.type; + + internalProps.pluralCategories = intl_GetPluralCategories( + internalProps.locale, + internalProps.type); + + internalProps.minimumIntegerDigits = lazyPluralRulesData.minimumIntegerDigits; + internalProps.minimumFractionDigits = lazyPluralRulesData.minimumFractionDigits; + internalProps.maximumFractionDigits = lazyPluralRulesData.maximumFractionDigits; + + if ("minimumSignificantDigits" in lazyPluralRulesData) { + assert("maximumSignificantDigits" in lazyPluralRulesData, "min/max sig digits mismatch"); + internalProps.minimumSignificantDigits = lazyPluralRulesData.minimumSignificantDigits; + internalProps.maximumSignificantDigits = lazyPluralRulesData.maximumSignificantDigits; + } + + return internalProps; +} + +/** + * Returns an object containing the PluralRules internal properties of |obj|, + * or throws a TypeError if |obj| isn't PluralRules-initialized. + */ +function getPluralRulesInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "PluralRules", methodName); + assert(internals.type === "PluralRules", "bad type escaped getIntlObjectInternals"); + + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + internalProps = resolvePluralRulesInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Initializes an object as a PluralRules. + * + * This method is complicated a moderate bit by its implementing initialization + * as a *lazy* concept. Everything that must happen now, does -- but we defer + * all the work we can until the object is actually used as a PluralRules. + * This later work occurs in |resolvePluralRulesInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript 402 API, PluralRules, 1.1.1. + */ +function InitializePluralRules(pluralRules, locales, options) { + assert(IsObject(pluralRules), "InitializePluralRules"); + + // Step 1. + if (isInitializedIntlObject(pluralRules)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + let internals = initializeIntlObject(pluralRules); + + // Lazy PluralRules data has the following structure: + // + // { + // requestedLocales: List of locales, + // type: "cardinal" / "ordinal", + // + // opt: // opt object computer in InitializePluralRules + // { + // localeMatcher: "lookup" / "best fit", + // } + // + // minimumIntegerDigits: integer ∈ [1, 21], + // minimumFractionDigits: integer ∈ [0, 20], + // maximumFractionDigits: integer ∈ [0, 20], + // + // // optional + // minimumSignificantDigits: integer ∈ [1, 21], + // maximumSignificantDigits: integer ∈ [1, 21], + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every PluralRules lazy data object has *all* these properties, never a + // subset of them. + const lazyPluralRulesData = std_Object_create(null); + + // Step 3. + let requestedLocales = CanonicalizeLocaleList(locales); + lazyPluralRulesData.requestedLocales = requestedLocales; + + // Steps 4-5. + if (options === undefined) + options = {}; + else + options = ToObject(options); + + // Step 6. + const type = GetOption(options, "type", "string", ["cardinal", "ordinal"], "cardinal"); + lazyPluralRulesData.type = type; + + // Step 8. + let opt = new Record(); + lazyPluralRulesData.opt = opt; + + // Steps 9-10. + let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + + // Step 11. + SetNumberFormatDigitOptions(lazyPluralRulesData, options, 0); + + // Step 12. + if (lazyPluralRulesData.maximumFractionDigits === undefined) { + lazyPluralRulesData.maximumFractionDigits = + std_Math_max(lazyPluralRulesData.minimumFractionDigits, 3); + } + + setLazyData(internals, "PluralRules", lazyPluralRulesData) +} + +/** + * Returns the subset of the given locale list for which this locale list has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript 402 API, PluralRules, 1.3.2. + */ +function Intl_PluralRules_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 1. + var availableLocales = callFunction(pluralRulesInternalProperties.availableLocales, + pluralRulesInternalProperties); + // Step 2. + let requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Returns a String value representing the plural category matching + * the number passed as value according to the + * effective locale and the formatting options of this PluralRules. + * + * Spec: ECMAScript 402 API, PluralRules, 1.4.3. + */ +function Intl_PluralRules_select(value) { + // Step 1. + let pluralRules = this; + // Step 2. + let internals = getPluralRulesInternals(pluralRules, "select"); + + // Steps 3-4. + let n = ToNumber(value); + + // Step 5. + return intl_SelectPluralRule(pluralRules, n); +} + +/** + * Returns the resolved options for a PluralRules object. + * + * Spec: ECMAScript 402 API, PluralRules, 1.4.4. + */ +function Intl_PluralRules_resolvedOptions() { + var internals = getPluralRulesInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + type: internals.type, + pluralCategories: callFunction(std_Array_slice, internals.pluralCategories, 0), + minimumIntegerDigits: internals.minimumIntegerDigits, + minimumFractionDigits: internals.minimumFractionDigits, + maximumFractionDigits: internals.maximumFractionDigits, + }; + + var optionalProperties = [ + "minimumSignificantDigits", + "maximumSignificantDigits" + ]; + + for (var i = 0; i < optionalProperties.length; i++) { + var p = optionalProperties[i]; + if (callFunction(std_Object_hasOwnProperty, internals, p)) + _DefineDataProperty(result, p, internals[p]); + } + return result; +} + + function Intl_getCanonicalLocales(locales) { let codes = CanonicalizeLocaleList(locales); let result = []; diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h index 789a573dc..a88406bc6 100644 --- a/js/src/vm/CommonPropertyNames.h +++ b/js/src/vm/CommonPropertyNames.h @@ -178,6 +178,7 @@ macro(InitializeCollator, InitializeCollator, "InitializeCollator") \ macro(InitializeDateTimeFormat, InitializeDateTimeFormat, "InitializeDateTimeFormat") \ macro(InitializeNumberFormat, InitializeNumberFormat, "InitializeNumberFormat") \ + macro(InitializePluralRules, InitializePluralRules, "InitializePluralRules") \ macro(innermost, innermost, "innermost") \ macro(inNursery, inNursery, "inNursery") \ macro(input, input, "input") \ @@ -270,6 +271,8 @@ macro(parseInt, parseInt, "parseInt") \ macro(pattern, pattern, "pattern") \ macro(pending, pending, "pending") \ + macro(PluralRules, PluralRules, "PluralRules") \ + macro(PluralRulesSelect, PluralRulesSelect, "Intl_PluralRules_Select") \ macro(public, public_, "public") \ macro(preventExtensions, preventExtensions, "preventExtensions") \ macro(private, private_, "private") \ diff --git a/js/src/vm/GlobalObject.h b/js/src/vm/GlobalObject.h index 011f90aa1..f9c0149f1 100644 --- a/js/src/vm/GlobalObject.h +++ b/js/src/vm/GlobalObject.h @@ -109,6 +109,7 @@ class GlobalObject : public NativeObject COLLATOR_PROTO, NUMBER_FORMAT_PROTO, DATE_TIME_FORMAT_PROTO, + PLURAL_RULES_PROTO, MODULE_PROTO, IMPORT_ENTRY_PROTO, EXPORT_ENTRY_PROTO, @@ -507,6 +508,11 @@ class GlobalObject : public NativeObject return getOrCreateObject(cx, global, DATE_TIME_FORMAT_PROTO, initIntlObject); } + static JSObject* + getOrCreatePluralRulesPrototype(JSContext* cx, Handle global) { + return getOrCreateObject(cx, global, PLURAL_RULES_PROTO, initIntlObject); + } + static bool ensureModulePrototypesCreated(JSContext *cx, Handle global); JSObject* maybeGetModulePrototype() { diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index dc1dfb9fa..df326a69e 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -2605,6 +2605,9 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FN("intl_NumberFormat_availableLocales", intl_NumberFormat_availableLocales, 0,0), JS_FN("intl_numberingSystem", intl_numberingSystem, 1,0), JS_FN("intl_patternForSkeleton", intl_patternForSkeleton, 2,0), + JS_FN("intl_PluralRules_availableLocales", intl_PluralRules_availableLocales, 0,0), + JS_FN("intl_GetPluralCategories", intl_GetPluralCategories, 2, 0), + JS_FN("intl_SelectPluralRule", intl_SelectPluralRule, 2,0), JS_INLINABLE_FN("IsRegExpObject", intrinsic_IsInstanceOfBuiltin, 1,0,