diff --git a/Libraries/LibJS/Runtime/ArrayPrototype.cpp b/Libraries/LibJS/Runtime/ArrayPrototype.cpp index 06d2289bba..6b8e29684b 100644 --- a/Libraries/LibJS/Runtime/ArrayPrototype.cpp +++ b/Libraries/LibJS/Runtime/ArrayPrototype.cpp @@ -59,6 +59,7 @@ ArrayPrototype::ArrayPrototype() put_native_function("slice", slice, 2, attr); put_native_function("indexOf", index_of, 1, attr); put_native_function("reduce", reduce, 1, attr); + put_native_function("reduceRight", reduce_right, 1, attr); put_native_function("reverse", reverse, 0, attr); put_native_function("lastIndexOf", last_index_of, 1, attr); put_native_function("includes", includes, 1, attr); @@ -398,6 +399,10 @@ Value ArrayPrototype::reduce(Interpreter& interpreter) if (interpreter.exception()) return {}; + auto* callback_function = callback_from_args(interpreter, "reduce"); + if (!callback_function) + return {}; + size_t start = 0; auto accumulator = js_undefined(); @@ -420,10 +425,6 @@ Value ArrayPrototype::reduce(Interpreter& interpreter) } } - auto* callback_function = callback_from_args(interpreter, "reduce"); - if (!callback_function) - return {}; - auto this_value = js_undefined(); for (size_t i = start; i < initial_length; ++i) { @@ -447,6 +448,65 @@ Value ArrayPrototype::reduce(Interpreter& interpreter) return accumulator; } +Value ArrayPrototype::reduce_right(Interpreter& interpreter) +{ + auto* this_object = interpreter.this_value().to_object(interpreter); + if (!this_object) + return {}; + + auto initial_length = get_length(interpreter, *this_object); + if (interpreter.exception()) + return {}; + + auto* callback_function = callback_from_args(interpreter, "reduceRight"); + if (!callback_function) + return {}; + + int start = initial_length - 1; + + auto accumulator = js_undefined(); + if (interpreter.argument_count() > 1) { + accumulator = interpreter.argument(1); + } else { + bool start_found = false; + while (!start_found && start >= 0) { + auto value = this_object->get_by_index(start); + if (interpreter.exception()) + return {}; + start_found = !value.is_empty(); + if (start_found) + accumulator = value; + start -= 1; + } + if (!start_found) { + interpreter.throw_exception("Reduce of empty array with no initial value"); + return {}; + } + } + + auto this_value = js_undefined(); + + for (int i = start; i >= 0; --i) { + auto value = this_object->get_by_index(i); + if (interpreter.exception()) + return {}; + if (value.is_empty()) + continue; + + MarkedValueList arguments(interpreter.heap()); + arguments.append(accumulator); + arguments.append(value); + arguments.append(Value(i)); + arguments.append(this_object); + + accumulator = interpreter.call(*callback_function, this_value, move(arguments)); + if (interpreter.exception()) + return {}; + } + + return accumulator; +} + Value ArrayPrototype::reverse(Interpreter& interpreter) { auto* array = array_from(interpreter); diff --git a/Libraries/LibJS/Runtime/ArrayPrototype.h b/Libraries/LibJS/Runtime/ArrayPrototype.h index 8baecb8105..d22b412534 100644 --- a/Libraries/LibJS/Runtime/ArrayPrototype.h +++ b/Libraries/LibJS/Runtime/ArrayPrototype.h @@ -52,6 +52,7 @@ private: static Value slice(Interpreter&); static Value index_of(Interpreter&); static Value reduce(Interpreter&); + static Value reduce_right(Interpreter&); static Value reverse(Interpreter&); static Value last_index_of(Interpreter&); static Value includes(Interpreter&); diff --git a/Libraries/LibJS/Tests/Array.prototype-generic-functions.js b/Libraries/LibJS/Tests/Array.prototype-generic-functions.js index 0eb664bf9f..702d575a4b 100644 --- a/Libraries/LibJS/Tests/Array.prototype-generic-functions.js +++ b/Libraries/LibJS/Tests/Array.prototype-generic-functions.js @@ -83,17 +83,31 @@ try { assert(visited[2] === "baz"); }); - ["reduce"].forEach(name => { + { const visited = []; - Array.prototype[name].call(o, function (_, value) { + Array.prototype.reduce.call(o, function (_, value) { visited.push(value); return false; }, "initial"); + assert(visited.length === 3); assert(visited[0] === "foo"); assert(visited[1] === "bar"); assert(visited[2] === "baz"); - }); + } + + { + const visited = []; + Array.prototype.reduceRight.call(o, function (_, value) { + visited.push(value); + return false; + }, "initial"); + + assert(visited.length === 3); + assert(visited[2] === "foo"); + assert(visited[1] === "bar"); + assert(visited[0] === "baz"); + } console.log("PASS"); } catch (e) { diff --git a/Libraries/LibJS/Tests/Array.prototype.reduceRight.js b/Libraries/LibJS/Tests/Array.prototype.reduceRight.js new file mode 100644 index 0000000000..98c2880990 --- /dev/null +++ b/Libraries/LibJS/Tests/Array.prototype.reduceRight.js @@ -0,0 +1,134 @@ +load("test-common.js"); + +try { + assert(Array.prototype.reduceRight.length === 1); + + assertThrowsError( + () => { + [1].reduceRight(); + }, + { + error: TypeError, + message: "Array.prototype.reduceRight() requires at least one argument", + } + ); + + assertThrowsError( + () => { + [1].reduceRight(undefined); + }, + { + error: TypeError, + message: "undefined is not a function", + } + ); + + assertThrowsError( + () => { + [].reduceRight((a, x) => x); + }, + { + error: TypeError, + message: "Reduce of empty array with no initial value", + } + ); + + assertThrowsError( + () => { + [, ,].reduceRight((a, x) => x); + }, + { + error: TypeError, + message: "Reduce of empty array with no initial value", + } + ); + + [1, 2].reduceRight(() => { + assert(this === undefined); + }); + + var callbackCalled = 0; + var callback = () => { + callbackCalled++; + return true; + }; + + assert([1].reduceRight(callback) === 1); + assert(callbackCalled === 0); + + assert([1].reduceRight(callback) === 1); + assert(callbackCalled === 0); + + callbackCalled = 0; + assert([1, 2, 3].reduceRight(callback) === true); + assert(callbackCalled === 2); + + callbackCalled = 0; + assert([1, 2, 3, ,].reduceRight(callback) === true); + assert(callbackCalled === 2); + + callbackCalled = 0; + assert([, , , 1, , , 10, , 100, , ,].reduceRight(callback) === true); + assert(callbackCalled === 2); + + var constantlySad = () => ":^("; + var result = [].reduceRight(constantlySad, ":^)"); + assert(result === ":^)"); + + result = [":^0"].reduceRight(constantlySad, ":^)"); + assert(result === ":^("); + + result = [":^0"].reduceRight(constantlySad); + assert(result === ":^0"); + + result = [5, 4, 3, 2, 1].reduceRight((accum, elem) => "" + accum + elem); + assert(result === "12345"); + + result = [1, 2, 3, 4, 5, 6].reduceRight((accum, elem) => { + return "" + accum + elem; + }, 100); + assert(result === "100654321"); + + result = [6, 5, 4, 3, 2, 1].reduceRight((accum, elem) => { + return "" + accum + elem; + }, 100); + assert(result === "100123456"); + + var indexes = []; + result = ["foo", 1, true].reduceRight((a, v, i) => { + indexes.push(i); + }); + assert(result === undefined); + assert(indexes.length === 2); + assert(indexes[0] === 1); + assert(indexes[1] === 0); + + indexes = []; + result = ["foo", 1, true].reduceRight((a, v, i) => { + indexes.push(i); + }, "foo"); + assert(result === undefined); + assert(indexes.length === 3); + assert(indexes[0] === 2); + assert(indexes[1] === 1); + assert(indexes[2] === 0); + + var mutable = { prop: 0 }; + result = ["foo", 1, true].reduceRight((a, v) => { + a.prop = v; + return a; + }, mutable); + assert(result === mutable); + assert(result.prop === "foo"); + + var a1 = [1, 2]; + var a2 = null; + a1.reduceRight((a, v, i, t) => { + a2 = t; + }); + assert(a1 === a2); + + console.log("PASS"); +} catch (e) { + console.log("FAIL: " + e); +}