diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 476e583..19969f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - name: install dependencies run: npm ci - name: build spec diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d7cd942..d91c183 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 - run: > npm ci && npm run build && diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef47e21..28cd987 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: 24 - name: install dependencies run: npm ci - name: build diff --git a/README.md b/README.md index 6f24152..fcf7750 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,14 @@ stronger than choosing SameValueZero to match `Array.prototype.includes`. `Array.prototype.includes` has a second parameter that starts the search from the given index instead of the beginning of the Array. This makes sense for the Array API because the alternative (slicing first) would first allocate another -Array and then perform a copy from that index. But Iterators have `drop` which -is a constant time/space operation, so there's no need to include this -parameter. That being said, it *could* be included to mirror the Array API, but -I don't think it's worth it. Additionally, if it was included, it would have to -reject negative values, which would be an unnecessarily surprising difference. +Array and then perform a copy from that index. Iterators have `drop`, so it's +unnecessary to include this parameter, but to avoid confusion for somebody who +is already familiar with the Array method, we choose to include it. +Unfortunately, there is still a difference in the interfaces because the +Iterator method cannot accept negative offsets, which would be a surprising +difference, but one that makes sense in context. Alternatively, we could throw +if a second argument is ever provided, but that would be a highly unusual +behaviour among JavaScript built-ins. ## chosen solution diff --git a/spec.emu b/spec.emu index 45be258..77efffe 100644 --- a/spec.emu +++ b/spec.emu @@ -10,14 +10,24 @@ copyright: false -

Iterator.prototype.includes ( _searchElement_ )

+

Iterator.prototype.includes ( _searchElement_ [ , _skippedElements_ ] )

1. Let _O_ be the *this* value. 1. If _O_ is not an Object, throw a *TypeError* exception. + 1. If _skippedElements_ is *undefined*, then + 1. Let _toSkip_ be 0. + 1. Else, + 1. If _skippedElements_ is not one of *+∞𝔽*, *-∞𝔽*, or an integral Number, throw a *TypeError* exception. + 1. Let _toSkip_ be the extended mathematical value of _skippedElements_. + 1. If _toSkip_ < 0, throw a *RangeError* exception. + 1. Let _skipped_ be 0. 1. Let _iterated_ be ? GetIteratorDirect(_O_). 1. Repeat, 1. Let _value_ be ? IteratorStepValue(_iterated_). 1. If _value_ is ~done~, return *false*. - 1. If SameValueZero(_value_, _searchElement_) is *true*, return ? IteratorClose(_iterated_, NormalCompletion(*true*)). + 1. If _skipped_ < _toSkip_, then + 1. Set _skipped_ to _skipped_ + 1. + 1. Else, + 1. If SameValueZero(_value_, _searchElement_) is *true*, return ? IteratorClose(_iterated_, NormalCompletion(*true*)).
diff --git a/src/index.ts b/src/index.ts index 66fb5df..4a7e4d2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,21 @@ -function includes(this: IterableIterator, searchElement: T): boolean { +function includes(this: IterableIterator, searchElement: T, skippedElements = undefined): boolean { + let toSkip = 0; + if (skippedElements !== undefined) { + if (!(skippedElements === 2e308 || skippedElements === -2e308 || typeof skippedElements === 'number' && Math.trunc(skippedElements) === skippedElements)) { + throw new TypeError; + } + toSkip = skippedElements as number; + } + if (toSkip < 0) { + throw new RangeError; + } + let skipped = 0; for (let e of this) { - if ([e].includes(searchElement)) return true; + if (skipped < toSkip) { + ++skipped; + } else if ([e].includes(searchElement)) { + return true; + } } return false; } diff --git a/test/index.mjs b/test/index.mjs index 8d2a6a2..733f654 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -75,6 +75,104 @@ test('closes iterator', async t => { assert.equal(closed, true); }); +test('skipped elements', async t => { + await test('negative integral', async t => { + assert.throws(() => { + [].values().includes(0, -1); + }, RangeError); + }); + + await test('negative non-integral', async t => { + assert.throws(() => { + [].values().includes(0, -0.1); + }, TypeError); + }); + + await test('negative infinity', async t => { + assert.throws(() => { + [].values().includes(0, -2e308); + }, RangeError); + }); + + await test('zero', async t => { + assert.equal([4, 5, 6, 7].values().includes(8, 0), false); + assert.equal([4, 5, 6, 7].values().includes(7, 0), true); + assert.equal([4, 5, 6, 7].values().includes(6, 0), true); + assert.equal([4, 5, 6, 7].values().includes(5, 0), true); + assert.equal([4, 5, 6, 7].values().includes(4, 0), true); + assert.equal([4, 5, 6, 7].values().includes(3, 0), false); + + assert.equal([4, 5, 6, 7].values().includes(8, -0), false); + assert.equal([4, 5, 6, 7].values().includes(7, -0), true); + assert.equal([4, 5, 6, 7].values().includes(6, -0), true); + assert.equal([4, 5, 6, 7].values().includes(5, -0), true); + assert.equal([4, 5, 6, 7].values().includes(4, -0), true); + assert.equal([4, 5, 6, 7].values().includes(3, -0), false); + }); + + await test('positive integral', async t => { + assert.equal([4, 5, 6, 7].values().includes(4, 1), false); + assert.equal([4, 5, 6, 7].values().includes(4, 2), false); + assert.equal([4, 5, 6, 7].values().includes(4, 3), false); + assert.equal([4, 5, 6, 7].values().includes(4, 4), false); + assert.equal([4, 5, 6, 7].values().includes(4, 5), false); + + assert.equal([4, 5, 6, 7].values().includes(5, 1), true); + assert.equal([4, 5, 6, 7].values().includes(5, 2), false); + assert.equal([4, 5, 6, 7].values().includes(5, 3), false); + assert.equal([4, 5, 6, 7].values().includes(5, 4), false); + assert.equal([4, 5, 6, 7].values().includes(5, 5), false); + + assert.equal([4, 5, 6, 7].values().includes(6, 1), true); + assert.equal([4, 5, 6, 7].values().includes(6, 2), true); + assert.equal([4, 5, 6, 7].values().includes(6, 3), false); + assert.equal([4, 5, 6, 7].values().includes(6, 4), false); + assert.equal([4, 5, 6, 7].values().includes(6, 5), false); + + assert.equal([4, 5, 6, 7].values().includes(7, 1), true); + assert.equal([4, 5, 6, 7].values().includes(7, 2), true); + assert.equal([4, 5, 6, 7].values().includes(7, 3), true); + assert.equal([4, 5, 6, 7].values().includes(7, 4), false); + assert.equal([4, 5, 6, 7].values().includes(7, 5), false); + }); + + await test('positive non-integral', async t => { + assert.throws(() => { + [].values().includes(0, 0.1); + }, TypeError); + }); + + await test('positive infinity', async t => { + let closed = false; + let i = 0; + let iter = { + __proto__: Iterator.prototype, + next() { + ++i; + if (i < 1000) { + return { value: i, done: false }; + } else { + closed = true; + return { value: undefined, done: true }; + } + }, + return() { + closed = true; + return { value: undefined, done: true }; + }, + }; + + assert.equal(iter.includes(1, Infinity), false); + assert.equal(closed, true); + }); + + await test('non-numeric', async t => { + assert.throws(() => { + [].values().includes(0, { valueOf() { return 0; } }); + }, TypeError); + }); +}); + test('name', async t => { assert.equal(Iterator.prototype.includes.name, 'includes'); });