UNPKG

itertools-ts

Version:

Extended itertools port for TypeScript and JavaScript. Provides a huge set of functions for working with iterable collections (including async ones)

1,025 lines (942 loc) 24.5 kB
import { toArray, toArrayAsync, toAsyncIterable, toIterable, } from "./transform"; import { InvalidArgumentError } from "./exceptions"; import { isAsyncIterable, isIterable, isIterator, isString } from "./summary"; import { distinct, distinctAsync } from "./set"; import { AsyncFlatMapper, Comparator, FlatMapper, Pair } from "./types"; import { zip, zipAsync } from "./multi"; /** * Map a function onto every element of the iteration. * * @param data * @param mapper */ export function* map<TInput, TOutput>( data: Iterable<TInput> | Iterator<TInput>, mapper: (datum: TInput) => TOutput ): Iterable<TOutput> { for (const datum of toIterable(data)) { yield mapper(datum); } } /** * Map a function onto every element of the iteration for async collections. * * Mapper may be also async. * * @param data * @param mapper */ export async function* mapAsync<TInput, TOutput>( data: | AsyncIterable<TInput> | AsyncIterator<TInput> | Iterable<TInput> | Iterator<TInput>, mapper: (datum: TInput) => TOutput | Promise<TOutput> ): AsyncIterable<TOutput> { for await (const datum of toAsyncIterable(data)) { yield await mapper(datum); } } /** * Compress an iterable by filtering out data that is not selected. * * Selectors indicate which data. True value selects item. False value filters out data. * * @param data * @param selectors */ export function* compress<T>( data: Iterable<T> | Iterator<T>, selectors: Iterable<number | boolean> | Iterator<number | boolean> ): Iterable<T> { for (const [datum, selector] of zip(data, selectors)) { if (selector) { yield datum as T; } } } /** * Compress an async iterable by filtering out data that is not selected. * * Selectors indicate which data. True value selects item. False value filters out data. * * Selectors may be also async collection. * * @param data * @param selectors */ export async function* compressAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, selectors: | AsyncIterable<number | boolean> | AsyncIterator<number | boolean> | Iterable<number | boolean> | Iterator<number | boolean> ): AsyncIterable<T> { for await (const [datum, selector] of zipAsync(data, selectors)) { if (selector) { yield datum as T; } } } /** * Drop elements from the iterable while the predicate function is true. * * Once the predicate function returns false once, all remaining elements are returned. * * @param data * @param predicate */ export function* dropWhile<T>( data: Iterable<T> | Iterator<T>, predicate: (item: T) => boolean ): Iterable<T> { let drop = true; for (const datum of toIterable(data)) { if (drop) { if (!predicate(datum)) { drop = false; yield datum; continue; } continue; } yield datum; } } /** * Drop elements from the async iterable while the predicate function is true. * * Once the predicate function returns false once, all remaining elements are returned. * * Predicate may be also async. * * @param data * @param predicate */ export async function* dropWhileAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, predicate: (item: T) => boolean | Promise<boolean> ): AsyncIterable<T> { let drop = true; for await (const datum of toAsyncIterable(data)) { if (drop) { if (!(await predicate(datum))) { drop = false; yield datum; continue; } continue; } yield datum; } } /** * Return elements from the iterable as long as the predicate is true. * * If no predicate is provided, the boolean value of the data is used. * * @param data * @param predicate */ export function* takeWhile<T>( data: Iterable<T> | Iterator<T>, predicate: (item: T) => boolean ): Iterable<T> { for (const datum of toIterable(data)) { if (predicate(datum)) { yield datum; } else { break; } } } /** * Return elements from the async iterable as long as the predicate is true. * * Predicate may be also async. * * If no predicate is provided, the boolean value of the data is used. * * @param data * @param predicate */ export async function* takeWhileAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, predicate: (item: T) => boolean | Promise<boolean> ): AsyncIterable<T> { for await (const datum of toAsyncIterable(data)) { if (await predicate(datum)) { yield datum; } else { break; } } } /** * Repeat an item. * * @param item * @param repetitions */ export function* repeat<T>(item: T, repetitions: number): Iterable<T> { if (repetitions < 0) { throw new InvalidArgumentError( `Number of repetitions cannot be negative: ${repetitions}` ); } for (let i = repetitions; i > 0; --i) { yield item; } } /** * Repeat an item given as promise. * * @param item * @param repetitions */ export async function* repeatAsync<T>( item: T | Promise<T>, repetitions: number ): AsyncIterable<T> { if (repetitions < 0) { throw new InvalidArgumentError( `Number of repetitions cannot be negative: ${repetitions}` ); } const value = await item; for (let i = repetitions; i > 0; --i) { yield value; } } /** * Returns a new collection formed by applying a given callback mapper function to each element * of the given collection, and then flattening the result by one level. * * The mapper function can return scalar or collections as a result. * * @param data * @param mapper */ export function* flatMap<TInput, TOutput>( data: Iterable<TInput> | Iterator<TInput>, mapper: FlatMapper<TInput, TOutput> ): Iterable<TOutput> { for (const datum of toIterable<TInput>(data)) { const unflattened = mapper(datum, mapper); if (isIterable(unflattened)) { for (const flattenedItem of toIterable( unflattened as Iterable<TOutput> | Iterator<TOutput> )) { yield flattenedItem; } } else { yield unflattened as TOutput; } } } /** * Returns a new async collection formed by applying a given callback mapper function to each element * of the given async collection, and then flattening the result by one level. * * The mapper function can return scalar or collections as a result. * * The mapper function may be also async. * * @param data * @param mapper */ export async function* flatMapAsync<TInput, TOutput>( data: | AsyncIterable<TInput> | AsyncIterator<TInput> | Iterable<TInput> | Iterator<TInput>, mapper: AsyncFlatMapper<TInput, TOutput> ): AsyncIterable<TOutput> { for await (const datum of toAsyncIterable<TInput>(data)) { const unflattened = await mapper(datum, mapper); if (isIterable(unflattened) || isAsyncIterable(unflattened)) { for await (const flattenedItem of toAsyncIterable( unflattened as | AsyncIterable<TOutput> | AsyncIterator<TOutput> | Iterable<TOutput> | Iterator<TOutput> )) { yield flattenedItem; } } else { yield unflattened as TOutput; } } } /** * Flatten an iterable by a number of dimensions. * * Ex: [[1, 2], [3, 4], 5] => [1, 2, 3, 4, 5] // Flattened by one dimension * * @param data * @param dimensions */ export function* flatten( data: Iterable<unknown> | Iterator<unknown>, dimensions = Infinity ): Iterable<unknown> { if (dimensions < 1) { for (let datum of toIterable(data)) { if (data instanceof Map) { datum = (datum as [unknown, unknown])[1]; } yield datum; } return; } for (let datum of toIterable(data)) { if (data instanceof Map) { datum = (datum as [unknown, unknown])[1]; } if ((isIterable(datum) || isIterator(datum)) && !isString(datum)) { for (const subDatum of flatten( datum as Iterable<unknown> | Iterator<unknown>, dimensions - 1 )) { yield subDatum; } } else { yield datum; } } } /** * Flatten an async iterable by a number of dimensions. * * Ex: [[1, 2], [3, 4], 5] => [1, 2, 3, 4, 5] // Flattened by one dimension * * @param data * @param dimensions */ export async function* flattenAsync( data: | AsyncIterable<unknown> | AsyncIterator<unknown> | Iterable<unknown> | Iterator<unknown>, dimensions = Infinity ): AsyncIterable<unknown> { if (dimensions < 1) { for await (let datum of toAsyncIterable(data)) { if (data instanceof Map) { datum = (datum as [unknown, unknown])[1]; } yield datum; } return; } for await (let datum of toAsyncIterable(data)) { if (data instanceof Map) { datum = (datum as [unknown, unknown])[1]; } if ((isAsyncIterable(datum) || isIterable(datum) || isIterator(datum)) && !isString(datum)) { for await (const subDatum of flattenAsync( datum as Iterable<unknown> | Iterator<unknown>, dimensions - 1 )) { yield subDatum; } } else { yield datum; } } } /** * Filter out elements from the iterable only returning elements where there predicate function is true. * * @param data * @param predicate */ export function* filter<T>( data: Iterable<T> | Iterator<T>, predicate: (datum: T) => boolean ): Iterable<T> { for (const datum of toIterable(data)) { if (predicate(datum)) { yield datum; } } } /** * Filter out elements from the async iterable only returning elements where there predicate function is true. * * Predicate may be also async. * * @param data * @param predicate */ export async function* filterAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, predicate: (datum: T) => boolean | Promise<boolean> ): AsyncIterable<T> { for await (const datum of toAsyncIterable(data)) { if (await predicate(datum)) { yield datum; } } } /** * Return overlapped chunks of elements from given collection. * * Chunk size must be at least 1. * * Overlap size must be less than chunk size. * * @param data * @param chunkSize * @param overlapSize * @param includeIncompleteTail */ export function* chunkwiseOverlap<T>( data: Iterable<T> | Iterator<T>, chunkSize: number, overlapSize: number, includeIncompleteTail = true ): Iterable<Array<T>> { if (chunkSize < 1) { throw new InvalidArgumentError(`Chunk size must be ≥ 1. Got ${chunkSize}`); } if (overlapSize >= chunkSize) { throw new InvalidArgumentError("Overlap size must be less than chunk size"); } let chunk: Array<T> = []; let isLastIterationYielded = false; for (const datum of toIterable(data)) { isLastIterationYielded = false; chunk.push(datum); if (chunk.length === chunkSize) { yield chunk; chunk = chunk.slice(chunkSize - overlapSize); isLastIterationYielded = true; } } if (!isLastIterationYielded && chunk.length > 0 && includeIncompleteTail) { yield chunk; } } /** * Return overlapped chunks of elements from given async collection. * * Chunk size must be at least 1. * * Overlap size must be less than chunk size. * * @param data * @param chunkSize * @param overlapSize * @param includeIncompleteTail */ export async function* chunkwiseOverlapAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, chunkSize: number, overlapSize: number, includeIncompleteTail = true ): AsyncIterable<Array<T>> { if (chunkSize < 1) { throw new InvalidArgumentError(`Chunk size must be ≥ 1. Got ${chunkSize}`); } if (overlapSize >= chunkSize) { throw new InvalidArgumentError("Overlap size must be less than chunk size"); } let chunk: Array<T> = []; let isLastIterationYielded = false; for await (const datum of toAsyncIterable(data)) { isLastIterationYielded = false; chunk.push(datum); if (chunk.length === chunkSize) { yield chunk; chunk = chunk.slice(chunkSize - overlapSize); isLastIterationYielded = true; } } if (!isLastIterationYielded && chunk.length > 0 && includeIncompleteTail) { yield chunk; } } /** * Return chunks of elements from given collection. * * Chunk size must be at least 1. * * @param data * @param chunkSize */ export function* chunkwise<T>( data: Iterable<T> | Iterator<T>, chunkSize: number ): Iterable<Array<T>> { for (const chunk of chunkwiseOverlap(data, chunkSize, 0)) { yield chunk; } } /** * Return chunks of elements from given async collection. * * Chunk size must be at least 1. * * @param data * @param chunkSize */ export async function* chunkwiseAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, chunkSize: number ): AsyncIterable<Array<T>> { for await (const chunk of chunkwiseOverlapAsync(data, chunkSize, 0)) { yield chunk; } } /** * Return pairs of elements from given collection. * * Returns empty generator if given collection contains less than 2 elements. * * @param data */ export function* pairwise<T>( data: Iterable<T> | Iterator<T> ): Iterable<Pair<T>> { const chunked = chunkwiseOverlap(data, 2, 1, false); for (const chunk of chunked) { yield chunk as Pair<T>; } } /** * Return pairs of elements from given async collection. * * Returns empty generator if given collection contains less than 2 elements. * * @param data */ export async function* pairwiseAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T> ): AsyncIterable<Pair<T>> { const chunked = chunkwiseOverlapAsync(data, 2, 1, false); for await (const chunk of chunked) { yield chunk as Pair<T>; } } /** * Limit iteration to a max size limit. * * @param data * @param count ≥ 0, max count of iteration */ export function* limit<T>( data: Iterable<T> | Iterator<T>, count: number ): Iterable<T> { if (count < 0) { throw new InvalidArgumentError(`Limit must be ≥ 0. Got ${count}`); } let i = 0; for (const datum of toIterable(data)) { if (i >= count) { return; } yield datum; ++i; } } /** * Limit iteration of async iterable to a max size limit. * * @param data * @param count ≥ 0, max count of iteration */ export async function* limitAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, count: number ): AsyncIterable<T> { if (count < 0) { throw new InvalidArgumentError(`Limit must be ≥ 0. Got ${count}`); } let i = 0; for await (const datum of toAsyncIterable(data)) { if (i >= count) { return; } yield datum; ++i; } } /** * Enumerates items of given collection. * * Ex: ['a', 'b', 'c'] => [[0, 'a'], [1, 'b'], [2, 'c']] * * @param data */ export function* enumerate<T>( data: Iterable<T> | Iterator<T> ): Iterable<[number, T]> { let i = 0; for (const datum of toIterable(data)) { yield [i++, datum]; } } /** * Enumerates items of given async collection. * * Ex: ['a', 'b', 'c'] => [[0, 'a'], [1, 'b'], [2, 'c']] * * @param data */ export async function* enumerateAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T> ): AsyncIterable<[number, T]> { let i = 0; for await (const datum of toAsyncIterable(data)) { yield [i++, datum]; } } /** * Extract a slice of the collection. * * @param data * @param start * @param count * @param step * * @throws InvalidArgumentError if `start` or `count` are negative or if `step` is not positive. */ export function* slice<T>( data: Iterable<T> | Iterator<T>, start = 0, count?: number, step = 1 ): Iterable<T> { if (start < 0) { throw new InvalidArgumentError("Parameter 'start' cannot be negative"); } if (count !== undefined && count < 0) { throw new InvalidArgumentError("Parameter 'count' cannot be negative"); } if (step <= 0) { throw new InvalidArgumentError("Parameter 'step' must be positive"); } let index = 0; let yielded = 0; for (const datum of toIterable(data)) { if (index++ < start || (index - start - 1) % step !== 0) { continue; } if (yielded++ === count && count !== undefined) { break; } yield datum; } } /** * Extract a slice of the async collection. * * @param data * @param start * @param count * @param step * * @throws InvalidArgumentError if `start` or `count` are negative or if `step` is not positive. */ export async function* sliceAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, start = 0, count?: number, step = 1 ): AsyncIterable<T> { if (start < 0) { throw new InvalidArgumentError("Parameter 'start' cannot be negative"); } if (count !== undefined && count < 0) { throw new InvalidArgumentError("Parameter 'count' cannot be negative"); } if (step <= 0) { throw new InvalidArgumentError("Parameter 'step' must be positive"); } let index = 0; let yielded = 0; for await (const datum of toAsyncIterable(data)) { if (index++ < start || (index - start - 1) % step !== 0) { continue; } if (yielded++ === count && count !== undefined) { break; } yield datum; } } /** * Iterates keys from the collection of key-value pairs. * * Ex: [[0, 'a'], [1, 'b'], [2, 'c']] => [0, 1, 2] * * @param collection */ export function* keys<TKey, TValue>( collection: Iterable<[TKey, TValue]> | Iterator<[TKey, TValue]> ): Iterable<TKey> { for (const [key] of toIterable(collection)) { yield key; } } /** * Iterates keys from the async collection of key-value pairs. * * Ex: [[0, 'a'], [1, 'b'], [2, 'c']] => [0, 1, 2] * * @param collection */ export async function* keysAsync<TKey, TValue>( collection: | AsyncIterable<[TKey, TValue]> | AsyncIterator<[TKey, TValue]> | Iterable<[TKey, TValue]> | Iterator<[TKey, TValue]> ): AsyncIterable<TKey> { for await (const [key] of toAsyncIterable(collection)) { yield key; } } /** * Skip n elements in the iterable after optional offset. * * @param data * @param count * @param offset * * @throws InvalidArgumentError if `count` or `offset` is less then 0 */ export function* skip<T>( data: Iterable<T> | Iterator<T>, count: number, offset = 0 ): Iterable<T> { if (count < 0 || offset < 0) { throw new InvalidArgumentError(); } let skipped = -offset; for (const datum of toIterable(data)) { if (skipped < 0 || skipped >= count) { yield datum; } ++skipped; } } /** * Skip n elements in the async iterable after optional offset. * * @param data * @param count * @param offset * * @throws InvalidArgumentError if `count` or `offset` is less then 0 */ export async function* skipAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, count: number, offset = 0 ): AsyncIterable<T> { if (count < 0 || offset < 0) { throw new InvalidArgumentError(); } let skipped = -offset; for await (const datum of toAsyncIterable(data)) { if (skipped < 0 || skipped >= count) { yield datum; } ++skipped; } } /** * Iterates values from the collection of key-value pairs. * * Ex: [[0, 'a'], [1, 'b'], [2, 'c']] => ['a', 'b', 'c'] * * @param collection */ export function* values<TKey, TValue>( collection: Iterable<[TKey, TValue]> | Iterator<[TKey, TValue]> ): Iterable<TValue> { for (const [, value] of toIterable(collection)) { yield value; } } /** * Iterates values from the async collection of key-value pairs. * * Ex: [[0, 'a'], [1, 'b'], [2, 'c']] => ['a', 'b', 'c'] * * @param collection */ export async function* valuesAsync<TKey, TValue>( collection: | AsyncIterable<[TKey, TValue]> | AsyncIterator<[TKey, TValue]> | Iterable<[TKey, TValue]> | Iterator<[TKey, TValue]> ): AsyncIterable<TValue> { for await (const [, value] of toAsyncIterable(collection)) { yield value; } } /** * Group data by a common data element. * * Iterate pairs of group name and collection of grouped items. * * Collection of grouped items may be an array or an object (depends on presence of `itemKeyFunction` param). * * @param data * @param groupKeyFunction - determines the key (or multiple keys) to group elements by. * @param itemKeyFunction - (optional) determines the key of element in group. */ export function* groupBy< T, TItemKeyFunction extends ((item: T) => string) | undefined, TResultItem extends TItemKeyFunction extends undefined ? [string, Array<T>] : [string, Record<string, T>] >( data: Iterable<T> | Iterator<T>, groupKeyFunction: (item: T) => string, itemKeyFunction?: TItemKeyFunction ): Iterable<TResultItem> { const groups = new Map(); const addGroup = (name: string) => { if (!groups.has(name)) { if (itemKeyFunction !== undefined) { groups.set(name, {}); } else { groups.set(name, []); } } }; for (const item of toIterable(data)) { const group = groupKeyFunction(item); const itemKey = itemKeyFunction !== undefined ? itemKeyFunction(item) : undefined; const itemGroups = (isIterable(group) || isIterator(group)) && !isString(group) ? group : [group]; for (const itemGroup of distinct(itemGroups)) { addGroup(itemGroup); if (itemKey === undefined) { groups.get(itemGroup).push(item); } else { groups.get(itemGroup)[itemKey] = item; } } } for (const group of groups) { yield group as TResultItem; } } /** * Group async data by a common data element. * * Iterate pairs of group name and collection of grouped items. * * Collection of grouped items may be an array or an object (depends on presence of `itemKeyFunction` param). * * Functions `groupKeyFunction` and `itemKeyFunction` may be async. * * @param data * @param groupKeyFunction - determines the key (or multiple keys) to group elements by. * @param itemKeyFunction - (optional) determines the key of element in group. */ export async function* groupByAsync< T, TItemKeyFunction extends ((item: T) => string) | undefined, TResultItem extends TItemKeyFunction extends undefined ? [string, Array<T>] : [string, Record<string, T>] >( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, groupKeyFunction: (item: T) => (string | Promise<string>), itemKeyFunction?: (item: T) => (string | Promise<string>) ): AsyncIterable<TResultItem> { const groups = new Map(); const addGroup = (name: string) => { if (!groups.has(name)) { if (itemKeyFunction !== undefined) { groups.set(name, {}); } else { groups.set(name, []); } } }; for await (const item of toAsyncIterable(data)) { const group = await groupKeyFunction(item); const itemKey = itemKeyFunction !== undefined ? await itemKeyFunction(item) : undefined; const itemGroups = (isAsyncIterable(group) || isIterable(group) || isIterator(group)) && !isString(group) ? group : [group]; for await (const itemGroup of distinctAsync(itemGroups)) { addGroup(itemGroup); if (itemKey === undefined) { groups.get(itemGroup).push(item); } else { groups.get(itemGroup)[itemKey] = item; } } } for (const group of groups) { yield group as TResultItem; } } /** * Sorts the given collection. * * If comparator is null, the elements of given iterable must be comparable. * * @param data * @param comparator */ export function* sort<T>( data: Iterable<T> | Iterator<T>, comparator?: Comparator<T> ): Iterable<T> { const result = toArray(data); if (comparator !== undefined) { result.sort(comparator); } else { result.sort(); } for (const datum of result) { yield datum; } } /** * Sorts the given collection. * * If comparator is null, the elements of given iterable must be comparable. * * @param data * @param comparator */ export async function* sortAsync<T>( data: AsyncIterable<T> | AsyncIterator<T> | Iterable<T> | Iterator<T>, comparator?: Comparator<T> ): AsyncIterable<T> { const result = await toArrayAsync(data); if (comparator !== undefined) { result.sort(comparator); } else { result.sort(); } for (const datum of result) { yield datum; } }