Quellcode durchsuchen

LibJS: Implement proposed Array.prototype.findLast{,Index}

Proposal: https://tc39.es/proposal-array-find-from-last/
davidot vor 3 Jahren
Ursprung
Commit
b6523906b3

+ 103 - 0
Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp

@@ -58,6 +58,8 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
     define_native_function(vm.names.includes, includes, 1, attr);
     define_native_function(vm.names.find, find, 1, attr);
     define_native_function(vm.names.findIndex, find_index, 1, attr);
+    define_native_function(vm.names.findLast, find_last, 1, attr);
+    define_native_function(vm.names.findLastIndex, find_last_index, 1, attr);
     define_native_function(vm.names.some, some, 1, attr);
     define_native_function(vm.names.every, every, 1, attr);
     define_native_function(vm.names.splice, splice, 2, attr);
@@ -77,12 +79,15 @@ void ArrayPrototype::initialize(GlobalObject& global_object)
     define_direct_property(*vm.well_known_symbol_iterator(), get(vm.names.values), attr);
 
     // 23.1.3.34 Array.prototype [ @@unscopables ], https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
+    // With proposal, https://tc39.es/proposal-array-find-from-last/index.html#sec-array.prototype-@@unscopables
     auto* unscopable_list = Object::create(global_object, nullptr);
     unscopable_list->create_data_property_or_throw(vm.names.copyWithin, Value(true));
     unscopable_list->create_data_property_or_throw(vm.names.entries, Value(true));
     unscopable_list->create_data_property_or_throw(vm.names.fill, Value(true));
     unscopable_list->create_data_property_or_throw(vm.names.find, Value(true));
     unscopable_list->create_data_property_or_throw(vm.names.findIndex, Value(true));
+    unscopable_list->create_data_property_or_throw(vm.names.findLast, Value(true));
+    unscopable_list->create_data_property_or_throw(vm.names.findLastIndex, Value(true));
     unscopable_list->create_data_property_or_throw(vm.names.flat, Value(true));
     unscopable_list->create_data_property_or_throw(vm.names.flatMap, Value(true));
     unscopable_list->create_data_property_or_throw(vm.names.includes, Value(true));
@@ -1492,6 +1497,104 @@ JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::find_index)
     return Value(-1);
 }
 
