diff options
author | Timothy Flynn <trflynn89@pm.me> | 2022-07-12 14:30:30 -0400 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-07-13 19:22:26 +0100 |
commit | 733192089fac01ac9f4128bdc393a72812317766 (patch) | |
tree | 61eb4d254fe0cdc15a3f6c5a9f584d37100ac389 | |
parent | cd4ee46b706e23ffd7efe57034f95e2d6090987a (diff) | |
download | serenity-733192089fac01ac9f4128bdc393a72812317766.zip |
LibJS: Implement Intl.NumberFormat V3's [[UseGrouping]] changes
In the main spec, [[UseGrouping]] can be true or false. In V3, it may be
one of:
auto: Respect the per-locale preference for grouping.
always: Ignore per-locale preference for grouping and always insert
the grouping separator (note: true is now an alias for always).
min2: Ignore per-locale preference for grouping and only insert the
grouping separator if the primary group has at least 2 digits.
false: Ignore per-locale preference for grouping and never insert
the grouping separator.
3 files changed, 142 insertions, 45 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index bbf38d3ea3..0fc47fb469 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -428,13 +428,6 @@ static ALWAYS_INLINE bool is_greater_than(Value number, i64 rhs) return number.as_bigint().big_integer() > Crypto::SignedBigInteger::create_from(rhs); } -static ALWAYS_INLINE bool is_greater_than_or_equal(Value number, i64 rhs) -{ - if (number.is_number()) - return number.as_double() >= rhs; - return number.as_bigint().big_integer() >= Crypto::SignedBigInteger::create_from(rhs); -} - static ALWAYS_INLINE bool is_less_than(Value number, i64 rhs) { if (number.is_number()) @@ -682,15 +675,31 @@ Vector<PatternPartition> partition_number_pattern(GlobalObject& global_object, N return result; } -static Vector<StringView> separate_integer_into_groups(Unicode::NumberGroupings const& grouping_sizes, StringView integer) +static Vector<StringView> separate_integer_into_groups(Unicode::NumberGroupings const& grouping_sizes, StringView integer, NumberFormat::UseGrouping use_grouping) { Utf8View utf8_integer { integer }; if (utf8_integer.length() <= grouping_sizes.primary_grouping_size) return { integer }; size_t index = utf8_integer.length() - grouping_sizes.primary_grouping_size; - if (index < grouping_sizes.minimum_grouping_digits) - return { integer }; + + switch (use_grouping) { + case NumberFormat::UseGrouping::Min2: + if (utf8_integer.length() < 5) + return { integer }; + break; + + case NumberFormat::UseGrouping::Auto: + if (index < grouping_sizes.minimum_grouping_digits) + return { integer }; + break; + + case NumberFormat::UseGrouping::Always: + break; + + default: + VERIFY_NOT_REACHED(); + } Vector<StringView> groups; @@ -712,6 +721,7 @@ static Vector<StringView> separate_integer_into_groups(Unicode::NumberGroupings } // 15.5.5 PartitionNotationSubPattern ( numberFormat, x, n, exponent ), https://tc39.es/ecma402/#sec-partitionnotationsubpattern +// 1.1.7 PartitionNotationSubPattern ( numberFormat, x, n, exponent ), https://tc39.es/proposal-intl-numberformat-v3/out/numberformat/proposed.html#sec-partitionnotationsubpattern Vector<PatternPartition> partition_notation_sub_pattern(GlobalObject& global_object, NumberFormat& number_format, Value number, String formatted_string, int exponent) { // 1. Let result be a new empty List. @@ -779,30 +789,18 @@ Vector<PatternPartition> partition_notation_sub_pattern(GlobalObject& global_obj // b. Let fraction be undefined. } - // 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 - // behavior in the "de-DE" locale tests, because this is how ICU (and therefore V8, SpiderMoney, etc.) has always behaved. - // - // So, in locales "de-*", we must have: - // Intl.NumberFormat("de", {notation: "compact"}).format(1234) === "1234" - // Intl.NumberFormat("de", {notation: "compact"}).format(12345) === "12.345" - // Intl.NumberFormat("de").format(1234) === "1.234" - // Intl.NumberFormat("de").format(12345) === "12.345" - // - // See: https://github.com/tc39/proposal-intl-numberformat-v3/issues/3 - if (number_format.has_compact_format()) - use_grouping = is_greater_than_or_equal(number, 10'000); - - // 6. If the numberFormat.[[UseGrouping]] is true, then - if (use_grouping) { + // 6. If the numberFormat.[[UseGrouping]] is false, then + if (number_format.use_grouping() == NumberFormat::UseGrouping::False) { + // a. Append a new Record { [[Type]]: "integer", [[Value]]: integer } as the last element of result. + result.append({ "integer"sv, integer }); + } + // 7. Else, + else { // a. Let groupSepSymbol be the implementation-, locale-, and numbering system-dependent (ILND) String representing the grouping separator. auto group_sep_symbol = Unicode::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), Unicode::NumericSymbol::Group).value_or(","sv); - // b. Let groups be a List whose elements are, in left to right order, the substrings defined by ILND set of locations within the integer. - auto groups = separate_integer_into_groups(*grouping_sizes, integer); + // b. Let groups be a List whose elements are, in left to right order, the substrings defined by ILND set of locations within the integer, which may depend on the value of numberFormat.[[UseGrouping]]. + auto groups = separate_integer_into_groups(*grouping_sizes, integer, number_format.use_grouping()); // c. Assert: The number of elements in groups List is greater than 0. VERIFY(!groups.is_empty()); @@ -822,11 +820,6 @@ Vector<PatternPartition> partition_notation_sub_pattern(GlobalObject& global_obj } } } - // 7. Else, - else { - // a. Append a new Record { [[Type]]: "integer", [[Value]]: integer } as the last element of result. - result.append({ "integer"sv, integer }); - } // 8. If fraction is not undefined, then if (fraction.has_value()) { diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js index c95735680a..50709781bc 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js @@ -347,29 +347,89 @@ describe("style=decimal", () => { expect(ar.format(-1)).toBe("\u061c-\u0661"); }); - test("useGrouping=true", () => { - const en = new Intl.NumberFormat("en", { useGrouping: true }); + test("useGrouping=always", () => { + const en = new Intl.NumberFormat("en", { useGrouping: "always" }); expect(en.format(123)).toBe("123"); expect(en.format(1234)).toBe("1,234"); expect(en.format(12345)).toBe("12,345"); expect(en.format(123456)).toBe("123,456"); expect(en.format(1234567)).toBe("1,234,567"); - const enIn = new Intl.NumberFormat("en-IN", { useGrouping: true }); + const enIn = new Intl.NumberFormat("en-IN", { useGrouping: "always" }); expect(enIn.format(123)).toBe("123"); expect(enIn.format(1234)).toBe("1,234"); expect(enIn.format(12345)).toBe("12,345"); expect(enIn.format(123456)).toBe("1,23,456"); expect(enIn.format(1234567)).toBe("12,34,567"); - const ar = new Intl.NumberFormat("ar", { useGrouping: true }); + const ar = new Intl.NumberFormat("ar", { useGrouping: "always" }); expect(ar.format(123)).toBe("\u0661\u0662\u0663"); expect(ar.format(1234)).toBe("\u0661\u066c\u0662\u0663\u0664"); expect(ar.format(12345)).toBe("\u0661\u0662\u066c\u0663\u0664\u0665"); expect(ar.format(123456)).toBe("\u0661\u0662\u0663\u066c\u0664\u0665\u0666"); expect(ar.format(1234567)).toBe("\u0661\u066c\u0662\u0663\u0664\u066c\u0665\u0666\u0667"); - const plPl = new Intl.NumberFormat("pl-PL", { useGrouping: true }); + const plPl = new Intl.NumberFormat("pl-PL", { useGrouping: "always" }); + expect(plPl.format(123)).toBe("123"); + expect(plPl.format(1234)).toBe("1\u00a0234"); + expect(plPl.format(12345)).toBe("12\u00a0345"); + expect(plPl.format(123456)).toBe("123\u00a0456"); + expect(plPl.format(1234567)).toBe("1\u00a0234\u00a0567"); + }); + + test("useGrouping=auto", () => { + const en = new Intl.NumberFormat("en", { useGrouping: "auto" }); + expect(en.format(123)).toBe("123"); + expect(en.format(1234)).toBe("1,234"); + expect(en.format(12345)).toBe("12,345"); + expect(en.format(123456)).toBe("123,456"); + expect(en.format(1234567)).toBe("1,234,567"); + + const enIn = new Intl.NumberFormat("en-IN", { useGrouping: "auto" }); + expect(enIn.format(123)).toBe("123"); + expect(enIn.format(1234)).toBe("1,234"); + expect(enIn.format(12345)).toBe("12,345"); + expect(enIn.format(123456)).toBe("1,23,456"); + expect(enIn.format(1234567)).toBe("12,34,567"); + + const ar = new Intl.NumberFormat("ar", { useGrouping: "auto" }); + expect(ar.format(123)).toBe("\u0661\u0662\u0663"); + expect(ar.format(1234)).toBe("\u0661\u066c\u0662\u0663\u0664"); + expect(ar.format(12345)).toBe("\u0661\u0662\u066c\u0663\u0664\u0665"); + expect(ar.format(123456)).toBe("\u0661\u0662\u0663\u066c\u0664\u0665\u0666"); + expect(ar.format(1234567)).toBe("\u0661\u066c\u0662\u0663\u0664\u066c\u0665\u0666\u0667"); + + const plPl = new Intl.NumberFormat("pl-PL", { useGrouping: "auto" }); + expect(plPl.format(123)).toBe("123"); + expect(plPl.format(1234)).toBe("1234"); + expect(plPl.format(12345)).toBe("12\u00a0345"); + expect(plPl.format(123456)).toBe("123\u00a0456"); + expect(plPl.format(1234567)).toBe("1\u00a0234\u00a0567"); + }); + + test("useGrouping=min2", () => { + const en = new Intl.NumberFormat("en", { useGrouping: "min2" }); + expect(en.format(123)).toBe("123"); + expect(en.format(1234)).toBe("1234"); + expect(en.format(12345)).toBe("12,345"); + expect(en.format(123456)).toBe("123,456"); + expect(en.format(1234567)).toBe("1,234,567"); + + const enIn = new Intl.NumberFormat("en-IN", { useGrouping: "min2" }); + expect(enIn.format(123)).toBe("123"); + expect(enIn.format(1234)).toBe("1234"); + expect(enIn.format(12345)).toBe("12,345"); + expect(enIn.format(123456)).toBe("1,23,456"); + expect(enIn.format(1234567)).toBe("12,34,567"); + + const ar = new Intl.NumberFormat("ar", { useGrouping: "min2" }); + expect(ar.format(123)).toBe("\u0661\u0662\u0663"); + expect(ar.format(1234)).toBe("\u0661\u0662\u0663\u0664"); + expect(ar.format(12345)).toBe("\u0661\u0662\u066c\u0663\u0664\u0665"); + expect(ar.format(123456)).toBe("\u0661\u0662\u0663\u066c\u0664\u0665\u0666"); + expect(ar.format(1234567)).toBe("\u0661\u066c\u0662\u0663\u0664\u066c\u0665\u0666\u0667"); + + const plPl = new Intl.NumberFormat("pl-PL", { useGrouping: "min2" }); expect(plPl.format(123)).toBe("123"); expect(plPl.format(1234)).toBe("1234"); expect(plPl.format(12345)).toBe("12\u00a0345"); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js index ceddabddd4..ab6adba25f 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js @@ -171,8 +171,34 @@ describe("style=decimal", () => { ]); }); - test("useGrouping=true", () => { - const en = new Intl.NumberFormat("en", { useGrouping: true }); + test("useGrouping=always", () => { + const en = new Intl.NumberFormat("en", { useGrouping: "always" }); + expect(en.formatToParts(1234)).toEqual([ + { type: "integer", value: "1" }, + { type: "group", value: "," }, + { type: "integer", value: "234" }, + ]); + expect(en.formatToParts(12345)).toEqual([ + { type: "integer", value: "12" }, + { type: "group", value: "," }, + { type: "integer", value: "345" }, + ]); + + const plPl = new Intl.NumberFormat("pl-PL", { useGrouping: "always" }); + expect(plPl.formatToParts(1234)).toEqual([ + { type: "integer", value: "1" }, + { type: "group", value: "\u00a0" }, + { type: "integer", value: "234" }, + ]); + expect(plPl.formatToParts(12345)).toEqual([ + { type: "integer", value: "12" }, + { type: "group", value: "\u00a0" }, + { type: "integer", value: "345" }, + ]); + }); + + test("useGrouping=auto", () => { + const en = new Intl.NumberFormat("en", { useGrouping: "auto" }); expect(en.formatToParts(123456)).toEqual([ { type: "integer", value: "123" }, { type: "group", value: "," }, @@ -186,7 +212,7 @@ describe("style=decimal", () => { { type: "integer", value: "567" }, ]); - const enIn = new Intl.NumberFormat("en-IN", { useGrouping: true }); + const enIn = new Intl.NumberFormat("en-IN", { useGrouping: "auto" }); expect(enIn.formatToParts(123456)).toEqual([ { type: "integer", value: "1" }, { type: "group", value: "," }, @@ -202,7 +228,7 @@ describe("style=decimal", () => { { type: "integer", value: "567" }, ]); - const ar = new Intl.NumberFormat("ar", { useGrouping: true }); + const ar = new Intl.NumberFormat("ar", { useGrouping: "auto" }); expect(ar.formatToParts(123456)).toEqual([ { type: "integer", value: "\u0661\u0662\u0663" }, { type: "group", value: "\u066c" }, @@ -217,6 +243,24 @@ describe("style=decimal", () => { ]); }); + test("useGrouping=min2", () => { + const en = new Intl.NumberFormat("en", { useGrouping: "min2" }); + expect(en.formatToParts(1234)).toEqual([{ type: "integer", value: "1234" }]); + expect(en.formatToParts(12345)).toEqual([ + { type: "integer", value: "12" }, + { type: "group", value: "," }, + { type: "integer", value: "345" }, + ]); + + const plPl = new Intl.NumberFormat("pl-PL", { useGrouping: "min2" }); + expect(plPl.formatToParts(1234)).toEqual([{ type: "integer", value: "1234" }]); + expect(plPl.formatToParts(12345)).toEqual([ + { type: "integer", value: "12" }, + { type: "group", value: "\u00a0" }, + { type: "integer", value: "345" }, + ]); + }); + test("useGrouping=false", () => { const en = new Intl.NumberFormat("en", { useGrouping: false }); expect(en.formatToParts(123456)).toEqual([{ type: "integer", value: "123456" }]); |