diff --git a/src/index.ts b/src/index.ts index 449728fcd..1cc280b33 100644 --- a/src/index.ts +++ b/src/index.ts @@ -447,6 +447,7 @@ export * from './util/ContentTypes'; export * from './util/FetchUtil'; export * from './util/GuardedStream'; export * from './util/HeaderUtil'; +export * from './util/IterableUtil'; export * from './util/PathUtil'; export * from './util/PromiseUtil'; export * from './util/QuadUtil'; diff --git a/src/util/IterableUtil.ts b/src/util/IterableUtil.ts new file mode 100644 index 000000000..2cb736f65 --- /dev/null +++ b/src/util/IterableUtil.ts @@ -0,0 +1,94 @@ +// Utility functions for iterables that avoid array conversion + +/** + * Creates a new iterable with the results of calling a provided function on every element in the calling array. + * Similar to the {@link Array.prototype.map} function. + * See the documentation of the above function for more details. + * + * @param iterable - Iterable on which to call the map function. + * @param callbackFn - Function that is called for every element. + * @param thisArg - Value to use as `this` when executing `callbackFn`. + */ +export function* map(iterable: Iterable, callbackFn: (element: TIn, index: number) => TOut, + thisArg?: any): Iterable { + const boundMapFn = callbackFn.bind(thisArg); + let count = 0; + for (const value of iterable) { + yield boundMapFn(value, count); + count += 1; + } +} + +/** + * Creates a new iterable with all elements that pass the test implemented by the provided function. + * Similar to the {@link Array.prototype.filter} function. + * See the documentation of the above function for more details. + * + * @param iterable - Iterable on which to call the map function. + * @param callbackFn - Function that is called to test every element. + * @param thisArg - Value to use as `this` when executing `callbackFn`. + */ +export function* filter(iterable: Iterable, callbackFn: (element: T, index: number) => boolean, + thisArg?: any): Iterable { + const boundFilterFn = callbackFn.bind(thisArg); + let count = 0; + for (const value of iterable) { + if (boundFilterFn(value, count)) { + yield value; + } + count += 1; + } +} + +/** + * Creates a new iterable that is a concatenation of all the iterables in the input. + * @param iterables - An iterable of which the contents will be concatenated into a new iterable. + */ +export function* concat(iterables: Iterable>): Iterable { + for (const iterable of iterables) { + yield* iterable; + } +} + +/** + * Similar to the {@link Array.prototype.reduce} function, but for an iterable. + * See the documentation of the above function for more details. + * The first element will be used as the initial value. + * + * @param iterable - Iterable of which to reduce the elements. + * @param callbackFn - A reducer function. + */ +export function reduce(iterable: Iterable, + callbackFn: (previousValue: TIn, currentValue: TIn, currentIndex: number) => TIn): TIn; +/** + * Similar to the {@link Array.prototype.reduce} function, but for an iterable. + * See the documentation of the above function for more details. + * + * @param iterable - Iterable of which to reduce the elements. + * @param callbackFn - A reducer function. + * @param initialValue - The value to start from. + */ +export function reduce(iterable: Iterable, + callbackFn: (previousValue: TOut, currentValue: TIn, currentIndex: number) => TOut, initialValue: TOut): TOut; +export function reduce(iterable: Iterable, + callbackFn: (previousValue: TOut, currentValue: TIn, currentIndex: number) => TOut, initialValue?: TOut): TOut { + const iterator = iterable[Symbol.iterator](); + let count = 0; + if (!initialValue) { + const next = iterator.next(); + if (next.done) { + throw new TypeError('Iterable is empty and no initial value was provided.'); + } + // `initialValue` being undefined means the first signature was used where TIn === TOut + initialValue = next.value as unknown as TOut; + count += 1; + } + let previousValue = initialValue; + let next = iterator.next(); + while (!next.done) { + previousValue = callbackFn(previousValue, next.value, count); + next = iterator.next(); + count += 1; + } + return previousValue; +} diff --git a/test/unit/util/IterableUtil.test.ts b/test/unit/util/IterableUtil.test.ts new file mode 100644 index 000000000..531c2b1b0 --- /dev/null +++ b/test/unit/util/IterableUtil.test.ts @@ -0,0 +1,41 @@ +import { concat, filter, map, reduce } from '../../../src/util/IterableUtil'; + +describe('IterableUtil', (): void => { + describe('#mapIterable', (): void => { + it('maps the values to a new iterable.', async(): Promise => { + const input = [ 1, 2, 3 ]; + expect([ ...map(input, (val): number => val + 3) ]).toEqual([ 4, 5, 6 ]); + }); + }); + + describe('#filterIterable', (): void => { + it('filters the values of the iterable.', async(): Promise => { + const input = [ 1, 2, 3 ]; + expect([ ...filter(input, (val): boolean => val % 2 === 1) ]).toEqual([ 1, 3 ]); + }); + }); + + describe('#concatIterables', (): void => { + it('concatenates all the iterables.', async(): Promise => { + const input = [[ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ]]; + expect([ ...concat(input) ]).toEqual([ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]); + }); + }); + + describe('#reduceIterable', (): void => { + it('reduces the values in an iterable.', async(): Promise => { + const input = [ 1, 2, 3 ]; + expect(reduce(input, (acc, cur): number => acc + cur)).toBe(6); + }); + + it('can take a starting value.', async(): Promise => { + const input = [ 1, 2, 3 ]; + expect(reduce(input, (acc, cur): number => acc + cur, 4)).toBe(10); + }); + + it('throws an error if the iterable is empty and there is no initial value.', async(): Promise => { + const input: number[] = []; + expect((): number => reduce(input, (acc, cur): number => acc + cur)).toThrow(TypeError); + }); + }); +});