UNPKG

iteragain

Version:

Javascript Iterable/Iterator/Generator-function utilities.

588 lines 24.7 kB
import { MAX_EMPTY_ERROR, MINMAX_EMPTY_ERROR, MIN_EMPTY_ERROR, REDUCE_EMPTY_ERROR } from './emptyIteratorError'; import toIterator from '../toIterator'; import ConcatIterator from './ConcatIterator'; import FilterIterator from './FilterIterator'; import FlattenIterator from './FlattenIterator'; import MapIterator from './MapIterator'; import WindowsIterator from './WindowsIterator'; import PairwiseIterator from './PairwiseIterator'; import SliceIterator from './SliceIterator'; import ZipIterator from './ZipIterator'; import ZipLongestIterator from './ZipLongestIterator'; import TapIterator from './TapIterator'; import TriplewiseIterator from './TriplewiseIterator'; import ChunksIterator from './ChunksIterator'; import TakeWhileIterator from './TakeWhileIterator'; import CycleIterator from './CycleIterator'; import ResumeIterator from './ResumeIterator'; import PermutationsIterator from './PermutationsIterator'; import FilterMapIterator from './FilterMapIterator'; import DropWhileIterator from './DropWhileIterator'; import CompressIterator from './CompressIterator'; import ProductIterator from './ProductIterator'; import CombinationsIterator from './CombinationsIterator'; import SeekableIterator from './SeekableIterator'; import TeedIterator from './TeedIterator'; import count from '../count'; import FlatMapIterator from './FlatMapIterator'; import GroupByIterator from './GroupByIterator'; import { promiseAll } from '../promiseAll'; const enumerator = (count = 0) => (v) => [count++, v]; /** * Extends and implements the IterableIterator interface. Methods marked with the `@lazy` prefix are chainable methods * that modify the internal iterator, but don't start iterating. Methods without the `@lazy` prefix do start iterating * some amount, depending on the method. */ export class ExtendedIterator { constructor(iterator) { this.iterator = iterator; } /** Returns a `{ value, done }` object that adheres to the Iterator interface. */ next(...args) { return this.iterator.next(...args); } /** Implements this as an Iterable so it's allowed to be used with "for of" loops. */ [Symbol.iterator]() { return this; } toString() { return 'ExtendedIterator'; } /** @lazy Returns a new ExtendedIterator that maps each element in this iterator to a new value. */ map(iteratee) { return new ExtendedIterator(new MapIterator(this.iterator, iteratee)); } filter(predicate) { return new ExtendedIterator(new FilterIterator(this.iterator, predicate)); } /** * @lazy * Maps and filters the input iterator in the same `iteratee` function. * @param iteratee A function that maps each value in this iterator to a new value and also filters out any that * return a nullish value. */ filterMap(iteratee) { return new ExtendedIterator(new FilterMapIterator(this.iterator, iteratee)); } /** @lazy Concatenates this iterator with the given iterators, in order of: `[this.iterator, ...others]`. */ concat(...args) { return new ExtendedIterator(new ConcatIterator([this.iterator, ...args.map(toIterator)])); } /** @lazy Prepends this iterator with the given iterators, in order of: `[...args, this.iterator]`. */ prepend(...args) { return new ExtendedIterator(new ConcatIterator([...args.map(toIterator), this.iterator])); } /** * @lazy * Works like `Array.prototype.slice`, returns a new slice of this iterator. * @note This does not support negative `start` and `end` indices, as it's not possible to know the length of the * iterator while iterating. * @param start The index to start at (inclusive). * @param end The index to end at (exclusive). * @returns A new ExtendedIterator that only includes the elements between `start` and `end`. */ slice(start = 0, end = Infinity) { return new ExtendedIterator(new SliceIterator(this.iterator, start, end)); } flatten(depth = Infinity) { return new ExtendedIterator(new FlattenIterator(this.iterator, depth)); } /** @lazy Attaches the index to each value as a pair like: `[0, value], [1, value]`, etc. */ enumerate() { return this.map(enumerator()); } /** * @lazy * The inverse of `zip` and `zipLongest`. This method disaggregates the elements of this iterator. The nth iterator * in the returned tuple contains the nth element of each value in this iterator. The length of the returned tuple is * determined by the length of the first value in this iterator. */ unzip() { const [head] = this.peek(); const n = Array.isArray(head) ? head.length : 1; if (n < 2) return [this]; return this.tee(n).map((it, i) => it.map(v => v[i])); } zip(...args) { return new ExtendedIterator(new ZipIterator([this.iterator, ...args.map(toIterator)])); } zipLongest(...args) { return new ExtendedIterator(new ZipLongestIterator([this.iterator, ...args.map(toIterator)])); } /** * @lazy * Return a new iterator of pairs (tuples) of the values in this one. The number of pairs will always be one fewer * than this iterator. Will be empty if this iterator has fewer than two values. * @example * iter([1,2,3]).pairwise().toArray() // [[1,2], [2,3]] * iter([1]).pairwise().toArray() // [] */ pairwise() { return new ExtendedIterator(new PairwiseIterator(this.iterator)); } /** * @lazy * Returns a new iterator of triplets (tuples) of the values in this one. The number of triplets will always be two * fewer than the number of values in this iterator. Will be empty if this iterator has fewer than three values. */ triplewise() { return new ExtendedIterator(new TriplewiseIterator(this.iterator)); } /** * @lazy * Take all elements from this iterator while the given `predicate` returns a truthy value. * @param predicate A function to call for each value. */ takeWhile(predicate) { return new ExtendedIterator(new TakeWhileIterator(this.iterator, predicate)); } /** * @lazy * Drop/skip values in this iterator while the passed `predicate` returns a truthy value. * @param predicate The function to call for each value. */ dropWhile(predicate) { return new ExtendedIterator(new DropWhileIterator(this.iterator, predicate)); } /** * @lazy * Tap into this iterator by supplying `func` which is passed each value of this iterator. The return value of * func is unused and this method is purely designed for a designated place to perform side effects. * @example * iter([1,2,3]) * .tap(console.log) // logs 1, 2, 3 to the console * .map(n => n * n) * .tap(console.log) // logs 1, 4, 9 to the console * .toArray() // returns [1, 4, 9] */ tap(func) { return new ExtendedIterator(new TapIterator(this.iterator, func)); } /** * @lazy * Yields non-overlapping chunks (tuples) of `length` from this iterator. * @param length The length of each chunk, must be greater than 0. * @param fill Optional, the value to fill the last chunk with if it's not the same length as the rest of the iterator. * @example * iter([1,2,3,4,5,6,7,8,9]).chunk(3).toArray() // [[1,2,3], [4,5,6], [7,8,9]] * iter([1,2,3,4,5,6,7,8,9]).chunk(2, 0).toArray() // [[1,2], [3,4], [5,6], [7,8], [9, 0]] */ chunks(length, fill) { return new ExtendedIterator(new ChunksIterator(this.iterator, length, fill)); } /** * @lazy * Yields sliding windows (tuples) of `length` from this iterator. Each window is separated by `offset` number of * elements. * @param length The length of each window, must be greater than 0. * @param offset The offset of each window from each other. Must be greater than 0. * @param fill Optional, the value to fill the last window with if it's not the same length as the rest of the iterator. * @example * iter([1,2,3,4,5]).windows(2, 1).toArray() // [[1,2], [2,3], [3,4], [4,5]] * iter([1,2,3,4,5]).windows(2, 3).toArray() // [[1,2], [4,5]] * iter([1,2,3,4,5]).windows(3, 3, 0).toArray() // [[1,2,3], [4,5,0]] */ windows(length, offset, fill) { return new ExtendedIterator(new WindowsIterator(this.iterator, length, offset, fill)); } /** * @lazy * Returns `n` independent iterators, each of which is a copy of this iterator at the time of calling `tee`. Once * `tee` has made a split, do not modify or call upon the original iterator, as the new iterators will not be * updated/informed. * This caches the original iterator's values as the new iterators are iterated through. So * depending on the size of the original iterator, there could be significant memory overhead in using `tee`. * `tee`'s intended use is to iterate over the returned iterators in parallel, or at least somewhat in parallel. In * general, if one returned iterator consumes most or all of it's values, then it is faster to just * use `toArray` and then iterate over that. * @param n The number of independent iterators to create. */ tee(n) { const seekable = new SeekableIterator(toIterator(this.iterator)); const indices = new Array(n).fill(0); return Array.from({ length: n }, (_, i) => new ExtendedIterator(new TeedIterator(i, seekable, indices))); } /** * @lazy * Makes this iterator cycle infinitely through it's values. * @param times The number of times to cycle through the iterator (default: Infinity). * @example * equal(iter([1,2,3]).cycle().take(5).toArray(), [1,2,3,1,2]) */ cycle(times = Infinity) { return new ExtendedIterator(new CycleIterator(this.iterator, times)); } /** * @lazy * Resumes this iterator a certain number of times after it's next value returns `{ done: true }`. * @param times The number of times to resume the iterator (default: Infinity). * @example * const it = iter([1,2,3]).resume(1); * equal(it.toArray(), [1,2,3]); * equal(it.toArray(), [1,2,3]); * equal(it.toArray(), []); */ resume(times = Infinity) { return new ExtendedIterator(new ResumeIterator(this.iterator, times)); } /** * @lazy * Filters/compresses this iterator to only values that correspond to truthy values in `selectors`. * @param selectors An iterator or iterable of falsey or truthy values to select which values to keep in this * iterator. */ compress(selectors) { return new ExtendedIterator(new CompressIterator(this.iterator, toIterator(selectors))); } /** * @lazy * Returns all successive `size` length permutations of this iterator. The permutations are emitted in lexicographic * ordering according to this iterator. So if this iterator is sorted, the permutations will be in sorted order. * Elements in the permutations are treated as unique based on their position in the iterator, not on their value. So * if the input iterator is unique, then there will be no repeat values. * @see https://docs.python.org/3/library/itertools.html#itertools.permutations for more info. * @param size The size of each permutation, must be greater than 0 and less than or equal to the length of this * iterator. */ permutations(size) { return new ExtendedIterator(new PermutationsIterator(this.iterator, size)); } /** * @lazy * Returns `size` length subsequences of this iterator. * @see https://docs.python.org/3/library/itertools.html#itertools.combinations for more info. * @see https://docs.python.org/3/library/itertools.html#itertools.combinations_with_replacement for more info. * @param size The size of each combination. * @param withReplacement Whether or not to allow duplicate elements in the combinations. */ combinations(size, withReplacement = false) { return new ExtendedIterator(new CombinationsIterator(this.iterator, size, withReplacement)); } product(...params) { var _a; const iterators = typeof params[0] === 'number' ? [] : params[0]; const repeat = (_a = params.find(param => typeof param === 'number')) !== null && _a !== void 0 ? _a : 1; return new ExtendedIterator(new ProductIterator([this.iterator, ...iterators.map(toIterator)], repeat)); } /** * @lazy * Filters this iterator to only unique values. * @param iteratee Iteratee to use to transform each value before being tested for uniqueness. * @param justSeen If true, will only test for uniqueness with the last value in the iterator and not all values. */ unique(params = v => v) { const { iteratee = (v) => v, justSeen = false } = typeof params === 'function' ? { iteratee: params } : params; if (justSeen) { let lastValue; let hasLastValue = false; return this.filter(value => { value = iteratee(value); if (!hasLastValue || value !== lastValue) { lastValue = value; hasLastValue = true; return true; } return false; }); } const seen = new Set(); return this.filter(value => { value = iteratee(value); if (!seen.has(value)) { seen.add(value); return true; } return false; }); } /** * @lazy * Reverses this iterator's order. Note that in order to reverse, it will attempt to iterate fully once, which * could cause significant memory usage. So because of this, only use on finite iterators. */ reverse() { let next; const result = []; while (!(next = this.iterator.next()).done) result.push(next.value); result.reverse(); return new ExtendedIterator(toIterator(result)); } /** @lazy Maps `key` from `T` in each value of this iterator. */ pluck(key) { return this.filterMap(value => value[key]); } reduce(reducer, initialValue) { let next; if (arguments.length < 2) { const first = this.iterator.next(); if (first.done) throw new TypeError(REDUCE_EMPTY_ERROR); let accumulator = first.value; while (!(next = this.iterator.next()).done) accumulator = reducer(accumulator, next.value); return accumulator; } let accumulator = initialValue; while (!(next = this.iterator.next()).done) accumulator = reducer(accumulator, next.value); return accumulator; } /** Consumes this iterator and returns the number of values/items in it. */ length() { return this.reduce((acc, _) => acc + 1, 0); } /** Returns the number of times the `predicate` returns a truthy value. */ quantify(predicate) { return this.reduce((acc, v) => acc + (predicate(v) ? 1 : 0), 0); } /** Returns the minimum value from this iterator. */ min(iteratee = v => v) { let next = this.iterator.next(); if (next.done) throw new TypeError(MIN_EMPTY_ERROR); let min = { value: next.value, comparison: iteratee(next.value) }; while (!(next = this.iterator.next()).done) { const comparison = iteratee(next.value); if (comparison < min.comparison) min = { value: next.value, comparison }; } return min.value; } /** Returns the maximum value from this iterator. */ max(iteratee = v => v) { let next = this.iterator.next(); if (next.done) throw new TypeError(MAX_EMPTY_ERROR); let max = { value: next.value, comparison: iteratee(next.value) }; while (!(next = this.iterator.next()).done) { const comparison = iteratee(next.value); if (comparison > max.comparison) max = { value: next.value, comparison }; } return max.value; } /** Returns the minimum and maximum from this iterator as a tuple: `[min, max]`. */ minmax(iteratee = v => v) { let next = this.iterator.next(); if (next.done) throw new TypeError(MINMAX_EMPTY_ERROR); let min = { value: next.value, comparison: iteratee(next.value) }; let max = { value: next.value, comparison: iteratee(next.value) }; while (!(next = this.iterator.next()).done) { const comparison = iteratee(next.value); if (comparison < min.comparison) min = { value: next.value, comparison }; if (comparison > max.comparison) max = { value: next.value, comparison }; } return [min.value, max.value]; } /** Iterate over this iterator using the `array.prototype.forEach` style of method. */ forEach(callback) { let next; while (!(next = this.iterator.next()).done) callback(next.value); } /** Return true if every element in this iterator matches the predicate. */ every(predicate) { let next; while (!(next = this.iterator.next()).done) if (!predicate(next.value)) return false; return true; } /** Return true if only one element in this iterator matches the predicate. */ some(predicate) { let next; while (!(next = this.iterator.next()).done) if (predicate(next.value)) return true; return false; } /** * Returns this iterator as a string with each value joined by `separator`. * @param separator The separator to use between each value (default: ','). */ join(separator = ',') { const first = this.iterator.next(); if (first.done) return ''; let result = `${first.value}`; let next; while (!(next = this.iterator.next()).done) result += separator + next.value; return result; } find(predicate) { let next; while (!(next = this.iterator.next()).done) if (predicate(next.value)) return next.value; } /** * Finds the index of the first value that passes a truthy vale to `predicate`, then returns it. Only consumes the * iterator's values up to the found value, then stops. So if it's not found, then the iterator is exhausted. */ findIndex(predicate) { let next; let i = -1; while ((i++, !(next = this.iterator.next()).done)) if (predicate(next.value)) return i; return -1; } /** * Maps this iterator to a new value `R` and flattens any resulting iterables or iterators by a depth of 1. * Behaves the same as `Array.prototype.flatMap`. * * Strings returned from `iteratee` are not flattened into characters; return an array (e.g. `[...str]`) instead. */ flatMap(iteratee) { return new ExtendedIterator(new FlatMapIterator(this.iterator, iteratee)); } /** Returns true if `value` strictly equals some value in this iterator. */ includes(value) { return this.some(v => v === value); } /** * Peek ahead of where the current iteration is. This doesn't consume any values of the iterator. * @param ahead optional, the number of elements to peek ahead. */ peek(ahead = 1) { const values = this.take(ahead); if (values.length) this.iterator = new ConcatIterator([toIterator(values), this.iterator]); return values; } /** * Take `n` number of values from this iterator. * @param n The number of values to take. */ take(n = 1) { const values = []; let next; while (n-- > 0 && !(next = this.iterator.next()).done) values.push(next.value); return values; } /** * @deprecated Use `consume` instead, as this is the more standard name for this type of method. * Start iterating through this iterator, but don't return the values from this method. * @param n optional, the number of elements to exhaust. */ exhaust(n = Infinity) { this.consume(n); } /** * Start iterating through this iterator, but don't return the values from this method. * @param n optional, the number of elements to consume (default: Infinity). */ consume(n = Infinity) { while (n-- > 0 && !this.iterator.next().done) ; } /** * Collects all values from this iterator, then shuffles the order of it's values. * @param seed A seed between 0 and 1. */ shuffle(seed = Math.random()) { const values = this.toArray(); for (let i = values.length - 1; i > 0; i--) { const j = Math.min(i, Math.max(0, Math.floor(seed * (i + 1)))); [values[i], values[j]] = [values[j], values[i]]; } return new ExtendedIterator(toIterator(values)); } /** Collects all values from this iterator, then sorts them. */ sort(comparator = (a, b) => (a < b ? -1 : a > b ? 1 : 0)) { return new ExtendedIterator(toIterator(this.toArray().sort(comparator))); } /** * Partitions this iterator into a tuple of `[falsey, truthy]` corresponding to what `predicate` returns for each * value. */ partition(predicate) { const falsey = []; const truthy = []; let next; while (!(next = this.iterator.next()).done) { if (predicate(next.value)) truthy.push(next.value); else falsey.push(next.value); } return [falsey, truthy]; } /** * @lazy * Distributes this iterator's values among `n` amount of smaller iterators. Does not maintain order so if order is * important, use `divide` instead. */ distribute(n) { return this.tee(n).map((it, i) => it.compress(new ExtendedIterator(count()).map(v => (v - i) % n === 0))); } groupBy(key = v => v) { return new ExtendedIterator(new GroupByIterator(this.iterator, key)); } /** * Divides this iterator into `n` amount of smaller iterators while maintaining order. Note, this method will fully * iterate through this iterator before returning a result. If you don't want this behavior and don't care about * order then use `distribute` instead. */ divide(n) { const array = this.toArray(); const result = []; const quotient = Math.floor(array.length / n); const remainder = array.length % n; let stop = 0; let start = 0; for (let i = 1; i < n + 1; i++) { start = stop; stop += i <= remainder ? quotient + 1 : quotient; result.push(new ExtendedIterator(toIterator(array.slice(start, stop)))); } return result; } /** * Iterates and finds the element at `index`. Returns undefined if not found. * @param index The index to find. Only supports positive indices. */ nth(index) { let i = 0; let next; while (!(next = this.iterator.next()).done && index !== i++) ; return next.value; } /** Iterates and collects all values into an Array. */ toArray() { const result = []; let next; while (!(next = this.iterator.next()).done) result.push(next.value); return result; } /** Calls `Promise.all` on all collected values, optionally limiting concurrent in-flight promises. */ promiseAll({ concurrency = Infinity } = {}) { return promiseAll(this, { concurrency }); } /** Calls `Promise.race` on all collected values. */ promiseRace() { return Promise.race(this.toArray()); } /** Shorthand for `new Set(this)`. */ toSet() { return new Set(this); } /** * Shorthand for `new Map<K, V>(this)`. The type of this iterator must extend `any[]` for this to work. And you may * also need to pass in your own values for the generics: e.g. `iterator.toMap<string, number>();` */ toMap() { return new Map(this); } } export default ExtendedIterator; //# sourceMappingURL=ExtendedIterator.js.map