Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 12 additions & 2 deletions spec.emu
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,24 @@ copyright: false
</pre>

<emu-clause id="sec-iterator.prototype.includes">
<h1>Iterator.prototype.includes ( _searchElement_ )</h1>
<h1>Iterator.prototype.includes ( _searchElement_ [ , _skippedElements_ ] )</h1>
<emu-alg>
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 *+∞<sub>𝔽</sub>*, *-∞<sub>𝔽</sub>*, or an integral Number, throw a *TypeError* exception.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No reason to accept -∞ here since we're going to throw below anyway.

Also, I weakly feel that we should coerce to match [].includes, though since we're already handling negative inputs differently it's less important than for e.g. .join.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We accept -∞ so that we can throw a RangeError for it instead of TypeError.

I don't want to coerce in new APIs unless we feel it's really important to do so, which I don't think is the case here.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yeah, the error is a fine reason.

I do not feel it's really important to coerce here although I do think you should call that out in committee explicitly.

1. Let _toSkip_ be the extended mathematical value of _skippedElements_.
1. If _toSkip_ &lt; 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_ &lt; _toSkip_, then
1. Set _skipped_ to _skipped_ + 1.
1. Else,
1. If SameValueZero(_value_, _searchElement_) is *true*, return ? IteratorClose(_iterated_, NormalCompletion(*true*)).
</emu-alg>
</emu-clause>
19 changes: 17 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
function includes<T>(this: IterableIterator<T>, searchElement: T): boolean {
function includes<T>(this: IterableIterator<T>, 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;
}
Expand Down
98 changes: 98 additions & 0 deletions test/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
Expand Down