Last active
January 17, 2024 06:56
-
-
Save nberlette/7a86cc1744bbdf6a36c5d6ee4b6ea68a to your computer and use it in GitHub Desktop.
Polyfill for the Iterator Helpers TC39 Proposal
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Iterator } from "./iterator.ts"; | |
/** Simple numeric iterator that returns the numbers passed to the constructor. Extremely useless. */ | |
export class NumericIterator extends Iterator<number> { | |
#index = 0; | |
#numbers: number[] = []; | |
constructor(...numbers: number[]) { | |
super(); | |
this.#numbers = numbers; | |
this.#index = 0; | |
} | |
override next(): IteratorResult<number> { | |
if (this.#index < this.#numbers.length) { | |
return { value: this.#numbers[this.#index++], done: false }; | |
} else { | |
return { value: undefined, done: true }; | |
} | |
} | |
} | |
/** Like the {@linkcode NumericIterator}, but for strings. */ | |
export class StringIterator extends Iterator<string> { | |
#index = 0; | |
#strings: string[] = []; | |
constructor(...strings: string[]) { | |
super(); | |
this.#strings = strings; | |
this.#index = 0; | |
} | |
override next(): IteratorResult<string> { | |
if (this.#index < this.#strings.length) { | |
return { value: this.#strings[this.#index++], done: false }; | |
} else { | |
return { value: undefined, done: true }; | |
} | |
} | |
} | |
/** A more complex iterator example: a Fibonacci sequence. */ | |
export class FibonacciIterator extends Iterator<number> { | |
#done = false; | |
#last = 0; | |
#limit = 1e4; | |
#value = 1; | |
constructor(limit?: number) { | |
super(); | |
this.#limit = limit ?? this.#limit; | |
} | |
override next(): IteratorResult<number> { | |
if ((this.#done ||= this.#value >= this.#limit)) { | |
return { value: undefined, done: true }; | |
} | |
const value = this.#value; | |
this.#value += this.#last; | |
this.#last = value; | |
return { value, done: false }; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// deno-lint-ignore-file ban-types | |
type GlobalIterator<T> = globalThis.Iterator<T>; | |
type IteratorSource<T = unknown> = | |
| string | |
| String | |
| Generator<T> | |
| GeneratorFunction | |
| Iterable<T> | |
| IterableIterator<T> | |
| GlobalIterator<T> | |
| (() => GlobalIterator<T>); | |
/** | |
* This is a spec-compliant polyfill for the new `Iterator` API introduced in | |
* the TC39 'Iterator Helpers' Proposal that's currently being implemented in | |
* major JavaScript runtimes. | |
* | |
* This polyfill is intended to be used in the interim, or in the future (for | |
* whatever else your heart desires), and is designed to be fully compliant | |
* with the specification, both in its behavior and type signature. | |
* | |
* @see https://github.com/tc39/proposal-iterator-helpers | |
* | |
* ## Usage | |
* | |
* ```ts | |
* import { Iterator } from "https://deno.land/x/iterator/mod.ts"; | |
* | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const dropped = it.drop(2); | |
* console.log([...dropped]); // [3, 4, 5] | |
* console.log([...it]); // [] (the original iterator is consumed) | |
* ``` | |
* | |
* ```ts | |
* import { Iterator } from "https://deno.land/x/iterator/mod.ts"; | |
* | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const allEven = it.every((n) => n % 2 === 0); | |
* console.log(allEven); // false | |
* console.log([...it]); // [2, 3, 4, 5] | |
* ``` | |
* | |
* ## Why? | |
* | |
* While the new API has shipped in V8 ~v12 (and thus Chrome 120, Node.js ~v21, | |
* and Deno ~v1.38.0), it is not yet available to many users of other or older | |
* runtimes. It has also not yet been added to TypeScript's lib.d.ts (at the | |
* time of writing, 1/16/2024), and so the API remains untyped and therefore | |
* largely undiscoverable by new users. | |
* | |
* @see https://github.com/tc39/proposal-iterator-helpers | |
* | |
* If you've found something that doesn't behave as it should, please reach out | |
* to me on GitHub so it can be fixed. If you're using this in a production | |
* environment, please consider adding a star to the repository :) | |
* | |
* @author Nicholas Berlette <https://github.com/nberlette> | |
* @license MIT | |
* @see https://github.com/nberlette/iterator | |
*/ | |
export class Iterator<T> implements Iterable<T>, GlobalIterator<T> { | |
/** | |
* Creates a new `Iterator` instance from a source object. The source object | |
* may be an iterable, iterator, generator, generator function, or a value. | |
* | |
* @template T The type of the iterator's elements. | |
* @param {IteratorSource<T>} source Object to create the iterator from. | |
* @returns {Iterator<T>} A new `Iterator` instance. | |
*/ | |
static from<T>(source: IteratorSource<T>): Iterator<T> { | |
if (typeof source === "string" || source instanceof String) { | |
source = source[Symbol.iterator](); | |
} else if (typeof source === "function") { | |
source = source(); | |
} else if (typeof source === "object" && source != null) { | |
if (Symbol.iterator in source) { | |
source = source[Symbol.iterator](); | |
} else if (Symbol.asyncIterator in source) { | |
source = source[Symbol.asyncIterator](); | |
} | |
} | |
if (ource != null) { | |
const o = Object(source); | |
if (Symbol.iterator in o && typeof o[Symbol.iterator] === "function") { | |
return Reflect.construct(Iterator, [ctorKey, o], Iterator); | |
} else if (source == null || typeof source !== "object") { | |
throw new TypeError("Iterable.from called on non-object"); | |
} | |
} else { | |
throw new TypeError("Iterator.from called on non-iterator"); | |
} | |
} | |
/** | |
* Returns the next element in the iterator as an {@link IteratorResult}, | |
* which is a plain object with a `done` property and a `value` property. | |
* If the iterator has completed, `done` will be true, and `value` will be | |
* `undefined`. Otherwise, `done` will be `false` (or omitted entirely), | |
* and `value` will contain the next element in the iterator. | |
* | |
* In order to create a subclass of `Iterator`, the `next` method must be | |
* implemented. It's recommended to use the `Iterator.from` static method to | |
* create new instances of `Iterator`, as it takes care of configuring the | |
* prototype chain and constructor for you as long as the object it's given | |
* is a valid iterable or iterator. | |
* | |
* @param [args] arguments passed to the underlying iterator's `next` method | |
* @returns {IteratorResult<T>} An {@link IteratorResult} object. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2]); | |
* console.log(it.next()); // { value: 1, done: false } | |
* console.log(it.next()); // { value: 2, done: false } | |
* console.log(it.next()); // { value: undefined, done: true } | |
* ``` | |
*/ | |
next<TNext = undefined>(...args: [] | [TNext]): IteratorResult<T> { | |
const it = getIterator(this); | |
return it?.next?.(...args as []); | |
} | |
/** | |
* Calls the specified {@link callbackfn|callback function} for all the | |
* elements in an iterator. The return value of the callback function is the | |
* accumulated result, and is provided as the first argument the next time it | |
* is called. If an {@link initialValue} is provided, it is used as the | |
* initial value to start the accumulation. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @param callbackfn A function that accepts up to four arguments. The reduce | |
* method calls this function once for every element in the iterator, | |
* consuming each element as it progresses. | |
* @param initialValue If specified, it will be the `previousValue` for the | |
* first invocation of {@link callbackfn}, which will receive this value as | |
* its first argument instead of a value from the iterator. | |
* @returns The final accumulated value. | |
*/ | |
reduce( | |
callbackfn: ( | |
previousValue: T, | |
currentValue: T, | |
currentIndex: number, | |
iterator: Iterator<T>, | |
) => T, | |
): T; | |
reduce( | |
callbackfn: ( | |
previousValue: T, | |
currentValue: T, | |
currentIndex: number, | |
iterator: Iterator<T>, | |
) => T, | |
initialValue: T, | |
): T; | |
/** | |
* Calls the specified {@link callbackfn|callback function} for all the | |
* elements in an iterator. The return value of the callback function is the | |
* accumulated result, and is provided as the first argument the next time it | |
* is called. If an {@link initialValue} is provided, it is used as the | |
* initial value to start the accumulation. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @param callbackfn A function that accepts up to four arguments. The reduce | |
* method calls this function once for every element in the iterator, | |
* consuming each element as it progresses. | |
* @param initialValue If specified, it will be the `previousValue` for the | |
* first invocation of {@link callbackfn}, which will receive this value as | |
* its first argument instead of a value from the iterator. | |
* @returns The final accumulated value. | |
*/ | |
reduce<U>( | |
callbackfn: ( | |
previousValue: U, | |
currentValue: T, | |
currentIndex: number, | |
iterator: Iterator<T>, | |
) => U, | |
initialValue: U, | |
): U; | |
/** | |
* Calls the specified {@link callbackfn|callback function} for all the | |
* elements in an iterator. The return value of the callback function is the | |
* accumulated result, and is provided as the first argument the next time it | |
* is called. If an {@link initialValue} is provided, it is used as the | |
* initial value to start the accumulation. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @param callbackfn A function that accepts up to four arguments. The reduce | |
* method calls this function once for every element in the iterator, | |
* consuming each element as it progresses. | |
* @param [initialValue] If specified, it will be the `previousValue` for the | |
* first invocation of {@link callbackfn}, which will receive this value as | |
* its first argument instead of a value from the iterator. | |
* @returns The final accumulated value. | |
*/ | |
reduce( | |
callbackfn: ( | |
previousValue: T, | |
currentValue: T, | |
index: number, | |
iterator: Iterator<T>, | |
) => T, | |
initialValue?: T, | |
): T { | |
if (typeof callbackfn !== "function") { | |
throw new TypeError("callbackfn is not a function"); | |
} | |
// deno-lint-ignore no-this-alias | |
const self = this; | |
const source = getSource(this); | |
const it = getIterator(this); | |
let i = 0; | |
let accumulator: T; | |
if (arguments.length > 1) { | |
accumulator = initialValue!; | |
} else { | |
const next = it.next(); | |
if (next.done) { | |
throw new TypeError("Reduce of empty iterator with no initial value"); | |
} | |
accumulator = next.value; | |
i++; | |
} | |
for (const value of source) { | |
accumulator = callbackfn(accumulator, value, i++, self); | |
} | |
return accumulator; | |
} | |
/** | |
* Returns an array containing the elements of this iterator. This method | |
* consumes the original iterator and its elements, and is equivalent to | |
* using `Array.from`, or spread operator `[...Iterator.from(...)]`. | |
*/ | |
toArray(): T[] { | |
return [...this]; | |
} | |
/** | |
* Calls the specified {@link callbackfn|callback function} once for each of | |
* the elements in the iterator. The callback function is invoked with three | |
* arguments: the value of the element, the index of the element, and the | |
* {@link Iterator} being traversed. If {@linkcode thisArg} is provided, its | |
* value will be available as the `this` binding of the callback function. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @param callbackfn A function that accepts up to three arguments. The | |
* forEach method calls the callbackfn function one time for each element in | |
* the iterator. | |
* @param thisArg An object to which the this keyword can refer in the | |
* callbackfn function. | |
* @throws {TypeError} If {@linkcode callbackfn} is not a function. | |
*/ | |
forEach( | |
callbackfn: (value: T, index: number, iterator: Iterator<T>) => void, | |
thisArg?: unknown, | |
): void { | |
if (typeof callbackfn !== "function") { | |
throw new TypeError("callbackfn is not a function"); | |
} | |
const source = getSource(this); | |
let i = 0; | |
for (const value of source) { | |
callbackfn.call(thisArg, value, i++, this); | |
} | |
} | |
/** | |
* Returns `true` if at least one element in the iterator satisfies the | |
* provided testing function. Otherwise `false` is returned. If the iterator | |
* is empty, `false` is returned. An optional value may be provided for | |
* {@link thisArg} to be used as the {@linkcode predicate} function's `this` | |
* binding. | |
* | |
* This method consumes the elements of the iterator as they are tested. As | |
* soon as predicate returns `true`, iteration stops and `true` is returned. | |
* The original iterator will contain the remaining elements, if any. | |
* | |
* @template {T} S The type of the elements in the iterator. | |
* @param {(value: T, index: number, iterator: Iterator<T>) => unknown} predicate | |
* The predicate function to test each element against. | |
* @param {unknown} [thisArg] The value to use as `this` when executing the predicate. | |
* @returns {boolean} `true` if every element satisfies the predicate. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const someEven = it.some((n) => n % 2 === 0); | |
* console.log(someEven); // true | |
* console.log([...it]); // [3, 4, 5] (the first two elements were consumed) | |
* ``` | |
*/ | |
some( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => unknown, | |
thisArg?: unknown, | |
): boolean { | |
if (typeof predicate !== "function") { | |
throw new TypeError("predicate is not a function"); | |
} | |
const source = getSource(this); | |
let i = 0; | |
for (const value of source) { | |
if (predicate.call(thisArg, value, i++, this)) return true; | |
} | |
return false; | |
} | |
/** | |
* Returns `true` if every element in the iterator satisfies the provided | |
* testing function. Otherwise `false` is returned. If the iterator is empty, | |
* `true` is returned. An optional value may be provided for {@link thisArg}, | |
* and will be used as the `this` value for each invocation of the predicate. | |
* | |
* This method consumes the elements of the iterator as they are tested. As | |
* soon as a predicate returns `false`, iteration stops, and `false` is then | |
* returned. The original iterator will contain the remaining elements, if | |
* any are left. | |
* | |
* @template {T} S The type of the elements in the iterator. | |
* @param {(value: T, index: number, iterator: Iterator<T>) => value is S} predicate | |
* The predicate function to test each element with. | |
* @param {unknown} [thisArg] The value to use as `this` when executing the predicate. | |
* @returns {this is Iterator<S>} `true` if every element satisfies the predicate. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const allEven = it.every((n) => n % 2 === 0); | |
* console.log(allEven); // false | |
* console.log([...it]); // [2, 3, 4, 5] (only the first element was consumed) | |
* ``` | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const lessThan3 = it.every((n) => n < 3); | |
* console.log(lessThan3); // false | |
* console.log([...it]); // [3, 4, 5] (the first two elements were consumed) | |
* ``` | |
*/ | |
every<S extends T>( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => value is S, | |
thisArg?: unknown, | |
): this is Iterator<S>; | |
/** | |
* Returns `true` if every element in the iterator satisfies the provided | |
* testing function. Otherwise `false` is returned. If the iterator is empty, | |
* `true` is returned. An optional value may be provided for {@link thisArg}, | |
* and will be used as the `this` value for each invocation of the predicate. | |
* | |
* This method consumes the elements of the iterator as they are tested. As | |
* soon as a predicate returns `false`, iteration stops, and `false` is then | |
* returned. The original iterator will contain the remaining elements, if | |
* any are left. | |
* | |
* @param {(value: T, index: number, iterator: Iterator<T>) => boolean} predicate | |
* The predicate function to test each element with. | |
* @param {unknown} [thisArg] The value to use as `this` when executing the predicate. | |
* @returns {boolean} `true` if every element satisfies the predicate. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const allEven = it.every((n) => n % 2 === 0); | |
* console.log(allEven); // false | |
* console.log([...it]); // [2, 3, 4, 5] (only the first element was consumed) | |
* ``` | |
*/ | |
every( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => unknown, | |
thisArg?: unknown, | |
): boolean; | |
/** | |
* Returns `true` if every element in the iterator satisfies the provided | |
* testing function. Otherwise `false` is returned. If the iterator is empty, | |
* `true` is returned. An optional value may be provided for {@link thisArg}, | |
* and will be used as the `this` value for each invocation of the predicate. | |
* | |
* This method consumes the elements of the iterator as they are tested. As | |
* soon as a predicate returns `false`, iteration stops, and `false` is then | |
* returned. The original iterator will contain the remaining elements, if | |
* any are left. | |
* | |
* @param {(value: T, index: number, iterator: Iterator<T>) => boolean} predicate | |
* The predicate function to test each element with. | |
* @param {unknown} [thisArg] The value to use as `this` when executing the predicate. | |
* @returns {boolean} `true` if every element satisfies the predicate. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const allEven = it.every((n) => n % 2 === 0); | |
* console.log(allEven); // false | |
* console.log([...it]); // [2, 3, 4, 5] (only the first element was consumed) | |
* ``` | |
*/ | |
every( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => unknown, | |
thisArg?: unknown, | |
): boolean { | |
if (typeof predicate !== "function") { | |
throw new TypeError("predicate is not a function"); | |
} | |
const source = getSource(this); | |
let i = 0; | |
for (const value of source) { | |
if (!predicate.call(thisArg, value, i++, this)) return false; | |
} | |
return true; | |
} | |
/** | |
* Returns the value of the first element in the iterator for which the given | |
* {@link predicate} function returns `true`. Otherwise, returns `undefined`. | |
* | |
* This method consumes the original iterator's elements as they are tested, | |
* and stops immediately when a matching element is found. The remaining | |
* elements, if any, will be left intact. | |
* | |
* @param predicate Called once for each element of the iterator, in order of | |
* iteration, until it finds one that returns `true`, immediately returning | |
* that element's value. If no elements are found, returns `undefined`. | |
* @param thisArg The contextual `this` binding of the {@link predicate} will | |
* be this value, if provided. Otherwise it will be `undefined`. | |
* @returns The first matching element, or `undefined` if none are found. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const firstEven = it.find((n) => n % 2 === 0); | |
* console.log(firstEven); // 2 | |
* console.log([...it]); // [3, 4, 5] (the first two elements were consumed) | |
* ``` | |
*/ | |
find<S extends T>( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => value is S, | |
thisArg?: unknown, | |
): S | undefined; | |
/** | |
* Returns the value of the first element in the iterator for which the given | |
* {@link predicate} function returns `true`. Otherwise, returns `undefined`. | |
* | |
* This method consumes the original iterator's elements as they are tested, | |
* and stops immediately when a matching element is found. The remaining | |
* elements, if any, will be left intact. | |
* | |
* @param predicate Called once for each element of the iterator, in order of | |
* iteration, until it finds one that returns `true`, immediately returning | |
* that element's value. If no elements are found, returns `undefined`. | |
* @param thisArg The contextual `this` binding of the {@link predicate} will | |
* be this value, if provided. Otherwise it will be `undefined`. | |
* @returns The first matching element, or `undefined` if none are found. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const firstEven = it.find((n) => n % 2 === 0); | |
* console.log(firstEven); // 2 | |
* console.log([...it]); // [3, 4, 5] (the first two elements were consumed) | |
* ``` | |
*/ | |
find( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => unknown, | |
thisArg?: unknown, | |
): T | undefined; | |
/** | |
* Returns the value of the first element in the iterator for which the given | |
* {@link predicate} function returns `true`. Otherwise, returns `undefined`. | |
* | |
* This method consumes the original iterator's elements as they are tested, | |
* and stops immediately when a matching element is found. The remaining | |
* elements, if any, will be left intact. | |
* | |
* @param predicate Called once for each element of the iterator, in order of | |
* iteration, until it finds one that returns `true`, immediately returning | |
* that element's value. If no elements are found, returns `undefined`. | |
* @param thisArg The contextual `this` binding of the {@link predicate} will | |
* be this value, if provided. Otherwise it will be `undefined`. | |
* @returns The first matching element, or `undefined` if none are found. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const firstEven = it.find((n) => n % 2 === 0); | |
* console.log(firstEven); // 2 | |
* console.log([...it]); // [3, 4, 5] (the first two elements were consumed) | |
* ``` | |
*/ | |
find( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => unknown, | |
thisArg?: unknown, | |
): T | undefined { | |
if (typeof predicate !== "function") { | |
throw new TypeError("predicate is not a function"); | |
} | |
const source = getSource(this); | |
let i = 0; | |
for (const value of source) { | |
if (predicate.call(thisArg, value, i++, this)) return value; | |
} | |
return undefined; | |
} | |
/** | |
* Calls a specified {@link callbackfn|callback function} once per element in | |
* this iterator, and returns a new {@link Iterator} with the results. If the | |
* | |
* The new iterator will use the same underlying source as the original one, | |
* and therefore will consume the original iterator when iterated over. See | |
* the example below for more details. | |
* | |
* @param callbackfn A function that accepts up to three arguments. The map | |
* method calls the callbackfn function one time for each element in the | |
* iterator, in the order of iteration, and yields the results. | |
* @param thisArg An object to which the this keyword can refer in the | |
* callbackfn function. | |
* @returns A new {@link Iterator} with the results of the mapping operation. | |
* @throws {TypeError} If {@linkcode callbackfn} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const mapped = it.map(String); | |
* console.log([...mapped]); // ["1", "2", "3", "4", "5"] | |
* console.log([...it]); // [] (the original iterator was consumed) | |
* ``` | |
* @example | |
* ```ts | |
* const original = Iterator.from([1, 2, 3, 4, 5]); | |
* const remapped = original.map(String); | |
* | |
* // the new iterator shares the same underlying source as the original | |
* console.log(remapped.next()); // { value: "1", done: false } | |
* console.log(original.next()); // { value: 2, done: false } | |
* | |
* // the original iterator is consumed when the new one is | |
* console.log(remapped.drop(1).toArray()); // ["4", "5"] | |
* console.log([...remapped]); // [] (consumed) | |
* console.log([...original]); // [] (consumed) | |
* ``` | |
*/ | |
map<S>( | |
callbackfn: (value: T, index: number, iterator: Iterator<T>) => S, | |
thisArg?: unknown, | |
): Iterator<S> { | |
if (typeof callbackfn !== "function") { | |
throw new TypeError("callbackfn is not a function"); | |
} | |
// deno-lint-ignore no-this-alias | |
const self = this; | |
const it = getIterator(this); | |
return Iterator.from({ | |
*[Symbol.iterator]() { | |
let i = 0; | |
while (true) { | |
const next = it.next(); | |
if (next.done) break; | |
yield callbackfn.call(thisArg, next.value, i++, self); | |
} | |
}, | |
}); | |
} | |
/** | |
* Returns a new {@link Iterator} that yields the elements of this iterator | |
* that match the given {@linkcode predicate}. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @template {T} S The type of the elements in the iterator. | |
* @param {(value: T, index: number, self: Iterator<T>) => value is S} predicate | |
* The predicate function to test each element against. | |
* @returns {Iterator<S>} A new `Iterator` that yields the matching elements. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const evens = it.filter((n) => n % 2 === 0); | |
* console.log([...evens]); // [2, 4] | |
* console.log([...it]); // [] (the original iterator is consumed) | |
* ``` | |
*/ | |
filter<S extends T>( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => value is S, | |
thisArg?: unknown, | |
): Iterator<S>; | |
/** | |
* Returns a new {@link Iterator} that yields the elements of this iterator | |
* that match the given {@linkcode predicate}. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @param {(value: T, index: number, self: Iterator<T>) => boolean} predicate | |
* The predicate function to test each element against. | |
* @returns {Iterator<T>} A new `Iterator` that yields the matching elements. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const evens = it.filter((n) => n % 2 === 0); | |
* console.log([...evens]); // [2, 4] | |
* console.log([...it]); // [] (the original iterator is consumed) | |
* ``` | |
*/ | |
filter( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => unknown, | |
thisArg?: unknown, | |
): Iterator<T>; | |
/** | |
* Returns a new {@link Iterator} that yields the elements of this iterator | |
* that match the given {@linkcode predicate}. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @param predicate The predicate function to test each element against. | |
* @returns {Iterator<T>} A new `Iterator` that yields the matching elements. | |
* @throws {TypeError} If {@linkcode predicate} is not a function. | |
*/ | |
filter( | |
predicate: (value: T, index: number, iterator: Iterator<T>) => unknown, | |
thisArg?: unknown, | |
): Iterator<T> { | |
if (typeof predicate !== "function") { | |
throw new TypeError("predicate is not a function"); | |
} | |
// deno-lint-ignore no-this-alias | |
const self = this; | |
const source = getSource(this); | |
return Iterator.from({ | |
*[Symbol.iterator]() { | |
let i = 0; | |
for (const value of source) { | |
if (predicate.call(thisArg, value, i++, self)) yield value; | |
} | |
}, | |
}); | |
} | |
/** | |
* Returns a new {@link Iterator} that yields the first {@linkcode count} | |
* elements of this iterator. If the iterator has fewer than {@linkcode count} | |
* elements, all of the elements are yielded. If {@linkcode count} is zero or | |
* negative, an empty iterator is returned. | |
* | |
* This method consumes the original iterator and its first {@linkcode count} | |
* elements. Any remaining elements will be left intact. | |
* | |
* @param {number} count The number of elements to take from the iterator. | |
* @returns {Iterator<T>} A new `Iterator` with the first {@linkcode count} | |
* elements of the original iterator. | |
* @throws {TypeError} If {@linkcode count} is not a number. | |
* @throws {RangeError} If {@linkcode count} is negative or NaN. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const taken = it.take(2); | |
* console.log([...taken]); // [1, 2] | |
* console.log([...it]); // [3, 4, 5] (the first two elements were consumed) | |
* ``` | |
*/ | |
take(count: number): Iterator<T> { | |
count = +count; | |
if (isNaN(count) || count < 0) { | |
throw new RangeError(`${count} must be positive`); | |
} | |
const it = getIterator(this); | |
return Iterator.from({ | |
*[Symbol.iterator]() { | |
let i = 0; | |
while (i++ < count) { | |
const next = it.next(); | |
if (next.done) break; | |
yield next.value; | |
} | |
}, | |
}); | |
} | |
/** | |
* Drops the first {@linkcode count} elements from this iterator, and returns | |
* a new {@link Iterator} with the remaining elements. If the iterator has | |
* fewer than {@linkcode count} elements, an empty iterator is returned. | |
* | |
* This method consumes the original iterator and its elements. | |
* | |
* @param {number} count The number of elements to drop from the Iterator. | |
* @returns {Iterator<T>} A new `Iterator` with the remaining elements. | |
* @throws {TypeError} If {@linkcode count} is not a number. | |
* @throws {RangeError} If {@linkcode count} is negative. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const dropped = it.drop(2); | |
* console.log([...dropped]); // [3, 4, 5] | |
* console.log([...it]); // [] (the original iterator is consumed) | |
* ``` | |
*/ | |
drop(count: number): Iterator<T> { | |
count = +count; | |
if (isNaN(count) || count < 0) { | |
throw new RangeError(`${count} must be positive`); | |
} | |
const it = getIterator(this); | |
return Iterator.from({ | |
*[Symbol.iterator]() { | |
let i = 0; | |
while (true) { | |
const next = it.next(); | |
if (next.done) break; | |
if (i++ < count) continue; | |
yield next.value; | |
} | |
}, | |
}); | |
} | |
/** | |
* Calls the specified {@link callbackfn|callback function} once per element | |
* in this iterator, and returns a new {@link Iterator} with the results. If | |
* the {@linkcode callbackfn} function yields an iterator, its elements will | |
* be flattened into the resulting iterator, similar to the behavior of the | |
* built-in `Array.prototype.flatMap` method. | |
* | |
* The new iterator will use the same underlying source as the original one, | |
* and therefore will consume the original iterator when iterated over. See | |
* the examples below for more details. | |
* | |
* @param callbackfn A function that accepts up to three arguments. This | |
* method calls the callbackfn function one time for each element in the | |
* iterator, in the order of iteration, and yields the flattened results. | |
* @param thisArg An object to which the this keyword can refer in the | |
* callbackfn function. If omitted, `this` will be `undefined` instead. | |
* @returns A new {@link Iterator} with the results of the mapping operation. | |
* @throws {TypeError} If {@linkcode callbackfn} is not a function. | |
* @example | |
* ```ts | |
* const it = Iterator.from([1, 2, 3, 4, 5]); | |
* const mapped = it.flatMap((n) => [n, n * 2]); | |
* console.log([...mapped]); // [1, 2, 2, 4, 3, 6, 4, 8, 5, 10] | |
* console.log([...it]); // [] (the original iterator was consumed) | |
* ``` | |
* @example | |
* ```ts | |
* const original = Iterator.from([1, 2, 3, 4, 5]); | |
* const remapped = original.flatMap((n) => [n, n * 2]); | |
* | |
* // the new iterator shares the same underlying source as the original | |
* console.log(remapped.take(2).toArray()); // [1, 2] | |
* console.log(original.next()); // { value: 3, done: false } | |
* | |
* // the original iterator is consumed when the new one is | |
* console.log(remapped.drop(1).toArray()); // [4, 8, 5, 10] | |
* console.log([...remapped]); // [] (consumed) | |
* console.log([...original]); // [] (consumed) | |
* ``` | |
*/ | |
flatMap<S>( | |
callbackfn: (value: T, index: number, iterator: Iterator<T>) => Iterable<S>, | |
thisArg?: unknown, | |
): Iterator<S> { | |
if (typeof callbackfn !== "function") { | |
throw new TypeError("callbackfn is not a function"); | |
} | |
// deno-lint-ignore no-this-alias | |
const self = this; | |
const source = getSource(this); | |
return Iterator.from({ | |
*[Symbol.iterator]() { | |
let i = 0; | |
for (const value of source) { | |
yield* callbackfn.call(thisArg, value, i++, self); | |
} | |
}, | |
}); | |
} | |
/** | |
* Returns a reference to this iterator itself. Called by the semantics of | |
* `for...of` operations, spread operators, and destructuring assignments. | |
*/ | |
[Symbol.iterator](): IterableIterator<T> { | |
return this; | |
} | |
declare readonly [Symbol.toStringTag]: string; | |
static { | |
Object.defineProperty(this.prototype, Symbol.toStringTag, { | |
value: "Iterator", | |
writable: true, | |
configurable: true, | |
}); | |
Object.defineProperties(this, { | |
toString: { | |
value: function toString(this: typeof Iterator) { | |
return `function ${this.name || "Iterator"}() { [native code] }`; | |
}, | |
}, | |
}); | |
} | |
protected constructor() { | |
const [key, source] = arguments; | |
if (new.target === Iterator) { | |
// iterators cannot be constructed directly. | |
// please use Iterator.from() instead. | |
if (key !== ctorKey) { | |
throw new TypeError( | |
"Abstract class Iterator not directly constructable", | |
); | |
} | |
if (source != null) setPrototype(this, source); | |
} else { | |
// subclassing is allowed, but the subclass must provide | |
// its own 'next' method implementation. | |
setPrototype(this); | |
} | |
} | |
} | |
// #region Internals and Helpers | |
const ctorKey = Symbol(); | |
const sourceCache = new WeakMap<Iterator<unknown>, Iterable<unknown>>(); | |
const iteratorCache = new WeakMap<Iterator<unknown>, GlobalIterator<unknown>>(); | |
const bind = (fn: Function, thisArg: unknown) => | |
Object.defineProperty(fn.bind(thisArg), "name", { value: fn.name }); | |
/** Resolves a source object into an iterable. */ | |
function getIterable<T>(source: IteratorSource<T>): Iterable<T> { | |
const obj = Object(source); | |
const iterable = obj[Symbol.iterator]; | |
if (typeof iterable !== "function") { | |
throw new TypeError("Iterator.from called on non-iterator"); | |
} | |
return iterable.call(obj); | |
} | |
function getSource<T>(it: Iterator<T>, obj?: IteratorSource<T>): Iterable<T> { | |
const cached = sourceCache.get(it); | |
if (cached != null) return cached as Iterable<T>; | |
return sourceCache.set(it, getIterable(obj ?? it)).get(it) as Iterable<T>; | |
} | |
function getIterator<T>(it: Iterator<T>): GlobalIterator<T> { | |
const cached = iteratorCache.get(it); | |
if (cached != null) return cached as GlobalIterator<T>; | |
return iteratorCache.set(it, getSource(it)[Symbol.iterator]()).get( | |
it, | |
) as GlobalIterator<T>; | |
} | |
function setPrototype<T>( | |
it: Iterator<T>, | |
obj?: IteratorSource<T>, | |
iterable: Iterable<T> = getSource(it, obj), | |
): GlobalIterator<T> { | |
const cached = iteratorCache.get(it); | |
if (cached != null) return cached as GlobalIterator<T>; | |
const iterator = iterable[Symbol.iterator](); | |
const original = Object.getPrototypeOf(iterator); | |
// Object.setPrototypeOf(original, Iterator.prototype); | |
const prototype = new Proxy(original, { | |
get(t, p) { | |
if (p === "constructor") return Iterator; | |
let v = Reflect.get(t, p); | |
let thisArg: unknown; | |
if (["next", "throw", "return"].includes(p as string)) { | |
thisArg = iterator; | |
} else { | |
thisArg = it; | |
} | |
if ( | |
!(Reflect.has(t, p) && v !== undefined) && | |
Reflect.has(Iterator.prototype, p) | |
) { | |
v = Reflect.get(Iterator.prototype, p, thisArg); | |
} | |
return typeof v === "function" ? bind(v, thisArg) : v; | |
}, | |
getPrototypeOf() { | |
return Iterator.prototype; | |
}, | |
}); | |
Object.setPrototypeOf(iterator, prototype); | |
it = Object.setPrototypeOf(it, iterator); | |
iteratorCache.set(it, iterator); | |
return iterator; | |
} | |
// #endregion Internals and Helpers |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment