Ver Fonte

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...".
Timothy Flynn há 3 anos atrás
pai
commit
dc984c53d8

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

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

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

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

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