+// 1 Array.prototype.findLast ( predicate [ , thisArg ] ), https://tc39.es/proposal-array-find-from-last/index.html#sec-array.prototype.findlast
+JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::find_last)
+{
+    auto predicate = vm.argument(0);
+    auto this_arg = vm.argument(1);
+
+    // 1. Let O be ? ToObject(this value).
+    auto* object = vm.this_value(global_object).to_object(global_object);
+    if (vm.exception())
+        return {};
+
+    // 2. Let len be ? LengthOfArrayLike(O).
+    auto length = length_of_array_like(global_object, *object);
+    if (vm.exception())
+        return {};
+
+    // 3. If IsCallable(predicate) is false, throw a TypeError exception.
+    if (!predicate.is_function()) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::NotAFunction, predicate.to_string_without_side_effects());
+        return {};
+    }
+
+    // 4. Let k be len - 1.
+    // 5. Repeat, while k ≥ 0,
+    for (i64 k = length - 1; k >= 0; --k) {
+        // a. Let Pk be ! ToString(𝔽(k)).
+        auto property_name = PropertyName { k };
+
+        // b. Let kValue be ? Get(O, Pk).
+        auto k_value = object->get(property_name);
+        if (vm.exception())
+            return {};
+
+        // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)).
+        auto test_result = vm.call(predicate.as_function(), this_arg, k_value, Value((double)k), object);
+        if (vm.exception())
+            return {};
+
+        // d. If testResult is true, return kValue.
+        if (test_result.to_boolean())
+            return k_value;
+
+        // e. Set k to k - 1.
+    }
+
+    // 6. Return undefined.
+    return js_undefined();
+}
+
+// 2 Array.prototype.findLastIndex ( predicate [ , thisArg ] ), https://tc39.es/proposal-array-find-from-last/index.html#sec-array.prototype.findlastindex
+JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::find_last_index)
+{
+    auto predicate = vm.argument(0);
+    auto this_arg = vm.argument(1);
+
+    // 1. Let O be ? ToObject(this value).
+    auto* object = vm.this_value(global_object).to_object(global_object);
+    if (vm.exception())
+        return {};
+
+    // 2. Let len be ? LengthOfArrayLike(O).
+    auto length = length_of_array_like(global_object, *object);
+    if (vm.exception())
+        return {};
+
+    // 3. If IsCallable(predicate) is false, throw a TypeError exception.
+    if (!predicate.is_function()) {
+        vm.throw_exception<TypeError>(global_object, ErrorType::NotAFunction, predicate.to_string_without_side_effects());
+        return {};
+    }
+
+    // 4. Let k be len - 1.
+    // 5. Repeat, while k ≥ 0,
+    for (i64 k = length - 1; k >= 0; --k) {
+        // a. Let Pk be ! ToString(𝔽(k)).
+        auto property_name = PropertyName { k };
+
+        // b. Let kValue be ? Get(O, Pk).
+        auto k_value = object->get(property_name);
+        if (vm.exception())
+            return {};
+
+        // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)).
+        auto test_result = vm.call(predicate.as_function(), this_arg, k_value, Value((double)k), object);
+        if (vm.exception())
+            return {};
+
+        // d. If testResult is true, return 𝔽(k).
+        if (test_result.to_boolean())
+            return Value((double)k);
+
+        // e. Set k to k - 1.
+    }
+
+    // 6. Return -1𝔽.
+    return Value(-1);
+}
+
 // 23.1.3.26 Array.prototype.some ( callbackfn [ , thisArg ] ), https://tc39.es/ecma262/#sec-array.prototype.some
 JS_DEFINE_NATIVE_FUNCTION(ArrayPrototype::some)
 {

+ 2 - 0
Userland/Libraries/LibJS/Runtime/ArrayPrototype.h

@@ -41,6 +41,8 @@ private:
     JS_DECLARE_NATIVE_FUNCTION(includes);
     JS_DECLARE_NATIVE_FUNCTION(find);
     JS_DECLARE_NATIVE_FUNCTION(find_index);
+    JS_DECLARE_NATIVE_FUNCTION(find_last);
+    JS_DECLARE_NATIVE_FUNCTION(find_last_index);
     JS_DECLARE_NATIVE_FUNCTION(some);
     JS_DECLARE_NATIVE_FUNCTION(every);
     JS_DECLARE_NATIVE_FUNCTION(splice);

+ 2 - 0
Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h

@@ -146,6 +146,8 @@ namespace JS {
     P(filter)                                \
     P(finally)                               \
     P(find)                                  \
+    P(findLast)                              \
+    P(findLastIndex)                         \
     P(findIndex)                             \
     P(fixed)                                 \
     P(flags)                                 \

+ 61 - 0
Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype.findLast.js

@@ -0,0 +1,61 @@
+test("length is 1", () => {
+    expect(Array.prototype.findLast).toHaveLength(1);
+});
+
+describe("errors", () => {
+    test("callback must be a function", () => {
+        expect(() => {
+            [].findLast(undefined);
+        }).toThrowWithMessage(TypeError, "undefined is not a function");
+    });
+});
+
+describe("normal behavior", () => {
+    test("basic functionality", () => {
+        var array = ["hello", "friends", 1, 2, false];
+
+        expect(array.findLast(value => value === "hello")).toBe("hello");
+        expect(array.findLast((value, index, arr) => index === 1)).toBe("friends");
+        expect(array.findLast(value => value == "1")).toBe(1);
+        expect(array.findLast(value => value === 1)).toBe(1);
+        expect(array.findLast(value => typeof value !== "string")).toBeFalse();
+        expect(array.findLast(value => typeof value === "boolean")).toBeFalse();
+        expect(array.findLast(value => typeof value === "string")).toBe("friends");
+        expect(array.findLast(value => value > 1)).toBe(2);
+        expect(array.findLast(value => value >= 1)).toBe(2);
+        expect(array.findLast(value => value > 1 && value < 3)).toBe(2);
+        expect(array.findLast(value => value > 100)).toBeUndefined();
+        expect([].findLast(value => value === 1)).toBeUndefined();
+    });
+
+    test("never calls callback with empty array", () => {
+        var callbackCalled = 0;
+        expect(
+            [].findLast(() => {
+                callbackCalled++;
+            })
+        ).toBeUndefined();
+        expect(callbackCalled).toBe(0);
+    });
+
+    test("calls callback once for every item", () => {
+        var callbackCalled = 0;
+        expect(
+            [1, 2, 3].findLast(() => {
+                callbackCalled++;
+            })
+        ).toBeUndefined();
+        expect(callbackCalled).toBe(3);
+    });
+
+    test("empty slots are treated as undefined", () => {
+        var callbackCalled = 0;
+        expect(
+            [1, , , "foo", , undefined, , , 6].findLast(value => {
+                callbackCalled++;
+                return value === undefined;
+            })
+        ).toBeUndefined();
+        expect(callbackCalled).toBe(2);
+    });
+});

+ 61 - 0
Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype.findLastIndex.js

@@ -0,0 +1,61 @@
+test("length is 1", () => {
+    expect(Array.prototype.findLastIndex).toHaveLength(1);
+});
+
+describe("errors", () => {
+    test("callback must be a function", () => {
+        expect(() => {
+            [].findLastIndex(undefined);
+        }).toThrowWithMessage(TypeError, "undefined is not a function");
+    });
+});
+
+describe("normal behavior", () => {
+    test("basic functionality", () => {
+        var array = ["hello", "friends", 1, 2, false];
+
+        expect(array.findLastIndex(value => value === "hello")).toBe(0);
+        expect(array.findLastIndex((value, index, arr) => index === 1)).toBe(1);
+        expect(array.findLastIndex(value => value == "1")).toBe(2);
+        expect(array.findLastIndex(value => value === 1)).toBe(2);
+        expect(array.findLastIndex(value => typeof value !== "string")).toBe(4);
+        expect(array.findLastIndex(value => typeof value === "boolean")).toBe(4);
+        expect(array.findLastIndex(value => typeof value === "string")).toBe(1);
+        expect(array.findLastIndex(value => value > 1)).toBe(3);
+        expect(array.findLastIndex(value => value >= 1)).toBe(3);
+        expect(array.findLastIndex(value => value > 1 && value < 3)).toBe(3);
+        expect(array.findLastIndex(value => value > 100)).toBe(-1);
+        expect([].findLastIndex(value => value === 1)).toBe(-1);
+    });
+
+    test("never calls callback with empty array", () => {
+        var callbackCalled = 0;
+        expect(
+            [].findLastIndex(() => {
+                callbackCalled++;
+            })
+        ).toBe(-1);
+        expect(callbackCalled).toBe(0);
+    });
+
+    test("calls callback once for every item", () => {
+        var callbackCalled = 0;
+        expect(
+            [1, 2, 3].findLastIndex(() => {
+                callbackCalled++;
+            })
+        ).toBe(-1);
+        expect(callbackCalled).toBe(3);
+    });
+
+    test("empty slots are treated as undefined", () => {
+        var callbackCalled = 0;
+        expect(
+            [1, , , "foo", , undefined, , , 6].findLastIndex(value => {
+                callbackCalled++;
+                return value === undefined;
+            })
+        ).toBe(7);
+        expect(callbackCalled).toBe(2);
+    });
+});