From f089c11b5bc8365dcc87f9f6faf13c5dde121896 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Mon, 11 Jul 2022 11:28:10 -0400 Subject: [PATCH] LibJS: Implement Intl.PluralRules.prototype.selectRange --- .../LibJS/Runtime/CommonPropertyNames.h | 1 + Userland/Libraries/LibJS/Runtime/ErrorTypes.h | 2 + .../LibJS/Runtime/Intl/PluralRules.cpp | 42 +++++++++++ .../LibJS/Runtime/Intl/PluralRules.h | 3 + .../Runtime/Intl/PluralRulesPrototype.cpp | 28 +++++++ .../LibJS/Runtime/Intl/PluralRulesPrototype.h | 1 + .../PluralRules.prototype.selectRange.js | 73 +++++++++++++++++++ 7 files changed, 150 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.selectRange.js diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 46e0dbaf2b..3f31a1bef4 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -409,6 +409,7 @@ namespace JS { P(secondsDisplay) \ P(segment) \ P(select) \ + P(selectRange) \ P(sensitivity) \ P(set) \ P(setBigInt64) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index bb9698990e..f97f323626 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -43,8 +43,10 @@ M(IntlInvalidLanguageTag, "{} is not a structurally valid language tag") \ 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 {}") \ M(IntlStartTimeAfterEndTime, "Start time {} is after end time {}") \ M(IntlMinimumExceedsMaximum, "Minimum value {} is larger than maximum value {}") \ + M(IntlNumberIsNaN, "{} must not be NaN") \ M(IntlNumberIsNaNOrInfinity, "Number must not be NaN or Infinity") \ M(IntlNumberIsNaNOrOutOfRange, "Value {} is NaN or is not between {} and {}") \ M(IntlOptionUndefined, "Option {} must be defined when option {} is {}") \ diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp index 0abb1ed2bd..fd6a86a0e6 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp @@ -130,4 +130,46 @@ Unicode::PluralCategory resolve_plural(GlobalObject& global_object, NumberFormat return plural_rule_select(locale, type, number, move(operands)); } +// 1.1.5 PluralRuleSelectRange ( locale, type, xp, yp ), https://tc39.es/proposal-intl-numberformat-v3/out/pluralrules/proposed.html#sec-pluralruleselectrange +Unicode::PluralCategory plural_rule_select_range(StringView locale, Unicode::PluralForm, Unicode::PluralCategory start, Unicode::PluralCategory end) +{ + return Unicode::determine_plural_range(locale, start, end); +} + +// 1.1.6 ResolvePluralRange ( pluralRules, x, y ), https://tc39.es/proposal-intl-numberformat-v3/out/pluralrules/proposed.html#sec-resolvepluralrange +ThrowCompletionOr resolve_plural_range(GlobalObject& global_object, PluralRules const& plural_rules, Value start, Value end) +{ + auto& vm = global_object.vm(); + + // 1. Assert: Type(pluralRules) is Object. + // 2. Assert: pluralRules has an [[InitializedPluralRules]] internal slot. + // 3. Assert: Type(x) is Number. + // 4. Assert: Type(y) is Number. + + // 5. If x is NaN or y is NaN, throw a RangeError exception. + if (start.is_nan()) + return vm.throw_completion(global_object, ErrorType::IntlNumberIsNaN, "start"sv); + if (end.is_nan()) + return vm.throw_completion(global_object, ErrorType::IntlNumberIsNaN, "end"sv); + + // 6. If x > y, throw a RangeError exception. + if (start.as_double() > end.as_double()) + return vm.throw_completion(global_object, ErrorType::IntlStartRangeAfterEndRange, start, end); + + // 7. Let xp be ! ResolvePlural(pluralRules, x). + auto start_plurality = resolve_plural(global_object, plural_rules, start); + + // 8. Let yp be ! ResolvePlural(pluralRules, y). + auto end_plurality = resolve_plural(global_object, plural_rules, end); + + // 9. Let locale be pluralRules.[[Locale]]. + auto const& locale = plural_rules.locale(); + + // 10. Let type be pluralRules.[[Type]]. + auto type = plural_rules.type(); + + // 11. Return ! PluralRuleSelectRange(locale, type, xp, yp). + return plural_rule_select_range(locale, type, start_plurality, end_plurality); +} + } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h index 1c11b5087a..f8ce0d1c24 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -33,5 +34,7 @@ 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); Unicode::PluralCategory resolve_plural(GlobalObject& global_object, NumberFormatBase const& number_format, Unicode::PluralForm type, Value number); +Unicode::PluralCategory plural_rule_select_range(StringView locale, Unicode::PluralForm, Unicode::PluralCategory start, Unicode::PluralCategory end); +ThrowCompletionOr resolve_plural_range(GlobalObject& global_object, PluralRules const& plural_rules, Value start, Value end); } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp index fe2241c695..ccc272b64c 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.cpp @@ -29,6 +29,7 @@ void PluralRulesPrototype::initialize(GlobalObject& global_object) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(vm.names.select, select, 1, attr); + define_native_function(vm.names.selectRange, select_range, 2, attr); define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr); } @@ -47,6 +48,33 @@ JS_DEFINE_NATIVE_FUNCTION(PluralRulesPrototype::select) return js_string(vm, Unicode::plural_category_to_string(plurality)); } +// 1.4.4 Intl.PluralRules.prototype.selectRange ( start, end ), https://tc39.es/proposal-intl-numberformat-v3/out/pluralrules/proposed.html#sec-intl.pluralrules.prototype.selectrange +JS_DEFINE_NATIVE_FUNCTION(PluralRulesPrototype::select_range) +{ + auto start = vm.argument(0); + auto end = vm.argument(1); + + // 1. Let pr be the this value. + // 2. Perform ? RequireInternalSlot(pr, [[InitializedPluralRules]]). + auto* plural_rules = TRY(typed_this_object(global_object)); + + // 3. If start is undefined or end is undefined, throw a TypeError exception. + if (start.is_undefined()) + return vm.throw_completion(global_object, ErrorType::IsUndefined, "start"sv); + if (end.is_undefined()) + return vm.throw_completion(global_object, ErrorType::IsUndefined, "end"sv); + + // 4. Let x be ? ToNumber(start). + auto x = TRY(start.to_number(global_object)); + + // 5. Let y be ? ToNumber(end). + auto y = TRY(end.to_number(global_object)); + + // 6. Return ? ResolvePluralRange(pr, x, y). + auto plurality = TRY(resolve_plural_range(global_object, *plural_rules, x, y)); + 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 dca4d31e69..298dbfae8e 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesPrototype.h @@ -21,6 +21,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(select); + JS_DECLARE_NATIVE_FUNCTION(select_range); JS_DECLARE_NATIVE_FUNCTION(resolved_options); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.selectRange.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.selectRange.js new file mode 100644 index 0000000000..bea135ea6e --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/PluralRules/PluralRules.prototype.selectRange.js @@ -0,0 +1,73 @@ +describe("errors", () => { + test("called on non-PluralRules object", () => { + expect(() => { + Intl.PluralRules.prototype.selectRange(); + }).toThrowWithMessage(TypeError, "Not an object of type Intl.PluralRules"); + }); + + test("called without enough values", () => { + expect(() => { + new Intl.PluralRules().selectRange(); + }).toThrowWithMessage(TypeError, "start is undefined"); + + expect(() => { + new Intl.PluralRules().selectRange(1); + }).toThrowWithMessage(TypeError, "end is undefined"); + }); + + test("called with values that cannot be converted to numbers", () => { + expect(() => { + new Intl.PluralRules().selectRange(Symbol.hasInstance, 1); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + + expect(() => { + new Intl.PluralRules().selectRange(1, Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + }); + + test("called with invalid numbers", () => { + expect(() => { + new Intl.PluralRules().selectRange(NaN, 1); + }).toThrowWithMessage(RangeError, "start must not be NaN"); + + expect(() => { + new Intl.PluralRules().selectRange(1, NaN); + }).toThrowWithMessage(RangeError, "end must not be NaN"); + + expect(() => { + new Intl.PluralRules().selectRange(1, 0); + }).toThrowWithMessage(RangeError, "Range start 1 is greater than range end 0"); + }); +}); + +describe("correct behavior", () => { + test("basic functionality", () => { + const en = new Intl.PluralRules("en"); + expect(en.selectRange(1, 2)).toBe("other"); // one + other = other + expect(en.selectRange(0, 1)).toBe("other"); // other + one = other + expect(en.selectRange(2, 3)).toBe("other"); // other + other = other + + const pl = new Intl.PluralRules("pl"); + expect(pl.selectRange(1, 2)).toBe("few"); // one + few = few + expect(pl.selectRange(1, 5)).toBe("many"); // one + many = many + expect(pl.selectRange(1, 3.14)).toBe("other"); // one + other = other + expect(pl.selectRange(2, 2)).toBe("few"); // few + few = few + expect(pl.selectRange(2, 5)).toBe("many"); // few + many = many + expect(pl.selectRange(2, 3.14)).toBe("other"); // few + other = other + expect(pl.selectRange(0, 1)).toBe("one"); // many + one = one + expect(pl.selectRange(0, 2)).toBe("few"); // many + few = few + expect(pl.selectRange(0, 5)).toBe("many"); // many + many = many + expect(pl.selectRange(0, 3.14)).toBe("other"); // many + other = other + expect(pl.selectRange(0.14, 1)).toBe("one"); // other + one = one + expect(pl.selectRange(0.14, 2)).toBe("few"); // other + few = few + expect(pl.selectRange(0.14, 5)).toBe("many"); // other + many = many + expect(pl.selectRange(0.14, 3.14)).toBe("other"); // other + other = other + }); + + test("default to end of range", () => { + // "so" specifies "one" to be the integer 1, but does not specify any ranges. + const so = new Intl.PluralRules("so"); + expect(so.selectRange(0, 1)).toBe("one"); + expect(so.selectRange(1, 2)).toBe("other"); + }); +});