|
@@ -0,0 +1,366 @@
|
|
|
+using Jint.Native.Function;
|
|
|
+using Jint.Native.Object;
|
|
|
+using Jint.Runtime;
|
|
|
+using Jint.Runtime.Descriptors;
|
|
|
+
|
|
|
+namespace Jint.Native.Intl;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// https://tc39.es/ecma402/#sec-intl-numberformat-constructor
|
|
|
+/// </summary>
|
|
|
+internal sealed class NumberFormatConstructor : Constructor
|
|
|
+{
|
|
|
+ private static readonly JsString _functionName = new("NumberFormat");
|
|
|
+
|
|
|
+ public NumberFormatConstructor(
|
|
|
+ Engine engine,
|
|
|
+ Realm realm,
|
|
|
+ FunctionPrototype functionPrototype,
|
|
|
+ ObjectPrototype objectPrototype) : base(engine, realm, _functionName)
|
|
|
+ {
|
|
|
+ _prototype = functionPrototype;
|
|
|
+ PrototypeObject = new NumberFormatPrototype(engine, realm, this, objectPrototype);
|
|
|
+ _length = new PropertyDescriptor(JsNumber.PositiveZero, PropertyFlag.Configurable);
|
|
|
+ _prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
|
|
|
+ }
|
|
|
+
|
|
|
+ public NumberFormatPrototype PrototypeObject { get; }
|
|
|
+
|
|
|
+ public object LocaleData { get; private set; } = null!;
|
|
|
+ public object AvailableLocales { get; private set; } = null!;
|
|
|
+ public object RelevantExtensionKeys { get; private set; } = null!;
|
|
|
+
|
|
|
+
|
|
|
+ protected override void Initialize()
|
|
|
+ {
|
|
|
+ LocaleData = new object();
|
|
|
+ AvailableLocales = new object();
|
|
|
+ RelevantExtensionKeys = new object();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-intl.numberformat
|
|
|
+ /// </summary>
|
|
|
+ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget)
|
|
|
+ {
|
|
|
+ var locales = arguments.At(0);
|
|
|
+ var options = arguments.At(1);
|
|
|
+
|
|
|
+ if (newTarget.IsUndefined())
|
|
|
+ {
|
|
|
+ newTarget = this;
|
|
|
+ }
|
|
|
+
|
|
|
+ var numberFormat = OrdinaryCreateFromConstructor<JsObject, object>(
|
|
|
+ newTarget,
|
|
|
+ static intrinsics => intrinsics.NumberFormat.PrototypeObject,
|
|
|
+ static (engine, _, _) => new JsObject(engine));
|
|
|
+
|
|
|
+ InitializeNumberFormat(numberFormat, locales, options);
|
|
|
+ return numberFormat;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-initializenumberformat
|
|
|
+ /// </summary>
|
|
|
+ private JsObject InitializeNumberFormat(JsObject numberFormat, JsValue locales, JsValue opts)
|
|
|
+ {
|
|
|
+ var requestedLocales = CanonicalizeLocaleList(locales);
|
|
|
+ var options = CoerceOptionsToObject(opts);
|
|
|
+ var opt = new JsObject(_engine);
|
|
|
+
|
|
|
+ var matcher = GetOption(options, "localeMatcher", OptionType.String, new JsValue[] { "lookup", "best fit" }, "best fit");
|
|
|
+ opt["localeMatcher"] = matcher;
|
|
|
+
|
|
|
+ var numberingSystem = GetOption(options, "numberingSystem", OptionType.String, System.Array.Empty<JsValue>(), Undefined);
|
|
|
+ if (!numberingSystem.IsUndefined())
|
|
|
+ {
|
|
|
+ // If numberingSystem does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception.
|
|
|
+ }
|
|
|
+
|
|
|
+ opt["nu"] = numberingSystem;
|
|
|
+ var localeData = LocaleData;
|
|
|
+ var r = ResolveLocale(_engine.Realm.Intrinsics.NumberFormat.AvailableLocales, requestedLocales, opt, _engine.Realm.Intrinsics.NumberFormat.RelevantExtensionKeys, localeData);
|
|
|
+ numberFormat["Locale"] = r["locale"];
|
|
|
+ numberFormat["DataLocale"] = r["dataLocale"];
|
|
|
+ numberFormat["NumberingSystem"] = r["nu"];
|
|
|
+ SetNumberFormatUnitOptions(numberFormat, options);
|
|
|
+
|
|
|
+ int mnfdDefault;
|
|
|
+ int mxfdDefault;
|
|
|
+ var style = numberFormat["Style"];
|
|
|
+ if (style == "currency")
|
|
|
+ {
|
|
|
+ var currency = numberFormat["Currency"];
|
|
|
+ var cDigits = CurrencyDigits(currency);
|
|
|
+ mnfdDefault = cDigits;
|
|
|
+ mxfdDefault = cDigits;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ mnfdDefault = 0;
|
|
|
+ mxfdDefault = style == "percent" ? 0 : 3;
|
|
|
+ }
|
|
|
+
|
|
|
+ var notation = GetOption(options, "notation", OptionType.String, new JsValue[] { "standard", "scientific", "engineering", "compact" }, "standard");
|
|
|
+ numberFormat["Notation"] = notation;
|
|
|
+ SetNumberFormatDigitOptions(numberFormat, options, mnfdDefault, mxfdDefault, notation.ToString());
|
|
|
+
|
|
|
+ var compactDisplay = GetOption(options, "compactDisplay", OptionType.String, new JsValue[] { "short", "long" }, "short");
|
|
|
+ if (notation == "compact")
|
|
|
+ {
|
|
|
+ numberFormat["CompactDisplay"] = compactDisplay;
|
|
|
+ }
|
|
|
+
|
|
|
+ var useGrouping = GetOption(options, "useGrouping", OptionType.Boolean, System.Array.Empty<JsValue>(), JsBoolean.True);
|
|
|
+ numberFormat["UseGrouping"] = useGrouping;
|
|
|
+ var signDisplay = GetOption(options, "signDisplay", OptionType.String, new JsValue[] { "auto", "never", "always", "exceptZero" }, "auto");
|
|
|
+ numberFormat["SignDisplay"] = signDisplay;
|
|
|
+
|
|
|
+ return numberFormat;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-resolvelocale
|
|
|
+ /// </summary>
|
|
|
+ private JsObject ResolveLocale(object availableLocales, JsArray requestedLocales, JsObject options, object relevantExtensionKeys, object localeData)
|
|
|
+ {
|
|
|
+ // TODO
|
|
|
+ var result = new JsObject(_engine);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-setnfdigitoptions
|
|
|
+ /// </summary>
|
|
|
+ private static void SetNumberFormatDigitOptions(JsObject numberFormat, ObjectInstance options, int mnfdDefault, int mxfdDefault, string notation)
|
|
|
+ {
|
|
|
+ // TODO
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-currencydigits
|
|
|
+ /// </summary>
|
|
|
+ private static int CurrencyDigits(JsValue currency)
|
|
|
+ {
|
|
|
+ // TODO
|
|
|
+ return 2;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-setnumberformatunitoptions
|
|
|
+ /// </summary>
|
|
|
+ private void SetNumberFormatUnitOptions(JsObject intlObj, JsValue options)
|
|
|
+ {
|
|
|
+ var style = GetOption(options, "style", OptionType.String, new JsValue[] { "decimal", "percent", "currency", "unit" }, "decimal");
|
|
|
+ intlObj["Style"] = style;
|
|
|
+ var currency = GetOption(options, "currency", OptionType.String, System.Array.Empty<JsValue>(), Undefined);
|
|
|
+ if (currency.IsUndefined())
|
|
|
+ {
|
|
|
+ if (style == "currency")
|
|
|
+ {
|
|
|
+ ExceptionHelper.ThrowTypeError(_realm, "No currency found when style currency requested");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (!IsWellFormedCurrencyCode(currency))
|
|
|
+ {
|
|
|
+ ExceptionHelper.ThrowRangeError(_realm, "Invalid currency code");
|
|
|
+ }
|
|
|
+
|
|
|
+ var currencyDisplay = GetOption(options, "currencyDisplay", OptionType.String, new JsValue[] { "code", "symbol", "narrowSymbol", "name" }, "symbol");
|
|
|
+ var currencySign = GetOption(options, "currencySign", OptionType.String, new JsValue[] { "standard", "accounting" }, "standard");
|
|
|
+ var unit = GetOption(options, "unit", OptionType.String, System.Array.Empty<JsValue>(), Undefined);
|
|
|
+
|
|
|
+ if (unit.IsUndefined())
|
|
|
+ {
|
|
|
+ if (style == "unit")
|
|
|
+ {
|
|
|
+ ExceptionHelper.ThrowTypeError(_realm, "No unit found when style unit requested");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (!IsWellFormedUnitIdentifier(unit))
|
|
|
+ {
|
|
|
+ ExceptionHelper.ThrowRangeError(_realm, "Invalid unit");
|
|
|
+ }
|
|
|
+
|
|
|
+ var unitDisplay = GetOption(options, "unitDisplay", OptionType.String, new JsValue[] { "short", "narrow", "long" }, "short");
|
|
|
+ if (style == "currency")
|
|
|
+ {
|
|
|
+ intlObj["Currency"] = currency.ToString().ToUpperInvariant();
|
|
|
+ intlObj["CurrencyDisplay"] = currencyDisplay;
|
|
|
+ intlObj["CurrencySign"] = currencySign;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (style == "unit")
|
|
|
+ {
|
|
|
+ intlObj["Unit"] = unit;
|
|
|
+ intlObj["UnitDisplay"] = unitDisplay;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-iswellformedunitidentifier
|
|
|
+ /// </summary>
|
|
|
+ private bool IsWellFormedUnitIdentifier(JsValue unitIdentifier)
|
|
|
+ {
|
|
|
+ var value = unitIdentifier.ToString();
|
|
|
+ if (IsSanctionedSingleUnitIdentifier(value))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ var i = value.IndexOf("-per-");
|
|
|
+ if (i == -1 || value.IndexOf("-per-", i + 1) != -1)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ var numerator = value.Substring(0, i);
|
|
|
+ var denominator = value.Substring(i + 5);
|
|
|
+ if (IsSanctionedSingleUnitIdentifier(numerator) && IsSanctionedSingleUnitIdentifier(denominator))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static readonly HashSet<string> _sanctionedSingleUnitIdentifiers = new()
|
|
|
+ {
|
|
|
+ "acre",
|
|
|
+ "bit",
|
|
|
+ "byte",
|
|
|
+ "celsius",
|
|
|
+ "centimeter",
|
|
|
+ "day",
|
|
|
+ "degree",
|
|
|
+ "fahrenheit",
|
|
|
+ "fluid-ounce",
|
|
|
+ "foot",
|
|
|
+ "gallon",
|
|
|
+ "gigabit",
|
|
|
+ "gigabyte",
|
|
|
+ "gram",
|
|
|
+ "hectare",
|
|
|
+ "hour",
|
|
|
+ "inch",
|
|
|
+ "kilobit",
|
|
|
+ "kilobyte",
|
|
|
+ "kilogram",
|
|
|
+ "kilometer",
|
|
|
+ "liter",
|
|
|
+ "megabit",
|
|
|
+ "megabyte",
|
|
|
+ "meter",
|
|
|
+ "microsecond",
|
|
|
+ "mile",
|
|
|
+ "mile-scandinavian",
|
|
|
+ "milliliter",
|
|
|
+ "millimeter",
|
|
|
+ "millisecond",
|
|
|
+ "minute",
|
|
|
+ "month",
|
|
|
+ "nanosecond",
|
|
|
+ "ounce",
|
|
|
+ "percent",
|
|
|
+ "petabyte",
|
|
|
+ "pound",
|
|
|
+ "second",
|
|
|
+ "stone",
|
|
|
+ "terabit",
|
|
|
+ "terabyte",
|
|
|
+ "week",
|
|
|
+ "yard",
|
|
|
+ "year",
|
|
|
+ };
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-issanctionedsingleunitidentifier
|
|
|
+ /// </summary>
|
|
|
+ private static bool IsSanctionedSingleUnitIdentifier(string unitIdentifier) => _sanctionedSingleUnitIdentifiers.Contains(unitIdentifier);
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-iswellformedcurrencycode
|
|
|
+ /// </summary>
|
|
|
+ private static bool IsWellFormedCurrencyCode(JsValue currency)
|
|
|
+ {
|
|
|
+ var value = currency.ToString();
|
|
|
+ return value.Length == 3 && char.IsLetter(value[0]) && char.IsLetter(value[1]) && char.IsLetter(value[2]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-coerceoptionstoobject
|
|
|
+ /// </summary>
|
|
|
+ private ObjectInstance CoerceOptionsToObject(JsValue options)
|
|
|
+ {
|
|
|
+ if (options.IsUndefined())
|
|
|
+ {
|
|
|
+ return OrdinaryObjectCreate(_engine, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ return TypeConverter.ToObject(_realm, options);
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-canonicalizelocalelist
|
|
|
+ /// </summary>
|
|
|
+ private JsArray CanonicalizeLocaleList(JsValue locales)
|
|
|
+ {
|
|
|
+ return new JsArray(_engine);
|
|
|
+ // TODO
|
|
|
+ }
|
|
|
+
|
|
|
+ private enum OptionType
|
|
|
+ {
|
|
|
+ Boolean,
|
|
|
+ Number,
|
|
|
+ String
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// https://tc39.es/ecma402/#sec-getoption
|
|
|
+ /// </summary>
|
|
|
+ private JsValue GetOption<T>(JsValue options, string property, OptionType type, T[] values, T defaultValue) where T : JsValue
|
|
|
+ {
|
|
|
+ var value = options.Get(property);
|
|
|
+ if (value.IsUndefined())
|
|
|
+ {
|
|
|
+ if (defaultValue == "required")
|
|
|
+ {
|
|
|
+ ExceptionHelper.ThrowRangeError(_realm, "Required value missing");
|
|
|
+ }
|
|
|
+
|
|
|
+ return defaultValue;
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (type)
|
|
|
+ {
|
|
|
+ case OptionType.Boolean:
|
|
|
+ value = TypeConverter.ToBoolean(value);
|
|
|
+ break;
|
|
|
+ case OptionType.Number:
|
|
|
+ {
|
|
|
+ var number = TypeConverter.ToNumber(value);
|
|
|
+ if (double.IsNaN(number))
|
|
|
+ {
|
|
|
+ ExceptionHelper.ThrowRangeError(_realm, "Invalid number value");
|
|
|
+ }
|
|
|
+ value = number;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case OptionType.String:
|
|
|
+ value = TypeConverter.ToString(value);
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ ExceptionHelper.ThrowArgumentOutOfRangeException(nameof(type), "Unknown type");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (values.Length > 0 && System.Array.IndexOf(values, value) == -1)
|
|
|
+ {
|
|
|
+ ExceptionHelper.ThrowRangeError(_realm, "Value not part of list");
|
|
|
+ }
|
|
|
+
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+}
|