iteragain
Version:
Javascript Iterable/Iterator/Generator-function utilities.
560 lines • 23.6 kB
JavaScript
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';
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;
return this.filter(value => {
value = iteratee(value);
if (!lastValue || value !== lastValue) {
lastValue = value;
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.unshift(next.value);
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;
let accumulator = initialValue !== null && initialValue !== void 0 ? initialValue : this.iterator.next().value;
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();
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();
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();
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 = ',') {
var _a;
return ((_a = this.reduce((str, v) => (str + separator + v))) !== null && _a !== void 0 ? _a : '');
}
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`.
*/
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.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. */
promiseAll() {
return Promise.all(this.toArray());
}
/** 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