Kaynağa Gözat

LibJS: Implement Number.prototype.toExponential

Timothy Flynn 3 yıl önce
ebeveyn
işleme
534b2be16f

+ 1 - 0
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)                             \

+ 152 - 0
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)
 {

+ 1 - 0
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);

+ 90 - 0
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]);
+        });
+    });
+});