diff --git a/js/src/builtin/Intl.cpp b/js/src/builtin/Intl.cpp deleted file mode 100644 index 494a0c0473..0000000000 --- a/js/src/builtin/Intl.cpp +++ /dev/null @@ -1,3995 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- - * 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/. */ - -/* - * The Intl module specified by standard ECMA-402, - * ECMAScript Internationalization API Specification. - */ - -#include "builtin/Intl.h" - -#include "mozilla/Casting.h" -#include "mozilla/FloatingPoint.h" -#include "mozilla/PodOperations.h" -#include "mozilla/Range.h" - -#include - -#include "jsapi.h" -#include "jsatom.h" -#include "jscntxt.h" -#include "jsobj.h" - -#include "builtin/IntlTimeZoneData.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/GlobalObject.h" -#include "vm/Interpreter.h" -#include "vm/Stack.h" -#include "vm/StringBuffer.h" -#include "vm/Unicode.h" - -#include "jsobjinlines.h" - -#include "vm/NativeObject-inl.h" - -using namespace js; - -using mozilla::AssertedCast; -using mozilla::IsFinite; -using mozilla::IsNaN; -using mozilla::IsNegativeZero; -using mozilla::PodCopy; - - -/* - * Pervasive note: ICU functions taking a UErrorCode in/out parameter always - * test that parameter before doing anything, and will return immediately if - * the value indicates that a failure occurred in a prior ICU call, - * without doing anything else. See - * http://userguide.icu-project.org/design#TOC-Error-Handling - */ - -/******************** Common to Intl constructors ********************/ - -static bool -IntlInitialize(JSContext* cx, HandleObject obj, Handle initializer, - HandleValue locales, HandleValue options) -{ - RootedValue initializerValue(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), initializer, &initializerValue)) - return false; - MOZ_ASSERT(initializerValue.isObject()); - MOZ_ASSERT(initializerValue.toObject().is()); - - FixedInvokeArgs<3> args(cx); - - args[0].setObject(*obj); - args[1].set(locales); - args[2].set(options); - - RootedValue thisv(cx, NullValue()); - RootedValue ignored(cx); - return js::Call(cx, initializerValue, thisv, args, &ignored); -} - -static bool -CreateDefaultOptions(JSContext* cx, MutableHandleValue defaultOptions) -{ - RootedObject options(cx, NewObjectWithGivenProto(cx, nullptr)); - if (!options) - return false; - defaultOptions.setObject(*options); - return true; -} - -// CountAvailable and GetAvailable describe the signatures used for ICU API -// to determine available locales for various functionality. -typedef int32_t -(* CountAvailable)(); - -typedef const char* -(* GetAvailable)(int32_t localeIndex); - -static bool -intl_availableLocales(JSContext* cx, CountAvailable countAvailable, - GetAvailable getAvailable, MutableHandleValue result) -{ - RootedObject locales(cx, NewObjectWithGivenProto(cx, nullptr)); - if (!locales) - return false; - - uint32_t count = countAvailable(); - RootedValue t(cx, BooleanValue(true)); - for (uint32_t i = 0; i < count; i++) { - const char* locale = getAvailable(i); - auto lang = DuplicateString(cx, locale); - if (!lang) - return false; - char* p; - while ((p = strchr(lang.get(), '_'))) - *p = '-'; - RootedAtom a(cx, Atomize(cx, lang.get(), strlen(lang.get()))); - if (!a) - return false; - if (!DefineProperty(cx, locales, a->asPropertyName(), t, nullptr, nullptr, - JSPROP_ENUMERATE)) - { - return false; - } - } - - result.setObject(*locales); - return true; -} - -/** - * Returns the object holding the internal properties for obj. - */ -static JSObject* -GetInternals(JSContext* cx, HandleObject obj) -{ - RootedValue getInternalsValue(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().getInternals, - &getInternalsValue)) - { - return nullptr; - } - MOZ_ASSERT(getInternalsValue.isObject()); - MOZ_ASSERT(getInternalsValue.toObject().is()); - - FixedInvokeArgs<1> args(cx); - - args[0].setObject(*obj); - - RootedValue v(cx, NullValue()); - if (!js::Call(cx, getInternalsValue, v, args, &v)) - return nullptr; - - return &v.toObject(); -} - -static bool -equal(const char* s1, const char* s2) -{ - return !strcmp(s1, s2); -} - -static bool -equal(JSAutoByteString& s1, const char* s2) -{ - return !strcmp(s1.ptr(), s2); -} - -static const char* -icuLocale(const char* locale) -{ - if (equal(locale, "und")) - return ""; // ICU root locale - return locale; -} - -// Simple RAII for ICU objects. Unfortunately, ICU's C++ API is uniformly -// unstable, so we can't use its smart pointers for this. -template -class ScopedICUObject -{ - T* ptr_; - - public: - explicit ScopedICUObject(T* ptr) - : ptr_(ptr) - {} - - ~ScopedICUObject() { - if (ptr_) - Delete(ptr_); - } - - // In cases where an object should be deleted on abnormal exits, - // but returned to the caller if everything goes well, call forget() - // to transfer the object just before returning. - T* forget() { - T* tmp = ptr_; - ptr_ = nullptr; - return tmp; - } -}; - -// The inline capacity we use for the char16_t Vectors. -static const size_t INITIAL_CHAR_BUFFER_SIZE = 32; - -/******************** Collator ********************/ - -static void collator_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UCOLLATOR_SLOT = 0; -static const uint32_t COLLATOR_SLOTS_COUNT = 1; - -static const ClassOps CollatorClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - collator_finalize -}; - -static const Class CollatorClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(COLLATOR_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &CollatorClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -collator_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().Collator); - return true; -} -#endif - -static const JSFunctionSpec collator_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec collator_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, collator_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 10.1.2 Intl.Collator([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -Collator(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - // We're following ECMA-402 1st Edition when Collator is called because of - // backward compatibility issues. - // See https://github.com/tc39/ecma402/issues/57 - if (!construct) { - // ES Intl 1st ed., 10.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., 10.1.2.1 step 4 - obj = ToObject(cx, self); - if (!obj) - return false; - - // ES Intl 1st ed., 10.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., 10.1.2.1 step 3.a - construct = true; - } - } - if (construct) { - // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global()); - if (!proto) - return false; - } - - obj = NewObjectWithGivenProto(cx, &CollatorClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UCOLLATOR_SLOT, PrivateValue(nullptr)); - } - - RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); - RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); - - // Step 6. - if (!IntlInitialize(cx, obj, cx->names().InitializeCollator, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -Collator(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return Collator(cx, args, args.isConstructing()); -} - -bool -js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_Collator 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 Collator(cx, args, true); -} - -static void -collator_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(UCOLLATOR_SLOT); - if (!slot.isUndefined()) { - if (UCollator* coll = static_cast(slot.toPrivate())) - ucol_close(coll); - } -} - -static JSObject* -CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx, GlobalObject::createConstructor(cx, &Collator, cx->names().Collator, - 0)); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &CollatorClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UCOLLATOR_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 10.2.2 - if (!JS_DefineFunctions(cx, ctor, collator_static_methods)) - return nullptr; - - // 10.3.2 and 10.3.3 - if (!JS_DefineFunctions(cx, proto, collator_methods)) - return nullptr; - - /* - * Install the getter for Collator.prototype.compare, which returns a bound - * comparison function for the specified Collator object (suitable for - * passing to methods like Array.prototype.sort). - */ - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().CollatorCompareGet, &getter)) - return nullptr; - if (!DefineProperty(cx, proto, cx->names().compare, UndefinedHandleValue, - JS_DATA_TO_FUNC_PTR(JSGetterOp, &getter.toObject()), - nullptr, JSPROP_GETTER | JSPROP_SHARED)) - { - return nullptr; - } - - RootedValue options(cx); - if (!CreateDefaultOptions(cx, &options)) - return nullptr; - - // 10.2.1 and 10.3 - if (!IntlInitialize(cx, proto, cx->names().InitializeCollator, UndefinedHandleValue, options)) - return nullptr; - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().Collator, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!intl_availableLocales(cx, ucol_countAvailable, ucol_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -bool -js::intl_availableCollations(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; - UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.ptr(), false, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(values); - - uint32_t count = uenum_count(values, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - RootedObject collations(cx, NewDenseEmptyArray(cx)); - if (!collations) - return false; - - uint32_t index = 0; - for (uint32_t i = 0; i < count; i++) { - const char* collation = uenum_next(values, nullptr, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - // Per ECMA-402, 10.2.3, we don't include standard and search: - // "The values 'standard' and 'search' must not be used as elements in - // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co - // array." - if (equal(collation, "standard") || equal(collation, "search")) - continue; - - // ICU returns old-style keyword values; map them to BCP 47 equivalents - // (see http://bugs.icu-project.org/trac/ticket/9620). - if (equal(collation, "dictionary")) - collation = "dict"; - else if (equal(collation, "gb2312han")) - collation = "gb2312"; - else if (equal(collation, "phonebook")) - collation = "phonebk"; - else if (equal(collation, "traditional")) - collation = "trad"; - - RootedString jscollation(cx, JS_NewStringCopyZ(cx, collation)); - if (!jscollation) - return false; - RootedValue element(cx, StringValue(jscollation)); - if (!DefineElement(cx, collations, index++, element)) - return false; - } - - args.rval().setObject(*collations); - return true; -} - -/** - * Returns a new UCollator with the locale and collation options - * of the given Collator. - */ -static UCollator* -NewUCollator(JSContext* cx, HandleObject collator) -{ - RootedValue value(cx); - - RootedObject internals(cx, GetInternals(cx, collator)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // UCollator options with default values. - UColAttributeValue uStrength = UCOL_DEFAULT; - UColAttributeValue uCaseLevel = UCOL_OFF; - UColAttributeValue uAlternate = UCOL_DEFAULT; - UColAttributeValue uNumeric = UCOL_OFF; - // Normalization is always on to meet the canonical equivalence requirement. - UColAttributeValue uNormalization = UCOL_ON; - UColAttributeValue uCaseFirst = UCOL_DEFAULT; - - if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) - return nullptr; - JSAutoByteString usage(cx, value.toString()); - if (!usage) - return nullptr; - if (equal(usage, "search")) { - // ICU expects search as a Unicode locale extension on locale. - // Unicode locale extensions must occur before private use extensions. - const char* oldLocale = locale.ptr(); - const char* p; - size_t index; - size_t localeLen = strlen(oldLocale); - if ((p = strstr(oldLocale, "-x-"))) - index = p - oldLocale; - else - index = localeLen; - - const char* insert; - if ((p = strstr(oldLocale, "-u-")) && static_cast(p - oldLocale) < index) { - index = p - oldLocale + 2; - insert = "-co-search"; - } else { - insert = "-u-co-search"; - } - size_t insertLen = strlen(insert); - char* newLocale = cx->pod_malloc(localeLen + insertLen + 1); - if (!newLocale) - return nullptr; - memcpy(newLocale, oldLocale, index); - memcpy(newLocale + index, insert, insertLen); - memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0' - locale.clear(); - locale.initBytes(newLocale); - } - - // We don't need to look at the collation property - it can only be set - // via the Unicode locale extension and is therefore already set on - // locale. - - if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) - return nullptr; - JSAutoByteString sensitivity(cx, value.toString()); - if (!sensitivity) - return nullptr; - if (equal(sensitivity, "base")) { - uStrength = UCOL_PRIMARY; - } else if (equal(sensitivity, "accent")) { - uStrength = UCOL_SECONDARY; - } else if (equal(sensitivity, "case")) { - uStrength = UCOL_PRIMARY; - uCaseLevel = UCOL_ON; - } else { - MOZ_ASSERT(equal(sensitivity, "variant")); - uStrength = UCOL_TERTIARY; - } - - if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value)) - return nullptr; - // According to the ICU team, UCOL_SHIFTED causes punctuation to be - // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data - // Markup Language, "shifted" causes whitespace and punctuation to be - // ignored - that's a bit more than asked for, but there's no way to get - // less. - if (value.toBoolean()) - uAlternate = UCOL_SHIFTED; - - if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) - return nullptr; - if (!value.isUndefined() && value.toBoolean()) - uNumeric = UCOL_ON; - - if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) - return nullptr; - if (!value.isUndefined()) { - JSAutoByteString caseFirst(cx, value.toString()); - if (!caseFirst) - return nullptr; - if (equal(caseFirst, "upper")) - uCaseFirst = UCOL_UPPER_FIRST; - else if (equal(caseFirst, "lower")) - uCaseFirst = UCOL_LOWER_FIRST; - else - MOZ_ASSERT(equal(caseFirst, "false")); - } - - UErrorCode status = U_ZERO_ERROR; - UCollator* coll = ucol_open(icuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return nullptr; - } - - ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status); - ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status); - ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status); - ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status); - ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status); - ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status); - if (U_FAILURE(status)) { - ucol_close(coll); - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return nullptr; - } - - return coll; -} - -static bool -intl_CompareStrings(JSContext* cx, UCollator* coll, HandleString str1, HandleString str2, - MutableHandleValue result) -{ - MOZ_ASSERT(str1); - MOZ_ASSERT(str2); - - if (str1 == str2) { - result.setInt32(0); - return true; - } - - AutoStableStringChars stableChars1(cx); - if (!stableChars1.initTwoByte(cx, str1)) - return false; - - AutoStableStringChars stableChars2(cx); - if (!stableChars2.initTwoByte(cx, str2)) - return false; - - mozilla::Range chars1 = stableChars1.twoByteRange(); - mozilla::Range chars2 = stableChars2.twoByteRange(); - - UCollationResult uresult = ucol_strcoll(coll, - Char16ToUChar(chars1.begin().get()), chars1.length(), - Char16ToUChar(chars2.begin().get()), chars2.length()); - int32_t res; - switch (uresult) { - case UCOL_LESS: res = -1; break; - case UCOL_EQUAL: res = 0; break; - case UCOL_GREATER: res = 1; break; - default: MOZ_CRASH("ucol_strcoll returned bad UCollationResult"); - } - result.setInt32(res); - return true; -} - -bool -js::intl_CompareStrings(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].isString()); - MOZ_ASSERT(args[2].isString()); - - RootedObject collator(cx, &args[0].toObject()); - - // Obtain a UCollator object, cached if possible. - // XXX Does this handle Collator instances from other globals correctly? - bool isCollatorInstance = collator->getClass() == &CollatorClass; - UCollator* coll; - if (isCollatorInstance) { - void* priv = collator->as().getReservedSlot(UCOLLATOR_SLOT).toPrivate(); - coll = static_cast(priv); - if (!coll) { - coll = NewUCollator(cx, collator); - if (!coll) - return false; - collator->as().setReservedSlot(UCOLLATOR_SLOT, PrivateValue(coll)); - } - } else { - // There's no good place to cache the ICU collator for an object - // that has been initialized as a Collator but is not a Collator - // instance. One possibility might be to add a Collator instance as an - // internal property to each such object. - coll = NewUCollator(cx, collator); - if (!coll) - return false; - } - - // Use the UCollator to actually compare the strings. - RootedString str1(cx, args[1].toString()); - RootedString str2(cx, args[2].toString()); - RootedValue result(cx); - bool success = intl_CompareStrings(cx, coll, str1, str2, &result); - - if (!isCollatorInstance) - ucol_close(coll); - if (!success) - return false; - args.rval().set(result); - 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().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 (!IntlInitialize(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().getReservedSlot(UNUMBER_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (UNumberFormat* nf = static_cast(slot.toPrivate())) - unum_close(nf); - } -} - -static JSObject* -CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle 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 (!CreateDefaultOptions(cx, &options)) - return nullptr; - - // 11.2.1 and 11.3 - if (!IntlInitialize(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 (!intl_availableLocales(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)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - ScopedICUObject 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 - * 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. - */ -static UNumberFormat* -NewUNumberFormat(JSContext* cx, HandleObject numberFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, GetInternals(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 (equal(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 (equal(currencyDisplay, "code")) { - uStyle = UNUM_CURRENCY_ISO; - } else if (equal(currencyDisplay, "symbol")) { - uStyle = UNUM_CURRENCY; - } else { - MOZ_ASSERT(equal(currencyDisplay, "name")); - uStyle = UNUM_CURRENCY_PLURAL; - } - } else if (equal(style, "percent")) { - uStyle = UNUM_PERCENT; - } else { - MOZ_ASSERT(equal(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(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()); - } - - 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)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return nullptr; - } - ScopedICUObject toClose(nf); - - if (uCurrency) { - unum_setTextAttribute(nf, UNUM_CURRENCY_CODE, uCurrency, 3, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - 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; - -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; - - MOZ_ASSERT(formattedChars.length() == 0, - "formattedChars must initially be empty"); - MOZ_ALWAYS_TRUE(formattedChars.resize(INITIAL_CHAR_BUFFER_SIZE)); - UErrorCode status = U_ZERO_ERROR; - - int32_t resultSize; - resultSize = - unum_formatDoubleForFields(nf, *x, - Char16ToUChar(formattedChars.begin()), INITIAL_CHAR_BUFFER_SIZE, - fpositer, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!formattedChars.resize(size_t(resultSize))) - return false; - status = U_ZERO_ERROR; -#ifdef DEBUG - int32_t size = -#endif - unum_formatDoubleForFields(nf, *x, Char16ToUChar(formattedChars.begin()), resultSize, - fpositer, &status); - MOZ_ASSERT(size == resultSize); - } - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - return formattedChars.resize(size_t(resultSize)); -} - -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(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)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - MOZ_ASSERT(fpositer); - ScopedICUObject 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(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; - 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 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(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().getReservedSlot(UNUMBER_FORMAT_SLOT).toPrivate(); - nf = static_cast(priv); - if (!nf) { - nf = NewUNumberFormat(cx, numberFormat); - if (!nf) - return false; - numberFormat->as().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 ********************/ - -static void dateTimeFormat_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t UDATE_FORMAT_SLOT = 0; -static const uint32_t DATE_TIME_FORMAT_SLOTS_COUNT = 1; - -static const ClassOps DateTimeFormatClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* setProperty */ - nullptr, /* enumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - dateTimeFormat_finalize -}; - -static const Class DateTimeFormatClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(DATE_TIME_FORMAT_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &DateTimeFormatClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().DateTimeFormat); - return true; -} -#endif - -static const JSFunctionSpec dateTimeFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec dateTimeFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -/** - * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) - * - * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b - */ -static bool -DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) -{ - RootedObject obj(cx); - - // We're following ECMA-402 1st Edition when DateTimeFormat is called - // because of backward compatibility issues. - // See https://github.com/tc39/ecma402/issues/57 - if (!construct) { - // ES Intl 1st ed., 12.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., 12.1.2.1 step 4 - obj = ToObject(cx, self); - if (!obj) - return false; - - // ES Intl 1st ed., 12.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., 12.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::getOrCreateDateTimeFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - obj = NewObjectWithGivenProto(cx, &DateTimeFormatClass, proto); - if (!obj) - return false; - - obj->as().setReservedSlot(UDATE_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 (!IntlInitialize(cx, obj, cx->names().InitializeDateTimeFormat, locales, options)) - return false; - - args.rval().setObject(*obj); - return true; -} - -static bool -DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - return DateTimeFormat(cx, args, args.isConstructing()); -} - -bool -js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(!args.isConstructing()); - // intl_DateTimeFormat 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 DateTimeFormat(cx, args, true); -} - -static void -dateTimeFormat_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(UDATE_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (UDateFormat* df = static_cast(slot.toPrivate())) - udat_close(df); - } -} - -static JSObject* -CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = GlobalObject::createConstructor(cx, &DateTimeFormat, cx->names().DateTimeFormat, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, - &DateTimeFormatClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - // 12.2.2 - if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) - return nullptr; - - // 12.3.2 and 12.3.3 - if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) - return nullptr; - - // Install a getter for DateTimeFormat.prototype.format that returns a - // formatting function bound to a specified DateTimeFormat object (suitable - // for passing to methods like Array.prototype.map). - RootedValue getter(cx); - if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, - &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 (!CreateDefaultOptions(cx, &options)) - return nullptr; - - // 12.2.1 and 12.3 - if (!IntlInitialize(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - - // 8.1 - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) - return nullptr; - - return proto; -} - -bool -js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - RootedValue result(cx); - if (!intl_availableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) - return false; - args.rval().set(result); - return true; -} - -// ICU returns old-style keyword values; map them to BCP 47 equivalents -// (see http://bugs.icu-project.org/trac/ticket/9620). -static const char* -bcp47CalendarName(const char* icuName) -{ - if (equal(icuName, "ethiopic-amete-alem")) - return "ethioaa"; - if (equal(icuName, "gregorian")) - return "gregory"; - if (equal(icuName, "islamic-civil")) - return "islamicc"; - return icuName; -} - -bool -js::intl_availableCalendars(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; - - RootedObject calendars(cx, NewDenseEmptyArray(cx)); - if (!calendars) - return false; - uint32_t index = 0; - - // We need the default calendar for the locale as the first result. - UErrorCode status = U_ZERO_ERROR; - RootedString jscalendar(cx); - { - UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); - - // This correctly handles nullptr |cal| when opening failed. - ScopedICUObject closeCalendar(cal); - - const char* calendar = ucal_getType(cal, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); - if (!jscalendar) - return false; - } - - RootedValue element(cx, StringValue(jscalendar)); - if (!DefineElement(cx, calendars, index++, element)) - return false; - - // Now get the calendars that "would make a difference", i.e., not the default. - UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(values); - - uint32_t count = uenum_count(values, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - for (; count > 0; count--) { - const char* calendar = uenum_next(values, nullptr, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); - if (!jscalendar) - return false; - element = StringValue(jscalendar); - if (!DefineElement(cx, calendars, index++, element)) - return false; - } - - args.rval().setObject(*calendars); - return true; -} - -template -static constexpr Char -ToUpperASCII(Char c) -{ - return ('a' <= c && c <= 'z') - ? (c & ~0x20) - : c; -} - -static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); -static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly"); -static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly"); -static_assert(ToUpperASCII(u'a') == u'A', "verifying u'a' uppercases correctly"); -static_assert(ToUpperASCII(u'k') == u'K', "verifying u'k' uppercases correctly"); -static_assert(ToUpperASCII(u'z') == u'Z', "verifying u'z' uppercases correctly"); - -template -static bool -EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, size_t len) -{ - for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) { - if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) - return false; - } - return true; -} - -template -static js::HashNumber -HashStringIgnoreCaseASCII(const Char* s, size_t length) -{ - uint32_t hash = 0; - for (size_t i = 0; i < length; i++) - hash = mozilla::AddToHash(hash, ToUpperASCII(s[i])); - return hash; -} - -js::SharedIntlData::TimeZoneHasher::Lookup::Lookup(JSFlatString* timeZone) - : isLatin1(timeZone->hasLatin1Chars()), length(timeZone->length()) -{ - if (isLatin1) { - latin1Chars = timeZone->latin1Chars(nogc); - hash = HashStringIgnoreCaseASCII(latin1Chars, length); - } else { - twoByteChars = timeZone->twoByteChars(nogc); - hash = HashStringIgnoreCaseASCII(twoByteChars, length); - } -} - -bool -js::SharedIntlData::TimeZoneHasher::match(TimeZoneName key, const Lookup& lookup) -{ - if (key->length() != lookup.length) - return false; - - // Compare time zone names ignoring ASCII case differences. - if (key->hasLatin1Chars()) { - const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); - if (lookup.isLatin1) - return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars, lookup.length); - return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); - } - - const char16_t* keyChars = key->twoByteChars(lookup.nogc); - if (lookup.isLatin1) - return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars, lookup.length); - return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); -} - -static bool -IsLegacyICUTimeZone(const char* timeZone) -{ - for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { - if (equal(timeZone, legacyTimeZone)) - return true; - } - return false; -} - -bool -js::SharedIntlData::ensureTimeZones(JSContext* cx) -{ - if (timeZoneDataInitialized) - return true; - - // If initTimeZones() was called previously, but didn't complete due to - // OOM, clear all sets/maps and start from scratch. - if (availableTimeZones.initialized()) - availableTimeZones.finish(); - if (!availableTimeZones.init()) { - ReportOutOfMemory(cx); - return false; - } - - UErrorCode status = U_ZERO_ERROR; - UEnumeration* values = ucal_openTimeZones(&status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(values); - - RootedAtom timeZone(cx); - while (true) { - int32_t size; - const char* rawTimeZone = uenum_next(values, &size, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - if (rawTimeZone == nullptr) - break; - - // Skip legacy ICU time zone names. - if (IsLegacyICUTimeZone(rawTimeZone)) - continue; - - MOZ_ASSERT(size >= 0); - timeZone = Atomize(cx, rawTimeZone, size_t(size)); - if (!timeZone) - return false; - - TimeZoneHasher::Lookup lookup(timeZone); - TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup); - - // ICU shouldn't report any duplicate time zone names, but if it does, - // just ignore the duplicate name. - if (!p && !availableTimeZones.add(p, timeZone)) { - ReportOutOfMemory(cx); - return false; - } - } - - if (ianaZonesTreatedAsLinksByICU.initialized()) - ianaZonesTreatedAsLinksByICU.finish(); - if (!ianaZonesTreatedAsLinksByICU.init()) { - ReportOutOfMemory(cx); - return false; - } - - for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) { - MOZ_ASSERT(rawTimeZone != nullptr); - timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone)); - if (!timeZone) - return false; - - TimeZoneHasher::Lookup lookup(timeZone); - TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup); - MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU"); - - if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) { - ReportOutOfMemory(cx); - return false; - } - } - - if (ianaLinksCanonicalizedDifferentlyByICU.initialized()) - ianaLinksCanonicalizedDifferentlyByICU.finish(); - if (!ianaLinksCanonicalizedDifferentlyByICU.init()) { - ReportOutOfMemory(cx); - return false; - } - - RootedAtom linkName(cx); - RootedAtom& target = timeZone; - for (const auto& linkAndTarget : timezone::ianaLinksCanonicalizedDifferentlyByICU) { - const char* rawLinkName = linkAndTarget.link; - const char* rawTarget = linkAndTarget.target; - - MOZ_ASSERT(rawLinkName != nullptr); - linkName = Atomize(cx, rawLinkName, strlen(rawLinkName)); - if (!linkName) - return false; - - MOZ_ASSERT(rawTarget != nullptr); - target = Atomize(cx, rawTarget, strlen(rawTarget)); - if (!target) - return false; - - TimeZoneHasher::Lookup lookup(linkName); - TimeZoneMap::AddPtr p = ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup); - MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU"); - - if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) { - ReportOutOfMemory(cx); - return false; - } - } - - MOZ_ASSERT(!timeZoneDataInitialized, "ensureTimeZones is neither reentrant nor thread-safe"); - timeZoneDataInitialized = true; - - return true; -} - -bool -js::SharedIntlData::validateTimeZoneName(JSContext* cx, HandleString timeZone, - MutableHandleString result) -{ - if (!ensureTimeZones(cx)) - return false; - - Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); - if (!timeZoneFlat) - return false; - - TimeZoneHasher::Lookup lookup(timeZoneFlat); - if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) - result.set(*p); - - return true; -} - -bool -js::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, HandleString timeZone, - MutableHandleString result) -{ - if (!ensureTimeZones(cx)) - return false; - - Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); - if (!timeZoneFlat) - return false; - - TimeZoneHasher::Lookup lookup(timeZoneFlat); - MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name"); - - if (TimeZoneMap::Ptr p = ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) { - // The effectively supported time zones aren't known at compile time, - // when - // 1. SpiderMonkey was compiled with "--with-system-icu". - // 2. ICU's dynamic time zone data loading feature was used. - // (ICU supports loading time zone files at runtime through the - // ICU_TIMEZONE_FILES_DIR environment variable.) - // Ensure ICU supports the new target zone before applying the update. - TimeZoneName targetTimeZone = p->value(); - TimeZoneHasher::Lookup targetLookup(targetTimeZone); - if (availableTimeZones.has(targetLookup)) - result.set(targetTimeZone); - } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) { - result.set(*p); - } - - return true; -} - -void -js::SharedIntlData::destroyInstance() -{ - availableTimeZones.finish(); - ianaZonesTreatedAsLinksByICU.finish(); - ianaLinksCanonicalizedDifferentlyByICU.finish(); -} - -void -js::SharedIntlData::trace(JSTracer* trc) -{ - // Atoms are always tenured. - if (!trc->runtime()->isHeapMinorCollecting()) { - availableTimeZones.trace(trc); - ianaZonesTreatedAsLinksByICU.trace(trc); - ianaLinksCanonicalizedDifferentlyByICU.trace(trc); - } -} - -size_t -js::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const -{ - return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + - ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + - ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf); -} - -bool -js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - RootedString timeZone(cx, args[0].toString()); - RootedString validatedTimeZone(cx); - if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) - return false; - - if (validatedTimeZone) - args.rval().setString(validatedTimeZone); - else - args.rval().setNull(); - - return true; -} - -bool -js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - MOZ_ASSERT(args[0].isString()); - - SharedIntlData& sharedIntlData = cx->sharedIntlData; - - // Some time zone names are canonicalized differently by ICU -- handle - // those first: - RootedString timeZone(cx, args[0].toString()); - RootedString ianaTimeZone(cx); - if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) - return false; - - if (ianaTimeZone) { - args.rval().setString(ianaTimeZone); - return true; - } - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, timeZone)) - return false; - - mozilla::Range tzchars = stableChars.twoByteRange(); - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UBool* isSystemID = nullptr; - UErrorCode status = U_ZERO_ERROR; - int32_t size = ucal_getCanonicalTimeZoneID(Char16ToUChar(tzchars.begin().get()), - tzchars.length(), Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, isSystemID, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - MOZ_ASSERT(size >= 0); - if (!chars.resize(size_t(size))) - return false; - status = U_ZERO_ERROR; - ucal_getCanonicalTimeZoneID(Char16ToUChar(tzchars.begin().get()), tzchars.length(), - Char16ToUChar(chars.begin()), size, isSystemID, &status); - } - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - MOZ_ASSERT(size >= 0); - JSString* str = NewStringCopyN(cx, chars.begin(), size_t(size)); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - // The current default might be stale, because JS::ResetTimeZone() doesn't - // immediately update ICU's default time zone. So perform an update if - // needed. - js::ResyncICUDefaultTimeZone(); - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UErrorCode status = U_ZERO_ERROR; - int32_t size = ucal_getDefaultTimeZone(Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, - &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - MOZ_ASSERT(size >= 0); - if (!chars.resize(size_t(size))) - return false; - status = U_ZERO_ERROR; - ucal_getDefaultTimeZone(Char16ToUChar(chars.begin()), size, &status); - } - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - MOZ_ASSERT(size >= 0); - JSString* str = NewStringCopyN(cx, chars.begin(), size_t(size)); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -bool -js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 0); - - UErrorCode status = U_ZERO_ERROR; - const UChar* uTimeZone = nullptr; - int32_t uTimeZoneLength = 0; - const char* rootLocale = ""; - UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(cal); - - int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - args.rval().setInt32(offset); - return true; -} - -bool -js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 2); - MOZ_ASSERT(args[0].isString()); - MOZ_ASSERT(args[1].isString()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); - if (!skeletonFlat) - return false; - - AutoStableStringChars stableChars(cx); - if (!stableChars.initTwoByte(cx, skeletonFlat)) - return false; - - mozilla::Range skeletonChars = stableChars.twoByteRange(); - uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); - - UErrorCode status = U_ZERO_ERROR; - UDateTimePatternGenerator* gen = udatpg_open(icuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(gen); - - int32_t size = udatpg_getBestPattern(gen, Char16ToUChar(skeletonChars.begin().get()), - skeletonLen, nullptr, 0, &status); - if (U_FAILURE(status) && status != U_BUFFER_OVERFLOW_ERROR) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedJSFreePtr pattern(cx->pod_malloc(size + 1)); - if (!pattern) - return false; - pattern[size] = '\0'; - status = U_ZERO_ERROR; - udatpg_getBestPattern(gen, Char16ToUChar(skeletonChars.begin().get()), - skeletonLen, pattern, size, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - RootedString str(cx, JS_NewUCStringCopyZ(cx, reinterpret_cast(pattern.get()))); - if (!str) - return false; - args.rval().setString(str); - return true; -} - -/** - * Returns a new UDateFormat with the locale and date-time formatting options - * of the given DateTimeFormat. - */ -static UDateFormat* -NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) -{ - RootedValue value(cx); - - RootedObject internals(cx, GetInternals(cx, dateTimeFormat)); - if (!internals) - return nullptr; - - if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) - return nullptr; - JSAutoByteString locale(cx, value.toString()); - if (!locale) - return nullptr; - - // We don't need to look at calendar and numberingSystem - they can only be - // set via the Unicode locale extension and are therefore already set on - // locale. - - if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) - return nullptr; - - AutoStableStringChars timeZoneChars(cx); - Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); - if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) - return nullptr; - - const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); - uint32_t uTimeZoneLength = u_strlen(uTimeZone); - - if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) - return nullptr; - - AutoStableStringChars patternChars(cx); - Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); - if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) - return nullptr; - - const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); - uint32_t uPatternLength = u_strlen(uPattern); - - UErrorCode status = U_ZERO_ERROR; - UDateFormat* df = - udat_open(UDAT_PATTERN, UDAT_PATTERN, icuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, - uPattern, uPatternLength, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return nullptr; - } - - // ECMAScript requires the Gregorian calendar to be used from the beginning - // of ECMAScript time. - UCalendar* cal = const_cast(udat_getCalendar(df)); - ucal_setGregorianChange(cal, StartOfTime, &status); - - // An error here means the calendar is not Gregorian, so we don't care. - - return df; -} - -static bool -intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); - return false; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - UErrorCode status = U_ZERO_ERROR; - int size = udat_format(df, x, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, - nullptr, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!chars.resize(size)) - return false; - status = U_ZERO_ERROR; - udat_format(df, x, Char16ToUChar(chars.begin()), size, nullptr, &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; - - result.setString(str); - - return true; -} - -static FieldType -GetFieldTypeForFormatField(UDateFormatField fieldName) -{ - // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This - // switch is deliberately exhaustive: cases might have to be added/removed - // if this code is compiled with a different ICU with more - // UDateFormatField enum initializers. Please guard such cases with - // appropriate ICU version-testing #ifdefs, should cross-version divergence - // occur. - switch (fieldName) { - case UDAT_ERA_FIELD: - return &JSAtomState::era; - case UDAT_YEAR_FIELD: - case UDAT_YEAR_WOY_FIELD: - case UDAT_EXTENDED_YEAR_FIELD: - case UDAT_YEAR_NAME_FIELD: - return &JSAtomState::year; - - case UDAT_MONTH_FIELD: - case UDAT_STANDALONE_MONTH_FIELD: - return &JSAtomState::month; - - case UDAT_DATE_FIELD: - case UDAT_JULIAN_DAY_FIELD: - return &JSAtomState::day; - - case UDAT_HOUR_OF_DAY1_FIELD: - case UDAT_HOUR_OF_DAY0_FIELD: - case UDAT_HOUR1_FIELD: - case UDAT_HOUR0_FIELD: - return &JSAtomState::hour; - - case UDAT_MINUTE_FIELD: - return &JSAtomState::minute; - - case UDAT_SECOND_FIELD: - return &JSAtomState::second; - - case UDAT_DAY_OF_WEEK_FIELD: - case UDAT_STANDALONE_DAY_FIELD: - case UDAT_DOW_LOCAL_FIELD: - case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: - return &JSAtomState::weekday; - - case UDAT_AM_PM_FIELD: - return &JSAtomState::dayPeriod; - - case UDAT_TIMEZONE_FIELD: - return &JSAtomState::timeZoneName; - - case UDAT_FRACTIONAL_SECOND_FIELD: - case UDAT_DAY_OF_YEAR_FIELD: - case UDAT_WEEK_OF_YEAR_FIELD: - case UDAT_WEEK_OF_MONTH_FIELD: - case UDAT_MILLISECONDS_IN_DAY_FIELD: - case UDAT_TIMEZONE_RFC_FIELD: - case UDAT_TIMEZONE_GENERIC_FIELD: - case UDAT_QUARTER_FIELD: - case UDAT_STANDALONE_QUARTER_FIELD: - case UDAT_TIMEZONE_SPECIAL_FIELD: - case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: - case UDAT_TIMEZONE_ISO_FIELD: - case UDAT_TIMEZONE_ISO_LOCAL_FIELD: -#ifndef U_HIDE_INTERNAL_API - case UDAT_RELATED_YEAR_FIELD: -#endif -#ifndef U_HIDE_DRAFT_API - case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: - case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: -#endif -#ifndef U_HIDE_INTERNAL_API - case UDAT_TIME_SEPARATOR_FIELD: -#endif - // These fields are all unsupported. - return nullptr; - - case UDAT_FIELD_COUNT: - MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " - "iterator!"); - } - - MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " - "by iterator"); - return nullptr; -} - -static bool -intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) -{ - if (!IsFinite(x)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); - return false; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - UErrorCode status = U_ZERO_ERROR; - UFieldPositionIterator* fpositer = ufieldpositer_open(&status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(fpositer); - - int resultSize = - udat_formatForFields(df, x, Char16ToUChar(chars.begin()), INITIAL_CHAR_BUFFER_SIZE, - fpositer, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!chars.resize(resultSize)) - return false; - status = U_ZERO_ERROR; - udat_formatForFields(df, x, Char16ToUChar(chars.begin()), resultSize, fpositer, &status); - } - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); - if (!partsArray) - return false; - if (resultSize == 0) { - // An empty string contains no parts, so avoid extra work below. - result.setObject(*partsArray); - return true; - } - - RootedString overallResult(cx, NewStringCopyN(cx, chars.begin(), resultSize)); - if (!overallResult) - return false; - - size_t lastEndIndex = 0; - - uint32_t partIndex = 0; - RootedObject singlePart(cx); - RootedValue partType(cx); - RootedValue val(cx); - - auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { - singlePart = NewBuiltinClassInstance(cx); - if (!singlePart) - return false; - - partType = StringValue(cx->names().*type); - if (!DefineProperty(cx, singlePart, cx->names().type, partType)) - return false; - - JSLinearString* partSubstr = - NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); - if (!partSubstr) - return false; - - val = StringValue(partSubstr); - if (!DefineProperty(cx, singlePart, cx->names().value, val)) - return false; - - val = ObjectValue(*singlePart); - if (!DefineElement(cx, partsArray, partIndex, val)) - return false; - - lastEndIndex = endIndex; - partIndex++; - return true; - }; - - 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, - "field iterator returning invalid range"); - - size_t beginIndex(beginIndexInt); - size_t endIndex(endIndexInt); - - // Technically this isn't guaranteed. But it appears true in pratice, - // and http://bugs.icu-project.org/trac/ticket/12024 is expected to - // correct the documentation lapse. - MOZ_ASSERT(lastEndIndex <= beginIndex, - "field iteration didn't return fields in order start to " - "finish as expected"); - - if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { - if (lastEndIndex < beginIndex) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) - return false; - } - - if (!AppendPart(type, beginIndex, endIndex)) - return false; - } - } - - // Append any final literal. - if (lastEndIndex < overallResult->length()) { - if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) - return false; - } - - result.setObject(*partsArray); - return true; -} - -bool -js::intl_FormatDateTime(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 dateTimeFormat(cx, &args[0].toObject()); - - // Obtain a UDateFormat object, cached if possible. - bool isDateTimeFormatInstance = dateTimeFormat->getClass() == &DateTimeFormatClass; - UDateFormat* df; - if (isDateTimeFormatInstance) { - void* priv = - dateTimeFormat->as().getReservedSlot(UDATE_FORMAT_SLOT).toPrivate(); - df = static_cast(priv); - if (!df) { - df = NewUDateFormat(cx, dateTimeFormat); - if (!df) - return false; - dateTimeFormat->as().setReservedSlot(UDATE_FORMAT_SLOT, PrivateValue(df)); - } - } else { - // There's no good place to cache the ICU date-time format for an object - // that has been initialized as a DateTimeFormat but is not a - // DateTimeFormat instance. One possibility might be to add a - // DateTimeFormat instance as an internal property to each such object. - df = NewUDateFormat(cx, dateTimeFormat); - if (!df) - return false; - } - - // Use the UDateFormat to actually format the time stamp. - RootedValue result(cx); - bool success = args[2].toBoolean() - ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), &result) - : intl_FormatDateTime(cx, df, args[1].toNumber(), &result); - - if (!isDateTimeFormatInstance) - udat_close(df); - if (!success) - return false; - args.rval().set(result); - 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; -} - -/**************** RelativeTimeFormat *****************/ - -static void relativeTimeFormat_finalize(FreeOp* fop, JSObject* obj); - -static const uint32_t URELATIVE_TIME_FORMAT_SLOT = 0; -static const uint32_t RELATIVE_TIME_FORMAT_SLOTS_COUNT = 1; - -static const ClassOps RelativeTimeFormatClassOps = { - nullptr, /* addProperty */ - nullptr, /* delProperty */ - nullptr, /* getProperty */ - nullptr, /* enumerate */ - nullptr, /* newEnumerate */ - nullptr, /* resolve */ - nullptr, /* mayResolve */ - relativeTimeFormat_finalize -}; - -static const Class RelativeTimeFormatClass = { - js_Object_str, - JSCLASS_HAS_RESERVED_SLOTS(RELATIVE_TIME_FORMAT_SLOTS_COUNT) | - JSCLASS_FOREGROUND_FINALIZE, - &RelativeTimeFormatClassOps -}; - -#if JS_HAS_TOSOURCE -static bool -relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().RelativeTimeFormat); - return true; -} -#endif - -static const JSFunctionSpec relativeTimeFormat_static_methods[] = { - JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0), - JS_FS_END -}; - -static const JSFunctionSpec relativeTimeFormat_methods[] = { - JS_SELF_HOSTED_FN("resolvedOptions", "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), - JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), -#endif - JS_FS_END -}; - -static const JSPropertySpec relativeTimeFormat_properties[] = { - JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), - JS_PS_END}; - -/** - * RelativeTimeFormat constructor. - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 - */ -static bool -RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - - // Step 1. - if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) - return false; - - // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). - RootedObject proto(cx); - if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) - return false; - - if (!proto) { - proto = GlobalObject::getOrCreateRelativeTimeFormatPrototype(cx, cx->global()); - if (!proto) - return false; - } - - RootedObject relativeTimeFormat(cx); - relativeTimeFormat = NewObjectWithGivenProto(cx, &RelativeTimeFormatClass, proto); - if (!relativeTimeFormat) - return false; - - relativeTimeFormat->as().setReservedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); - - RootedValue locales(cx, args.get(0)); - RootedValue options(cx, args.get(1)); - - // Step 3. - if (!IntlInitialize(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) - return false; - - args.rval().setObject(*relativeTimeFormat); - return true; -} - -static void -relativeTimeFormat_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(URELATIVE_TIME_FORMAT_SLOT); - if (!slot.isUndefined()) { - if (URelativeDateTimeFormatter* rtf = static_cast(slot.toPrivate())) - ureldatefmt_close(rtf); - } -} - -static JSObject* -CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) -{ - RootedFunction ctor(cx); - ctor = global->createConstructor(cx, &RelativeTimeFormat, cx->names().RelativeTimeFormat, 0); - if (!ctor) - return nullptr; - - RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &RelativeTimeFormatClass)); - if (!proto) - return nullptr; - proto->setReservedSlot(URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); - - if (!LinkConstructorAndPrototype(cx, ctor, proto)) - return nullptr; - - if (!JS_DefineFunctions(cx, ctor, relativeTimeFormat_static_methods)) - return nullptr; - - if (!JS_DefineFunctions(cx, proto, relativeTimeFormat_methods)) - return nullptr; - - if (!JS_DefineProperties(cx, proto, relativeTimeFormat_properties)) - return nullptr; - - RootedValue options(cx); - if (!CreateDefaultOptions(cx, &options)) - return nullptr; - - if (!IntlInitialize(cx, proto, cx->names().InitializeRelativeTimeFormat, UndefinedHandleValue, - options)) - { - return nullptr; - } - - RootedValue ctorValue(cx, ObjectValue(*ctor)); - if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0)) { - return nullptr; - } - - return proto; -} - -bool -js::intl_RelativeTimeFormat_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; -} - - -enum class RelativeTimeNumeric -{ - /** - * Only strings with numeric components like `1 day ago`. - */ - Always, - /** - * Natural-language strings like `yesterday` when possible, - * otherwise strings with numeric components as in `7 months ago`. - */ - Auto, -}; - -bool -js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - - RootedObject relativeTimeFormat(cx, &args[0].toObject()); - - RootedObject internals(cx, GetInternals(cx, relativeTimeFormat)); - 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().style, &value)) - return false; - RootedLinearString style(cx, value.toString()->ensureLinear(cx)); - if (!style) - return false; - - double t = args[1].toNumber(); - - UDateRelativeDateTimeFormatterStyle relDateTimeStyle; - - if (StringEqualsAscii(style, "short")) { - relDateTimeStyle = UDAT_STYLE_SHORT; - } else if (StringEqualsAscii(style, "narrow")) { - relDateTimeStyle = UDAT_STYLE_NARROW; - } else { - MOZ_ASSERT(StringEqualsAscii(style, "long")); - relDateTimeStyle = UDAT_STYLE_LONG; - } - - URelativeDateTimeUnit relDateTimeUnit; - { - JSLinearString* unit = args[2].toString()->ensureLinear(cx); - if (!unit) { - return false; - } - - if (StringEqualsAscii(unit, "second") || StringEqualsAscii(unit, "seconds")) { - relDateTimeUnit = UDAT_REL_UNIT_SECOND; - } else if (StringEqualsAscii(unit, "minute") || StringEqualsAscii(unit, "minutes")) { - relDateTimeUnit = UDAT_REL_UNIT_MINUTE; - } else if (StringEqualsAscii(unit, "hour") || StringEqualsAscii(unit, "hours")) { - relDateTimeUnit = UDAT_REL_UNIT_HOUR; - } else if (StringEqualsAscii(unit, "day") || StringEqualsAscii(unit, "days")) { - relDateTimeUnit = UDAT_REL_UNIT_DAY; - } else if (StringEqualsAscii(unit, "week") || StringEqualsAscii(unit, "weeks")) { - relDateTimeUnit = UDAT_REL_UNIT_WEEK; - } else if (StringEqualsAscii(unit, "month") || StringEqualsAscii(unit, "months")) { - relDateTimeUnit = UDAT_REL_UNIT_MONTH; - } else if (StringEqualsAscii(unit, "quarter") || StringEqualsAscii(unit, "quarters")) { - relDateTimeUnit = UDAT_REL_UNIT_QUARTER; - } else { - MOZ_ASSERT(StringEqualsAscii(unit, "year") || StringEqualsAscii(unit, "years")); - relDateTimeUnit = UDAT_REL_UNIT_YEAR; - } - } - - if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) - return false; - RootedLinearString numeric(cx, value.toString()->ensureLinear(cx)); - if (!numeric) - return false; - - RelativeTimeNumeric relDateTimeNumeric; - - if (StringEqualsAscii(numeric, "auto")) { - relDateTimeNumeric = RelativeTimeNumeric::Auto; - } else { - MOZ_ASSERT(StringEqualsAscii(numeric, "always")); - relDateTimeNumeric = RelativeTimeNumeric::Always; - } - - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - UErrorCode status = U_ZERO_ERROR; - URelativeDateTimeFormatter* rtf = - ureldatefmt_open(icuLocale(locale.ptr()), nullptr, relDateTimeStyle, - UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - int32_t size; - - if (relDateTimeNumeric == RelativeTimeNumeric::Auto) { - size = ureldatefmt_format(rtf, t, relDateTimeUnit, Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, &status); - } else { - MOZ_ASSERT(relDateTimeNumeric == RelativeTimeNumeric::Always); - size = ureldatefmt_formatNumeric(rtf, t, relDateTimeUnit, Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, &status); - } - - ScopedICUObject closeRelativeTimeFormat(rtf); - - JSString* str = NewStringCopyN(cx, chars.begin(), size); - if (!str) - return false; - - args.rval().setString(str); - return true; -} - -bool -js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 1); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - UErrorCode status = U_ZERO_ERROR; - const UChar* uTimeZone = nullptr; - int32_t uTimeZoneLength = 0; - UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, locale.ptr(), UCAL_DEFAULT, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject toClose(cal); - - RootedObject info(cx, NewBuiltinClassInstance(cx)); - if (!info) - return false; - - RootedValue v(cx); - int32_t firstDayOfWeek = ucal_getAttribute(cal, UCAL_FIRST_DAY_OF_WEEK); - v.setInt32(firstDayOfWeek); - - if (!DefineProperty(cx, info, cx->names().firstDayOfWeek, v)) - return false; - - int32_t minDays = ucal_getAttribute(cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK); - v.setInt32(minDays); - if (!DefineProperty(cx, info, cx->names().minDays, v)) - return false; - - UCalendarWeekdayType prevDayType = ucal_getDayOfWeekType(cal, UCAL_SATURDAY, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - RootedValue weekendStart(cx), weekendEnd(cx); - - for (int i = UCAL_SUNDAY; i <= UCAL_SATURDAY; i++) { - UCalendarDaysOfWeek dayOfWeek = static_cast(i); - UCalendarWeekdayType type = ucal_getDayOfWeekType(cal, dayOfWeek, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - if (prevDayType != type) { - switch (type) { - case UCAL_WEEKDAY: - // If the first Weekday after Weekend is Sunday (1), - // then the last Weekend day is Saturday (7). - // Otherwise we'll just take the previous days number. - weekendEnd.setInt32(i == 1 ? 7 : i - 1); - break; - case UCAL_WEEKEND: - weekendStart.setInt32(i); - break; - case UCAL_WEEKEND_ONSET: - case UCAL_WEEKEND_CEASE: - // At the time this code was added, ICU apparently never behaves this way, - // so just throw, so that users will report a bug and we can decide what to - // do. - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - default: - break; - } - } - - prevDayType = type; - } - - MOZ_ASSERT(weekendStart.isInt32()); - MOZ_ASSERT(weekendEnd.isInt32()); - - if (!DefineProperty(cx, info, cx->names().weekendStart, weekendStart)) - return false; - - if (!DefineProperty(cx, info, cx->names().weekendEnd, weekendEnd)) - return false; - - args.rval().setObject(*info); - return true; -} - -template -inline bool -MatchPart(const char** pattern, const char (&part)[N]) -{ - if (strncmp(*pattern, part, N - 1)) - return false; - - *pattern += N - 1; - return true; -} - -bool -js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - MOZ_ASSERT(args.length() == 3); - // 1. Assert: locale is a string. - MOZ_ASSERT(args[0].isString()); - // 2. Assert: style is a string. - MOZ_ASSERT(args[1].isString()); - // 3. Assert: keys is an Array. - MOZ_ASSERT(args[2].isObject()); - - JSAutoByteString locale(cx, args[0].toString()); - if (!locale) - return false; - - JSAutoByteString style(cx, args[1].toString()); - if (!style) - return false; - - RootedArrayObject keys(cx, &args[2].toObject().as()); - if (!keys) - return false; - - // 4. Let result be ArrayCreate(0). - RootedArrayObject result(cx, NewDenseUnallocatedArray(cx, keys->length())); - if (!result) - return false; - - UErrorCode status = U_ZERO_ERROR; - - UDateFormat* fmt = - udat_open(UDAT_DEFAULT, UDAT_DEFAULT, icuLocale(locale.ptr()), - nullptr, 0, nullptr, 0, &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject datToClose(fmt); - - // UDateTimePatternGenerator will be needed for translations of date and - // time fields like "month", "week", "day" etc. - UDateTimePatternGenerator* dtpg = udatpg_open(icuLocale(locale.ptr()), &status); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - ScopedICUObject datPgToClose(dtpg); - - RootedValue keyValue(cx); - RootedString keyValStr(cx); - RootedValue wordVal(cx); - Vector chars(cx); - if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) - return false; - - // 5. For each element of keys, - for (uint32_t i = 0; i < keys->length(); i++) { - /** - * We iterate over keys array looking for paths that we have code - * branches for. - * - * For any unknown path branch, the wordVal will keep NullValue and - * we'll throw at the end. - */ - - if (!GetElement(cx, keys, keys, i, &keyValue)) - return false; - - JSAutoByteString pattern; - keyValStr = keyValue.toString(); - if (!pattern.encodeUtf8(cx, keyValStr)) - return false; - - wordVal.setNull(); - - // 5.a. Perform an implementation dependent algorithm to map a key to a - // corresponding display name. - const char* pat = pattern.ptr(); - - if (!MatchPart(&pat, "dates")) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - if (!MatchPart(&pat, "/")) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - if (MatchPart(&pat, "fields")) { - if (!MatchPart(&pat, "/")) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - UDateTimePatternField fieldType; - - if (MatchPart(&pat, "year")) { - fieldType = UDATPG_YEAR_FIELD; - } else if (MatchPart(&pat, "month")) { - fieldType = UDATPG_MONTH_FIELD; - } else if (MatchPart(&pat, "week")) { - fieldType = UDATPG_WEEK_OF_YEAR_FIELD; - } else if (MatchPart(&pat, "day")) { - fieldType = UDATPG_DAY_FIELD; - } else { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - // This part must be the final part with no trailing data. - if (*pat != '\0') { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - int32_t resultSize; - - const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize); - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - JSString* word = NewStringCopyN(cx, UCharToChar16(value), resultSize); - if (!word) - return false; - - wordVal.setString(word); - } else if (MatchPart(&pat, "gregorian")) { - if (!MatchPart(&pat, "/")) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - UDateFormatSymbolType symbolType; - int32_t index; - - if (MatchPart(&pat, "months")) { - if (!MatchPart(&pat, "/")) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - if (equal(style, "narrow")) { - symbolType = UDAT_STANDALONE_NARROW_MONTHS; - } else if (equal(style, "short")) { - symbolType = UDAT_STANDALONE_SHORT_MONTHS; - } else { - MOZ_ASSERT(equal(style, "long")); - symbolType = UDAT_STANDALONE_MONTHS; - } - - if (MatchPart(&pat, "january")) { - index = UCAL_JANUARY; - } else if (MatchPart(&pat, "february")) { - index = UCAL_FEBRUARY; - } else if (MatchPart(&pat, "march")) { - index = UCAL_MARCH; - } else if (MatchPart(&pat, "april")) { - index = UCAL_APRIL; - } else if (MatchPart(&pat, "may")) { - index = UCAL_MAY; - } else if (MatchPart(&pat, "june")) { - index = UCAL_JUNE; - } else if (MatchPart(&pat, "july")) { - index = UCAL_JULY; - } else if (MatchPart(&pat, "august")) { - index = UCAL_AUGUST; - } else if (MatchPart(&pat, "september")) { - index = UCAL_SEPTEMBER; - } else if (MatchPart(&pat, "october")) { - index = UCAL_OCTOBER; - } else if (MatchPart(&pat, "november")) { - index = UCAL_NOVEMBER; - } else if (MatchPart(&pat, "december")) { - index = UCAL_DECEMBER; - } else { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - } else if (MatchPart(&pat, "weekdays")) { - if (!MatchPart(&pat, "/")) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - if (equal(style, "narrow")) { - symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS; - } else if (equal(style, "short")) { - symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS; - } else { - MOZ_ASSERT(equal(style, "long")); - symbolType = UDAT_STANDALONE_WEEKDAYS; - } - - if (MatchPart(&pat, "monday")) { - index = UCAL_MONDAY; - } else if (MatchPart(&pat, "tuesday")) { - index = UCAL_TUESDAY; - } else if (MatchPart(&pat, "wednesday")) { - index = UCAL_WEDNESDAY; - } else if (MatchPart(&pat, "thursday")) { - index = UCAL_THURSDAY; - } else if (MatchPart(&pat, "friday")) { - index = UCAL_FRIDAY; - } else if (MatchPart(&pat, "saturday")) { - index = UCAL_SATURDAY; - } else if (MatchPart(&pat, "sunday")) { - index = UCAL_SUNDAY; - } else { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - } else if (MatchPart(&pat, "dayperiods")) { - if (!MatchPart(&pat, "/")) { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - symbolType = UDAT_AM_PMS; - - if (MatchPart(&pat, "am")) { - index = UCAL_AM; - } else if (MatchPart(&pat, "pm")) { - index = UCAL_PM; - } else { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - } else { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - // This part must be the final part with no trailing data. - if (*pat != '\0') { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - int32_t resultSize = - udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), - INITIAL_CHAR_BUFFER_SIZE, &status); - if (status == U_BUFFER_OVERFLOW_ERROR) { - if (!chars.resize(resultSize)) - return false; - status = U_ZERO_ERROR; - udat_getSymbols(fmt, symbolType, index, Char16ToUChar(chars.begin()), - resultSize, &status); - } - if (U_FAILURE(status)) { - JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); - return false; - } - - JSString* word = NewStringCopyN(cx, chars.begin(), resultSize); - if (!word) - return false; - - wordVal.setString(word); - } else { - JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); - return false; - } - - MOZ_ASSERT(wordVal.isString()); - - // 5.b. Append the result string to result. - if (!DefineElement(cx, result, i, wordVal)) - return false; - } - - // 6. Return result. - args.rval().setObject(*result); - return true; -} - -/******************** Intl ********************/ - -const Class js::IntlClass = { - js_Object_str, - JSCLASS_HAS_CACHED_PROTO(JSProto_Intl) -}; - -#if JS_HAS_TOSOURCE -static bool -intl_toSource(JSContext* cx, unsigned argc, Value* vp) -{ - CallArgs args = CallArgsFromVp(argc, vp); - args.rval().setString(cx->names().Intl); - return true; -} -#endif - -static const JSFunctionSpec intl_static_methods[] = { -#if JS_HAS_TOSOURCE - JS_FN(js_toSource_str, intl_toSource, 0, 0), -#endif - JS_SELF_HOSTED_FN("getCanonicalLocales", "Intl_getCanonicalLocales", 1, 0), - JS_FS_END -}; - -/** - * Initializes the Intl Object and its standard built-in properties. - * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 - */ -/* static */ bool -GlobalObject::initIntlObject(JSContext* cx, Handle global) -{ - RootedObject proto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global)); - if (!proto) - return false; - - // The |Intl| object is just a plain object with some "static" function - // properties and some constructor properties. - RootedObject intl(cx, NewObjectWithGivenProto(cx, &IntlClass, proto, SingletonObject)); - if (!intl) - return false; - - // Add the static functions. - if (!JS_DefineFunctions(cx, intl, intl_static_methods)) - return false; - - // Add the constructor properties, computing and returning the relevant - // prototype objects needed below. - RootedObject collatorProto(cx, CreateCollatorPrototype(cx, intl, global)); - if (!collatorProto) - return false; - RootedObject dateTimeFormatProto(cx, CreateDateTimeFormatPrototype(cx, intl, global)); - if (!dateTimeFormatProto) - return false; - RootedObject numberFormatProto(cx, CreateNumberFormatPrototype(cx, intl, global)); - if (!numberFormatProto) - return false; - RootedObject pluralRulesProto(cx, CreatePluralRulesPrototype(cx, intl, global)); - if (!pluralRulesProto) - return false; - - RootedObject relativeTimeFmtProto(cx, CreateRelativeTimeFormatPrototype(cx, intl, global)); - if (!relativeTimeFmtProto) { - return false; - } - - // The |Intl| object is fully set up now, so define the global property. - RootedValue intlValue(cx, ObjectValue(*intl)); - if (!DefineProperty(cx, global, cx->names().Intl, intlValue, nullptr, nullptr, - JSPROP_RESOLVING)) - { - return false; - } - - // Now that the |Intl| object is successfully added, we can OOM-safely fill - // in all relevant reserved global slots. - - // Cache the various prototypes, for use in creating instances of these - // objects with the proper [[Prototype]] as "the original value of - // |Intl.Collator.prototype|" and similar. For builtin classes like - // |String.prototype| we have |JSProto_*| that enables - // |getPrototype(JSProto_*)|, but that has global-object-property-related - // baggage we don't need or want, so we use one-off reserved slots. - 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)); - global->setReservedSlot(RELATIVE_TIME_FORMAT_PROTO, ObjectValue(*relativeTimeFmtProto)); - - // Also cache |Intl| to implement spec language that conditions behavior - // based on values being equal to "the standard built-in |Intl| object". - // Use |setConstructor| to correspond with |JSProto_Intl|. - // - // XXX We should possibly do a one-off reserved slot like above. - global->setConstructor(JSProto_Intl, ObjectValue(*intl)); - return true; -} - -JSObject* -js::InitIntlClass(JSContext* cx, HandleObject obj) -{ - Handle global = obj.as(); - if (!GlobalObject::initIntlObject(cx, global)) - return nullptr; - - return &global->getConstructor(JSProto_Intl).toObject(); -} diff --git a/js/src/builtin/Intl.h b/js/src/builtin/Intl.h deleted file mode 100644 index 330b8a4963..0000000000 --- a/js/src/builtin/Intl.h +++ /dev/null @@ -1,531 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- - * 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_h -#define builtin_Intl_h - -#include "mozilla/HashFunctions.h" -#include "mozilla/MemoryReporting.h" - -#include "jsalloc.h" -#include "NamespaceImports.h" - -#include "js/GCAPI.h" -#include "js/GCHashTable.h" - -#include "unicode/utypes.h" - -/* - * The Intl module specified by standard ECMA-402, - * ECMAScript Internationalization API Specification. - */ - -namespace js { - -/** - * Initializes the Intl Object and its standard built-in properties. - * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 - */ -extern JSObject* -InitIntlClass(JSContext* cx, HandleObject obj); - -/** - * Stores Intl data which can be shared across compartments (but not contexts). - * - * Used for data which is expensive when computed repeatedly or is not - * available through ICU. - */ -class SharedIntlData -{ - /** - * Information tracking the set of the supported time zone names, derived - * from the IANA time zone database . - * - * There are two kinds of IANA time zone names: Zone and Link (denoted as - * such in database source files). Zone names are the canonical, preferred - * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to - * target Zone names for their meaning, e.g. Asia/Calcutta targets - * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a - * sense of deprecation: some Link names also exist partly for convenience, - * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC. - * - * Two data sources determine the time zone names we support: those ICU - * supports and IANA's zone information. - * - * Unfortunately the names ICU and IANA support, and their Link - * relationships from name to target, aren't identical, so we can't simply - * implicitly trust ICU's name handling. We must perform various - * preprocessing of user-provided zone names and post-processing of - * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior. - * - * Also see and - * . - */ - - using TimeZoneName = JSAtom*; - - struct TimeZoneHasher - { - struct Lookup - { - union { - const JS::Latin1Char* latin1Chars; - const char16_t* twoByteChars; - }; - bool isLatin1; - size_t length; - JS::AutoCheckCannotGC nogc; - HashNumber hash; - - explicit Lookup(JSFlatString* timeZone); - }; - - static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } - static bool match(TimeZoneName key, const Lookup& lookup); - }; - - using TimeZoneSet = js::GCHashSet; - - using TimeZoneMap = js::GCHashMap; - - /** - * As a threshold matter, available time zones are those time zones ICU - * supports, via ucal_openTimeZones. But ICU supports additional non-IANA - * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in - * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards - * compatibility purposes. This set consists of ICU's supported time zones, - * minus all backwards-compatibility time zones. - */ - TimeZoneSet availableTimeZones; - - /** - * IANA treats some time zone names as Zones, that ICU instead treats as - * Links. For example, IANA considers "America/Indiana/Indianapolis" to be - * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU - * considers the former a Link that targets "America/Indianapolis" (which - * IANA treats as a Link). - * - * ECMA-402 requires that we respect IANA data, so if we're asked to - * canonicalize a time zone name in this set, we must *not* return ICU's - * canonicalization. - */ - TimeZoneSet ianaZonesTreatedAsLinksByICU; - - /** - * IANA treats some time zone names as Links to one target, that ICU - * instead treats as either Zones, or Links to different targets. An - * example of the former is "Asia/Calcutta, which IANA assigns the target - * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter - * is "America/Virgin", which IANA assigns the target - * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas". - * - * ECMA-402 requires that we respect IANA data, so if we're asked to - * canonicalize a time zone name that's a key in this map, we *must* return - * the corresponding value and *must not* return ICU's canonicalization. - */ - TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU; - - bool timeZoneDataInitialized = false; - - /** - * Precomputes the available time zone names, because it's too expensive to - * call ucal_openTimeZones() repeatedly. - */ - bool ensureTimeZones(JSContext* cx); - - public: - /** - * Returns the validated time zone name in |result|. If the input time zone - * isn't a valid IANA time zone name, |result| remains unchanged. - */ - bool validateTimeZoneName(JSContext* cx, JS::HandleString timeZone, - JS::MutableHandleString result); - - /** - * Returns the canonical time zone name in |result|. If no canonical name - * was found, |result| remains unchanged. - * - * This method only handles time zones which are canonicalized differently - * by ICU when compared to IANA. - */ - bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, - JS::MutableHandleString result); - - void destroyInstance(); - - void trace(JSTracer* trc); - - size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; -}; - -/* - * The following functions are for use by self-hosted code. - */ - - -/******************** Collator ********************/ - -/** - * Returns a new instance of the standard built-in Collator 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: collator = intl_Collator(locales, options) - */ -extern MOZ_MUST_USE bool -intl_Collator(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for collation - * 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_Collator_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an array with the collation type identifiers per Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * collations supported for the given locale. "standard" and "search" are - * excluded. - * - * Usage: collations = intl_availableCollations(locale) - */ -extern MOZ_MUST_USE bool -intl_availableCollations(JSContext* cx, unsigned argc, Value* vp); - -/** - * Compares x and y (which must be String values), and returns a number less - * than 0 if x < y, 0 if x = y, or a number greater than 0 if x > y according - * to the sort order for the locale and collation options of the given - * Collator. - * - * Spec: ECMAScript Internationalization API Specification, 10.3.2. - * - * Usage: result = intl_CompareStrings(collator, x, y) - */ -extern MOZ_MUST_USE bool -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 ********************/ - -/** - * Returns a new instance of the standard built-in DateTimeFormat 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: dateTimeFormat = intl_DateTimeFormat(locales, options) - */ -extern MOZ_MUST_USE bool -intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an object indicating the supported locales for date and time - * 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_DateTimeFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an array with the calendar type identifiers per Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * supported calendars for the given locale. The default calendar is - * element 0. - * - * Usage: calendars = intl_availableCalendars(locale) - */ -extern MOZ_MUST_USE bool -intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp); - -/** - * 6.4.1 IsValidTimeZoneName ( timeZone ) - * - * Verifies that the given string is a valid time zone name. If it is a valid - * time zone name, its IANA time zone name is returned. Otherwise returns null. - * - * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 - * - * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone) - */ -extern MOZ_MUST_USE bool -intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the canonicalized time zone name. Canonicalization resolves link - * names to their target time zones. - * - * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone) - */ -extern MOZ_MUST_USE bool -intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the default time zone name. The time zone name is not canonicalized. - * - * Usage: icuDefaultTimeZone = intl_defaultTimeZone() - */ -extern MOZ_MUST_USE bool -intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return the raw offset from GMT in milliseconds for the default time zone. - * - * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset() - */ -extern MOZ_MUST_USE bool -intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); - -/** - * Return a pattern in the date-time format pattern language of Unicode - * Technical Standard 35, Unicode Locale Data Markup Language, for the - * best-fit date-time format pattern corresponding to skeleton for the - * given locale. - * - * Usage: pattern = intl_patternForSkeleton(locale, skeleton) - */ -extern MOZ_MUST_USE bool -intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a String value representing x (which must be a Number value) - * according to the effective locale and the formatting options of the - * given DateTimeFormat. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - * - * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x) - */ -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); - -/******************** RelativeTimeFormat ********************/ - -/** - * Returns an object indicating the supported locales for relative time format - * 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_RelativeTimeFormat_availableLocales() - */ -extern MOZ_MUST_USE bool -intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns a relative time as a string formatted according to the effective - * locale and the formatting options of the given RelativeTimeFormat. - * - * t should be a number representing a number to be formatted. - * unit should be "second", "minute", "hour", "day", "week", "month", "quarter", or "year". - * - * Usage: formatted = intl_FormatRelativeTime(relativeTimeFormat, t, unit) - */ -extern MOZ_MUST_USE bool -intl_FormatRelativeTime(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 - * properties: - * - * firstDayOfWeek - * an integer in the range 1=Sunday to 7=Saturday indicating the day - * considered the first day of the week in calendars, e.g. 1 for en-US, - * 2 for en-GB, 1 for bn-IN - * minDays - * an integer in the range of 1 to 7 indicating the minimum number - * of days required in the first week of the year, e.g. 1 for en-US, 4 for de - * weekendStart - * an integer in the range 1=Sunday to 7=Saturday indicating the day - * considered the beginning of a weekend, e.g. 7 for en-US, 7 for en-GB, - * 1 for bn-IN - * weekendEnd - * an integer in the range 1=Sunday to 7=Saturday indicating the day - * considered the end of a weekend, e.g. 1 for en-US, 1 for en-GB, - * 1 for bn-IN (note that "weekend" is *not* necessarily two days) - * - * NOTE: "calendar" and "locale" properties are *not* added to the object. - */ -extern MOZ_MUST_USE bool -intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp); - -/** - * Returns an Array with CLDR-based fields display names. - * The function takes three arguments: - * - * locale - * BCP47 compliant locale string - * style - * A string with values: long or short or narrow - * keys - * An array or path-like strings that identify keys to be returned - * At the moment the following types of keys are supported: - * - * 'dates/fields/{year|month|week|day}' - * 'dates/gregorian/months/{january|...|december}' - * 'dates/gregorian/weekdays/{sunday|...|saturday}' - * 'dates/gregorian/dayperiods/{am|pm}' - * - * Example: - * - * let info = intl_ComputeDisplayNames( - * 'en-US', - * 'long', - * [ - * 'dates/fields/year', - * 'dates/gregorian/months/january', - * 'dates/gregorian/weekdays/monday', - * 'dates/gregorian/dayperiods/am', - * ] - * ); - * - * Returned value: - * - * [ - * 'year', - * 'January', - * 'Monday', - * 'AM' - * ] - */ -extern MOZ_MUST_USE bool -intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp); - -/** - * Cast char16_t* strings to UChar* strings used by ICU. - */ -inline const UChar* -Char16ToUChar(const char16_t* chars) -{ - return reinterpret_cast(chars); -} - -inline UChar* -Char16ToUChar(char16_t* chars) -{ - return reinterpret_cast(chars); -} - -inline char16_t* -UCharToChar16(UChar* chars) -{ - return reinterpret_cast(chars); -} - -inline const char16_t* -UCharToChar16(const UChar* chars) -{ - return reinterpret_cast(chars); -} - -} // namespace js - -#endif /* builtin_Intl_h */ diff --git a/js/src/builtin/Intl.js b/js/src/builtin/Intl.js deleted file mode 100644 index e7d1b58ed6..0000000000 --- a/js/src/builtin/Intl.js +++ /dev/null @@ -1,3663 +0,0 @@ -/* 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/. */ - -/* Portions Copyright Norbert Lindenberg 2011-2012. */ - -/*global JSMSG_INTL_OBJECT_NOT_INITED: false, JSMSG_INVALID_LOCALES_ELEMENT: false, - JSMSG_INVALID_LANGUAGE_TAG: false, JSMSG_INVALID_LOCALE_MATCHER: false, - JSMSG_INVALID_OPTION_VALUE: false, JSMSG_INVALID_DIGITS_VALUE: false, - JSMSG_INTL_OBJECT_REINITED: false, JSMSG_INVALID_CURRENCY_CODE: false, - JSMSG_UNDEFINED_CURRENCY: false, JSMSG_INVALID_TIME_ZONE: false, - JSMSG_DATE_NOT_FINITE: false, JSMSG_INVALID_KEYS_TYPE: false, - JSMSG_INVALID_KEY: false, - intl_Collator_availableLocales: false, - intl_availableCollations: false, - intl_CompareStrings: false, - intl_NumberFormat_availableLocales: false, - intl_numberingSystem: false, - intl_FormatNumber: false, - intl_DateTimeFormat_availableLocales: false, - intl_availableCalendars: false, - intl_patternForSkeleton: false, - intl_FormatDateTime: false, - intl_SelectPluralRule: false, - intl_GetPluralCategories: false, - intl_FormatRelativeTime: false, - intl_GetCalendarInfo: false, -*/ - -/* - * The Intl module specified by standard ECMA-402, - * ECMAScript Internationalization API Specification. - */ - - -/********** Locales, Time Zones, and Currencies **********/ - - -/** - * Convert s to upper case, but limited to characters a-z. - * - * Spec: ECMAScript Internationalization API Specification, 6.1. - */ -function toASCIIUpperCase(s) { - assert(typeof s === "string", "toASCIIUpperCase"); - - // String.prototype.toUpperCase may map non-ASCII characters into ASCII, - // so go character by character (actually code unit by code unit, but - // since we only care about ASCII characters here, that's OK). - var result = ""; - for (var i = 0; i < s.length; i++) { - var c = callFunction(std_String_charCodeAt, s, i); - result += (0x61 <= c && c <= 0x7A) - ? callFunction(std_String_fromCharCode, null, c & ~0x20) - : s[i]; - } - return result; -} - -/** - * Holder object for encapsulating regexp instances. - * - * Regular expression instances should be created after the initialization of - * self-hosted global. - */ -var internalIntlRegExps = std_Object_create(null); -internalIntlRegExps.unicodeLocaleExtensionSequenceRE = null; -internalIntlRegExps.languageTagRE = null; -internalIntlRegExps.duplicateVariantRE = null; -internalIntlRegExps.duplicateSingletonRE = null; -internalIntlRegExps.isWellFormedCurrencyCodeRE = null; -internalIntlRegExps.currencyDigitsRE = null; - -/** - * Regular expression matching a "Unicode locale extension sequence", which the - * specification defines as: "any substring of a language tag that starts with - * a separator '-' and the singleton 'u' and includes the maximum sequence of - * following non-singleton subtags and their preceding '-' separators." - * - * Alternatively, this may be defined as: the components of a language tag that - * match the extension production in RFC 5646, where the singleton component is - * "u". - * - * Spec: ECMAScript Internationalization API Specification, 6.2.1. - */ -function getUnicodeLocaleExtensionSequenceRE() { - return internalIntlRegExps.unicodeLocaleExtensionSequenceRE || - (internalIntlRegExps.unicodeLocaleExtensionSequenceRE = - RegExpCreate("-u(?:-[a-z0-9]{2,8})+")); -} - - -/** - * Removes Unicode locale extension sequences from the given language tag. - */ -function removeUnicodeExtensions(locale) { - // A wholly-privateuse locale has no extension sequences. - if (callFunction(std_String_startsWith, locale, "x-")) - return locale; - - // Otherwise, split on "-x-" marking the start of any privateuse component. - // Replace Unicode locale extension sequences in the left half, and return - // the concatenation. - var pos = callFunction(std_String_indexOf, locale, "-x-"); - if (pos < 0) - pos = locale.length; - - var left = callFunction(String_substring, locale, 0, pos); - var right = callFunction(String_substring, locale, pos); - - var extensions; - var unicodeLocaleExtensionSequenceRE = getUnicodeLocaleExtensionSequenceRE(); - while ((extensions = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, left)) !== null) { - left = StringReplaceString(left, extensions[0], ""); - unicodeLocaleExtensionSequenceRE.lastIndex = 0; - } - - var combined = left + right; - assert(IsStructurallyValidLanguageTag(combined), "recombination produced an invalid language tag"); - assert(function() { - var uindex = callFunction(std_String_indexOf, combined, "-u-"); - if (uindex < 0) - return true; - var xindex = callFunction(std_String_indexOf, combined, "-x-"); - return xindex > 0 && xindex < uindex; - }(), "recombination failed to remove all Unicode locale extension sequences"); - - return combined; -} - - -/** - * Regular expression defining BCP 47 language tags. - * - * Spec: RFC 5646 section 2.1. - */ -function getLanguageTagRE() { - if (internalIntlRegExps.languageTagRE) - return internalIntlRegExps.languageTagRE; - - // RFC 5234 section B.1 - // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - var ALPHA = "[a-zA-Z]"; - // DIGIT = %x30-39 - // ; 0-9 - var DIGIT = "[0-9]"; - - // RFC 5646 section 2.1 - // alphanum = (ALPHA / DIGIT) ; letters and numbers - var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; - // regular = "art-lojban" ; these tags match the 'langtag' - // / "cel-gaulish" ; production, but their subtags - // / "no-bok" ; are not extended language - // / "no-nyn" ; or variant subtags: their meaning - // / "zh-guoyu" ; is defined by their registration - // / "zh-hakka" ; and all of these are deprecated - // / "zh-min" ; in favor of a more modern - // / "zh-min-nan" ; subtag or sequence of subtags - // / "zh-xiang" - var regular = "(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)"; - // irregular = "en-GB-oed" ; irregular tags do not match - // / "i-ami" ; the 'langtag' production and - // / "i-bnn" ; would not otherwise be - // / "i-default" ; considered 'well-formed' - // / "i-enochian" ; These tags are all valid, - // / "i-hak" ; but most are deprecated - // / "i-klingon" ; in favor of more modern - // / "i-lux" ; subtags or subtag - // / "i-mingo" ; combination - // / "i-navajo" - // / "i-pwn" - // / "i-tao" - // / "i-tay" - // / "i-tsu" - // / "sgn-BE-FR" - // / "sgn-BE-NL" - // / "sgn-CH-DE" - var irregular = "(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)"; - // grandfathered = irregular ; non-redundant tags registered - // / regular ; during the RFC 3066 era - var grandfathered = "(?:" + irregular + "|" + regular + ")"; - // privateuse = "x" 1*("-" (1*8alphanum)) - var privateuse = "(?:x(?:-[a-z0-9]{1,8})+)"; - // singleton = DIGIT ; 0 - 9 - // / %x41-57 ; A - W - // / %x59-5A ; Y - Z - // / %x61-77 ; a - w - // / %x79-7A ; y - z - var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])"; - // extension = singleton 1*("-" (2*8alphanum)) - var extension = "(?:" + singleton + "(?:-" + alphanum + "{2,8})+)"; - // variant = 5*8alphanum ; registered variants - // / (DIGIT 3alphanum) - var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))"; - // region = 2ALPHA ; ISO 3166-1 code - // / 3DIGIT ; UN M.49 code - var region = "(?:" + ALPHA + "{2}|" + DIGIT + "{3})"; - // script = 4ALPHA ; ISO 15924 code - var script = "(?:" + ALPHA + "{4})"; - // extlang = 3ALPHA ; selected ISO 639 codes - // *2("-" 3ALPHA) ; permanently reserved - var extlang = "(?:" + ALPHA + "{3}(?:-" + ALPHA + "{3}){0,2})"; - // language = 2*3ALPHA ; shortest ISO 639 code - // ["-" extlang] ; sometimes followed by - // ; extended language subtags - // / 4ALPHA ; or reserved for future use - // / 5*8ALPHA ; or registered language subtag - var language = "(?:" + ALPHA + "{2,3}(?:-" + extlang + ")?|" + ALPHA + "{4}|" + ALPHA + "{5,8})"; - // langtag = language - // ["-" script] - // ["-" region] - // *("-" variant) - // *("-" extension) - // ["-" privateuse] - var langtag = language + "(?:-" + script + ")?(?:-" + region + ")?(?:-" + - variant + ")*(?:-" + extension + ")*(?:-" + privateuse + ")?"; - // Language-Tag = langtag ; normal language tags - // / privateuse ; private use tag - // / grandfathered ; grandfathered tags - var languageTag = "^(?:" + langtag + "|" + privateuse + "|" + grandfathered + ")$"; - - // Language tags are case insensitive (RFC 5646 section 2.1.1). - return (internalIntlRegExps.languageTagRE = RegExpCreate(languageTag, "i")); -} - - -function getDuplicateVariantRE() { - if (internalIntlRegExps.duplicateVariantRE) - return internalIntlRegExps.duplicateVariantRE; - - // RFC 5234 section B.1 - // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - var ALPHA = "[a-zA-Z]"; - // DIGIT = %x30-39 - // ; 0-9 - var DIGIT = "[0-9]"; - - // RFC 5646 section 2.1 - // alphanum = (ALPHA / DIGIT) ; letters and numbers - var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; - // variant = 5*8alphanum ; registered variants - // / (DIGIT 3alphanum) - var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))"; - - // Match a langtag that contains a duplicate variant. - var duplicateVariant = - // Match everything in a langtag prior to any variants, and maybe some - // of the variants as well (which makes this pattern inefficient but - // not wrong, for our purposes); - "(?:" + alphanum + "{2,8}-)+" + - // a variant, parenthesised so that we can refer back to it later; - "(" + variant + ")-" + - // zero or more subtags at least two characters long (thus stopping - // before extension and privateuse components); - "(?:" + alphanum + "{2,8}-)*" + - // and the same variant again - "\\1" + - // ...but not followed by any characters that would turn it into a - // different subtag. - "(?!" + alphanum + ")"; - - // Language tags are case insensitive (RFC 5646 section 2.1.1). Using - // character classes covering both upper- and lower-case characters nearly - // addresses this -- but for the possibility of variant repetition with - // differing case, e.g. "en-variant-Variant". Use a case-insensitive - // regular expression to address this. (Note that there's no worry about - // case transformation accepting invalid characters here: users have - // already verified the string is alphanumeric Latin plus "-".) - return (internalIntlRegExps.duplicateVariantRE = RegExpCreate(duplicateVariant, "i")); -} - - -function getDuplicateSingletonRE() { - if (internalIntlRegExps.duplicateSingletonRE) - return internalIntlRegExps.duplicateSingletonRE; - - // RFC 5234 section B.1 - // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z - var ALPHA = "[a-zA-Z]"; - // DIGIT = %x30-39 - // ; 0-9 - var DIGIT = "[0-9]"; - - // RFC 5646 section 2.1 - // alphanum = (ALPHA / DIGIT) ; letters and numbers - var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; - // singleton = DIGIT ; 0 - 9 - // / %x41-57 ; A - W - // / %x59-5A ; Y - Z - // / %x61-77 ; a - w - // / %x79-7A ; y - z - var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])"; - - // Match a langtag that contains a duplicate singleton. - var duplicateSingleton = - // Match a singleton subtag, parenthesised so that we can refer back to - // it later; - "-(" + singleton + ")-" + - // then zero or more subtags; - "(?:" + alphanum + "+-)*" + - // and the same singleton again - "\\1" + - // ...but not followed by any characters that would turn it into a - // different subtag. - "(?!" + alphanum + ")"; - - // Language tags are case insensitive (RFC 5646 section 2.1.1). Using - // character classes covering both upper- and lower-case characters nearly - // addresses this -- but for the possibility of singleton repetition with - // differing case, e.g. "en-u-foo-U-foo". Use a case-insensitive regular - // expression to address this. (Note that there's no worry about case - // transformation accepting invalid characters here: users have already - // verified the string is alphanumeric Latin plus "-".) - return (internalIntlRegExps.duplicateSingletonRE = RegExpCreate(duplicateSingleton, "i")); -} - - -/** - * Verifies that the given string is a well-formed BCP 47 language tag - * with no duplicate variant or singleton subtags. - * - * Spec: ECMAScript Internationalization API Specification, 6.2.2. - */ -function IsStructurallyValidLanguageTag(locale) { - assert(typeof locale === "string", "IsStructurallyValidLanguageTag"); - var languageTagRE = getLanguageTagRE(); - if (!regexp_test_no_statics(languageTagRE, locale)) - return false; - - // Before checking for duplicate variant or singleton subtags with - // regular expressions, we have to get private use subtag sequences - // out of the picture. - if (callFunction(std_String_startsWith, locale, "x-")) - return true; - var pos = callFunction(std_String_indexOf, locale, "-x-"); - if (pos !== -1) - locale = callFunction(String_substring, locale, 0, pos); - - // Check for duplicate variant or singleton subtags. - var duplicateVariantRE = getDuplicateVariantRE(); - var duplicateSingletonRE = getDuplicateSingletonRE(); - return !regexp_test_no_statics(duplicateVariantRE, locale) && - !regexp_test_no_statics(duplicateSingletonRE, locale); -} - - -/** - * Canonicalizes the given structurally valid BCP 47 language tag, including - * regularized case of subtags. For example, the language tag - * Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE, where - * - * Zh ; 2*3ALPHA - * -NAN ; ["-" extlang] - * -haNS ; ["-" script] - * -bu ; ["-" region] - * -variant2 ; *("-" variant) - * -Variant1 - * -u-ca-chinese ; *("-" extension) - * -t-Zh-laTN - * -x-PRIVATE ; ["-" privateuse] - * - * becomes nan-Hans-mm-variant2-variant1-t-zh-latn-u-ca-chinese-x-private - * - * Spec: ECMAScript Internationalization API Specification, 6.2.3. - * Spec: RFC 5646, section 4.5. - */ -function CanonicalizeLanguageTag(locale) { - assert(IsStructurallyValidLanguageTag(locale), "CanonicalizeLanguageTag"); - - // The input - // "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" - // will be used throughout this method to illustrate how it works. - - // Language tags are compared and processed case-insensitively, so - // technically it's not necessary to adjust case. But for easier processing, - // and because the canonical form for most subtags is lower case, we start - // with lower case for all. - // "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" -> - // "zh-nan-hans-bu-variant2-variant1-u-ca-chinese-t-zh-latn-x-private" - locale = callFunction(std_String_toLowerCase, locale); - - // Handle mappings for complete tags. - if (callFunction(std_Object_hasOwnProperty, langTagMappings, locale)) - return langTagMappings[locale]; - - var subtags = StringSplitString(ToString(locale), "-"); - var i = 0; - - // Handle the standard part: All subtags before the first singleton or "x". - // "zh-nan-hans-bu-variant2-variant1" - while (i < subtags.length) { - var subtag = subtags[i]; - - // If we reach the start of an extension sequence or private use part, - // we're done with this loop. We have to check for i > 0 because for - // irregular language tags, such as i-klingon, the single-character - // subtag "i" is not the start of an extension sequence. - // In the example, we break at "u". - if (subtag.length === 1 && (i > 0 || subtag === "x")) - break; - - if (i !== 0) { - if (subtag.length === 4) { - // 4-character subtags that are not in initial position are - // script codes; their first character needs to be capitalized. - // "hans" -> "Hans" - subtag = callFunction(std_String_toUpperCase, subtag[0]) + - callFunction(String_substring, subtag, 1); - } else if (subtag.length === 2) { - // 2-character subtags that are not in initial position are - // region codes; they need to be upper case. "bu" -> "BU" - subtag = callFunction(std_String_toUpperCase, subtag); - } - } - if (callFunction(std_Object_hasOwnProperty, langSubtagMappings, subtag)) { - // Replace deprecated subtags with their preferred values. - // "BU" -> "MM" - // This has to come after we capitalize region codes because - // otherwise some language and region codes could be confused. - // For example, "in" is an obsolete language code for Indonesian, - // but "IN" is the country code for India. - // Note that the script generating langSubtagMappings makes sure - // that no regular subtag mapping will replace an extlang code. - subtag = langSubtagMappings[subtag]; - } else if (callFunction(std_Object_hasOwnProperty, extlangMappings, subtag)) { - // Replace deprecated extlang subtags with their preferred values, - // and remove the preceding subtag if it's a redundant prefix. - // "zh-nan" -> "nan" - // Note that the script generating extlangMappings makes sure that - // no extlang mapping will replace a normal language code. - subtag = extlangMappings[subtag].preferred; - if (i === 1 && extlangMappings[subtag].prefix === subtags[0]) { - callFunction(std_Array_shift, subtags); - i--; - } - } - subtags[i] = subtag; - i++; - } - var normal = ArrayJoinRange(subtags, "-", 0, i); - - // Extension sequences are sorted by their singleton characters. - // "u-ca-chinese-t-zh-latn" -> "t-zh-latn-u-ca-chinese" - var extensions = new List(); - while (i < subtags.length && subtags[i] !== "x") { - var extensionStart = i; - i++; - while (i < subtags.length && subtags[i].length > 1) - i++; - var extension = ArrayJoinRange(subtags, "-", extensionStart, i); - callFunction(std_Array_push, extensions, extension); - } - callFunction(std_Array_sort, extensions); - - // Private use sequences are left as is. "x-private" - var privateUse = ""; - if (i < subtags.length) - privateUse = ArrayJoinRange(subtags, "-", i); - - // Put everything back together. - var canonical = normal; - if (extensions.length > 0) - canonical += "-" + callFunction(std_Array_join, extensions, "-"); - if (privateUse.length > 0) { - // Be careful of a Language-Tag that is entirely privateuse. - if (canonical.length > 0) - canonical += "-" + privateUse; - else - canonical = privateUse; - } - - return canonical; -} - -/** - * Joins the array elements in the given range with the supplied separator. - */ -function ArrayJoinRange(array, separator, from, to = array.length) { - assert(typeof separator === "string", "|separator| is a string value"); - assert(typeof from === "number", "|from| is a number value"); - assert(typeof to === "number", "|to| is a number value"); - assert(0 <= from && from <= to && to <= array.length, "|from| and |to| form a valid range"); - - if (from === to) - return ""; - - var result = array[from]; - for (var i = from + 1; i < to; i++) { - result += separator + array[i]; - } - return result; -} - -function localeContainsNoUnicodeExtensions(locale) { - // No "-u-", no possible Unicode extension. - if (callFunction(std_String_indexOf, locale, "-u-") === -1) - return true; - - // "-u-" within privateuse also isn't one. - if (callFunction(std_String_indexOf, locale, "-u-") > callFunction(std_String_indexOf, locale, "-x-")) - return true; - - // An entirely-privateuse tag doesn't contain extensions. - if (callFunction(std_String_startsWith, locale, "x-")) - return true; - - // Otherwise, we have a Unicode extension sequence. - return false; -} - - -// The last-ditch locale is used if none of the available locales satisfies a -// request. "en-GB" is used based on the assumptions that English is the most -// common second language, that both en-GB and en-US are normally available in -// an implementation, and that en-GB is more representative of the English used -// in other locales. -function lastDitchLocale() { - // Per bug 1177929, strings don't clone out of self-hosted code as atoms, - // breaking IonBuilder::constant. Put this in a function for now. - return "en-GB"; -} - - -// Certain old, commonly-used language tags that lack a script, are expected to -// nonetheless imply one. This object maps these old-style tags to modern -// equivalents. -var oldStyleLanguageTagMappings = { - "pa-PK": "pa-Arab-PK", - "zh-CN": "zh-Hans-CN", - "zh-HK": "zh-Hant-HK", - "zh-SG": "zh-Hans-SG", - "zh-TW": "zh-Hant-TW", -}; - - -var localeCandidateCache = { - runtimeDefaultLocale: undefined, - candidateDefaultLocale: undefined, -}; - - -var localeCache = { - runtimeDefaultLocale: undefined, - defaultLocale: undefined, -}; - - -/** - * Compute the candidate default locale: the locale *requested* to be used as - * the default locale. We'll use it if and only if ICU provides support (maybe - * fallback support, e.g. supporting "de-ZA" through "de" support implied by a - * "de-DE" locale). - */ -function DefaultLocaleIgnoringAvailableLocales() { - const runtimeDefaultLocale = RuntimeDefaultLocale(); - if (runtimeDefaultLocale === localeCandidateCache.runtimeDefaultLocale) - return localeCandidateCache.candidateDefaultLocale; - - // If we didn't get a cache hit, compute the candidate default locale and - // cache it. Fall back on the last-ditch locale when necessary. - var candidate; - if (!IsStructurallyValidLanguageTag(runtimeDefaultLocale)) { - candidate = lastDitchLocale(); - } else { - candidate = CanonicalizeLanguageTag(runtimeDefaultLocale); - - // The default locale must be in [[availableLocales]], and that list - // must not contain any locales with Unicode extension sequences, so - // remove any present in the candidate. - candidate = removeUnicodeExtensions(candidate); - - if (callFunction(std_Object_hasOwnProperty, oldStyleLanguageTagMappings, candidate)) - candidate = oldStyleLanguageTagMappings[candidate]; - } - - // Cache the candidate locale until the runtime default locale changes. - localeCandidateCache.candidateDefaultLocale = candidate; - localeCandidateCache.runtimeDefaultLocale = runtimeDefaultLocale; - - assert(IsStructurallyValidLanguageTag(candidate), - "the candidate must be structurally valid"); - assert(localeContainsNoUnicodeExtensions(candidate), - "the candidate must not contain a Unicode extension sequence"); - - return candidate; -} - - -/** - * Returns the BCP 47 language tag for the host environment's current locale. - * - * Spec: ECMAScript Internationalization API Specification, 6.2.4. - */ -function DefaultLocale() { - const runtimeDefaultLocale = RuntimeDefaultLocale(); - if (runtimeDefaultLocale === localeCache.runtimeDefaultLocale) - return localeCache.defaultLocale; - - // If we didn't have a cache hit, compute the candidate default locale. - // Then use it as the actual default locale if ICU supports that locale - // (perhaps via fallback). Otherwise use the last-ditch locale. - var candidate = DefaultLocaleIgnoringAvailableLocales(); - var locale; - if (BestAvailableLocaleIgnoringDefault(callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties), - candidate) && - BestAvailableLocaleIgnoringDefault(callFunction(numberFormatInternalProperties.availableLocales, - numberFormatInternalProperties), - candidate) && - BestAvailableLocaleIgnoringDefault(callFunction(dateTimeFormatInternalProperties.availableLocales, - dateTimeFormatInternalProperties), - candidate)) - { - locale = candidate; - } else { - locale = lastDitchLocale(); - } - - assert(IsStructurallyValidLanguageTag(locale), - "the computed default locale must be structurally valid"); - assert(locale === CanonicalizeLanguageTag(locale), - "the computed default locale must be canonical"); - assert(localeContainsNoUnicodeExtensions(locale), - "the computed default locale must not contain a Unicode extension sequence"); - - localeCache.defaultLocale = locale; - localeCache.runtimeDefaultLocale = runtimeDefaultLocale; - - return locale; -} - - -/** - * Verifies that the given string is a well-formed ISO 4217 currency code. - * - * Spec: ECMAScript Internationalization API Specification, 6.3.1. - */ -function getIsWellFormedCurrencyCodeRE() { - return internalIntlRegExps.isWellFormedCurrencyCodeRE || - (internalIntlRegExps.isWellFormedCurrencyCodeRE = RegExpCreate("[^A-Z]")); -} -function IsWellFormedCurrencyCode(currency) { - var c = ToString(currency); - var normalized = toASCIIUpperCase(c); - if (normalized.length !== 3) - return false; - return !regexp_test_no_statics(getIsWellFormedCurrencyCodeRE(), normalized); -} - - -var timeZoneCache = { - icuDefaultTimeZone: undefined, - defaultTimeZone: undefined, -}; - - -/** - * 6.4.2 CanonicalizeTimeZoneName ( timeZone ) - * - * Canonicalizes the given IANA time zone name. - * - * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 - */ -function CanonicalizeTimeZoneName(timeZone) { - assert(typeof timeZone === "string", "CanonicalizeTimeZoneName"); - - // Step 1. (Not applicable, the input is already a valid IANA time zone.) - assert(timeZone !== "Etc/Unknown", "Invalid time zone"); - assert(timeZone === intl_IsValidTimeZoneName(timeZone), "Time zone name not normalized"); - - // Step 2. - var ianaTimeZone = intl_canonicalizeTimeZone(timeZone); - assert(ianaTimeZone !== "Etc/Unknown", "Invalid canonical time zone"); - assert(ianaTimeZone === intl_IsValidTimeZoneName(ianaTimeZone), "Unsupported canonical time zone"); - - // Step 3. - if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") { - // ICU/CLDR canonicalizes Etc/UCT to Etc/GMT, but following IANA and - // ECMA-402 to the letter means Etc/UCT is a separate time zone. - if (timeZone === "Etc/UCT" || timeZone === "UCT") - ianaTimeZone = "Etc/UCT"; - else - ianaTimeZone = "UTC"; - } - - // Step 4. - return ianaTimeZone; -} - - -/** - * 6.4.3 DefaultTimeZone () - * - * Returns the IANA time zone name for the host environment's current time zone. - * - * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 - */ -function DefaultTimeZone() { - const icuDefaultTimeZone = intl_defaultTimeZone(); - if (timeZoneCache.icuDefaultTimeZone === icuDefaultTimeZone) - return timeZoneCache.defaultTimeZone; - - // Verify that the current ICU time zone is a valid ECMA-402 time zone. - var timeZone = intl_IsValidTimeZoneName(icuDefaultTimeZone); - if (timeZone === null) { - // Before defaulting to "UTC", try to represent the default time zone - // using the Etc/GMT + offset format. This format only accepts full - // hour offsets. - const msPerHour = 60 * 60 * 1000; - var offset = intl_defaultTimeZoneOffset(); - assert(offset === (offset | 0), - "milliseconds offset shouldn't be able to exceed int32_t range"); - var offsetHours = offset / msPerHour, offsetHoursFraction = offset % msPerHour; - if (offsetHoursFraction === 0) { - // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset - // means a location west of GMT. - timeZone = "Etc/GMT" + (offsetHours < 0 ? "+" : "-") + std_Math_abs(offsetHours); - - // Check if the fallback is valid. - timeZone = intl_IsValidTimeZoneName(timeZone); - } - - // Fallback to "UTC" if everything else fails. - if (timeZone === null) - timeZone = "UTC"; - } - - // Canonicalize the ICU time zone, e.g. change Etc/UTC to UTC. - var defaultTimeZone = CanonicalizeTimeZoneName(timeZone); - - timeZoneCache.defaultTimeZone = defaultTimeZone; - timeZoneCache.icuDefaultTimeZone = icuDefaultTimeZone; - - return defaultTimeZone; -} - - -/********** Locale and Parameter Negotiation **********/ - -/** - * Add old-style language tags without script code for locales that in current - * usage would include a script subtag. Also add an entry for the last-ditch - * locale, in case ICU doesn't directly support it (but does support it through - * fallback, e.g. supporting "en-GB" indirectly using "en" support). - */ -function addSpecialMissingLanguageTags(availableLocales) { - // Certain old-style language tags lack a script code, but in current usage - // they *would* include a script code. Map these over to modern forms. - var oldStyleLocales = std_Object_getOwnPropertyNames(oldStyleLanguageTagMappings); - for (var i = 0; i < oldStyleLocales.length; i++) { - var oldStyleLocale = oldStyleLocales[i]; - if (availableLocales[oldStyleLanguageTagMappings[oldStyleLocale]]) - availableLocales[oldStyleLocale] = true; - } - - // Also forcibly provide the last-ditch locale. - var lastDitch = lastDitchLocale(); - assert(lastDitch === "en-GB" && availableLocales["en"], - "shouldn't be a need to add every locale implied by the last-" + - "ditch locale, merely just the last-ditch locale"); - availableLocales[lastDitch] = true; -} - - -/** - * Canonicalizes a locale list. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.1. - */ -function CanonicalizeLocaleList(locales) { - if (locales === undefined) - return new List(); - var seen = new List(); - if (typeof locales === "string") - locales = [locales]; - var O = ToObject(locales); - var len = ToLength(O.length); - var k = 0; - while (k < len) { - // Don't call ToString(k) - SpiderMonkey is faster with integers. - var kPresent = HasProperty(O, k); - if (kPresent) { - var kValue = O[k]; - if (!(typeof kValue === "string" || IsObject(kValue))) - ThrowTypeError(JSMSG_INVALID_LOCALES_ELEMENT); - var tag = ToString(kValue); - if (!IsStructurallyValidLanguageTag(tag)) - ThrowRangeError(JSMSG_INVALID_LANGUAGE_TAG, tag); - tag = CanonicalizeLanguageTag(tag); - if (callFunction(ArrayIndexOf, seen, tag) === -1) - callFunction(std_Array_push, seen, tag); - } - k++; - } - return seen; -} - - -function BestAvailableLocaleHelper(availableLocales, locale, considerDefaultLocale) { - assert(IsStructurallyValidLanguageTag(locale), "invalid BestAvailableLocale locale structure"); - assert(locale === CanonicalizeLanguageTag(locale), "non-canonical BestAvailableLocale locale"); - assert(localeContainsNoUnicodeExtensions(locale), "locale must contain no Unicode extensions"); - - // In the spec, [[availableLocales]] is formally a list of all available - // locales. But in our implementation, it's an *incomplete* list, not - // necessarily including the default locale (and all locales implied by it, - // e.g. "de" implied by "de-CH"), if that locale isn't in every - // [[availableLocales]] list (because that locale is supported through - // fallback, e.g. "de-CH" supported through "de"). - // - // If we're considering the default locale, augment the spec loop with - // additional checks to also test whether the current prefix is a prefix of - // the default locale. - - var defaultLocale; - if (considerDefaultLocale) - defaultLocale = DefaultLocale(); - - var candidate = locale; - while (true) { - if (availableLocales[candidate]) - return candidate; - - if (considerDefaultLocale && candidate.length <= defaultLocale.length) { - if (candidate === defaultLocale) - return candidate; - if (callFunction(std_String_startsWith, defaultLocale, candidate + "-")) - return candidate; - } - - var pos = callFunction(std_String_lastIndexOf, candidate, "-"); - if (pos === -1) - return undefined; - - if (pos >= 2 && candidate[pos - 2] === "-") - pos -= 2; - - candidate = callFunction(String_substring, candidate, 0, pos); - } -} - - -/** - * Compares a BCP 47 language tag against the locales in availableLocales - * and returns the best available match. Uses the fallback - * mechanism of RFC 4647, section 3.4. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.2. - * Spec: RFC 4647, section 3.4. - */ -function BestAvailableLocale(availableLocales, locale) { - return BestAvailableLocaleHelper(availableLocales, locale, true); -} - - -/** - * Identical to BestAvailableLocale, but does not consider the default locale - * during computation. - */ -function BestAvailableLocaleIgnoringDefault(availableLocales, locale) { - return BestAvailableLocaleHelper(availableLocales, locale, false); -} - -var noRelevantExtensionKeys = []; - -/** - * Compares a BCP 47 language priority list against the set of locales in - * availableLocales and determines the best available language to meet the - * request. Options specified through Unicode extension subsequences are - * ignored in the lookup, but information about such subsequences is returned - * separately. - * - * This variant is based on the Lookup algorithm of RFC 4647 section 3.4. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.3. - * Spec: RFC 4647, section 3.4. - */ -function LookupMatcher(availableLocales, requestedLocales) { - var i = 0; - var len = requestedLocales.length; - var availableLocale; - var locale, noExtensionsLocale; - while (i < len && availableLocale === undefined) { - locale = requestedLocales[i]; - noExtensionsLocale = removeUnicodeExtensions(locale); - availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale); - i++; - } - - var result = new Record(); - if (availableLocale !== undefined) { - result.locale = availableLocale; - if (locale !== noExtensionsLocale) { - var unicodeLocaleExtensionSequenceRE = getUnicodeLocaleExtensionSequenceRE(); - var extensionMatch = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, locale); - var extension = extensionMatch[0]; - var extensionIndex = extensionMatch.index; - result.extension = extension; - result.extensionIndex = extensionIndex; - } - } else { - result.locale = DefaultLocale(); - } - return result; -} - - -/** - * Compares a BCP 47 language priority list against the set of locales in - * availableLocales and determines the best available language to meet the - * request. Options specified through Unicode extension subsequences are - * ignored in the lookup, but information about such subsequences is returned - * separately. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.4. - */ -function BestFitMatcher(availableLocales, requestedLocales) { - // this implementation doesn't have anything better - return LookupMatcher(availableLocales, requestedLocales); -} - - -/** - * Compares a BCP 47 language priority list against availableLocales and - * determines the best available language to meet the request. Options specified - * through Unicode extension subsequences are negotiated separately, taking the - * caller's relevant extensions and locale data as well as client-provided - * options into consideration. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.5. - */ -function ResolveLocale(availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) { - /*jshint laxbreak: true */ - - // Steps 1-3. - var matcher = options.localeMatcher; - var r = (matcher === "lookup") - ? LookupMatcher(availableLocales, requestedLocales) - : BestFitMatcher(availableLocales, requestedLocales); - - // Step 4. - var foundLocale = r.locale; - - // Step 5.a. - var extension = r.extension; - var extensionIndex, extensionSubtags, extensionSubtagsLength; - - // Step 5. - if (extension !== undefined) { - // Step 5.b. - extensionIndex = r.extensionIndex; - - // Steps 5.d-e. - extensionSubtags = StringSplitString(ToString(extension), "-"); - extensionSubtagsLength = extensionSubtags.length; - } - - // Steps 6-7. - var result = new Record(); - result.dataLocale = foundLocale; - - // Step 8. - var supportedExtension = "-u"; - - // Steps 9-11. - var i = 0; - var len = relevantExtensionKeys.length; - while (i < len) { - // Steps 11.a-c. - var key = relevantExtensionKeys[i]; - - // In this implementation, localeData is a function, not an object. - var foundLocaleData = localeData(foundLocale); - var keyLocaleData = foundLocaleData[key]; - - // Locale data provides default value. - // Step 11.d. - var value = keyLocaleData[0]; - - // Locale tag may override. - - // Step 11.e. - var supportedExtensionAddition = ""; - - // Step 11.f is implemented by Utilities.js. - - var valuePos; - - // Step 11.g. - if (extensionSubtags !== undefined) { - // Step 11.g.i. - var keyPos = callFunction(ArrayIndexOf, extensionSubtags, key); - - // Step 11.g.ii. - if (keyPos !== -1) { - // Step 11.g.ii.1. - if (keyPos + 1 < extensionSubtagsLength && - extensionSubtags[keyPos + 1].length > 2) - { - // Step 11.g.ii.1.a. - var requestedValue = extensionSubtags[keyPos + 1]; - - // Step 11.g.ii.1.b. - valuePos = callFunction(ArrayIndexOf, keyLocaleData, requestedValue); - - // Step 11.g.ii.1.c. - if (valuePos !== -1) { - value = requestedValue; - supportedExtensionAddition = "-" + key + "-" + value; - } - } else { - // Step 11.g.ii.2. - - // According to the LDML spec, if there's no type value, - // and true is an allowed value, it's used. - - // Step 11.g.ii.2.a. - valuePos = callFunction(ArrayIndexOf, keyLocaleData, "true"); - - // Step 11.g.ii.2.b. - if (valuePos !== -1) - value = "true"; - } - } - } - - // Options override all. - - // Step 11.h.i. - var optionsValue = options[key]; - - // Step 11.h, 11.h.ii. - if (optionsValue !== undefined && - callFunction(ArrayIndexOf, keyLocaleData, optionsValue) !== -1) - { - // Step 11.h.ii.1. - if (optionsValue !== value) { - value = optionsValue; - supportedExtensionAddition = ""; - } - } - - // Steps 11.i-k. - result[key] = value; - supportedExtension += supportedExtensionAddition; - i++; - } - - // Step 12. - if (supportedExtension.length > 2) { - var preExtension = callFunction(String_substring, foundLocale, 0, extensionIndex); - var postExtension = callFunction(String_substring, foundLocale, extensionIndex); - foundLocale = preExtension + supportedExtension + postExtension; - } - - // Steps 13-14. - result.locale = foundLocale; - return result; -} - - -/** - * Returns the subset of requestedLocales for which availableLocales has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.6. - */ -function LookupSupportedLocales(availableLocales, requestedLocales) { - // Steps 1-2. - var len = requestedLocales.length; - var subset = new List(); - - // Steps 3-4. - var k = 0; - while (k < len) { - // Steps 4.a-b. - var locale = requestedLocales[k]; - var noExtensionsLocale = removeUnicodeExtensions(locale); - - // Step 4.c-d. - var availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale); - if (availableLocale !== undefined) - callFunction(std_Array_push, subset, locale); - - // Step 4.e. - k++; - } - - // Steps 5-6. - return callFunction(std_Array_slice, subset, 0); -} - - -/** - * Returns the subset of requestedLocales for which availableLocales has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.7. - */ -function BestFitSupportedLocales(availableLocales, requestedLocales) { - // don't have anything better - return LookupSupportedLocales(availableLocales, requestedLocales); -} - - -/** - * Returns the subset of requestedLocales for which availableLocales has a - * matching (possibly fallback) locale. Locales appear in the same order in the - * returned list as in the input list. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.8. - */ -function SupportedLocales(availableLocales, requestedLocales, options) { - /*jshint laxbreak: true */ - - // Step 1. - var matcher; - if (options !== undefined) { - // Steps 1.a-b. - options = ToObject(options); - matcher = options.localeMatcher; - - // Step 1.c. - if (matcher !== undefined) { - matcher = ToString(matcher); - if (matcher !== "lookup" && matcher !== "best fit") - ThrowRangeError(JSMSG_INVALID_LOCALE_MATCHER, matcher); - } - } - - // Steps 2-3. - var subset = (matcher === undefined || matcher === "best fit") - ? BestFitSupportedLocales(availableLocales, requestedLocales) - : LookupSupportedLocales(availableLocales, requestedLocales); - - // Step 4. - for (var i = 0; i < subset.length; i++) { - _DefineDataProperty(subset, i, subset[i], - ATTR_ENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); - } - _DefineDataProperty(subset, "length", subset.length, - ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); - - // Step 5. - return subset; -} - - -/** - * Extracts a property value from the provided options object, converts it to - * the required type, checks whether it is one of a list of allowed values, - * and fills in a fallback value if necessary. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.9. - */ -function GetOption(options, property, type, values, fallback) { - // Step 1. - var value = options[property]; - - // Step 2. - if (value !== undefined) { - // Steps 2.a-c. - if (type === "boolean") - value = ToBoolean(value); - else if (type === "string") - value = ToString(value); - else - assert(false, "GetOption"); - - // Step 2.d. - if (values !== undefined && callFunction(ArrayIndexOf, values, value) === -1) - ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, property, value); - - // Step 2.e. - return value; - } - - // Step 3. - return fallback; -} - -/** - * Extracts a property value from the provided options object, converts it to a - * Number value, checks whether it is in the allowed range, and fills in a - * fallback value if necessary. - * - * Spec: ECMAScript Internationalization API Specification, 9.2.10. - */ -function GetNumberOption(options, property, minimum, maximum, fallback) { - assert(typeof minimum === "number", "GetNumberOption"); - assert(typeof maximum === "number", "GetNumberOption"); - assert(fallback === undefined || (fallback >= minimum && fallback <= maximum), "GetNumberOption"); - - // Step 1. - var value = options[property]; - - // Step 2. - if (value !== undefined) { - value = ToNumber(value); - if (Number_isNaN(value) || value < minimum || value > maximum) - ThrowRangeError(JSMSG_INVALID_DIGITS_VALUE, value); - return std_Math_floor(value); - } - - // Step 3. - return fallback; -} - - -/********** Property access for Intl objects **********/ - - -/** - * Weak map used to track the initialize-as-Intl status (and, if an object has - * been so initialized, the Intl-specific internal properties) of all objects. - * Presence of an object as a key within this map indicates that the object has - * its [[initializedIntlObject]] internal property set to true. The associated - * value is an object whose structure is documented in |initializeIntlObject| - * below. - * - * Ideally we'd be using private symbols for internal properties, but - * SpiderMonkey doesn't have those yet. - */ -var internalsMap = new WeakMap(); - - -/** - * Set the [[initializedIntlObject]] internal property of |obj| to true. - */ -function initializeIntlObject(obj) { - assert(IsObject(obj), "Non-object passed to initializeIntlObject"); - - // Intl-initialized objects are weird. They have [[initializedIntlObject]] - // set on them, but they don't *necessarily* have any other properties. - - var internals = std_Object_create(null); - - // The meaning of an internals object for an object |obj| is as follows. - // - // If the .type is "partial", |obj| has [[initializedIntlObject]] set but - // nothing else. No other property of |internals| can be used. (This - // occurs when InitializeCollator or similar marks an object as - // [[initializedIntlObject]] but fails before marking it as the appropriate - // more-specific type ["Collator", "DateTimeFormat", "NumberFormat"].) - // - // Otherwise, the .type indicates the type of Intl object that |obj| is: - // "Collator", "DateTimeFormat", or "NumberFormat" (likely with more coming - // in future Intl specs). In these cases |obj| *conceptually* also has - // [[initializedCollator]] or similar set, and all the other properties - // implied by that. - // - // If |internals| doesn't have a "partial" .type, two additional properties - // have meaning. The .lazyData property stores information needed to - // compute -- without observable side effects -- the actual internal Intl - // properties of |obj|. If it is non-null, then the actual internal - // properties haven't been computed, and .lazyData must be processed by - // |setInternalProperties| before internal Intl property values are - // available. If it is null, then the .internalProps property contains an - // object whose properties are the internal Intl properties of |obj|. - - internals.type = "partial"; - internals.lazyData = null; - internals.internalProps = null; - - callFunction(std_WeakMap_set, internalsMap, obj, internals); - return internals; -} - - -/** - * Mark |internals| as having the given type and lazy data. - */ -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" || type === "PluralRules" || - type === "RelativeTimeFormat", - "bad type"); - assert(IsObject(lazyData), "non-object lazy data"); - - // Set in reverse order so that the .type change is a barrier. - internals.lazyData = lazyData; - internals.type = type; -} - - -/** - * Set the internal properties object for an |internals| object previously - * associated with lazy data. - */ -function setInternalProperties(internals, internalProps) -{ - assert(internals.type !== "partial", "newborn internals can't have computed internals"); - assert(IsObject(internals.lazyData), "lazy data must exist already"); - assert(IsObject(internalProps), "internalProps argument should be an object"); - - // Set in reverse order so that the .lazyData nulling is a barrier. - internals.internalProps = internalProps; - internals.lazyData = null; -} - - -/** - * Get the existing internal properties out of a non-newborn |internals|, or - * null if none have been computed. - */ -function maybeInternalProperties(internals) -{ - assert(IsObject(internals), "non-object passed to maybeInternalProperties"); - assert(internals.type !== "partial", "maybeInternalProperties must only be used on completely-initialized internals objects"); - var lazyData = internals.lazyData; - if (lazyData) - return null; - assert(IsObject(internals.internalProps), "missing lazy data and computed internals"); - return internals.internalProps; -} - - -/** - * Return whether |obj| has an[[initializedIntlObject]] property set to true. - */ -function isInitializedIntlObject(obj) { -#ifdef DEBUG - var internals = callFunction(std_WeakMap_get, internalsMap, 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" || - type === "PluralRules" || type === "RelativeTimeFormat", - "unexpected type"); - assert(callFunction(std_Object_hasOwnProperty, internals, "lazyData"), "missing lazyData"); - assert(callFunction(std_Object_hasOwnProperty, internals, "internalProps"), "missing internalProps"); - } else { - assert(internals === undefined, "bad mapping for |obj|"); - } -#endif - return callFunction(std_WeakMap_has, internalsMap, obj); -} - - -/** - * Check that |obj| meets the requirements for "this Collator object", "this - * NumberFormat object", or "this DateTimeFormat object" as used in the method - * with the given name. Throw a TypeError if |obj| doesn't meet these - * requirements. But if it does, return |obj|'s internals object (*not* the - * object holding its internal properties!), associated with it by - * |internalsMap|, with structure specified above. - * - * Spec: ECMAScript Internationalization API Specification, 10.3. - * Spec: ECMAScript Internationalization API Specification, 11.3. - * Spec: ECMAScript Internationalization API Specification, 12.3. - */ -function getIntlObjectInternals(obj, className, methodName) { - assert(typeof className === "string", "bad className for getIntlObjectInternals"); - - var internals = callFunction(std_WeakMap_get, internalsMap, obj); - assert(internals === undefined || isInitializedIntlObject(obj), "bad mapping in internalsMap"); - - if (internals === undefined || internals.type !== className) - ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, className, methodName, className); - - return internals; -} - - -/** - * Get the internal properties of known-Intl object |obj|. For use only by - * C++ code that knows what it's doing! - */ -function getInternals(obj) -{ - assert(isInitializedIntlObject(obj), "for use only on guaranteed Intl objects"); - - var internals = callFunction(std_WeakMap_get, internalsMap, obj); - - assert(internals.type !== "partial", "must have been successfully initialized"); - var lazyData = internals.lazyData; - if (!lazyData) - return internals.internalProps; - - var internalProps; - var type = internals.type; - - switch (type) { - case "Collator": - internalProps = resolveCollatorInternals(lazyData); - break; - case "DateTimeFormat": - internalProps = resolveDateTimeFormatInternals(lazyData); - break; - case "PluralRules": - internalProps = resolvePluralRulesInternals(lazyData); - break; - case "RelativeTimeFormat": - internalProps = resolveRelativeTimeFormatInternals(lazyData); - break; - default: // type === "NumberFormat" - internalProps = resolveNumberFormatInternals(lazyData); - break; - } - setInternalProperties(internals, internalProps); - return internalProps; -} - - -/********** Intl.Collator **********/ - - -/** - * Mapping from Unicode extension keys for collation to options properties, - * their types and permissible values. - * - * Spec: ECMAScript Internationalization API Specification, 10.1.1. - */ -var collatorKeyMappings = { - kn: {property: "numeric", type: "boolean"}, - kf: {property: "caseFirst", type: "string", values: ["upper", "lower", "false"]} -}; - - -/** - * Compute an internal properties object from |lazyCollatorData|. - */ -function resolveCollatorInternals(lazyCollatorData) -{ - assert(IsObject(lazyCollatorData), "lazy data not an object?"); - - var internalProps = std_Object_create(null); - - // Step 7. - internalProps.usage = lazyCollatorData.usage; - - // Step 8. - var Collator = collatorInternalProperties; - - // Step 9. - var collatorIsSorting = lazyCollatorData.usage === "sort"; - var localeData = collatorIsSorting - ? Collator.sortLocaleData - : Collator.searchLocaleData; - - // Compute effective locale. - // Step 14. - var relevantExtensionKeys = Collator.relevantExtensionKeys; - - // Step 15. - var r = ResolveLocale(callFunction(Collator.availableLocales, Collator), - lazyCollatorData.requestedLocales, - lazyCollatorData.opt, - relevantExtensionKeys, - localeData); - - // Step 16. - internalProps.locale = r.locale; - - // Steps 17-19. - var key, property, value, mapping; - var i = 0, len = relevantExtensionKeys.length; - while (i < len) { - // Step 19.a. - key = relevantExtensionKeys[i]; - if (key === "co") { - // Step 19.b. - property = "collation"; - value = r.co === null ? "default" : r.co; - } else { - // Step 19.c. - mapping = collatorKeyMappings[key]; - property = mapping.property; - value = r[key]; - if (mapping.type === "boolean") - value = value === "true"; - } - - // Step 19.d. - internalProps[property] = value; - - // Step 19.e. - i++; - } - - // Compute remaining collation options. - // Steps 21-22. - var s = lazyCollatorData.rawSensitivity; - if (s === undefined) { - if (collatorIsSorting) { - // Step 21.a. - s = "variant"; - } else { - // Step 21.b. - var dataLocale = r.dataLocale; - var dataLocaleData = localeData(dataLocale); - s = dataLocaleData.sensitivity; - } - } - internalProps.sensitivity = s; - - // Step 24. - internalProps.ignorePunctuation = lazyCollatorData.ignorePunctuation; - - // Step 25. - internalProps.boundFormat = undefined; - - // The caller is responsible for associating |internalProps| with the right - // object using |setInternalProperties|. - return internalProps; -} - - -/** - * Returns an object containing the Collator internal properties of |obj|, or - * throws a TypeError if |obj| isn't Collator-initialized. - */ -function getCollatorInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "Collator", methodName); - assert(internals.type === "Collator", "bad type escaped getIntlObjectInternals"); - - // If internal properties have already been computed, use them. - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - // Otherwise it's time to fully create them. - internalProps = resolveCollatorInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - - -/** - * Initializes an object as a Collator. - * - * 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 Collator. This - * later work occurs in |resolveCollatorInternals|; steps not noted here occur - * there. - * - * Spec: ECMAScript Internationalization API Specification, 10.1.1. - */ -function InitializeCollator(collator, locales, options) { - assert(IsObject(collator), "InitializeCollator"); - - // Step 1. - if (isInitializedIntlObject(collator)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - // Step 2. - var internals = initializeIntlObject(collator); - - // Lazy Collator data has the following structure: - // - // { - // requestedLocales: List of locales, - // usage: "sort" / "search", - // opt: // opt object computed in InitializeCollator - // { - // localeMatcher: "lookup" / "best fit", - // kn: true / false / undefined, - // kf: "upper" / "lower" / "false" / undefined - // } - // rawSensitivity: "base" / "accent" / "case" / "variant" / undefined, - // ignorePunctuation: true / false - // } - // - // 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 - // subset of them. - var lazyCollatorData = std_Object_create(null); - - // Step 3. - var requestedLocales = CanonicalizeLocaleList(locales); - lazyCollatorData.requestedLocales = requestedLocales; - - // Steps 4-5. - // - // If we ever need more speed here at startup, we should try to detect the - // case where |options === undefined| and Object.prototype hasn't been - // mucked with. (|options| is fully consumed in this method, so it's not a - // concern that Object.prototype might be touched between now and when - // |resolveCollatorInternals| is called.) For now, just keep it simple. - if (options === undefined) - options = {}; - else - options = ToObject(options); - - // Compute options that impact interpretation of locale. - // Step 6. - var u = GetOption(options, "usage", "string", ["sort", "search"], "sort"); - lazyCollatorData.usage = u; - - // Step 10. - var opt = new Record(); - lazyCollatorData.opt = opt; - - // Steps 11-12. - var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); - opt.localeMatcher = matcher; - - // Step 13, unrolled. - var numericValue = GetOption(options, "numeric", "boolean", undefined, undefined); - if (numericValue !== undefined) - numericValue = numericValue ? 'true' : 'false'; - opt.kn = numericValue; - - var caseFirstValue = GetOption(options, "caseFirst", "string", ["upper", "lower", "false"], undefined); - opt.kf = caseFirstValue; - - // Compute remaining collation options. - // Step 20. - var s = GetOption(options, "sensitivity", "string", - ["base", "accent", "case", "variant"], undefined); - lazyCollatorData.rawSensitivity = s; - - // Step 23. - var ip = GetOption(options, "ignorePunctuation", "boolean", undefined, false); - lazyCollatorData.ignorePunctuation = ip; - - // Step 26. - // - // We've done everything that must be done now: mark the lazy data as fully - // computed and install it. - setLazyData(internals, "Collator", lazyCollatorData); -} - - -/** - * 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 Internationalization API Specification, 10.2.2. - */ -function Intl_Collator_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - var availableLocales = callFunction(collatorInternalProperties.availableLocales, - collatorInternalProperties); - var requestedLocales = CanonicalizeLocaleList(locales); - return SupportedLocales(availableLocales, requestedLocales, options); -} - - -/** - * Collator internal properties. - * - * Spec: ECMAScript Internationalization API Specification, 9.1 and 10.2.3. - */ -var collatorInternalProperties = { - sortLocaleData: collatorSortLocaleData, - searchLocaleData: collatorSearchLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_Collator_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: ["co", "kn"] -}; - - -function collatorSortLocaleData(locale) { - var collations = intl_availableCollations(locale); - callFunction(std_Array_unshift, collations, null); - return { - co: collations, - kn: ["false", "true"] - }; -} - - -function collatorSearchLocaleData(locale) { - return { - co: [null], - kn: ["false", "true"], - // In theory the default sensitivity is locale dependent; - // in reality the CLDR/ICU default strength is always tertiary. - sensitivity: "variant" - }; -} - - -/** - * Function to be bound and returned by Intl.Collator.prototype.format. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - */ -function collatorCompareToBind(x, y) { - // Steps 1.a.i-ii implemented by ECMAScript declaration binding instantiation, - // ES5.1 10.5, step 4.d.ii. - - // Step 1.a.iii-v. - var X = ToString(x); - var Y = ToString(y); - return intl_CompareStrings(this, X, Y); -} - - -/** - * Returns a function bound to this Collator that compares x (converted to a - * String value) and y (converted to a String value), - * and returns a number less than 0 if x < y, 0 if x = y, or a number greater - * than 0 if x > y according to the sort order for the locale and collation - * options of this Collator object. - * - * Spec: ECMAScript Internationalization API Specification, 10.3.2. - */ -function Intl_Collator_compare_get() { - // Check "this Collator object" per introduction of section 10.3. - var internals = getCollatorInternals(this, "compare"); - - // Step 1. - if (internals.boundCompare === undefined) { - // Step 1.a. - var F = collatorCompareToBind; - - // Step 1.b-d. - var bc = callFunction(FunctionBind, F, this); - internals.boundCompare = bc; - } - - // Step 2. - return internals.boundCompare; -} - - -/** - * Returns the resolved options for a Collator object. - * - * Spec: ECMAScript Internationalization API Specification, 10.3.3 and 10.4. - */ -function Intl_Collator_resolvedOptions() { - // Check "this Collator object" per introduction of section 10.3. - var internals = getCollatorInternals(this, "resolvedOptions"); - - var result = { - locale: internals.locale, - usage: internals.usage, - sensitivity: internals.sensitivity, - ignorePunctuation: internals.ignorePunctuation - }; - - var relevantExtensionKeys = collatorInternalProperties.relevantExtensionKeys; - for (var i = 0; i < relevantExtensionKeys.length; i++) { - var key = relevantExtensionKeys[i]; - var property = (key === "co") ? "collation" : collatorKeyMappings[key].property; - _DefineDataProperty(result, property, internals[property]); - } - return result; -} - - -/********** Intl.NumberFormat **********/ - - -/** - * NumberFormat internal properties. - * - * Spec: ECMAScript Internationalization API Specification, 9.1 and 11.2.3. - */ -var numberFormatInternalProperties = { - localeData: numberFormatLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_NumberFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: ["nu"] -}; - - -/** - * Compute an internal properties object from |lazyNumberFormatData|. - */ -function resolveNumberFormatInternals(lazyNumberFormatData) { - assert(IsObject(lazyNumberFormatData), "lazy data not an object?"); - - var internalProps = std_Object_create(null); - - // Step 3. - var requestedLocales = lazyNumberFormatData.requestedLocales; - - // Compute options that impact interpretation of locale. - // Step 6. - var opt = lazyNumberFormatData.opt; - - var NumberFormat = numberFormatInternalProperties; - - // Step 9. - var localeData = NumberFormat.localeData; - - // Step 10. - var r = ResolveLocale(callFunction(NumberFormat.availableLocales, NumberFormat), - lazyNumberFormatData.requestedLocales, - lazyNumberFormatData.opt, - NumberFormat.relevantExtensionKeys, - localeData); - - // Steps 11-12. (Step 13 is not relevant to our implementation.) - internalProps.locale = r.locale; - internalProps.numberingSystem = r.nu; - - // Compute formatting options. - // Step 15. - var s = lazyNumberFormatData.style; - internalProps.style = s; - - // Steps 19, 21. - if (s === "currency") { - internalProps.currency = lazyNumberFormatData.currency; - internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay; - } - - internalProps.minimumIntegerDigits = lazyNumberFormatData.minimumIntegerDigits; - - internalProps.minimumFractionDigits = lazyNumberFormatData.minimumFractionDigits; - - internalProps.maximumFractionDigits = lazyNumberFormatData.maximumFractionDigits; - - if ("minimumSignificantDigits" in lazyNumberFormatData) { - // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the - // actual presence (versus undefined-ness) of these properties. - assert("maximumSignificantDigits" in lazyNumberFormatData, "min/max sig digits mismatch"); - internalProps.minimumSignificantDigits = lazyNumberFormatData.minimumSignificantDigits; - internalProps.maximumSignificantDigits = lazyNumberFormatData.maximumSignificantDigits; - } - - // Step 27. - internalProps.useGrouping = lazyNumberFormatData.useGrouping; - - // Step 34. - internalProps.boundFormat = undefined; - - // The caller is responsible for associating |internalProps| with the right - // object using |setInternalProperties|. - return internalProps; -} - - -/** - * Returns an object containing the NumberFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't NumberFormat-initialized. - */ -function getNumberFormatInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "NumberFormat", methodName); - assert(internals.type === "NumberFormat", "bad type escaped getIntlObjectInternals"); - - // If internal properties have already been computed, use them. - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - // Otherwise it's time to fully create them. - internalProps = resolveNumberFormatInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - 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. - * - * 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 NumberFormat. - * This later work occurs in |resolveNumberFormatInternals|; steps not noted - * here occur there. - * - * Spec: ECMAScript Internationalization API Specification, 11.1.1. - */ -function InitializeNumberFormat(numberFormat, locales, options) { - assert(IsObject(numberFormat), "InitializeNumberFormat"); - - // Step 1. - if (isInitializedIntlObject(numberFormat)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - // Step 2. - var internals = initializeIntlObject(numberFormat); - - // Lazy NumberFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // style: "decimal" / "percent" / "currency", - // - // // fields present only if style === "currency": - // currency: a well-formed currency code (IsWellFormedCurrencyCode), - // currencyDisplay: "code" / "symbol" / "name", - // - // opt: // opt object computed in InitializeNumberFormat - // { - // 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], - // - // useGrouping: true / false, - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every NumberFormat lazy data object has *all* these properties, never a - // subset of them. - var lazyNumberFormatData = std_Object_create(null); - - // Step 3. - var requestedLocales = CanonicalizeLocaleList(locales); - lazyNumberFormatData.requestedLocales = requestedLocales; - - // Steps 4-5. - // - // If we ever need more speed here at startup, we should try to detect the - // case where |options === undefined| and Object.prototype hasn't been - // mucked with. (|options| is fully consumed in this method, so it's not a - // concern that Object.prototype might be touched between now and when - // |resolveNumberFormatInternals| is called.) For now just keep it simple. - if (options === undefined) - options = {}; - else - options = ToObject(options); - - // Compute options that impact interpretation of locale. - // Step 6. - var opt = new Record(); - lazyNumberFormatData.opt = opt; - - // Steps 7-8. - var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); - opt.localeMatcher = matcher; - - // Compute formatting options. - // Step 14. - var s = GetOption(options, "style", "string", ["decimal", "percent", "currency"], "decimal"); - lazyNumberFormatData.style = s; - - // Steps 16-19. - var c = GetOption(options, "currency", "string", undefined, undefined); - if (c !== undefined && !IsWellFormedCurrencyCode(c)) - ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, c); - var cDigits; - if (s === "currency") { - if (c === undefined) - ThrowTypeError(JSMSG_UNDEFINED_CURRENCY); - - // Steps 19.a-c. - c = toASCIIUpperCase(c); - lazyNumberFormatData.currency = c; - cDigits = CurrencyDigits(c); - } - - // Step 20. - var cd = GetOption(options, "currencyDisplay", "string", ["code", "symbol", "name"], "symbol"); - if (s === "currency") - lazyNumberFormatData.currencyDisplay = cd; - - // Steps 22-24. - SetNumberFormatDigitOptions(lazyNumberFormatData, options, s === "currency" ? cDigits: 0); - - // Step 25. - if (lazyNumberFormatData.maximumFractionDigits === undefined) { - let mxfdDefault = s === "currency" - ? cDigits - : s === "percent" - ? 0 - : 3; - lazyNumberFormatData.maximumFractionDigits = - std_Math_max(lazyNumberFormatData.minimumFractionDigits, mxfdDefault); - } - - // Step 26. - var g = GetOption(options, "useGrouping", "boolean", undefined, true); - lazyNumberFormatData.useGrouping = g; - - // Steps 35-36. - // - // We've done everything that must be done now: mark the lazy data as fully - // computed and install it. - setLazyData(internals, "NumberFormat", lazyNumberFormatData); -} - - -/** - * Mapping from currency codes to the number of decimal digits used for them. - * Default is 2 digits. - * - * Spec: ISO 4217 Currency and Funds Code List. - * http://www.currency-iso.org/en/home/tables/table-a1.html - */ -var currencyDigits = { - BHD: 3, - BIF: 0, - BYR: 0, - CLF: 4, - CLP: 0, - DJF: 0, - GNF: 0, - IQD: 3, - ISK: 0, - JOD: 3, - JPY: 0, - KMF: 0, - KRW: 0, - KWD: 3, - LYD: 3, - OMR: 3, - PYG: 0, - RWF: 0, - TND: 3, - UGX: 0, - UYI: 0, - VND: 0, - VUV: 0, - XAF: 0, - XOF: 0, - XPF: 0 -}; - - -/** - * Returns the number of decimal digits to be used for the given currency. - * - * Spec: ECMAScript Internationalization API Specification, 11.1.1. - */ -function getCurrencyDigitsRE() { - return internalIntlRegExps.currencyDigitsRE || - (internalIntlRegExps.currencyDigitsRE = RegExpCreate("^[A-Z]{3}$")); -} -function CurrencyDigits(currency) { - assert(typeof currency === "string", "CurrencyDigits"); - assert(regexp_test_no_statics(getCurrencyDigitsRE(), currency), "CurrencyDigits"); - - if (callFunction(std_Object_hasOwnProperty, currencyDigits, currency)) - return currencyDigits[currency]; - return 2; -} - - -/** - * 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 Internationalization API Specification, 11.2.2. - */ -function Intl_NumberFormat_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - var availableLocales = callFunction(numberFormatInternalProperties.availableLocales, - numberFormatInternalProperties); - var requestedLocales = CanonicalizeLocaleList(locales); - return SupportedLocales(availableLocales, requestedLocales, options); -} - - -function getNumberingSystems(locale) { - // ICU doesn't have an API to determine the set of numbering systems - // supported for a locale; it generally pretends that any numbering system - // can be used with any locale. Supporting a decimal numbering system - // (where only the digits are replaced) is easy, so we offer them all here. - // Algorithmic numbering systems are typically tied to one locale, so for - // lack of information we don't offer them. To increase chances that - // other software will process output correctly, we further restrict to - // those decimal numbering systems explicitly listed in table 2 of - // the ECMAScript Internationalization API Specification, 11.3.2, which - // in turn are those with full specifications in version 21 of Unicode - // Technical Standard #35 using digits that were defined in Unicode 5.0, - // the Unicode version supported in Windows Vista. - // The one thing we can find out from ICU is the default numbering system - // for a locale. - var defaultNumberingSystem = intl_numberingSystem(locale); - return [ - defaultNumberingSystem, - "arab", "arabext", "bali", "beng", "deva", - "fullwide", "gujr", "guru", "hanidec", "khmr", - "knda", "laoo", "latn", "limb", "mlym", - "mong", "mymr", "orya", "tamldec", "telu", - "thai", "tibt" - ]; -} - - -function numberFormatLocaleData(locale) { - return { - nu: getNumberingSystems(locale) - }; -} - - -/** - * Function to be bound and returned by Intl.NumberFormat.prototype.format. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.2. - */ -function numberFormatFormatToBind(value) { - // Steps 1.a.i implemented by ECMAScript declaration binding instantiation, - // ES5.1 10.5, step 4.d.ii. - - // Step 1.a.ii-iii. - var x = ToNumber(value); - return intl_FormatNumber(this, x, /* formatToParts = */ false); -} - - -/** - * Returns a function bound to this NumberFormat that returns a String value - * representing the result of calling ToNumber(value) according to the - * effective locale and the formatting options of this NumberFormat. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.2. - */ -function Intl_NumberFormat_format_get() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "format"); - - // Step 1. - if (internals.boundFormat === undefined) { - // Step 1.a. - var F = numberFormatFormatToBind; - - // Step 1.b-d. - var bf = callFunction(FunctionBind, F, this); - internals.boundFormat = bf; - } - // Step 2. - return internals.boundFormat; -} - -function Intl_NumberFormat_formatToParts(value) { - // Step 1. - var nf = this; - - // Steps 2-3. - getNumberFormatInternals(nf, "formatToParts"); - - // Step 4. - var x = ToNumber(value); - - // Step 5. - return intl_FormatNumber(nf, x, /* formatToParts = */ true); -} - -/** - * Returns the resolved options for a NumberFormat object. - * - * Spec: ECMAScript Internationalization API Specification, 11.3.3 and 11.4. - */ -function Intl_NumberFormat_resolvedOptions() { - // Check "this NumberFormat object" per introduction of section 11.3. - var internals = getNumberFormatInternals(this, "resolvedOptions"); - - var result = { - locale: internals.locale, - numberingSystem: internals.numberingSystem, - style: internals.style, - minimumIntegerDigits: internals.minimumIntegerDigits, - minimumFractionDigits: internals.minimumFractionDigits, - maximumFractionDigits: internals.maximumFractionDigits, - useGrouping: internals.useGrouping - }; - var optionalProperties = [ - "currency", - "currencyDisplay", - "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; -} - - -/********** Intl.DateTimeFormat **********/ - - -/** - * Compute an internal properties object from |lazyDateTimeFormatData|. - */ -function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { - assert(IsObject(lazyDateTimeFormatData), "lazy data not an object?"); - - // Lazy DateTimeFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // - // localeOpt: // *first* opt computed in InitializeDateTimeFormat - // { - // localeMatcher: "lookup" / "best fit", - // - // hour12: true / false, // optional - // } - // - // timeZone: IANA time zone name, - // - // formatOpt: // *second* opt computed in InitializeDateTimeFormat - // { - // // all the properties/values listed in Table 3 - // // (weekday, era, year, month, day, &c.) - // } - // - // formatMatcher: "basic" / "best fit", - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every DateTimeFormat lazy data object has *all* these properties, - // never a subset of them. - - var internalProps = std_Object_create(null); - - // Compute effective locale. - // Step 8. - var DateTimeFormat = dateTimeFormatInternalProperties; - - // Step 9. - var localeData = DateTimeFormat.localeData; - - // Step 10. - var r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), - lazyDateTimeFormatData.requestedLocales, - lazyDateTimeFormatData.localeOpt, - DateTimeFormat.relevantExtensionKeys, - localeData); - - // Steps 11-13. - internalProps.locale = r.locale; - internalProps.calendar = r.ca; - internalProps.numberingSystem = r.nu; - - // Compute formatting options. - // Step 14. - var dataLocale = r.dataLocale; - - // Steps 15-17. - var tz = lazyDateTimeFormatData.timeZone; - if (tz === undefined) { - // Step 16. - tz = DefaultTimeZone(); - } - internalProps.timeZone = tz; - - // Step 18. - var formatOpt = lazyDateTimeFormatData.formatOpt; - - // Steps 27-28, more or less - see comment after this function. - var pattern = toBestICUPattern(dataLocale, formatOpt); - - // Step 29. - internalProps.pattern = pattern; - - // Step 30. - internalProps.boundFormat = undefined; - - // The caller is responsible for associating |internalProps| with the right - // object using |setInternalProperties|. - return internalProps; -} - - -/** - * Returns an object containing the DateTimeFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't DateTimeFormat-initialized. - */ -function getDateTimeFormatInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "DateTimeFormat", methodName); - assert(internals.type === "DateTimeFormat", "bad type escaped getIntlObjectInternals"); - - // If internal properties have already been computed, use them. - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - // Otherwise it's time to fully create them. - internalProps = resolveDateTimeFormatInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - -/** - * Components of date and time formats and their values. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -var dateTimeComponentValues = { - weekday: ["narrow", "short", "long"], - era: ["narrow", "short", "long"], - year: ["2-digit", "numeric"], - month: ["2-digit", "numeric", "narrow", "short", "long"], - day: ["2-digit", "numeric"], - hour: ["2-digit", "numeric"], - minute: ["2-digit", "numeric"], - second: ["2-digit", "numeric"], - timeZoneName: ["short", "long"] -}; - - -var dateTimeComponents = std_Object_getOwnPropertyNames(dateTimeComponentValues); - - -/** - * Initializes an object as a DateTimeFormat. - * - * 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 DateTimeFormat. - * This later work occurs in |resolveDateTimeFormatInternals|; steps not noted - * here occur there. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function InitializeDateTimeFormat(dateTimeFormat, locales, options) { - assert(IsObject(dateTimeFormat), "InitializeDateTimeFormat"); - - // Step 1. - if (isInitializedIntlObject(dateTimeFormat)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - // Step 2. - var internals = initializeIntlObject(dateTimeFormat); - - // Lazy DateTimeFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // - // localeOpt: // *first* opt computed in InitializeDateTimeFormat - // { - // localeMatcher: "lookup" / "best fit", - // } - // - // timeZone: IANA time zone name, - // - // formatOpt: // *second* opt computed in InitializeDateTimeFormat - // { - // // all the properties/values listed in Table 3 - // // (weekday, era, year, month, day, &c.) - // - // hour12: true / false // optional - // } - // - // formatMatcher: "basic" / "best fit", - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every DateTimeFormat lazy data object has *all* these properties, - // never a subset of them. - var lazyDateTimeFormatData = std_Object_create(null); - - // Step 3. - var requestedLocales = CanonicalizeLocaleList(locales); - lazyDateTimeFormatData.requestedLocales = requestedLocales; - - // Step 4. - options = ToDateTimeOptions(options, "any", "date"); - - // Compute options that impact interpretation of locale. - // Step 5. - var localeOpt = new Record(); - lazyDateTimeFormatData.localeOpt = localeOpt; - - // Steps 6-7. - var localeMatcher = - GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], - "best fit"); - localeOpt.localeMatcher = localeMatcher; - - // Steps 15-17. - var tz = options.timeZone; - if (tz !== undefined) { - // Step 15.a. - tz = ToString(tz); - - // Step 15.b. - var timeZone = intl_IsValidTimeZoneName(tz); - if (timeZone === null) - ThrowRangeError(JSMSG_INVALID_TIME_ZONE, tz); - - // Step 15.c. - tz = CanonicalizeTimeZoneName(timeZone); - } - lazyDateTimeFormatData.timeZone = tz; - - // Step 18. - var formatOpt = new Record(); - lazyDateTimeFormatData.formatOpt = formatOpt; - - // Step 19. - var i, prop; - for (i = 0; i < dateTimeComponents.length; i++) { - prop = dateTimeComponents[i]; - var value = GetOption(options, prop, "string", dateTimeComponentValues[prop], undefined); - formatOpt[prop] = value; - } - - // Steps 20-21 provided by ICU - see comment after this function. - - // Step 22. - // - // For some reason (ICU not exposing enough interface?) we drop the - // requested format matcher on the floor after this. In any case, even if - // doing so is justified, we have to do this work here in case it triggers - // getters or similar. (bug 852837) - var formatMatcher = - GetOption(options, "formatMatcher", "string", ["basic", "best fit"], - "best fit"); - - // Steps 23-25 provided by ICU, more or less - see comment after this function. - - // Step 26. - var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined); - - // Pass hr12 on to ICU. - if (hr12 !== undefined) - formatOpt.hour12 = hr12; - - // Step 31. - // - // We've done everything that must be done now: mark the lazy data as fully - // computed and install it. - setLazyData(internals, "DateTimeFormat", lazyDateTimeFormatData); -} - - -// Intl.DateTimeFormat and ICU skeletons and patterns -// ================================================== -// -// Different locales have different ways to display dates using the same -// basic components. For example, en-US might use "Sept. 24, 2012" while -// fr-FR might use "24 Sept. 2012". The intent of Intl.DateTimeFormat is to -// permit production of a format for the locale that best matches the -// set of date-time components and their desired representation as specified -// by the API client. -// -// ICU supports specification of date and time formats in three ways: -// -// 1) A style is just one of the identifiers FULL, LONG, MEDIUM, or SHORT. -// The date-time components included in each style and their representation -// are defined by ICU using CLDR locale data (CLDR is the Unicode -// Consortium's Common Locale Data Repository). -// -// 2) A skeleton is a string specifying which date-time components to include, -// and which representations to use for them. For example, "yyyyMMMMdd" -// specifies a year with at least four digits, a full month name, and a -// two-digit day. It does not specify in which order the components appear, -// how they are separated, the localized strings for textual components -// (such as weekday or month), whether the month is in format or -// stand-alone form¹, or the numbering system used for numeric components. -// All that information is filled in by ICU using CLDR locale data. -// ¹ The format form is the one used in formatted strings that include a -// day; the stand-alone form is used when not including days, e.g., in -// calendar headers. The two forms differ at least in some Slavic languages, -// e.g. Russian: "22 марта 2013 г." vs. "Март 2013". -// -// 3) A pattern is a string specifying which date-time components to include, -// in which order, with which separators, in which grammatical case. For -// example, "EEEE, d MMMM y" specifies the full localized weekday name, -// followed by comma and space, followed by the day, followed by space, -// followed by the full month name in format form, followed by space, -// followed by the full year. It -// still does not specify localized strings for textual components and the -// numbering system - these are determined by ICU using CLDR locale data or -// possibly API parameters. -// -// All actual formatting in ICU is done with patterns; styles and skeletons -// have to be mapped to patterns before processing. -// -// The options of DateTimeFormat most closely correspond to ICU skeletons. This -// implementation therefore, in the toBestICUPattern function, converts -// DateTimeFormat options to ICU skeletons, and then lets ICU map skeletons to -// actual ICU patterns. The pattern may not directly correspond to what the -// skeleton requests, as the mapper (UDateTimePatternGenerator) is constrained -// by the available locale data for the locale. The resulting ICU pattern is -// kept as the DateTimeFormat's [[pattern]] internal property and passed to ICU -// in the format method. -// -// An ICU pattern represents the information of the following DateTimeFormat -// internal properties described in the specification, which therefore don't -// exist separately in the implementation: -// - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], -// [[second]], [[timeZoneName]] -// - [[hour12]] -// - [[hourNo0]] -// When needed for the resolvedOptions method, the resolveICUPattern function -// maps the instance's ICU pattern back to the specified properties of the -// object returned by resolvedOptions. -// -// ICU date-time skeletons and patterns aren't fully documented in the ICU -// documentation (see http://bugs.icu-project.org/trac/ticket/9627). The best -// documentation at this point is in UTR 35: -// http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns - - -/** - * Returns an ICU pattern string for the given locale and representing the - * specified options as closely as possible given available locale data. - */ -function toBestICUPattern(locale, options) { - // Create an ICU skeleton representing the specified options. See - // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table - var skeleton = ""; - switch (options.weekday) { - case "narrow": - skeleton += "EEEEE"; - break; - case "short": - skeleton += "E"; - break; - case "long": - skeleton += "EEEE"; - } - switch (options.era) { - case "narrow": - skeleton += "GGGGG"; - break; - case "short": - skeleton += "G"; - break; - case "long": - skeleton += "GGGG"; - break; - } - switch (options.year) { - case "2-digit": - skeleton += "yy"; - break; - case "numeric": - skeleton += "y"; - break; - } - switch (options.month) { - case "2-digit": - skeleton += "MM"; - break; - case "numeric": - skeleton += "M"; - break; - case "narrow": - skeleton += "MMMMM"; - break; - case "short": - skeleton += "MMM"; - break; - case "long": - skeleton += "MMMM"; - break; - } - switch (options.day) { - case "2-digit": - skeleton += "dd"; - break; - case "numeric": - skeleton += "d"; - break; - } - var hourSkeletonChar = "j"; - if (options.hour12 !== undefined) { - if (options.hour12) - hourSkeletonChar = "h"; - else - hourSkeletonChar = "H"; - } - switch (options.hour) { - case "2-digit": - skeleton += hourSkeletonChar + hourSkeletonChar; - break; - case "numeric": - skeleton += hourSkeletonChar; - break; - } - switch (options.minute) { - case "2-digit": - skeleton += "mm"; - break; - case "numeric": - skeleton += "m"; - break; - } - switch (options.second) { - case "2-digit": - skeleton += "ss"; - break; - case "numeric": - skeleton += "s"; - break; - } - switch (options.timeZoneName) { - case "short": - skeleton += "z"; - break; - case "long": - skeleton += "zzzz"; - break; - } - - // Let ICU convert the ICU skeleton to an ICU pattern for the given locale. - return intl_patternForSkeleton(locale, skeleton); -} - - -/** - * Returns a new options object that includes the provided options (if any) - * and fills in default components if required components are not defined. - * Required can be "date", "time", or "any". - * Defaults can be "date", "time", or "all". - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function ToDateTimeOptions(options, required, defaults) { - assert(typeof required === "string", "ToDateTimeOptions"); - assert(typeof defaults === "string", "ToDateTimeOptions"); - - // Steps 1-3. - if (options === undefined) - options = null; - else - options = ToObject(options); - options = std_Object_create(options); - - // Step 4. - var needDefaults = true; - - // Step 5. - if ((required === "date" || required === "any") && - (options.weekday !== undefined || options.year !== undefined || - options.month !== undefined || options.day !== undefined)) - { - needDefaults = false; - } - - // Step 6. - if ((required === "time" || required === "any") && - (options.hour !== undefined || options.minute !== undefined || - options.second !== undefined)) - { - needDefaults = false; - } - - // Step 7. - if (needDefaults && (defaults === "date" || defaults === "all")) { - // The specification says to call [[DefineOwnProperty]] with false for - // the Throw parameter, while Object.defineProperty uses true. For the - // calls here, the difference doesn't matter because we're adding - // properties to a new object. - _DefineDataProperty(options, "year", "numeric"); - _DefineDataProperty(options, "month", "numeric"); - _DefineDataProperty(options, "day", "numeric"); - } - - // Step 8. - if (needDefaults && (defaults === "time" || defaults === "all")) { - // See comment for step 7. - _DefineDataProperty(options, "hour", "numeric"); - _DefineDataProperty(options, "minute", "numeric"); - _DefineDataProperty(options, "second", "numeric"); - } - - // Step 9. - return options; -} - - -/** - * Compares the date and time components requested by options with the available - * date and time formats in formats, and selects the best match according - * to a specified basic matching algorithm. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function BasicFormatMatcher(options, formats) { - // Steps 1-6. - var removalPenalty = 120, - additionPenalty = 20, - longLessPenalty = 8, - longMorePenalty = 6, - shortLessPenalty = 6, - shortMorePenalty = 3; - - // Table 3. - var properties = ["weekday", "era", "year", "month", "day", - "hour", "minute", "second", "timeZoneName"]; - - // Step 11.c.vi.1. - var values = ["2-digit", "numeric", "narrow", "short", "long"]; - - // Steps 7-8. - var bestScore = -Infinity; - var bestFormat; - - // Steps 9-11. - var i = 0; - var len = formats.length; - while (i < len) { - // Steps 11.a-b. - var format = formats[i]; - var score = 0; - - // Step 11.c. - var formatProp; - for (var j = 0; j < properties.length; j++) { - var property = properties[j]; - - // Step 11.c.i. - var optionsProp = options[property]; - // Step missing from spec. - // https://bugs.ecmascript.org/show_bug.cgi?id=1254 - formatProp = undefined; - - // Steps 11.c.ii-iii. - if (callFunction(std_Object_hasOwnProperty, format, property)) - formatProp = format[property]; - - if (optionsProp === undefined && formatProp !== undefined) { - // Step 11.c.iv. - score -= additionPenalty; - } else if (optionsProp !== undefined && formatProp === undefined) { - // Step 11.c.v. - score -= removalPenalty; - } else { - // Step 11.c.vi. - var optionsPropIndex = callFunction(ArrayIndexOf, values, optionsProp); - var formatPropIndex = callFunction(ArrayIndexOf, values, formatProp); - var delta = std_Math_max(std_Math_min(formatPropIndex - optionsPropIndex, 2), -2); - if (delta === 2) - score -= longMorePenalty; - else if (delta === 1) - score -= shortMorePenalty; - else if (delta === -1) - score -= shortLessPenalty; - else if (delta === -2) - score -= longLessPenalty; - } - } - - // Step 11.d. - if (score > bestScore) { - bestScore = score; - bestFormat = format; - } - - // Step 11.e. - i++; - } - - // Step 12. - return bestFormat; -} - - -/** - * Compares the date and time components requested by options with the available - * date and time formats in formats, and selects the best match according - * to an unspecified best-fit matching algorithm. - * - * Spec: ECMAScript Internationalization API Specification, 12.1.1. - */ -function BestFitFormatMatcher(options, formats) { - // this implementation doesn't have anything better - return BasicFormatMatcher(options, formats); -} - - -/** - * 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 Internationalization API Specification, 12.2.2. - */ -function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - var availableLocales = callFunction(dateTimeFormatInternalProperties.availableLocales, - dateTimeFormatInternalProperties); - var requestedLocales = CanonicalizeLocaleList(locales); - return SupportedLocales(availableLocales, requestedLocales, options); -} - - -/** - * DateTimeFormat internal properties. - * - * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.2.3. - */ -var dateTimeFormatInternalProperties = { - localeData: dateTimeFormatLocaleData, - _availableLocales: null, - availableLocales: function() - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_DateTimeFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: ["ca", "nu"] -}; - - -function dateTimeFormatLocaleData(locale) { - return { - ca: intl_availableCalendars(locale), - nu: getNumberingSystems(locale) - }; -} - - -/** - * Function to be bound and returned by Intl.DateTimeFormat.prototype.format. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - */ -function dateTimeFormatFormatToBind() { - // Steps 1.a.i-ii - var date = arguments.length > 0 ? arguments[0] : undefined; - var x = (date === undefined) ? std_Date_now() : ToNumber(date); - - // Step 1.a.iii. - return intl_FormatDateTime(this, x, false); -} - -/** - * Returns a function bound to this DateTimeFormat that returns a String value - * representing the result of calling ToNumber(date) according to the - * effective locale and the formatting options of this DateTimeFormat. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.2. - */ -function Intl_DateTimeFormat_format_get() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - var internals = getDateTimeFormatInternals(this, "format"); - - // Step 1. - if (internals.boundFormat === undefined) { - // Step 1.a. - var F = dateTimeFormatFormatToBind; - - // Step 1.b-d. - var bf = callFunction(FunctionBind, F, this); - internals.boundFormat = bf; - } - - // Step 2. - return internals.boundFormat; -} - - -function Intl_DateTimeFormat_formatToParts() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - getDateTimeFormatInternals(this, "formatToParts"); - - // Steps 1.a.i-ii - var date = arguments.length > 0 ? arguments[0] : undefined; - var x = (date === undefined) ? std_Date_now() : ToNumber(date); - - // Step 1.a.iii. - return intl_FormatDateTime(this, x, true); -} - - -/** - * Returns the resolved options for a DateTimeFormat object. - * - * Spec: ECMAScript Internationalization API Specification, 12.3.3 and 12.4. - */ -function Intl_DateTimeFormat_resolvedOptions() { - // Check "this DateTimeFormat object" per introduction of section 12.3. - var internals = getDateTimeFormatInternals(this, "resolvedOptions"); - - var result = { - locale: internals.locale, - calendar: internals.calendar, - numberingSystem: internals.numberingSystem, - timeZone: internals.timeZone - }; - resolveICUPattern(internals.pattern, result); - return result; -} - - -// Table mapping ICU pattern characters back to the corresponding date-time -// components of DateTimeFormat. See -// http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table -var icuPatternCharToComponent = { - E: "weekday", - G: "era", - y: "year", - M: "month", - L: "month", - d: "day", - h: "hour", - H: "hour", - k: "hour", - K: "hour", - m: "minute", - s: "second", - z: "timeZoneName", - v: "timeZoneName", - V: "timeZoneName" -}; - - -/** - * Maps an ICU pattern string to a corresponding set of date-time components - * and their values, and adds properties for these components to the result - * object, which will be returned by the resolvedOptions method. For the - * interpretation of ICU pattern characters, see - * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table - */ -function resolveICUPattern(pattern, result) { - assert(IsObject(result), "resolveICUPattern"); - var i = 0; - while (i < pattern.length) { - var c = pattern[i++]; - if (c === "'") { - while (i < pattern.length && pattern[i] !== "'") - i++; - i++; - } else { - var count = 1; - while (i < pattern.length && pattern[i] === c) { - i++; - count++; - } - var value; - switch (c) { - // "text" cases - case "G": - case "E": - case "z": - case "v": - case "V": - if (count <= 3) - value = "short"; - else if (count === 4) - value = "long"; - else - value = "narrow"; - break; - // "number" cases - case "y": - case "d": - case "h": - case "H": - case "m": - case "s": - case "k": - case "K": - if (count === 2) - value = "2-digit"; - else - value = "numeric"; - break; - // "text & number" cases - case "M": - case "L": - if (count === 1) - value = "numeric"; - else if (count === 2) - value = "2-digit"; - else if (count === 3) - value = "short"; - else if (count === 4) - value = "long"; - else - value = "narrow"; - break; - default: - // skip other pattern characters and literal text - } - if (callFunction(std_Object_hasOwnProperty, icuPatternCharToComponent, c)) - _DefineDataProperty(result, icuPatternCharToComponent[c], value); - if (c === "h" || c === "K") - _DefineDataProperty(result, "hour12", true); - else if (c === "H" || c === "k") - _DefineDataProperty(result, "hour12", false); - } - } -} - -/********** 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; -} - - -/********** Intl.RelativeTimeFormat **********/ - -/** - * RelativeTimeFormat internal properties. - * - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.3. - */ -var relativeTimeFormatInternalProperties = { - localeData: relativeTimeFormatLocaleData, - _availableLocales: null, - availableLocales: function() // eslint-disable-line object-shorthand - { - var locales = this._availableLocales; - if (locales) - return locales; - - locales = intl_RelativeTimeFormat_availableLocales(); - addSpecialMissingLanguageTags(locales); - return (this._availableLocales = locales); - }, - relevantExtensionKeys: [], -}; - -function relativeTimeFormatLocaleData() { - // RelativeTimeFormat doesn't support any extension keys. - return {}; -} - -/** - * Compute an internal properties object from |lazyRelativeTimeFormatData|. - */ -function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { - assert(IsObject(lazyRelativeTimeFormatData), "lazy data not an object?"); - - var internalProps = std_Object_create(null); - - var RelativeTimeFormat = relativeTimeFormatInternalProperties; - - // Steps 7-8. - const r = ResolveLocale(callFunction(RelativeTimeFormat.availableLocales, RelativeTimeFormat), - lazyRelativeTimeFormatData.requestedLocales, - lazyRelativeTimeFormatData.opt, - RelativeTimeFormat.relevantExtensionKeys, - RelativeTimeFormat.localeData); - - // Step 9-10. - internalProps.locale = r.locale; - - // Step 11. - assert(r.locale === r.dataLocale, - "resolved locale matches the resolved data-locale when no extension-keys are present"); - - // Step 13. - internalProps.style = lazyRelativeTimeFormatData.style; - - // Step 15. - internalProps.numeric = lazyRelativeTimeFormatData.numeric; - - // Steps 16-20 (Not relevant in our implementation). - - return internalProps; -} - -/** - * Returns an object containing the RelativeTimeFormat internal properties of |obj|, - * or throws a TypeError if |obj| isn't RelativeTimeFormat-initialized. - */ -function getRelativeTimeFormatInternals(obj, methodName) { - var internals = getIntlObjectInternals(obj, "RelativeTimeFormat", methodName); - assert(internals.type === "RelativeTimeFormat", "bad type escaped getIntlObjectInternals"); - - var internalProps = maybeInternalProperties(internals); - if (internalProps) - return internalProps; - - internalProps = resolveRelativeTimeFormatInternals(internals.lazyData); - setInternalProperties(internals, internalProps); - return internalProps; -} - -/** - * Initializes an object as a RelativeTimeFormat. - * - * 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 RelativeTimeFormat. - * This later work occurs in |resolveRelativeTimeFormatInternals|; steps not noted - * here occur there. - * - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.1. - */ -function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) { - assert(IsObject(relativeTimeFormat), "InitializeRelativeTimeFormat"); - - if (isInitializedIntlObject(relativeTimeFormat)) - ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); - - let internals = initializeIntlObject(relativeTimeFormat); - - // Lazy RelativeTimeFormat data has the following structure: - // - // { - // requestedLocales: List of locales, - // style: "long" / "short" / "narrow", - // numeric: "always" / "auto", - // - // opt: // opt object computed in InitializeRelativeTimeFormat - // { - // localeMatcher: "lookup" / "best fit", - // } - // } - // - // Note that lazy data is only installed as a final step of initialization, - // so every RelativeTimeFormat lazy data object has *all* these properties, never a - // subset of them. - const lazyRelativeTimeFormatData = std_Object_create(null); - - // Step 1. - let requestedLocales = CanonicalizeLocaleList(locales); - lazyRelativeTimeFormatData.requestedLocales = requestedLocales; - - // Steps 2-3. - if (options === undefined) - options = std_Object_create(null); - else - options = ToObject(options); - - // Step 4. - let opt = new Record(); - - // Steps 5-6. - let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); - opt.localeMatcher = matcher; - - lazyRelativeTimeFormatData.opt = opt; - - // Steps 12-13. - const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); - lazyRelativeTimeFormatData.style = style; - - // Steps 14-15. - const numeric = GetOption(options, "numeric", "string", ["always", "auto"], "always"); - lazyRelativeTimeFormatData.numeric = numeric; - - setLazyData(internals, "RelativeTimeFormat", lazyRelativeTimeFormatData) -} - -/** - * 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, RelativeTimeFormat, 1.3.2. - */ -function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) { - var options = arguments.length > 1 ? arguments[1] : undefined; - - // Step 1. - var availableLocales = callFunction(relativeTimeFormatInternalProperties.availableLocales, - relativeTimeFormatInternalProperties); - // Step 2. - let requestedLocales = CanonicalizeLocaleList(locales); - - // Step 3. - return SupportedLocales(availableLocales, requestedLocales, options); -} - -/** - * Returns a String value representing the written form of a relative date - * formatted according to the effective locale and the formatting options - * of this RelativeTimeFormat object. - * - * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.3. - */ -function Intl_RelativeTimeFormat_format(value, unit) { - // Step 1. - let relativeTimeFormat = this; - - // Step 2. - let internals = getRelativeTimeFormatInternals(relativeTimeFormat, "format"); - - // Step 3. - let t = ToNumber(value); - - // Step 4. - let u = ToString(unit); - - // PartitionRelativeTimePattern, step 4. - if (!Number_isFinite(t)) { - ThrowRangeError(JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat"); - } - - // PartitionRelativeTimePattern, step 5. - switch (u) { - case "second": - case "seconds": - case "minute": - case "minutes": - case "hour": - case "hours": - case "day": - case "days": - case "week": - case "weeks": - case "month": - case "months": - case "quarter": - case "quarters": - case "year": - case "years": - break; - default: - ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, "unit", u); - } - - // Step 5. - return intl_FormatRelativeTime(relativeTimeFormat, t, u, internals.numeric); -} - -/** - * Returns the resolved options for a PluralRules object. - * - * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.4. - */ -function Intl_RelativeTimeFormat_resolvedOptions() { - var internals = getRelativeTimeFormatInternals(this, "resolvedOptions"); - - // Steps 4-5. - var result = { - locale: internals.locale, - style: internals.style, - numeric: internals.numeric, - }; - - // Step 6. - return result; -} - -function Intl_getCanonicalLocales(locales) { - let codes = CanonicalizeLocaleList(locales); - let result = []; - - let len = codes.length; - let k = 0; - - while (k < len) { - _DefineDataProperty(result, k, codes[k]); - k++; - } - return result; -} - -function Intl_getCalendarInfo(locales) { - const requestedLocales = CanonicalizeLocaleList(locales); - - const DateTimeFormat = dateTimeFormatInternalProperties; - const localeData = DateTimeFormat.localeData; - - const localeOpt = new Record(); - localeOpt.localeMatcher = "best fit"; - - const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), - requestedLocales, - localeOpt, - DateTimeFormat.relevantExtensionKeys, - localeData); - - const result = intl_GetCalendarInfo(r.locale); - result.calendar = r.ca; - result.locale = r.locale; - - return result; -} - -/** - * This function is a custom method designed after Intl API, but currently - * not part of the spec or spec proposal. - * We want to use it internally to retrieve translated values from CLDR in - * order to ensure they're aligned with what Intl API returns. - * - * This API may one day be a foundation for an ECMA402 API spec proposal. - * - * The function takes two arguments - locales which is a list of locale strings - * and options which is an object with two optional properties: - * - * keys: - * an Array of string values that are paths to individual terms - * - * style: - * a String with a value "long", "short" or "narrow" - * - * It returns an object with properties: - * - * locale: - * a negotiated locale string - * - * style: - * negotiated style - * - * values: - * A key-value pair list of requested keys and corresponding - * translated values - * - */ -function Intl_getDisplayNames(locales, options) { - // 1. Let requestLocales be ? CanonicalizeLocaleList(locales). - const requestedLocales = CanonicalizeLocaleList(locales); - - // 2. If options is undefined, then - if (options === undefined) - // a. Let options be ObjectCreate(%ObjectPrototype%). - options = {}; - // 3. Else, - else - // a. Let options be ? ToObject(options). - options = ToObject(options); - - const DateTimeFormat = dateTimeFormatInternalProperties; - - // 4. Let localeData be %DateTimeFormat%.[[localeData]]. - const localeData = DateTimeFormat.localeData; - - // 5. Let opt be a new Record. - const localeOpt = new Record(); - // 6. Set localeOpt.[[localeMatcher]] to "best fit". - localeOpt.localeMatcher = "best fit"; - - // 7. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], requestedLocales, localeOpt, - // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). - const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), - requestedLocales, - localeOpt, - DateTimeFormat.relevantExtensionKeys, - localeData); - - // 8. Let style be ? GetOption(options, "style", "string", « "long", "short", "narrow" », "long"). - const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); - // 9. Let keys be ? Get(options, "keys"). - let keys = options.keys; - - // 10. If keys is undefined, - if (keys === undefined) { - // a. Let keys be ArrayCreate(0). - keys = []; - } else if (!IsObject(keys)) { - // 11. Else, - // a. If Type(keys) is not Object, throw a TypeError exception. - ThrowTypeError(JSMSG_INVALID_KEYS_TYPE); - } - - // 12. Let processedKeys be ArrayCreate(0). - // (This really should be a List, but we use an Array here in order that - // |intl_ComputeDisplayNames| may infallibly access the list's length via - // |ArrayObject::length|.) - let processedKeys = []; - // 13. Let len be ? ToLength(? Get(keys, "length")). - let len = ToLength(keys.length); - // 14. Let i be 0. - // 15. Repeat, while i < len - for (let i = 0; i < len; i++) { - // a. Let processedKey be ? ToString(? Get(keys, i)). - // b. Perform ? CreateDataPropertyOrThrow(processedKeys, i, processedKey). - callFunction(std_Array_push, processedKeys, ToString(keys[i])); - } - - // 16. Let names be ? ComputeDisplayNames(r.[[locale]], style, processedKeys). - const names = intl_ComputeDisplayNames(r.locale, style, processedKeys); - - // 17. Let values be ObjectCreate(%ObjectPrototype%). - const values = {}; - - // 18. Set i to 0. - // 19. Repeat, while i < len - for (let i = 0; i < len; i++) { - // a. Let key be ? Get(processedKeys, i). - const key = processedKeys[i]; - // b. Let name be ? Get(names, i). - const name = names[i]; - // c. Assert: Type(name) is string. - assert(typeof name === "string", "unexpected non-string value"); - // d. Assert: the length of name is greater than zero. - assert(name.length > 0, "empty string value"); - // e. Perform ? DefinePropertyOrThrow(values, key, name). - _DefineDataProperty(values, key, name); - } - - // 20. Let options be ObjectCreate(%ObjectPrototype%). - // 21. Perform ! DefinePropertyOrThrow(result, "locale", r.[[locale]]). - // 22. Perform ! DefinePropertyOrThrow(result, "style", style). - // 23. Perform ! DefinePropertyOrThrow(result, "values", values). - const result = { locale: r.locale, style, values }; - - // 24. Return result. - return result; - -} diff --git a/js/src/builtin/SelfHostingDefines.h b/js/src/builtin/SelfHostingDefines.h index e233f5c397..5bd31f58ec 100644 --- a/js/src/builtin/SelfHostingDefines.h +++ b/js/src/builtin/SelfHostingDefines.h @@ -96,6 +96,8 @@ #define REGEXP_STRING_ITERATOR_FLAGS_SLOT 2 #define REGEXP_STRING_ITERATOR_DONE_SLOT 3 +#define INTL_INTERNALS_OBJECT_SLOT 0 + #define MODULE_OBJECT_ENVIRONMENT_SLOT 1 #define MODULE_OBJECT_STATUS_SLOT 3 #define MODULE_OBJECT_EVALUATION_ERROR_SLOT 4 diff --git a/js/src/builtin/intl/Collator.cpp b/js/src/builtin/intl/Collator.cpp new file mode 100644 index 0000000000..426daedc5a --- /dev/null +++ b/js/src/builtin/intl/Collator.cpp @@ -0,0 +1,530 @@ +/* -*- 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.Collator implementation. */ + +#include "builtin/intl/Collator.h" + +#include "mozilla/Assertions.h" + +#include "jsapi.h" +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "js/TypeDecls.h" +#include "vm/GlobalObject.h" +#include "vm/Runtime.h" +#include "vm/String.h" + +#include "jsobjinlines.h" + +using namespace js; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::ReportInternalError; +using js::intl::SharedIntlData; +using js::intl::StringsAreEqual; + +/******************** Collator ********************/ + +const ClassOps CollatorObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + CollatorObject::finalize +}; + +const Class CollatorObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(CollatorObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &CollatorObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +collator_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Collator); + return true; +} +#endif + +static const JSFunctionSpec collator_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_Collator_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec collator_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_Collator_resolvedOptions", 0, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, collator_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 10.1.2 Intl.Collator([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +Collator(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + // We're following ECMA-402 1st Edition when Collator is called because of + // backward compatibility issues. + // See https://github.com/tc39/ecma402/issues/57 + if (!construct) { + // ES Intl 1st ed., 10.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., 10.1.2.1 step 4 + obj = ToObject(cx, self); + if (!obj) + return false; + + // ES Intl 1st ed., 10.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., 10.1.2.1 step 3.a + construct = true; + } + } + if (construct) { + // Steps 2-5 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateCollatorPrototype(cx, cx->global()); + if (!proto) + return false; + } + + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(CollatorObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.length() > 0 ? args[0] : UndefinedValue()); + RootedValue options(cx, args.length() > 1 ? args[1] : UndefinedValue()); + + // Step 6. + if (!intl::InitializeObject(cx, obj, cx->names().InitializeCollator, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +Collator(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return Collator(cx, args, args.isConstructing()); +} + +bool +js::intl_Collator(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_Collator 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 Collator(cx, args, true); +} + +void +js::CollatorObject::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(CollatorObject::UCOLLATOR_SLOT); + if (!slot.isUndefined()) { + if (UCollator* coll = static_cast(slot.toPrivate())) + ucol_close(coll); + } +} + +JSObject* +js::CreateCollatorPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx, GlobalObject::createConstructor(cx, &Collator, cx->names().Collator, + 0)); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &CollatorObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 10.2.2 + if (!JS_DefineFunctions(cx, ctor, collator_static_methods)) + return nullptr; + + // 10.3.2 and 10.3.3 + if (!JS_DefineFunctions(cx, proto, collator_methods)) + return nullptr; + + /* + * Install the getter for Collator.prototype.compare, which returns a bound + * comparison function for the specified Collator object (suitable for + * passing to methods like Array.prototype.sort). + */ + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().CollatorCompareGet, &getter)) + return nullptr; + if (!DefineProperty(cx, proto, cx->names().compare, 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; + + // 10.2.1 and 10.3 + if (!intl::InitializeObject(cx, proto, cx->names().InitializeCollator, UndefinedHandleValue, options)) + return nullptr; + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().Collator, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + if (!GetAvailableLocales(cx, ucol_countAvailable, ucol_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +bool +js::intl_availableCollations(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; + UEnumeration* values = ucol_getKeywordValuesForLocale("co", locale.ptr(), false, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + uint32_t count = uenum_count(values, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + RootedObject collations(cx, NewDenseEmptyArray(cx)); + if (!collations) + return false; + + uint32_t index = 0; + for (uint32_t i = 0; i < count; i++) { + const char* collation = uenum_next(values, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + // Per ECMA-402, 10.2.3, we don't include standard and search: + // "The values 'standard' and 'search' must not be used as elements in + // any [[sortLocaleData]][locale].co and [[searchLocaleData]][locale].co + // array." + if (StringsAreEqual(collation, "standard") || StringsAreEqual(collation, "search")) + continue; + + // ICU returns old-style keyword values; map them to BCP 47 equivalents + // (see http://bugs.icu-project.org/trac/ticket/9620). + if (StringsAreEqual(collation, "dictionary")) + collation = "dict"; + else if (StringsAreEqual(collation, "gb2312han")) + collation = "gb2312"; + else if (StringsAreEqual(collation, "phonebook")) + collation = "phonebk"; + else if (StringsAreEqual(collation, "traditional")) + collation = "trad"; + + RootedString jscollation(cx, JS_NewStringCopyZ(cx, collation)); + if (!jscollation) + return false; + RootedValue element(cx, StringValue(jscollation)); + if (!DefineElement(cx, collations, index++, element)) + return false; + } + + args.rval().setObject(*collations); + return true; +} + +/** + * Returns a new UCollator with the locale and collation options + * of the given Collator. + */ +static UCollator* +NewUCollator(JSContext* cx, HandleObject collator) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, collator)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // UCollator options with default values. + UColAttributeValue uStrength = UCOL_DEFAULT; + UColAttributeValue uCaseLevel = UCOL_OFF; + UColAttributeValue uAlternate = UCOL_DEFAULT; + UColAttributeValue uNumeric = UCOL_OFF; + // Normalization is always on to meet the canonical equivalence requirement. + UColAttributeValue uNormalization = UCOL_ON; + UColAttributeValue uCaseFirst = UCOL_DEFAULT; + + if (!GetProperty(cx, internals, internals, cx->names().usage, &value)) + return nullptr; + JSAutoByteString usage(cx, value.toString()); + if (!usage) + return nullptr; + if (StringsAreEqual(usage, "search")) { + // ICU expects search as a Unicode locale extension on locale. + // Unicode locale extensions must occur before private use extensions. + const char* oldLocale = locale.ptr(); + const char* p; + size_t index; + size_t localeLen = strlen(oldLocale); + if ((p = strstr(oldLocale, "-x-"))) + index = p - oldLocale; + else + index = localeLen; + + const char* insert; + if ((p = strstr(oldLocale, "-u-")) && static_cast(p - oldLocale) < index) { + index = p - oldLocale + 2; + insert = "-co-search"; + } else { + insert = "-u-co-search"; + } + size_t insertLen = strlen(insert); + char* newLocale = cx->pod_malloc(localeLen + insertLen + 1); + if (!newLocale) + return nullptr; + memcpy(newLocale, oldLocale, index); + memcpy(newLocale + index, insert, insertLen); + memcpy(newLocale + index + insertLen, oldLocale + index, localeLen - index + 1); // '\0' + locale.clear(); + locale.initBytes(newLocale); + } + + // We don't need to look at the collation property - it can only be set + // via the Unicode locale extension and is therefore already set on + // locale. + + if (!GetProperty(cx, internals, internals, cx->names().sensitivity, &value)) + return nullptr; + JSAutoByteString sensitivity(cx, value.toString()); + if (!sensitivity) + return nullptr; + if (StringsAreEqual(sensitivity, "base")) { + uStrength = UCOL_PRIMARY; + } else if (StringsAreEqual(sensitivity, "accent")) { + uStrength = UCOL_SECONDARY; + } else if (StringsAreEqual(sensitivity, "case")) { + uStrength = UCOL_PRIMARY; + uCaseLevel = UCOL_ON; + } else { + MOZ_ASSERT(StringsAreEqual(sensitivity, "variant")); + uStrength = UCOL_TERTIARY; + } + + if (!GetProperty(cx, internals, internals, cx->names().ignorePunctuation, &value)) + return nullptr; + // According to the ICU team, UCOL_SHIFTED causes punctuation to be + // ignored. Looking at Unicode Technical Report 35, Unicode Locale Data + // Markup Language, "shifted" causes whitespace and punctuation to be + // ignored - that's a bit more than asked for, but there's no way to get + // less. + if (value.toBoolean()) + uAlternate = UCOL_SHIFTED; + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) + return nullptr; + if (!value.isUndefined() && value.toBoolean()) + uNumeric = UCOL_ON; + + if (!GetProperty(cx, internals, internals, cx->names().caseFirst, &value)) + return nullptr; + if (!value.isUndefined()) { + JSAutoByteString caseFirst(cx, value.toString()); + if (!caseFirst) + return nullptr; + if (StringsAreEqual(caseFirst, "upper")) + uCaseFirst = UCOL_UPPER_FIRST; + else if (StringsAreEqual(caseFirst, "lower")) + uCaseFirst = UCOL_LOWER_FIRST; + else + MOZ_ASSERT(StringsAreEqual(caseFirst, "false")); + } + + UErrorCode status = U_ZERO_ERROR; + UCollator* coll = ucol_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + ucol_setAttribute(coll, UCOL_STRENGTH, uStrength, &status); + ucol_setAttribute(coll, UCOL_CASE_LEVEL, uCaseLevel, &status); + ucol_setAttribute(coll, UCOL_ALTERNATE_HANDLING, uAlternate, &status); + ucol_setAttribute(coll, UCOL_NUMERIC_COLLATION, uNumeric, &status); + ucol_setAttribute(coll, UCOL_NORMALIZATION_MODE, uNormalization, &status); + ucol_setAttribute(coll, UCOL_CASE_FIRST, uCaseFirst, &status); + if (U_FAILURE(status)) { + ucol_close(coll); + intl::ReportInternalError(cx); + return nullptr; + } + + return coll; +} + +static bool +intl_CompareStrings(JSContext* cx, UCollator* coll, HandleString str1, HandleString str2, + MutableHandleValue result) +{ + MOZ_ASSERT(str1); + MOZ_ASSERT(str2); + + if (str1 == str2) { + result.setInt32(0); + return true; + } + + AutoStableStringChars stableChars1(cx); + if (!stableChars1.initTwoByte(cx, str1)) + return false; + + AutoStableStringChars stableChars2(cx); + if (!stableChars2.initTwoByte(cx, str2)) + return false; + + mozilla::Range chars1 = stableChars1.twoByteRange(); + mozilla::Range chars2 = stableChars2.twoByteRange(); + + UCollationResult uresult = ucol_strcoll(coll, + Char16ToUChar(chars1.begin().get()), chars1.length(), + Char16ToUChar(chars2.begin().get()), chars2.length()); + int32_t res; + switch (uresult) { + case UCOL_LESS: res = -1; break; + case UCOL_EQUAL: res = 0; break; + case UCOL_GREATER: res = 1; break; + default: MOZ_CRASH("ucol_strcoll returned bad UCollationResult"); + } + result.setInt32(res); + return true; +} + +bool +js::intl_CompareStrings(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].isString()); + MOZ_ASSERT(args[2].isString()); + + Rooted collator(cx, &args[0].toObject().as()); + + // Obtain a UCollator object, cached if possible. + // XXX Does this handle Collator instances from other globals correctly? + bool isCollatorInstance = collator->getClass() == &CollatorObject::class_; + UCollator* coll; + if (isCollatorInstance) { + void* priv = collator->getReservedSlot(CollatorObject::UCOLLATOR_SLOT).toPrivate(); + coll = static_cast(priv); + if (!coll) { + coll = NewUCollator(cx, collator); + if (!coll) + return false; + collator->setReservedSlot(CollatorObject::UCOLLATOR_SLOT, PrivateValue(coll)); + } + } else { + // There's no good place to cache the ICU collator for an object + // that has been initialized as a Collator but is not a Collator + // instance. One possibility might be to add a Collator instance as an + // internal property to each such object. + coll = NewUCollator(cx, collator); + if (!coll) + return false; + } + + // Use the UCollator to actually compare the strings. + RootedString str1(cx, args[1].toString()); + RootedString str2(cx, args[2].toString()); + RootedValue result(cx); + bool success = intl_CompareStrings(cx, coll, str1, str2, &result); + + if (!isCollatorInstance) + ucol_close(coll); + if (!success) + return false; + args.rval().set(result); + return true; +} diff --git a/js/src/builtin/intl/Collator.h b/js/src/builtin/intl/Collator.h new file mode 100644 index 0000000000..c748a7f724 --- /dev/null +++ b/js/src/builtin/intl/Collator.h @@ -0,0 +1,94 @@ +/* -*- 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_Collator_h +#define builtin_intl_Collator_h + +#include "mozilla/Attributes.h" + +#include + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; +class GlobalObject; + +/******************** Collator ********************/ + +class CollatorObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UCOLLATOR_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* +CreateCollatorPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns a new instance of the standard built-in Collator 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: collator = intl_Collator(locales, options) + */ +extern MOZ_MUST_USE bool +intl_Collator(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for collation + * 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_Collator_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_Collator_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an array with the collation type identifiers per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * collations supported for the given locale. "standard" and "search" are + * excluded. + * + * Usage: collations = intl_availableCollations(locale) + */ +extern MOZ_MUST_USE bool +intl_availableCollations(JSContext* cx, unsigned argc, Value* vp); + +/** + * Compares x and y (which must be String values), and returns a number less + * than 0 if x < y, 0 if x = y, or a number greater than 0 if x > y according + * to the sort order for the locale and collation options of the given + * Collator. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.2. + * + * Usage: result = intl_CompareStrings(collator, x, y) + */ +extern MOZ_MUST_USE bool +intl_CompareStrings(JSContext* cx, unsigned argc, Value* vp); + + +} // namespace js + +#endif /* builtin_intl_Collator_h */ \ No newline at end of file diff --git a/js/src/builtin/intl/Collator.js b/js/src/builtin/intl/Collator.js new file mode 100644 index 0000000000..5fb85f8e6a --- /dev/null +++ b/js/src/builtin/intl/Collator.js @@ -0,0 +1,355 @@ +/* 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.Collator **********/ + + +/** + * Mapping from Unicode extension keys for collation to options properties, + * their types and permissible values. + * + * Spec: ECMAScript Internationalization API Specification, 10.1.1. + */ +var collatorKeyMappings = { + kn: {property: "numeric", type: "boolean"}, + kf: {property: "caseFirst", type: "string", values: ["upper", "lower", "false"]} +}; + + +/** + * Compute an internal properties object from |lazyCollatorData|. + */ +function resolveCollatorInternals(lazyCollatorData) +{ + assert(IsObject(lazyCollatorData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + // Step 7. + internalProps.usage = lazyCollatorData.usage; + + // Step 8. + var Collator = collatorInternalProperties; + + // Step 9. + var collatorIsSorting = lazyCollatorData.usage === "sort"; + var localeData = collatorIsSorting + ? Collator.sortLocaleData + : Collator.searchLocaleData; + + // Compute effective locale. + // Step 14. + var relevantExtensionKeys = Collator.relevantExtensionKeys; + + // Step 15. + var r = ResolveLocale(callFunction(Collator.availableLocales, Collator), + lazyCollatorData.requestedLocales, + lazyCollatorData.opt, + relevantExtensionKeys, + localeData); + + // Step 16. + internalProps.locale = r.locale; + + // Steps 17-19. + var key, property, value, mapping; + var i = 0, len = relevantExtensionKeys.length; + while (i < len) { + // Step 19.a. + key = relevantExtensionKeys[i]; + if (key === "co") { + // Step 19.b. + property = "collation"; + value = r.co === null ? "default" : r.co; + } else { + // Step 19.c. + mapping = collatorKeyMappings[key]; + property = mapping.property; + value = r[key]; + if (mapping.type === "boolean") + value = value === "true"; + } + + // Step 19.d. + internalProps[property] = value; + + // Step 19.e. + i++; + } + + // Compute remaining collation options. + // Steps 21-22. + var s = lazyCollatorData.rawSensitivity; + if (s === undefined) { + if (collatorIsSorting) { + // Step 21.a. + s = "variant"; + } else { + // Step 21.b. + var dataLocale = r.dataLocale; + var dataLocaleData = localeData(dataLocale); + s = dataLocaleData.sensitivity; + } + } + internalProps.sensitivity = s; + + // Step 24. + internalProps.ignorePunctuation = lazyCollatorData.ignorePunctuation; + + // Step 25. + internalProps.boundFormat = undefined; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + + +/** + * Returns an object containing the Collator internal properties of |obj|, or + * throws a TypeError if |obj| isn't Collator-initialized. + */ +function getCollatorInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "Collator", methodName); + assert(internals.type === "Collator", "bad type escaped getIntlObjectInternals"); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + // Otherwise it's time to fully create them. + internalProps = resolveCollatorInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + + +/** + * Initializes an object as a Collator. + * + * 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 Collator. This + * later work occurs in |resolveCollatorInternals|; steps not noted here occur + * there. + * + * Spec: ECMAScript Internationalization API Specification, 10.1.1. + */ +function InitializeCollator(collator, locales, options) { + assert(IsObject(collator), "InitializeCollator"); + + // Step 1. + if (isInitializedIntlObject(collator)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + // Step 2. + var internals = initializeIntlObject(collator); + + // Lazy Collator data has the following structure: + // + // { + // requestedLocales: List of locales, + // usage: "sort" / "search", + // opt: // opt object computed in InitializeCollator + // { + // localeMatcher: "lookup" / "best fit", + // kn: true / false / undefined, + // kf: "upper" / "lower" / "false" / undefined + // } + // rawSensitivity: "base" / "accent" / "case" / "variant" / undefined, + // ignorePunctuation: true / false + // } + // + // 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 + // subset of them. + var lazyCollatorData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyCollatorData.requestedLocales = requestedLocales; + + // Steps 4-5. + // + // If we ever need more speed here at startup, we should try to detect the + // case where |options === undefined| and Object.prototype hasn't been + // mucked with. (|options| is fully consumed in this method, so it's not a + // concern that Object.prototype might be touched between now and when + // |resolveCollatorInternals| is called.) For now, just keep it simple. + if (options === undefined) + options = {}; + else + options = ToObject(options); + + // Compute options that impact interpretation of locale. + // Step 6. + var u = GetOption(options, "usage", "string", ["sort", "search"], "sort"); + lazyCollatorData.usage = u; + + // Step 10. + var opt = new Record(); + lazyCollatorData.opt = opt; + + // Steps 11-12. + var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + // Step 13, unrolled. + var numericValue = GetOption(options, "numeric", "boolean", undefined, undefined); + if (numericValue !== undefined) + numericValue = numericValue ? 'true' : 'false'; + opt.kn = numericValue; + + var caseFirstValue = GetOption(options, "caseFirst", "string", ["upper", "lower", "false"], undefined); + opt.kf = caseFirstValue; + + // Compute remaining collation options. + // Step 20. + var s = GetOption(options, "sensitivity", "string", + ["base", "accent", "case", "variant"], undefined); + lazyCollatorData.rawSensitivity = s; + + // Step 23. + var ip = GetOption(options, "ignorePunctuation", "boolean", undefined, false); + lazyCollatorData.ignorePunctuation = ip; + + // Step 26. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + setLazyData(internals, "Collator", lazyCollatorData); +} + + +/** + * 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 Internationalization API Specification, 10.2.2. + */ +function Intl_Collator_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + var availableLocales = callFunction(collatorInternalProperties.availableLocales, + collatorInternalProperties); + var requestedLocales = CanonicalizeLocaleList(locales); + return SupportedLocales(availableLocales, requestedLocales, options); +} + + +/** + * Collator internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 10.2.3. + */ +var collatorInternalProperties = { + sortLocaleData: collatorSortLocaleData, + searchLocaleData: collatorSearchLocaleData, + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_Collator_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: ["co", "kn"] +}; + + +function collatorSortLocaleData(locale) { + var collations = intl_availableCollations(locale); + callFunction(std_Array_unshift, collations, null); + return { + co: collations, + kn: ["false", "true"] + }; +} + + +function collatorSearchLocaleData(locale) { + return { + co: [null], + kn: ["false", "true"], + // In theory the default sensitivity is locale dependent; + // in reality the CLDR/ICU default strength is always tertiary. + sensitivity: "variant" + }; +} + + +/** + * Function to be bound and returned by Intl.Collator.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + */ +function collatorCompareToBind(x, y) { + // Steps 1.a.i-ii implemented by ECMAScript declaration binding instantiation, + // ES5.1 10.5, step 4.d.ii. + + // Step 1.a.iii-v. + var X = ToString(x); + var Y = ToString(y); + return intl_CompareStrings(this, X, Y); +} + + +/** + * Returns a function bound to this Collator that compares x (converted to a + * String value) and y (converted to a String value), + * and returns a number less than 0 if x < y, 0 if x = y, or a number greater + * than 0 if x > y according to the sort order for the locale and collation + * options of this Collator object. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.2. + */ +function Intl_Collator_compare_get() { + // Check "this Collator object" per introduction of section 10.3. + var internals = getCollatorInternals(this, "compare"); + + // Step 1. + if (internals.boundCompare === undefined) { + // Step 1.a. + var F = collatorCompareToBind; + + // Step 1.b-d. + var bc = callFunction(FunctionBind, F, this); + internals.boundCompare = bc; + } + + // Step 2. + return internals.boundCompare; +} + + +/** + * Returns the resolved options for a Collator object. + * + * Spec: ECMAScript Internationalization API Specification, 10.3.3 and 10.4. + */ +function Intl_Collator_resolvedOptions() { + // Check "this Collator object" per introduction of section 10.3. + var internals = getCollatorInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + usage: internals.usage, + sensitivity: internals.sensitivity, + ignorePunctuation: internals.ignorePunctuation + }; + + var relevantExtensionKeys = collatorInternalProperties.relevantExtensionKeys; + for (var i = 0; i < relevantExtensionKeys.length; i++) { + var key = relevantExtensionKeys[i]; + var property = (key === "co") ? "collation" : collatorKeyMappings[key].property; + _DefineDataProperty(result, property, internals[property]); + } + return result; +} + diff --git a/js/src/builtin/intl/CommonFunctions.cpp b/js/src/builtin/intl/CommonFunctions.cpp new file mode 100644 index 0000000000..c211ff42ed --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.cpp @@ -0,0 +1,116 @@ +/* -*- 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/. */ + +/* Operations used to implement multiple Intl.* classes. */ + +#include "builtin/intl/CommonFunctions.h" + +#include "mozilla/Assertions.h" + +#include "jscntxt.h" +#include "jsfriendapi.h" // for GetErrorMessage, JSMSG_INTERNAL_INTL_ERROR +#include "jsobj.h" + +#include "js/Value.h" +#include "vm/SelfHosting.h" +#include "vm/Stack.h" + +#include "jsobjinlines.h" + +bool +js::intl::CreateDefaultOptions(JSContext* cx, MutableHandleValue defaultOptions) +{ + RootedObject options(cx, NewObjectWithGivenProto(cx, nullptr)); + if (!options) + return false; + defaultOptions.setObject(*options); + return true; +} + +bool +js::intl::InitializeObject(JSContext* cx, HandleObject obj, Handle initializer, + HandleValue locales, HandleValue options) +{ + RootedValue initializerValue(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), initializer, &initializerValue)) + return false; + MOZ_ASSERT(initializerValue.isObject()); + MOZ_ASSERT(initializerValue.toObject().is()); + + FixedInvokeArgs<3> args(cx); + + args[0].setObject(*obj); + args[1].set(locales); + args[2].set(options); + + RootedValue thisv(cx, NullValue()); + RootedValue ignored(cx); + return js::Call(cx, initializerValue, thisv, args, &ignored); +} + +/** + * Returns the object holding the internal properties for obj. + */ +JSObject* +js::intl::GetInternalsObject(JSContext* cx, HandleObject obj) +{ + RootedValue getInternalsValue(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().getInternals, + &getInternalsValue)) + { + return nullptr; + } + MOZ_ASSERT(getInternalsValue.isObject()); + MOZ_ASSERT(getInternalsValue.toObject().is()); + + FixedInvokeArgs<1> args(cx); + + args[0].setObject(*obj); + + RootedValue v(cx, NullValue()); + if (!js::Call(cx, getInternalsValue, v, args, &v)) + return nullptr; + + return &v.toObject(); +} + +void +js::intl::ReportInternalError(JSContext* cx) +{ + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INTERNAL_INTL_ERROR); +} + +bool +js::intl::GetAvailableLocales(JSContext* cx, CountAvailable countAvailable, + GetAvailable getAvailable, MutableHandleValue result) +{ + RootedObject locales(cx, NewObjectWithGivenProto(cx, nullptr)); + if (!locales) + return false; + + uint32_t count = countAvailable(); + RootedValue t(cx, BooleanValue(true)); + for (uint32_t i = 0; i < count; i++) { + const char* locale = getAvailable(i); + auto lang = DuplicateString(cx, locale); + if (!lang) + return false; + char* p; + while ((p = strchr(lang.get(), '_'))) + *p = '-'; + RootedAtom a(cx, Atomize(cx, lang.get(), strlen(lang.get()))); + if (!a) + return false; + if (!DefineProperty(cx, locales, a->asPropertyName(), t, nullptr, nullptr, + JSPROP_ENUMERATE)) + { + return false; + } + } + + result.setObject(*locales); + return true; +} diff --git a/js/src/builtin/intl/CommonFunctions.h b/js/src/builtin/intl/CommonFunctions.h new file mode 100644 index 0000000000..aae221b011 --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.h @@ -0,0 +1,146 @@ +/* -*- 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_CommonFunctions_h +#define builtin_intl_CommonFunctions_h + +#include "mozilla/Assertions.h" +#include "mozilla/TypeTraits.h" + +#include +#include +#include + +#include "builtin/intl/ICUHeader.h" +#include "js/RootingAPI.h" +#include "js/Vector.h" +#include "vm/String.h" + +namespace JS { class Value; } + +class JSObject; + +namespace js { + +namespace intl { + +/** + * Setup the |options| argument of |IntlInitialize| + */ +extern bool +CreateDefaultOptions(JSContext* cx, MutableHandleValue defaultOptions); + +/** + * Initialize a new Intl.* object using the named self-hosted function. + */ +extern bool +InitializeObject(JSContext* cx, HandleObject obj, Handle initializer, + HandleValue locales, HandleValue options); + +/** + * Returns the object holding the internal properties for obj. + */ +extern JSObject* +GetInternalsObject(JSContext* cx, JS::Handle obj); + +/** Report an Intl internal error not directly tied to a spec step. */ +extern void +ReportInternalError(JSContext* cx); + +static inline bool +StringsAreEqual(const char* s1, const char* s2) +{ + return !strcmp(s1, s2); +} + +static inline bool +StringsAreEqual(JSAutoByteString& s1, const char* s2) +{ + return !strcmp(s1.ptr(), s2); +} + +static inline const char* +IcuLocale(const char* locale) +{ + if (StringsAreEqual(locale, "und")) + return ""; // ICU root locale + + return locale; +} + +// Starting with ICU 59, UChar defaults to char16_t. +static_assert(mozilla::IsSame::value, + "SpiderMonkey doesn't support redefining UChar to a different type"); + +// The inline capacity we use for a Vector. Use this to ensure that +// our uses of ICU string functions, below and elsewhere, will try to fill the +// buffer's entire inline capacity before growing it and heap-allocating. +static const size_t INITIAL_CHAR_BUFFER_SIZE = 32; + +template +static int32_t +CallICU(JSContext* cx, Vector& chars, const ICUStringFunction& strFn) +{ + MOZ_ASSERT(chars.length() == 0); + MOZ_ALWAYS_TRUE(chars.resize(InlineCapacity)); + + UErrorCode status = U_ZERO_ERROR; + int32_t size = strFn(chars.begin(), InlineCapacity, &status); + if (status == U_BUFFER_OVERFLOW_ERROR) { + MOZ_ASSERT(size >= 0); + if (!chars.resize(size_t(size))) + return -1; + status = U_ZERO_ERROR; + strFn(chars.begin(), size, &status); + } + if (U_FAILURE(status)) { + ReportInternalError(cx); + return -1; + } + + MOZ_ASSERT(size >= 0); + if (!chars.resize(size_t(size))) + return -1; + return size; +} + +template +static JSString* +CallICU(JSContext* cx, const ICUStringFunction& strFn) +{ + Vector chars(cx); + + int32_t size = CallICU(cx, chars, strFn); + if (size < 0) + return nullptr; + + return NewStringCopyN(cx, chars.begin(), size_t(size)); +} + +// CountAvailable and GetAvailable describe the signatures used for ICU API +// to determine available locales for various functionality. +using CountAvailable = int32_t (*)(); +using GetAvailable = const char* (*)(int32_t localeIndex); + +/** + * Return an object whose own property names are the locales indicated as + * available by |countAvailable| that provides an overall count, and by + * |getAvailable| that when called passing a number less than that count, + * returns the corresponding locale as a borrowed string. For example: + * + * RootedValue v(cx); + * if (!GetAvailableLocales(cx, unum_countAvailable, unum_getAvailable, &v)) + * return false; + */ +extern bool +GetAvailableLocales(JSContext* cx, CountAvailable countAvailable, GetAvailable getAvailable, + JS::MutableHandle result); + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_CommonFunctions_h */ \ No newline at end of file diff --git a/js/src/builtin/intl/CommonFunctions.js b/js/src/builtin/intl/CommonFunctions.js new file mode 100644 index 0000000000..48337e666a --- /dev/null +++ b/js/src/builtin/intl/CommonFunctions.js @@ -0,0 +1,1245 @@ +/* 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/. */ + +/* Portions Copyright Norbert Lindenberg 2011-2012. */ + + +/** + * Holder object for encapsulating regexp instances. + * + * Regular expression instances should be created after the initialization of + * self-hosted global. + */ +var internalIntlRegExps = std_Object_create(null); +internalIntlRegExps.unicodeLocaleExtensionSequenceRE = null; +internalIntlRegExps.languageTagRE = null; +internalIntlRegExps.duplicateVariantRE = null; +internalIntlRegExps.duplicateSingletonRE = null; +internalIntlRegExps.isWellFormedCurrencyCodeRE = null; +internalIntlRegExps.currencyDigitsRE = null; + +/** + * Regular expression matching a "Unicode locale extension sequence", which the + * specification defines as: "any substring of a language tag that starts with + * a separator '-' and the singleton 'u' and includes the maximum sequence of + * following non-singleton subtags and their preceding '-' separators." + * + * Alternatively, this may be defined as: the components of a language tag that + * match the extension production in RFC 5646, where the singleton component is + * "u". + * + * Spec: ECMAScript Internationalization API Specification, 6.2.1. + */ +function getUnicodeLocaleExtensionSequenceRE() { + return internalIntlRegExps.unicodeLocaleExtensionSequenceRE || + (internalIntlRegExps.unicodeLocaleExtensionSequenceRE = + RegExpCreate("-u(?:-[a-z0-9]{2,8})+")); +} + + +/** + * Removes Unicode locale extension sequences from the given language tag. + */ +function removeUnicodeExtensions(locale) { + // A wholly-privateuse locale has no extension sequences. + if (callFunction(std_String_startsWith, locale, "x-")) + return locale; + + // Otherwise, split on "-x-" marking the start of any privateuse component. + // Replace Unicode locale extension sequences in the left half, and return + // the concatenation. + var pos = callFunction(std_String_indexOf, locale, "-x-"); + if (pos < 0) + pos = locale.length; + + var left = callFunction(String_substring, locale, 0, pos); + var right = callFunction(String_substring, locale, pos); + + var extensions; + var unicodeLocaleExtensionSequenceRE = getUnicodeLocaleExtensionSequenceRE(); + while ((extensions = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, left)) !== null) { + left = StringReplaceString(left, extensions[0], ""); + unicodeLocaleExtensionSequenceRE.lastIndex = 0; + } + + var combined = left + right; + assert(IsStructurallyValidLanguageTag(combined), "recombination produced an invalid language tag"); + assert(function() { + var uindex = callFunction(std_String_indexOf, combined, "-u-"); + if (uindex < 0) + return true; + var xindex = callFunction(std_String_indexOf, combined, "-x-"); + return xindex > 0 && xindex < uindex; + }(), "recombination failed to remove all Unicode locale extension sequences"); + + return combined; +} + + +/** + * Regular expression defining BCP 47 language tags. + * + * Spec: RFC 5646 section 2.1. + */ +function getLanguageTagRE() { + if (internalIntlRegExps.languageTagRE) + return internalIntlRegExps.languageTagRE; + + // RFC 5234 section B.1 + // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + var ALPHA = "[a-zA-Z]"; + // DIGIT = %x30-39 + // ; 0-9 + var DIGIT = "[0-9]"; + + // RFC 5646 section 2.1 + // alphanum = (ALPHA / DIGIT) ; letters and numbers + var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; + // regular = "art-lojban" ; these tags match the 'langtag' + // / "cel-gaulish" ; production, but their subtags + // / "no-bok" ; are not extended language + // / "no-nyn" ; or variant subtags: their meaning + // / "zh-guoyu" ; is defined by their registration + // / "zh-hakka" ; and all of these are deprecated + // / "zh-min" ; in favor of a more modern + // / "zh-min-nan" ; subtag or sequence of subtags + // / "zh-xiang" + var regular = "(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)"; + // irregular = "en-GB-oed" ; irregular tags do not match + // / "i-ami" ; the 'langtag' production and + // / "i-bnn" ; would not otherwise be + // / "i-default" ; considered 'well-formed' + // / "i-enochian" ; These tags are all valid, + // / "i-hak" ; but most are deprecated + // / "i-klingon" ; in favor of more modern + // / "i-lux" ; subtags or subtag + // / "i-mingo" ; combination + // / "i-navajo" + // / "i-pwn" + // / "i-tao" + // / "i-tay" + // / "i-tsu" + // / "sgn-BE-FR" + // / "sgn-BE-NL" + // / "sgn-CH-DE" + var irregular = "(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)"; + // grandfathered = irregular ; non-redundant tags registered + // / regular ; during the RFC 3066 era + var grandfathered = "(?:" + irregular + "|" + regular + ")"; + // privateuse = "x" 1*("-" (1*8alphanum)) + var privateuse = "(?:x(?:-[a-z0-9]{1,8})+)"; + // singleton = DIGIT ; 0 - 9 + // / %x41-57 ; A - W + // / %x59-5A ; Y - Z + // / %x61-77 ; a - w + // / %x79-7A ; y - z + var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])"; + // extension = singleton 1*("-" (2*8alphanum)) + var extension = "(?:" + singleton + "(?:-" + alphanum + "{2,8})+)"; + // variant = 5*8alphanum ; registered variants + // / (DIGIT 3alphanum) + var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))"; + // region = 2ALPHA ; ISO 3166-1 code + // / 3DIGIT ; UN M.49 code + var region = "(?:" + ALPHA + "{2}|" + DIGIT + "{3})"; + // script = 4ALPHA ; ISO 15924 code + var script = "(?:" + ALPHA + "{4})"; + // extlang = 3ALPHA ; selected ISO 639 codes + // *2("-" 3ALPHA) ; permanently reserved + var extlang = "(?:" + ALPHA + "{3}(?:-" + ALPHA + "{3}){0,2})"; + // language = 2*3ALPHA ; shortest ISO 639 code + // ["-" extlang] ; sometimes followed by + // ; extended language subtags + // / 4ALPHA ; or reserved for future use + // / 5*8ALPHA ; or registered language subtag + var language = "(?:" + ALPHA + "{2,3}(?:-" + extlang + ")?|" + ALPHA + "{4}|" + ALPHA + "{5,8})"; + // langtag = language + // ["-" script] + // ["-" region] + // *("-" variant) + // *("-" extension) + // ["-" privateuse] + var langtag = language + "(?:-" + script + ")?(?:-" + region + ")?(?:-" + + variant + ")*(?:-" + extension + ")*(?:-" + privateuse + ")?"; + // Language-Tag = langtag ; normal language tags + // / privateuse ; private use tag + // / grandfathered ; grandfathered tags + var languageTag = "^(?:" + langtag + "|" + privateuse + "|" + grandfathered + ")$"; + + // Language tags are case insensitive (RFC 5646 section 2.1.1). + return (internalIntlRegExps.languageTagRE = RegExpCreate(languageTag, "i")); +} + + +function getDuplicateVariantRE() { + if (internalIntlRegExps.duplicateVariantRE) + return internalIntlRegExps.duplicateVariantRE; + + // RFC 5234 section B.1 + // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + var ALPHA = "[a-zA-Z]"; + // DIGIT = %x30-39 + // ; 0-9 + var DIGIT = "[0-9]"; + + // RFC 5646 section 2.1 + // alphanum = (ALPHA / DIGIT) ; letters and numbers + var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; + // variant = 5*8alphanum ; registered variants + // / (DIGIT 3alphanum) + var variant = "(?:" + alphanum + "{5,8}|(?:" + DIGIT + alphanum + "{3}))"; + + // Match a langtag that contains a duplicate variant. + var duplicateVariant = + // Match everything in a langtag prior to any variants, and maybe some + // of the variants as well (which makes this pattern inefficient but + // not wrong, for our purposes); + "(?:" + alphanum + "{2,8}-)+" + + // a variant, parenthesised so that we can refer back to it later; + "(" + variant + ")-" + + // zero or more subtags at least two characters long (thus stopping + // before extension and privateuse components); + "(?:" + alphanum + "{2,8}-)*" + + // and the same variant again + "\\1" + + // ...but not followed by any characters that would turn it into a + // different subtag. + "(?!" + alphanum + ")"; + + // Language tags are case insensitive (RFC 5646 section 2.1.1). Using + // character classes covering both upper- and lower-case characters nearly + // addresses this -- but for the possibility of variant repetition with + // differing case, e.g. "en-variant-Variant". Use a case-insensitive + // regular expression to address this. (Note that there's no worry about + // case transformation accepting invalid characters here: users have + // already verified the string is alphanumeric Latin plus "-".) + return (internalIntlRegExps.duplicateVariantRE = RegExpCreate(duplicateVariant, "i")); +} + + +function getDuplicateSingletonRE() { + if (internalIntlRegExps.duplicateSingletonRE) + return internalIntlRegExps.duplicateSingletonRE; + + // RFC 5234 section B.1 + // ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + var ALPHA = "[a-zA-Z]"; + // DIGIT = %x30-39 + // ; 0-9 + var DIGIT = "[0-9]"; + + // RFC 5646 section 2.1 + // alphanum = (ALPHA / DIGIT) ; letters and numbers + var alphanum = "(?:" + ALPHA + "|" + DIGIT + ")"; + // singleton = DIGIT ; 0 - 9 + // / %x41-57 ; A - W + // / %x59-5A ; Y - Z + // / %x61-77 ; a - w + // / %x79-7A ; y - z + var singleton = "(?:" + DIGIT + "|[A-WY-Za-wy-z])"; + + // Match a langtag that contains a duplicate singleton. + var duplicateSingleton = + // Match a singleton subtag, parenthesised so that we can refer back to + // it later; + "-(" + singleton + ")-" + + // then zero or more subtags; + "(?:" + alphanum + "+-)*" + + // and the same singleton again + "\\1" + + // ...but not followed by any characters that would turn it into a + // different subtag. + "(?!" + alphanum + ")"; + + // Language tags are case insensitive (RFC 5646 section 2.1.1). Using + // character classes covering both upper- and lower-case characters nearly + // addresses this -- but for the possibility of singleton repetition with + // differing case, e.g. "en-u-foo-U-foo". Use a case-insensitive regular + // expression to address this. (Note that there's no worry about case + // transformation accepting invalid characters here: users have already + // verified the string is alphanumeric Latin plus "-".) + return (internalIntlRegExps.duplicateSingletonRE = RegExpCreate(duplicateSingleton, "i")); +} + + +/** + * Verifies that the given string is a well-formed BCP 47 language tag + * with no duplicate variant or singleton subtags. + * + * Spec: ECMAScript Internationalization API Specification, 6.2.2. + */ +function IsStructurallyValidLanguageTag(locale) { + assert(typeof locale === "string", "IsStructurallyValidLanguageTag"); + var languageTagRE = getLanguageTagRE(); + if (!regexp_test_no_statics(languageTagRE, locale)) + return false; + + // Before checking for duplicate variant or singleton subtags with + // regular expressions, we have to get private use subtag sequences + // out of the picture. + if (callFunction(std_String_startsWith, locale, "x-")) + return true; + var pos = callFunction(std_String_indexOf, locale, "-x-"); + if (pos !== -1) + locale = callFunction(String_substring, locale, 0, pos); + + // Check for duplicate variant or singleton subtags. + var duplicateVariantRE = getDuplicateVariantRE(); + var duplicateSingletonRE = getDuplicateSingletonRE(); + return !regexp_test_no_statics(duplicateVariantRE, locale) && + !regexp_test_no_statics(duplicateSingletonRE, locale); +} + +/** + * Joins the array elements in the given range with the supplied separator. + */ +function ArrayJoinRange(array, separator, from, to = array.length) { + assert(typeof separator === "string", "|separator| is a string value"); + assert(typeof from === "number", "|from| is a number value"); + assert(typeof to === "number", "|to| is a number value"); + assert(0 <= from && from <= to && to <= array.length, "|from| and |to| form a valid range"); + + if (from === to) + return ""; + + var result = array[from]; + for (var i = from + 1; i < to; i++) { + result += separator + array[i]; + } + return result; +} + +/** + * Canonicalizes the given structurally valid BCP 47 language tag, including + * regularized case of subtags. For example, the language tag + * Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE, where + * + * Zh ; 2*3ALPHA + * -NAN ; ["-" extlang] + * -haNS ; ["-" script] + * -bu ; ["-" region] + * -variant2 ; *("-" variant) + * -Variant1 + * -u-ca-chinese ; *("-" extension) + * -t-Zh-laTN + * -x-PRIVATE ; ["-" privateuse] + * + * becomes nan-Hans-mm-variant2-variant1-t-zh-latn-u-ca-chinese-x-private + * + * Spec: ECMAScript Internationalization API Specification, 6.2.3. + * Spec: RFC 5646, section 4.5. + */ +function CanonicalizeLanguageTag(locale) { + assert(IsStructurallyValidLanguageTag(locale), "CanonicalizeLanguageTag"); + + // The input + // "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" + // will be used throughout this method to illustrate how it works. + + // Language tags are compared and processed case-insensitively, so + // technically it's not necessary to adjust case. But for easier processing, + // and because the canonical form for most subtags is lower case, we start + // with lower case for all. + // "Zh-NAN-haNS-bu-variant2-Variant1-u-ca-chinese-t-Zh-laTN-x-PRIVATE" -> + // "zh-nan-hans-bu-variant2-variant1-u-ca-chinese-t-zh-latn-x-private" + locale = callFunction(std_String_toLowerCase, locale); + + // Handle mappings for complete tags. + if (callFunction(std_Object_hasOwnProperty, langTagMappings, locale)) + return langTagMappings[locale]; + + var subtags = StringSplitString(ToString(locale), "-"); + var i = 0; + + // Handle the standard part: All subtags before the first singleton or "x". + // "zh-nan-hans-bu-variant2-variant1" + while (i < subtags.length) { + var subtag = subtags[i]; + + // If we reach the start of an extension sequence or private use part, + // we're done with this loop. We have to check for i > 0 because for + // irregular language tags, such as i-klingon, the single-character + // subtag "i" is not the start of an extension sequence. + // In the example, we break at "u". + if (subtag.length === 1 && (i > 0 || subtag === "x")) + break; + + if (i !== 0) { + if (subtag.length === 4) { + // 4-character subtags that are not in initial position are + // script codes; their first character needs to be capitalized. + // "hans" -> "Hans" + subtag = callFunction(std_String_toUpperCase, subtag[0]) + + callFunction(String_substring, subtag, 1); + } else if (subtag.length === 2) { + // 2-character subtags that are not in initial position are + // region codes; they need to be upper case. "bu" -> "BU" + subtag = callFunction(std_String_toUpperCase, subtag); + } + } + if (callFunction(std_Object_hasOwnProperty, langSubtagMappings, subtag)) { + // Replace deprecated subtags with their preferred values. + // "BU" -> "MM" + // This has to come after we capitalize region codes because + // otherwise some language and region codes could be confused. + // For example, "in" is an obsolete language code for Indonesian, + // but "IN" is the country code for India. + // Note that the script generating langSubtagMappings makes sure + // that no regular subtag mapping will replace an extlang code. + subtag = langSubtagMappings[subtag]; + } else if (callFunction(std_Object_hasOwnProperty, extlangMappings, subtag)) { + // Replace deprecated extlang subtags with their preferred values, + // and remove the preceding subtag if it's a redundant prefix. + // "zh-nan" -> "nan" + // Note that the script generating extlangMappings makes sure that + // no extlang mapping will replace a normal language code. + subtag = extlangMappings[subtag].preferred; + if (i === 1 && extlangMappings[subtag].prefix === subtags[0]) { + callFunction(std_Array_shift, subtags); + i--; + } + } + subtags[i] = subtag; + i++; + } + var normal = ArrayJoinRange(subtags, "-", 0, i); + + // Extension sequences are sorted by their singleton characters. + // "u-ca-chinese-t-zh-latn" -> "t-zh-latn-u-ca-chinese" + var extensions = new List(); + while (i < subtags.length && subtags[i] !== "x") { + var extensionStart = i; + i++; + while (i < subtags.length && subtags[i].length > 1) + i++; + var extension = ArrayJoinRange(subtags, "-", extensionStart, i); + callFunction(std_Array_push, extensions, extension); + } + callFunction(std_Array_sort, extensions); + + // Private use sequences are left as is. "x-private" + var privateUse = ""; + if (i < subtags.length) + privateUse = ArrayJoinRange(subtags, "-", i); + + // Put everything back together. + var canonical = normal; + if (extensions.length > 0) + canonical += "-" + callFunction(std_Array_join, extensions, "-"); + if (privateUse.length > 0) { + // Be careful of a Language-Tag that is entirely privateuse. + if (canonical.length > 0) + canonical += "-" + privateUse; + else + canonical = privateUse; + } + + return canonical; +} + +function localeContainsNoUnicodeExtensions(locale) { + // No "-u-", no possible Unicode extension. + if (callFunction(std_String_indexOf, locale, "-u-") === -1) + return true; + + // "-u-" within privateuse also isn't one. + if (callFunction(std_String_indexOf, locale, "-u-") > callFunction(std_String_indexOf, locale, "-x-")) + return true; + + // An entirely-privateuse tag doesn't contain extensions. + if (callFunction(std_String_startsWith, locale, "x-")) + return true; + + // Otherwise, we have a Unicode extension sequence. + return false; +} + + +// The last-ditch locale is used if none of the available locales satisfies a +// request. "en-GB" is used based on the assumptions that English is the most +// common second language, that both en-GB and en-US are normally available in +// an implementation, and that en-GB is more representative of the English used +// in other locales. +function lastDitchLocale() { + // Per bug 1177929, strings don't clone out of self-hosted code as atoms, + // breaking IonBuilder::constant. Put this in a function for now. + return "en-GB"; +} + + +// Certain old, commonly-used language tags that lack a script, are expected to +// nonetheless imply one. This object maps these old-style tags to modern +// equivalents. +var oldStyleLanguageTagMappings = { + "pa-PK": "pa-Arab-PK", + "zh-CN": "zh-Hans-CN", + "zh-HK": "zh-Hant-HK", + "zh-SG": "zh-Hans-SG", + "zh-TW": "zh-Hant-TW", +}; + + +var localeCandidateCache = { + runtimeDefaultLocale: undefined, + candidateDefaultLocale: undefined, +}; + + +var localeCache = { + runtimeDefaultLocale: undefined, + defaultLocale: undefined, +}; + + +/** + * Compute the candidate default locale: the locale *requested* to be used as + * the default locale. We'll use it if and only if ICU provides support (maybe + * fallback support, e.g. supporting "de-ZA" through "de" support implied by a + * "de-DE" locale). + */ +function DefaultLocaleIgnoringAvailableLocales() { + const runtimeDefaultLocale = RuntimeDefaultLocale(); + if (runtimeDefaultLocale === localeCandidateCache.runtimeDefaultLocale) + return localeCandidateCache.candidateDefaultLocale; + + // If we didn't get a cache hit, compute the candidate default locale and + // cache it. Fall back on the last-ditch locale when necessary. + var candidate; + if (!IsStructurallyValidLanguageTag(runtimeDefaultLocale)) { + candidate = lastDitchLocale(); + } else { + candidate = CanonicalizeLanguageTag(runtimeDefaultLocale); + + // The default locale must be in [[availableLocales]], and that list + // must not contain any locales with Unicode extension sequences, so + // remove any present in the candidate. + candidate = removeUnicodeExtensions(candidate); + + if (callFunction(std_Object_hasOwnProperty, oldStyleLanguageTagMappings, candidate)) + candidate = oldStyleLanguageTagMappings[candidate]; + } + + // Cache the candidate locale until the runtime default locale changes. + localeCandidateCache.candidateDefaultLocale = candidate; + localeCandidateCache.runtimeDefaultLocale = runtimeDefaultLocale; + + assert(IsStructurallyValidLanguageTag(candidate), + "the candidate must be structurally valid"); + assert(localeContainsNoUnicodeExtensions(candidate), + "the candidate must not contain a Unicode extension sequence"); + + return candidate; +} + + +/** + * Returns the BCP 47 language tag for the host environment's current locale. + * + * Spec: ECMAScript Internationalization API Specification, 6.2.4. + */ +function DefaultLocale() { + const runtimeDefaultLocale = RuntimeDefaultLocale(); + if (runtimeDefaultLocale === localeCache.runtimeDefaultLocale) + return localeCache.defaultLocale; + + // If we didn't have a cache hit, compute the candidate default locale. + // Then use it as the actual default locale if ICU supports that locale + // (perhaps via fallback). Otherwise use the last-ditch locale. + var candidate = DefaultLocaleIgnoringAvailableLocales(); + var locale; + if (BestAvailableLocaleIgnoringDefault(callFunction(collatorInternalProperties.availableLocales, + collatorInternalProperties), + candidate) && + BestAvailableLocaleIgnoringDefault(callFunction(numberFormatInternalProperties.availableLocales, + numberFormatInternalProperties), + candidate) && + BestAvailableLocaleIgnoringDefault(callFunction(dateTimeFormatInternalProperties.availableLocales, + dateTimeFormatInternalProperties), + candidate)) + { + locale = candidate; + } else { + locale = lastDitchLocale(); + } + + assert(IsStructurallyValidLanguageTag(locale), + "the computed default locale must be structurally valid"); + assert(locale === CanonicalizeLanguageTag(locale), + "the computed default locale must be canonical"); + assert(localeContainsNoUnicodeExtensions(locale), + "the computed default locale must not contain a Unicode extension sequence"); + + localeCache.defaultLocale = locale; + localeCache.runtimeDefaultLocale = runtimeDefaultLocale; + + return locale; +} + + +/** + * Add old-style language tags without script code for locales that in current + * usage would include a script subtag. Also add an entry for the last-ditch + * locale, in case ICU doesn't directly support it (but does support it through + * fallback, e.g. supporting "en-GB" indirectly using "en" support). + */ +function addSpecialMissingLanguageTags(availableLocales) { + // Certain old-style language tags lack a script code, but in current usage + // they *would* include a script code. Map these over to modern forms. + var oldStyleLocales = std_Object_getOwnPropertyNames(oldStyleLanguageTagMappings); + for (var i = 0; i < oldStyleLocales.length; i++) { + var oldStyleLocale = oldStyleLocales[i]; + if (availableLocales[oldStyleLanguageTagMappings[oldStyleLocale]]) + availableLocales[oldStyleLocale] = true; + } + + // Also forcibly provide the last-ditch locale. + var lastDitch = lastDitchLocale(); + assert(lastDitch === "en-GB" && availableLocales["en"], + "shouldn't be a need to add every locale implied by the last-" + + "ditch locale, merely just the last-ditch locale"); + availableLocales[lastDitch] = true; +} + + +/** + * Canonicalizes a locale list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.1. + */ +function CanonicalizeLocaleList(locales) { + if (locales === undefined) + return new List(); + var seen = new List(); + if (typeof locales === "string") + locales = [locales]; + var O = ToObject(locales); + var len = ToLength(O.length); + var k = 0; + while (k < len) { + // Don't call ToString(k) - SpiderMonkey is faster with integers. + var kPresent = HasProperty(O, k); + if (kPresent) { + var kValue = O[k]; + if (!(typeof kValue === "string" || IsObject(kValue))) + ThrowTypeError(JSMSG_INVALID_LOCALES_ELEMENT); + var tag = ToString(kValue); + if (!IsStructurallyValidLanguageTag(tag)) + ThrowRangeError(JSMSG_INVALID_LANGUAGE_TAG, tag); + tag = CanonicalizeLanguageTag(tag); + if (callFunction(ArrayIndexOf, seen, tag) === -1) + callFunction(std_Array_push, seen, tag); + } + k++; + } + return seen; +} + + +function BestAvailableLocaleHelper(availableLocales, locale, considerDefaultLocale) { + assert(IsStructurallyValidLanguageTag(locale), "invalid BestAvailableLocale locale structure"); + assert(locale === CanonicalizeLanguageTag(locale), "non-canonical BestAvailableLocale locale"); + assert(localeContainsNoUnicodeExtensions(locale), "locale must contain no Unicode extensions"); + + // In the spec, [[availableLocales]] is formally a list of all available + // locales. But in our implementation, it's an *incomplete* list, not + // necessarily including the default locale (and all locales implied by it, + // e.g. "de" implied by "de-CH"), if that locale isn't in every + // [[availableLocales]] list (because that locale is supported through + // fallback, e.g. "de-CH" supported through "de"). + // + // If we're considering the default locale, augment the spec loop with + // additional checks to also test whether the current prefix is a prefix of + // the default locale. + + var defaultLocale; + if (considerDefaultLocale) + defaultLocale = DefaultLocale(); + + var candidate = locale; + while (true) { + if (availableLocales[candidate]) + return candidate; + + if (considerDefaultLocale && candidate.length <= defaultLocale.length) { + if (candidate === defaultLocale) + return candidate; + if (callFunction(std_String_startsWith, defaultLocale, candidate + "-")) + return candidate; + } + + var pos = callFunction(std_String_lastIndexOf, candidate, "-"); + if (pos === -1) + return undefined; + + if (pos >= 2 && candidate[pos - 2] === "-") + pos -= 2; + + candidate = callFunction(String_substring, candidate, 0, pos); + } +} + + +/** + * Compares a BCP 47 language tag against the locales in availableLocales + * and returns the best available match. Uses the fallback + * mechanism of RFC 4647, section 3.4. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.2. + * Spec: RFC 4647, section 3.4. + */ +function BestAvailableLocale(availableLocales, locale) { + return BestAvailableLocaleHelper(availableLocales, locale, true); +} + + +/** + * Identical to BestAvailableLocale, but does not consider the default locale + * during computation. + */ +function BestAvailableLocaleIgnoringDefault(availableLocales, locale) { + return BestAvailableLocaleHelper(availableLocales, locale, false); +} + +var noRelevantExtensionKeys = []; + +/** + * Compares a BCP 47 language priority list against the set of locales in + * availableLocales and determines the best available language to meet the + * request. Options specified through Unicode extension subsequences are + * ignored in the lookup, but information about such subsequences is returned + * separately. + * + * This variant is based on the Lookup algorithm of RFC 4647 section 3.4. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.3. + * Spec: RFC 4647, section 3.4. + */ +function LookupMatcher(availableLocales, requestedLocales) { + var i = 0; + var len = requestedLocales.length; + var availableLocale; + var locale, noExtensionsLocale; + while (i < len && availableLocale === undefined) { + locale = requestedLocales[i]; + noExtensionsLocale = removeUnicodeExtensions(locale); + availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale); + i++; + } + + var result = new Record(); + if (availableLocale !== undefined) { + result.locale = availableLocale; + if (locale !== noExtensionsLocale) { + var unicodeLocaleExtensionSequenceRE = getUnicodeLocaleExtensionSequenceRE(); + var extensionMatch = regexp_exec_no_statics(unicodeLocaleExtensionSequenceRE, locale); + var extension = extensionMatch[0]; + var extensionIndex = extensionMatch.index; + result.extension = extension; + result.extensionIndex = extensionIndex; + } + } else { + result.locale = DefaultLocale(); + } + return result; +} + + +/** + * Compares a BCP 47 language priority list against the set of locales in + * availableLocales and determines the best available language to meet the + * request. Options specified through Unicode extension subsequences are + * ignored in the lookup, but information about such subsequences is returned + * separately. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.4. + */ +function BestFitMatcher(availableLocales, requestedLocales) { + // this implementation doesn't have anything better + return LookupMatcher(availableLocales, requestedLocales); +} + + +/** + * Compares a BCP 47 language priority list against availableLocales and + * determines the best available language to meet the request. Options specified + * through Unicode extension subsequences are negotiated separately, taking the + * caller's relevant extensions and locale data as well as client-provided + * options into consideration. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.5. + */ +function ResolveLocale(availableLocales, requestedLocales, options, relevantExtensionKeys, localeData) { + /*jshint laxbreak: true */ + + // Steps 1-3. + var matcher = options.localeMatcher; + var r = (matcher === "lookup") + ? LookupMatcher(availableLocales, requestedLocales) + : BestFitMatcher(availableLocales, requestedLocales); + + // Step 4. + var foundLocale = r.locale; + + // Step 5.a. + var extension = r.extension; + var extensionIndex, extensionSubtags, extensionSubtagsLength; + + // Step 5. + if (extension !== undefined) { + // Step 5.b. + extensionIndex = r.extensionIndex; + + // Steps 5.d-e. + extensionSubtags = StringSplitString(ToString(extension), "-"); + extensionSubtagsLength = extensionSubtags.length; + } + + // Steps 6-7. + var result = new Record(); + result.dataLocale = foundLocale; + + // Step 8. + var supportedExtension = "-u"; + + // Steps 9-11. + var i = 0; + var len = relevantExtensionKeys.length; + while (i < len) { + // Steps 11.a-c. + var key = relevantExtensionKeys[i]; + + // In this implementation, localeData is a function, not an object. + var foundLocaleData = localeData(foundLocale); + var keyLocaleData = foundLocaleData[key]; + + // Locale data provides default value. + // Step 11.d. + var value = keyLocaleData[0]; + + // Locale tag may override. + + // Step 11.e. + var supportedExtensionAddition = ""; + + // Step 11.f is implemented by Utilities.js. + + var valuePos; + + // Step 11.g. + if (extensionSubtags !== undefined) { + // Step 11.g.i. + var keyPos = callFunction(ArrayIndexOf, extensionSubtags, key); + + // Step 11.g.ii. + if (keyPos !== -1) { + // Step 11.g.ii.1. + if (keyPos + 1 < extensionSubtagsLength && + extensionSubtags[keyPos + 1].length > 2) + { + // Step 11.g.ii.1.a. + var requestedValue = extensionSubtags[keyPos + 1]; + + // Step 11.g.ii.1.b. + valuePos = callFunction(ArrayIndexOf, keyLocaleData, requestedValue); + + // Step 11.g.ii.1.c. + if (valuePos !== -1) { + value = requestedValue; + supportedExtensionAddition = "-" + key + "-" + value; + } + } else { + // Step 11.g.ii.2. + + // According to the LDML spec, if there's no type value, + // and true is an allowed value, it's used. + + // Step 11.g.ii.2.a. + valuePos = callFunction(ArrayIndexOf, keyLocaleData, "true"); + + // Step 11.g.ii.2.b. + if (valuePos !== -1) + value = "true"; + } + } + } + + // Options override all. + + // Step 11.h.i. + var optionsValue = options[key]; + + // Step 11.h, 11.h.ii. + if (optionsValue !== undefined && + callFunction(ArrayIndexOf, keyLocaleData, optionsValue) !== -1) + { + // Step 11.h.ii.1. + if (optionsValue !== value) { + value = optionsValue; + supportedExtensionAddition = ""; + } + } + + // Steps 11.i-k. + result[key] = value; + supportedExtension += supportedExtensionAddition; + i++; + } + + // Step 12. + if (supportedExtension.length > 2) { + var preExtension = callFunction(String_substring, foundLocale, 0, extensionIndex); + var postExtension = callFunction(String_substring, foundLocale, extensionIndex); + foundLocale = preExtension + supportedExtension + postExtension; + } + + // Steps 13-14. + result.locale = foundLocale; + return result; +} + + +/** + * Returns the subset of requestedLocales for which availableLocales has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.6. + */ +function LookupSupportedLocales(availableLocales, requestedLocales) { + // Steps 1-2. + var len = requestedLocales.length; + var subset = new List(); + + // Steps 3-4. + var k = 0; + while (k < len) { + // Steps 4.a-b. + var locale = requestedLocales[k]; + var noExtensionsLocale = removeUnicodeExtensions(locale); + + // Step 4.c-d. + var availableLocale = BestAvailableLocale(availableLocales, noExtensionsLocale); + if (availableLocale !== undefined) + callFunction(std_Array_push, subset, locale); + + // Step 4.e. + k++; + } + + // Steps 5-6. + return callFunction(std_Array_slice, subset, 0); +} + + +/** + * Returns the subset of requestedLocales for which availableLocales has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.7. + */ +function BestFitSupportedLocales(availableLocales, requestedLocales) { + // don't have anything better + return LookupSupportedLocales(availableLocales, requestedLocales); +} + + +/** + * Returns the subset of requestedLocales for which availableLocales has a + * matching (possibly fallback) locale. Locales appear in the same order in the + * returned list as in the input list. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.8. + */ +function SupportedLocales(availableLocales, requestedLocales, options) { + /*jshint laxbreak: true */ + + // Step 1. + var matcher; + if (options !== undefined) { + // Steps 1.a-b. + options = ToObject(options); + matcher = options.localeMatcher; + + // Step 1.c. + if (matcher !== undefined) { + matcher = ToString(matcher); + if (matcher !== "lookup" && matcher !== "best fit") + ThrowRangeError(JSMSG_INVALID_LOCALE_MATCHER, matcher); + } + } + + // Steps 2-3. + var subset = (matcher === undefined || matcher === "best fit") + ? BestFitSupportedLocales(availableLocales, requestedLocales) + : LookupSupportedLocales(availableLocales, requestedLocales); + + // Step 4. + for (var i = 0; i < subset.length; i++) { + _DefineDataProperty(subset, i, subset[i], + ATTR_ENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); + } + _DefineDataProperty(subset, "length", subset.length, + ATTR_NONENUMERABLE | ATTR_NONCONFIGURABLE | ATTR_NONWRITABLE); + + // Step 5. + return subset; +} + + +/** + * Extracts a property value from the provided options object, converts it to + * the required type, checks whether it is one of a list of allowed values, + * and fills in a fallback value if necessary. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.9. + */ +function GetOption(options, property, type, values, fallback) { + // Step 1. + var value = options[property]; + + // Step 2. + if (value !== undefined) { + // Steps 2.a-c. + if (type === "boolean") + value = ToBoolean(value); + else if (type === "string") + value = ToString(value); + else + assert(false, "GetOption"); + + // Step 2.d. + if (values !== undefined && callFunction(ArrayIndexOf, values, value) === -1) + ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, property, value); + + // Step 2.e. + return value; + } + + // Step 3. + return fallback; +} + +/** + * Extracts a property value from the provided options object, converts it to a + * Number value, checks whether it is in the allowed range, and fills in a + * fallback value if necessary. + * + * Spec: ECMAScript Internationalization API Specification, 9.2.10. + */ +function GetNumberOption(options, property, minimum, maximum, fallback) { + assert(typeof minimum === "number", "GetNumberOption"); + assert(typeof maximum === "number", "GetNumberOption"); + assert(fallback === undefined || (fallback >= minimum && fallback <= maximum), "GetNumberOption"); + + // Step 1. + var value = options[property]; + + // Step 2. + if (value !== undefined) { + value = ToNumber(value); + if (Number_isNaN(value) || value < minimum || value > maximum) + ThrowRangeError(JSMSG_INVALID_DIGITS_VALUE, value); + return std_Math_floor(value); + } + + // Step 3. + return fallback; +} + + +/** + * Weak map used to track the initialize-as-Intl status (and, if an object has + * been so initialized, the Intl-specific internal properties) of all objects. + * Presence of an object as a key within this map indicates that the object has + * its [[initializedIntlObject]] internal property set to true. The associated + * value is an object whose structure is documented in |initializeIntlObject| + * below. + * + * Ideally we'd be using private symbols for internal properties, but + * SpiderMonkey doesn't have those yet. + */ +var internalsMap = new WeakMap(); + + +/** + * Set the [[initializedIntlObject]] internal property of |obj| to true. + */ +function initializeIntlObject(obj) { + assert(IsObject(obj), "Non-object passed to initializeIntlObject"); + + // Intl-initialized objects are weird. They have [[initializedIntlObject]] + // set on them, but they don't *necessarily* have any other properties. + + var internals = std_Object_create(null); + + // The meaning of an internals object for an object |obj| is as follows. + // + // If the .type is "partial", |obj| has [[initializedIntlObject]] set but + // nothing else. No other property of |internals| can be used. (This + // occurs when InitializeCollator or similar marks an object as + // [[initializedIntlObject]] but fails before marking it as the appropriate + // more-specific type ["Collator", "DateTimeFormat", "NumberFormat"].) + // + // Otherwise, the .type indicates the type of Intl object that |obj| is: + // "Collator", "DateTimeFormat", or "NumberFormat" (likely with more coming + // in future Intl specs). In these cases |obj| *conceptually* also has + // [[initializedCollator]] or similar set, and all the other properties + // implied by that. + // + // If |internals| doesn't have a "partial" .type, two additional properties + // have meaning. The .lazyData property stores information needed to + // compute -- without observable side effects -- the actual internal Intl + // properties of |obj|. If it is non-null, then the actual internal + // properties haven't been computed, and .lazyData must be processed by + // |setInternalProperties| before internal Intl property values are + // available. If it is null, then the .internalProps property contains an + // object whose properties are the internal Intl properties of |obj|. + + internals.type = "partial"; + internals.lazyData = null; + internals.internalProps = null; + + callFunction(std_WeakMap_set, internalsMap, obj, internals); + return internals; +} + + +/** + * Mark |internals| as having the given type and lazy data. + */ +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" || type === "PluralRules" || + type === "RelativeTimeFormat", + "bad type"); + assert(IsObject(lazyData), "non-object lazy data"); + + // Set in reverse order so that the .type change is a barrier. + internals.lazyData = lazyData; + internals.type = type; +} + + +/** + * Set the internal properties object for an |internals| object previously + * associated with lazy data. + */ +function setInternalProperties(internals, internalProps) +{ + assert(internals.type !== "partial", "newborn internals can't have computed internals"); + assert(IsObject(internals.lazyData), "lazy data must exist already"); + assert(IsObject(internalProps), "internalProps argument should be an object"); + + // Set in reverse order so that the .lazyData nulling is a barrier. + internals.internalProps = internalProps; + internals.lazyData = null; +} + + +/** + * Get the existing internal properties out of a non-newborn |internals|, or + * null if none have been computed. + */ +function maybeInternalProperties(internals) +{ + assert(IsObject(internals), "non-object passed to maybeInternalProperties"); + assert(internals.type !== "partial", "maybeInternalProperties must only be used on completely-initialized internals objects"); + var lazyData = internals.lazyData; + if (lazyData) + return null; + assert(IsObject(internals.internalProps), "missing lazy data and computed internals"); + return internals.internalProps; +} + + +/** + * Return whether |obj| has an[[initializedIntlObject]] property set to true. + */ +function isInitializedIntlObject(obj) { +#ifdef DEBUG + var internals = callFunction(std_WeakMap_get, internalsMap, 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" || + type === "PluralRules" || type === "RelativeTimeFormat", + "unexpected type"); + assert(callFunction(std_Object_hasOwnProperty, internals, "lazyData"), "missing lazyData"); + assert(callFunction(std_Object_hasOwnProperty, internals, "internalProps"), "missing internalProps"); + } else { + assert(internals === undefined, "bad mapping for |obj|"); + } +#endif + return callFunction(std_WeakMap_has, internalsMap, obj); +} + + +/** + * Check that |obj| meets the requirements for "this Collator object", "this + * NumberFormat object", or "this DateTimeFormat object" as used in the method + * with the given name. Throw a TypeError if |obj| doesn't meet these + * requirements. But if it does, return |obj|'s internals object (*not* the + * object holding its internal properties!), associated with it by + * |internalsMap|, with structure specified above. + * + * Spec: ECMAScript Internationalization API Specification, 10.3. + * Spec: ECMAScript Internationalization API Specification, 11.3. + * Spec: ECMAScript Internationalization API Specification, 12.3. + */ +function getIntlObjectInternals(obj, className, methodName) { + assert(typeof className === "string", "bad className for getIntlObjectInternals"); + + var internals = callFunction(std_WeakMap_get, internalsMap, obj); + assert(internals === undefined || isInitializedIntlObject(obj), "bad mapping in internalsMap"); + + if (internals === undefined || internals.type !== className) + ThrowTypeError(JSMSG_INTL_OBJECT_NOT_INITED, className, methodName, className); + + return internals; +} + + +/** + * Get the internal properties of known-Intl object |obj|. For use only by + * C++ code that knows what it's doing! + */ +function getInternals(obj) +{ + assert(isInitializedIntlObject(obj), "for use only on guaranteed Intl objects"); + + var internals = callFunction(std_WeakMap_get, internalsMap, obj); + + assert(internals.type !== "partial", "must have been successfully initialized"); + var lazyData = internals.lazyData; + if (!lazyData) + return internals.internalProps; + + var internalProps; + var type = internals.type; + + switch (type) { + case "Collator": + internalProps = resolveCollatorInternals(lazyData); + break; + case "DateTimeFormat": + internalProps = resolveDateTimeFormatInternals(lazyData); + break; + case "PluralRules": + internalProps = resolvePluralRulesInternals(lazyData); + break; + case "RelativeTimeFormat": + internalProps = resolveRelativeTimeFormatInternals(lazyData); + break; + default: // type === "NumberFormat" + internalProps = resolveNumberFormatInternals(lazyData); + break; + } + setInternalProperties(internals, internalProps); + return internalProps; +} + diff --git a/js/src/builtin/intl/DateTimeFormat.cpp b/js/src/builtin/intl/DateTimeFormat.cpp new file mode 100644 index 0000000000..bb43f45bda --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.cpp @@ -0,0 +1,819 @@ +/* -*- 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.DateTimeFormat implementation. */ + +#include "builtin/intl/DateTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Range.h" + +#include "jscntxt.h" +#include "jsfriendapi.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/SharedIntlData.h" +#include "builtin/intl/TimeZoneDataGenerated.h" +#include "vm/GlobalObject.h" +#include "vm/Runtime.h" + +#include "jsobjinlines.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::IsFinite; + +using JS::ClippedTime; +using JS::TimeClip; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::SharedIntlData; +using js::intl::StringsAreEqual; + +/******************** DateTimeFormat ********************/ + +const ClassOps DateTimeFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + DateTimeFormatObject::finalize +}; + +const Class DateTimeFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(DateTimeFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &DateTimeFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +dateTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().DateTimeFormat); + return true; +} +#endif + +static const JSFunctionSpec dateTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_DateTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec dateTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_DateTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("formatToParts", "Intl_DateTimeFormat_formatToParts", 0, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, dateTimeFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +/** + * 12.2.1 Intl.DateTimeFormat([ locales [, options]]) + * + * ES2017 Intl draft rev 94045d234762ad107a3d09bb6f7381a65f1a2f9b + */ +static bool +DateTimeFormat(JSContext* cx, const CallArgs& args, bool construct) +{ + RootedObject obj(cx); + + // We're following ECMA-402 1st Edition when DateTimeFormat is called + // because of backward compatibility issues. + // See https://github.com/tc39/ecma402/issues/57 + if (!construct) { + // ES Intl 1st ed., 12.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., 12.1.2.1 step 4 + obj = ToObject(cx, self); + if (!obj) + return false; + + // ES Intl 1st ed., 12.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., 12.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::getOrCreateDateTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + obj = NewObjectWithGivenProto(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(DateTimeFormatObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(DateTimeFormatObject::UDATE_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().InitializeDateTimeFormat, locales, options)) + return false; + + args.rval().setObject(*obj); + return true; +} + +static bool +DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + return DateTimeFormat(cx, args, args.isConstructing()); +} + +bool +js::intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(!args.isConstructing()); + // intl_DateTimeFormat 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 DateTimeFormat(cx, args, true); +} + +void +js::DateTimeFormatObject::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(DateTimeFormatObject::UDATE_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (UDateFormat* df = static_cast(slot.toPrivate())) + udat_close(df); + } +} + +JSObject* +js::CreateDateTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = GlobalObject::createConstructor(cx, &DateTimeFormat, cx->names().DateTimeFormat, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, + &DateTimeFormatObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + // 12.2.2 + if (!JS_DefineFunctions(cx, ctor, dateTimeFormat_static_methods)) + return nullptr; + + // 12.3.2 and 12.3.3 + if (!JS_DefineFunctions(cx, proto, dateTimeFormat_methods)) + return nullptr; + + // Install a getter for DateTimeFormat.prototype.format that returns a + // formatting function bound to a specified DateTimeFormat object (suitable + // for passing to methods like Array.prototype.map). + RootedValue getter(cx); + if (!GlobalObject::getIntrinsicValue(cx, cx->global(), cx->names().DateTimeFormatFormatGet, + &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; + + // 12.2.1 and 12.3 + if (!intl::InitializeObject(cx, proto, cx->names().InitializeDateTimeFormat, UndefinedHandleValue, + options)) + { + return nullptr; + } + + // 8.1 + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().DateTimeFormat, ctorValue, nullptr, nullptr, 0)) + return nullptr; + + return proto; +} + +bool +js::intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + RootedValue result(cx); + if (!GetAvailableLocales(cx, udat_countAvailable, udat_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + +// ICU returns old-style keyword values; map them to BCP 47 equivalents +// (see http://bugs.icu-project.org/trac/ticket/9620). +static const char* +bcp47CalendarName(const char* icuName) +{ + if (StringsAreEqual(icuName, "ethiopic-amete-alem")) + return "ethioaa"; + if (StringsAreEqual(icuName, "gregorian")) + return "gregory"; + if (StringsAreEqual(icuName, "islamic-civil")) + return "islamicc"; + return icuName; +} + +bool +js::intl_availableCalendars(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; + + RootedObject calendars(cx, NewDenseEmptyArray(cx)); + if (!calendars) + return false; + uint32_t index = 0; + + // We need the default calendar for the locale as the first result. + UErrorCode status = U_ZERO_ERROR; + RootedString jscalendar(cx); + { + UCalendar* cal = ucal_open(nullptr, 0, locale.ptr(), UCAL_DEFAULT, &status); + + // This correctly handles nullptr |cal| when opening failed. + ScopedICUObject closeCalendar(cal); + + const char* calendar = ucal_getType(cal, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); + if (!jscalendar) + return false; + } + + RootedValue element(cx, StringValue(jscalendar)); + if (!DefineElement(cx, calendars, index++, element)) + return false; + + // Now get the calendars that "would make a difference", i.e., not the default. + UEnumeration* values = ucal_getKeywordValuesForLocale("ca", locale.ptr(), false, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + uint32_t count = uenum_count(values, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + for (; count > 0; count--) { + const char* calendar = uenum_next(values, nullptr, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + jscalendar = JS_NewStringCopyZ(cx, bcp47CalendarName(calendar)); + if (!jscalendar) + return false; + element = StringValue(jscalendar); + if (!DefineElement(cx, calendars, index++, element)) + return false; + } + + args.rval().setObject(*calendars); + return true; +} + +bool +js::intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->sharedIntlData; + + RootedString timeZone(cx, args[0].toString()); + RootedString validatedTimeZone(cx); + if (!sharedIntlData.validateTimeZoneName(cx, timeZone, &validatedTimeZone)) + return false; + + if (validatedTimeZone) + args.rval().setString(validatedTimeZone); + else + args.rval().setNull(); + + return true; +} + +bool +js::intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + MOZ_ASSERT(args[0].isString()); + + SharedIntlData& sharedIntlData = cx->sharedIntlData; + + // Some time zone names are canonicalized differently by ICU -- handle + // those first: + RootedString timeZone(cx, args[0].toString()); + RootedString ianaTimeZone(cx); + if (!sharedIntlData.tryCanonicalizeTimeZoneConsistentWithIANA(cx, timeZone, &ianaTimeZone)) + return false; + + if (ianaTimeZone) { + args.rval().setString(ianaTimeZone); + return true; + } + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, timeZone)) + return false; + + mozilla::Range tzchars = stableChars.twoByteRange(); + + JSString* str = CallICU(cx, [&tzchars](UChar* chars, uint32_t size, UErrorCode* status) { + return ucal_getCanonicalTimeZoneID(tzchars.begin().get(), tzchars.length(), + chars, size, nullptr, status); + }); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + // The current default might be stale, because JS::ResetTimeZone() doesn't + // immediately update ICU's default time zone. So perform an update if + // needed. + js::ResyncICUDefaultTimeZone(); + + JSString* str = CallICU(cx, ucal_getDefaultTimeZone); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +bool +js::intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp) { + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 0); + + UErrorCode status = U_ZERO_ERROR; + const UChar* uTimeZone = nullptr; + int32_t uTimeZoneLength = 0; + const char* rootLocale = ""; + UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, rootLocale, UCAL_DEFAULT, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(cal); + + int32_t offset = ucal_get(cal, UCAL_ZONE_OFFSET, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + args.rval().setInt32(offset); + return true; +} + +bool +js::intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 2); + MOZ_ASSERT(args[0].isString()); + MOZ_ASSERT(args[1].isString()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSFlatString* skeletonFlat = args[1].toString()->ensureFlat(cx); + if (!skeletonFlat) + return false; + + AutoStableStringChars stableChars(cx); + if (!stableChars.initTwoByte(cx, skeletonFlat)) + return false; + + mozilla::Range skeletonChars = stableChars.twoByteRange(); + uint32_t skeletonLen = u_strlen(Char16ToUChar(skeletonChars.begin().get())); + + UErrorCode status = U_ZERO_ERROR; + UDateTimePatternGenerator* gen = udatpg_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(gen); + + JSString* str = + CallICU(cx, [gen, &skeletonChars, skeletonLen](UChar* chars, uint32_t size, UErrorCode* status) { + return udatpg_getBestPattern(gen, skeletonChars.begin().get(), skeletonLen, + chars, size, status); + }); + if (!str) + return false; + args.rval().setString(str); + return true; +} + +/** + * Returns a new UDateFormat with the locale and date-time formatting options + * of the given DateTimeFormat. + */ +static UDateFormat* +NewUDateFormat(JSContext* cx, HandleObject dateTimeFormat) +{ + RootedValue value(cx); + + RootedObject internals(cx, intl::GetInternalsObject(cx, dateTimeFormat)); + if (!internals) + return nullptr; + + if (!GetProperty(cx, internals, internals, cx->names().locale, &value)) + return nullptr; + JSAutoByteString locale(cx, value.toString()); + if (!locale) + return nullptr; + + // We don't need to look at calendar and numberingSystem - they can only be + // set via the Unicode locale extension and are therefore already set on + // locale. + + if (!GetProperty(cx, internals, internals, cx->names().timeZone, &value)) + return nullptr; + + AutoStableStringChars timeZoneChars(cx); + Rooted timeZoneFlat(cx, value.toString()->ensureFlat(cx)); + if (!timeZoneFlat || !timeZoneChars.initTwoByte(cx, timeZoneFlat)) + return nullptr; + + const UChar* uTimeZone = Char16ToUChar(timeZoneChars.twoByteRange().begin().get()); + uint32_t uTimeZoneLength = u_strlen(uTimeZone); + + if (!GetProperty(cx, internals, internals, cx->names().pattern, &value)) + return nullptr; + + AutoStableStringChars patternChars(cx); + Rooted patternFlat(cx, value.toString()->ensureFlat(cx)); + if (!patternFlat || !patternChars.initTwoByte(cx, patternFlat)) + return nullptr; + + const UChar* uPattern = Char16ToUChar(patternChars.twoByteRange().begin().get()); + uint32_t uPatternLength = u_strlen(uPattern); + + UErrorCode status = U_ZERO_ERROR; + UDateFormat* df = + udat_open(UDAT_PATTERN, UDAT_PATTERN, IcuLocale(locale.ptr()), uTimeZone, uTimeZoneLength, + uPattern, uPatternLength, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return nullptr; + } + + // ECMAScript requires the Gregorian calendar to be used from the beginning + // of ECMAScript time. + UCalendar* cal = const_cast(udat_getCalendar(df)); + ucal_setGregorianChange(cal, StartOfTime, &status); + + // An error here means the calendar is not Gregorian, so we don't care. + + return df; +} + +static bool +intl_FormatDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + return false; + } + + JSString* str = CallICU(cx, [df, x](UChar* chars, int32_t size, UErrorCode* status) { + return udat_format(df, x, chars, size, nullptr, status); + }); + if (!str) + return false; + + result.setString(str); + return true; +} + +using FieldType = ImmutablePropertyNamePtr JSAtomState::*; + +static FieldType +GetFieldTypeForFormatField(UDateFormatField fieldName) +{ + // See intl/icu/source/i18n/unicode/udat.h for a detailed field list. This + // switch is deliberately exhaustive: cases might have to be added/removed + // if this code is compiled with a different ICU with more + // UDateFormatField enum initializers. Please guard such cases with + // appropriate ICU version-testing #ifdefs, should cross-version divergence + // occur. + switch (fieldName) { + case UDAT_ERA_FIELD: + return &JSAtomState::era; + case UDAT_YEAR_FIELD: + case UDAT_YEAR_WOY_FIELD: + case UDAT_EXTENDED_YEAR_FIELD: + case UDAT_YEAR_NAME_FIELD: + return &JSAtomState::year; + + case UDAT_MONTH_FIELD: + case UDAT_STANDALONE_MONTH_FIELD: + return &JSAtomState::month; + + case UDAT_DATE_FIELD: + case UDAT_JULIAN_DAY_FIELD: + return &JSAtomState::day; + + case UDAT_HOUR_OF_DAY1_FIELD: + case UDAT_HOUR_OF_DAY0_FIELD: + case UDAT_HOUR1_FIELD: + case UDAT_HOUR0_FIELD: + return &JSAtomState::hour; + + case UDAT_MINUTE_FIELD: + return &JSAtomState::minute; + + case UDAT_SECOND_FIELD: + return &JSAtomState::second; + + case UDAT_DAY_OF_WEEK_FIELD: + case UDAT_STANDALONE_DAY_FIELD: + case UDAT_DOW_LOCAL_FIELD: + case UDAT_DAY_OF_WEEK_IN_MONTH_FIELD: + return &JSAtomState::weekday; + + case UDAT_AM_PM_FIELD: + return &JSAtomState::dayPeriod; + + case UDAT_TIMEZONE_FIELD: + return &JSAtomState::timeZoneName; + + case UDAT_FRACTIONAL_SECOND_FIELD: + case UDAT_DAY_OF_YEAR_FIELD: + case UDAT_WEEK_OF_YEAR_FIELD: + case UDAT_WEEK_OF_MONTH_FIELD: + case UDAT_MILLISECONDS_IN_DAY_FIELD: + case UDAT_TIMEZONE_RFC_FIELD: + case UDAT_TIMEZONE_GENERIC_FIELD: + case UDAT_QUARTER_FIELD: + case UDAT_STANDALONE_QUARTER_FIELD: + case UDAT_TIMEZONE_SPECIAL_FIELD: + case UDAT_TIMEZONE_LOCALIZED_GMT_OFFSET_FIELD: + case UDAT_TIMEZONE_ISO_FIELD: + case UDAT_TIMEZONE_ISO_LOCAL_FIELD: +#ifndef U_HIDE_INTERNAL_API + case UDAT_RELATED_YEAR_FIELD: +#endif +#ifndef U_HIDE_DRAFT_API + case UDAT_AM_PM_MIDNIGHT_NOON_FIELD: + case UDAT_FLEXIBLE_DAY_PERIOD_FIELD: +#endif +#ifndef U_HIDE_INTERNAL_API + case UDAT_TIME_SEPARATOR_FIELD: +#endif + // These fields are all unsupported. + return nullptr; + + case UDAT_FIELD_COUNT: + MOZ_ASSERT_UNREACHABLE("format field sentinel value returned by " + "iterator!"); + } + + MOZ_ASSERT_UNREACHABLE("unenumerated, undocumented format field returned " + "by iterator"); + return nullptr; +} + +static bool +intl_FormatToPartsDateTime(JSContext* cx, UDateFormat* df, double x, MutableHandleValue result) +{ + if (!IsFinite(x)) { + JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_DATE_NOT_FINITE); + return false; + } + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + UErrorCode status = U_ZERO_ERROR; + UFieldPositionIterator* fpositer = ufieldpositer_open(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(fpositer); + + RootedString overallResult(cx); + overallResult = CallICU(cx, [df, x, fpositer](UChar* chars, int32_t size, UErrorCode* status) { + return udat_formatForFields(df, x, chars, size, fpositer, status); + }); + if (!overallResult) + return false; + + RootedArrayObject partsArray(cx, NewDenseEmptyArray(cx)); + if (!partsArray) + return false; + if (overallResult->length() == 0) { + // An empty string contains no parts, so avoid extra work below. + result.setObject(*partsArray); + return true; + } + + size_t lastEndIndex = 0; + + uint32_t partIndex = 0; + RootedObject singlePart(cx); + RootedValue partType(cx); + RootedValue val(cx); + + auto AppendPart = [&](FieldType type, size_t beginIndex, size_t endIndex) { + singlePart = NewBuiltinClassInstance(cx); + if (!singlePart) + return false; + + partType = StringValue(cx->names().*type); + if (!DefineProperty(cx, singlePart, cx->names().type, partType)) + return false; + + JSLinearString* partSubstr = + NewDependentString(cx, overallResult, beginIndex, endIndex - beginIndex); + if (!partSubstr) + return false; + + val = StringValue(partSubstr); + if (!DefineProperty(cx, singlePart, cx->names().value, val)) + return false; + + val = ObjectValue(*singlePart); + if (!DefineElement(cx, partsArray, partIndex, val)) + return false; + + lastEndIndex = endIndex; + partIndex++; + return true; + }; + + 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, + "field iterator returning invalid range"); + + size_t beginIndex(beginIndexInt); + size_t endIndex(endIndexInt); + + // Technically this isn't guaranteed. But it appears true in pratice, + // and http://bugs.icu-project.org/trac/ticket/12024 is expected to + // correct the documentation lapse. + MOZ_ASSERT(lastEndIndex <= beginIndex, + "field iteration didn't return fields in order start to " + "finish as expected"); + + if (FieldType type = GetFieldTypeForFormatField(static_cast(fieldInt))) { + if (lastEndIndex < beginIndex) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, beginIndex)) + return false; + } + + if (!AppendPart(type, beginIndex, endIndex)) + return false; + } + } + + // Append any final literal. + if (lastEndIndex < overallResult->length()) { + if (!AppendPart(&JSAtomState::literal, lastEndIndex, overallResult->length())) + return false; + } + + result.setObject(*partsArray); + return true; +} + +bool +js::intl_FormatDateTime(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 dateTimeFormat(cx, &args[0].toObject()); + + // Obtain a UDateFormat object, cached if possible. + bool isDateTimeFormatInstance = dateTimeFormat->getClass() == &DateTimeFormatObject::class_; + UDateFormat* df; + if (isDateTimeFormatInstance) { + void* priv = + dateTimeFormat->as().getReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT).toPrivate(); + df = static_cast(priv); + if (!df) { + df = NewUDateFormat(cx, dateTimeFormat); + if (!df) + return false; + dateTimeFormat->as().setReservedSlot(DateTimeFormatObject::UDATE_FORMAT_SLOT, PrivateValue(df)); + } + } else { + // There's no good place to cache the ICU date-time format for an object + // that has been initialized as a DateTimeFormat but is not a + // DateTimeFormat instance. One possibility might be to add a + // DateTimeFormat instance as an internal property to each such object. + df = NewUDateFormat(cx, dateTimeFormat); + if (!df) + return false; + } + + // Use the UDateFormat to actually format the time stamp. + RootedValue result(cx); + bool success = args[2].toBoolean() + ? intl_FormatToPartsDateTime(cx, df, args[1].toNumber(), &result) + : intl_FormatDateTime(cx, df, args[1].toNumber(), &result); + + if (!isDateTimeFormatInstance) + udat_close(df); + if (!success) + return false; + args.rval().set(result); + return true; +} + diff --git a/js/src/builtin/intl/DateTimeFormat.h b/js/src/builtin/intl/DateTimeFormat.h new file mode 100644 index 0000000000..9bdddc58f2 --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.h @@ -0,0 +1,143 @@ +/* -*- 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_DateTimeFormat_h +#define builtin_intl_DateTimeFormat_h + +#include "mozilla/Attributes.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "js/RootingAPI.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; +class GlobalObject; + +/******************** DateTimeFormat ********************/ + +class DateTimeFormatObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UDATE_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* + +CreateDateTimeFormatPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns a new instance of the standard built-in DateTimeFormat 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: dateTimeFormat = intl_DateTimeFormat(locales, options) + */ +extern MOZ_MUST_USE bool +intl_DateTimeFormat(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an object indicating the supported locales for date and time + * 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_DateTimeFormat_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_DateTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns an array with the calendar type identifiers per Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * supported calendars for the given locale. The default calendar is + * element 0. + * + * Usage: calendars = intl_availableCalendars(locale) + */ +extern MOZ_MUST_USE bool +intl_availableCalendars(JSContext* cx, unsigned argc, Value* vp); + +/** + * 6.4.1 IsValidTimeZoneName ( timeZone ) + * + * Verifies that the given string is a valid time zone name. If it is a valid + * time zone name, its IANA time zone name is returned. Otherwise returns null. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + * + * Usage: ianaTimeZone = intl_IsValidTimeZoneName(timeZone) + */ +extern MOZ_MUST_USE bool +intl_IsValidTimeZoneName(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the canonicalized time zone name. Canonicalization resolves link + * names to their target time zones. + * + * Usage: ianaTimeZone = intl_canonicalizeTimeZone(timeZone) + */ +extern MOZ_MUST_USE bool +intl_canonicalizeTimeZone(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the default time zone name. The time zone name is not canonicalized. + * + * Usage: icuDefaultTimeZone = intl_defaultTimeZone() + */ +extern MOZ_MUST_USE bool +intl_defaultTimeZone(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return the raw offset from GMT in milliseconds for the default time zone. + * + * Usage: defaultTimeZoneOffset = intl_defaultTimeZoneOffset() + */ +extern MOZ_MUST_USE bool +intl_defaultTimeZoneOffset(JSContext* cx, unsigned argc, Value* vp); + +/** + * Return a pattern in the date-time format pattern language of Unicode + * Technical Standard 35, Unicode Locale Data Markup Language, for the + * best-fit date-time format pattern corresponding to skeleton for the + * given locale. + * + * Usage: pattern = intl_patternForSkeleton(locale, skeleton) + */ +extern MOZ_MUST_USE bool +intl_patternForSkeleton(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a String value representing x (which must be a Number value) + * according to the effective locale and the formatting options of the + * given DateTimeFormat. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + * + * Usage: formatted = intl_FormatDateTime(dateTimeFormat, x) + */ +extern MOZ_MUST_USE bool +intl_FormatDateTime(JSContext* cx, unsigned argc, Value* vp); + + +} // namespace js + +#endif /* builtin_intl_DateTimeFormat_h */ \ No newline at end of file diff --git a/js/src/builtin/intl/DateTimeFormat.js b/js/src/builtin/intl/DateTimeFormat.js new file mode 100644 index 0000000000..75d7ead61a --- /dev/null +++ b/js/src/builtin/intl/DateTimeFormat.js @@ -0,0 +1,898 @@ +/* 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.DateTimeFormat **********/ + + +/** + * Compute an internal properties object from |lazyDateTimeFormatData|. + */ +function resolveDateTimeFormatInternals(lazyDateTimeFormatData) { + assert(IsObject(lazyDateTimeFormatData), "lazy data not an object?"); + + // Lazy DateTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // + // localeOpt: // *first* opt computed in InitializeDateTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // + // hour12: true / false, // optional + // } + // + // timeZone: IANA time zone name, + // + // formatOpt: // *second* opt computed in InitializeDateTimeFormat + // { + // // all the properties/values listed in Table 3 + // // (weekday, era, year, month, day, &c.) + // } + // + // formatMatcher: "basic" / "best fit", + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every DateTimeFormat lazy data object has *all* these properties, + // never a subset of them. + + var internalProps = std_Object_create(null); + + // Compute effective locale. + // Step 8. + var DateTimeFormat = dateTimeFormatInternalProperties; + + // Step 9. + var localeData = DateTimeFormat.localeData; + + // Step 10. + var r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + lazyDateTimeFormatData.requestedLocales, + lazyDateTimeFormatData.localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + // Steps 11-13. + internalProps.locale = r.locale; + internalProps.calendar = r.ca; + internalProps.numberingSystem = r.nu; + + // Compute formatting options. + // Step 14. + var dataLocale = r.dataLocale; + + // Steps 15-17. + var tz = lazyDateTimeFormatData.timeZone; + if (tz === undefined) { + // Step 16. + tz = DefaultTimeZone(); + } + internalProps.timeZone = tz; + + // Step 18. + var formatOpt = lazyDateTimeFormatData.formatOpt; + + // Steps 27-28, more or less - see comment after this function. + var pattern = toBestICUPattern(dataLocale, formatOpt); + + // Step 29. + internalProps.pattern = pattern; + + // Step 30. + internalProps.boundFormat = undefined; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + + +/** + * Returns an object containing the DateTimeFormat internal properties of |obj|, + * or throws a TypeError if |obj| isn't DateTimeFormat-initialized. + */ +function getDateTimeFormatInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "DateTimeFormat", methodName); + assert(internals.type === "DateTimeFormat", "bad type escaped getIntlObjectInternals"); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + // Otherwise it's time to fully create them. + internalProps = resolveDateTimeFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Components of date and time formats and their values. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +var dateTimeComponentValues = { + weekday: ["narrow", "short", "long"], + era: ["narrow", "short", "long"], + year: ["2-digit", "numeric"], + month: ["2-digit", "numeric", "narrow", "short", "long"], + day: ["2-digit", "numeric"], + hour: ["2-digit", "numeric"], + minute: ["2-digit", "numeric"], + second: ["2-digit", "numeric"], + timeZoneName: ["short", "long"] +}; + + +var dateTimeComponents = std_Object_getOwnPropertyNames(dateTimeComponentValues); + +var timeZoneCache = { + icuDefaultTimeZone: undefined, + defaultTimeZone: undefined, +}; + + +/** + * 6.4.2 CanonicalizeTimeZoneName ( timeZone ) + * + * Canonicalizes the given IANA time zone name. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + */ +function CanonicalizeTimeZoneName(timeZone) { + assert(typeof timeZone === "string", "CanonicalizeTimeZoneName"); + + // Step 1. (Not applicable, the input is already a valid IANA time zone.) + assert(timeZone !== "Etc/Unknown", "Invalid time zone"); + assert(timeZone === intl_IsValidTimeZoneName(timeZone), "Time zone name not normalized"); + + // Step 2. + var ianaTimeZone = intl_canonicalizeTimeZone(timeZone); + assert(ianaTimeZone !== "Etc/Unknown", "Invalid canonical time zone"); + assert(ianaTimeZone === intl_IsValidTimeZoneName(ianaTimeZone), "Unsupported canonical time zone"); + + // Step 3. + if (ianaTimeZone === "Etc/UTC" || ianaTimeZone === "Etc/GMT") { + // ICU/CLDR canonicalizes Etc/UCT to Etc/GMT, but following IANA and + // ECMA-402 to the letter means Etc/UCT is a separate time zone. + if (timeZone === "Etc/UCT" || timeZone === "UCT") + ianaTimeZone = "Etc/UCT"; + else + ianaTimeZone = "UTC"; + } + + // Step 4. + return ianaTimeZone; +} + + +/** + * 6.4.3 DefaultTimeZone () + * + * Returns the IANA time zone name for the host environment's current time zone. + * + * ES2017 Intl draft rev 4a23f407336d382ed5e3471200c690c9b020b5f3 + */ +function DefaultTimeZone() { + const icuDefaultTimeZone = intl_defaultTimeZone(); + if (timeZoneCache.icuDefaultTimeZone === icuDefaultTimeZone) + return timeZoneCache.defaultTimeZone; + + // Verify that the current ICU time zone is a valid ECMA-402 time zone. + var timeZone = intl_IsValidTimeZoneName(icuDefaultTimeZone); + if (timeZone === null) { + // Before defaulting to "UTC", try to represent the default time zone + // using the Etc/GMT + offset format. This format only accepts full + // hour offsets. + const msPerHour = 60 * 60 * 1000; + var offset = intl_defaultTimeZoneOffset(); + assert(offset === (offset | 0), + "milliseconds offset shouldn't be able to exceed int32_t range"); + var offsetHours = offset / msPerHour, offsetHoursFraction = offset % msPerHour; + if (offsetHoursFraction === 0) { + // Etc/GMT + offset uses POSIX-style signs, i.e. a positive offset + // means a location west of GMT. + timeZone = "Etc/GMT" + (offsetHours < 0 ? "+" : "-") + std_Math_abs(offsetHours); + + // Check if the fallback is valid. + timeZone = intl_IsValidTimeZoneName(timeZone); + } + + // Fallback to "UTC" if everything else fails. + if (timeZone === null) + timeZone = "UTC"; + } + + // Canonicalize the ICU time zone, e.g. change Etc/UTC to UTC. + var defaultTimeZone = CanonicalizeTimeZoneName(timeZone); + + timeZoneCache.defaultTimeZone = defaultTimeZone; + timeZoneCache.icuDefaultTimeZone = icuDefaultTimeZone; + + return defaultTimeZone; +} + +/** + * Initializes an object as a DateTimeFormat. + * + * 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 DateTimeFormat. + * This later work occurs in |resolveDateTimeFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function InitializeDateTimeFormat(dateTimeFormat, locales, options) { + assert(IsObject(dateTimeFormat), "InitializeDateTimeFormat"); + + // Step 1. + if (isInitializedIntlObject(dateTimeFormat)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + // Step 2. + var internals = initializeIntlObject(dateTimeFormat); + + // Lazy DateTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // + // localeOpt: // *first* opt computed in InitializeDateTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // } + // + // timeZone: IANA time zone name, + // + // formatOpt: // *second* opt computed in InitializeDateTimeFormat + // { + // // all the properties/values listed in Table 3 + // // (weekday, era, year, month, day, &c.) + // + // hour12: true / false // optional + // } + // + // formatMatcher: "basic" / "best fit", + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every DateTimeFormat lazy data object has *all* these properties, + // never a subset of them. + var lazyDateTimeFormatData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyDateTimeFormatData.requestedLocales = requestedLocales; + + // Step 4. + options = ToDateTimeOptions(options, "any", "date"); + + // Compute options that impact interpretation of locale. + // Step 5. + var localeOpt = new Record(); + lazyDateTimeFormatData.localeOpt = localeOpt; + + // Steps 6-7. + var localeMatcher = + GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], + "best fit"); + localeOpt.localeMatcher = localeMatcher; + + // Steps 15-17. + var tz = options.timeZone; + if (tz !== undefined) { + // Step 15.a. + tz = ToString(tz); + + // Step 15.b. + var timeZone = intl_IsValidTimeZoneName(tz); + if (timeZone === null) + ThrowRangeError(JSMSG_INVALID_TIME_ZONE, tz); + + // Step 15.c. + tz = CanonicalizeTimeZoneName(timeZone); + } + lazyDateTimeFormatData.timeZone = tz; + + // Step 18. + var formatOpt = new Record(); + lazyDateTimeFormatData.formatOpt = formatOpt; + + // Step 19. + var i, prop; + for (i = 0; i < dateTimeComponents.length; i++) { + prop = dateTimeComponents[i]; + var value = GetOption(options, prop, "string", dateTimeComponentValues[prop], undefined); + formatOpt[prop] = value; + } + + // Steps 20-21 provided by ICU - see comment after this function. + + // Step 22. + // + // For some reason (ICU not exposing enough interface?) we drop the + // requested format matcher on the floor after this. In any case, even if + // doing so is justified, we have to do this work here in case it triggers + // getters or similar. (bug 852837) + var formatMatcher = + GetOption(options, "formatMatcher", "string", ["basic", "best fit"], + "best fit"); + + // Steps 23-25 provided by ICU, more or less - see comment after this function. + + // Step 26. + var hr12 = GetOption(options, "hour12", "boolean", undefined, undefined); + + // Pass hr12 on to ICU. + if (hr12 !== undefined) + formatOpt.hour12 = hr12; + + // Step 31. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + setLazyData(internals, "DateTimeFormat", lazyDateTimeFormatData); +} + + +// Intl.DateTimeFormat and ICU skeletons and patterns +// ================================================== +// +// Different locales have different ways to display dates using the same +// basic components. For example, en-US might use "Sept. 24, 2012" while +// fr-FR might use "24 Sept. 2012". The intent of Intl.DateTimeFormat is to +// permit production of a format for the locale that best matches the +// set of date-time components and their desired representation as specified +// by the API client. +// +// ICU supports specification of date and time formats in three ways: +// +// 1) A style is just one of the identifiers FULL, LONG, MEDIUM, or SHORT. +// The date-time components included in each style and their representation +// are defined by ICU using CLDR locale data (CLDR is the Unicode +// Consortium's Common Locale Data Repository). +// +// 2) A skeleton is a string specifying which date-time components to include, +// and which representations to use for them. For example, "yyyyMMMMdd" +// specifies a year with at least four digits, a full month name, and a +// two-digit day. It does not specify in which order the components appear, +// how they are separated, the localized strings for textual components +// (such as weekday or month), whether the month is in format or +// stand-alone form¹, or the numbering system used for numeric components. +// All that information is filled in by ICU using CLDR locale data. +// ¹ The format form is the one used in formatted strings that include a +// day; the stand-alone form is used when not including days, e.g., in +// calendar headers. The two forms differ at least in some Slavic languages, +// e.g. Russian: "22 марта 2013 г." vs. "Март 2013". +// +// 3) A pattern is a string specifying which date-time components to include, +// in which order, with which separators, in which grammatical case. For +// example, "EEEE, d MMMM y" specifies the full localized weekday name, +// followed by comma and space, followed by the day, followed by space, +// followed by the full month name in format form, followed by space, +// followed by the full year. It +// still does not specify localized strings for textual components and the +// numbering system - these are determined by ICU using CLDR locale data or +// possibly API parameters. +// +// All actual formatting in ICU is done with patterns; styles and skeletons +// have to be mapped to patterns before processing. +// +// The options of DateTimeFormat most closely correspond to ICU skeletons. This +// implementation therefore, in the toBestICUPattern function, converts +// DateTimeFormat options to ICU skeletons, and then lets ICU map skeletons to +// actual ICU patterns. The pattern may not directly correspond to what the +// skeleton requests, as the mapper (UDateTimePatternGenerator) is constrained +// by the available locale data for the locale. The resulting ICU pattern is +// kept as the DateTimeFormat's [[pattern]] internal property and passed to ICU +// in the format method. +// +// An ICU pattern represents the information of the following DateTimeFormat +// internal properties described in the specification, which therefore don't +// exist separately in the implementation: +// - [[weekday]], [[era]], [[year]], [[month]], [[day]], [[hour]], [[minute]], +// [[second]], [[timeZoneName]] +// - [[hour12]] +// - [[hourNo0]] +// When needed for the resolvedOptions method, the resolveICUPattern function +// maps the instance's ICU pattern back to the specified properties of the +// object returned by resolvedOptions. +// +// ICU date-time skeletons and patterns aren't fully documented in the ICU +// documentation (see http://bugs.icu-project.org/trac/ticket/9627). The best +// documentation at this point is in UTR 35: +// http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + + +/** + * Returns an ICU pattern string for the given locale and representing the + * specified options as closely as possible given available locale data. + */ +function toBestICUPattern(locale, options) { + // Create an ICU skeleton representing the specified options. See + // http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + var skeleton = ""; + switch (options.weekday) { + case "narrow": + skeleton += "EEEEE"; + break; + case "short": + skeleton += "E"; + break; + case "long": + skeleton += "EEEE"; + } + switch (options.era) { + case "narrow": + skeleton += "GGGGG"; + break; + case "short": + skeleton += "G"; + break; + case "long": + skeleton += "GGGG"; + break; + } + switch (options.year) { + case "2-digit": + skeleton += "yy"; + break; + case "numeric": + skeleton += "y"; + break; + } + switch (options.month) { + case "2-digit": + skeleton += "MM"; + break; + case "numeric": + skeleton += "M"; + break; + case "narrow": + skeleton += "MMMMM"; + break; + case "short": + skeleton += "MMM"; + break; + case "long": + skeleton += "MMMM"; + break; + } + switch (options.day) { + case "2-digit": + skeleton += "dd"; + break; + case "numeric": + skeleton += "d"; + break; + } + var hourSkeletonChar = "j"; + if (options.hour12 !== undefined) { + if (options.hour12) + hourSkeletonChar = "h"; + else + hourSkeletonChar = "H"; + } + switch (options.hour) { + case "2-digit": + skeleton += hourSkeletonChar + hourSkeletonChar; + break; + case "numeric": + skeleton += hourSkeletonChar; + break; + } + switch (options.minute) { + case "2-digit": + skeleton += "mm"; + break; + case "numeric": + skeleton += "m"; + break; + } + switch (options.second) { + case "2-digit": + skeleton += "ss"; + break; + case "numeric": + skeleton += "s"; + break; + } + switch (options.timeZoneName) { + case "short": + skeleton += "z"; + break; + case "long": + skeleton += "zzzz"; + break; + } + + // Let ICU convert the ICU skeleton to an ICU pattern for the given locale. + return intl_patternForSkeleton(locale, skeleton); +} + + +/** + * Returns a new options object that includes the provided options (if any) + * and fills in default components if required components are not defined. + * Required can be "date", "time", or "any". + * Defaults can be "date", "time", or "all". + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function ToDateTimeOptions(options, required, defaults) { + assert(typeof required === "string", "ToDateTimeOptions"); + assert(typeof defaults === "string", "ToDateTimeOptions"); + + // Steps 1-3. + if (options === undefined) + options = null; + else + options = ToObject(options); + options = std_Object_create(options); + + // Step 4. + var needDefaults = true; + + // Step 5. + if ((required === "date" || required === "any") && + (options.weekday !== undefined || options.year !== undefined || + options.month !== undefined || options.day !== undefined)) + { + needDefaults = false; + } + + // Step 6. + if ((required === "time" || required === "any") && + (options.hour !== undefined || options.minute !== undefined || + options.second !== undefined)) + { + needDefaults = false; + } + + // Step 7. + if (needDefaults && (defaults === "date" || defaults === "all")) { + // The specification says to call [[DefineOwnProperty]] with false for + // the Throw parameter, while Object.defineProperty uses true. For the + // calls here, the difference doesn't matter because we're adding + // properties to a new object. + _DefineDataProperty(options, "year", "numeric"); + _DefineDataProperty(options, "month", "numeric"); + _DefineDataProperty(options, "day", "numeric"); + } + + // Step 8. + if (needDefaults && (defaults === "time" || defaults === "all")) { + // See comment for step 7. + _DefineDataProperty(options, "hour", "numeric"); + _DefineDataProperty(options, "minute", "numeric"); + _DefineDataProperty(options, "second", "numeric"); + } + + // Step 9. + return options; +} + + +/** + * Compares the date and time components requested by options with the available + * date and time formats in formats, and selects the best match according + * to a specified basic matching algorithm. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function BasicFormatMatcher(options, formats) { + // Steps 1-6. + var removalPenalty = 120, + additionPenalty = 20, + longLessPenalty = 8, + longMorePenalty = 6, + shortLessPenalty = 6, + shortMorePenalty = 3; + + // Table 3. + var properties = ["weekday", "era", "year", "month", "day", + "hour", "minute", "second", "timeZoneName"]; + + // Step 11.c.vi.1. + var values = ["2-digit", "numeric", "narrow", "short", "long"]; + + // Steps 7-8. + var bestScore = -Infinity; + var bestFormat; + + // Steps 9-11. + var i = 0; + var len = formats.length; + while (i < len) { + // Steps 11.a-b. + var format = formats[i]; + var score = 0; + + // Step 11.c. + var formatProp; + for (var j = 0; j < properties.length; j++) { + var property = properties[j]; + + // Step 11.c.i. + var optionsProp = options[property]; + // Step missing from spec. + // https://bugs.ecmascript.org/show_bug.cgi?id=1254 + formatProp = undefined; + + // Steps 11.c.ii-iii. + if (callFunction(std_Object_hasOwnProperty, format, property)) + formatProp = format[property]; + + if (optionsProp === undefined && formatProp !== undefined) { + // Step 11.c.iv. + score -= additionPenalty; + } else if (optionsProp !== undefined && formatProp === undefined) { + // Step 11.c.v. + score -= removalPenalty; + } else { + // Step 11.c.vi. + var optionsPropIndex = callFunction(ArrayIndexOf, values, optionsProp); + var formatPropIndex = callFunction(ArrayIndexOf, values, formatProp); + var delta = std_Math_max(std_Math_min(formatPropIndex - optionsPropIndex, 2), -2); + if (delta === 2) + score -= longMorePenalty; + else if (delta === 1) + score -= shortMorePenalty; + else if (delta === -1) + score -= shortLessPenalty; + else if (delta === -2) + score -= longLessPenalty; + } + } + + // Step 11.d. + if (score > bestScore) { + bestScore = score; + bestFormat = format; + } + + // Step 11.e. + i++; + } + + // Step 12. + return bestFormat; +} + + +/** + * Compares the date and time components requested by options with the available + * date and time formats in formats, and selects the best match according + * to an unspecified best-fit matching algorithm. + * + * Spec: ECMAScript Internationalization API Specification, 12.1.1. + */ +function BestFitFormatMatcher(options, formats) { + // this implementation doesn't have anything better + return BasicFormatMatcher(options, formats); +} + + +/** + * 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 Internationalization API Specification, 12.2.2. + */ +function Intl_DateTimeFormat_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + var availableLocales = callFunction(dateTimeFormatInternalProperties.availableLocales, + dateTimeFormatInternalProperties); + var requestedLocales = CanonicalizeLocaleList(locales); + return SupportedLocales(availableLocales, requestedLocales, options); +} + + +/** + * DateTimeFormat internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 12.2.3. + */ +var dateTimeFormatInternalProperties = { + localeData: dateTimeFormatLocaleData, + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_DateTimeFormat_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: ["ca", "nu"] +}; + + +function dateTimeFormatLocaleData(locale) { + return { + ca: intl_availableCalendars(locale), + nu: getNumberingSystems(locale) + }; +} + + +/** + * Function to be bound and returned by Intl.DateTimeFormat.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + */ +function dateTimeFormatFormatToBind() { + // Steps 1.a.i-ii + var date = arguments.length > 0 ? arguments[0] : undefined; + var x = (date === undefined) ? std_Date_now() : ToNumber(date); + + // Step 1.a.iii. + return intl_FormatDateTime(this, x, false); +} + +/** + * Returns a function bound to this DateTimeFormat that returns a String value + * representing the result of calling ToNumber(date) according to the + * effective locale and the formatting options of this DateTimeFormat. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.2. + */ +function Intl_DateTimeFormat_format_get() { + // Check "this DateTimeFormat object" per introduction of section 12.3. + var internals = getDateTimeFormatInternals(this, "format"); + + // Step 1. + if (internals.boundFormat === undefined) { + // Step 1.a. + var F = dateTimeFormatFormatToBind; + + // Step 1.b-d. + var bf = callFunction(FunctionBind, F, this); + internals.boundFormat = bf; + } + + // Step 2. + return internals.boundFormat; +} + + +function Intl_DateTimeFormat_formatToParts() { + // Check "this DateTimeFormat object" per introduction of section 12.3. + getDateTimeFormatInternals(this, "formatToParts"); + + // Steps 1.a.i-ii + var date = arguments.length > 0 ? arguments[0] : undefined; + var x = (date === undefined) ? std_Date_now() : ToNumber(date); + + // Step 1.a.iii. + return intl_FormatDateTime(this, x, true); +} + + +/** + * Returns the resolved options for a DateTimeFormat object. + * + * Spec: ECMAScript Internationalization API Specification, 12.3.3 and 12.4. + */ +function Intl_DateTimeFormat_resolvedOptions() { + // Check "this DateTimeFormat object" per introduction of section 12.3. + var internals = getDateTimeFormatInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + calendar: internals.calendar, + numberingSystem: internals.numberingSystem, + timeZone: internals.timeZone + }; + resolveICUPattern(internals.pattern, result); + return result; +} + + +// Table mapping ICU pattern characters back to the corresponding date-time +// components of DateTimeFormat. See +// http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table +var icuPatternCharToComponent = { + E: "weekday", + G: "era", + y: "year", + M: "month", + L: "month", + d: "day", + h: "hour", + H: "hour", + k: "hour", + K: "hour", + m: "minute", + s: "second", + z: "timeZoneName", + v: "timeZoneName", + V: "timeZoneName" +}; + + +/** + * Maps an ICU pattern string to a corresponding set of date-time components + * and their values, and adds properties for these components to the result + * object, which will be returned by the resolvedOptions method. For the + * interpretation of ICU pattern characters, see + * http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + */ +function resolveICUPattern(pattern, result) { + assert(IsObject(result), "resolveICUPattern"); + var i = 0; + while (i < pattern.length) { + var c = pattern[i++]; + if (c === "'") { + while (i < pattern.length && pattern[i] !== "'") + i++; + i++; + } else { + var count = 1; + while (i < pattern.length && pattern[i] === c) { + i++; + count++; + } + var value; + switch (c) { + // "text" cases + case "G": + case "E": + case "z": + case "v": + case "V": + if (count <= 3) + value = "short"; + else if (count === 4) + value = "long"; + else + value = "narrow"; + break; + // "number" cases + case "y": + case "d": + case "h": + case "H": + case "m": + case "s": + case "k": + case "K": + if (count === 2) + value = "2-digit"; + else + value = "numeric"; + break; + // "text & number" cases + case "M": + case "L": + if (count === 1) + value = "numeric"; + else if (count === 2) + value = "2-digit"; + else if (count === 3) + value = "short"; + else if (count === 4) + value = "long"; + else + value = "narrow"; + break; + default: + // skip other pattern characters and literal text + } + if (callFunction(std_Object_hasOwnProperty, icuPatternCharToComponent, c)) + _DefineDataProperty(result, icuPatternCharToComponent[c], value); + if (c === "h" || c === "K") + _DefineDataProperty(result, "hour12", true); + else if (c === "H" || c === "k") + _DefineDataProperty(result, "hour12", false); + } + } +} + diff --git a/js/src/builtin/intl/ICUHeader.h b/js/src/builtin/intl/ICUHeader.h new file mode 100644 index 0000000000..08fe2c1719 --- /dev/null +++ b/js/src/builtin/intl/ICUHeader.h @@ -0,0 +1,50 @@ +/* -*- 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" + +/** + * Cast char16_t* strings to UChar* strings used by ICU. + */ +inline const UChar* +Char16ToUChar(const char16_t* chars) +{ + return reinterpret_cast(chars); +} + +inline UChar* +Char16ToUChar(char16_t* chars) +{ + return reinterpret_cast(chars); +} + +inline char16_t* +UCharToChar16(UChar* chars) +{ + return reinterpret_cast(chars); +} + +inline const char16_t* +UCharToChar16(const UChar* chars) +{ + return reinterpret_cast(chars); +} + +#endif /* builtin_intl_ICUHeader_h */ diff --git a/js/src/builtin/intl/IntlObject.cpp b/js/src/builtin/intl/IntlObject.cpp new file mode 100644 index 0000000000..6bb57adf41 --- /dev/null +++ b/js/src/builtin/intl/IntlObject.cpp @@ -0,0 +1,512 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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 object and its non-constructor properties. */ + +#include "builtin/intl/IntlObject.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Likely.h" +#include "mozilla/Range.h" + +#include "jsapi.h" +#include "jscntxt.h" +#include "jsobj.h" + +#include "builtin/intl/Collator.h" +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/DateTimeFormat.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/NumberFormat.h" +#include "builtin/intl/PluralRules.h" +#include "builtin/intl/RelativeTimeFormat.h" +#include "builtin/intl/ScopedICUObject.h" +#include "vm/GlobalObject.h" + +#include "jsobjinlines.h" + +using namespace js; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; + +/******************** Intl ********************/ + +bool +js::intl_GetCalendarInfo(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 1); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + UErrorCode status = U_ZERO_ERROR; + const UChar* uTimeZone = nullptr; + int32_t uTimeZoneLength = 0; + UCalendar* cal = ucal_open(uTimeZone, uTimeZoneLength, locale.ptr(), UCAL_DEFAULT, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(cal); + + RootedObject info(cx, NewBuiltinClassInstance(cx)); + if (!info) + return false; + + RootedValue v(cx); + int32_t firstDayOfWeek = ucal_getAttribute(cal, UCAL_FIRST_DAY_OF_WEEK); + v.setInt32(firstDayOfWeek); + + if (!DefineProperty(cx, info, cx->names().firstDayOfWeek, v)) + return false; + + int32_t minDays = ucal_getAttribute(cal, UCAL_MINIMAL_DAYS_IN_FIRST_WEEK); + v.setInt32(minDays); + if (!DefineProperty(cx, info, cx->names().minDays, v)) + return false; + + UCalendarWeekdayType prevDayType = ucal_getDayOfWeekType(cal, UCAL_SATURDAY, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + RootedValue weekendStart(cx), weekendEnd(cx); + + for (int i = UCAL_SUNDAY; i <= UCAL_SATURDAY; i++) { + UCalendarDaysOfWeek dayOfWeek = static_cast(i); + UCalendarWeekdayType type = ucal_getDayOfWeekType(cal, dayOfWeek, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + if (prevDayType != type) { + switch (type) { + case UCAL_WEEKDAY: + // If the first Weekday after Weekend is Sunday (1), + // then the last Weekend day is Saturday (7). + // Otherwise we'll just take the previous days number. + weekendEnd.setInt32(i == 1 ? 7 : i - 1); + break; + case UCAL_WEEKEND: + weekendStart.setInt32(i); + break; + case UCAL_WEEKEND_ONSET: + case UCAL_WEEKEND_CEASE: + // At the time this code was added, ICU apparently never behaves this way, + // so just throw, so that users will report a bug and we can decide what to + // do. + intl::ReportInternalError(cx); + return false; + default: + break; + } + } + + prevDayType = type; + } + + MOZ_ASSERT(weekendStart.isInt32()); + MOZ_ASSERT(weekendEnd.isInt32()); + + if (!DefineProperty(cx, info, cx->names().weekendStart, weekendStart)) + return false; + + if (!DefineProperty(cx, info, cx->names().weekendEnd, weekendEnd)) + return false; + + args.rval().setObject(*info); + return true; +} + +template +inline bool +MatchPart(const char** pattern, const char (&part)[N]) +{ + if (strncmp(*pattern, part, N - 1)) + return false; + + *pattern += N - 1; + return true; +} + +bool +js::intl_ComputeDisplayNames(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + // 1. Assert: locale is a string. + MOZ_ASSERT(args[0].isString()); + // 2. Assert: style is a string. + MOZ_ASSERT(args[1].isString()); + // 3. Assert: keys is an Array. + MOZ_ASSERT(args[2].isObject()); + + JSAutoByteString locale(cx, args[0].toString()); + if (!locale) + return false; + + JSAutoByteString style(cx, args[1].toString()); + if (!style) + return false; + + RootedArrayObject keys(cx, &args[2].toObject().as()); + if (!keys) + return false; + + // 4. Let result be ArrayCreate(0). + RootedArrayObject result(cx, NewDenseUnallocatedArray(cx, keys->length())); + if (!result) + return false; + + UErrorCode status = U_ZERO_ERROR; + + UDateFormat* fmt = + udat_open(UDAT_DEFAULT, UDAT_DEFAULT, IcuLocale(locale.ptr()), + nullptr, 0, nullptr, 0, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject datToClose(fmt); + + // UDateTimePatternGenerator will be needed for translations of date and + // time fields like "month", "week", "day" etc. + UDateTimePatternGenerator* dtpg = udatpg_open(IcuLocale(locale.ptr()), &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject datPgToClose(dtpg); + + RootedValue keyValue(cx); + RootedString keyValStr(cx); + RootedValue wordVal(cx); + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + + // 5. For each element of keys, + for (uint32_t i = 0; i < keys->length(); i++) { + /** + * We iterate over keys array looking for paths that we have code + * branches for. + * + * For any unknown path branch, the wordVal will keep NullValue and + * we'll throw at the end. + */ + + if (!GetElement(cx, keys, keys, i, &keyValue)) + return false; + + JSAutoByteString pattern; + keyValStr = keyValue.toString(); + if (!pattern.encodeUtf8(cx, keyValStr)) + return false; + + wordVal.setNull(); + + // 5.a. Perform an implementation dependent algorithm to map a key to a + // corresponding display name. + const char* pat = pattern.ptr(); + + if (!MatchPart(&pat, "dates")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (MatchPart(&pat, "fields")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + UDateTimePatternField fieldType; + + if (MatchPart(&pat, "year")) { + fieldType = UDATPG_YEAR_FIELD; + } else if (MatchPart(&pat, "month")) { + fieldType = UDATPG_MONTH_FIELD; + } else if (MatchPart(&pat, "week")) { + fieldType = UDATPG_WEEK_OF_YEAR_FIELD; + } else if (MatchPart(&pat, "day")) { + fieldType = UDATPG_DAY_FIELD; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + // This part must be the final part with no trailing data. + if (*pat != '\0') { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + int32_t resultSize; + + const UChar* value = udatpg_getAppendItemName(dtpg, fieldType, &resultSize); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + JSString* word = NewStringCopyN(cx, UCharToChar16(value), resultSize); + if (!word) + return false; + + wordVal.setString(word); + } else if (MatchPart(&pat, "gregorian")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + UDateFormatSymbolType symbolType; + int32_t index; + + if (MatchPart(&pat, "months")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (StringsAreEqual(style, "narrow")) { + symbolType = UDAT_STANDALONE_NARROW_MONTHS; + } else if (StringsAreEqual(style, "short")) { + symbolType = UDAT_STANDALONE_SHORT_MONTHS; + } else { + MOZ_ASSERT(StringsAreEqual(style, "long")); + symbolType = UDAT_STANDALONE_MONTHS; + } + + if (MatchPart(&pat, "january")) { + index = UCAL_JANUARY; + } else if (MatchPart(&pat, "february")) { + index = UCAL_FEBRUARY; + } else if (MatchPart(&pat, "march")) { + index = UCAL_MARCH; + } else if (MatchPart(&pat, "april")) { + index = UCAL_APRIL; + } else if (MatchPart(&pat, "may")) { + index = UCAL_MAY; + } else if (MatchPart(&pat, "june")) { + index = UCAL_JUNE; + } else if (MatchPart(&pat, "july")) { + index = UCAL_JULY; + } else if (MatchPart(&pat, "august")) { + index = UCAL_AUGUST; + } else if (MatchPart(&pat, "september")) { + index = UCAL_SEPTEMBER; + } else if (MatchPart(&pat, "october")) { + index = UCAL_OCTOBER; + } else if (MatchPart(&pat, "november")) { + index = UCAL_NOVEMBER; + } else if (MatchPart(&pat, "december")) { + index = UCAL_DECEMBER; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else if (MatchPart(&pat, "weekdays")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + if (StringsAreEqual(style, "narrow")) { + symbolType = UDAT_STANDALONE_NARROW_WEEKDAYS; + } else if (StringsAreEqual(style, "short")) { + symbolType = UDAT_STANDALONE_SHORT_WEEKDAYS; + } else { + MOZ_ASSERT(StringsAreEqual(style, "long")); + symbolType = UDAT_STANDALONE_WEEKDAYS; + } + + if (MatchPart(&pat, "monday")) { + index = UCAL_MONDAY; + } else if (MatchPart(&pat, "tuesday")) { + index = UCAL_TUESDAY; + } else if (MatchPart(&pat, "wednesday")) { + index = UCAL_WEDNESDAY; + } else if (MatchPart(&pat, "thursday")) { + index = UCAL_THURSDAY; + } else if (MatchPart(&pat, "friday")) { + index = UCAL_FRIDAY; + } else if (MatchPart(&pat, "saturday")) { + index = UCAL_SATURDAY; + } else if (MatchPart(&pat, "sunday")) { + index = UCAL_SUNDAY; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else if (MatchPart(&pat, "dayperiods")) { + if (!MatchPart(&pat, "/")) { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + symbolType = UDAT_AM_PMS; + + if (MatchPart(&pat, "am")) { + index = UCAL_AM; + } else if (MatchPart(&pat, "pm")) { + index = UCAL_PM; + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + // This part must be the final part with no trailing data. + if (*pat != '\0') { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + JSString* word = CallICU(cx, [fmt, symbolType, index](UChar* chars, int32_t size, UErrorCode* status) { + return udat_getSymbols(fmt, symbolType, index, chars, size, status); + }); + if (!word) + return false; + + wordVal.setString(word); + } else { + JS_ReportErrorNumberUTF8(cx, GetErrorMessage, nullptr, JSMSG_INVALID_KEY, pattern.ptr()); + return false; + } + + MOZ_ASSERT(wordVal.isString()); + + // 5.b. Append the result string to result. + if (!DefineElement(cx, result, i, wordVal)) + return false; + } + + // 6. Return result. + args.rval().setObject(*result); + return true; +} + +const Class js::IntlClass = { + js_Object_str, + JSCLASS_HAS_CACHED_PROTO(JSProto_Intl) +}; + +#if JS_HAS_TOSOURCE +static bool +intl_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().Intl); + return true; +} +#endif + +static const JSFunctionSpec intl_static_methods[] = { +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, intl_toSource, 0, 0), +#endif + JS_SELF_HOSTED_FN("getCanonicalLocales", "Intl_getCanonicalLocales", 1, 0), + JS_FS_END +}; + +/** + * Initializes the Intl Object and its standard built-in properties. + * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 + */ +/* static */ bool +GlobalObject::initIntlObject(JSContext* cx, Handle global) +{ + RootedObject proto(cx, GlobalObject::getOrCreateObjectPrototype(cx, global)); + if (!proto) + return false; + + // The |Intl| object is just a plain object with some "static" function + // properties and some constructor properties. + RootedObject intl(cx, NewObjectWithGivenProto(cx, &IntlClass, proto, SingletonObject)); + if (!intl) + return false; + + // Add the static functions. + if (!JS_DefineFunctions(cx, intl, intl_static_methods)) + return false; + + // Add the constructor properties, computing and returning the relevant + // prototype objects needed below. + RootedObject collatorProto(cx, CreateCollatorPrototype(cx, intl, global)); + if (!collatorProto) + return false; + RootedObject dateTimeFormatProto(cx, CreateDateTimeFormatPrototype(cx, intl, global)); + if (!dateTimeFormatProto) + return false; + RootedObject numberFormatProto(cx, CreateNumberFormatPrototype(cx, intl, global)); + if (!numberFormatProto) + return false; + RootedObject pluralRulesProto(cx, CreatePluralRulesPrototype(cx, intl, global)); + if (!pluralRulesProto) + return false; + + RootedObject relativeTimeFmtProto(cx, CreateRelativeTimeFormatPrototype(cx, intl, global)); + if (!relativeTimeFmtProto) { + return false; + } + + // The |Intl| object is fully set up now, so define the global property. + RootedValue intlValue(cx, ObjectValue(*intl)); + if (!DefineProperty(cx, global, cx->names().Intl, intlValue, nullptr, nullptr, + JSPROP_RESOLVING)) + { + return false; + } + + // Now that the |Intl| object is successfully added, we can OOM-safely fill + // in all relevant reserved global slots. + + // Cache the various prototypes, for use in creating instances of these + // objects with the proper [[Prototype]] as "the original value of + // |Intl.Collator.prototype|" and similar. For builtin classes like + // |String.prototype| we have |JSProto_*| that enables + // |getPrototype(JSProto_*)|, but that has global-object-property-related + // baggage we don't need or want, so we use one-off reserved slots. + 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)); + global->setReservedSlot(RELATIVE_TIME_FORMAT_PROTO, ObjectValue(*relativeTimeFmtProto)); + + // Also cache |Intl| to implement spec language that conditions behavior + // based on values being equal to "the standard built-in |Intl| object". + // Use |setConstructor| to correspond with |JSProto_Intl|. + // + // XXX We should possibly do a one-off reserved slot like above. + global->setConstructor(JSProto_Intl, ObjectValue(*intl)); + return true; +} + +JSObject* +js::InitIntlClass(JSContext* cx, HandleObject obj) +{ + Handle global = obj.as(); + if (!GlobalObject::initIntlObject(cx, global)) + return nullptr; + + return &global->getConstructor(JSProto_Intl).toObject(); +} diff --git a/js/src/builtin/intl/IntlObject.h b/js/src/builtin/intl/IntlObject.h new file mode 100644 index 0000000000..1f0b26c545 --- /dev/null +++ b/js/src/builtin/intl/IntlObject.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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_IntlObject_h +#define builtin_intl_IntlObject_h + +#include "mozilla/Attributes.h" + +#include "js/RootingAPI.h" + +struct JSContext; +class JSObject; +namespace JS { class Value; } + +namespace js { + +/** + * Initializes the Intl Object and its standard built-in properties. + * Spec: ECMAScript Internationalization API Specification, 8.0, 8.1 + */ +extern JSObject* +InitIntlClass(JSContext* cx, JS::Handle obj); + +/* + * The following functions are for use by self-hosted code. + */ + +/** + * Returns a plain object with calendar information for a single valid locale + * (callers must perform this validation). The object will have these + * properties: + * + * firstDayOfWeek + * an integer in the range 1=Sunday to 7=Saturday indicating the day + * considered the first day of the week in calendars, e.g. 1 for en-US, + * 2 for en-GB, 1 for bn-IN + * minDays + * an integer in the range of 1 to 7 indicating the minimum number + * of days required in the first week of the year, e.g. 1 for en-US, 4 for de + * weekendStart + * an integer in the range 1=Sunday to 7=Saturday indicating the day + * considered the beginning of a weekend, e.g. 7 for en-US, 7 for en-GB, + * 1 for bn-IN + * weekendEnd + * an integer in the range 1=Sunday to 7=Saturday indicating the day + * considered the end of a weekend, e.g. 1 for en-US, 1 for en-GB, + * 1 for bn-IN (note that "weekend" is *not* necessarily two days) + * + * NOTE: "calendar" and "locale" properties are *not* added to the object. + */ +extern MOZ_MUST_USE bool +intl_GetCalendarInfo(JSContext* cx, unsigned argc, JS::Value* vp); + +/** + * Returns an Array with CLDR-based fields display names. + * The function takes three arguments: + * + * locale + * BCP47 compliant locale string + * style + * A string with values: long or short or narrow + * keys + * An array or path-like strings that identify keys to be returned + * At the moment the following types of keys are supported: + * + * 'dates/fields/{year|month|week|day}' + * 'dates/gregorian/months/{january|...|december}' + * 'dates/gregorian/weekdays/{sunday|...|saturday}' + * 'dates/gregorian/dayperiods/{am|pm}' + * + * Example: + * + * let info = intl_ComputeDisplayNames( + * 'en-US', + * 'long', + * [ + * 'dates/fields/year', + * 'dates/gregorian/months/january', + * 'dates/gregorian/weekdays/monday', + * 'dates/gregorian/dayperiods/am', + * ] + * ); + * + * Returned value: + * + * [ + * 'year', + * 'January', + * 'Monday', + * 'AM' + * ] + */ +extern MOZ_MUST_USE bool +intl_ComputeDisplayNames(JSContext* cx, unsigned argc, JS::Value* vp); + +} // namespace js + +#endif /* builtin_intl_IntlObject_h */ diff --git a/js/src/builtin/intl/IntlObject.js b/js/src/builtin/intl/IntlObject.js new file mode 100644 index 0000000000..826ad27ff0 --- /dev/null +++ b/js/src/builtin/intl/IntlObject.js @@ -0,0 +1,162 @@ +/* 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/. */ + +function Intl_getCanonicalLocales(locales) { + let codes = CanonicalizeLocaleList(locales); + let result = []; + + let len = codes.length; + let k = 0; + + while (k < len) { + _DefineDataProperty(result, k, codes[k]); + k++; + } + return result; +} + +function Intl_getCalendarInfo(locales) { + const requestedLocales = CanonicalizeLocaleList(locales); + + const DateTimeFormat = dateTimeFormatInternalProperties; + const localeData = DateTimeFormat.localeData; + + const localeOpt = new Record(); + localeOpt.localeMatcher = "best fit"; + + const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + const result = intl_GetCalendarInfo(r.locale); + result.calendar = r.ca; + result.locale = r.locale; + + return result; +} + +/** + * This function is a custom method designed after Intl API, but currently + * not part of the spec or spec proposal. + * We want to use it internally to retrieve translated values from CLDR in + * order to ensure they're aligned with what Intl API returns. + * + * This API may one day be a foundation for an ECMA402 API spec proposal. + * + * The function takes two arguments - locales which is a list of locale strings + * and options which is an object with two optional properties: + * + * keys: + * an Array of string values that are paths to individual terms + * + * style: + * a String with a value "long", "short" or "narrow" + * + * It returns an object with properties: + * + * locale: + * a negotiated locale string + * + * style: + * negotiated style + * + * values: + * A key-value pair list of requested keys and corresponding + * translated values + * + */ +function Intl_getDisplayNames(locales, options) { + // 1. Let requestLocales be ? CanonicalizeLocaleList(locales). + const requestedLocales = CanonicalizeLocaleList(locales); + + // 2. If options is undefined, then + if (options === undefined) + // a. Let options be ObjectCreate(%ObjectPrototype%). + options = {}; + // 3. Else, + else + // a. Let options be ? ToObject(options). + options = ToObject(options); + + const DateTimeFormat = dateTimeFormatInternalProperties; + + // 4. Let localeData be %DateTimeFormat%.[[localeData]]. + const localeData = DateTimeFormat.localeData; + + // 5. Let opt be a new Record. + const localeOpt = new Record(); + // 6. Set localeOpt.[[localeMatcher]] to "best fit". + localeOpt.localeMatcher = "best fit"; + + // 7. Let r be ResolveLocale(%DateTimeFormat%.[[availableLocales]], requestedLocales, localeOpt, + // %DateTimeFormat%.[[relevantExtensionKeys]], localeData). + const r = ResolveLocale(callFunction(DateTimeFormat.availableLocales, DateTimeFormat), + requestedLocales, + localeOpt, + DateTimeFormat.relevantExtensionKeys, + localeData); + + // 8. Let style be ? GetOption(options, "style", "string", « "long", "short", "narrow" », "long"). + const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); + // 9. Let keys be ? Get(options, "keys"). + let keys = options.keys; + + // 10. If keys is undefined, + if (keys === undefined) { + // a. Let keys be ArrayCreate(0). + keys = []; + } else if (!IsObject(keys)) { + // 11. Else, + // a. If Type(keys) is not Object, throw a TypeError exception. + ThrowTypeError(JSMSG_INVALID_KEYS_TYPE); + } + + // 12. Let processedKeys be ArrayCreate(0). + // (This really should be a List, but we use an Array here in order that + // |intl_ComputeDisplayNames| may infallibly access the list's length via + // |ArrayObject::length|.) + let processedKeys = []; + // 13. Let len be ? ToLength(? Get(keys, "length")). + let len = ToLength(keys.length); + // 14. Let i be 0. + // 15. Repeat, while i < len + for (let i = 0; i < len; i++) { + // a. Let processedKey be ? ToString(? Get(keys, i)). + // b. Perform ? CreateDataPropertyOrThrow(processedKeys, i, processedKey). + callFunction(std_Array_push, processedKeys, ToString(keys[i])); + } + + // 16. Let names be ? ComputeDisplayNames(r.[[locale]], style, processedKeys). + const names = intl_ComputeDisplayNames(r.locale, style, processedKeys); + + // 17. Let values be ObjectCreate(%ObjectPrototype%). + const values = {}; + + // 18. Set i to 0. + // 19. Repeat, while i < len + for (let i = 0; i < len; i++) { + // a. Let key be ? Get(processedKeys, i). + const key = processedKeys[i]; + // b. Let name be ? Get(names, i). + const name = names[i]; + // c. Assert: Type(name) is string. + assert(typeof name === "string", "unexpected non-string value"); + // d. Assert: the length of name is greater than zero. + assert(name.length > 0, "empty string value"); + // e. Perform ? DefinePropertyOrThrow(values, key, name). + _DefineDataProperty(values, key, name); + } + + // 20. Let options be ObjectCreate(%ObjectPrototype%). + // 21. Perform ! DefinePropertyOrThrow(result, "locale", r.[[locale]]). + // 22. Perform ! DefinePropertyOrThrow(result, "style", style). + // 23. Perform ! DefinePropertyOrThrow(result, "values", values). + const result = { locale: r.locale, style, values }; + + // 24. Return result. + return result; + +} diff --git a/js/src/builtin/IntlData.js b/js/src/builtin/intl/LangTagMappingsGenerated.js similarity index 100% rename from js/src/builtin/IntlData.js rename to js/src/builtin/intl/LangTagMappingsGenerated.js diff --git a/js/src/builtin/intl/NumberFormat.cpp b/js/src/builtin/intl/NumberFormat.cpp new file mode 100644 index 0000000000..4fc08a0920 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.cpp @@ -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 +#include +#include + +#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(cx, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(NumberFormatObject::INTERNALS_SLOT, NullValue()); + obj->as().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().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (UNumberFormat* nf = static_cast(slot.toPrivate())) + unum_close(nf); + } +} + +JSObject* +js::CreateNumberFormatPrototype(JSContext* cx, HandleObject Intl, Handle 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 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(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()); + } + + 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 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 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; + 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 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(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().getReservedSlot(NumberFormatObject::UNUMBER_FORMAT_SLOT).toPrivate(); + nf = static_cast(priv); + if (!nf) { + nf = NewUNumberFormat(cx, numberFormat); + if (!nf) + return false; + numberFormat->as().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; +} + diff --git a/js/src/builtin/intl/NumberFormat.h b/js/src/builtin/intl/NumberFormat.h new file mode 100644 index 0000000000..37cf943972 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.h @@ -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 + +#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 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 */ \ No newline at end of file diff --git a/js/src/builtin/intl/NumberFormat.js b/js/src/builtin/intl/NumberFormat.js new file mode 100644 index 0000000000..2ef1661586 --- /dev/null +++ b/js/src/builtin/intl/NumberFormat.js @@ -0,0 +1,511 @@ +/* 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 **********/ + + +/** + * NumberFormat internal properties. + * + * Spec: ECMAScript Internationalization API Specification, 9.1 and 11.2.3. + */ +var numberFormatInternalProperties = { + localeData: numberFormatLocaleData, + _availableLocales: null, + availableLocales: function() + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_NumberFormat_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: ["nu"] +}; + + +/** + * Compute an internal properties object from |lazyNumberFormatData|. + */ +function resolveNumberFormatInternals(lazyNumberFormatData) { + assert(IsObject(lazyNumberFormatData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + // Step 3. + var requestedLocales = lazyNumberFormatData.requestedLocales; + + // Compute options that impact interpretation of locale. + // Step 6. + var opt = lazyNumberFormatData.opt; + + var NumberFormat = numberFormatInternalProperties; + + // Step 9. + var localeData = NumberFormat.localeData; + + // Step 10. + var r = ResolveLocale(callFunction(NumberFormat.availableLocales, NumberFormat), + lazyNumberFormatData.requestedLocales, + lazyNumberFormatData.opt, + NumberFormat.relevantExtensionKeys, + localeData); + + // Steps 11-12. (Step 13 is not relevant to our implementation.) + internalProps.locale = r.locale; + internalProps.numberingSystem = r.nu; + + // Compute formatting options. + // Step 15. + var s = lazyNumberFormatData.style; + internalProps.style = s; + + // Steps 19, 21. + if (s === "currency") { + internalProps.currency = lazyNumberFormatData.currency; + internalProps.currencyDisplay = lazyNumberFormatData.currencyDisplay; + } + + internalProps.minimumIntegerDigits = lazyNumberFormatData.minimumIntegerDigits; + + internalProps.minimumFractionDigits = lazyNumberFormatData.minimumFractionDigits; + + internalProps.maximumFractionDigits = lazyNumberFormatData.maximumFractionDigits; + + if ("minimumSignificantDigits" in lazyNumberFormatData) { + // Note: Intl.NumberFormat.prototype.resolvedOptions() exposes the + // actual presence (versus undefined-ness) of these properties. + assert("maximumSignificantDigits" in lazyNumberFormatData, "min/max sig digits mismatch"); + internalProps.minimumSignificantDigits = lazyNumberFormatData.minimumSignificantDigits; + internalProps.maximumSignificantDigits = lazyNumberFormatData.maximumSignificantDigits; + } + + // Step 27. + internalProps.useGrouping = lazyNumberFormatData.useGrouping; + + // Step 34. + internalProps.boundFormat = undefined; + + // The caller is responsible for associating |internalProps| with the right + // object using |setInternalProperties|. + return internalProps; +} + + +/** + * Returns an object containing the NumberFormat internal properties of |obj|, + * or throws a TypeError if |obj| isn't NumberFormat-initialized. + */ +function getNumberFormatInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "NumberFormat", methodName); + assert(internals.type === "NumberFormat", "bad type escaped getIntlObjectInternals"); + + // If internal properties have already been computed, use them. + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + // Otherwise it's time to fully create them. + internalProps = resolveNumberFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + 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; + } +} + +/** + * Convert s to upper case, but limited to characters a-z. + * + * Spec: ECMAScript Internationalization API Specification, 6.1. + */ +function toASCIIUpperCase(s) { + assert(typeof s === "string", "toASCIIUpperCase"); + + // String.prototype.toUpperCase may map non-ASCII characters into ASCII, + // so go character by character (actually code unit by code unit, but + // since we only care about ASCII characters here, that's OK). + var result = ""; + for (var i = 0; i < s.length; i++) { + var c = callFunction(std_String_charCodeAt, s, i); + result += (0x61 <= c && c <= 0x7A) + ? callFunction(std_String_fromCharCode, null, c & ~0x20) + : s[i]; + } + return result; +} + +/** + * Verifies that the given string is a well-formed ISO 4217 currency code. + * + * Spec: ECMAScript Internationalization API Specification, 6.3.1. + */ +function getIsWellFormedCurrencyCodeRE() { + return internalIntlRegExps.isWellFormedCurrencyCodeRE || + (internalIntlRegExps.isWellFormedCurrencyCodeRE = RegExpCreate("[^A-Z]")); +} + +function IsWellFormedCurrencyCode(currency) { + var c = ToString(currency); + var normalized = toASCIIUpperCase(c); + if (normalized.length !== 3) + return false; + return !regexp_test_no_statics(getIsWellFormedCurrencyCodeRE(), normalized); +} + +/** + * Initializes an object as a NumberFormat. + * + * 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 NumberFormat. + * This later work occurs in |resolveNumberFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.1. + */ +function InitializeNumberFormat(numberFormat, locales, options) { + assert(IsObject(numberFormat), "InitializeNumberFormat"); + + // Step 1. + if (isInitializedIntlObject(numberFormat)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + // Step 2. + var internals = initializeIntlObject(numberFormat); + + // Lazy NumberFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // style: "decimal" / "percent" / "currency", + // + // // fields present only if style === "currency": + // currency: a well-formed currency code (IsWellFormedCurrencyCode), + // currencyDisplay: "code" / "symbol" / "name", + // + // opt: // opt object computed in InitializeNumberFormat + // { + // 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], + // + // useGrouping: true / false, + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every NumberFormat lazy data object has *all* these properties, never a + // subset of them. + var lazyNumberFormatData = std_Object_create(null); + + // Step 3. + var requestedLocales = CanonicalizeLocaleList(locales); + lazyNumberFormatData.requestedLocales = requestedLocales; + + // Steps 4-5. + // + // If we ever need more speed here at startup, we should try to detect the + // case where |options === undefined| and Object.prototype hasn't been + // mucked with. (|options| is fully consumed in this method, so it's not a + // concern that Object.prototype might be touched between now and when + // |resolveNumberFormatInternals| is called.) For now just keep it simple. + if (options === undefined) + options = {}; + else + options = ToObject(options); + + // Compute options that impact interpretation of locale. + // Step 6. + var opt = new Record(); + lazyNumberFormatData.opt = opt; + + // Steps 7-8. + var matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + // Compute formatting options. + // Step 14. + var s = GetOption(options, "style", "string", ["decimal", "percent", "currency"], "decimal"); + lazyNumberFormatData.style = s; + + // Steps 16-19. + var c = GetOption(options, "currency", "string", undefined, undefined); + if (c !== undefined && !IsWellFormedCurrencyCode(c)) + ThrowRangeError(JSMSG_INVALID_CURRENCY_CODE, c); + var cDigits; + if (s === "currency") { + if (c === undefined) + ThrowTypeError(JSMSG_UNDEFINED_CURRENCY); + + // Steps 19.a-c. + c = toASCIIUpperCase(c); + lazyNumberFormatData.currency = c; + cDigits = CurrencyDigits(c); + } + + // Step 20. + var cd = GetOption(options, "currencyDisplay", "string", ["code", "symbol", "name"], "symbol"); + if (s === "currency") + lazyNumberFormatData.currencyDisplay = cd; + + // Steps 22-24. + SetNumberFormatDigitOptions(lazyNumberFormatData, options, s === "currency" ? cDigits: 0); + + // Step 25. + if (lazyNumberFormatData.maximumFractionDigits === undefined) { + let mxfdDefault = s === "currency" + ? cDigits + : s === "percent" + ? 0 + : 3; + lazyNumberFormatData.maximumFractionDigits = + std_Math_max(lazyNumberFormatData.minimumFractionDigits, mxfdDefault); + } + + // Step 26. + var g = GetOption(options, "useGrouping", "boolean", undefined, true); + lazyNumberFormatData.useGrouping = g; + + // Steps 35-36. + // + // We've done everything that must be done now: mark the lazy data as fully + // computed and install it. + setLazyData(internals, "NumberFormat", lazyNumberFormatData); +} + + +/** + * Mapping from currency codes to the number of decimal digits used for them. + * Default is 2 digits. + * + * Spec: ISO 4217 Currency and Funds Code List. + * http://www.currency-iso.org/en/home/tables/table-a1.html + */ +var currencyDigits = { + BHD: 3, + BIF: 0, + BYR: 0, + CLF: 4, + CLP: 0, + DJF: 0, + GNF: 0, + IQD: 3, + ISK: 0, + JOD: 3, + JPY: 0, + KMF: 0, + KRW: 0, + KWD: 3, + LYD: 3, + OMR: 3, + PYG: 0, + RWF: 0, + TND: 3, + UGX: 0, + UYI: 0, + VND: 0, + VUV: 0, + XAF: 0, + XOF: 0, + XPF: 0 +}; + + +/** + * Returns the number of decimal digits to be used for the given currency. + * + * Spec: ECMAScript Internationalization API Specification, 11.1.1. + */ +function getCurrencyDigitsRE() { + return internalIntlRegExps.currencyDigitsRE || + (internalIntlRegExps.currencyDigitsRE = RegExpCreate("^[A-Z]{3}$")); +} +function CurrencyDigits(currency) { + assert(typeof currency === "string", "CurrencyDigits"); + assert(regexp_test_no_statics(getCurrencyDigitsRE(), currency), "CurrencyDigits"); + + if (callFunction(std_Object_hasOwnProperty, currencyDigits, currency)) + return currencyDigits[currency]; + return 2; +} + + +/** + * 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 Internationalization API Specification, 11.2.2. + */ +function Intl_NumberFormat_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + var availableLocales = callFunction(numberFormatInternalProperties.availableLocales, + numberFormatInternalProperties); + var requestedLocales = CanonicalizeLocaleList(locales); + return SupportedLocales(availableLocales, requestedLocales, options); +} + + +function getNumberingSystems(locale) { + // ICU doesn't have an API to determine the set of numbering systems + // supported for a locale; it generally pretends that any numbering system + // can be used with any locale. Supporting a decimal numbering system + // (where only the digits are replaced) is easy, so we offer them all here. + // Algorithmic numbering systems are typically tied to one locale, so for + // lack of information we don't offer them. To increase chances that + // other software will process output correctly, we further restrict to + // those decimal numbering systems explicitly listed in table 2 of + // the ECMAScript Internationalization API Specification, 11.3.2, which + // in turn are those with full specifications in version 21 of Unicode + // Technical Standard #35 using digits that were defined in Unicode 5.0, + // the Unicode version supported in Windows Vista. + // The one thing we can find out from ICU is the default numbering system + // for a locale. + var defaultNumberingSystem = intl_numberingSystem(locale); + return [ + defaultNumberingSystem, + "arab", "arabext", "bali", "beng", "deva", + "fullwide", "gujr", "guru", "hanidec", "khmr", + "knda", "laoo", "latn", "limb", "mlym", + "mong", "mymr", "orya", "tamldec", "telu", + "thai", "tibt" + ]; +} + + +function numberFormatLocaleData(locale) { + return { + nu: getNumberingSystems(locale) + }; +} + + +/** + * Function to be bound and returned by Intl.NumberFormat.prototype.format. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + */ +function numberFormatFormatToBind(value) { + // Steps 1.a.i implemented by ECMAScript declaration binding instantiation, + // ES5.1 10.5, step 4.d.ii. + + // Step 1.a.ii-iii. + var x = ToNumber(value); + return intl_FormatNumber(this, x, /* formatToParts = */ false); +} + + +/** + * Returns a function bound to this NumberFormat that returns a String value + * representing the result of calling ToNumber(value) according to the + * effective locale and the formatting options of this NumberFormat. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.2. + */ +function Intl_NumberFormat_format_get() { + // Check "this NumberFormat object" per introduction of section 11.3. + var internals = getNumberFormatInternals(this, "format"); + + // Step 1. + if (internals.boundFormat === undefined) { + // Step 1.a. + var F = numberFormatFormatToBind; + + // Step 1.b-d. + var bf = callFunction(FunctionBind, F, this); + internals.boundFormat = bf; + } + // Step 2. + return internals.boundFormat; +} + +function Intl_NumberFormat_formatToParts(value) { + // Step 1. + var nf = this; + + // Steps 2-3. + getNumberFormatInternals(nf, "formatToParts"); + + // Step 4. + var x = ToNumber(value); + + // Step 5. + return intl_FormatNumber(nf, x, /* formatToParts = */ true); +} + +/** + * Returns the resolved options for a NumberFormat object. + * + * Spec: ECMAScript Internationalization API Specification, 11.3.3 and 11.4. + */ +function Intl_NumberFormat_resolvedOptions() { + // Check "this NumberFormat object" per introduction of section 11.3. + var internals = getNumberFormatInternals(this, "resolvedOptions"); + + var result = { + locale: internals.locale, + numberingSystem: internals.numberingSystem, + style: internals.style, + minimumIntegerDigits: internals.minimumIntegerDigits, + minimumFractionDigits: internals.minimumFractionDigits, + maximumFractionDigits: internals.maximumFractionDigits, + useGrouping: internals.useGrouping + }; + var optionalProperties = [ + "currency", + "currencyDisplay", + "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; +} + + diff --git a/js/src/builtin/intl/PluralRules.cpp b/js/src/builtin/intl/PluralRules.cpp new file mode 100644 index 0000000000..78a2f3a847 --- /dev/null +++ b/js/src/builtin/intl/PluralRules.cpp @@ -0,0 +1,473 @@ +/* -*- 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/. */ + +/* Implementation of the Intl.PluralRules proposal. */ + +#include "builtin/intl/PluralRules.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "vm/GlobalObject.h" +#include "vm/String.h" + +#include "jsobjinlines.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::AssertedCast; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::INITIAL_CHAR_BUFFER_SIZE; +using js::intl::StringsAreEqual; + +/**************** PluralRules *****************/ + +const ClassOps PluralRulesObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* setProperty */ + nullptr, /* enumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + PluralRulesObject::finalize +}; + +const Class PluralRulesObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(PluralRulesObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &PluralRulesObject::classOps_ +}; + +#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, proto); + if (!obj) + return false; + + obj->as().setReservedSlot(PluralRulesObject::INTERNALS_SLOT, NullValue()); + obj->as().setReservedSlot(PluralRulesObject::UPLURAL_RULES_SLOT, PrivateValue(nullptr)); + } + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + if (!intl::InitializeObject(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); +} + +void +js::PluralRulesObject::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(PluralRulesObject::UPLURAL_RULES_SLOT); + if (!slot.isUndefined()) { + if (UPluralRules* pr = static_cast(slot.toPrivate())) + uplrules_close(pr); + } +} + +JSObject* +js::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, &PluralRulesObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(PluralRulesObject::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 (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + if (!intl::InitializeObject(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 (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + return false; + args.rval().set(result); + 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, intl::GetInternalsObject(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)) { + intl::ReportInternalError(cx); + 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(); +} + +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, intl::GetInternalsObject(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)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject closeUFormattable(fmt); + + double y = ufmt_getDouble(fmt, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + UPluralType category; + + if (StringsAreEqual(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(StringsAreEqual(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType(IcuLocale(locale.ptr()), category, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + ScopedICUObject closePluralRules(pr); + + JSString* str = CallICU(cx, [pr, y](UChar* chars, int32_t size, UErrorCode* status) { + return uplrules_select(pr, y, 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() == 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 (StringsAreEqual(type, "cardinal")) { + category = UPLURAL_TYPE_CARDINAL; + } else { + MOZ_ASSERT(StringsAreEqual(type, "ordinal")); + category = UPLURAL_TYPE_ORDINAL; + } + + UPluralRules* pr = uplrules_openForType( + IcuLocale(locale.ptr()), + category, + &status + ); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + 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)) { + intl::ReportInternalError(cx); + 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)) { + intl::ReportInternalError(cx); + 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; +} diff --git a/js/src/builtin/intl/PluralRules.h b/js/src/builtin/intl/PluralRules.h new file mode 100644 index 0000000000..7213786766 --- /dev/null +++ b/js/src/builtin/intl/PluralRules.h @@ -0,0 +1,91 @@ +/* -*- 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_PluralRules_h +#define builtin_intl_PluralRules_h + +#include "mozilla/Attributes.h" + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "js/RootingAPI.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; + +class PluralRulesObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t UPLURAL_RULES_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* +CreatePluralRulesPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * 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); + +} // namespace js + +#endif /* builtin_intl_PluralRules_h */ \ No newline at end of file diff --git a/js/src/builtin/intl/PluralRules.js b/js/src/builtin/intl/PluralRules.js new file mode 100644 index 0000000000..1fac1c9352 --- /dev/null +++ b/js/src/builtin/intl/PluralRules.js @@ -0,0 +1,231 @@ +/* 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.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; +} + diff --git a/js/src/builtin/intl/RelativeTimeFormat.cpp b/js/src/builtin/intl/RelativeTimeFormat.cpp new file mode 100644 index 0000000000..101d1eb935 --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.cpp @@ -0,0 +1,321 @@ +/* -*- 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/. */ + +/* Implementation of the Intl.RelativeTimeFormat proposal. */ + +#include "builtin/intl/RelativeTimeFormat.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Casting.h" + +#include "jscntxt.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "vm/GlobalObject.h" + +#include "vm/NativeObject-inl.h" + +using namespace js; + +using mozilla::IsNegativeZero; +using mozilla::Range; +using mozilla::RangedPtr; + +using js::intl::CallICU; +using js::intl::GetAvailableLocales; +using js::intl::IcuLocale; +using js::intl::StringsAreEqual; + +/**************** RelativeTimeFormat *****************/ + +const ClassOps RelativeTimeFormatObject::classOps_ = { + nullptr, /* addProperty */ + nullptr, /* delProperty */ + nullptr, /* getProperty */ + nullptr, /* enumerate */ + nullptr, /* newEnumerate */ + nullptr, /* resolve */ + nullptr, /* mayResolve */ + RelativeTimeFormatObject::finalize +}; + +const Class RelativeTimeFormatObject::class_ = { + js_Object_str, + JSCLASS_HAS_RESERVED_SLOTS(RelativeTimeFormatObject::SLOT_COUNT) | + JSCLASS_FOREGROUND_FINALIZE, + &RelativeTimeFormatObject::classOps_ +}; + +#if JS_HAS_TOSOURCE +static bool +relativeTimeFormat_toSource(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + args.rval().setString(cx->names().RelativeTimeFormat); + return true; +} +#endif + +static const JSFunctionSpec relativeTimeFormat_static_methods[] = { + JS_SELF_HOSTED_FN("supportedLocalesOf", "Intl_RelativeTimeFormat_supportedLocalesOf", 1, 0), + JS_FS_END +}; + +static const JSFunctionSpec relativeTimeFormat_methods[] = { + JS_SELF_HOSTED_FN("resolvedOptions", "Intl_RelativeTimeFormat_resolvedOptions", 0, 0), + JS_SELF_HOSTED_FN("format", "Intl_RelativeTimeFormat_format", 2, 0), +#if JS_HAS_TOSOURCE + JS_FN(js_toSource_str, relativeTimeFormat_toSource, 0, 0), +#endif + JS_FS_END +}; + +static const JSPropertySpec relativeTimeFormat_properties[] = { + JS_STRING_SYM_PS(toStringTag, "Intl.RelativeTimeFormat", JSPROP_READONLY), + JS_PS_END}; + +/** + * RelativeTimeFormat constructor. + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1 + */ +static bool +RelativeTimeFormat(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + + // Step 1. + if (!ThrowIfNotConstructing(cx, args, "Intl.RelativeTimeFormat")) + return false; + + // Step 2 (Inlined 9.1.14, OrdinaryCreateFromConstructor). + RootedObject proto(cx); + if (args.isConstructing() && !GetPrototypeFromCallableConstructor(cx, args, &proto)) + return false; + + if (!proto) { + proto = GlobalObject::getOrCreateRelativeTimeFormatPrototype(cx, cx->global()); + if (!proto) + return false; + } + + RootedObject relativeTimeFormat(cx); + relativeTimeFormat = NewObjectWithGivenProto(cx, proto); + if (!relativeTimeFormat) + return false; + + relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::INTERNALS_SLOT, NullValue()); + relativeTimeFormat->as().setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); + + RootedValue locales(cx, args.get(0)); + RootedValue options(cx, args.get(1)); + + // Step 3. + if (!intl::InitializeObject(cx, relativeTimeFormat, cx->names().InitializeRelativeTimeFormat, locales, options)) + return false; + + args.rval().setObject(*relativeTimeFormat); + return true; +} + +void +js::RelativeTimeFormatObject::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(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT); + if (!slot.isUndefined()) { + if (URelativeDateTimeFormatter* rtf = static_cast(slot.toPrivate())) + ureldatefmt_close(rtf); + } +} + +JSObject* +js::CreateRelativeTimeFormatPrototype(JSContext* cx, HandleObject Intl, Handle global) +{ + RootedFunction ctor(cx); + ctor = global->createConstructor(cx, &RelativeTimeFormat, cx->names().RelativeTimeFormat, 0); + if (!ctor) + return nullptr; + + RootedNativeObject proto(cx, GlobalObject::createBlankPrototype(cx, global, &RelativeTimeFormatObject::class_)); + if (!proto) + return nullptr; + proto->setReservedSlot(RelativeTimeFormatObject::URELATIVE_TIME_FORMAT_SLOT, PrivateValue(nullptr)); + + if (!LinkConstructorAndPrototype(cx, ctor, proto)) + return nullptr; + + if (!JS_DefineFunctions(cx, ctor, relativeTimeFormat_static_methods)) + return nullptr; + + if (!JS_DefineFunctions(cx, proto, relativeTimeFormat_methods)) + return nullptr; + + if (!JS_DefineProperties(cx, proto, relativeTimeFormat_properties)) + return nullptr; + + RootedValue options(cx); + if (!intl::CreateDefaultOptions(cx, &options)) + return nullptr; + + if (!intl::InitializeObject(cx, proto, cx->names().InitializeRelativeTimeFormat, UndefinedHandleValue, + options)) + { + return nullptr; + } + + RootedValue ctorValue(cx, ObjectValue(*ctor)); + if (!DefineProperty(cx, Intl, cx->names().RelativeTimeFormat, ctorValue, nullptr, nullptr, 0)) { + return nullptr; + } + + return proto; +} + +bool +js::intl_RelativeTimeFormat_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 (!GetAvailableLocales(cx, uloc_countAvailable, uloc_getAvailable, &result)) + return false; + args.rval().set(result); + return true; +} + + +enum class RelativeTimeNumeric +{ + /** + * Only strings with numeric components like `1 day ago`. + */ + Always, + /** + * Natural-language strings like `yesterday` when possible, + * otherwise strings with numeric components as in `7 months ago`. + */ + Auto, +}; + +bool +js::intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp) +{ + CallArgs args = CallArgsFromVp(argc, vp); + MOZ_ASSERT(args.length() == 3); + + RootedObject relativeTimeFormat(cx, &args[0].toObject()); + + RootedObject internals(cx, intl::GetInternalsObject(cx, relativeTimeFormat)); + 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().style, &value)) + return false; + RootedLinearString style(cx, value.toString()->ensureLinear(cx)); + if (!style) + return false; + + double t = args[1].toNumber(); + + UDateRelativeDateTimeFormatterStyle relDateTimeStyle; + + if (StringEqualsAscii(style, "short")) { + relDateTimeStyle = UDAT_STYLE_SHORT; + } else if (StringEqualsAscii(style, "narrow")) { + relDateTimeStyle = UDAT_STYLE_NARROW; + } else { + MOZ_ASSERT(StringEqualsAscii(style, "long")); + relDateTimeStyle = UDAT_STYLE_LONG; + } + + URelativeDateTimeUnit relDateTimeUnit; + { + JSLinearString* unit = args[2].toString()->ensureLinear(cx); + if (!unit) { + return false; + } + + if (StringEqualsAscii(unit, "second") || StringEqualsAscii(unit, "seconds")) { + relDateTimeUnit = UDAT_REL_UNIT_SECOND; + } else if (StringEqualsAscii(unit, "minute") || StringEqualsAscii(unit, "minutes")) { + relDateTimeUnit = UDAT_REL_UNIT_MINUTE; + } else if (StringEqualsAscii(unit, "hour") || StringEqualsAscii(unit, "hours")) { + relDateTimeUnit = UDAT_REL_UNIT_HOUR; + } else if (StringEqualsAscii(unit, "day") || StringEqualsAscii(unit, "days")) { + relDateTimeUnit = UDAT_REL_UNIT_DAY; + } else if (StringEqualsAscii(unit, "week") || StringEqualsAscii(unit, "weeks")) { + relDateTimeUnit = UDAT_REL_UNIT_WEEK; + } else if (StringEqualsAscii(unit, "month") || StringEqualsAscii(unit, "months")) { + relDateTimeUnit = UDAT_REL_UNIT_MONTH; + } else if (StringEqualsAscii(unit, "quarter") || StringEqualsAscii(unit, "quarters")) { + relDateTimeUnit = UDAT_REL_UNIT_QUARTER; + } else { + MOZ_ASSERT(StringEqualsAscii(unit, "year") || StringEqualsAscii(unit, "years")); + relDateTimeUnit = UDAT_REL_UNIT_YEAR; + } + } + + if (!GetProperty(cx, internals, internals, cx->names().numeric, &value)) + return false; + RootedLinearString numeric(cx, value.toString()->ensureLinear(cx)); + if (!numeric) + return false; + + RelativeTimeNumeric relDateTimeNumeric; + + if (StringEqualsAscii(numeric, "auto")) { + relDateTimeNumeric = RelativeTimeNumeric::Auto; + } else { + MOZ_ASSERT(StringEqualsAscii(numeric, "always")); + relDateTimeNumeric = RelativeTimeNumeric::Always; + } + + Vector chars(cx); + if (!chars.resize(INITIAL_CHAR_BUFFER_SIZE)) + return false; + UErrorCode status = U_ZERO_ERROR; + URelativeDateTimeFormatter* rtf = + ureldatefmt_open(IcuLocale(locale.ptr()), nullptr, relDateTimeStyle, + UDISPCTX_CAPITALIZATION_FOR_STANDALONE, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject closeRelativeTimeFormat(rtf); + + JSString* str = + CallICU(cx, [rtf, t, relDateTimeUnit, relDateTimeNumeric](UChar* chars, int32_t size, + UErrorCode* status) + { + auto fmt = relDateTimeNumeric == RelativeTimeNumeric::Auto + ? ureldatefmt_format + : ureldatefmt_formatNumeric; + return fmt(rtf, t, relDateTimeUnit, chars, size, status); + }); + if (!str) + return false; + + args.rval().setString(str); + return true; +} diff --git a/js/src/builtin/intl/RelativeTimeFormat.h b/js/src/builtin/intl/RelativeTimeFormat.h new file mode 100644 index 0000000000..70185c182b --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.h @@ -0,0 +1,68 @@ +/* -*- 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_RelativeTimeFormat_h +#define builtin_intl_RelativeTimeFormat_h + +#include "mozilla/Attributes.h" + +#include + +#include "builtin/SelfHostingDefines.h" +#include "js/Class.h" +#include "vm/NativeObject.h" + +namespace js { + +class FreeOp; + +class RelativeTimeFormatObject : public NativeObject +{ + public: + static const Class class_; + + static constexpr uint32_t INTERNALS_SLOT = 0; + static constexpr uint32_t URELATIVE_TIME_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* +CreateRelativeTimeFormatPrototype(JSContext* cx, JS::Handle Intl, + JS::Handle global); + +/** + * Returns an object indicating the supported locales for relative time format + * 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_RelativeTimeFormat_availableLocales() + */ +extern MOZ_MUST_USE bool +intl_RelativeTimeFormat_availableLocales(JSContext* cx, unsigned argc, Value* vp); + +/** + * Returns a relative time as a string formatted according to the effective + * locale and the formatting options of the given RelativeTimeFormat. + * + * t should be a number representing a number to be formatted. + * unit should be "second", "minute", "hour", "day", "week", "month", "quarter", or "year". + * + * Usage: formatted = intl_FormatRelativeTime(relativeTimeFormat, t, unit) + */ +extern MOZ_MUST_USE bool +intl_FormatRelativeTime(JSContext* cx, unsigned argc, Value* vp); + +} // namespace js + +#endif /* builtin_intl_RelativeTimeFormat_h */ \ No newline at end of file diff --git a/js/src/builtin/intl/RelativeTimeFormat.js b/js/src/builtin/intl/RelativeTimeFormat.js new file mode 100644 index 0000000000..a37f067825 --- /dev/null +++ b/js/src/builtin/intl/RelativeTimeFormat.js @@ -0,0 +1,242 @@ +/* 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.RelativeTimeFormat **********/ + +/** + * RelativeTimeFormat internal properties. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.3.3. + */ +var relativeTimeFormatInternalProperties = { + localeData: relativeTimeFormatLocaleData, + _availableLocales: null, + availableLocales: function() // eslint-disable-line object-shorthand + { + var locales = this._availableLocales; + if (locales) + return locales; + + locales = intl_RelativeTimeFormat_availableLocales(); + addSpecialMissingLanguageTags(locales); + return (this._availableLocales = locales); + }, + relevantExtensionKeys: [], +}; + +function relativeTimeFormatLocaleData() { + // RelativeTimeFormat doesn't support any extension keys. + return {}; +} + +/** + * Compute an internal properties object from |lazyRelativeTimeFormatData|. + */ +function resolveRelativeTimeFormatInternals(lazyRelativeTimeFormatData) { + assert(IsObject(lazyRelativeTimeFormatData), "lazy data not an object?"); + + var internalProps = std_Object_create(null); + + var RelativeTimeFormat = relativeTimeFormatInternalProperties; + + // Steps 7-8. + const r = ResolveLocale(callFunction(RelativeTimeFormat.availableLocales, RelativeTimeFormat), + lazyRelativeTimeFormatData.requestedLocales, + lazyRelativeTimeFormatData.opt, + RelativeTimeFormat.relevantExtensionKeys, + RelativeTimeFormat.localeData); + + // Step 9-10. + internalProps.locale = r.locale; + + // Step 11. + assert(r.locale === r.dataLocale, + "resolved locale matches the resolved data-locale when no extension-keys are present"); + + // Step 13. + internalProps.style = lazyRelativeTimeFormatData.style; + + // Step 15. + internalProps.numeric = lazyRelativeTimeFormatData.numeric; + + // Steps 16-20 (Not relevant in our implementation). + + return internalProps; +} + +/** + * Returns an object containing the RelativeTimeFormat internal properties of |obj|, + * or throws a TypeError if |obj| isn't RelativeTimeFormat-initialized. + */ +function getRelativeTimeFormatInternals(obj, methodName) { + var internals = getIntlObjectInternals(obj, "RelativeTimeFormat", methodName); + assert(internals.type === "RelativeTimeFormat", "bad type escaped getIntlObjectInternals"); + + var internalProps = maybeInternalProperties(internals); + if (internalProps) + return internalProps; + + internalProps = resolveRelativeTimeFormatInternals(internals.lazyData); + setInternalProperties(internals, internalProps); + return internalProps; +} + +/** + * Initializes an object as a RelativeTimeFormat. + * + * 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 RelativeTimeFormat. + * This later work occurs in |resolveRelativeTimeFormatInternals|; steps not noted + * here occur there. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.1.1. + */ +function InitializeRelativeTimeFormat(relativeTimeFormat, locales, options) { + assert(IsObject(relativeTimeFormat), "InitializeRelativeTimeFormat"); + + if (isInitializedIntlObject(relativeTimeFormat)) + ThrowTypeError(JSMSG_INTL_OBJECT_REINITED); + + let internals = initializeIntlObject(relativeTimeFormat); + + // Lazy RelativeTimeFormat data has the following structure: + // + // { + // requestedLocales: List of locales, + // style: "long" / "short" / "narrow", + // numeric: "always" / "auto", + // + // opt: // opt object computed in InitializeRelativeTimeFormat + // { + // localeMatcher: "lookup" / "best fit", + // } + // } + // + // Note that lazy data is only installed as a final step of initialization, + // so every RelativeTimeFormat lazy data object has *all* these properties, never a + // subset of them. + const lazyRelativeTimeFormatData = std_Object_create(null); + + // Step 1. + let requestedLocales = CanonicalizeLocaleList(locales); + lazyRelativeTimeFormatData.requestedLocales = requestedLocales; + + // Steps 2-3. + if (options === undefined) + options = std_Object_create(null); + else + options = ToObject(options); + + // Step 4. + let opt = new Record(); + + // Steps 5-6. + let matcher = GetOption(options, "localeMatcher", "string", ["lookup", "best fit"], "best fit"); + opt.localeMatcher = matcher; + + lazyRelativeTimeFormatData.opt = opt; + + // Steps 12-13. + const style = GetOption(options, "style", "string", ["long", "short", "narrow"], "long"); + lazyRelativeTimeFormatData.style = style; + + // Steps 14-15. + const numeric = GetOption(options, "numeric", "string", ["always", "auto"], "always"); + lazyRelativeTimeFormatData.numeric = numeric; + + setLazyData(internals, "RelativeTimeFormat", lazyRelativeTimeFormatData) +} + +/** + * 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, RelativeTimeFormat, 1.3.2. + */ +function Intl_RelativeTimeFormat_supportedLocalesOf(locales /*, options*/) { + var options = arguments.length > 1 ? arguments[1] : undefined; + + // Step 1. + var availableLocales = callFunction(relativeTimeFormatInternalProperties.availableLocales, + relativeTimeFormatInternalProperties); + // Step 2. + let requestedLocales = CanonicalizeLocaleList(locales); + + // Step 3. + return SupportedLocales(availableLocales, requestedLocales, options); +} + +/** + * Returns a String value representing the written form of a relative date + * formatted according to the effective locale and the formatting options + * of this RelativeTimeFormat object. + * + * Spec: ECMAScript 402 API, RelativeTImeFormat, 1.4.3. + */ +function Intl_RelativeTimeFormat_format(value, unit) { + // Step 1. + let relativeTimeFormat = this; + + // Step 2. + let internals = getRelativeTimeFormatInternals(relativeTimeFormat, "format"); + + // Step 3. + let t = ToNumber(value); + + // Step 4. + let u = ToString(unit); + + // PartitionRelativeTimePattern, step 4. + if (!Number_isFinite(t)) { + ThrowRangeError(JSMSG_DATE_NOT_FINITE, "RelativeTimeFormat"); + } + + // PartitionRelativeTimePattern, step 5. + switch (u) { + case "second": + case "seconds": + case "minute": + case "minutes": + case "hour": + case "hours": + case "day": + case "days": + case "week": + case "weeks": + case "month": + case "months": + case "quarter": + case "quarters": + case "year": + case "years": + break; + default: + ThrowRangeError(JSMSG_INVALID_OPTION_VALUE, "unit", u); + } + + // Step 5. + return intl_FormatRelativeTime(relativeTimeFormat, t, u, internals.numeric); +} + +/** + * Returns the resolved options for a PluralRules object. + * + * Spec: ECMAScript 402 API, RelativeTimeFormat, 1.4.4. + */ +function Intl_RelativeTimeFormat_resolvedOptions() { + var internals = getRelativeTimeFormatInternals(this, "resolvedOptions"); + + // Steps 4-5. + var result = { + locale: internals.locale, + style: internals.style, + numeric: internals.numeric, + }; + + // Step 6. + return result; +} + diff --git a/js/src/builtin/intl/ScopedICUObject.h b/js/src/builtin/intl/ScopedICUObject.h new file mode 100644 index 0000000000..4a2e88186d --- /dev/null +++ b/js/src/builtin/intl/ScopedICUObject.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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_ScopedICUObject_h +#define builtin_intl_ScopedICUObject_h + +/* + * A simple RAII class to assure ICU objects are automatically deallocated at + * scope end. Unfortunately, ICU's C++ API is uniformly unstable, so we can't + * use its smart pointers for this. + */ + +namespace js { + +template +class ScopedICUObject +{ + T* ptr_; + + public: + explicit ScopedICUObject(T* ptr) + : ptr_(ptr) + {} + + ~ScopedICUObject() { + if (ptr_) + Delete(ptr_); + } + + // In cases where an object should be deleted on abnormal exits, + // but returned to the caller if everything goes well, call forget() + // to transfer the object just before returning. + T* forget() { + T* tmp = ptr_; + ptr_ = nullptr; + return tmp; + } +}; + +} // namespace js + +#endif /* builtin_intl_ScopedICUObject_h */ \ No newline at end of file diff --git a/js/src/builtin/intl/SharedIntlData.cpp b/js/src/builtin/intl/SharedIntlData.cpp new file mode 100644 index 0000000000..8ae3c17fe2 --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.cpp @@ -0,0 +1,300 @@ +/* -*- 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/. */ + +/* Runtime-wide Intl data shared across compartments. */ + +#include "builtin/intl/SharedIntlData.h" + +#include "mozilla/Assertions.h" +#include "mozilla/HashFunctions.h" + +#include + +#include "jsatom.h" +#include "jsstr.h" + +#include "builtin/intl/CommonFunctions.h" +#include "builtin/intl/ICUHeader.h" +#include "builtin/intl/ScopedICUObject.h" +#include "builtin/intl/TimeZoneDataGenerated.h" +#include "js/Utility.h" + +using js::HashNumber; +using js::intl::StringsAreEqual; + +template +static constexpr Char +ToUpperASCII(Char c) +{ + return ('a' <= c && c <= 'z') + ? (c & ~0x20) + : c; +} + +static_assert(ToUpperASCII('a') == 'A', "verifying 'a' uppercases correctly"); +static_assert(ToUpperASCII('m') == 'M', "verifying 'm' uppercases correctly"); +static_assert(ToUpperASCII('z') == 'Z', "verifying 'z' uppercases correctly"); +static_assert(ToUpperASCII(u'a') == u'A', "verifying u'a' uppercases correctly"); +static_assert(ToUpperASCII(u'k') == u'K', "verifying u'k' uppercases correctly"); +static_assert(ToUpperASCII(u'z') == u'Z', "verifying u'z' uppercases correctly"); + +template +static HashNumber +HashStringIgnoreCaseASCII(const Char* s, size_t length) +{ + uint32_t hash = 0; + for (size_t i = 0; i < length; i++) + hash = mozilla::AddToHash(hash, ToUpperASCII(s[i])); + return hash; +} + +template +static bool +EqualCharsIgnoreCaseASCII(const Char1* s1, const Char2* s2, size_t len) +{ + for (const Char1* s1end = s1 + len; s1 < s1end; s1++, s2++) { + if (ToUpperASCII(*s1) != ToUpperASCII(*s2)) + return false; + } + return true; +} + +js::intl::SharedIntlData::TimeZoneHasher::Lookup::Lookup(JSFlatString* timeZone) + : isLatin1(timeZone->hasLatin1Chars()), length(timeZone->length()) +{ + if (isLatin1) { + latin1Chars = timeZone->latin1Chars(nogc); + hash = HashStringIgnoreCaseASCII(latin1Chars, length); + } else { + twoByteChars = timeZone->twoByteChars(nogc); + hash = HashStringIgnoreCaseASCII(twoByteChars, length); + } +} + +bool +js::intl::SharedIntlData::TimeZoneHasher::match(TimeZoneName key, const Lookup& lookup) +{ + if (key->length() != lookup.length) + return false; + + // Compare time zone names ignoring ASCII case differences. + if (key->hasLatin1Chars()) { + const Latin1Char* keyChars = key->latin1Chars(lookup.nogc); + if (lookup.isLatin1) + return EqualCharsIgnoreCaseASCII(keyChars, lookup.latin1Chars, lookup.length); + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); + } + + const char16_t* keyChars = key->twoByteChars(lookup.nogc); + if (lookup.isLatin1) + return EqualCharsIgnoreCaseASCII(lookup.latin1Chars, keyChars, lookup.length); + return EqualCharsIgnoreCaseASCII(keyChars, lookup.twoByteChars, lookup.length); +} + +static bool +IsLegacyICUTimeZone(const char* timeZone) +{ + for (const auto& legacyTimeZone : js::timezone::legacyICUTimeZones) { + if (StringsAreEqual(timeZone, legacyTimeZone)) + return true; + } + return false; +} + +bool +js::intl::SharedIntlData::ensureTimeZones(JSContext* cx) +{ + if (timeZoneDataInitialized) + return true; + + // If initTimeZones() was called previously, but didn't complete due to + // OOM, clear all sets/maps and start from scratch. + if (availableTimeZones.initialized()) + availableTimeZones.finish(); + if (!availableTimeZones.init()) { + ReportOutOfMemory(cx); + return false; + } + + UErrorCode status = U_ZERO_ERROR; + UEnumeration* values = ucal_openTimeZones(&status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + ScopedICUObject toClose(values); + + RootedAtom timeZone(cx); + while (true) { + int32_t size; + const char* rawTimeZone = uenum_next(values, &size, &status); + if (U_FAILURE(status)) { + intl::ReportInternalError(cx); + return false; + } + + if (rawTimeZone == nullptr) + break; + + // Skip legacy ICU time zone names. + if (IsLegacyICUTimeZone(rawTimeZone)) + continue; + + MOZ_ASSERT(size >= 0); + timeZone = Atomize(cx, rawTimeZone, size_t(size)); + if (!timeZone) + return false; + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = availableTimeZones.lookupForAdd(lookup); + + // ICU shouldn't report any duplicate time zone names, but if it does, + // just ignore the duplicate name. + if (!p && !availableTimeZones.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + if (ianaZonesTreatedAsLinksByICU.initialized()) + ianaZonesTreatedAsLinksByICU.finish(); + if (!ianaZonesTreatedAsLinksByICU.init()) { + ReportOutOfMemory(cx); + return false; + } + + for (const char* rawTimeZone : timezone::ianaZonesTreatedAsLinksByICU) { + MOZ_ASSERT(rawTimeZone != nullptr); + timeZone = Atomize(cx, rawTimeZone, strlen(rawTimeZone)); + if (!timeZone) + return false; + + TimeZoneHasher::Lookup lookup(timeZone); + TimeZoneSet::AddPtr p = ianaZonesTreatedAsLinksByICU.lookupForAdd(lookup); + MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaZonesTreatedAsLinksByICU"); + + if (!ianaZonesTreatedAsLinksByICU.add(p, timeZone)) { + ReportOutOfMemory(cx); + return false; + } + } + + if (ianaLinksCanonicalizedDifferentlyByICU.initialized()) + ianaLinksCanonicalizedDifferentlyByICU.finish(); + if (!ianaLinksCanonicalizedDifferentlyByICU.init()) { + ReportOutOfMemory(cx); + return false; + } + + RootedAtom linkName(cx); + RootedAtom& target = timeZone; + for (const auto& linkAndTarget : timezone::ianaLinksCanonicalizedDifferentlyByICU) { + const char* rawLinkName = linkAndTarget.link; + const char* rawTarget = linkAndTarget.target; + + MOZ_ASSERT(rawLinkName != nullptr); + linkName = Atomize(cx, rawLinkName, strlen(rawLinkName)); + if (!linkName) + return false; + + MOZ_ASSERT(rawTarget != nullptr); + target = Atomize(cx, rawTarget, strlen(rawTarget)); + if (!target) + return false; + + TimeZoneHasher::Lookup lookup(linkName); + TimeZoneMap::AddPtr p = ianaLinksCanonicalizedDifferentlyByICU.lookupForAdd(lookup); + MOZ_ASSERT(!p, "Duplicate entry in timezone::ianaLinksCanonicalizedDifferentlyByICU"); + + if (!ianaLinksCanonicalizedDifferentlyByICU.add(p, linkName, target)) { + ReportOutOfMemory(cx); + return false; + } + } + + MOZ_ASSERT(!timeZoneDataInitialized, "ensureTimeZones is neither reentrant nor thread-safe"); + timeZoneDataInitialized = true; + + return true; +} + +bool +js::intl::SharedIntlData::validateTimeZoneName(JSContext* cx, HandleString timeZone, + MutableHandleString result) +{ + if (!ensureTimeZones(cx)) + return false; + + Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); + if (!timeZoneFlat) + return false; + + TimeZoneHasher::Lookup lookup(timeZoneFlat); + if (TimeZoneSet::Ptr p = availableTimeZones.lookup(lookup)) + result.set(*p); + + return true; +} + +bool +js::intl::SharedIntlData::tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, HandleString timeZone, + MutableHandleString result) +{ + if (!ensureTimeZones(cx)) + return false; + + Rooted timeZoneFlat(cx, timeZone->ensureFlat(cx)); + if (!timeZoneFlat) + return false; + + TimeZoneHasher::Lookup lookup(timeZoneFlat); + MOZ_ASSERT(availableTimeZones.has(lookup), "Invalid time zone name"); + + if (TimeZoneMap::Ptr p = ianaLinksCanonicalizedDifferentlyByICU.lookup(lookup)) { + // The effectively supported time zones aren't known at compile time, + // when + // 1. SpiderMonkey was compiled with "--with-system-icu". + // 2. ICU's dynamic time zone data loading feature was used. + // (ICU supports loading time zone files at runtime through the + // ICU_TIMEZONE_FILES_DIR environment variable.) + // Ensure ICU supports the new target zone before applying the update. + TimeZoneName targetTimeZone = p->value(); + TimeZoneHasher::Lookup targetLookup(targetTimeZone); + if (availableTimeZones.has(targetLookup)) + result.set(targetTimeZone); + } else if (TimeZoneSet::Ptr p = ianaZonesTreatedAsLinksByICU.lookup(lookup)) { + result.set(*p); + } + + return true; +} + +void +js::intl::SharedIntlData::destroyInstance() +{ + availableTimeZones.finish(); + ianaZonesTreatedAsLinksByICU.finish(); + ianaLinksCanonicalizedDifferentlyByICU.finish(); +} + +void +js::intl::SharedIntlData::trace(JSTracer* trc) +{ + // Atoms are always tenured. + if (!trc->runtime()->isHeapMinorCollecting()) { + availableTimeZones.trace(trc); + ianaZonesTreatedAsLinksByICU.trace(trc); + ianaLinksCanonicalizedDifferentlyByICU.trace(trc); + } +} + +size_t +js::intl::SharedIntlData::sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const +{ + return availableTimeZones.sizeOfExcludingThis(mallocSizeOf) + + ianaZonesTreatedAsLinksByICU.sizeOfExcludingThis(mallocSizeOf) + + ianaLinksCanonicalizedDifferentlyByICU.sizeOfExcludingThis(mallocSizeOf); +} diff --git a/js/src/builtin/intl/SharedIntlData.h b/js/src/builtin/intl/SharedIntlData.h new file mode 100644 index 0000000000..c047de3801 --- /dev/null +++ b/js/src/builtin/intl/SharedIntlData.h @@ -0,0 +1,164 @@ +/* -*- 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_SharedIntlData_h +#define builtin_intl_SharedIntlData_h + +#include "mozilla/MemoryReporting.h" + +#include + +#include "jsalloc.h" +#include "js/CharacterEncoding.h" +#include "js/GCAPI.h" +#include "js/GCHashTable.h" +#include "js/RootingAPI.h" +#include "js/Utility.h" +#include "vm/String.h" + +namespace js { + +namespace intl { + +/** + * Stores Intl data which can be shared across compartments (but not contexts). + * + * Used for data which is expensive when computed repeatedly or is not + * available through ICU. + */ +class SharedIntlData +{ + /** + * Information tracking the set of the supported time zone names, derived + * from the IANA time zone database . + * + * There are two kinds of IANA time zone names: Zone and Link (denoted as + * such in database source files). Zone names are the canonical, preferred + * name for a time zone, e.g. Asia/Kolkata. Link names simply refer to + * target Zone names for their meaning, e.g. Asia/Calcutta targets + * Asia/Kolkata. That a name is a Link doesn't *necessarily* reflect a + * sense of deprecation: some Link names also exist partly for convenience, + * e.g. UTC and GMT as Link names targeting the Zone name Etc/UTC. + * + * Two data sources determine the time zone names we support: those ICU + * supports and IANA's zone information. + * + * Unfortunately the names ICU and IANA support, and their Link + * relationships from name to target, aren't identical, so we can't simply + * implicitly trust ICU's name handling. We must perform various + * preprocessing of user-provided zone names and post-processing of + * ICU-provided zone names to implement ECMA-402's IANA-consistent behavior. + * + * Also see and + * . + */ + + using TimeZoneName = JSAtom*; + + struct TimeZoneHasher + { + struct Lookup + { + union { + const JS::Latin1Char* latin1Chars; + const char16_t* twoByteChars; + }; + bool isLatin1; + size_t length; + JS::AutoCheckCannotGC nogc; + HashNumber hash; + + explicit Lookup(JSFlatString* timeZone); + }; + + static js::HashNumber hash(const Lookup& lookup) { return lookup.hash; } + static bool match(TimeZoneName key, const Lookup& lookup); + }; + + using TimeZoneSet = js::GCHashSet; + + using TimeZoneMap = js::GCHashMap; + + /** + * As a threshold matter, available time zones are those time zones ICU + * supports, via ucal_openTimeZones. But ICU supports additional non-IANA + * time zones described in intl/icu/source/tools/tzcode/icuzones (listed in + * IntlTimeZoneData.cpp's |legacyICUTimeZones|) for its own backwards + * compatibility purposes. This set consists of ICU's supported time zones, + * minus all backwards-compatibility time zones. + */ + TimeZoneSet availableTimeZones; + + /** + * IANA treats some time zone names as Zones, that ICU instead treats as + * Links. For example, IANA considers "America/Indiana/Indianapolis" to be + * a Zone and "America/Fort_Wayne" a Link that targets it, but ICU + * considers the former a Link that targets "America/Indianapolis" (which + * IANA treats as a Link). + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name in this set, we must *not* return ICU's + * canonicalization. + */ + TimeZoneSet ianaZonesTreatedAsLinksByICU; + + /** + * IANA treats some time zone names as Links to one target, that ICU + * instead treats as either Zones, or Links to different targets. An + * example of the former is "Asia/Calcutta, which IANA assigns the target + * "Asia/Kolkata" but ICU considers its own Zone. An example of the latter + * is "America/Virgin", which IANA assigns the target + * "America/Port_of_Spain" but ICU assigns the target "America/St_Thomas". + * + * ECMA-402 requires that we respect IANA data, so if we're asked to + * canonicalize a time zone name that's a key in this map, we *must* return + * the corresponding value and *must not* return ICU's canonicalization. + */ + TimeZoneMap ianaLinksCanonicalizedDifferentlyByICU; + + bool timeZoneDataInitialized = false; + + /** + * Precomputes the available time zone names, because it's too expensive to + * call ucal_openTimeZones() repeatedly. + */ + bool ensureTimeZones(JSContext* cx); + + public: + /** + * Returns the validated time zone name in |result|. If the input time zone + * isn't a valid IANA time zone name, |result| remains unchanged. + */ + bool validateTimeZoneName(JSContext* cx, JS::HandleString timeZone, + JS::MutableHandleString result); + + /** + * Returns the canonical time zone name in |result|. If no canonical name + * was found, |result| remains unchanged. + * + * This method only handles time zones which are canonicalized differently + * by ICU when compared to IANA. + */ + bool tryCanonicalizeTimeZoneConsistentWithIANA(JSContext* cx, JS::HandleString timeZone, + JS::MutableHandleString result); + + void destroyInstance(); + + void trace(JSTracer* trc); + + size_t sizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; +}; + +} // namespace intl + +} // namespace js + +#endif /* builtin_intl_SharedIntlData_h */ \ No newline at end of file diff --git a/js/src/builtin/IntlTimeZoneData.h b/js/src/builtin/intl/TimeZoneDataGenerated.h similarity index 97% rename from js/src/builtin/IntlTimeZoneData.h rename to js/src/builtin/intl/TimeZoneDataGenerated.h index c0e9491567..a0e1895e37 100644 --- a/js/src/builtin/IntlTimeZoneData.h +++ b/js/src/builtin/intl/TimeZoneDataGenerated.h @@ -1,8 +1,8 @@ // Generated by make_intl_data.py. DO NOT EDIT. // tzdata version = 2022e -#ifndef builtin_IntlTimeZoneData_h -#define builtin_IntlTimeZoneData_h +#ifndef builtin_intl_TimeZoneDataGenerated_h +#define builtin_intl_TimeZoneDataGenerated_h namespace js { namespace timezone { @@ -143,4 +143,4 @@ const char* const legacyICUTimeZones[] = { } // namespace timezone } // namespace js -#endif /* builtin_IntlTimeZoneData_h */ +#endif /* builtin_intl_TimeZoneDataGenerated_h */ diff --git a/js/src/builtin/make_intl_data.py b/js/src/builtin/intl/make_intl_data.py similarity index 97% rename from js/src/builtin/make_intl_data.py rename to js/src/builtin/intl/make_intl_data.py index b81d5951f2..a81001e0f3 100644 --- a/js/src/builtin/make_intl_data.py +++ b/js/src/builtin/intl/make_intl_data.py @@ -12,8 +12,8 @@ Target "langtags": This script extracts information about mappings between deprecated and current BCP 47 language tags from the IANA Language Subtag Registry and - converts it to JavaScript object definitions in IntlData.js. The definitions - are used in Intl.js. + converts it to JavaScript object definitions in + LangTagMappingsGenerated.js. The definitions are used in Intl.js. The IANA Language Subtag Registry is imported from https://www.iana.org/assignments/language-subtag-registry @@ -190,7 +190,7 @@ def writeLanguageTagData(intlData, fileDate, url, langTagMappings, langSubtagMap "Mappings from extlang subtags to preferred values", fileDate, url) def updateLangTags(args): - """ Update the IntlData.js file. """ + """ Update the LangTagMappingsGenerated.js file. """ url = args.url out = args.out filename = args.file @@ -685,8 +685,8 @@ def processTimeZones(tzdataDir, icuDir, icuTzDir, version, ignoreBackzone, ignor println(tzdataVersionComment.format(version)) println(u"") - println(u"#ifndef builtin_IntlTimeZoneData_h") - println(u"#define builtin_IntlTimeZoneData_h") + println(u"#ifndef builtin_intl_TimeZoneDataGenerated_h") + println(u"#define builtin_intl_TimeZoneDataGenerated_h") println(u"") println(u"namespace js {") @@ -727,7 +727,7 @@ def processTimeZones(tzdataDir, icuDir, icuTzDir, version, ignoreBackzone, ignor println(u"} // namespace timezone") println(u"} // namespace js") println(u"") - println(u"#endif /* builtin_IntlTimeZoneData_h */") + println(u"#endif /* builtin_intl_TimeZoneDataGenerated_h */") def updateBackzoneLinks(tzdataDir, links): (backzoneZones, backzoneLinks) = readIANAFiles(tzdataDir, ["backzone"]) @@ -878,16 +878,9 @@ def generateTzDataTests(tzdataDir, version, ignoreBackzone, testDir): generateTzDataTestBackzone(tzdataDir, version, ignoreBackzone, testDir) generateTzDataTestBackzoneLinks(tzdataDir, version, ignoreBackzone, testDir) -def updateTzdata(args): +def updateTzdata(topsrcdir, args): """ Update the time zone cpp file. """ - # This script must reside in js/src/builtin to work correctly. - (thisDir, thisFile) = os.path.split(os.path.abspath(sys.argv[0])) - thisDir = os.path.normpath(thisDir) - if "/".join(thisDir.split(os.sep)[-3:]) != "js/src/builtin": - raise RuntimeError("%s must reside in js/src/builtin" % sys.argv[0]) - topsrcdir = "/".join(thisDir.split(os.sep)[:-3]) - icuDir = os.path.join(topsrcdir, "intl/icu/source") if not os.path.isdir(icuDir): raise RuntimeError("not a directory: %s" % icuDir) @@ -947,6 +940,13 @@ def updateTzdata(args): if __name__ == "__main__": import argparse + # This script must reside in js/src/builtin/intl to work correctly. + (thisDir, thisFile) = os.path.split(os.path.abspath(sys.argv[0])) + dirPaths = os.path.normpath(thisDir).split(os.sep) + if "/".join(dirPaths[-4:]) != "js/src/builtin/intl": + raise RuntimeError("%s must reside in js/src/builtin/intl" % sys.argv[0]) + topsrcdir = "/".join(dirPaths[:-4]) + def EnsureHttps(v): if not v.startswith("https:"): raise argparse.ArgumentTypeError("URL protocol must be https: " % v) @@ -963,7 +963,7 @@ if __name__ == "__main__": type=EnsureHttps, help="Download url for language-subtag-registry.txt (default: %(default)s)") parser_tags.add_argument("--out", - default="IntlData.js", + default="LangTagMappingsGenerated.js", help="Output file (default: %(default)s)") parser_tags.add_argument("file", nargs="?", @@ -984,9 +984,9 @@ if __name__ == "__main__": "accurate time zone canonicalization reflecting the actual time " "zones as used by ICU.") parser_tz.add_argument("--out", - default="IntlTimeZoneData.h", + default="TimeZoneDataGenerated.h", help="Output file (default: %(default)s)") - parser_tz.set_defaults(func=updateTzdata) + parser_tz.set_defaults(func=partial(updateTzdata, topsrcdir)) args = parser.parse_args() args.func(args) diff --git a/js/src/jsapi.cpp b/js/src/jsapi.cpp index 2b660e2c11..bae1a06475 100644 --- a/js/src/jsapi.cpp +++ b/js/src/jsapi.cpp @@ -43,7 +43,6 @@ #include "builtin/AtomicsObject.h" #include "builtin/Eval.h" -#include "builtin/Intl.h" #include "builtin/MapObject.h" #include "builtin/Promise.h" #include "builtin/RegExp.h" diff --git a/js/src/jsstr.cpp b/js/src/jsstr.cpp index b9e10b61b9..6726da9457 100644 --- a/js/src/jsstr.cpp +++ b/js/src/jsstr.cpp @@ -30,7 +30,7 @@ #include "jstypes.h" #include "jsutil.h" -#include "builtin/Intl.h" +#include "builtin/intl/ICUHeader.h" #include "builtin/RegExp.h" #include "jit/InlinableNatives.h" #include "js/Conversions.h" diff --git a/js/src/moz.build b/js/src/moz.build index 948840139c..d54547aecf 100644 --- a/js/src/moz.build +++ b/js/src/moz.build @@ -114,7 +114,14 @@ EXPORTS.js += [ UNIFIED_SOURCES += [ 'builtin/AtomicsObject.cpp', 'builtin/Eval.cpp', - 'builtin/Intl.cpp', + 'builtin/intl/Collator.cpp', + 'builtin/intl/CommonFunctions.cpp', + 'builtin/intl/DateTimeFormat.cpp', + 'builtin/intl/IntlObject.cpp', + 'builtin/intl/NumberFormat.cpp', + 'builtin/intl/PluralRules.cpp', + 'builtin/intl/RelativeTimeFormat.cpp', + 'builtin/intl/SharedIntlData.cpp', 'builtin/MapObject.cpp', 'builtin/ModuleObject.cpp', 'builtin/Object.cpp', @@ -661,8 +668,14 @@ selfhosted.inputs = [ 'builtin/Error.js', 'builtin/Function.js', 'builtin/Generator.js', - 'builtin/Intl.js', - 'builtin/IntlData.js', + 'builtin/intl/Collator.js', + 'builtin/intl/CommonFunctions.js', + 'builtin/intl/DateTimeFormat.js', + 'builtin/intl/IntlObject.js', + 'builtin/intl/LangTagMappingsGenerated.js', + 'builtin/intl/NumberFormat.js', + 'builtin/intl/PluralRules.js', + 'builtin/intl/RelativeTimeFormat.js', 'builtin/Iterator.js', 'builtin/Map.js', 'builtin/Module.js', diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index 2c379eee89..85a001d6a3 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -16,7 +16,6 @@ #include "builtin/AtomicsObject.h" #include "builtin/Eval.h" -#include "builtin/Intl.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" diff --git a/js/src/vm/Runtime.h b/js/src/vm/Runtime.h index 1bbe4658fb..8ad1b00623 100644 --- a/js/src/vm/Runtime.h +++ b/js/src/vm/Runtime.h @@ -25,7 +25,7 @@ # include "wasm/WasmSignalHandlers.h" #endif #include "builtin/AtomicsObject.h" -#include "builtin/Intl.h" +#include "builtin/intl/SharedIntlData.h" #include "builtin/Promise.h" #include "ds/FixedSizeHash.h" #include "frontend/NameCollections.h" @@ -810,7 +810,7 @@ struct JSRuntime : public JS::shadow::Runtime, const char* getDefaultLocale(); /* Shared Intl data for this runtime. */ - js::SharedIntlData sharedIntlData; + js::intl::SharedIntlData sharedIntlData; void traceSharedIntlData(JSTracer* trc); diff --git a/js/src/vm/SelfHosting.cpp b/js/src/vm/SelfHosting.cpp index 48b2da7b2c..73f2a0a291 100644 --- a/js/src/vm/SelfHosting.cpp +++ b/js/src/vm/SelfHosting.cpp @@ -22,7 +22,12 @@ #include "jswrapper.h" #include "selfhosted.out.h" -#include "builtin/Intl.h" +#include "builtin/intl/Collator.h" +#include "builtin/intl/DateTimeFormat.h" +#include "builtin/intl/IntlObject.h" +#include "builtin/intl/NumberFormat.h" +#include "builtin/intl/PluralRules.h" +#include "builtin/intl/RelativeTimeFormat.h" #include "builtin/MapObject.h" #include "builtin/ModuleObject.h" #include "builtin/Object.h" @@ -2443,7 +2448,7 @@ static const JSFunctionSpec intrinsic_functions[] = { JS_FOR_EACH_REFERENCE_TYPE_REPR(LOAD_AND_STORE_REFERENCE_FN_DECLS) #undef LOAD_AND_STORE_REFERENCE_FN_DECLS - // See builtin/Intl.h for descriptions of the intl_* functions. + // See builtin/intl/*.h for descriptions of the intl_* functions. JS_FN("intl_availableCalendars", intl_availableCalendars, 1,0), JS_FN("intl_availableCollations", intl_availableCollations, 1,0), JS_FN("intl_canonicalizeTimeZone", intl_canonicalizeTimeZone, 1,0), diff --git a/layout/style/CSSStyleSheet.cpp b/layout/style/CSSStyleSheet.cpp index 40a4ee4d84..4cefdfad95 100644 --- a/layout/style/CSSStyleSheet.cpp +++ b/layout/style/CSSStyleSheet.cpp @@ -1618,6 +1618,12 @@ CSSStyleSheet::DidDirty() ClearRuleCascades(); } +void +CSSStyleSheet::AssertHasUniqueInner() +{ + MOZ_ASSERT(mInner->mSheets.Length() == 1, "expected unique inner"); +} + nsresult CSSStyleSheet::RegisterNamespaceRule(css::Rule* aRule) { diff --git a/layout/style/CSSStyleSheet.h b/layout/style/CSSStyleSheet.h index 89189d7816..d777b1f652 100644 --- a/layout/style/CSSStyleSheet.h +++ b/layout/style/CSSStyleSheet.h @@ -217,6 +217,7 @@ public: void WillDirty(); void DidDirty(); + void AssertHasUniqueInner(); private: CSSStyleSheet(const CSSStyleSheet& aCopy, diff --git a/layout/style/StyleRule.cpp b/layout/style/StyleRule.cpp index bf5eefa156..8fe19bb34a 100644 --- a/layout/style/StyleRule.cpp +++ b/layout/style/StyleRule.cpp @@ -1571,9 +1571,43 @@ StyleRule::GetSelectorText(nsAString& aSelectorText) void StyleRule::SetSelectorText(const nsAString& aSelectorText) { - // XXX TBI - get a parser and re-parse the selectors, - // XXX then need to re-compute the cascade - // XXX and dirty sheet + CSSStyleSheet* sheet = GetStyleSheet(); + + nsIDocument* doc = GetDocument(); + RefPtr loader; + + if (doc) { + loader = doc->CSSLoader(); + } + + // NOTE: Passing a null loader means that the parser is always in + // standards mode and never in quirks mode. + nsCSSParser css(loader, sheet); + + // StyleRule lives inside of the Inner, it is unsafe to call WillDirty + // if sheet does not already have a unique Inner. + sheet->AssertHasUniqueInner(); + sheet->WillDirty(); + + nsCSSSelectorList* selectorList = nullptr; + + nsresult result = css.ParseSelectorString( + aSelectorText, sheet->GetSheetURI(), 0, &selectorList); + if (NS_FAILED(result)) { + // Ignore parsing errors and continue to use the previous value. + return; + } + + // Replace selector. + delete mSelector; + mSelector = selectorList; + + sheet->DidDirty(); + + if (doc) { + mozAutoDocUpdate updateBatch(doc, UPDATE_STYLE, true); + doc->StyleRuleChanged(sheet, this); + } } /* virtual */ size_t diff --git a/layout/style/StyleSheet.h b/layout/style/StyleSheet.h index 55d1147005..0b4af9f13b 100644 --- a/layout/style/StyleSheet.h +++ b/layout/style/StyleSheet.h @@ -186,6 +186,7 @@ public: // WillDirty and then make no change and skip the DidDirty call. inline void WillDirty(); inline void DidDirty(); + inline void AssertHasUniqueInner(); private: // Get a handle to the various stylesheet bits which live on the 'inner' for diff --git a/python/mozbuild/mozbuild/mach_commands.py b/python/mozbuild/mozbuild/mach_commands.py index 6a7b28810a..a99ad6d47d 100644 --- a/python/mozbuild/mozbuild/mach_commands.py +++ b/python/mozbuild/mozbuild/mach_commands.py @@ -710,15 +710,36 @@ class Clobber(MachCommandBase): raise if 'python' in what: + # TODO: Once we move to Python 3, we should probably use + # shutil.which to get the fully qualified path for these commands. + cmd = '' if os.path.isdir(mozpath.join(self.topsrcdir, '.hg')): cmd = ['hg', 'purge', '--all', '-I', 'glob:**.py[co]'] elif os.path.isdir(mozpath.join(self.topsrcdir, '.git')): cmd = ['git', 'clean', '-f', '-x', '*.py[co]'] + + if not cmd: + ret = self.clobber_python_fallback() else: - cmd = ['find', '.', '-type', 'f', '-name', '*.py[co]', '-delete'] - ret = subprocess.call(cmd, cwd=self.topsrcdir) + # It is possible that git or hg is either not installed or + # excluded from PATH despite the existence of their data + # directories, so use a fallback instead of failing early. + try: + ret = subprocess.call(cmd, cwd=self.topsrcdir) + except OSError as e: + ret = self.clobber_python_fallback() + return ret + def clobber_python_fallback(self): + cmd = ['find', '.', '-type', 'f', '-name', '*.py[co]', '-delete'] + # Execute the command through the shell if we're on Windows to ensure + # that our copy of `find` is run rather than the OS default. + # This is because on Windows, Popen (and by extension, subprocess.call) + # ignores PATH and looks only at the current working directory. + use_shell = sys.platform.startswith('win') + return subprocess.call(cmd, cwd=self.topsrcdir, shell=use_shell) + @CommandProvider class Logs(MachCommandBase): """Provide commands to read mach logs.""" diff --git a/storage/mozStorageService.cpp b/storage/mozStorageService.cpp index 8c6f65232c..04be1f8733 100644 --- a/storage/mozStorageService.cpp +++ b/storage/mozStorageService.cpp @@ -27,7 +27,7 @@ #include "sqlite3.h" -#ifdef SQLITE_OS_WIN +#ifdef XP_WIN // "windows.h" was included and it can #define lots of things we care about... #undef CompareString #endif @@ -917,6 +917,12 @@ Service::Observe(nsISupports *, const char *aTopic, const char16_t *) getConnections(connections); for (uint32_t i = 0, n = connections.Length(); i < n; i++) { if (!connections[i]->isClosed()) { +#ifdef DEBUG + nsCString msg; + msg.AppendPrintf("Storage connection to '%s' was not closed.", + connections[i]->getFilename().get()); + NS_ERROR(msg.get()); +#endif MOZ_CRASH(); } }