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:
parent
3b7f5af042
commit
15c5fbd9e9
3 changed files with 171 additions and 12 deletions
|
@ -5,6 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <AK/Array.h>
|
#include <AK/Array.h>
|
||||||
|
#include <AK/Utf8View.h>
|
||||||
#include <LibJS/Runtime/Array.h>
|
#include <LibJS/Runtime/Array.h>
|
||||||
#include <LibJS/Runtime/GlobalObject.h>
|
#include <LibJS/Runtime/GlobalObject.h>
|
||||||
#include <LibJS/Runtime/Intl/NumberFormat.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();
|
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
|
// 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)
|
Vector<PatternPartition> partition_notation_sub_pattern(NumberFormat& number_format, double number, String formatted_string, int exponent)
|
||||||
{
|
{
|
||||||
// 1. Let result be a new empty List.
|
// 1. Let result be a new empty List.
|
||||||
Vector<PatternPartition> result;
|
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
|
// 2. If x is NaN, then
|
||||||
if (Value(number).is_nan()) {
|
if (Value(number).is_nan()) {
|
||||||
// a. Append a new Record { [[Type]]: "nan", [[Value]]: n } as the last element of result.
|
// 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
|
// 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.
|
if (number_format.use_grouping()) {
|
||||||
// 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.
|
// a. Let groupSepSymbol be the implementation-, locale-, and numbering system-dependent (ILND) String representing the grouping separator.
|
||||||
// c. Assert: The number of elements in groups List is greater than 0.
|
auto group_sep_symbol = Unicode::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), "group"sv).value_or(","sv);
|
||||||
// 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.
|
|
||||||
|
|
||||||
// FIXME: Implement grouping.
|
// 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.
|
||||||
result.append({ "integer"sv, 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
|
// 8. If fraction is not undefined, then
|
||||||
if (fraction.has_value()) {
|
if (fraction.has_value()) {
|
||||||
|
|
|
@ -204,6 +204,52 @@ describe("style=decimal", () => {
|
||||||
expect(ar.format(-0)).toBe("\u0660");
|
expect(ar.format(-0)).toBe("\u0660");
|
||||||
expect(ar.format(-1)).toBe("\u061c-\u0661");
|
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", () => {
|
describe("style=percent", () => {
|
||||||
|
|
|
@ -180,6 +180,70 @@ describe("style=decimal", () => {
|
||||||
{ type: "integer", value: "\u0661" },
|
{ 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", () => {
|
describe("style=percent", () => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue