1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 02:47:34 +00:00

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.
This commit is contained in:
Timothy Flynn 2022-07-12 13:18:23 -04:00 committed by Linus Groh
parent cd8bcd06c6
commit 33698b9615
11 changed files with 589 additions and 75 deletions

View file

@ -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) \

View file

@ -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 {}") \

View file

@ -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)
{

View file

@ -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;

View file

@ -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();

View file

@ -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]]

View file

@ -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;
// 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<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 {};

View file

@ -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;
}

View file

@ -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();
});
});
});

View file

@ -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);
});
});
});

View file

@ -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)