using Jint.Native.Function; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Descriptors; namespace Jint.Native.Intl; /// /// https://tc39.es/ecma402/#sec-intl-numberformat-constructor /// 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(); } /// /// https://tc39.es/ecma402/#sec-intl.numberformat /// 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( newTarget, static intrinsics => intrinsics.NumberFormat.PrototypeObject, static (engine, _, _) => new JsObject(engine)); InitializeNumberFormat(numberFormat, locales, options); return numberFormat; } /// /// https://tc39.es/ecma402/#sec-initializenumberformat /// 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(), 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(), JsBoolean.True); numberFormat["UseGrouping"] = useGrouping; var signDisplay = GetOption(options, "signDisplay", OptionType.String, new JsValue[] { "auto", "never", "always", "exceptZero" }, "auto"); numberFormat["SignDisplay"] = signDisplay; return numberFormat; } /// /// https://tc39.es/ecma402/#sec-resolvelocale /// private JsObject ResolveLocale(object availableLocales, JsArray requestedLocales, JsObject options, object relevantExtensionKeys, object localeData) { // TODO var result = new JsObject(_engine); return result; } /// /// https://tc39.es/ecma402/#sec-setnfdigitoptions /// private static void SetNumberFormatDigitOptions(JsObject numberFormat, ObjectInstance options, int mnfdDefault, int mxfdDefault, string notation) { // TODO } /// /// https://tc39.es/ecma402/#sec-currencydigits /// private static int CurrencyDigits(JsValue currency) { // TODO return 2; } /// /// https://tc39.es/ecma402/#sec-setnumberformatunitoptions /// 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(), 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(), 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; } } /// /// https://tc39.es/ecma402/#sec-iswellformedunitidentifier /// private static bool IsWellFormedUnitIdentifier(JsValue unitIdentifier) { var value = unitIdentifier.ToString(); if (IsSanctionedSingleUnitIdentifier(value)) { return true; } var i = value.IndexOf("-per-", StringComparison.Ordinal); if (i == -1 || value.IndexOf("-per-", i + 1, StringComparison.Ordinal) != -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 _sanctionedSingleUnitIdentifiers = new(StringComparer.Ordinal) { "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", }; /// /// https://tc39.es/ecma402/#sec-issanctionedsingleunitidentifier /// private static bool IsSanctionedSingleUnitIdentifier(string unitIdentifier) => _sanctionedSingleUnitIdentifiers.Contains(unitIdentifier); /// /// https://tc39.es/ecma402/#sec-iswellformedcurrencycode /// 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]); } /// /// https://tc39.es/ecma402/#sec-coerceoptionstoobject /// private ObjectInstance CoerceOptionsToObject(JsValue options) { if (options.IsUndefined()) { return OrdinaryObjectCreate(_engine, null); } return TypeConverter.ToObject(_realm, options); } /// /// https://tc39.es/ecma402/#sec-canonicalizelocalelist /// private JsArray CanonicalizeLocaleList(JsValue locales) { return new JsArray(_engine); // TODO } private enum OptionType { Boolean, Number, String } /// /// https://tc39.es/ecma402/#sec-getoption /// private JsValue GetOption(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; } }