diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 7dd97a3579..626652d13e 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -157,6 +157,7 @@ namespace JS { P(disposed) \ P(done) \ P(dotAll) \ + P(drop) \ P(encodeURI) \ P(encodeURIComponent) \ P(endsWith) \ diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp index d9cbfd451f..405b7eebbe 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.cpp @@ -33,6 +33,7 @@ ThrowCompletionOr IteratorPrototype::initialize(Realm& realm) define_native_function(realm, vm.names.map, map, 1, attr); define_native_function(realm, vm.names.filter, filter, 1, attr); define_native_function(realm, vm.names.take, take, 1, attr); + define_native_function(realm, vm.names.drop, drop, 1, attr); return {}; } @@ -238,4 +239,75 @@ JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::take) return result; } +// 3.1.3.5 Iterator.prototype.drop ( limit ), https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.drop +JS_DEFINE_NATIVE_FUNCTION(IteratorPrototype::drop) +{ + auto& realm = *vm.current_realm(); + + auto limit = vm.argument(0); + + // 1. Let O be the this value. + // 2. If O is not an Object, throw a TypeError exception. + auto object = TRY(this_object(vm)); + + // 3. Let numLimit be ? ToNumber(limit). + auto numeric_limit = TRY(limit.to_number(vm)); + + // 4. If numLimit is NaN, throw a RangeError exception. + if (numeric_limit.is_nan()) + return vm.throw_completion(ErrorType::NumberIsNaN, "limit"sv); + + // 5. Let integerLimit be ! ToIntegerOrInfinity(numLimit). + auto integer_limit = MUST(numeric_limit.to_integer_or_infinity(vm)); + + // 6. If integerLimit < 0, throw a RangeError exception. + if (integer_limit < 0) + return vm.throw_completion(ErrorType::NumberIsNegative, "limit"sv); + + // 7. Let iterated be ? GetIteratorDirect(O). + auto iterated = TRY(get_iterator_direct(vm, object)); + + // 8. Let closure be a new Abstract Closure with no parameters that captures iterated and integerLimit and performs the following steps when called: + IteratorHelper::Closure closure = [integer_limit](auto& iterator) -> ThrowCompletionOr { + auto& vm = iterator.vm(); + + auto const& iterated = iterator.underlying_iterator(); + + // a. Let remaining be integerLimit. + // b. Repeat, while remaining > 0, + while (iterator.counter() < integer_limit) { + // i. If remaining is not +∞, then + // 1. Set remaining to remaining - 1. + iterator.increment_counter(); + + // ii. Let next be ? IteratorStep(iterated). + auto next = TRY(iterator_step(vm, iterated)); + + // iii. If next is false, return undefined. + if (!next) + return iterator.result(js_undefined()); + } + + // c. Repeat, + + // i. Let next be ? IteratorStep(iterated). + auto next = TRY(iterator_step(vm, iterated)); + + // ii. If next is false, return undefined. + if (!next) + return iterator.result(js_undefined()); + + // iii. Let completion be Completion(Yield(? IteratorValue(next))). + // iv. IfAbruptCloseIterator(completion, iterated). + return iterator.result(TRY(iterator_value(vm, *next))); + }; + + // 9. Let result be CreateIteratorFromClosure(closure, "Iterator Helper", %IteratorHelperPrototype%, « [[UnderlyingIterator]] »). + // 10. Set result.[[UnderlyingIterator]] to iterated. + auto result = TRY(IteratorHelper::create(realm, move(iterated), move(closure))); + + // 11. Return result. + return result; +} + } diff --git a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h index cee670d89c..b1aeefa40b 100644 --- a/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h +++ b/Userland/Libraries/LibJS/Runtime/IteratorPrototype.h @@ -25,6 +25,7 @@ private: JS_DECLARE_NATIVE_FUNCTION(map); JS_DECLARE_NATIVE_FUNCTION(filter); JS_DECLARE_NATIVE_FUNCTION(take); + JS_DECLARE_NATIVE_FUNCTION(drop); }; } diff --git a/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.drop.js b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.drop.js new file mode 100644 index 0000000000..db2aecb3ea --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Iterator/Iterator.prototype.drop.js @@ -0,0 +1,118 @@ +describe("errors", () => { + test("called with non-numeric object", () => { + expect(() => { + Iterator.prototype.drop(Symbol.hasInstance); + }).toThrowWithMessage(TypeError, "Cannot convert symbol to number"); + }); + + test("called with invalid numbers", () => { + expect(() => { + Iterator.prototype.drop(NaN); + }).toThrowWithMessage(RangeError, "limit must not be NaN"); + + expect(() => { + Iterator.prototype.drop(-1); + }).toThrowWithMessage(RangeError, "limit must not be negative"); + }); + + test("iterator's next method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + throw new TestError(); + } + } + + expect(() => { + const iterator = new TestIterator().drop(1); + iterator.next(); + }).toThrow(TestError); + }); + + test("value returned by iterator's next method throws", () => { + function TestError() {} + + class TestIterator extends Iterator { + next() { + return { + done: false, + get value() { + throw new TestError(); + }, + }; + } + } + + expect(() => { + const iterator = new TestIterator().drop(1); + iterator.next(); + }).toThrow(TestError); + }); +}); + +describe("normal behavior", () => { + test("length is 1", () => { + expect(Iterator.prototype.drop).toHaveLength(1); + }); + + test("drop zero values", () => { + function* generator() { + yield "a"; + yield "b"; + } + + const iterator = generator().drop(0); + + let value = iterator.next(); + expect(value.value).toBe("a"); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBe("b"); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + }); + + test("drop fewer than the number of values", () => { + function* generator() { + yield "a"; + yield "b"; + yield "c"; + } + + const iterator = generator().drop(1); + + let value = iterator.next(); + expect(value.value).toBe("b"); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBe("c"); + expect(value.done).toBeFalse(); + + value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + }); + + test("drop more than the number of values", () => { + function* generator() { + yield "a"; + yield "b"; + } + + const iterator = generator().drop(Infinity); + + let value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + + value = iterator.next(); + expect(value.value).toBeUndefined(); + expect(value.done).toBeTrue(); + }); +});