diff options
Diffstat (limited to 'Userland')
11 files changed, 585 insertions, 71 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<Object*> 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<StringOrBoolean> get_string_or_boolean_option(GlobalObject& global_object, Object const& options, PropertyKey const& property, Span<StringView const> 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<Optional<int>> default_number_option(GlobalObject& global_object, Value value, int minimum, int maximum, Optional<int> 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<StringView, bool>; + Optional<Unicode::LocaleID> 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<String> lookup_supported_locales(Vector<String> const& requested_locales) Vector<String> best_fit_supported_locales(Vector<String> const& requested_locales); ThrowCompletionOr<Array*> supported_locales(GlobalObject&, Vector<String> const& requested_locales, Value options); ThrowCompletionOr<Object*> coerce_options_to_object(GlobalObject& global_object, Value options); +ThrowCompletionOr<StringOrBoolean> get_string_or_boolean_option(GlobalObject& global_object, Object const& options, PropertyKey const& property, Span<StringView const> values, StringOrBoolean true_value, StringOrBoolean falsy_value, StringOrBoolean fallback); ThrowCompletionOr<Optional<int>> default_number_option(GlobalObject& global_object, Value value, int minimum, int maximum, Optional<int> fallback); ThrowCompletionOr<Optional<int>> get_number_option(GlobalObject& global_object, Object const& options, PropertyKey const& property, int minimum, int maximum, Optional<int> fallback); Vector<PatternPartition> partition_pattern(StringView pattern); +template<size_t Size> +ThrowCompletionOr<StringOrBoolean> 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<StringView const> { 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<PatternPartition> 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<Variant<StringView, String>> 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<int> m_min_fraction_digits {}; // [[MinimumFractionDigits]] - Optional<int> m_max_fraction_digits {}; // [[MaximumFractionDigits]] - Optional<int> m_min_significant_digits {}; // [[MinimumSignificantDigits]] - Optional<int> 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<int> m_min_fraction_digits {}; // [[MinimumFractionDigits]] + Optional<int> m_max_fraction_digits {}; // [[MaximumFractionDigits]] + Optional<int> m_min_significant_digits {}; // [[MinimumSignificantDigits]] + Optional<int> 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<CurrencySign> m_currency_sign {}; // [[CurrencySign]] Optional<String> m_unit {}; // [[Unit]] Optional<Unicode::Style> m_unit_display {}; // [[UnitDisplay]] - bool m_use_grouping { false }; // [[UseGrouping]] + UseGrouping m_use_grouping { false }; // [[UseGrouping]] Notation m_notation { Notation::Invalid }; // [[Notation]] Optional<CompactDisplay> 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<NumberFormat*> 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<NumberFormat*> 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<RangeError>(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<TypeError>(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<RangeError>(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<void> 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<void> 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; + + // 13. Let needFd be true. + bool need_fraction_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)); + // 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. 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; + } + } - // 14. If needSd is true, then + // 15. If needSd is true, then if (need_significant_digits) { - // a. Assert: hasSd is true. - VERIFY(has_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)); - // b. 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)); - // 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)); + // iii. Set intlObj.[[MinimumSignificantDigits]] to mnsd. + intl_object.set_min_significant_digits(*min_digits); - // d. 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); - // e. Set intlObj.[[MaximumSignificantDigits]] to mxsd. - intl_object.set_max_significant_digits(*max_digits); + // ii. Set intlObj.[[MaximumSignificantDigits]] to 21. + intl_object.set_max_significant_digits(21); + } } - // 15. If needFd is true, then + // 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<void> 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()))); + + 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; + } - // 5. Return options. + // 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<JS::Object*>& seen_objects) |