/** * Copyright (c) 2012-2016 André Bargull * Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms. * * <https://github.com/anba/es6draft> */ package com.github.anba.es6draft.runtime.objects.intl; import static com.github.anba.es6draft.runtime.AbstractOperations.Get; import static com.github.anba.es6draft.runtime.AbstractOperations.ToNumber; import static com.github.anba.es6draft.runtime.AbstractOperations.ToObject; import static com.github.anba.es6draft.runtime.internal.Errors.newRangeError; import static com.github.anba.es6draft.runtime.internal.Errors.newTypeError; import static com.github.anba.es6draft.runtime.internal.Properties.createProperties; import static com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.*; import static java.util.Arrays.asList; import java.util.HashSet; import java.util.List; import java.util.Set; import com.github.anba.es6draft.runtime.ExecutionContext; import com.github.anba.es6draft.runtime.Realm; import com.github.anba.es6draft.runtime.internal.Initializable; import com.github.anba.es6draft.runtime.internal.Lazy; import com.github.anba.es6draft.runtime.internal.Messages; import com.github.anba.es6draft.runtime.internal.Properties.Attributes; import com.github.anba.es6draft.runtime.internal.Properties.Function; import com.github.anba.es6draft.runtime.internal.Properties.Prototype; import com.github.anba.es6draft.runtime.internal.Properties.Value; import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.ExtensionKey; import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.LocaleData; import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.LocaleDataInfo; import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.OptionsRecord; import com.github.anba.es6draft.runtime.objects.intl.IntlAbstractOperations.ResolvedLocale; import com.github.anba.es6draft.runtime.types.Constructor; import com.github.anba.es6draft.runtime.types.Intrinsics; import com.github.anba.es6draft.runtime.types.ScriptObject; import com.github.anba.es6draft.runtime.types.Type; import com.github.anba.es6draft.runtime.types.builtins.BuiltinConstructor; import com.github.anba.es6draft.runtime.types.builtins.BuiltinFunction; import com.ibm.icu.text.NumberingSystem; import com.ibm.icu.util.ULocale; /** * <h1>11 NumberFormat Objects</h1> * <ul> * <li>11.1 Abstract Operations For NumberFormat Objects * <li>11.2 The Intl.NumberFormat Constructor * <li>11.3 Properties of the Intl.NumberFormat Constructor * </ul> */ public final class NumberFormatConstructor extends BuiltinConstructor implements Initializable { /** [[availableLocales]] */ private final Lazy<Set<String>> availableLocales = Lazy .of(() -> GetAvailableLocales(LanguageData.getAvailableNumberFormatLocales())); /** * [[availableLocales]] * * @param cx * the execution context * @return the set of available locales supported by {@code Intl.NumberFormat} */ public static Set<String> getAvailableLocales(ExecutionContext cx) { return getAvailableLocalesLazy(cx).get(); } private static Lazy<Set<String>> getAvailableLocalesLazy(ExecutionContext cx) { NumberFormatConstructor numberFormat = (NumberFormatConstructor) cx.getIntrinsic(Intrinsics.Intl_NumberFormat); return numberFormat.availableLocales; } /** [[relevantExtensionKeys]] */ private static final List<ExtensionKey> relevantExtensionKeys = asList(ExtensionKey.nu); /** [[localeData]] */ private static final class NumberFormatLocaleData implements LocaleData { @Override public LocaleDataInfo info(ULocale locale) { return new NumberFormatLocaleDataInfo(locale); } } /** [[localeData]] */ private static final class NumberFormatLocaleDataInfo implements LocaleDataInfo { private final ULocale locale; public NumberFormatLocaleDataInfo(ULocale locale) { this.locale = locale; } @Override public String defaultValue(ExtensionKey extensionKey) { switch (extensionKey) { case nu: return NumberingSystem.getInstance(locale).getName(); default: throw new IllegalArgumentException(extensionKey.name()); } } @Override public List<String> entries(ExtensionKey extensionKey) { switch (extensionKey) { case nu: return getNumberInfo(); default: throw new IllegalArgumentException(extensionKey.name()); } } private List<String> getNumberInfo() { // ICU4J does not provide an API to retrieve the numbering systems per locale, go with // Spidermonkey instead and return default numbering system of locale + Table 2 entries String localeNumberingSystem = NumberingSystem.getInstance(locale).getName(); return asList(localeNumberingSystem, "arab", "arabtext", "bali", "beng", "deva", "fullwide", "gujr", "guru", "hanidec", "khmr", "knda", "laoo", "latn", "limb", "mlym", "mong", "mymr", "orya", "tamldec", "telu", "thai", "tibt"); } } /** * Constructs a new NumberFormat constructor function. * * @param realm * the realm object */ public NumberFormatConstructor(Realm realm) { super(realm, "NumberFormat", 0); } @Override public void initialize(Realm realm) { createProperties(realm, this, Properties.class); } @Override public NumberFormatConstructor clone() { return new NumberFormatConstructor(getRealm()); } @SafeVarargs private static <T> Set<T> set(T... elements) { return new HashSet<>(asList(elements)); } /** * 11.1.1 InitializeNumberFormat (numberFormat, locales, options) * * @param cx * the execution context * @param numberFormat * the number format object * @param locales * the locales array * @param opts * the options object */ public static void InitializeNumberFormat(ExecutionContext cx, NumberFormatObject numberFormat, Object locales, Object opts) { /* steps 1-2 (FIXME: spec bug - unnecessary internal slot) */ /* step 3 */ Set<String> requestedLocales = CanonicalizeLocaleList(cx, locales); /* steps 4-5 */ ScriptObject options; if (Type.isUndefined(opts)) { options = ObjectCreate(cx, Intrinsics.ObjectPrototype); } else { options = ToObject(cx, opts); } /* step 7 */ String matcher = GetStringOption(cx, options, "localeMatcher", set("lookup", "best fit"), "best fit"); /* step 6, 8 */ OptionsRecord opt = new OptionsRecord(OptionsRecord.MatcherType.forName(matcher)); /* step 9 */ NumberFormatLocaleData localeData = new NumberFormatLocaleData(); /* step 10 */ ResolvedLocale r = ResolveLocale(cx.getRealm(), getAvailableLocalesLazy(cx), requestedLocales, opt, relevantExtensionKeys, localeData); /* step 11 */ numberFormat.setLocale(r.getLocale()); /* step 12 */ numberFormat.setNumberingSystem(r.getValue(ExtensionKey.nu)); /* step 13 (not applicable) */ /* step 14 */ String s = GetStringOption(cx, options, "style", set("decimal", "percent", "currency"), "decimal"); /* step 15 */ numberFormat.setStyle(s); /* step 16 */ String c = GetStringOption(cx, options, "currency", null, null); /* step 17 */ if (c != null && !IsWellFormedCurrencyCode(c)) { throw newRangeError(cx, Messages.Key.IntlInvalidCurrency, c); } /* step 18 */ if ("currency".equals(s) && c == null) { throw newTypeError(cx, Messages.Key.IntlInvalidCurrency, "null"); } /* step 19 */ int cDigits = -1; if ("currency".equals(s)) { c = ToUpperCase(c); numberFormat.setCurrency(c); cDigits = CurrencyDigits(c); } /* step 20 */ String cd = GetStringOption(cx, options, "currencyDisplay", set("code", "symbol", "name"), "symbol"); /* step 21 */ if ("currency".equals(s)) { numberFormat.setCurrencyDisplay(cd); } /* step 22 */ int mnid = GetNumberOption(cx, options, "minimumIntegerDigits", 1, 21, 1); /* step 23 */ numberFormat.setMinimumIntegerDigits(mnid); /* step 24 */ int mnfdDefault = "currency".equals(s) ? cDigits : 0; /* step 25 */ int mnfd = GetNumberOption(cx, options, "minimumFractionDigits", 0, 20, mnfdDefault); /* step 26 */ numberFormat.setMinimumFractionDigits(mnfd); /* step 27 */ int mxfdDefault = "currency".equals(s) ? Math.max(mnfd, cDigits) : "percent".equals(s) ? Math.max(mnfd, 0) : Math.max(mnfd, 3); /* step 28 */ int mxfd = GetNumberOption(cx, options, "maximumFractionDigits", mnfd, 20, mxfdDefault); /* step 29 */ numberFormat.setMaximumFractionDigits(mxfd); /* step 30 */ Object mnsd = Get(cx, options, "minimumSignificantDigits"); /* step 31 */ Object mxsd = Get(cx, options, "maximumSignificantDigits"); /* step 32 */ if (!Type.isUndefined(mnsd) || !Type.isUndefined(mxsd)) { int _mnsd = GetNumberOption(cx, options, "minimumSignificantDigits", 1, 21, 1); int _mxsd = GetNumberOption(cx, options, "maximumSignificantDigits", _mnsd, 21, 21); numberFormat.setMinimumSignificantDigits(_mnsd); numberFormat.setMaximumSignificantDigits(_mxsd); } /* step 33 */ boolean g = GetBooleanOption(cx, options, "useGrouping", true); /* step 34 */ numberFormat.setUseGrouping(g); /* steps 35-40 (not applicable) */ /* step 41 */ numberFormat.setBoundFormat(null); /* step 42 (FIXME: spec bug - unnecessary internal slot) */ /* step 43 (omitted) */ } /** * 11.1.1 InitializeNumberFormat (numberFormat, locales, options) * * @param realm * the realm instance * @param numberFormat * the number format object */ public static void InitializeDefaultNumberFormat(Realm realm, NumberFormatObject numberFormat) { /* steps 1-2 (FIXME: spec bug - unnecessary internal slot) */ /* steps 3-8 (not applicable) */ /* step 9 */ NumberFormatLocaleData localeData = new NumberFormatLocaleData(); /* step 10 */ ResolvedLocale r = ResolveDefaultLocale(realm, relevantExtensionKeys, localeData); /* step 11 */ numberFormat.setLocale(r.getLocale()); /* step 12 */ numberFormat.setNumberingSystem(r.getValue(ExtensionKey.nu)); /* step 13 (not applicable) */ /* steps 14-15 */ numberFormat.setStyle("decimal"); /* steps 16-21 (not applicable) */ /* steps 22-23 */ numberFormat.setMinimumIntegerDigits(1); /* steps 24-26 */ numberFormat.setMinimumFractionDigits(0); /* steps 27-29 */ numberFormat.setMaximumFractionDigits(3); /* steps 30-32 (not applicable) */ /* steps 33-34 */ numberFormat.setUseGrouping(true); /* steps 35-40 (not applicable) */ /* step 41 */ numberFormat.setBoundFormat(null); /* step 42 (FIXME: spec bug - unnecessary internal slot) */ /* step 43 (omitted) */ } /** * 11.1.2 CurrencyDigits (currency) * * @param c * the currency * @return the number of currency digits */ private static int CurrencyDigits(String c) { // http://www.currency-iso.org/dam/downloads/lists/list_one.xml // Last updated: 2015-06-19 switch (c) { case "BIF": case "BYR": case "CLP": case "DJF": case "GNF": case "ISK": case "JPY": case "KMF": case "KRW": case "PYG": case "RWF": case "UGX": case "UYI": case "VND": case "VUV": case "XAF": case "XOF": case "XPF": return 0; case "BHD": case "IQD": case "JOD": case "KWD": case "LYD": case "OMR": case "TND": return 3; case "CLF": return 4; default: return 2; } } /** * 11.1.3 Number Format Functions */ public static final class FormatFunction extends BuiltinFunction { public FormatFunction(Realm realm) { super(realm, "format", 1); createDefaultFunctionProperties(); } private FormatFunction(Realm realm, Void ignore) { super(realm, "format", 1); } @Override public FormatFunction clone() { return new FormatFunction(getRealm(), null); } @Override public String call(ExecutionContext callerContext, Object thisValue, Object... args) { ExecutionContext calleeContext = calleeContext(); /* steps 1-2 */ assert thisValue instanceof NumberFormatObject; NumberFormatObject nf = (NumberFormatObject) thisValue; /* step 3 */ Object value = argument(args, 0); /* step 4 */ double x = ToNumber(calleeContext, value); /* step 5 */ return FormatNumber(nf, x); } } /** * 11.1.4 FormatNumber(numberFormat, x) * * @param numberFormat * the number format object * @param x * the number value * @return the formatted number string */ public static String FormatNumber(NumberFormatObject numberFormat, double x) { if (x == -0.0) { // -0 is not considered to be negative, cf. step 3a x = +0.0; } /* steps 1-8 */ return numberFormat.getNumberFormat().format(x); } /** * 11.2.1 Intl.NumberFormat([ locales [, options]]) */ @Override public ScriptObject call(ExecutionContext callerContext, Object thisValue, Object... args) { /* steps 1-3 */ return construct(callerContext, this, args); } /** * 11.2.1 Intl.NumberFormat([ locales [, options]]) */ @Override public NumberFormatObject construct(ExecutionContext callerContext, Constructor newTarget, Object... args) { ExecutionContext calleeContext = calleeContext(); Object locales = argument(args, 0); Object options = argument(args, 1); /* step 1 (not applicable) */ /* step 2 */ NumberFormatObject obj = OrdinaryCreateFromConstructor(calleeContext, newTarget, Intrinsics.Intl_NumberFormatPrototype, NumberFormatObject::new); /* step 3 */ InitializeNumberFormat(calleeContext, obj, locales, options); return obj; } /** * 11.3 Properties of the Intl.NumberFormat Constructor */ public enum Properties { ; @Prototype public static final Intrinsics __proto__ = Intrinsics.FunctionPrototype; @Value(name = "length", attributes = @Attributes(writable = false, enumerable = false, configurable = true)) public static final int length = 0; @Value(name = "name", attributes = @Attributes(writable = false, enumerable = false, configurable = true)) public static final String name = "NumberFormat"; /** * 11.3.1 Intl.NumberFormat.prototype */ @Value(name = "prototype", attributes = @Attributes(writable = false, enumerable = false, configurable = false)) public static final Intrinsics prototype = Intrinsics.Intl_NumberFormatPrototype; /** * 11.3.2 Intl.NumberFormat.supportedLocalesOf (locales [, options ]) * * @param cx * the execution context * @param thisValue * the function this-value * @param locales * the locales array * @param options * the options object * @return the array of supported locales */ @Function(name = "supportedLocalesOf", arity = 1) public static Object supportedLocalesOf(ExecutionContext cx, Object thisValue, Object locales, Object options) { /* step 1 */ Set<String> availableLocales = getAvailableLocales(cx); /* step 2 */ Set<String> requestedLocales = CanonicalizeLocaleList(cx, locales); /* step 3 */ return SupportedLocales(cx, availableLocales, requestedLocales, options); } } }