LibJS: Add notation to Intl.PluralRules

This is a normative change in the ECMA-402 spec. See:
a7ff535
This commit is contained in:
Timothy Flynn 2025-05-27 08:05:19 -04:00 committed by Tim Flynn
commit 8e5cc74eb1
Notes: github-actions[bot] 2025-05-27 14:40:25 +00:00
9 changed files with 101 additions and 29 deletions

View file

@ -717,6 +717,8 @@ ErrorOr<void> print_intl_plural_rules(JS::PrintContext& print_context, JS::Intl:
TRY(print_value(print_context, JS::PrimitiveString::create(plural_rules.vm(), plural_rules.locale()), seen_objects));
TRY(js_out(print_context, "\n type: "));
TRY(print_value(print_context, JS::PrimitiveString::create(plural_rules.vm(), plural_rules.type_string()), seen_objects));
TRY(js_out(print_context, "\n notation: "));
TRY(print_value(print_context, JS::PrimitiveString::create(plural_rules.vm(), plural_rules.notation_string()), seen_objects));
TRY(js_out(print_context, "\n minimumIntegerDigits: "));
TRY(print_value(print_context, JS::Value(plural_rules.min_integer_digits()), seen_objects));
if (plural_rules.has_min_fraction_digits()) {

View file

@ -111,20 +111,28 @@ Unicode::RoundingOptions NumberFormatBase::rounding_options() const
};
}
Unicode::DisplayOptions NumberFormatBase::display_options() const
{
Unicode::DisplayOptions options;
options.notation = m_notation;
options.compact_display = m_compact_display;
return options;
}
Unicode::DisplayOptions NumberFormat::display_options() const
{
return {
.style = m_style,
.sign_display = m_sign_display,
.notation = m_notation,
.compact_display = m_compact_display,
.grouping = m_use_grouping,
.currency = m_currency,
.currency_display = m_currency_display,
.currency_sign = m_currency_sign,
.unit = m_unit,
.unit_display = m_unit_display,
};
auto options = Base::display_options();
options.style = m_style;
options.sign_display = m_sign_display;
options.grouping = m_use_grouping;
options.currency = m_currency;
options.currency_display = m_currency_display;
options.currency_sign = m_currency_sign;
options.unit = m_unit;
options.unit_display = m_unit_display;
return options;
}
// 16.5.1 CurrencyDigits ( currency ), https://tc39.es/ecma402/#sec-currencydigits

View file

@ -52,6 +52,15 @@ public:
int max_significant_digits() const { return *m_max_significant_digits; }
void set_max_significant_digits(int max_significant_digits) { m_max_significant_digits = max_significant_digits; }
Unicode::Notation notation() const { return m_notation; }
StringView notation_string() const { return Unicode::notation_to_string(m_notation); }
void set_notation(StringView notation) { m_notation = Unicode::notation_from_string(notation); }
bool has_compact_display() const { return m_compact_display.has_value(); }
Unicode::CompactDisplay compact_display() const { return *m_compact_display; }
StringView compact_display_string() const { return Unicode::compact_display_to_string(*m_compact_display); }
void set_compact_display(StringView compact_display) { m_compact_display = Unicode::compact_display_from_string(compact_display); }
Unicode::RoundingType rounding_type() const { return m_rounding_type; }
StringView rounding_type_string() const { return Unicode::rounding_type_to_string(m_rounding_type); }
void set_rounding_type(Unicode::RoundingType rounding_type) { m_rounding_type = rounding_type; }
@ -71,6 +80,7 @@ public:
StringView trailing_zero_display_string() const { return Unicode::trailing_zero_display_to_string(m_trailing_zero_display); }
void set_trailing_zero_display(StringView trailing_zero_display) { m_trailing_zero_display = Unicode::trailing_zero_display_from_string(trailing_zero_display); }
virtual Unicode::DisplayOptions display_options() const;
Unicode::RoundingOptions rounding_options() const;
Unicode::NumberFormat const& formatter() const { return *m_formatter; }
@ -86,6 +96,8 @@ private:
Optional<int> m_max_fraction_digits {}; // [[MaximumFractionDigits]]
Optional<int> m_min_significant_digits {}; // [[MinimumSignificantDigits]]
Optional<int> m_max_significant_digits {}; // [[MaximumSignificantDigits]]
Unicode::Notation m_notation; // [[Notation]]
Optional<Unicode::CompactDisplay> m_compact_display; // [[CompactDisplay]]
Unicode::RoundingType m_rounding_type; // [[RoundingType]]
ComputedRoundingPriority m_computed_rounding_priority { ComputedRoundingPriority::Invalid }; // [[ComputedRoundingPriority]]
Unicode::RoundingMode m_rounding_mode; // [[RoundingMode]]
@ -140,15 +152,6 @@ public:
Value use_grouping_to_value(VM&) const;
void set_use_grouping(StringOrBoolean const& use_grouping);
Unicode::Notation notation() const { return m_notation; }
StringView notation_string() const { return Unicode::notation_to_string(m_notation); }
void set_notation(StringView notation) { m_notation = Unicode::notation_from_string(notation); }
bool has_compact_display() const { return m_compact_display.has_value(); }
Unicode::CompactDisplay compact_display() const { return *m_compact_display; }
StringView compact_display_string() const { return Unicode::compact_display_to_string(*m_compact_display); }
void set_compact_display(StringView compact_display) { m_compact_display = Unicode::compact_display_from_string(compact_display); }
Unicode::SignDisplay sign_display() const { return m_sign_display; }
StringView sign_display_string() const { return Unicode::sign_display_to_string(m_sign_display); }
void set_sign_display(StringView sign_display) { m_sign_display = Unicode::sign_display_from_string(sign_display); }
@ -156,7 +159,7 @@ public:
NativeFunction* bound_format() const { return m_bound_format; }
void set_bound_format(NativeFunction* bound_format) { m_bound_format = bound_format; }
Unicode::DisplayOptions display_options() const;
Unicode::DisplayOptions display_options() const override;
private:
explicit NumberFormat(Object& prototype);
@ -172,8 +175,6 @@ private:
Optional<String> m_unit; // [[Unit]]
Optional<Unicode::Style> m_unit_display; // [[UnitDisplay]]
Unicode::Grouping m_use_grouping { Unicode::Grouping::False }; // [[UseGrouping]]
Unicode::Notation m_notation; // [[Notation]]
Optional<Unicode::CompactDisplay> m_compact_display; // [[CompactDisplay]]
Unicode::SignDisplay m_sign_display; // [[SignDisplay]]
GC::Ptr<NativeFunction> m_bound_format; // [[BoundFormat]]
};

