diff options
author | Timothy Flynn <trflynn89@pm.me> | 2022-01-30 13:00:09 -0500 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-01-30 20:05:27 +0000 |
commit | d6e926e5b1cdb629b8faff05453190de90f3727c (patch) | |
tree | 9a93f1be6bf4aa00ac3104fb9c923589b5ecca43 | |
parent | a0253af8c1d5e7374d102800d540a014060c9901 (diff) | |
download | serenity-d6e926e5b1cdb629b8faff05453190de90f3727c.zip |
LibJS: Support BigInt number formatting with Intl.NumberFormat
5 files changed, 171 insertions, 45 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index d12071745f..1bc18c5b00 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -5,7 +5,9 @@ */ #include <AK/Utf8View.h> +#include <LibCrypto/BigInt/SignedBigInteger.h> #include <LibJS/Runtime/Array.h> +#include <LibJS/Runtime/BigInt.h> #include <LibJS/Runtime/GlobalObject.h> #include <LibJS/Runtime/Intl/NumberFormat.h> #include <LibJS/Runtime/Intl/NumberFormatFunction.h> @@ -251,21 +253,28 @@ static ALWAYS_INLINE int log10floor(Value number) { if (number.is_number()) return static_cast<int>(floor(log10(number.as_double()))); - VERIFY_NOT_REACHED(); + + // FIXME: Can we do this without string conversion? + auto as_string = number.as_bigint().big_integer().to_base(10); + return as_string.length() - 1; } -static Value multiply(GlobalObject&, Value lhs, i64 rhs) +static Value multiply(GlobalObject& global_object, Value lhs, i64 rhs) { if (lhs.is_number()) return Value(lhs.as_double() * rhs); - VERIFY_NOT_REACHED(); + + auto rhs_bigint = Crypto::SignedBigInteger::create_from(rhs); + return js_bigint(global_object.vm(), lhs.as_bigint().big_integer().multiplied_by(rhs_bigint)); } -static Value divide(GlobalObject&, Value lhs, i64 rhs) +static Value divide(GlobalObject& global_object, Value lhs, i64 rhs) { if (lhs.is_number()) return Value(lhs.as_double() / rhs); - VERIFY_NOT_REACHED(); + + auto rhs_bigint = Crypto::SignedBigInteger::create_from(rhs); + return js_bigint(global_object.vm(), lhs.as_bigint().big_integer().divided_by(rhs_bigint).quotient); } static ALWAYS_INLINE Value multiply_by_power(GlobalObject& global_object, Value number, i64 exponent) @@ -286,42 +295,42 @@ static ALWAYS_INLINE Value rounded(Value number) { if (number.is_number()) return Value(round(number.as_double())); - VERIFY_NOT_REACHED(); + return number; } static ALWAYS_INLINE bool is_zero(Value number) { if (number.is_number()) return number.as_double() == 0.0; - VERIFY_NOT_REACHED(); + return number.as_bigint().big_integer() == Crypto::SignedBigInteger::create_from(0); } static ALWAYS_INLINE bool is_greater_than(Value number, i64 rhs) { if (number.is_number()) return number.as_double() > rhs; - VERIFY_NOT_REACHED(); + return number.as_bigint().big_integer() > Crypto::SignedBigInteger::create_from(rhs); } static ALWAYS_INLINE bool is_greater_than_or_equal(Value number, i64 rhs) { if (number.is_number()) return number.as_double() >= rhs; - VERIFY_NOT_REACHED(); + return number.as_bigint().big_integer() >= Crypto::SignedBigInteger::create_from(rhs); } static ALWAYS_INLINE bool is_less_than(Value number, i64 rhs) { if (number.is_number()) return number.as_double() < rhs; - VERIFY_NOT_REACHED(); + return number.as_bigint().big_integer() < Crypto::SignedBigInteger::create_from(rhs); } static ALWAYS_INLINE String number_to_string(Value number) { if (number.is_number()) return number.to_string_without_side_effects(); - VERIFY_NOT_REACHED(); + return number.as_bigint().big_integer().to_base(10); } // 15.1.1 SetNumberFormatDigitOptions ( intlObj, options, mnfdDefault, mxfdDefault, notation ), https://tc39.es/ecma402/#sec-setnfdigitoptions @@ -833,7 +842,7 @@ Vector<PatternPartition> partition_notation_sub_pattern(GlobalObject& global_obj result.append({ "nan"sv, move(formatted_string) }); } // 3. Else if x is a non-finite Number, then - else if (!number.is_finite_number()) { + else if (number.is_number() && !number.is_finite_number()) { // a. Append a new Record { [[Type]]: "infinity", [[Value]]: n } as the last element of result. result.append({ "infinity"sv, move(formatted_string) }); } @@ -1089,7 +1098,6 @@ RawFormatResult to_raw_precision(GlobalObject& global_object, Value number, int RawFormatResult result {}; // 1. Set x to ā(x). - // FIXME: Support BigInt number formatting. // 2. Let p be maxPrecision. int precision = max_precision; @@ -1116,7 +1124,23 @@ RawFormatResult to_raw_precision(GlobalObject& global_object, Value number, int // a. Let e and n be integers such that 10^(pā1) ā¤ n < 10^p and for which n Ć 10^(eāp+1) ā x is as close to zero as possible. // If there are two such sets of e and n, pick the e and n for which n Ć 10^(eāp+1) is larger. exponent = log10floor(number); - auto n = rounded(divide_by_power(global_object, number, exponent - precision + 1)); + Value n; + + if (number.is_number()) { + n = rounded(divide_by_power(global_object, number, exponent - precision + 1)); + } else { + // NOTE: In order to round the BigInt to the proper precision, this computation is initially off by a + // factor of 10. This lets us inspect the ones digit and then round up if needed. + n = divide_by_power(global_object, number, exponent - precision); + + // FIXME: Can we do this without string conversion? + auto digits = n.as_bigint().big_integer().to_base(10); + auto digit = digits.substring_view(digits.length() - 1); + + n = divide(global_object, n, 10); + if (digit.to_uint().value() >= 5) + n = js_bigint(global_object.vm(), n.as_bigint().big_integer().plus(Crypto::SignedBigInteger::create_from(1))); + } // b. Let m be the String consisting of the digits of the decimal representation of n (in order, with no leading zeroes). result.formatted_string = number_to_string(n); @@ -1180,7 +1204,6 @@ RawFormatResult to_raw_fixed(GlobalObject& global_object, Value number, int min_ RawFormatResult result {}; // 1. Set x to ā(x). - // FIXME: Support BigInt number formatting. // 2. Let f be maxFraction. int fraction = max_fraction; @@ -1318,7 +1341,10 @@ Optional<Variant<StringView, String>> get_number_format_pattern(NumberFormat& nu auto as_number = [&]() { if (number.is_number()) return number.as_double(); - VERIFY_NOT_REACHED(); + + // FIXME: This should be okay for now as our naive Unicode::select_pattern_with_plurality implementation + // checks against just a few specific small values. But revisit this if precision becomes a concern. + return number.as_bigint().big_integer().to_double(); }; // 1. Let localeData be %NumberFormat%.[[LocaleData]]. @@ -1400,7 +1426,7 @@ Optional<Variant<StringView, String>> get_number_format_pattern(NumberFormat& nu StringView pattern; - bool is_positive_zero = number.is_positive_zero(); + bool is_positive_zero = number.is_positive_zero() || (number.is_bigint() && is_zero(number)); bool is_negative_zero = number.is_negative_zero(); bool is_nan = number.is_nan(); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp index 1a9dc04829..842c9472ee 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp @@ -44,10 +44,6 @@ ThrowCompletionOr<Value> NumberFormatFunction::call() // 4. 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<InternalError>(global_object, ErrorType::NotImplemented, "BigInt number formatting"); - // 5. Return ? FormatNumeric(nf, x). // Note: Our implementation of FormatNumeric does not throw. auto formatted = format_numeric(global_object, m_number_format, value); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp index be9e1de020..3880891147 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatPrototype.cpp @@ -70,10 +70,6 @@ JS_DEFINE_NATIVE_FUNCTION(NumberFormatPrototype::format_to_parts) // 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<InternalError>(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); 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 e14df84869..c95735680a 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 @@ -14,16 +14,6 @@ describe("errors", () => { Intl.NumberFormat().format(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().format(1n); - }).toThrowWithMessage( - InternalError, - "BigInt number formatting is not implemented in LibJS" - ); - }); }); describe("special values", () => { @@ -1019,3 +1009,63 @@ describe("style=unit", () => { expect(ja.format(123)).toBe("123km/h"); }); }); + +describe("bigint", () => { + test("default", () => { + const en = new Intl.NumberFormat("en"); + expect(en.format(1n)).toBe("1"); + expect(en.format(12n)).toBe("12"); + expect(en.format(123n)).toBe("123"); + expect(en.format(123456789123456789123456789123456789n)).toBe( + "123,456,789,123,456,789,123,456,789,123,456,789" + ); + + const ar = new Intl.NumberFormat("ar"); + expect(ar.format(1n)).toBe("\u0661"); + expect(ar.format(12n)).toBe("\u0661\u0662"); + expect(ar.format(123n)).toBe("\u0661\u0662\u0663"); + expect(ar.format(123456789123456789123456789123456789n)).toBe( + "\u0661\u0662\u0663\u066c\u0664\u0665\u0666\u066c\u0667\u0668\u0669\u066c\u0661\u0662\u0663\u066c\u0664\u0665\u0666\u066c\u0667\u0668\u0669\u066c\u0661\u0662\u0663\u066c\u0664\u0665\u0666\u066c\u0667\u0668\u0669\u066c\u0661\u0662\u0663\u066c\u0664\u0665\u0666\u066c\u0667\u0668\u0669" + ); + }); + + test("integer digits", () => { + const en = new Intl.NumberFormat("en", { minimumIntegerDigits: 2 }); + expect(en.format(1n)).toBe("01"); + expect(en.format(12n)).toBe("12"); + expect(en.format(123n)).toBe("123"); + + const ar = new Intl.NumberFormat("ar", { minimumIntegerDigits: 2 }); + expect(ar.format(1n)).toBe("\u0660\u0661"); + expect(ar.format(12n)).toBe("\u0661\u0662"); + expect(ar.format(123n)).toBe("\u0661\u0662\u0663"); + }); + + test("significant digits", () => { + const en = new Intl.NumberFormat("en", { + minimumSignificantDigits: 4, + maximumSignificantDigits: 6, + }); + expect(en.format(1n)).toBe("1.000"); + expect(en.format(12n)).toBe("12.00"); + expect(en.format(123n)).toBe("123.0"); + expect(en.format(1234n)).toBe("1,234"); + expect(en.format(12345n)).toBe("12,345"); + expect(en.format(123456n)).toBe("123,456"); + expect(en.format(1234567n)).toBe("1,234,570"); + expect(en.format(1234561n)).toBe("1,234,560"); + + const ar = new Intl.NumberFormat("ar", { + minimumSignificantDigits: 4, + maximumSignificantDigits: 6, + }); + expect(ar.format(1n)).toBe("\u0661\u066b\u0660\u0660\u0660"); + expect(ar.format(12n)).toBe("\u0661\u0662\u066b\u0660\u0660"); + expect(ar.format(123n)).toBe("\u0661\u0662\u0663\u066b\u0660"); + expect(ar.format(1234n)).toBe("\u0661\u066c\u0662\u0663\u0664"); + expect(ar.format(12345n)).toBe("\u0661\u0662\u066c\u0663\u0664\u0665"); + expect(ar.format(123456n)).toBe("\u0661\u0662\u0663\u066c\u0664\u0665\u0666"); + expect(ar.format(1234567n)).toBe("\u0661\u066c\u0662\u0663\u0664\u066c\u0665\u0667\u0660"); + expect(ar.format(1234561n)).toBe("\u0661\u066c\u0662\u0663\u0664\u066c\u0665\u0666\u0660"); + }); +}); 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 5a144e1ebc..ceddabddd4 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 @@ -10,16 +10,6 @@ describe("errors", () => { 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", () => { @@ -1310,3 +1300,71 @@ describe("style=unit", () => { ]); }); }); + +describe("bigint", () => { + test("default", () => { + const en = new Intl.NumberFormat("en"); + expect(en.formatToParts(123456n)).toEqual([ + { type: "integer", value: "123" }, + { type: "group", value: "," }, + { type: "integer", value: "456" }, + ]); + + const ar = new Intl.NumberFormat("ar"); + expect(ar.formatToParts(123456n)).toEqual([ + { type: "integer", value: "\u0661\u0662\u0663" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0664\u0665\u0666" }, + ]); + }); + + test("useGrouping=false", () => { + const en = new Intl.NumberFormat("en", { useGrouping: false }); + expect(en.formatToParts(123456n)).toEqual([{ type: "integer", value: "123456" }]); + + const ar = new Intl.NumberFormat("ar", { useGrouping: false }); + expect(ar.formatToParts(123456n)).toEqual([ + { type: "integer", value: "\u0661\u0662\u0663\u0664\u0665\u0666" }, + ]); + }); + + test("significant digits", () => { + const en = new Intl.NumberFormat("en", { + minimumSignificantDigits: 4, + maximumSignificantDigits: 6, + }); + expect(en.formatToParts(1234567n)).toEqual([ + { type: "integer", value: "1" }, + { type: "group", value: "," }, + { type: "integer", value: "234" }, + { type: "group", value: "," }, + { type: "integer", value: "570" }, + ]); + expect(en.formatToParts(1234561n)).toEqual([ + { type: "integer", value: "1" }, + { type: "group", value: "," }, + { type: "integer", value: "234" }, + { type: "group", value: "," }, + { type: "integer", value: "560" }, + ]); + + const ar = new Intl.NumberFormat("ar", { + minimumSignificantDigits: 4, + maximumSignificantDigits: 6, + }); + expect(ar.formatToParts(1234567n)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0662\u0663\u0664" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0665\u0667\u0660" }, + ]); + expect(ar.formatToParts(1234561n)).toEqual([ + { type: "integer", value: "\u0661" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0662\u0663\u0664" }, + { type: "group", value: "\u066c" }, + { type: "integer", value: "\u0665\u0666\u0660" }, + ]); + }); +}); |