diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index aa0f9bb63e..ff08d0d274 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -807,12 +808,43 @@ static String replace_digits_for_number_format(NumberFormat& number_format, Stri return builder.build(); } +static Vector separate_integer_into_groups(Unicode::NumberGroupings const& grouping_sizes, StringView integer) +{ + Utf8View utf8_integer { integer }; + Vector groups; + + auto add_group = [&](size_t index, size_t length) { + groups.prepend(utf8_integer.unicode_substring_view(index, length).as_string()); + }; + + if (utf8_integer.length() > grouping_sizes.primary_grouping_size) { + size_t index = utf8_integer.length() - grouping_sizes.primary_grouping_size; + add_group(index, grouping_sizes.primary_grouping_size); + + while (index > grouping_sizes.secondary_grouping_size) { + index -= grouping_sizes.secondary_grouping_size; + add_group(index, grouping_sizes.secondary_grouping_size); + } + + if (index > 0) + add_group(0, index); + } else { + groups.append(integer); + } + + return groups; +} + // 15.1.7 PartitionNotationSubPattern ( numberFormat, x, n, exponent ), https://tc39.es/ecma402/#sec-partitionnotationsubpattern Vector partition_notation_sub_pattern(NumberFormat& number_format, double number, String formatted_string, int exponent) { // 1. Let result be a new empty List. Vector result; + auto grouping_sizes = Unicode::get_number_system_groupings(number_format.data_locale(), number_format.numbering_system()); + if (!grouping_sizes.has_value()) + return {}; + // 2. If x is NaN, then if (Value(number).is_nan()) { // a. Append a new Record { [[Type]]: "nan", [[Value]]: n } as the last element of result. @@ -870,19 +902,36 @@ Vector partition_notation_sub_pattern(NumberFormat& number_for } // 6. If the numberFormat.[[UseGrouping]] is true, then - // a. Let groupSepSymbol be the implementation-, locale-, and numbering system-dependent (ILND) String representing the grouping separator. - // 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. - // c. Assert: The number of elements in groups List is greater than 0. - // d. Repeat, while groups List is not empty, - // i. Remove the first element from groups and let integerGroup be the value of that element. - // ii. Append a new Record { [[Type]]: "integer", [[Value]]: integerGroup } as the last element of result. - // iii. If groups List is not empty, then - // i. Append a new Record { [[Type]]: "group", [[Value]]: groupSepSymbol } as the last element of result. - // 7. Else, - // a. Append a new Record { [[Type]]: "integer", [[Value]]: integer } as the last element of result. + if (number_format.use_grouping()) { + // 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(), "group"sv).value_or(","sv); - // FIXME: Implement grouping. - result.append({ "integer"sv, 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. + auto groups = separate_integer_into_groups(*grouping_sizes, integer); + + // c. Assert: The number of elements in groups List is greater than 0. + VERIFY(!groups.is_empty()); + + // d. Repeat, while groups List is not empty, + while (!groups.is_empty()) { + // i. Remove the first element from groups and let integerGroup be the value of that element. + auto integer_group = groups.take_first(); + + // ii. Append a new Record { [[Type]]: "integer", [[Value]]: integerGroup } as the last element of result. + result.append({ "integer"sv, integer_group }); + + // iii. If groups List is not empty, then + if (!groups.is_empty()) { + // i. Append a new Record { [[Type]]: "group", [[Value]]: groupSepSymbol } as the last element of result. + result.append({ "group"sv, group_sep_symbol }); + } + } + } + // 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 11dcdac25c..0dc3659349 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 @@ -204,6 +204,52 @@ describe("style=decimal", () => { expect(ar.format(-0)).toBe("\u0660"); expect(ar.format(-1)).toBe("\u061c-\u0661"); }); + + test("useGrouping=true", () => { + const en = new Intl.NumberFormat("en", { useGrouping: true }); + 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 }); + 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 }); + 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"); + }); + + test("useGrouping=false", () => { + const en = new Intl.NumberFormat("en", { useGrouping: false }); + expect(en.format(123)).toBe("123"); + expect(en.format(1234)).toBe("1234"); + expect(en.format(12345)).toBe("12345"); + expect(en.format(123456)).toBe("123456"); + expect(en.format(1234567)).toBe("1234567"); + + const enIn = new Intl.NumberFormat("en-IN", { useGrouping: false }); + expect(enIn.format(123)).toBe("123"); + expect(enIn.format(1234)).toBe("1234"); + expect(enIn.format(12345)).toBe("12345"); + expect(enIn.format(123456)).toBe("123456"); + expect(enIn.format(1234567)).toBe("1234567"); + + const ar = new Intl.NumberFormat("ar", { useGrouping: false }); + expect(ar.format(123)).toBe("\u0661\u0662\u0663"); + expect(ar.format(1234)).toBe("\u0661\u0662\u0663\u0664"); + expect(ar.format(12345)).toBe("\u0661\u0662\u0663\u0664\u0665"); + expect(ar.format(123456)).toBe("\u0661\u0662\u0663\u0664\u0665\u0666"); + expect(ar.format(1234567)).toBe("\u0661\u0662\u0663\u0664\u0665\u0666\u0667"); + }); }); describe("style=percent", () => { 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 48c0c0b2a1..4055ae8822 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 @@ -180,6 +180,70 @@ describe("style=decimal", () => { { type: "integer", value: "\u0661" }, ]); }); + + test("useGrouping=true", () => { + const en = new Intl.NumberFormat("en", { useGrouping: true }); + expect(en.formatToParts(123456)).toEqual([ + { type: "integer", value: "123" }, + { type: "group", value: "," }, + { type: "integer", value: "456" }, + ]); + expect(en.formatToParts(1234567)).toEqual([ + { type: "integer", value: "1" }, + { type: "group", value: "," }, + { type: "integer", value: "234" }, + { type: "group", value: "," }, + { type: "integer", value: "567" }, + ]); + + const enIn = new Intl.NumberFormat("en-IN", { useGrouping: true }); + expect(enIn.formatToParts(123456)).toEqual([ + { type: "integer", value: "1" }, + { type: "group", value: "," }, + { type: "integer", value: "23" }, + { type: "group", value: "," }, + { type: "integer", value: "456" }, + ]); + expect(enIn.formatToParts(1234567)).toEqual([ + { type: "integer", value: "12" }, + { type: "group", value: "," }, + { type: "integer", value: "34" }, + { type: "group", value: "," }, + { type: "integer", value: "567" }, + ]); + + const ar = new Intl.NumberFormat("ar", { useGrouping: true }); + expect(ar.formatToParts(123456)).toEqual([ + { type: "integer", value: "\u0661\u0662\u0663" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0664\u0665\u0666" }, + ]); + expect(ar.formatToParts(1234567)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0662\u0663\u0664" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0665\u0666\u0667" }, + ]); + }); + + test("useGrouping=false", () => { + const en = new Intl.NumberFormat("en", { useGrouping: false }); + expect(en.formatToParts(123456)).toEqual([{ type: "integer", value: "123456" }]); + expect(en.formatToParts(1234567)).toEqual([{ type: "integer", value: "1234567" }]); + + const enIn = new Intl.NumberFormat("en-IN", { useGrouping: false }); + expect(enIn.formatToParts(123456)).toEqual([{ type: "integer", value: "123456" }]); + expect(enIn.formatToParts(1234567)).toEqual([{ type: "integer", value: "1234567" }]); + + const ar = new Intl.NumberFormat("ar", { useGrouping: false }); + expect(ar.formatToParts(123456)).toEqual([ + { type: "integer", value: "\u0661\u0662\u0663\u0664\u0665\u0666" }, + ]); + expect(ar.formatToParts(1234567)).toEqual([ + { type: "integer", value: "\u0661\u0662\u0663\u0664\u0665\u0666\u0667" }, + ]); + }); }); describe("style=percent", () => {