View file

@ -45,8 +45,9 @@ Unicode::PluralCategory resolve_plural(PluralRules const& plural_rules, Value nu
// 3. Let s be res.[[FormattedString]].
// 4. Let locale be pluralRules.[[Locale]].
// 5. Let type be pluralRules.[[Type]].
// 6. Let p be PluralRuleSelect(locale, type, s).
// 7. Return the Record { [[PluralCategory]]: p, [[FormattedString]]: s }.
// 6. Let notation be pluralRules.[[Notation]].
// 7. Let p be PluralRuleSelect(locale, type, notation, s).
// 8. Return the Record { [[PluralCategory]]: p, [[FormattedString]]: s }.
return plural_rules.formatter().select_plural(number.as_double());
}
@ -65,7 +66,8 @@ ThrowCompletionOr<Unicode::PluralCategory> resolve_plural_range(VM& vm, PluralRu
// a. Return xp.[[PluralCategory]].
// 5. Let locale be pluralRules.[[Locale]].
// 6. Let type be pluralRules.[[Type]].
// 7. Return PluralRuleSelectRange(locale, type, xp.[[PluralCategory]], yp.[[PluralCategory]]).
// 7. Let notation be pluralRules.[[Notation]].
// 8. Return PluralRuleSelectRange(locale, type, notation, xp.[[PluralCategory]], yp.[[PluralCategory]]).
return plural_rules.formatter().select_plural_range(start.as_double(), end.as_double());
}

View file

