فهرست منبع

LibJS: Implement parseInt()

Here's a reasonably faithful implementation of ECMAScript 2021 18.2.5.
Some corner cases are not covered, I've left them as FIXME's in the
included unit test.

Also I had to tweak JS::Value::to_i32() to always convert infinity to
zero, which is in accordance with ToInt32 AFAICT.
Andreas Kling 4 سال پیش
والد
کامیت
e6dadd9e5b

+ 1 - 0
Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -168,6 +168,7 @@ namespace JS {
     P(padStart)                              \
     P(parse)                                 \
     P(parseFloat)                            \
+    P(parseInt)                              \
     P(pop)                                   \
     P(pow)                                   \
     P(preventExtensions)                     \

+ 71 - 0
Libraries/LibJS/Runtime/GlobalObject.cpp

@@ -26,6 +26,7 @@
  */
 
 #include <AK/LogStream.h>
+#include <AK/Utf8View.h>
 #include <LibJS/Console.h>
 #include <LibJS/Heap/DeferGC.h>
 #include <LibJS/Runtime/ArrayBufferConstructor.h>
@@ -68,6 +69,7 @@
 #include <LibJS/Runtime/TypedArrayConstructor.h>
 #include <LibJS/Runtime/TypedArrayPrototype.h>
 #include <LibJS/Runtime/Value.h>
+#include <ctype.h>
 
 namespace JS {
 
@@ -117,6 +119,7 @@ void GlobalObject::initialize()
     define_native_function(vm.names.isNaN, is_nan, 1, attr);
     define_native_function(vm.names.isFinite, is_finite, 1, attr);
     define_native_function(vm.names.parseFloat, parse_float, 1, attr);
+    define_native_function(vm.names.parseInt, parse_int, 1, attr);
 
     define_property(vm.names.NaN, js_nan(), 0);
     define_property(vm.names.Infinity, js_infinity(), 0);
@@ -213,6 +216,74 @@ JS_DEFINE_NATIVE_FUNCTION(GlobalObject::parse_float)
     return js_nan();
 }
 
+JS_DEFINE_NATIVE_FUNCTION(GlobalObject::parse_int)
+{
+    // 18.2.5 parseInt ( string, radix )
+    auto input_string = vm.argument(0).to_string(global_object);
+    if (vm.exception())
+        return {};
+
+    // FIXME: There's a bunch of unnecessary string copying here.
+    double sign = 1;
+    auto s = input_string.trim_whitespace(TrimMode::Left);
+    if (!s.is_empty() && s[0] == '-')
+        sign = -1;
+    if (!s.is_empty() && (s[0] == '+' || s[0] == '-'))
+        s = s.substring(1, s.length() - 1);
+
+    auto radix = vm.argument(1).to_i32(global_object);
+    if (vm.exception())
+        return {};
+
+    bool strip_prefix = true;
+    if (radix != 0) {
+        if (radix < 2 || radix > 36)
+            return js_nan();
+        if (radix != 16)
+            strip_prefix = false;
+    } else {
+        radix = 10;
+    }
+
+    if (strip_prefix) {
+        if (s.length() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
+            s = s.substring(2, s.length() - 2);
+            radix = 16;
+        }
+    }
+
+    auto parse_digit = [&](u32 codepoint, i32 radix) -> Optional<i32> {
+        i32 digit = -1;
+
+        if (isdigit(codepoint))
+            digit = codepoint - '0';
+        else if (islower(codepoint))
+            digit = 10 + (codepoint - 'a');
+        else if (isupper(codepoint))
+            digit = 10 + (codepoint - 'A');
+
+        if (digit == -1 || digit >= radix)
+            return {};
+        return digit;
+    };
+
+    bool had_digits = false;
+    double number = 0;
+    for (auto codepoint : Utf8View(s)) {
+        auto digit = parse_digit(codepoint, radix);
+        if (!digit.has_value())
+            break;
+        had_digits = true;
+        number *= radix;
+        number += digit.value();
+    }
+
+    if (!had_digits)
+        return js_nan();
+
+    return Value(sign * number);
+}
+
 Optional<Variable> GlobalObject::get_from_scope(const FlyString& name) const
 {
     auto value = get(name);

+ 1 - 0
Libraries/LibJS/Runtime/GlobalObject.h

@@ -82,6 +82,7 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(is_nan);
     JS_DECLARE_NATIVE_FUNCTION(is_finite);
     JS_DECLARE_NATIVE_FUNCTION(parse_float);
+    JS_DECLARE_NATIVE_FUNCTION(parse_int);
 
     NonnullOwnPtr<Console> m_console;
 

+ 1 - 3
Libraries/LibJS/Runtime/Value.cpp

@@ -356,10 +356,8 @@ i32 Value::to_i32(GlobalObject& global_object) const
     auto number = to_number(global_object);
     if (global_object.vm().exception())
         return 0;
-    if (number.is_nan())
+    if (number.is_nan() || number.is_infinity())
         return 0;
-    // FIXME: What about infinity though - that's UB...
-    // Maybe NumericLimits<i32>::max() for +Infinity and NumericLimits<i32>::min() for -Infinity?
     return number.as_i32();
 }
 

+ 51 - 0
Libraries/LibJS/Tests/parseInt.js

@@ -0,0 +1,51 @@
+test("basic parseInt() functionality", () => {
+    expect(parseInt("0")).toBe(0);
+    expect(parseInt("100")).toBe(100);
+    expect(parseInt("1000", 16)).toBe(4096);
+    expect(parseInt('0xF', 16)).toBe(15)
+    expect(parseInt('F', 16)).toBe(15)
+    expect(parseInt('17', 8)).toBe(15)
+    expect(parseInt(021, 8)).toBe(15)
+    expect(parseInt('015', 10)).toBe(15)
+    expect(parseInt(15.99, 10)).toBe(15)
+    expect(parseInt('15,123', 10)).toBe(15)
+    expect(parseInt('FXX123', 16)).toBe(15)
+    expect(parseInt('1111', 2)).toBe(15)
+    expect(parseInt('15 * 3', 10)).toBe(15)
+    expect(parseInt('15e2', 10)).toBe(15)
+    expect(parseInt('15px', 10)).toBe(15)
+    expect(parseInt('12', 13)).toBe(15)
+    expect(parseInt('Hello', 8)).toBeNaN();
+    expect(parseInt('546', 2)).toBeNaN();
+    expect(parseInt('-F', 16)).toBe(-15);
+    expect(parseInt('-0F', 16)).toBe(-15);
+    expect(parseInt('-0XF', 16)).toBe(-15);
+    expect(parseInt(-15.1, 10)).toBe(-15);
+    expect(parseInt('-17', 8)).toBe(-15);
+    expect(parseInt('-15', 10)).toBe(-15);
+    expect(parseInt('-1111', 2)).toBe(-15);
+    expect(parseInt('-15e1', 10)).toBe(-15);
+    expect(parseInt('-12', 13)).toBe(-15);
+    expect(parseInt(4.7, 10)).toBe(4);
+    expect(parseInt('0e0', 16)).toBe(224);
+    expect(parseInt('123_456')).toBe(123);
+
+    // FIXME: expect(parseInt(4.7 * 1e22, 10)).toBe(4);
+    // FIXME: expect(parseInt(0.00000000000434, 10)).toBe(4);
+    // FIXME: expect(parseInt(0.0000001,11)).toBe(1);
+    // FIXME: expect(parseInt(0.000000124,10)).toBe(1);
+    // FIXME: expect(parseInt(1e-7,10)).toBe(1);
+    // FIXME: expect(parseInt(1000000000000100000000,10)).toBe(1);
+    // FIXME: expect(parseInt(123000000000010000000000,10)).toBe(1);
+    // FIXME: expect(parseInt(1e+21,10)).toBe(1);
+    // FIXME: expect(parseInt('900719925474099267n')).toBe(900719925474099300)
+});
+
+test("parseInt() radix is coerced to a number", () => {
+    const obj = { valueOf() { return 8; } };
+    expect(parseInt('11', obj)).toBe(9);
+    obj.valueOf = function() { return 1; }
+    expect(parseInt('11', obj)).toBeNaN();
+    obj.valueOf = function() { return Infinity; }
+    expect(parseInt('11', obj)).toBe(11);
+});