@drew887121/iteratorfuncs
Version:
A small package to make working with iterators easier
251 lines (239 loc) • 9.58 kB
JavaScript
/**
* IteratorFuncs a simple single file package to make working with JS iterators easier.
*
* The documentation lives online [here](https://drew887.github.io/iteratorFuncs/modules.html).
*
* If you've ever had a generator, or a Set, or a Map that you've needed to map/filter/reduce over in JS, you've been hit
* by the pain of having to either convert to an Array with `Array.from` and then use the method you need, or write a
* for...of loop and do your logic in there.
*
* While the for...of loop is almost certainly the "proper" or "js way" of doing things, and is what you normally get as
* an answer when searching for this, it can be a little disappointing or jarring for those of us wanting a more fp like
* approach that js normally lets you take.
*
* So this package is simply to help facilitate these patterns for IterableIterators.
*
* tldr:
```typescript
const set = new Set([1, 2, 3, 4, 5]);
const mappedAndFiltered =
mapIterator(set, (item) => item * 2)
.filter((item) => item > 5);
console.log(Array.from(mappedAndFiltered)); // logs out [6, 8, 10]
```
*
* @packageDocumentation
*/
// ============= Non Greedy Versions ================
/**
* An AugmentedIterator represents one of the IterableIterators we return.
* This abstract class is exported only to provide type/intellisense information,
* and should be only implemented internally.
*/
export class AugmentedIterator {
[Symbol.iterator]() {
return this;
}
map(mapper) {
return mapIterator(this, mapper);
}
filter(filter) {
return filterIterator(this, filter);
}
reduce(reducer, initial) {
return reduceIterator(this, reducer, initial);
}
}
/**
* This is a private class for encapsulating the logic for reducing.
*/
class _ReducingIterator extends AugmentedIterator {
constructor(iterable, reducer, state) {
super();
this.reducer = reducer;
this.state = state;
this.iterable = iterable[Symbol.iterator]();
}
next(...args) {
const result = this.iterable.next(...args);
if (!result.done) {
this.state = this.reducer(this.state, result.value);
return Object.assign(Object.assign({}, result), { value: this.state });
}
return result;
}
}
/**
* This is a private class for encapsulating the logic for mapping.
*/
class _MappingIterator extends AugmentedIterator {
constructor(iterable, mapper) {
super();
this.mapper = mapper;
this.iterable = iterable[Symbol.iterator]();
}
next(...args) {
const result = this.iterable.next(...args);
if (!result.done) {
return Object.assign(Object.assign({}, result), { value: this.mapper(result.value) });
}
return result;
}
}
/**
* This is a private class for encapsulating the logic for filtering.
*/
class _FilteringIterator extends AugmentedIterator {
constructor(iterable, filterer) {
super();
this.filterer = filterer;
this.iterable = iterable[Symbol.iterator]();
}
next(...args) {
let result = this.iterable.next(...args);
while (!result.done && !this.filterer(result.value)) {
result = this.iterable.next(...args);
}
return result;
}
}
/**
* Given an Iterable or IterableIterator, a reducer, and an initial value, return a new IterableIterator that yields
* values that are automatically piped through said reducer.
* @param {Iterable} iterator - The iterator you wish to reduce over
* @param {Function} reducer - A function to use to reduce the elements iterator produces
* @param initial The initial value to pass to reducer with the first element
* @typeParam TValue - The value returned from the iterator
* @typeParam TReducerReturn - The return type of your reducer function
* @typeParam TReturn - If your iterator has a return type it must be the same as TIteratorValue, otherwise undefined
* @typeParam TNext - The type your iterator is expecting as what's passed to its next function. For generators this is the type returned after a yield.
*
* @example
const set: Set<number> = new Set([1, 2, 3, 4, 5]);
const summingIterator = reduceIterator(
set.values(),
(carry: number, item: number): number => {
return carry + item;
},
0,
);
// summingIterator will now yield the sum for each value seen thus far
const finalValues = Array.from(summingIterator);
// finalValues will now be [1,3,6,10,15];
*/
export function reduceIterator(iterator, reducer, initial) {
return new _ReducingIterator(iterator, reducer, initial);
}
/**
* Given an IteratorArg and a mapping function, returns a new IterableIterator that yields values from the initial iterator
* but passed through the mapping function.
* @param {Iterable} iterator - The iterator you wish to map elements from
* @param {Function} mapper - A function to use to map the elements iterator produces
* @typeParam TValue - The value returned from the iterator
* @typeParam TMapperReturn - The return type of your reducer function
* @typeParam TReturn - If your iterator has a return type it must be the same as TIteratorValue, otherwise undefined
* @typeParam TNext - The type your iterator is expecting as what's passed to its next function. For generators this is the type returned after a yield.
* @example
const set = new Set([1, 2, 3, 4, 5]);
const mapped = mapIterator(set, (item) => item * 2);
console.log(Array.from(mapped)); // logs out [2, 4, 6, 8, 10]
*/
export function mapIterator(iterator, mapper) {
// Because we can't guarantee a 0 value for the types, we can't just fall back to reduce.
return new _MappingIterator(iterator, mapper);
}
/**
* Given an IteratorArg and a filtering function it will return a new IterableIterator that when polled will eagerly pull
* values from iterator and pass them to filterer until filterer returns true for an item, or we reach the end of iterator;
* it will then yield this value itself.
*
* **NOTE:** if iterator relies on values being passed to next, the returned IterableIterator will re-use the same next
* until a value passes the filterer.
* value until an item is raised that passes the filterer. This can lead to strange behaviours.
* @param {Iterable} iterator - The iterator you wish to map elements from
* @param {Function} filterer - Only values for which this function returns true are yielded.
* @typeParam TValue - The value returned from the iterator
* @typeParam TReturn - If your iterator has a return type it must be the same as TIteratorValue, otherwise undefined
* @typeParam TNext - The type your iterator is expecting as what's passed to its next function. For generators this is the type returned after a yield.
*
* @example
const set = new Set([1, 2, 3, 4, 5]);
const filtered = filterIterator(set, (item) => item % 2 === 0);
console.log(Array.from(filtered)); // logs out [2, 4]
*/
export function filterIterator(iterator, filterer) {
return new _FilteringIterator(iterator, filterer);
}
// ============= Greedy Versions ================
/**
* Given an iterator, eagerly reduce over its results until it finishes and return the result
* @param {Iterable} iterator - The iterator you wish to reduce over
* @param {Function} reducer - A function to use to reduce the elements iterator produces
* @param initial The initial value to pass to reducer with the first element. Also returned if iterator never yields a value
* @typeParam TValue - The type of values that iterator will yield and that initial must be
* @typeParam TReturn - The type of values that reducer will return
*
* @example
const set: Set<number> = new Set([1, 2, 3, 4, 5]);
const sum = eagerlyReduceIterator(
set.values(),
(carry: number, item: number): number => {
return carry + item;
},
0,
);
// sum is now 15
*/
export function eagerlyReduceIterator(iterator, reducer, initial) {
let result = initial;
for (const a of iterator) {
result = reducer(result, a);
}
return result;
}
/**
* Eagerly Map over an iterator and return an array of results
* @param {Iterable} iterator - The iterator you wish to map over
* @param {Function} mapper - The function you want to use to process any elements yielded from iterator
* @typeParam TValue - The type of values that iterator will yield
* @typeParam TReturn - The type of values that mapper will return
*
* @example
const set: Set<number> = new Set([1, 2, 3, 4, 5]);
const squaredValues = mapIteratorToArray(
set.values(),
(item: number): number => {
return item * item;
},
);
*/
export function mapIteratorToArray(iterator, mapper) {
return eagerlyReduceIterator(iterator, (carry, item) => {
carry.push(mapper(item));
return carry;
}, []);
}
/**
* Eagerly Filter over an iterator and return an array of results
* @param {Iterable} iterator - The iterator you wish to filter over
* @param {Function} filter - Your filtering function
* @typeParam TValue - The type of values that iterator will yield
*
* @example
const set: Set<number> = new Set([1, 2, 3, 4, 5]);
const evenValues = filterIteratorToArray(
set.values(),
(item: number): boolean => {
return item % 2 === 0;
},
);
// evenValues = [2, 4];
*/
export function filterIteratorToArray(iterator, filter) {
return eagerlyReduceIterator(iterator, (carry, item) => {
if (filter(item)) {
carry.push(item);
}
return carry;
}, []);
}