@ -52,7 +52,7 @@ ThrowCompletionOr<GC::Ref<Object>> PluralRulesConstructor::construct(FunctionObj
auto locales_value = vm.argument(0);
auto options_value = vm.argument(1);
// 2. Let pluralRules be ? OrdinaryCreateFromConstructor(NewTarget, "%Intl.PluralRules.prototype%", « [[InitializedPluralRules]], [[Locale]], [[Type]], [[MinimumIntegerDigits]], [[MinimumFractionDigits]], [[MaximumFractionDigits]], [[MinimumSignificantDigits]], [[MaximumSignificantDigits]], [[RoundingType]], [[RoundingIncrement]], [[RoundingMode]], [[ComputedRoundingPriority]], [[TrailingZeroDisplay]] »).
// 2. Let pluralRules be ? OrdinaryCreateFromConstructor(NewTarget, "%Intl.PluralRules.prototype%", « [[InitializedPluralRules]], [[Locale]], [[Type]], [[Notation]], [[MinimumIntegerDigits]], [[MinimumFractionDigits]], [[MaximumFractionDigits]], [[MinimumSignificantDigits]], [[MaximumSignificantDigits]], [[RoundingType]], [[RoundingIncrement]], [[RoundingMode]], [[ComputedRoundingPriority]], [[TrailingZeroDisplay]] »).
auto plural_rules = TRY(ordinary_create_from_constructor<PluralRules>(vm, new_target, &Intrinsics::intl_plural_rules_prototype));
// 3. Let optionsResolution be ? ResolveOptions(%Intl.PluralRules%, %Intl.PluralRules%.[[LocaleData]], locales, options, « COERCE-OPTIONS »).
@ -69,13 +69,26 @@ ThrowCompletionOr<GC::Ref<Object>> PluralRulesConstructor::construct(FunctionObj
// 8. Set pluralRules.[[Type]] to t.
plural_rules->set_type(type.as_string().utf8_string_view());
// 9. Let notation be ? GetOption(options, "notation", string, « "standard", "scientific", "engineering", "compact" », "standard").
auto notation = TRY(get_option(vm, *options, vm.names.notation, OptionType::String, { "standard"sv, "scientific"sv, "engineering"sv, "compact"sv }, "standard"sv));
// 10. Set pluralRules.[[Notation]] to notation.
plural_rules->set_notation(notation.as_string().utf8_string_view());
// 9. Perform ? SetNumberFormatDigitOptions(pluralRules, options, 0, 3, "standard").
TRY(set_number_format_digit_options(vm, plural_rules, *options, 0, 3, Unicode::Notation::Standard));
// FIXME: Spec issue: The patch which added `notation` to Intl.PluralRules neglected to also add `compactDisplay`
// for when the notation is compact. TC39 is planning to add this option soon. We default to `short` for now
// to match the Intl.NumberFormat default, as LibUnicode requires the compact display to be set. See:
// https://github.com/tc39/ecma402/pull/989#issuecomment-2906752480
if (plural_rules->notation() == Unicode::Notation::Compact)
plural_rules->set_compact_display("short"sv);
// Non-standard, create an ICU number formatter for this Intl object.
auto formatter = Unicode::NumberFormat::create(
result.icu_locale,
{},
plural_rules->display_options(),
plural_rules->rounding_options());
formatter->create_plural_rules(plural_rules->type());

View file

@ -66,6 +66,7 @@ JS_DEFINE_NATIVE_FUNCTION(PluralRulesPrototype::resolved_options)
// i. Perform ! CreateDataPropertyOrThrow(options, p, v).
MUST(options->create_data_property_or_throw(vm.names.locale, PrimitiveString::create(vm, plural_rules->locale())));
MUST(options->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, plural_rules->type_string())));
MUST(options->create_data_property_or_throw(vm.names.notation, PrimitiveString::create(vm, plural_rules->notation_string())));
MUST(options->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value(plural_rules->min_integer_digits())));
if (plural_rules->has_min_fraction_digits())
MUST(options->create_data_property_or_throw(vm.names.minimumFractionDigits, Value(plural_rules->min_fraction_digits())));

View file

@ -117,6 +117,12 @@ describe("errors", () => {
}).toThrowWithMessage(RangeError, "Value 22 is NaN or is not between 1 and 21");
});
test("notation option is invalid ", () => {
expect(() => {
new Intl.PluralRules("en", { notation: "hello!" });
}).toThrowWithMessage(RangeError, "hello! is not a valid value for option notation");
});
test("roundingPriority option is invalid", () => {
expect(() => {
new Intl.PluralRules("en", { roundingPriority: "hello!" });
@ -239,6 +245,14 @@ describe("normal behavior", () => {
}
});
test("all valid notation options", () => {
["standard", "scientific", "engineering", "compact"].forEach(notation => {
expect(() => {
new Intl.PluralRules("en", { notation: notation });
}).not.toThrow();
});
});
test("all valid roundingPriority options", () => {
["auto", "morePrecision", "lessPrecision"].forEach(roundingPriority => {
expect(() => {

View file

@ -78,6 +78,16 @@ describe("correct behavior", () => {
expect(en3.resolvedOptions().maximumSignificantDigits).toBe(10);
});
test("notation", () => {
const en1 = new Intl.PluralRules("en");
expect(en1.resolvedOptions().notation).toBe("standard");
["standard", "scientific", "engineering", "compact"].forEach(notation => {
const en2 = new Intl.PluralRules("en", { notation: notation });
expect(en2.resolvedOptions().notation).toBe(notation);
});
});
test("plural categories", () => {
const enCardinal = new Intl.PluralRules("en", { type: "cardinal" }).resolvedOptions();
expect(enCardinal.pluralCategories).toEqual(["one", "other"]);

View file

@ -139,4 +139,25 @@ describe("correct behavior", () => {
expect(mk.select(27)).toBe("many");
expect(mk.select(28)).toBe("many");
});
test("notation", () => {
const standard = new Intl.PluralRules("fr", { notation: "standard" });
const engineering = new Intl.PluralRules("fr", { notation: "engineering" });
const scientific = new Intl.PluralRules("fr", { notation: "scientific" });
const compact = new Intl.PluralRules("fr", { notation: "compact" });
// prettier-ignore
const data = [
{ value: 1e6, standard: "many", engineering: "many", scientific: "many", compact: "many" },
{ value: 1.5e6, standard: "other", engineering: "many", scientific: "many", compact: "many" },
{ value: 1e-6, standard: "one", engineering: "many", scientific: "many", compact: "one" },
];
data.forEach(d => {
expect(standard.select(d.value)).toBe(d.standard);
expect(engineering.select(d.value)).toBe(d.engineering);
expect(scientific.select(d.value)).toBe(d.scientific);
expect(compact.select(d.value)).toBe(d.compact);
});
});
});