diff --git a/Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp b/Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp index 718364be00..aa32e12814 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayPrototype.cpp +++ b/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(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(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) { diff --git a/Userland/Libraries/LibJS/Runtime/ArrayPrototype.h b/Userland/Libraries/LibJS/Runtime/ArrayPrototype.h index 7d392762ec..f58557cd2c 100644 --- a/Userland/Libraries/LibJS/Runtime/ArrayPrototype.h +++ b/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); diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 649c19a181..a26cba67e8 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/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) \ diff --git a/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype.findLast.js b/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype.findLast.js new file mode 100644 index 0000000000..692dc41c11 --- /dev/null +++ b/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); + }); +}); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype.findLastIndex.js b/Userland/Libraries/LibJS/Tests/builtins/Array/Array.prototype.findLastIndex.js new file mode 100644 index 0000000000..f5d6562189 --- /dev/null +++ b/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); + }); +});