1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-26 08:47:34 +00:00

LibJS: Implement number grouping for Intl.NumberFormat

For example, in en-US, the number 123456 should be formatted as the
string "123,456". In en-IN, it should be formatted as "1,23,456".
This commit is contained in:
Timothy Flynn 2021-11-13 23:59:12 -05:00 committed by Linus Groh
parent 3b7f5af042
commit 15c5fbd9e9
3 changed files with 171 additions and 12 deletions

View file

@ -5,6 +5,7 @@
*/
#include <AK/Array.h>
#include <AK/Utf8View.h>
#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/GlobalObject.h>
#include <LibJS/Runtime/Intl/NumberFormat.h>
@ -807,12 +808,43 @@ static String replace_digits_for_number_format(NumberFormat& number_format, Stri
return builder.build();
}
static Vector<StringView> separate_integer_into_groups(Unicode::NumberGroupings const& grouping_sizes, StringView integer)
{
Utf8View utf8_integer { integer };
Vector<StringView> 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<PatternPartition> partition_notation_sub_pattern(NumberFormat& number_format, double number, String formatted_string, int exponent)
{
// 1. Let result be a new empty List.
Vector<PatternPartition> 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<PatternPartition> 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()) {

View file

@ -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", () => {

View file

@ -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", () => {