From c19c3205ffe0747fa299950a725a57ebd6667e95 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 16 Nov 2021 19:35:40 -0500 Subject: [PATCH] LibJS: Implement ECMA-402 Number.prototype.toLocaleString --- .../LibJS/Runtime/NumberPrototype.cpp | 27 +++++++ .../Libraries/LibJS/Runtime/NumberPrototype.h | 1 + .../Number/Number.prototype.toLocaleString.js | 78 +++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toLocaleString.js diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp index 1213b26e9b..07ddaf2f40 100644 --- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp @@ -7,9 +7,12 @@ #include #include +#include #include #include #include +#include +#include #include #include @@ -37,6 +40,7 @@ void NumberPrototype::initialize(GlobalObject& object) Object::initialize(object); u8 attr = Attribute::Configurable | Attribute::Writable; define_native_function(vm.names.toFixed, to_fixed, 1, attr); + define_native_function(vm.names.toLocaleString, to_locale_string, 0, attr); define_native_function(vm.names.toString, to_string, 1, attr); define_native_function(vm.names.valueOf, value_of, 0, attr); } @@ -77,6 +81,29 @@ JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_fixed) return js_string(vm, String::formatted("{:0.{1}}", number, static_cast(fraction_digits))); } +// 18.2.1 Number.prototype.toLocaleString ( [ locales [ , options ] ] ), https://tc39.es/ecma402/#sup-number.prototype.tolocalestring +JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_locale_string) +{ + auto locales = vm.argument(0); + auto options = vm.argument(1); + + // 1. Let x be ? thisNumberValue(this value). + auto number_value = TRY(this_number_value(global_object, vm.this_value(global_object))); + + MarkedValueList arguments { vm.heap() }; + arguments.append(locales); + arguments.append(options); + + // 2. Let numberFormat be ? Construct(%NumberFormat%, « locales, options »). + auto* number_format = static_cast(TRY(construct(global_object, *global_object.intl_number_format_constructor(), move(arguments)))); + + // 3. Return ? FormatNumeric(numberFormat, x). + // Note: Our implementation of FormatNumeric does not throw. + auto formatted = Intl::format_numeric(*number_format, number_value.as_double()); + + return js_string(vm, move(formatted)); +} + // 21.1.3.6 Number.prototype.toString ( [ radix ] ), https://tc39.es/ecma262/#sec-number.prototype.tostring JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_string) { diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.h b/Userland/Libraries/LibJS/Runtime/NumberPrototype.h index ed36778376..bf35eb8332 100644 --- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.h @@ -19,6 +19,7 @@ public: virtual ~NumberPrototype() override; JS_DECLARE_NATIVE_FUNCTION(to_fixed); + JS_DECLARE_NATIVE_FUNCTION(to_locale_string); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(value_of); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toLocaleString.js b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toLocaleString.js new file mode 100644 index 0000000000..72ffb24573 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toLocaleString.js @@ -0,0 +1,78 @@ +describe("errors", () => { + test("must be called with numeric |this|", () => { + [true, [], {}, Symbol("foo"), "bar", 1n].forEach(value => { + expect(() => Number.prototype.toLocaleString.call(value)).toThrowWithMessage( + TypeError, + "Not an object of type Number" + ); + }); + }); +}); + +describe("correct behavior", () => { + test("length", () => { + expect(Number.prototype.toLocaleString).toHaveLength(0); + }); +}); + +describe("special values", () => { + test("NaN", () => { + expect(NaN.toLocaleString()).toBe("NaN"); + expect(NaN.toLocaleString("en")).toBe("NaN"); + expect(NaN.toLocaleString("ar")).toBe("ليس رقم"); + }); + + test("Infinity", () => { + expect(Infinity.toLocaleString()).toBe("∞"); + expect(Infinity.toLocaleString("en")).toBe("∞"); + expect(Infinity.toLocaleString("ar")).toBe("∞"); + }); +}); + +describe("styles", () => { + test("decimal", () => { + expect((12).toLocaleString("en")).toBe("12"); + expect((12).toLocaleString("ar")).toBe("\u0661\u0662"); + }); + + test("percent", () => { + expect((0.234).toLocaleString("en", { style: "percent" })).toBe("23%"); + expect((0.234).toLocaleString("ar", { style: "percent" })).toBe("\u0662\u0663\u066a\u061c"); + }); + + test("currency", () => { + expect( + (1.23).toLocaleString("en", { + style: "currency", + currency: "USD", + currencyDisplay: "name", + }) + ).toBe("1.23 US dollars"); + + expect( + (1.23).toLocaleString("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "name", + }) + ).toBe("\u0661\u066b\u0662\u0663 دولار أمريكي"); + }); + + test("unit", () => { + expect( + (1.23).toLocaleString("en", { + style: "unit", + unit: "kilometer-per-hour", + unitDisplay: "long", + }) + ).toBe("1.23 kilometers per hour"); + + expect( + (1.23).toLocaleString("ar", { + style: "unit", + unit: "kilometer-per-hour", + unitDisplay: "long", + }) + ).toBe("\u0661\u066b\u0662\u0663 كيلومتر في الساعة"); + }); +});