Skip to content

Instantly share code, notes, and snippets.

@nberlette
Last active January 17, 2024 06:56
Show Gist options
  • Save nberlette/7a86cc1744bbdf6a36c5d6ee4b6ea68a to your computer and use it in GitHub Desktop.
Save nberlette/7a86cc1744bbdf6a36c5d6ee4b6ea68a to your computer and use it in GitHub Desktop.
Polyfill for the Iterator Helpers TC39 Proposal
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 };
}
}
// 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