diff options
author | Timothy Flynn <trflynn89@pm.me> | 2022-01-02 23:06:51 -0500 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-01-04 13:07:42 +0000 |
commit | dc984c53d8b3976bd83eaa817e00025d0f62ad22 (patch) | |
tree | bb748ef4d9cf0fc488e4974d284b5666c0133980 /Userland | |
parent | f1eb975a7a53986889ff8ffdb93fd8dbd1463a4d (diff) | |
download | serenity-dc984c53d8b3976bd83eaa817e00025d0f62ad22.zip |
LibJS: Implement Number.prototype.toPrecision
As noted in the prototype comments, this implementation becomes less
accurate as the precision approaches the limit of 100. For example:
(3).toPrecision(100)
Should result in "3." followed by 99 "0"s. However, due to the loss of
accuracy in the floating point computations, we currently result in
"2.9999999...".
Diffstat (limited to 'Userland')
5 files changed, 260 insertions, 0 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 45c0736943..30f5e2ddf0 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -468,6 +468,7 @@ namespace JS { P(toPlainMonthDay) \ P(toPlainTime) \ P(toPlainYearMonth) \ + P(toPrecision) \ P(toString) \ P(total) \ P(toTemporalInstant) \ diff --git a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h index 6ee6ee6d29..f4b6017541 100644 --- a/Userland/Libraries/LibJS/Runtime/ErrorTypes.h +++ b/Userland/Libraries/LibJS/Runtime/ErrorTypes.h @@ -50,6 +50,7 @@ M(InvalidIndex, "Index must be a positive integer") \ M(InvalidLeftHandAssignment, "Invalid left-hand side in assignment") \ M(InvalidLength, "Invalid {} length") \ + M(InvalidPrecision, "Precision must be an integer no less than 1, and no greater than 100") \ M(InvalidTimeValue, "Invalid time value") \ M(InvalidRadix, "Radix must be an integer no less than 2, and no greater than 36") \ M(IsNotA, "{} is not a {}") \ diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp index 6b9773b967..5ae2cd7209 100644 --- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp @@ -15,6 +15,7 @@ #include <LibJS/Runtime/Intl/NumberFormatConstructor.h> #include <LibJS/Runtime/NumberObject.h> #include <LibJS/Runtime/NumberPrototype.h> +#include <math.h> namespace JS { @@ -29,6 +30,23 @@ static const u8 max_precision_for_radix[37] = { static char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; +static String decimal_digits_to_string(double number) +{ + StringBuilder builder; + + double integral_part = 0; + (void)modf(number, &integral_part); + + while (integral_part > 0) { + auto index = static_cast<size_t>(fmod(integral_part, 10)); + builder.append(digits[index]); + + integral_part = floor(integral_part / 10.0); + } + + return builder.build().reverse(); +} + NumberPrototype::NumberPrototype(GlobalObject& global_object) : NumberObject(0, *global_object.object_prototype()) { @@ -41,6 +59,7 @@ void NumberPrototype::initialize(GlobalObject& 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.toPrecision, to_precision, 1, attr); define_native_function(vm.names.toString, to_string, 1, attr); define_native_function(vm.names.valueOf, value_of, 0, attr); } @@ -133,6 +152,138 @@ JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_locale_string) return js_string(vm, move(formatted)); } +// 21.1.3.5 Number.prototype.toPrecision ( precision ), https://tc39.es/ecma262/#sec-number.prototype.toprecision +JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_precision) +{ + auto precision_value = vm.argument(0); + + // 1. Let x be ? thisNumberValue(this value). + auto number_value = TRY(this_number_value(global_object, vm.this_value(global_object))); + + // 2. If precision is undefined, return ! ToString(x). + if (precision_value.is_undefined()) + return js_string(vm, MUST(number_value.to_string(global_object))); + + // 3. Let p be ? ToIntegerOrInfinity(precision). + auto precision = TRY(precision_value.to_integer_or_infinity(global_object)); + + // 4. If x is not finite, return ! Number::toString(x). + if (!number_value.is_finite_number()) + return js_string(vm, MUST(number_value.to_string(global_object))); + + // 5. If p < 1 or p > 100, throw a RangeError exception. + if ((precision < 1) || (precision > 100)) + return vm.throw_completion<RangeError>(global_object, ErrorType::InvalidPrecision); + + // 6. Set x to ℝ(x). + auto number = number_value.as_double(); + + // 7. Let s be the empty String. + auto sign = ""sv; + + String number_string; + int exponent = 0; + + // 8. If x < 0, then + if (number < 0) { + // a. Set s to the code unit 0x002D (HYPHEN-MINUS). + sign = "-"sv; + + // b. Set x to -x. + number = -number; + } + + // 9. If x = 0, then + if (number == 0) { + // a. Let m be the String value consisting of p occurrences of the code unit 0x0030 (DIGIT ZERO). + number_string = String::repeated('0', precision); + + // b. Let e be 0. + exponent = 0; + } + // 10. Else, + else { + // FIXME: The computations below fall apart for large values of 'p'. A double typically has 52 mantissa bits, which gives us + // up to 2^52 before loss of precision. However, the largest value of 'p' may be 100, resulting in numbers on the order + // of 10^100, thus we lose precision in these computations. + + // 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 = static_cast<int>(floor(log10(number))); + number = round(number / pow(10, exponent - precision + 1)); + + // b. Let m be the String value consisting of the digits of the decimal representation of n (in order, with no leading zeroes). + number_string = decimal_digits_to_string(number); + + // c. If e < -6 or e ≥ p, then + if ((exponent < -6) || (exponent >= precision)) { + // i. Assert: e ≠ 0. + VERIFY(exponent != 0); + + // ii. If p ≠ 1, then + if (precision != 1) { + // 1. Let a be the first code unit of m. + auto first = number_string.substring_view(0, 1); + + // 2. Let b be the other p - 1 code units of m. + auto second = number_string.substring_view(1); + + // 3. Set m to the string-concatenation of a, ".", and b. + number_string = String::formatted("{}.{}", first, second); + } + + char exponent_sign = 0; + + // iii. If e > 0, then + if (exponent > 0) { + // 1. Let c be the code unit 0x002B (PLUS SIGN). + exponent_sign = '+'; + } + // iv. Else, + else { + // 1. Assert: e < 0. + VERIFY(exponent < 0); + + // 2. Let c be the code unit 0x002D (HYPHEN-MINUS). + exponent_sign = '-'; + + // 3. Set e to -e. + exponent = -exponent; + } + + // v. Let d be the String value consisting of the digits of the decimal representation of e (in order, with no leading zeroes). + auto exponent_string = String::number(exponent); + + // vi. Return the string-concatenation of s, m, the code unit 0x0065 (LATIN SMALL LETTER E), c, and d. + return js_string(vm, String::formatted("{}{}e{}{}", sign, number_string, exponent_sign, exponent_string)); + } + } + + // 11. If e = p - 1, return the string-concatenation of s and m. + if (exponent == precision - 1) + return js_string(vm, String::formatted("{}{}", sign, number_string)); + + // 12. If e ≥ 0, then + if (exponent >= 0) { + // a. Set m to the string-concatenation of the first e + 1 code units of m, the code unit 0x002E (FULL STOP), and the remaining p - (e + 1) code units of m. + number_string = String::formatted( + "{}.{}", + number_string.substring_view(0, exponent + 1), + number_string.substring_view(exponent + 1)); + } + // 13. Else, + else { + // a. Set m to the string-concatenation of the code unit 0x0030 (DIGIT ZERO), the code unit 0x002E (FULL STOP), -(e + 1) occurrences of the code unit 0x0030 (DIGIT ZERO), and the String m. + number_string = String::formatted( + "0.{}{}", + String::repeated('0', -1 * (exponent + 1)), + number_string); + } + + // 14. Return the string-concatenation of s and m. + return js_string(vm, String::formatted("{}{}", sign, number_string)); +} + // 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 bf35eb8332..8baced75ea 100644 --- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.h @@ -20,6 +20,7 @@ public: JS_DECLARE_NATIVE_FUNCTION(to_fixed); JS_DECLARE_NATIVE_FUNCTION(to_locale_string); + JS_DECLARE_NATIVE_FUNCTION(to_precision); JS_DECLARE_NATIVE_FUNCTION(to_string); JS_DECLARE_NATIVE_FUNCTION(value_of); }; diff --git a/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toPrecision.js b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toPrecision.js new file mode 100644 index 0000000000..6b3203576b --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toPrecision.js @@ -0,0 +1,106 @@ +describe("errors", () => { + test("must be called with numeric |this|", () => { + [true, [], {}, Symbol("foo"), "bar", 1n].forEach(value => { + expect(() => { + Number.prototype.toPrecision.call(value); + }).toThrowWithMessage(TypeError, "Not an object of type Number"); + }); + }); + + test("precision must be coercible to a number", () => { + expect(() => { + (0).toPrecision(Symbol("foo")); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + + expect(() => { + (0).toPrecision(1n); + }).toThrowWithMessage(TypeError, "Cannot convert BigInt to number"); + }); + + test("out of range precision", () => { + [-Infinity, 0, 101, Infinity].forEach(value => { + expect(() => { + (0).toPrecision(value); + }).toThrowWithMessage( + RangeError, + "Precision must be an integer no less than 1, and no greater than 100" + ); + }); + }); +}); + +describe("correct behavior", () => { + test("special values", () => { + [ + [Infinity, 6, "Infinity"], + [-Infinity, 7, "-Infinity"], + [NaN, 8, "NaN"], + [0, 1, "0"], + [0, 3, "0.00"], + [0, 5, "0.0000"], + ].forEach(test => { + expect(test[0].toPrecision(test[1])).toBe(test[2]); + }); + }); + + test("undefined precision yields plain number-to-string conversion", () => { + [ + [123, undefined, "123"], + [3.14, undefined, "3.14"], + ].forEach(test => { + expect(test[0].toPrecision(test[1])).toBe(test[2]); + }); + }); + + test("formatted as exponential string", () => { + [ + // exponent < -6 + [0.0000002, 5, "2.0000e-7"], + [0.00000000189, 3, "1.89e-9"], + [0.00000000189, 2, "1.9e-9"], + + // exponent >= precision + [100, 1, "1e+2"], + [100, 2, "1.0e+2"], + [1234589, 3, "1.23e+6"], + [1234589, 4, "1.235e+6"], + [1234589, 5, "1.2346e+6"], + ].forEach(test => { + expect(test[0].toPrecision(test[1])).toBe(test[2]); + }); + }); + + test("formatted without decimal", () => { + [ + // exponent == precision - 1 + [1, 1, "1"], + [123, 3, "123"], + [123.45, 3, "123"], + ].forEach(test => { + expect(test[0].toPrecision(test[1])).toBe(test[2]); + }); + }); + + test("non-negative exponent", () => { + [ + // exponent >= 0 + [1, 4, "1.000"], + [123, 4, "123.0"], + [123.45, 4, "123.5"], + ].forEach(test => { + expect(test[0].toPrecision(test[1])).toBe(test[2]); + }); + }); + + test("negative exponent", () => { + [ + // exponent < 0 + [0.1, 1, "0.1"], + [0.0123, 3, "0.0123"], + [0.0012345, 3, "0.00123"], + [0.0012345, 4, "0.001235"], + ].forEach(test => { + expect(test[0].toPrecision(test[1])).toBe(test[2]); + }); + }); +}); |