From 33698b961542618e64c6aede5213c3b0624399d0 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 12 Jul 2022 13:18:23 -0400 Subject: [PATCH] LibJS+js: Parse new constructor options from Intl.NumberFormat V3 This contains minimal changes to parse newly added and modified options from the Intl.NumberFormat V3 proposal, while maintaining main spec behavior in Intl.NumberFormat.prototype.format. The parsed options are reflected only in Intl.NumberFormat.prototype.resolvedOptions and the js REPL. --- .../LibJS/Runtime/CommonPropertyNames.h | 2 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 3 + .../LibJS/Runtime/Intl/AbstractOperations.cpp | 33 ++++ .../LibJS/Runtime/Intl/AbstractOperations.h | 9 + .../LibJS/Runtime/Intl/NumberFormat.cpp | 123 +++++++++++- .../LibJS/Runtime/Intl/NumberFormat.h | 69 +++++-- .../Runtime/Intl/NumberFormatConstructor.cpp | 182 +++++++++++++----- .../Runtime/Intl/NumberFormatPrototype.cpp | 25 ++- .../Intl/NumberFormat/NumberFormat.js | 122 +++++++++++- .../NumberFormat.prototype.resolvedOptions.js | 88 ++++++++- Userland/Utilities/js.cpp | 8 +- 11 files changed, 589 insertions(+), 75 deletions(-) diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 3f31a1bef4..fa43ef2a49 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -402,6 +402,7 @@ namespace JS { P(round) \ P(roundingIncrement) \ P(roundingMode) \ + P(roundingPriority) \ P(script) \ P(seal) \ P(second) \ @@ -514,6 +515,7 @@ namespace JS { P(toZonedDateTime) \ P(toZonedDateTimeISO) \ P(trace) \ + P(trailingZeroDisplay) \ P(trim) \ P(trimEnd) \ P(trimLeft) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 0cf24d2edb..4bf8692948 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -43,6 +43,9 @@ M(IntlInvalidDateTimeFormatOption, "Option {} cannot be set when also providing {}") \ M(IntlInvalidKey, "{} is not a valid key") \ M(IntlInvalidLanguageTag, "{} is not a structurally valid language tag") \ + M(IntlInvalidRoundingIncrement, "{} is not a valid rounding increment") \ + M(IntlInvalidRoundingIncrementForFractionDigits, "{} is not a valid rounding increment for inequal min/max fraction digits") \ + M(IntlInvalidRoundingIncrementForRoundingType, "{} is not a valid rounding increment for rounding type {}") \ M(IntlInvalidTime, "Time value must be between -8.64E15 and 8.64E15") \ M(IntlInvalidUnit, "Unit {} is not a valid time unit") \ M(IntlStartRangeAfterEndRange, "Range start {} is greater than range end {}") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp index 403a774493..b22bf44c7c 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.cpp @@ -605,6 +605,39 @@ ThrowCompletionOr coerce_options_to_object(GlobalObject& global_object, // NOTE: 9.2.13 GetOption has been removed and is being pulled in from ECMA-262 in the Temporal proposal. +// 1.2.12 GetStringOrBooleanOption ( options, property, values, trueValue, falsyValue, fallback ), https://tc39.es/proposal-intl-numberformat-v3/out/negotiation/proposed.html#sec-getstringorbooleanoption +ThrowCompletionOr get_string_or_boolean_option(GlobalObject& global_object, Object const& options, PropertyKey const& property, Span values, StringOrBoolean true_value, StringOrBoolean falsy_value, StringOrBoolean fallback) +{ + // 1. Let value be ? Get(options, property). + auto value = TRY(options.get(property)); + + // 2. If value is undefined, return fallback. + if (value.is_undefined()) + return fallback; + + // 3. If value is true, return trueValue. + if (value.is_boolean() && value.as_bool()) + return true_value; + + // 4. Let valueBoolean be ToBoolean(value). + auto value_boolean = value.to_boolean(); + + // 5. If valueBoolean is false, return falsyValue. + if (!value_boolean) + return falsy_value; + + // 6. Let value be ? ToString(value). + auto value_string = TRY(value.to_string(global_object)); + + // 7. If values does not contain an element equal to value, return fallback. + auto it = find(values.begin(), values.end(), value_string); + if (it == values.end()) + return fallback; + + // 8. Return value. + return StringOrBoolean { *it }; +} + // 9.2.14 DefaultNumberOption ( value, minimum, maximum, fallback ), https://tc39.es/ecma402/#sec-defaultnumberoption ThrowCompletionOr> default_number_option(GlobalObject& global_object, Value value, int minimum, int maximum, Optional fallback) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h index 561d909aef..93122774b2 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -65,6 +65,8 @@ constexpr auto extra_sanctioned_single_unit_identifiers() return AK::Array { "microsecond"sv, "nanosecond"sv }; } +using StringOrBoolean = Variant; + Optional is_structurally_valid_language_tag(StringView locale); String canonicalize_unicode_locale_id(Unicode::LocaleID& locale); bool is_well_formed_currency_code(StringView currency); @@ -77,10 +79,17 @@ Vector lookup_supported_locales(Vector const& requested_locales) Vector best_fit_supported_locales(Vector const& requested_locales); ThrowCompletionOr supported_locales(GlobalObject&, Vector const& requested_locales, Value options); ThrowCompletionOr coerce_options_to_object(GlobalObject& global_object, Value options); +ThrowCompletionOr get_string_or_boolean_option(GlobalObject& global_object, Object const& options, PropertyKey const& property, Span values, StringOrBoolean true_value, StringOrBoolean falsy_value, StringOrBoolean fallback); ThrowCompletionOr> default_number_option(GlobalObject& global_object, Value value, int minimum, int maximum, Optional fallback); ThrowCompletionOr> get_number_option(GlobalObject& global_object, Object const& options, PropertyKey const& property, int minimum, int maximum, Optional fallback); Vector partition_pattern(StringView pattern); +template +ThrowCompletionOr get_string_or_boolean_option(GlobalObject& global_object, Object const& options, PropertyKey const& property, StringView const (&values)[Size], StringOrBoolean true_value, StringOrBoolean falsy_value, StringOrBoolean fallback) +{ + return get_string_or_boolean_option(global_object, options, property, Span { values }, move(true_value), move(falsy_value), move(fallback)); +} + // NOTE: ECMA-402's GetOption is being removed in favor of a shared ECMA-262 GetOption in the Temporal proposal. // Until Temporal is merged into ECMA-262, our implementation lives in the Temporal-specific AO file & namespace. using Temporal::get_option; diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index 6863ce6b34..bbf38d3ea3 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -161,11 +161,122 @@ StringView NumberFormatBase::rounding_type_string() const return "fractionDigits"sv; case RoundingType::CompactRounding: return "compactRounding"sv; + case RoundingType::MorePrecision: + return "morePrecision"sv; + case RoundingType::LessPrecision: + return "lessPrecision"sv; default: VERIFY_NOT_REACHED(); } } +StringView NumberFormatBase::rounding_mode_string() const +{ + switch (m_rounding_mode) { + case RoundingMode::Ceil: + return "ceil"sv; + case RoundingMode::Expand: + return "expand"sv; + case RoundingMode::Floor: + return "floor"sv; + case RoundingMode::HalfCeil: + return "halfCeil"sv; + case RoundingMode::HalfEven: + return "halfEven"sv; + case RoundingMode::HalfExpand: + return "halfExpand"sv; + case RoundingMode::HalfFloor: + return "halfFloor"sv; + case RoundingMode::HalfTrunc: + return "halfTrunc"sv; + case RoundingMode::Trunc: + return "trunc"sv; + default: + VERIFY_NOT_REACHED(); + } +} + +void NumberFormatBase::set_rounding_mode(StringView rounding_mode) +{ + if (rounding_mode == "ceil"sv) + m_rounding_mode = RoundingMode::Ceil; + else if (rounding_mode == "expand"sv) + m_rounding_mode = RoundingMode::Expand; + else if (rounding_mode == "floor"sv) + m_rounding_mode = RoundingMode::Floor; + else if (rounding_mode == "halfCeil"sv) + m_rounding_mode = RoundingMode::HalfCeil; + else if (rounding_mode == "halfEven"sv) + m_rounding_mode = RoundingMode::HalfEven; + else if (rounding_mode == "halfExpand"sv) + m_rounding_mode = RoundingMode::HalfExpand; + else if (rounding_mode == "halfFloor"sv) + m_rounding_mode = RoundingMode::HalfFloor; + else if (rounding_mode == "halfTrunc"sv) + m_rounding_mode = RoundingMode::HalfTrunc; + else if (rounding_mode == "trunc"sv) + m_rounding_mode = RoundingMode::Trunc; +} + +StringView NumberFormatBase::trailing_zero_display_string() const +{ + switch (m_trailing_zero_display) { + case TrailingZeroDisplay::Auto: + return "auto"sv; + case TrailingZeroDisplay::StripIfInteger: + return "stripIfInteger"sv; + default: + VERIFY_NOT_REACHED(); + } +} + +void NumberFormatBase::set_trailing_zero_display(StringView trailing_zero_display) +{ + if (trailing_zero_display == "auto"sv) + m_trailing_zero_display = TrailingZeroDisplay::Auto; + else if (trailing_zero_display == "stripIfInteger"sv) + m_trailing_zero_display = TrailingZeroDisplay::StripIfInteger; + else + VERIFY_NOT_REACHED(); +} + +Value NumberFormat::use_grouping_to_value(GlobalObject& global_object) const +{ + auto& vm = global_object.vm(); + + switch (m_use_grouping) { + case UseGrouping::Always: + return js_string(vm, "always"sv); + case UseGrouping::Auto: + return js_string(vm, "auto"sv); + case UseGrouping::Min2: + return js_string(vm, "min2"sv); + case UseGrouping::False: + return Value(false); + default: + VERIFY_NOT_REACHED(); + } +} + +void NumberFormat::set_use_grouping(StringOrBoolean const& use_grouping) +{ + use_grouping.visit( + [this](StringView grouping) { + if (grouping == "always"sv) + m_use_grouping = UseGrouping::Always; + else if (grouping == "auto"sv) + m_use_grouping = UseGrouping::Auto; + else if (grouping == "min2"sv) + m_use_grouping = UseGrouping::Min2; + else + VERIFY_NOT_REACHED(); + }, + [this](bool grouping) { + VERIFY(!grouping); + m_use_grouping = UseGrouping::False; + }); +} + void NumberFormat::set_notation(StringView notation) { if (notation == "standard"sv) @@ -230,6 +341,8 @@ void NumberFormat::set_sign_display(StringView sign_display) m_sign_display = SignDisplay::Always; else if (sign_display == "exceptZero"sv) m_sign_display = SignDisplay::ExceptZero; + else if (sign_display == "negative"sv) + m_sign_display = SignDisplay::Negative; else VERIFY_NOT_REACHED(); } @@ -245,6 +358,8 @@ StringView NumberFormat::sign_display_string() const return "always"sv; case SignDisplay::ExceptZero: return "exceptZero"sv; + case SignDisplay::Negative: + return "negative"sv; default: VERIFY_NOT_REACHED(); } @@ -372,6 +487,8 @@ FormatResult format_numeric_to_string(GlobalObject& global_object, NumberFormatB break; // 5. Else, + case NumberFormatBase::RoundingType::MorePrecision: // FIXME: Handle this case for NumberFormat V3. + case NumberFormatBase::RoundingType::LessPrecision: // FIXME: Handle this case for NumberFormat V3. case NumberFormatBase::RoundingType::CompactRounding: // a. Assert: intlObject.[[RoundingType]] is compactRounding. // b. Let result be ToRawPrecision(x, 1, 2). @@ -662,7 +779,8 @@ Vector partition_notation_sub_pattern(GlobalObject& global_obj // b. Let fraction be undefined. } - bool use_grouping = number_format.use_grouping(); + // FIXME: Handle all NumberFormat V3 [[UseGrouping]] options. + bool use_grouping = number_format.use_grouping() != NumberFormat::UseGrouping::False; // FIXME: The spec doesn't indicate this, but grouping should be disabled for numbers less than 10,000 when the notation is compact. // This is addressed in Intl.NumberFormat V3 with the "min2" [[UseGrouping]] option. However, test262 explicitly expects this @@ -1174,7 +1292,8 @@ Optional> get_number_format_pattern(GlobalObject& gl break; default: - VERIFY_NOT_REACHED(); + // FIXME: Handle all NumberFormat V3 [[SignDisplay]] options. + return {}; } found_pattern = patterns.release_value(); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h index 1f08fe8ae5..56caf70875 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h @@ -24,7 +24,28 @@ public: Invalid, SignificantDigits, FractionDigits, - CompactRounding, + CompactRounding, // FIXME: Remove this when corresponding AOs are updated for NumberFormat V3. + MorePrecision, + LessPrecision, + }; + + enum class RoundingMode { + Invalid, + Ceil, + Expand, + Floor, + HalfCeil, + HalfEven, + HalfExpand, + HalfFloor, + HalfTrunc, + Trunc, + }; + + enum class TrailingZeroDisplay { + Invalid, + Auto, + StripIfInteger, }; NumberFormatBase(Object& prototype); @@ -59,15 +80,29 @@ public: StringView rounding_type_string() const; void set_rounding_type(RoundingType rounding_type) { m_rounding_type = rounding_type; } + RoundingMode rounding_mode() const { return m_rounding_mode; } + StringView rounding_mode_string() const; + void set_rounding_mode(StringView rounding_mode); + + int rounding_increment() const { return m_rounding_increment; } + void set_rounding_increment(int rounding_increment) { m_rounding_increment = rounding_increment; } + + TrailingZeroDisplay trailing_zero_display() const { return m_trailing_zero_display; } + StringView trailing_zero_display_string() const; + void set_trailing_zero_display(StringView trailing_zero_display); + private: - String m_locale; // [[Locale]] - String m_data_locale; // [[DataLocale]] - int m_min_integer_digits { 0 }; // [[MinimumIntegerDigits]] - Optional m_min_fraction_digits {}; // [[MinimumFractionDigits]] - Optional m_max_fraction_digits {}; // [[MaximumFractionDigits]] - Optional m_min_significant_digits {}; // [[MinimumSignificantDigits]] - Optional m_max_significant_digits {}; // [[MaximumSignificantDigits]] - RoundingType m_rounding_type { RoundingType::Invalid }; // [[RoundingType]] + String m_locale; // [[Locale]] + String m_data_locale; // [[DataLocale]] + int m_min_integer_digits { 0 }; // [[MinimumIntegerDigits]] + Optional m_min_fraction_digits {}; // [[MinimumFractionDigits]] + Optional m_max_fraction_digits {}; // [[MaximumFractionDigits]] + Optional m_min_significant_digits {}; // [[MinimumSignificantDigits]] + Optional m_max_significant_digits {}; // [[MaximumSignificantDigits]] + RoundingType m_rounding_type { RoundingType::Invalid }; // [[RoundingType]] + RoundingMode m_rounding_mode { RoundingMode::Invalid }; // [[RoundingMode]] + int m_rounding_increment { 1 }; // [[RoundingIncrement]] + TrailingZeroDisplay m_trailing_zero_display { TrailingZeroDisplay::Invalid }; // [[TrailingZeroDisplay]] }; class NumberFormat final : public NumberFormatBase { @@ -120,6 +155,15 @@ public: Never, Always, ExceptZero, + Negative, + }; + + enum class UseGrouping { + Invalid, + Always, + Auto, + Min2, + False, }; static constexpr auto relevant_extension_keys() @@ -163,8 +207,9 @@ public: StringView unit_display_string() const { return Unicode::style_to_string(*m_unit_display); } void set_unit_display(StringView unit_display) { m_unit_display = Unicode::style_from_string(unit_display); } - bool use_grouping() const { return m_use_grouping; } - void set_use_grouping(bool use_grouping) { m_use_grouping = use_grouping; } + UseGrouping use_grouping() const { return m_use_grouping; } + Value use_grouping_to_value(GlobalObject&) const; + void set_use_grouping(StringOrBoolean const& use_grouping); Notation notation() const { return m_notation; } StringView notation_string() const; @@ -198,7 +243,7 @@ private: Optional m_currency_sign {}; // [[CurrencySign]] Optional m_unit {}; // [[Unit]] Optional m_unit_display {}; // [[UnitDisplay]] - bool m_use_grouping { false }; // [[UseGrouping]] + UseGrouping m_use_grouping { false }; // [[UseGrouping]] Notation m_notation { Notation::Invalid }; // [[Notation]] Optional m_compact_display {}; // [[CompactDisplay]] SignDisplay m_sign_display { SignDisplay::Invalid }; // [[SignDisplay]] diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp index 58a9a3cfa2..bc7fef857f 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp @@ -80,6 +80,7 @@ JS_DEFINE_NATIVE_FUNCTION(NumberFormatConstructor::supported_locales_of) } // 15.1.2 InitializeNumberFormat ( numberFormat, locales, options ), https://tc39.es/ecma402/#sec-initializenumberformat +// 1.1.2 InitializeNumberFormat ( numberFormat, locales, options ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-initializenumberformat ThrowCompletionOr initialize_number_format(GlobalObject& global_object, NumberFormat& number_format, Value locales_value, Value options_value) { auto& vm = global_object.vm(); @@ -170,32 +171,71 @@ ThrowCompletionOr initialize_number_format(GlobalObject& global_o // 20. Perform ? SetNumberFormatDigitOptions(numberFormat, options, mnfdDefault, mxfdDefault, notation). TRY(set_number_format_digit_options(global_object, number_format, *options, default_min_fraction_digits, default_max_fraction_digits, number_format.notation())); - // 21. Let compactDisplay be ? GetOption(options, "compactDisplay", "string", « "short", "long" », "short"). + // 21. Let roundingIncrement be ? GetNumberOption(options, "roundingIncrement", 1, 5000, 1). + auto rounding_increment = TRY(get_number_option(global_object, *options, vm.names.roundingIncrement, 1, 5000, 1)); + + // 22. If roundingIncrement is not in « 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000 », throw a RangeError exception. + static constexpr auto sanctioned_rounding_increments = AK::Array { 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000 }; + + if (!sanctioned_rounding_increments.span().contains_slow(*rounding_increment)) + return vm.throw_completion(global_object, ErrorType::IntlInvalidRoundingIncrement, *rounding_increment); + + // 23. If roundingIncrement is not 1 and numberFormat.[[RoundingType]] is not fractionDigits, throw a TypeError exception. + if ((rounding_increment != 1) && (number_format.rounding_type() != NumberFormatBase::RoundingType::FractionDigits)) + return vm.throw_completion(global_object, ErrorType::IntlInvalidRoundingIncrementForRoundingType, *rounding_increment, number_format.rounding_type_string()); + + // 24. If roundingIncrement is not 1 and numberFormat.[[MaximumFractionDigits]] is not equal to numberFormat.[[MinimumFractionDigits]], throw a RangeError exception. + if ((rounding_increment != 1) && (number_format.max_fraction_digits() != number_format.min_fraction_digits())) + return vm.throw_completion(global_object, ErrorType::IntlInvalidRoundingIncrementForFractionDigits, *rounding_increment); + + // 25. Set numberFormat.[[RoundingIncrement]] to roundingIncrement. + number_format.set_rounding_increment(*rounding_increment); + + // 26. Let trailingZeroDisplay be ? GetOption(options, "trailingZeroDisplay", "string", « "auto", "stripIfInteger" », "auto"). + auto trailing_zero_display = TRY(get_option(global_object, *options, vm.names.trailingZeroDisplay, OptionType::String, { "auto"sv, "stripIfInteger"sv }, "auto"sv)); + + // 27. Set numberFormat.[[TrailingZeroDisplay]] to trailingZeroDisplay. + number_format.set_trailing_zero_display(trailing_zero_display.as_string().string()); + + // 28. Let compactDisplay be ? GetOption(options, "compactDisplay", "string", « "short", "long" », "short"). auto compact_display = TRY(get_option(global_object, *options, vm.names.compactDisplay, OptionType::String, { "short"sv, "long"sv }, "short"sv)); - // 22. If notation is "compact", then + // 29. Let defaultUseGrouping be "auto". + auto default_use_grouping = "auto"sv; + + // 30. If notation is "compact", then if (number_format.notation() == NumberFormat::Notation::Compact) { // a. Set numberFormat.[[CompactDisplay]] to compactDisplay. number_format.set_compact_display(compact_display.as_string().string()); + + // b. Set defaultUseGrouping to "min2". + default_use_grouping = "min2"sv; } - // 23. Let useGrouping be ? GetOption(options, "useGrouping", "boolean", undefined, true). - auto use_grouping = TRY(get_option(global_object, *options, vm.names.useGrouping, OptionType::Boolean, {}, true)); + // 31. Let useGrouping be ? GetStringOrBooleanOption(options, "useGrouping", « "min2", "auto", "always" », "always", false, defaultUseGrouping). + auto use_grouping = TRY(get_string_or_boolean_option(global_object, *options, vm.names.useGrouping, { "min2"sv, "auto"sv, "always"sv }, "always"sv, false, default_use_grouping)); - // 24. Set numberFormat.[[UseGrouping]] to useGrouping. - number_format.set_use_grouping(use_grouping.as_bool()); + // 32. Set numberFormat.[[UseGrouping]] to useGrouping. + number_format.set_use_grouping(use_grouping); - // 25. Let signDisplay be ? GetOption(options, "signDisplay", "string", « "auto", "never", "always", "exceptZero" », "auto"). - auto sign_display = TRY(get_option(global_object, *options, vm.names.signDisplay, OptionType::String, { "auto"sv, "never"sv, "always"sv, "exceptZero"sv }, "auto"sv)); + // 33. Let signDisplay be ? GetOption(options, "signDisplay", "string", « "auto", "never", "always", "exceptZero, "negative" », "auto"). + auto sign_display = TRY(get_option(global_object, *options, vm.names.signDisplay, OptionType::String, { "auto"sv, "never"sv, "always"sv, "exceptZero"sv, "negative"sv }, "auto"sv)); - // 26. Set numberFormat.[[SignDisplay]] to signDisplay. + // 34. Set numberFormat.[[SignDisplay]] to signDisplay. number_format.set_sign_display(sign_display.as_string().string()); - // 27. Return numberFormat. + // 35. Let roundingMode be ? GetOption(options, "roundingMode", "string", « "ceil", "floor", "expand", "trunc", "halfCeil", "halfFloor", "halfExpand", "halfTrunc", "halfEven" », "halfExpand"). + auto rounding_mode = TRY(get_option(global_object, *options, vm.names.roundingMode, OptionType::String, { "ceil"sv, "floor"sv, "expand"sv, "trunc"sv, "halfCeil"sv, "halfFloor"sv, "halfExpand"sv, "halfTrunc"sv, "halfEven"sv }, "halfExpand"sv)); + + // 36. Set numberFormat.[[RoundingMode]] to roundingMode. + number_format.set_rounding_mode(rounding_mode.as_string().string()); + + // 37. Return numberFormat. return &number_format; } // 15.1.3 SetNumberFormatDigitOptions ( intlObj, options, mnfdDefault, mxfdDefault, notation ), https://tc39.es/ecma402/#sec-setnfdigitoptions +// 1.1.1 SetNumberFormatDigitOptions ( intlObj, options, mnfdDefault, mxfdDefault, notation ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-setnfdigitoptions ThrowCompletionOr set_number_format_digit_options(GlobalObject& global_object, NumberFormatBase& intl_object, Object const& options, int default_min_fraction_digits, int default_max_fraction_digits, NumberFormat::Notation notation) { auto& vm = global_object.vm(); @@ -218,46 +258,66 @@ ThrowCompletionOr set_number_format_digit_options(GlobalObject& global_obj // 6. Set intlObj.[[MinimumIntegerDigits]] to mnid. intl_object.set_min_integer_digits(*min_integer_digits); - // 7. If mnsd is not undefined or mxsd is not undefined, then + // 7. Let roundingPriority be ? GetOption(options, "roundingPriority", "string", « "auto", "morePrecision", "lessPrecision" », "auto"). + auto rounding_priority = TRY(get_option(global_object, options, vm.names.roundingPriority, OptionType::String, { "auto"sv, "morePrecision"sv, "lessPrecision"sv }, "auto"sv)); + + // 8. If mnsd is not undefined or mxsd is not undefined, then // a. Let hasSd be true. - // 8. Else, + // 9. Else, // a. Let hasSd be false. bool has_significant_digits = !min_significant_digits.is_undefined() || !max_significant_digits.is_undefined(); - // 9. If mnfd is not undefined or mxfd is not undefined, then + // 10. If mnfd is not undefined or mxfd is not undefined, then // a. Let hasFd be true. - // 10. Else, + // 11. Else, // a. Let hasFd be false. bool has_fraction_digits = !min_fraction_digits.is_undefined() || !max_fraction_digits.is_undefined(); - // 11. Let needSd be hasSd. - bool need_significant_digits = has_significant_digits; + // 12. Let needSd be true. + bool need_significant_digits = true; - // 12. If hasSd is true, or hasFd is false and notation is "compact", then - // a. Let needFd be false. - // 13. Else, - // a. Let needFd be true. - bool need_fraction_digits = !has_significant_digits && (has_fraction_digits || (notation != NumberFormat::Notation::Compact)); + // 13. Let needFd be true. + bool need_fraction_digits = true; - // 14. If needSd is true, then - if (need_significant_digits) { - // a. Assert: hasSd is true. - VERIFY(has_significant_digits); + // 14. If roundingPriority is "auto", then + if (rounding_priority.as_string().string() == "auto"sv) { + // a. Set needSd to hasSd. + need_significant_digits = has_significant_digits; - // b. Set mnsd to ? DefaultNumberOption(mnsd, 1, 21, 1). - auto min_digits = TRY(default_number_option(global_object, min_significant_digits, 1, 21, 1)); - - // c. Set mxsd to ? DefaultNumberOption(mxsd, mnsd, 21, 21). - auto max_digits = TRY(default_number_option(global_object, max_significant_digits, *min_digits, 21, 21)); - - // d. Set intlObj.[[MinimumSignificantDigits]] to mnsd. - intl_object.set_min_significant_digits(*min_digits); - - // e. Set intlObj.[[MaximumSignificantDigits]] to mxsd. - intl_object.set_max_significant_digits(*max_digits); + // b. If hasSd is true, or hasFd is false and notation is "compact", then + if (has_significant_digits || (!has_fraction_digits && notation == NumberFormat::Notation::Compact)) { + // i. Set needFd to false. + need_fraction_digits = false; + } } - // 15. If needFd is true, then + // 15. If needSd is true, then + if (need_significant_digits) { + // a. If hasSd is true, then + if (has_significant_digits) { + // i. Set mnsd to ? DefaultNumberOption(mnsd, 1, 21, 1). + auto min_digits = TRY(default_number_option(global_object, min_significant_digits, 1, 21, 1)); + + // ii. Set mxsd to ? DefaultNumberOption(mxsd, mnsd, 21, 21). + auto max_digits = TRY(default_number_option(global_object, max_significant_digits, *min_digits, 21, 21)); + + // iii. Set intlObj.[[MinimumSignificantDigits]] to mnsd. + intl_object.set_min_significant_digits(*min_digits); + + // iv. Set intlObj.[[MaximumSignificantDigits]] to mxsd. + intl_object.set_max_significant_digits(*max_digits); + } + // b. Else, + else { + // i. Set intlObj.[[MinimumSignificantDigits]] to 1. + intl_object.set_min_significant_digits(1); + + // ii. Set intlObj.[[MaximumSignificantDigits]] to 21. + intl_object.set_max_significant_digits(21); + } + } + + // 16. If needFd is true, then if (need_fraction_digits) { // a. If hasFd is true, then if (has_fraction_digits) { @@ -293,20 +353,46 @@ ThrowCompletionOr set_number_format_digit_options(GlobalObject& global_obj } } - // 16. If needSd is false and needFd is false, then - if (!need_significant_digits && !need_fraction_digits) { - // a. Set intlObj.[[RoundingType]] to compactRounding. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::CompactRounding); - } - // 17. Else if hasSd is true, then - else if (has_significant_digits) { - // a. Set intlObj.[[RoundingType]] to significantDigits. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::SignificantDigits); + // 17. If needSd is true or needFd is true, then + if (need_significant_digits || need_fraction_digits) { + // a. If roundingPriority is "morePrecision", then + if (rounding_priority.as_string().string() == "morePrecision"sv) { + // i. Set intlObj.[[RoundingType]] to morePrecision. + intl_object.set_rounding_type(NumberFormatBase::RoundingType::MorePrecision); + } + // b. Else if roundingPriority is "lessPrecision", then + else if (rounding_priority.as_string().string() == "lessPrecision"sv) { + // i. Set intlObj.[[RoundingType]] to lessPrecision. + intl_object.set_rounding_type(NumberFormatBase::RoundingType::LessPrecision); + } + // c. Else if hasSd is true, then + else if (has_significant_digits) { + // i. Set intlObj.[[RoundingType]] to significantDigits. + intl_object.set_rounding_type(NumberFormatBase::RoundingType::SignificantDigits); + } + // d. Else, + else { + // i. Set intlObj.[[RoundingType]] to fractionDigits. + intl_object.set_rounding_type(NumberFormatBase::RoundingType::FractionDigits); + } } + // 18. Else, else { - // a. Set intlObj.[[RoundingType]] to fractionDigits. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::FractionDigits); + // a. Set intlObj.[[RoundingType]] to morePrecision. + intl_object.set_rounding_type(NumberFormatBase::RoundingType::MorePrecision); + + // b. Set intlObj.[[MinimumFractionDigits]] to 0. + intl_object.set_min_fraction_digits(0); + + // c. Set intlObj.[[MaximumFractionDigits]] to 0. + intl_object.set_max_fraction_digits(0); + + // d. Set intlObj.[[MinimumSignificantDigits]] to 1. + intl_object.set_min_significant_digits(1); + + // e. Set intlObj.[[MaximumSignificantDigits]] to 2. + intl_object.set_max_significant_digits(2); } return {}; diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp index 9f9d4771aa..907b384fdd 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp @@ -114,13 +114,34 @@ JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::resolved_options) MUST(options->create_data_property_or_throw(vm.names.minimumSignificantDigits, Value(number_format->min_significant_digits()))); if (number_format->has_max_significant_digits()) MUST(options->create_data_property_or_throw(vm.names.maximumSignificantDigits, Value(number_format->max_significant_digits()))); - MUST(options->create_data_property_or_throw(vm.names.useGrouping, Value(number_format->use_grouping()))); + MUST(options->create_data_property_or_throw(vm.names.useGrouping, number_format->use_grouping_to_value(global_object))); MUST(options->create_data_property_or_throw(vm.names.notation, js_string(vm, number_format->notation_string()))); if (number_format->has_compact_display()) MUST(options->create_data_property_or_throw(vm.names.compactDisplay, js_string(vm, number_format->compact_display_string()))); MUST(options->create_data_property_or_throw(vm.names.signDisplay, js_string(vm, number_format->sign_display_string()))); + MUST(options->create_data_property_or_throw(vm.names.roundingMode, js_string(vm, number_format->rounding_mode_string()))); + MUST(options->create_data_property_or_throw(vm.names.roundingIncrement, Value(number_format->rounding_increment()))); + MUST(options->create_data_property_or_throw(vm.names.trailingZeroDisplay, js_string(vm, number_format->trailing_zero_display_string()))); - // 5. Return options. + switch (number_format->rounding_type()) { + // 6. If nf.[[RoundingType]] is morePrecision, then + case NumberFormatBase::RoundingType::MorePrecision: + // a. Perform ! CreateDataPropertyOrThrow(options, "roundingPriority", "morePrecision"). + MUST(options->create_data_property_or_throw(vm.names.roundingPriority, js_string(vm, "morePrecision"sv))); + break; + // 7. Else if nf.[[RoundingType]] is lessPrecision, then + case NumberFormatBase::RoundingType::LessPrecision: + // a. Perform ! CreateDataPropertyOrThrow(options, "roundingPriority", "lessPrecision"). + MUST(options->create_data_property_or_throw(vm.names.roundingPriority, js_string(vm, "lessPrecision"sv))); + break; + // 8. Else, + default: + // a. Perform ! CreateDataPropertyOrThrow(options, "roundingPriority", "auto"). + MUST(options->create_data_property_or_throw(vm.names.roundingPriority, js_string(vm, "auto"sv))); + break; + } + + // 9. Return options. return options; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.js index d56577db7e..46a6509175 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.js @@ -208,6 +208,70 @@ describe("errors", () => { new Intl.NumberFormat("en", { signDisplay: "hello!" }); }).toThrowWithMessage(RangeError, "hello! is not a valid value for option signDisplay"); }); + + test("roundingPriority option is invalid", () => { + expect(() => { + new Intl.NumberFormat("en", { roundingPriority: "hello!" }); + }).toThrowWithMessage( + RangeError, + "hello! is not a valid value for option roundingPriority" + ); + }); + + test("roundingMode option is invalid", () => { + expect(() => { + new Intl.NumberFormat("en", { roundingMode: "hello!" }); + }).toThrowWithMessage(RangeError, "hello! is not a valid value for option roundingMode"); + }); + + test("roundingIncrement option is invalid", () => { + expect(() => { + new Intl.NumberFormat("en", { roundingIncrement: "hello!" }); + }).toThrowWithMessage(RangeError, "Value NaN is NaN or is not between 1 and 5000"); + + expect(() => { + new Intl.NumberFormat("en", { roundingIncrement: 0 }); + }).toThrowWithMessage(RangeError, "Value 0 is NaN or is not between 1 and 5000"); + + expect(() => { + new Intl.NumberFormat("en", { roundingIncrement: 5001 }); + }).toThrowWithMessage(RangeError, "Value 5001 is NaN or is not between 1 and 5000"); + + expect(() => { + new Intl.NumberFormat("en", { + roundingIncrement: 3, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + }).toThrowWithMessage(RangeError, "3 is not a valid rounding increment"); + + expect(() => { + new Intl.NumberFormat("en", { roundingIncrement: 5, minimumSignificantDigits: 1 }); + }).toThrowWithMessage( + TypeError, + "5 is not a valid rounding increment for rounding type significantDigits" + ); + + expect(() => { + new Intl.NumberFormat("en", { + roundingIncrement: 5, + minimumFractionDigits: 2, + maximumFractionDigits: 3, + }); + }).toThrowWithMessage( + RangeError, + "5 is not a valid rounding increment for inequal min/max fraction digits" + ); + }); + + test("trailingZeroDisplay option is invalid", () => { + expect(() => { + new Intl.NumberFormat("en", { trailingZeroDisplay: "hello!" }); + }).toThrowWithMessage( + RangeError, + "hello! is not a valid value for option trailingZeroDisplay" + ); + }); }); describe("normal behavior", () => { @@ -344,10 +408,66 @@ describe("normal behavior", () => { }); test("all valid signDisplay options", () => { - ["auto", "never", "always", "exceptZero"].forEach(signDisplay => { + ["auto", "never", "always", "exceptZero", "negative"].forEach(signDisplay => { expect(() => { new Intl.NumberFormat("en", { signDisplay: signDisplay }); }).not.toThrow(); }); }); + + test("valid useGrouping options", () => { + ["min2", "auto", "always", false, true, ""].forEach(useGrouping => { + expect(() => { + new Intl.NumberFormat("en", { useGrouping: useGrouping }); + }).not.toThrow(); + }); + }); + + test("all valid roundingPriority options", () => { + ["auto", "morePrecision", "lessPrecision"].forEach(roundingPriority => { + expect(() => { + new Intl.NumberFormat("en", { roundingPriority: roundingPriority }); + }).not.toThrow(); + }); + }); + + test("all valid roundingMode options", () => { + [ + "ceil", + "floor", + "expand", + "trunc", + "halfCeil", + "halfFloor", + "halfExpand", + "halfTrunc", + "halfEven", + ].forEach(roundingMode => { + expect(() => { + new Intl.NumberFormat("en", { roundingMode: roundingMode }); + }).not.toThrow(); + }); + }); + + test("all valid roundingIncrement options", () => { + [1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000].forEach( + roundingIncrement => { + expect(() => { + new Intl.NumberFormat("en", { + roundingIncrement: roundingIncrement, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + }).not.toThrow(); + } + ); + }); + + test("all valid trailingZeroDisplay options", () => { + ["auto", "stripIfInteger"].forEach(trailingZeroDisplay => { + expect(() => { + new Intl.NumberFormat("en", { trailingZeroDisplay: trailingZeroDisplay }); + }).not.toThrow(); + }); + }); }); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.resolvedOptions.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.resolvedOptions.js index 70ebdde2df..8dafec921c 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.resolvedOptions.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.resolvedOptions.js @@ -176,12 +176,12 @@ describe("correct behavior", () => { }); }); - test("compact notation causes all min/max digits to be undefined by default", () => { + test("compact notation causes all min/max digits to be set to default values", () => { const en = new Intl.NumberFormat("en", { notation: "compact" }); - expect(en.resolvedOptions().minimumFractionDigits).toBeUndefined(); - expect(en.resolvedOptions().maximumFractionDigits).toBeUndefined(); - expect(en.resolvedOptions().minimumSignificantDigits).toBeUndefined(); - expect(en.resolvedOptions().maximumSignificantDigits).toBeUndefined(); + expect(en.resolvedOptions().minimumFractionDigits).toBe(0); + expect(en.resolvedOptions().maximumFractionDigits).toBe(0); + expect(en.resolvedOptions().minimumSignificantDigits).toBe(1); + expect(en.resolvedOptions().maximumSignificantDigits).toBe(2); }); test("currency display and sign only defined when style is currency", () => { @@ -276,19 +276,89 @@ describe("correct behavior", () => { test("use grouping", () => { const en1 = new Intl.NumberFormat("en"); - expect(en1.resolvedOptions().useGrouping).toBeTrue(); + expect(en1.resolvedOptions().useGrouping).toBe("auto"); - const en2 = new Intl.NumberFormat("en", { useGrouping: false }); - expect(en2.resolvedOptions().useGrouping).toBeFalse(); + const en2 = new Intl.NumberFormat("en", { notation: "compact" }); + expect(en2.resolvedOptions().useGrouping).toBe("min2"); + + const en3 = new Intl.NumberFormat("en", { useGrouping: false }); + expect(en3.resolvedOptions().useGrouping).toBeFalse(); + + const en4 = new Intl.NumberFormat("en", { useGrouping: true }); + expect(en4.resolvedOptions().useGrouping).toBe("always"); + + ["auto", "always", "min2"].forEach(useGrouping => { + const en5 = new Intl.NumberFormat("en", { useGrouping: useGrouping }); + expect(en5.resolvedOptions().useGrouping).toBe(useGrouping); + }); }); test("sign display", () => { const en1 = new Intl.NumberFormat("en"); expect(en1.resolvedOptions().signDisplay).toBe("auto"); - ["auto", "never", "always", "exceptZero"].forEach(signDisplay => { + ["auto", "never", "always", "exceptZero", "negative"].forEach(signDisplay => { const en2 = new Intl.NumberFormat("en", { signDisplay: signDisplay }); expect(en2.resolvedOptions().signDisplay).toBe(signDisplay); }); }); + + test("rounding priority", () => { + const en1 = new Intl.NumberFormat("en"); + expect(en1.resolvedOptions().roundingPriority).toBe("auto"); + + const en2 = new Intl.NumberFormat("en", { notation: "compact" }); + expect(en2.resolvedOptions().roundingPriority).toBe("morePrecision"); + + ["auto", "morePrecision", "lessPrecision"].forEach(roundingPriority => { + const en3 = new Intl.NumberFormat("en", { roundingPriority: roundingPriority }); + expect(en3.resolvedOptions().roundingPriority).toBe(roundingPriority); + }); + }); + + test("rounding mode", () => { + const en1 = new Intl.NumberFormat("en"); + expect(en1.resolvedOptions().roundingMode).toBe("halfExpand"); + + [ + "ceil", + "floor", + "expand", + "trunc", + "halfCeil", + "halfFloor", + "halfExpand", + "halfTrunc", + "halfEven", + ].forEach(roundingMode => { + const en2 = new Intl.NumberFormat("en", { roundingMode: roundingMode }); + expect(en2.resolvedOptions().roundingMode).toBe(roundingMode); + }); + }); + + test("rounding increment", () => { + const en1 = new Intl.NumberFormat("en"); + expect(en1.resolvedOptions().roundingIncrement).toBe(1); + + [1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 2500, 5000].forEach( + roundingIncrement => { + const en2 = new Intl.NumberFormat("en", { + roundingIncrement: roundingIncrement, + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }); + expect(en2.resolvedOptions().roundingIncrement).toBe(roundingIncrement); + } + ); + }); + + test("trailing zero display", () => { + const en1 = new Intl.NumberFormat("en"); + expect(en1.resolvedOptions().trailingZeroDisplay).toBe("auto"); + + ["auto", "stripIfInteger"].forEach(trailingZeroDisplay => { + const en2 = new Intl.NumberFormat("en", { trailingZeroDisplay: trailingZeroDisplay }); + expect(en2.resolvedOptions().trailingZeroDisplay).toBe(trailingZeroDisplay); + }); + }); }); diff --git a/Userland/Utilities/js.cpp b/Userland/Utilities/js.cpp index cb71990442..1d3b1b5b2d 100644 --- a/Userland/Utilities/js.cpp +++ b/Userland/Utilities/js.cpp @@ -747,9 +747,13 @@ static void print_intl_number_format(JS::Intl::NumberFormat const& number_format print_value(JS::Value(number_format.max_significant_digits()), seen_objects); } js_out("\n useGrouping: "); - print_value(JS::Value(number_format.use_grouping()), seen_objects); + print_value(number_format.use_grouping_to_value(number_format.global_object()), seen_objects); js_out("\n roundingType: "); print_value(js_string(number_format.vm(), number_format.rounding_type_string()), seen_objects); + js_out("\n roundingMode: "); + print_value(js_string(number_format.vm(), number_format.rounding_mode_string()), seen_objects); + js_out("\n roundingIncrement: "); + print_value(JS::Value(number_format.rounding_increment()), seen_objects); js_out("\n notation: "); print_value(js_string(number_format.vm(), number_format.notation_string()), seen_objects); if (number_format.has_compact_display()) { @@ -758,6 +762,8 @@ static void print_intl_number_format(JS::Intl::NumberFormat const& number_format } js_out("\n signDisplay: "); print_value(js_string(number_format.vm(), number_format.sign_display_string()), seen_objects); + js_out("\n trailingZeroDisplay: "); + print_value(js_string(number_format.vm(), number_format.trailing_zero_display_string()), seen_objects); } static void print_intl_date_time_format(JS::Intl::DateTimeFormat& date_time_format, HashTable& seen_objects)