From 3450def494fe28c21ce982caf1afd7c644381c4d Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sat, 13 Nov 2021 11:33:55 -0500 Subject: [PATCH] LibJS: Implement Intl.NumberFormat.prototype.formatToParts --- .../LibJS/Runtime/Intl/NumberFormat.cpp | 38 + .../LibJS/Runtime/Intl/NumberFormat.h | 1 + .../Runtime/Intl/NumberFormatPrototype.cpp | 23 + .../Runtime/Intl/NumberFormatPrototype.h | 1 + .../NumberFormat.prototype.formatToParts.js | 912 ++++++++++++++++++ 5 files changed, 975 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index a28152f347..aa0f9bb63e 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 @@ -959,6 +960,43 @@ String format_numeric(NumberFormat& number_format, double number) return result.build(); } +// 15.1.9 FormatNumericToParts ( numberFormat, x ), https://tc39.es/ecma402/#sec-formatnumbertoparts +Array* format_numeric_to_parts(GlobalObject& global_object, NumberFormat& number_format, double number) +{ + auto& vm = global_object.vm(); + + // 1. Let parts be ? PartitionNumberPattern(numberFormat, x). + // Note: Our implementation of PartitionNumberPattern does not throw. + auto parts = partition_number_pattern(number_format, number); + + // 2. Let result be ArrayCreate(0). + auto* result = MUST(Array::create(global_object, 0)); + + // 3. Let n be 0. + size_t n = 0; + + // 4. For each Record { [[Type]], [[Value]] } part in parts, do + for (auto& part : parts) { + // a. Let O be OrdinaryObjectCreate(%Object.prototype%). + auto* object = Object::create(global_object, global_object.object_prototype()); + + // b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]). + MUST(object->create_data_property_or_throw(vm.names.type, js_string(vm, part.type))); + + // c. Perform ! CreateDataPropertyOrThrow(O, "value", part.[[Value]]). + MUST(object->create_data_property_or_throw(vm.names.value, js_string(vm, move(part.value)))); + + // d. Perform ! CreateDataPropertyOrThrow(result, ! ToString(n), O). + MUST(result->create_data_property_or_throw(n, object)); + + // e. Increment n by 1. + ++n; + } + + // 5. Return result. + return result; +} + static String cut_trailing_zeroes(StringView string, int cut) { // These steps are exactly the same between ToRawPrecision and ToRawFixed. diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h index 62a90fdb63..c186c326fb 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h @@ -199,6 +199,7 @@ FormatResult format_numeric_to_string(NumberFormat& number_format, double number Vector partition_number_pattern(NumberFormat& number_format, double number); Vector partition_notation_sub_pattern(NumberFormat& number_format, double number, String formatted_string, int exponent); String format_numeric(NumberFormat& number_format, double number); +Array* format_numeric_to_parts(GlobalObject& global_object, NumberFormat& number_format, double number); RawFormatResult to_raw_precision(double number, int min_precision, int max_precision); RawFormatResult to_raw_fixed(double number, int min_fraction, int max_fraction); ThrowCompletionOr set_number_format_unit_options(GlobalObject& global_object, NumberFormat& intl_object, Object const& options); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp index 49038bb651..bcc02b7e79 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include @@ -30,6 +31,7 @@ void NumberFormatPrototype::initialize(GlobalObject& global_object) define_native_accessor(vm.names.format, format, nullptr, Attribute::Configurable); u8 attr = Attribute::Writable | Attribute::Configurable; + define_native_function(vm.names.formatToParts, format_to_parts, 1, attr); define_native_function(vm.names.resolvedOptions, resolved_options, 0, attr); } @@ -56,6 +58,27 @@ JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::format) return number_format->bound_format(); } +// 15.4.4 Intl.NumberFormat.prototype.formatToParts ( value ), https://tc39.es/ecma402/#sec-intl.numberformat.prototype.formattoparts +JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::format_to_parts) +{ + auto value = vm.argument(0); + + // 1. Let nf be the this value. + // 2. Perform ? RequireInternalSlot(nf, [[InitializedNumberFormat]]). + auto* number_format = TRY(typed_this_object(global_object)); + + // 3. Let x be ? ToNumeric(value). + value = TRY(value.to_numeric(global_object)); + + // FIXME: Support BigInt number formatting. + if (value.is_bigint()) + return vm.throw_completion(global_object, ErrorType::NotImplemented, "BigInt number formatting"); + + // 4. Return ? FormatNumericToParts(nf, x). + // Note: Our implementation of FormatNumericToParts does not throw. + return format_numeric_to_parts(global_object, *number_format, value.as_double()); +} + // 15.4.5 Intl.NumberFormat.prototype.resolvedOptions ( ), https://tc39.es/ecma402/#sec-intl.numberformat.prototype.resolvedoptions JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::resolved_options) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.h b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.h index db55681b6e..d6f7a270b9 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.h @@ -21,6 +21,7 @@ public: private: JS_DECLARE_NATIVE_FUNCTION(format); + JS_DECLARE_NATIVE_FUNCTION(format_to_parts); JS_DECLARE_NATIVE_FUNCTION(resolved_options); }; 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 new file mode 100644 index 0000000000..48c0c0b2a1 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js @@ -0,0 +1,912 @@ +describe("errors", () => { + test("called on non-NumberFormat object", () => { + expect(() => { + Intl.NumberFormat.prototype.formatToParts(1); + }).toThrowWithMessage(TypeError, "Not an object of type Intl.NumberFormat"); + }); + + test("called with value that cannot be converted to a number", () => { + expect(() => { + Intl.NumberFormat().formatToParts(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + }); + + // FIXME: Remove this and add BigInt tests when BigInt number formatting is supported. + test("bigint", () => { + expect(() => { + Intl.NumberFormat().formatToParts(1n); + }).toThrowWithMessage( + InternalError, + "BigInt number formatting is not implemented in LibJS" + ); + }); +}); + +describe("special values", () => { + test("NaN", () => { + const en = new Intl.NumberFormat("en"); + expect(en.formatToParts()).toEqual([{ type: "nan", value: "NaN" }]); + expect(en.formatToParts(NaN)).toEqual([{ type: "nan", value: "NaN" }]); + expect(en.formatToParts(undefined)).toEqual([{ type: "nan", value: "NaN" }]); + + const ar = new Intl.NumberFormat("ar"); + expect(ar.formatToParts()).toEqual([{ type: "nan", value: "ليس رقم" }]); + expect(ar.formatToParts(NaN)).toEqual([{ type: "nan", value: "ليس رقم" }]); + expect(ar.formatToParts(undefined)).toEqual([{ type: "nan", value: "ليس رقم" }]); + }); + + test("Infinity", () => { + const en = new Intl.NumberFormat("en"); + expect(en.formatToParts(Infinity)).toEqual([{ type: "infinity", value: "∞" }]); + expect(en.formatToParts(-Infinity)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "infinity", value: "∞" }, + ]); + + const ar = new Intl.NumberFormat("ar"); + expect(ar.formatToParts(Infinity)).toEqual([{ type: "infinity", value: "∞" }]); + expect(ar.formatToParts(-Infinity)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "infinity", value: "∞" }, + ]); + }); +}); + +describe("style=decimal", () => { + test("default", () => { + const en = new Intl.NumberFormat("en"); + expect(en.formatToParts(123)).toEqual([{ type: "integer", value: "123" }]); + expect(en.formatToParts(1.23)).toEqual([ + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "23" }, + ]); + expect(en.formatToParts(12.3)).toEqual([ + { type: "integer", value: "12" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "3" }, + ]); + + const ar = new Intl.NumberFormat("ar"); + expect(ar.formatToParts(123)).toEqual([{ type: "integer", value: "\u0661\u0662\u0663" }]); + expect(ar.formatToParts(1.23)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0662\u0663" }, + ]); + expect(ar.formatToParts(12.3)).toEqual([ + { type: "integer", value: "\u0661\u0662" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0663" }, + ]); + }); + + test("signDisplay=never", () => { + const en = new Intl.NumberFormat("en", { signDisplay: "never" }); + expect(en.formatToParts(1)).toEqual([{ type: "integer", value: "1" }]); + expect(en.formatToParts(-1)).toEqual([{ type: "integer", value: "1" }]); + + const ar = new Intl.NumberFormat("ar", { signDisplay: "never" }); + expect(ar.formatToParts(1)).toEqual([{ type: "integer", value: "\u0661" }]); + expect(ar.formatToParts(-1)).toEqual([{ type: "integer", value: "\u0661" }]); + }); + + test("signDisplay=auto", () => { + const en = new Intl.NumberFormat("en", { signDisplay: "auto" }); + expect(en.formatToParts(0)).toEqual([{ type: "integer", value: "0" }]); + expect(en.formatToParts(1)).toEqual([{ type: "integer", value: "1" }]); + expect(en.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "0" }, + ]); + expect(en.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "1" }, + ]); + + const ar = new Intl.NumberFormat("ar", { signDisplay: "auto" }); + expect(ar.formatToParts(0)).toEqual([{ type: "integer", value: "\u0660" }]); + expect(ar.formatToParts(1)).toEqual([{ type: "integer", value: "\u0661" }]); + expect(ar.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0660" }, + ]); + expect(ar.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + ]); + }); + + test("signDisplay=always", () => { + const en = new Intl.NumberFormat("en", { signDisplay: "always" }); + expect(en.formatToParts(0)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "integer", value: "0" }, + ]); + expect(en.formatToParts(1)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "integer", value: "1" }, + ]); + expect(en.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "0" }, + ]); + expect(en.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "1" }, + ]); + + const ar = new Intl.NumberFormat("ar", { signDisplay: "always" }); + expect(ar.formatToParts(0)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0660" }, + ]); + expect(ar.formatToParts(1)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0661" }, + ]); + expect(ar.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0660" }, + ]); + expect(ar.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + ]); + }); + + test("signDisplay=exceptZero", () => { + const en = new Intl.NumberFormat("en", { signDisplay: "exceptZero" }); + expect(en.formatToParts(0)).toEqual([{ type: "integer", value: "0" }]); + expect(en.formatToParts(1)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "integer", value: "1" }, + ]); + expect(en.formatToParts(-0)).toEqual([{ type: "integer", value: "0" }]); + expect(en.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "1" }, + ]); + + const ar = new Intl.NumberFormat("ar", { signDisplay: "exceptZero" }); + expect(ar.formatToParts(0)).toEqual([{ type: "integer", value: "\u0660" }]); + expect(ar.formatToParts(1)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0661" }, + ]); + expect(ar.formatToParts(-0)).toEqual([{ type: "integer", value: "\u0660" }]); + expect(ar.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + ]); + }); +}); + +describe("style=percent", () => { + test("default", () => { + const en = new Intl.NumberFormat("en", { style: "percent", minimumFractionDigits: 2 }); + expect(en.formatToParts(1)).toEqual([ + { type: "integer", value: "100" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(1.2345)).toEqual([ + { type: "integer", value: "123" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "45" }, + { type: "percentSign", value: "%" }, + ]); + + const ar = new Intl.NumberFormat("ar", { style: "percent", minimumFractionDigits: 2 }); + expect(ar.formatToParts(1)).toEqual([ + { type: "integer", value: "\u0661\u0660\u0660" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(1.2345)).toEqual([ + { type: "integer", value: "\u0661\u0662\u0663" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0664\u0665" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + }); + + test("signDisplay=never", () => { + const en = new Intl.NumberFormat("en", { style: "percent", signDisplay: "never" }); + expect(en.formatToParts(0.01)).toEqual([ + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(-0.01)).toEqual([ + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + + const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "never" }); + expect(ar.formatToParts(0.01)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(-0.01)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + }); + + test("signDisplay=auto", () => { + const en = new Intl.NumberFormat("en", { style: "percent", signDisplay: "auto" }); + expect(en.formatToParts(0.0)).toEqual([ + { type: "integer", value: "0" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(0.01)).toEqual([ + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(-0.0)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "0" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(-0.01)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + + const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "auto" }); + expect(ar.formatToParts(0.0)).toEqual([ + { type: "integer", value: "\u0660" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(0.01)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(-0.0)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0660" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(-0.01)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + }); + + test("signDisplay=always", () => { + const en = new Intl.NumberFormat("en", { style: "percent", signDisplay: "always" }); + expect(en.formatToParts(0.0)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "integer", value: "0" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(0.01)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(-0.0)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "0" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(-0.01)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + + const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "always" }); + expect(ar.formatToParts(0.0)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0660" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(0.01)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(-0.0)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0660" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(-0.01)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + }); + + test("signDisplay=exceptZero", () => { + const en = new Intl.NumberFormat("en", { style: "percent", signDisplay: "exceptZero" }); + expect(en.formatToParts(0.0)).toEqual([ + { type: "integer", value: "0" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(0.01)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(-0.0)).toEqual([ + { type: "integer", value: "0" }, + { type: "percentSign", value: "%" }, + ]); + expect(en.formatToParts(-0.01)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "integer", value: "1" }, + { type: "percentSign", value: "%" }, + ]); + + const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "exceptZero" }); + expect(ar.formatToParts(0.0)).toEqual([ + { type: "integer", value: "\u0660" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(0.01)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(-0.0)).toEqual([ + { type: "integer", value: "\u0660" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + expect(ar.formatToParts(-0.01)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + { type: "percentSign", value: "\u066a\u061c" }, + ]); + }); +}); + +describe("style=currency", () => { + test("currencyDisplay=code", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "code", + }); + expect(en.formatToParts(1)).toEqual([ + { type: "currency", value: "USD" }, + { type: "literal", value: "\u00a0" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(1.23)).toEqual([ + { type: "currency", value: "USD" }, + { type: "literal", value: "\u00a0" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "23" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "code", + }); + expect(ar.formatToParts(1)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "USD" }, + ]); + expect(ar.formatToParts(1.23)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0662\u0663" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "USD" }, + ]); + }); + + test("currencyDisplay=symbol", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "symbol", + }); + expect(en.formatToParts(1)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(1.23)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "23" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "symbol", + }); + expect(ar.formatToParts(1)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(1.23)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0662\u0663" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + }); + + test("currencyDisplay=narrowSymbol", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + }); + expect(en.formatToParts(1)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(1.23)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "23" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + }); + expect(ar.formatToParts(1)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(1.23)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0662\u0663" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + }); + + test("currencyDisplay=name", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "name", + }); + expect(en.formatToParts(1)).toEqual([ + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + { type: "literal", value: " " }, + { type: "currency", value: "US dollars" }, + ]); + expect(en.formatToParts(1.23)).toEqual([ + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "23" }, + { type: "literal", value: " " }, + { type: "currency", value: "US dollars" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "name", + }); + expect(ar.formatToParts(1)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: " " }, + { type: "currency", value: "دولار أمريكي" }, + ]); + expect(ar.formatToParts(1.23)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0662\u0663" }, + { type: "literal", value: " " }, + { type: "currency", value: "دولار أمريكي" }, + ]); + }); + + test("signDisplay=never", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "never", + }); + expect(en.formatToParts(1)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(-1)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "never", + }); + expect(ar.formatToParts(1)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(-1)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + + const zh = new Intl.NumberFormat("zh-Hant-u-nu-hanidec", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "never", + }); + expect(zh.formatToParts(1)).toEqual([ + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(-1)).toEqual([ + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + }); + + test("signDisplay=auto", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "auto", + }); + expect(en.formatToParts(0)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "0" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(1)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "currency", value: "$" }, + { type: "integer", value: "0" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "auto", + }); + expect(ar.formatToParts(0)).toEqual([ + { type: "integer", value: "\u0660" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(1)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0660" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + + const zh = new Intl.NumberFormat("zh-Hant-u-nu-hanidec", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "auto", + }); + expect(zh.formatToParts(0)).toEqual([ + { type: "currency", value: "US$" }, + { type: "integer", value: "〇" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(1)).toEqual([ + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(-0)).toEqual([ + { type: "literal", value: "(" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "〇" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + { type: "literal", value: ")" }, + ]); + expect(zh.formatToParts(-1)).toEqual([ + { type: "literal", value: "(" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + { type: "literal", value: ")" }, + ]); + }); + + test("signDisplay=always", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "always", + }); + expect(en.formatToParts(0)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "currency", value: "$" }, + { type: "integer", value: "0" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(1)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "currency", value: "$" }, + { type: "integer", value: "0" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "always", + }); + expect(ar.formatToParts(0)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0660" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(1)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(-0)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0660" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + + const zh = new Intl.NumberFormat("zh-Hant-u-nu-hanidec", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "always", + }); + expect(zh.formatToParts(0)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "〇" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(1)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(-0)).toEqual([ + { type: "literal", value: "(" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "〇" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + { type: "literal", value: ")" }, + ]); + expect(zh.formatToParts(-1)).toEqual([ + { type: "literal", value: "(" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + { type: "literal", value: ")" }, + ]); + }); + + test("signDisplay=exceptZero", () => { + const en = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "exceptZero", + }); + expect(en.formatToParts(0)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "0" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(1)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(-0)).toEqual([ + { type: "currency", value: "$" }, + { type: "integer", value: "0" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + expect(en.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "-" }, + { type: "currency", value: "$" }, + { type: "integer", value: "1" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "00" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "exceptZero", + }); + expect(ar.formatToParts(0)).toEqual([ + { type: "integer", value: "\u0660" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(1)).toEqual([ + { type: "plusSign", value: "\u061c+" }, + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(-0)).toEqual([ + { type: "integer", value: "\u0660" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + expect(ar.formatToParts(-1)).toEqual([ + { type: "minusSign", value: "\u061c-" }, + { type: "integer", value: "\u0661" }, + { type: "decimal", value: "\u066b" }, + { type: "fraction", value: "\u0660\u0660" }, + { type: "literal", value: "\u00a0" }, + { type: "currency", value: "US$" }, + ]); + + const zh = new Intl.NumberFormat("zh-Hant-u-nu-hanidec", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "exceptZero", + }); + expect(zh.formatToParts(0)).toEqual([ + { type: "currency", value: "US$" }, + { type: "integer", value: "〇" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(1)).toEqual([ + { type: "plusSign", value: "+" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(-0)).toEqual([ + { type: "currency", value: "US$" }, + { type: "integer", value: "〇" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + ]); + expect(zh.formatToParts(-1)).toEqual([ + { type: "literal", value: "(" }, + { type: "currency", value: "US$" }, + { type: "integer", value: "一" }, + { type: "decimal", value: "." }, + { type: "fraction", value: "〇〇" }, + { type: "literal", value: ")" }, + ]); + }); +});