/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set ts=8 sts=2 et sw=2 tw=80: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Implementation of the Intl.PluralRules proposal. */ #include "builtin/intl/PluralRules.h" #include "mozilla/Assertions.h" #include "mozilla/Casting.h" #include "builtin/Array.h" #include "builtin/intl/CommonFunctions.h" #include "builtin/intl/NumberFormat.h" #include "builtin/intl/ScopedICUObject.h" #include "gc/FreeOp.h" #include "js/CharacterEncoding.h" #include "js/PropertySpec.h" #include "unicode/uenum.h" #include "unicode/uloc.h" #include "unicode/unumberformatter.h" #include "unicode/upluralrules.h" #include "unicode/utypes.h" #include "vm/GlobalObject.h" #include "vm/JSContext.h" #include "vm/PlainObject.h" // js::PlainObject #include "vm/StringType.h" #include "vm/JSObject-inl.h" #include "vm/NativeObject-inl.h" using namespace js; using mozilla::AssertedCast; using js::intl::CallICU; using js::intl::IcuLocale; const JSClassOps PluralRulesObject::classOps_ = { nullptr, // addProperty nullptr, // delProperty nullptr, // enumerate nullptr, // newEnumerate nullptr, // resolve nullptr, // mayResolve PluralRulesObject::finalize, // finalize nullptr, // call nullptr, // hasInstance nullptr, // construct nullptr, // trace }; const JSClass PluralRulesObject::class_ = { "Intl.PluralRules", JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) | JSCLASS_HAS_CACHED_PROTO(JSProto_PluralRules) | JSCLASS_FOREGROUND_FINALIZE, &PluralRulesObject::classOps_, &PluralRulesObject::classSpec_}; const JSClass& PluralRulesObject::protoClass_ = PlainObject::class_; static bool pluralRules_toSource(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); args.rval().setString(cx->names().PluralRules); return true; } 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), JS_FN(js_toSource_str, pluralRules_toSource, 0, 0), JS_FS_END}; static const JSPropertySpec pluralRules_properties[] = { JS_STRING_SYM_PS(toStringTag, "Intl.PluralRules", JSPROP_READONLY), JS_PS_END}; static bool PluralRules(JSContext* cx, unsigned argc, Value* vp); const ClassSpec PluralRulesObject::classSpec_ = { GenericCreateConstructor, GenericCreatePrototype, pluralRules_static_methods, nullptr, pluralRules_methods, pluralRules_properties, nullptr, ClassSpec::DontDefineConstructor}; /** * PluralRules constructor. * Spec: ECMAScript 402 API, PluralRules, 13.2.1 */ static bool PluralRules(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); // Step 1. if (!ThrowIfNotConstructing(cx, args, "Intl.PluralRules")) { return false; } // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). RootedObject proto(cx); if (!GetPrototypeFromBuiltinConstructor(cx, args, JSProto_PluralRules, &proto)) { return false; } Rooted pluralRules(cx); pluralRules = NewObjectWithClassProto(cx, proto); if (!pluralRules) { return false; } HandleValue locales = args.get(0); HandleValue options = args.get(1); // Step 3. if (!intl::InitializeObject(cx, pluralRules, cx->names().InitializePluralRules, locales, options)) { return false; } args.rval().setObject(*pluralRules); return true; } void js::PluralRulesObject::finalize(JSFreeOp* fop, JSObject* obj) { MOZ_ASSERT(fop->onMainThread()); auto* pluralRules = &obj->as(); UPluralRules* pr = pluralRules->getPluralRules(); UNumberFormatter* nf = pluralRules->getNumberFormatter(); UFormattedNumber* formatted = pluralRules->getFormattedNumber(); if (pr) { intl::RemoveICUCellMemory( fop, obj, PluralRulesObject::UPluralRulesEstimatedMemoryUse); } if (nf) { intl::RemoveICUCellMemory( fop, obj, PluralRulesObject::UNumberFormatterEstimatedMemoryUse); // UFormattedNumber memory tracked as part of UNumberFormatter. } if (pr) { uplrules_close(pr); } if (nf) { unumf_close(nf); } if (formatted) { unumf_closeResult(formatted); } } /** * This creates a new UNumberFormatter with calculated digit formatting * properties for PluralRules. * * This is similar to NewUNumberFormatter but doesn't allow for currency or * percent types. */ static UNumberFormatter* NewUNumberFormatterForPluralRules( JSContext* cx, Handle pluralRules) { RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); if (!internals) { return nullptr; } RootedValue value(cx); if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { return nullptr; } UniqueChars locale = intl::EncodeLocale(cx, value.toString()); if (!locale) { return nullptr; } intl::NumberFormatterSkeleton skeleton(cx); bool hasMinimumSignificantDigits; if (!HasProperty(cx, internals, cx->names().minimumSignificantDigits, &hasMinimumSignificantDigits)) { return nullptr; } if (hasMinimumSignificantDigits) { if (!GetProperty(cx, internals, internals, cx->names().minimumSignificantDigits, &value)) { return nullptr; } uint32_t minimumSignificantDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().maximumSignificantDigits, &value)) { return nullptr; } uint32_t maximumSignificantDigits = AssertedCast(value.toInt32()); if (!skeleton.significantDigits(minimumSignificantDigits, maximumSignificantDigits)) { return nullptr; } } else { if (!GetProperty(cx, internals, internals, cx->names().minimumFractionDigits, &value)) { return nullptr; } uint32_t minimumFractionDigits = AssertedCast(value.toInt32()); if (!GetProperty(cx, internals, internals, cx->names().maximumFractionDigits, &value)) { return nullptr; } uint32_t maximumFractionDigits = AssertedCast(value.toInt32()); if (!skeleton.fractionDigits(minimumFractionDigits, maximumFractionDigits)) { return nullptr; } } if (!GetProperty(cx, internals, internals, cx->names().minimumIntegerDigits, &value)) { return nullptr; } uint32_t minimumIntegerDigits = AssertedCast(value.toInt32()); if (!skeleton.integerWidth(minimumIntegerDigits)) { return nullptr; } if (!skeleton.roundingModeHalfUp()) { return nullptr; } return skeleton.toFormatter(cx, locale.get()); } static UFormattedNumber* NewUFormattedNumberForPluralRules(JSContext* cx) { UErrorCode status = U_ZERO_ERROR; UFormattedNumber* formatted = unumf_openResult(&status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return nullptr; } return formatted; } /** * Returns a new UPluralRules with the locale and type options of the given * PluralRules. */ static UPluralRules* NewUPluralRules(JSContext* cx, Handle pluralRules) { RootedObject internals(cx, intl::GetInternalsObject(cx, pluralRules)); if (!internals) { return nullptr; } RootedValue value(cx); if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) { return nullptr; } UniqueChars locale = intl::EncodeLocale(cx, value.toString()); if (!locale) { return nullptr; } if (!GetProperty(cx, internals, internals, cx->names().type, &value)) { return nullptr; } UPluralType category; { JSLinearString* type = value.toString()->ensureLinear(cx); if (!type) { return nullptr; } if (StringEqualsLiteral(type, "cardinal")) { category = UPLURAL_TYPE_CARDINAL; } else { MOZ_ASSERT(StringEqualsLiteral(type, "ordinal")); category = UPLURAL_TYPE_ORDINAL; } } UErrorCode status = U_ZERO_ERROR; UPluralRules* pr = uplrules_openForType(IcuLocale(locale.get()), category, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return nullptr; } return pr; } bool js::intl_SelectPluralRule(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); MOZ_ASSERT(args.length() == 2); Rooted pluralRules( cx, &args[0].toObject().as()); double x = args[1].toNumber(); // Obtain a cached UPluralRules object. UPluralRules* pr = pluralRules->getPluralRules(); if (!pr) { pr = NewUPluralRules(cx, pluralRules); if (!pr) { return false; } pluralRules->setPluralRules(pr); intl::AddICUCellMemory(pluralRules, PluralRulesObject::UPluralRulesEstimatedMemoryUse); } // Obtain a cached UNumberFormat object. UNumberFormatter* nf = pluralRules->getNumberFormatter(); if (!nf) { nf = NewUNumberFormatterForPluralRules(cx, pluralRules); if (!nf) { return false; } pluralRules->setNumberFormatter(nf); intl::AddICUCellMemory( pluralRules, PluralRulesObject::UNumberFormatterEstimatedMemoryUse); } // Obtain a cached UFormattedNumber object. UFormattedNumber* formatted = pluralRules->getFormattedNumber(); if (!formatted) { formatted = NewUFormattedNumberForPluralRules(cx); if (!formatted) { return false; } pluralRules->setFormattedNumber(formatted); // UFormattedNumber memory tracked as part of UNumberFormatter. } UErrorCode status = U_ZERO_ERROR; unumf_formatDouble(nf, x, formatted, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } JSString* str = CallICU( cx, [pr, formatted](UChar* chars, int32_t size, UErrorCode* status) { return uplrules_selectFormatted(pr, formatted, chars, size, status); }); 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() == 1); Rooted pluralRules( cx, &args[0].toObject().as()); // Obtain a cached UPluralRules object. UPluralRules* pr = pluralRules->getPluralRules(); if (!pr) { pr = NewUPluralRules(cx, pluralRules); if (!pr) { return false; } pluralRules->setPluralRules(pr); intl::AddICUCellMemory(pluralRules, PluralRulesObject::UPluralRulesEstimatedMemoryUse); } UErrorCode status = U_ZERO_ERROR; UEnumeration* ue = uplrules_getKeywords(pr, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } ScopedICUObject closeEnum(ue); RootedObject res(cx, NewDenseEmptyArray(cx)); if (!res) { return false; } do { int32_t catSize; const char* cat = uenum_next(ue, &catSize, &status); if (U_FAILURE(status)) { intl::ReportInternalError(cx); return false; } if (!cat) { break; } MOZ_ASSERT(catSize >= 0); JSString* str = NewStringCopyN(cx, cat, catSize); if (!str) { return false; } if (!NewbornArrayPush(cx, res, StringValue(str))) { return false; } } while (true); args.rval().setObject(*res); return true; }