From 36abcd820d1a5b899f67469d418985863d68232a Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Thu, 7 Jul 2022 10:01:00 -0400 Subject: LibJS: Implement Intl.PluralRules.prototype.select --- .../Libraries/LibJS/Runtime/CommonPropertyNames.h | 1 + .../Libraries/LibJS/Runtime/Intl/PluralRules.cpp | 111 +++++++++++++++++ .../Libraries/LibJS/Runtime/Intl/PluralRules.h | 4 + .../LibJS/Runtime/Intl/PluralRulesPrototype.cpp | 16 +++ .../LibJS/Runtime/Intl/PluralRulesPrototype.h | 1 + .../PluralRules/PluralRules.prototype.select.js | 137 +++++++++++++++++++++ 6 files changed, 270 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.select.js (limited to 'Userland/Libraries') diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 320c1ee618..46e0dbaf2b 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -408,6 +408,7 @@ namespace JS { P(seconds) \ P(secondsDisplay) \ P(segment) \ + P(select) \ P(sensitivity) \ P(set) \ P(setBigInt64) \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp index fae56e7453..b4d4bfe963 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp @@ -4,7 +4,10 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include +#include +#include namespace JS::Intl { @@ -14,4 +17,112 @@ PluralRules::PluralRules(Object& prototype) { } +// 16.5.1 GetOperands ( s ), https://tc39.es/ecma402/#sec-getoperands +Unicode::PluralOperands get_operands(String const& string) +{ + // 1.Let n be ! ToNumber(s). + char* end { nullptr }; + auto number = strtod(string.characters(), &end); + VERIFY(!*end); + + // 2. Assert: n is finite. + VERIFY(isfinite(number)); + + // 3. Let dp be StringIndexOf(s, ".", 0). + auto decimal_point = string.find('.'); + + Variant integer_part; + StringView fraction_slice; + + // 4. If dp = -1, then + if (!decimal_point.has_value()) { + // a. Let intPart be n. + integer_part = number; + + // b. Let fracSlice be "". + } + // 5. Else, + else { + // a. Let intPart be the substring of s from 0 to dp. + integer_part = string.substring_view(0, *decimal_point); + + // b. Let fracSlice be the substring of s from dp + 1. + fraction_slice = string.substring_view(*decimal_point + 1); + } + + // 6. Let i be abs(! ToNumber(intPart)). + auto integer = integer_part.visit( + [](Empty) -> u64 { VERIFY_NOT_REACHED(); }, + [](double value) { + return static_cast(fabs(value)); + }, + [](StringView value) { + auto value_as_int = value.template to_int().value(); + return static_cast(value_as_int); + }); + + // 7. Let fracDigitCount be the length of fracSlice. + auto fraction_digit_count = fraction_slice.length(); + + // 8. Let f be ! ToNumber(fracSlice). + auto fraction = fraction_slice.is_empty() ? 0u : fraction_slice.template to_uint().value(); + + // 9. Let significantFracSlice be the value of fracSlice stripped of trailing "0". + auto significant_fraction_slice = fraction_slice.trim("0"sv, TrimMode::Right); + + // 10. Let significantFracDigitCount be the length of significantFracSlice. + auto significant_fraction_digit_count = significant_fraction_slice.length(); + + // 11. Let significantFrac be ! ToNumber(significantFracSlice). + auto significant_fraction = significant_fraction_slice.is_empty() ? 0u : significant_fraction_slice.template to_uint().value(); + + // 12. Return a new Record { [[Number]]: abs(n), [[IntegerDigits]]: i, [[FractionDigits]]: f, [[NumberOfFractionDigits]]: fracDigitCount, [[FractionDigitsWithoutTrailing]]: significantFrac, [[NumberOfFractionDigitsWithoutTrailing]]: significantFracDigitCount }. + return Unicode::PluralOperands { + .number = fabs(number), + .integer_digits = integer, + .fraction_digits = fraction, + .number_of_fraction_digits = fraction_digit_count, + .fraction_digits_without_trailing = significant_fraction, + .number_of_fraction_digits_without_trailing = significant_fraction_digit_count, + }; +} + +// 16.5.2 PluralRuleSelect ( locale, type, n, operands ), https://tc39.es/ecma402/#sec-pluralruleselect +Unicode::PluralCategory plural_rule_select(StringView locale, Unicode::PluralForm type, Value, Unicode::PluralOperands operands) +{ + return Unicode::determine_plural_category(locale, type, move(operands)); +} + +// 16.5.3 ResolvePlural ( pluralRules, n ), https://tc39.es/ecma402/#sec-resolveplural +Unicode::PluralCategory resolve_plural(GlobalObject& global_object, PluralRules const& plural_rules, Value number) +{ + // 1. Assert: Type(pluralRules) is Object. + // 2. Assert: pluralRules has an [[InitializedPluralRules]] internal slot. + // 3. Assert: Type(n) is Number. + + // 4. If n is not a finite Number, then + if (!number.is_finite_number()) { + // a. Return "other". + return Unicode::plural_category_from_string("other"sv).value(); + } + + // 5. Let locale be pluralRules.[[Locale]]. + auto const& locale = plural_rules.locale(); + + // 6. Let type be pluralRules.[[Type]]. + auto type = plural_rules.type(); + + // 7. Let res be ! FormatNumericToString(pluralRules, n). + auto result = format_numeric_to_string(global_object, plural_rules, number); + + // 8. Let s be res.[[FormattedString]]. + auto const& string = result.formatted_string; + + // 9. Let operands be ! GetOperands(s). + auto operands = get_operands(string); + + // 10. Return ! PluralRuleSelect(locale, type, n, operands). + return plural_rule_select(locale, type, number, move(operands)); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h index 643bb459d8..32614e28e5 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h @@ -29,4 +29,8 @@ private: Unicode::PluralForm m_type { Unicode::PluralForm::Cardinal }; // [[Type]] }; +Unicode::PluralOperands get_operands(String const& string); +Unicode::PluralCategory plural_rule_select(StringView locale, Unicode::PluralForm type, Value number, Unicode::PluralOperands operands); +Unicode::PluralCategory resolve_plural(GlobalObject& global_object, PluralRules const& plural_rules, Value number); + } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp index 8793c10db0..fe2241c695 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp @@ -28,9 +28,25 @@ void PluralRulesPrototype::initialize(GlobalObject& global_object) define_direct_property(*vm.well_known_symbol_to_string_tag(), js_string(vm, "Intl.PluralRules"sv), Attribute::Configurable); u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.select, select, 1, attr); define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr); } +// 16.3.3 Intl.PluralRules.prototype.select ( value ), https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.select +JS_DEFINE_NATIVE_FUNCTION(PluralRulesPrototype::select) +{ + // 1. Let pr be the this value. + // 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]). + auto* plural_rules = TRY(typed_this_object(global_object)); + + // 3. Let n be ? ToNumber(value). + auto number = TRY(vm.argument(0).to_number(global_object)); + + // 4. Return ! ResolvePlural(pr, n). + auto plurality = resolve_plural(global_object, *plural_rules, number); + return js_string(vm, Unicode::plural_category_to_string(plurality)); +} + // 16.3.4 Intl.PluralRules.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.pluralrules.prototype.resolvedoptions JS_DEFINE_NATIVE_FUNCTION(PluralRulesPrototype::resolved_options) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.h b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.h index 67c3cb0486..dca4d31e69 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.h @@ -20,6 +20,7 @@ public: virtual ~PluralRulesPrototype() override = default; private: + JS_DECLARE_NATIVE_FUNCTION(select); JS_DECLARE_NATIVE_FUNCTION(resolved_options); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.select.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.select.js new file mode 100644 index 0000000000..ea284da38c --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.select.js @@ -0,0 +1,137 @@ +describe("errors", () => { + test("called on non-PluralRules object", () => { + expect(() => { + Intl.PluralRules.prototype.select(); + }).toThrowWithMessage(TypeError, "Not an object of type Intl.PluralRules"); + }); + + test("called with value that cannot be converted to a number", () => { + expect(() => { + new Intl.PluralRules().select(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + }); +}); + +describe("non-finite values", () => { + test("NaN", () => { + expect(new Intl.PluralRules("en").select(NaN)).toBe("other"); + expect(new Intl.PluralRules("ar").select(NaN)).toBe("other"); + expect(new Intl.PluralRules("pl").select(NaN)).toBe("other"); + }); + + test("Infinity", () => { + expect(new Intl.PluralRules("en").select(Infinity)).toBe("other"); + expect(new Intl.PluralRules("ar").select(Infinity)).toBe("other"); + expect(new Intl.PluralRules("pl").select(Infinity)).toBe("other"); + }); + + test("-Infinity", () => { + expect(new Intl.PluralRules("en").select(-Infinity)).toBe("other"); + expect(new Intl.PluralRules("ar").select(-Infinity)).toBe("other"); + expect(new Intl.PluralRules("pl").select(-Infinity)).toBe("other"); + }); +}); + +describe("correct behavior", () => { + test("cardinal", () => { + const en = new Intl.PluralRules("en", { type: "cardinal" }); + expect(en.select(0)).toBe("other"); + expect(en.select(1)).toBe("one"); + expect(en.select(2)).toBe("other"); + expect(en.select(3)).toBe("other"); + + // In "he": + // "many" is specified to be integers larger than 10 which are multiples of 10. + const he = new Intl.PluralRules("he", { type: "cardinal" }); + expect(he.select(0)).toBe("other"); + expect(he.select(1)).toBe("one"); + expect(en.select(2)).toBe("other"); + expect(he.select(10)).toBe("other"); + expect(he.select(19)).toBe("other"); + expect(he.select(20)).toBe("many"); + expect(he.select(21)).toBe("other"); + expect(he.select(29)).toBe("other"); + expect(he.select(30)).toBe("many"); + expect(he.select(31)).toBe("other"); + + // In "pl": + // "few" is specified to be integers such that (i % 10 == 2..4 && i % 100 != 12..14). + // "many" is specified to be all other integers != 1. + // "other" is specified to be non-integers. + const pl = new Intl.PluralRules("pl", { type: "cardinal" }); + expect(pl.select(0)).toBe("many"); + expect(pl.select(1)).toBe("one"); + expect(pl.select(2)).toBe("few"); + expect(pl.select(3)).toBe("few"); + expect(pl.select(4)).toBe("few"); + expect(pl.select(5)).toBe("many"); + expect(pl.select(12)).toBe("many"); + expect(pl.select(13)).toBe("many"); + expect(pl.select(14)).toBe("many"); + expect(pl.select(21)).toBe("many"); + expect(pl.select(22)).toBe("few"); + expect(pl.select(23)).toBe("few"); + expect(pl.select(24)).toBe("few"); + expect(pl.select(25)).toBe("many"); + expect(pl.select(3.14)).toBe("other"); + + // In "am": + // "one" is specified to be the integers 0 and 1, and non-integers whose integer part is 0. + const am = new Intl.PluralRules("am", { type: "cardinal" }); + expect(am.select(0)).toBe("one"); + expect(am.select(0.1)).toBe("one"); + expect(am.select(0.2)).toBe("one"); + expect(am.select(0.8)).toBe("one"); + expect(am.select(0.9)).toBe("one"); + expect(am.select(1)).toBe("one"); + expect(am.select(1.1)).toBe("other"); + expect(am.select(1.9)).toBe("other"); + expect(am.select(2)).toBe("other"); + expect(am.select(3)).toBe("other"); + }); + + test("ordinal", () => { + // In "en": + // "one" is specified to be integers such that (i % 10 == 1), excluding 11. + // "two" is specified to be integers such that (i % 10 == 2), excluding 12. + // "few" is specified to be integers such that (i % 10 == 3), excluding 13. + const en = new Intl.PluralRules("en", { type: "ordinal" }); + expect(en.select(0)).toBe("other"); + expect(en.select(1)).toBe("one"); + expect(en.select(2)).toBe("two"); + expect(en.select(3)).toBe("few"); + expect(en.select(4)).toBe("other"); + expect(en.select(10)).toBe("other"); + expect(en.select(11)).toBe("other"); + expect(en.select(12)).toBe("other"); + expect(en.select(13)).toBe("other"); + expect(en.select(14)).toBe("other"); + expect(en.select(20)).toBe("other"); + expect(en.select(21)).toBe("one"); + expect(en.select(22)).toBe("two"); + expect(en.select(23)).toBe("few"); + expect(en.select(24)).toBe("other"); + + // In "mk": + // "one" is specified to be integers such that (i % 10 == 1 && i % 100 != 11). + // "two" is specified to be integers such that (i % 10 == 2 && i % 100 != 12). + // "many" is specified to be integers such that (i % 10 == 7,8 && i % 100 != 17,18). + const mk = new Intl.PluralRules("mk", { type: "ordinal" }); + expect(mk.select(0)).toBe("other"); + expect(mk.select(1)).toBe("one"); + expect(mk.select(2)).toBe("two"); + expect(mk.select(3)).toBe("other"); + expect(mk.select(6)).toBe("other"); + expect(mk.select(7)).toBe("many"); + expect(mk.select(8)).toBe("many"); + expect(mk.select(9)).toBe("other"); + expect(mk.select(11)).toBe("other"); + expect(mk.select(12)).toBe("other"); + expect(mk.select(17)).toBe("other"); + expect(mk.select(18)).toBe("other"); + expect(mk.select(21)).toBe("one"); + expect(mk.select(22)).toBe("two"); + expect(mk.select(27)).toBe("many"); + expect(mk.select(28)).toBe("many"); + }); +}); -- cgit v1.2.3