diff options
author | Timothy Flynn <trflynn89@pm.me> | 2022-01-03 19:48:44 -0500 |
---|---|---|
committer | Linus Groh <mail@linusgroh.de> | 2022-01-04 13:07:42 +0000 |
commit | 534b2be16fd8857f28d00ce96151ef49c737509f (patch) | |
tree | 42926c092e6a98240baddf6a2ea91db59afec848 | |
parent | dc984c53d8b3976bd83eaa817e00025d0f62ad22 (diff) | |
download | serenity-534b2be16fd8857f28d00ce96151ef49c737509f.zip |
LibJS: Implement Number.prototype.toExponential
4 files changed, 244 insertions, 0 deletions
diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 30f5e2ddf0..56aca802e0 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -452,6 +452,7 @@ namespace JS { P(timeZone) \ P(timeZoneName) \ P(toDateString) \ + P(toExponential) \ P(toFixed) \ P(toGMTString) \ P(toInstant) \ diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp index 5ae2cd7209..0c828888af 100644 --- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp @@ -47,6 +47,36 @@ static String decimal_digits_to_string(double number) return builder.build().reverse(); } +static size_t compute_fraction_digits(double number, int exponent) +{ + double integral_part = 0; + double fraction_part = modf(number, &integral_part); + + auto fraction = String::number(fraction_part); + size_t fraction_digits = 0; + + if (integral_part != 0) + fraction_digits = exponent; + + if (auto decimal_index = fraction.find('.'); decimal_index.has_value()) { + fraction_digits += fraction.length() - *decimal_index - 1; + + if (integral_part == 0) { + --fraction_digits; + + for (size_t i = *decimal_index + 1; (i < fraction.length()) && (fraction[i] == '0'); ++i) + --fraction_digits; + } + } else if (integral_part != 0) { + auto integral = decimal_digits_to_string(integral_part); + + for (size_t i = integral.length(); (i > 0) && (integral[i - 1] == '0'); --i) + --fraction_digits; + } + + return fraction_digits; +} + NumberPrototype::NumberPrototype(GlobalObject& global_object) : NumberObject(0, *global_object.object_prototype()) { @@ -57,6 +87,7 @@ void NumberPrototype::initialize(GlobalObject& object) auto& vm = this->vm(); Object::initialize(object); u8 attr = Attribute::Configurable | Attribute::Writable; + define_native_function(vm.names.toExponential, to_exponential, 1, attr); 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); @@ -89,6 +120,127 @@ static ThrowCompletionOr<Value> this_number_value(GlobalObject& global_object, V return vm.throw_completion<TypeError>(global_object, ErrorType::NotAnObjectOfType, "Number"); } +// 21.1.3.2 Number.prototype.toExponential ( fractionDigits ), https://tc39.es/ecma262/#sec-number.prototype.toexponential +JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_exponential) +{ + auto fraction_digits_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. Let f be ? ToIntegerOrInfinity(fractionDigits). + auto fraction_digits = TRY(fraction_digits_value.to_integer_or_infinity(global_object)); + + // 3. Assert: If fractionDigits is undefined, then f is 0. + VERIFY(!fraction_digits_value.is_undefined() || (fraction_digits == 0)); + + // 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 f < 0 or f > 100, throw a RangeError exception. + if (fraction_digits < 0 || fraction_digits > 100) + return vm.throw_completion<RangeError>(global_object, ErrorType::InvalidFractionDigits); + + // 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 "-". + 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 f + 1 occurrences of the code unit 0x0030 (DIGIT ZERO). + number_string = String::repeated('0', fraction_digits + 1); + + // b. Let e be 0. + exponent = 0; + } + // 10. Else, + else { + // FIXME: The computations below fall apart for large values of 'f'. A double typically has 52 mantissa bits, which gives us + // up to 2^52 before loss of precision. However, the largest value of 'f' may be 100, resulting in numbers on the order + // of 10^100, thus we lose precision in these computations. + + // a. If fractionDigits is not undefined, then + // i. Let e and n be integers such that 10^f ≤ n < 10^(f+1) and for which n × 10^(e-f) - 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-f) is larger. + // b. Else, + // i. Let e, n, and f be integers such that f ≥ 0, 10^f ≤ n < 10^(f+1), 𝔽(n × 10^(e-f)) is 𝔽(x), and f is as small as possible. + // Note that the decimal representation of n has f + 1 digits, n is not divisible by 10, and the least significant digit of n is not necessarily uniquely determined by these criteria. + exponent = static_cast<int>(floor(log10(number))); + + if (fraction_digits_value.is_undefined()) + fraction_digits = compute_fraction_digits(number, exponent); + + number = round(number / pow(10, exponent - fraction_digits)); + + // c. 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); + } + + // 11. If f ≠ 0, then + if (fraction_digits != 0) { + // a. Let a be the first code unit of m. + auto first = number_string.substring_view(0, 1); + + // b. Let b be the other f code units of m. + auto second = number_string.substring_view(1); + + // c. Set m to the string-concatenation of a, ".", and b. + number_string = String::formatted("{}.{}", first, second); + } + + char exponent_sign = 0; + String exponent_string; + + // 12. If e = 0, then + if (exponent == 0) { + // a. Let c be "+". + exponent_sign = '+'; + + // b. Let d be "0". + exponent_string = "0"sv; + } + // 13. Else, + else { + // a. If e > 0, let c be "+". + if (exponent > 0) { + exponent_sign = '+'; + } + // b. Else, + else { + // i. Assert: e < 0. + VERIFY(exponent < 0); + + // ii. Let c be "-". + exponent_sign = '-'; + + // iii. Set e to -e. + exponent = -exponent; + } + + // c. Let d be the String value consisting of the digits of the decimal representation of e (in order, with no leading zeroes). + exponent_string = String::number(exponent); + } + + // 14. Set m to the string-concatenation of m, "e", c, and d. + // 15. Return the string-concatenation of s and m. + return js_string(vm, String::formatted("{}{}e{}{}", sign, number_string, exponent_sign, exponent_string)); +} + // 21.1.3.3 Number.prototype.toFixed ( fractionDigits ), https://tc39.es/ecma262/#sec-number.prototype.tofixed JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_fixed) { diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.h b/Userland/Libraries/LibJS/Runtime/NumberPrototype.h index 8baced75ea..2cdf545364 100644 --- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.h @@ -18,6 +18,7 @@ public: virtual void initialize(GlobalObject&) override; virtual ~NumberPrototype() override; + JS_DECLARE_NATIVE_FUNCTION(to_exponential); JS_DECLARE_NATIVE_FUNCTION(to_fixed); JS_DECLARE_NATIVE_FUNCTION(to_locale_string); JS_DECLARE_NATIVE_FUNCTION(to_precision); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toExponential.js b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toExponential.js new file mode 100644 index 0000000000..567139d98b --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Number/Number.prototype.toExponential.js @@ -0,0 +1,90 @@ +describe("errors", () => { + test("must be called with numeric |this|", () => { + [true, [], {}, Symbol("foo"), "bar", 1n].forEach(value => { + expect(() => { + Number.prototype.toExponential.call(value); + }).toThrowWithMessage(TypeError, "Not an object of type Number"); + }); + }); + + test("fraction digits must be coercible to a number", () => { + expect(() => { + (0).toExponential(Symbol("foo")); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + + expect(() => { + (0).toExponential(1n); + }).toThrowWithMessage(TypeError, "Cannot convert BigInt to number"); + }); + + test("out of range fraction digits", () => { + [-Infinity, -1, 101, Infinity].forEach(value => { + expect(() => { + (0).toExponential(value); + }).toThrowWithMessage( + RangeError, + "Fraction Digits must be an integer no less than 0, and no greater than 100" + ); + }); + }); +}); + +describe("correct behavior", () => { + test("special values", () => { + [ + [Infinity, 6, "Infinity"], + [-Infinity, 7, "-Infinity"], + [NaN, 8, "NaN"], + [0, 0, "0e+0"], + [0, 1, "0.0e+0"], + [0, 3, "0.000e+0"], + ].forEach(test => { + expect(test[0].toExponential(test[1])).toBe(test[2]); + }); + }); + + test("zero exponent", () => { + [ + [1, 0, "1e+0"], + [5, 1, "5.0e+0"], + [9, 3, "9.000e+0"], + ].forEach(test => { + expect(test[0].toExponential(test[1])).toBe(test[2]); + }); + }); + + test("positive exponent", () => { + [ + [12, 0, "1e+1"], + [345, 1, "3.5e+2"], + [6789, 3, "6.789e+3"], + ].forEach(test => { + expect(test[0].toExponential(test[1])).toBe(test[2]); + }); + }); + + test("negative exponent", () => { + [ + [0.12, 0, "1e-1"], + [0.0345, 1, "3.5e-2"], + [0.006789, 3, "6.789e-3"], + ].forEach(test => { + expect(test[0].toExponential(test[1])).toBe(test[2]); + }); + }); + + test("undefined precision", () => { + [ + [123.456, "1.23456e+2"], + [13, "1.3e+1"], + [100, "1e+2"], + [345, "3.45e+2"], + [6789, "6.789e+3"], + [0.13, "1.3e-1"], + [0.0345, "3.45e-2"], + [0.006789, "6.789e-3"], + ].forEach(test => { + expect(test[0].toExponential()).toBe(test[1]); + }); + }); +}); |