UNPKG

doddle

Version:

Tiny yet feature-packed (async) iteration toolkit.

1,364 lines (1,311 loc) 59.8 kB
import type { Doddle, DoddleAsync } from "../doddle/index.js" import { doddle, lazyOperator, pull } from "../doddle/index.js" import { DoddleError, chk, loadCheckers, reduceOnEmptyError } from "../errors/error.js" import type { DoddleReadableStream } from "../readable-stream-polyfill.js" import { Stage, _aiter, createCompareKey, orderedStages, shuffleArray, type MaybeDoddleAsync, type MaybePromise } from "../utils.js" import { aseq } from "./aseq.ctor.js" import { SkippingMode, type EachCallStage, type Get_Concat_Element_Type, type SkipWhileOptions, type TakeWhileOptions, type getWindowArgsType, type getWindowOutputType, type getZipValuesType } from "./common-types.js" import { Seq } from "./seq.class.js" import { seq } from "./seq.ctor.js" const SPECIAL = Symbol("special") /** * The ASeq class, which wraps an async iterable. * * @category Use */ export abstract class ASeq<T> implements AsyncIterable<T> { /** {@link concatMap} */ flatMap = this.concatMap /** @internal */ constructor() { // Class name is used for various checks // Need to make sure it's accessible even while minified loadCheckers(ASeq.prototype) } /** @internal */ get [Symbol.toStringTag]() { return "ASeq" } /** @internal */ get _qr() { return this.toArray().pull() } /** * Calls a side-effect function after all elements have been yielded, but before iteration * finishes. * * ⚠️ If the client stops iterating early, the function won't be called. * * @param action A function to invoke after iteration completes. * @returns A new sequence that acts like `this` but invokes `action` before it finishes. */ after(action: ASeq.NoInputAction): ASeq<T> { chk(this.after).action(action) return ASeqOperator(this, async function* after(input) { yield* input await pull(action()) }) } /** * Reinterprets the declared element type of `this` as another, arbitrary type. * * ℹ️ This is only useful in TypeScript and has no runtime effects. * * @template S The new element type. * @returns The same sequence, but with a different declared type. */ as<S>() { return this as any as ASeq<S> } /** * 🦥**Lazily** gets the element at the given index in `this` sequence, or undefined if the * index is out of bounds. * * ℹ️ Negative indexes count from the end of the sequence.\ * ⚠️ Requires iterating over the sequence up to the given index. * * @param index The index of the item to retrieve. * @returns A 🦥{@link DoddleAsync} that resolves to the item at the given index. */ at(index: number): DoddleAsync<T | undefined> { chk(this.at).index(index) return lazyOperator(this, async function at(input) { if (index < 0) { return input.take(index).first().pull() } return input.skip(index).first().pull() as any }) } /** * Executes a side effect action once before any elements are yielded, but after iteration has * begun. * * @param action Invokes before any elements are yielded. * @returns A new async sequence that performs `action` before yielding elements. */ before(action: ASeq.NoInputAction): ASeq<T> { chk(this.before).action(action) return ASeqOperator(this, async function* before(input) { await pull(action()) yield* input }) as any } /** * Caches the elements of `this` sequence as they're iterated over, so that it's evaluated only * once. * * @returns A new sequence with the same elements as the original sequence. */ cache(): ASeq<T> { const self = this const _cache: T[] = [] let alreadyDone = false let iterator: AsyncIterator<T> let pending: Promise<void> | undefined return ASeqOperator(this, async function* cache() { let i = 0 for (;;) { if (i < _cache.length) { const cur = _cache[i] yield cur i++ } else if (!alreadyDone) { iterator ??= _aiter(self) if (!pending) { pending = (async () => { const { done, value } = await iterator.next() if (done) { alreadyDone = true return } _cache.push(value) pending = undefined return })() } await pending } else { return } } }) as any } /** * Handles errors thrown while iterating over `this` sequence. * * @param handler A handler that will be called with the error and the index of the element that * caused it. Should return a new sequence or `undefined`, which stops iteration. * @returns A new sequence that handles errors. */ catch<S = T>(handler: ASeq.Iteratee<unknown, ASeq.Input<S> | void>): ASeq<T | S> catch<S>( handler: ASeq.Iteratee<unknown, void | Promise<void> | ASeq.SimpleInput<S>> ): ASeq<any> { chk(this.catch).handler(handler) return ASeqOperator(this, async function* catch_(input) { let i = 0 const iterator = _aiter(input) for (;;) { try { const result = await iterator.next() var value = result.value if (result.done) { return } yield value } catch (err: any) { const error = err const result = await pull(handler(error, i)) if (!result) { return } const pulled = pull(result as any) yield* aseq(pulled) return } i++ } }) } /** * Splits `this` sequence into chunks of the given size, optionally applying a projection to * each chunk. * * ℹ️ The last chunk may be smaller than the given size. * * @param size The size of each chunk. * @param projection Optionally, an N-ary projection to apply to each chunk. Defaults to * collecting the elements into an array. * @returns A new sequence. */ chunk<L extends number, S>( size: L, projection: (...window: getWindowArgsType<T, L>) => Doddle.MaybePromised<S> ): ASeq<S> /** * Splits `this` sequence into chunks of the given size,. * * ℹ️ The last chunk may be smaller than the given size. * * @param size The size of each chunk. * @returns A new sequence. */ chunk<L extends number>(size: L): ASeq<getWindowOutputType<T, L>> /** * Splits `this` async sequence into chunks of the given size, optionally applying a projection * to each chunk. * * @param size The size of each chunk. The last chunk may be smaller. * @param projection Optionally, an N-ary projection to apply to each chunk. * @returns A new async sequence of chunks, each containing consecutive elements from the * original. */ chunk<L extends number, S>( size: L, projection?: (...window: getWindowArgsType<T, L>) => S ): ASeq<getWindowOutputType<T, L>> { const c = chk(this.chunk) c.size(size) projection ??= (...chunk: any) => chunk as any c.projection(projection) return ASeqOperator(this, async function* chunk(input) { let chunk: T[] = [] for await (const item of input) { chunk.push(item) if (chunk.length === size) { yield pull(projection(...(chunk as any))) chunk = [] } } if (chunk.length) { yield pull(projection(...(chunk as any))) } }) as any } /** * Returns a new sequence. When iterated, before yielding its first element, it will iterate * over all the elements of `this` and store them in memory. Then it will yield all of them one * by one. * * ℹ️ Used to control side-effects. Makes sure all side-effects execute before continuing to * apply other operators. * * @returns A new sequence with the same elements as this one, but where iteration has already * completed. */ collect(): ASeq<T> { return ASeqOperator(this, async function* collect(input) { const everything: T[] = [] for await (const element of input) { everything.push(element) } yield* everything }) } /** * Concatenates one or more sequences to the end of `this`, so that their elements appear in * order. * * @param _inputs The sequential inputs to concatenate to the end of `this`. * @returns A new sequence with the concatenated elements. */ concat<ASeqs extends ASeq.SimpleInput<any>[]>( ..._inputs: ASeqs ): ASeq<T | ASeq.ElementOfInput<ASeqs[number]>> { const inputs = _inputs.map(aseq) return ASeqOperator(this, async function* concat(input) { for await (const element of input) { yield element } for (const iterable of inputs) { for await (const element of iterable) { yield element } } }) as any } /** * Applies a sequence projection on each element of `this` sequence and concatenates the * resulting sequences. * * @param projection The sequence projection to apply to each element. * @returns A new sequence with the flattened results. */ concatMap<S>( projection: ASeq.Iteratee<T, ASeq.SimpleInput<S>> ): ASeq<Get_Concat_Element_Type<T, S>> { chk(this.concatMap).projection(projection) return ASeqOperator(this, async function* concatMap(input) { let index = 0 for await (const element of input) { for await (const projected of aseq(await pull(projection(element, index++)))) { yield pull(projected) } } }) as any } /** * Concatenates `this` sequence to the end of one or more other sequences. * * ℹ️ Input sequences are concatenated in the order that they appear. * * @param inputs One or more other sequences. * @returns A new sequence with the concatenated elements. * @see {@link ASeq.concat} */ concatTo<Seqs extends ASeq.Input<any>[]>( ...others: Seqs ): ASeq<T | ASeq.ElementOfInput<Seqs[number]>> { return aseq([]).concat(...others, this) as any } /** * 🦥**Lazily** counts the number of elements in `this` sequence. * * ⚠️ Requires iterating over the entire sequence. * * @returns A 🦥{@link DoddleAsync} that resolves to the number of elements in `this`. */ count(): DoddleAsync<number> /** * 🦥**Lazily** counts the number of elements in `this` sequence that match the given predicate. * * ⚠️ Requires iterating over the entire sequence. * * @param predicate The predicate used to test each element. * @returns A 🦥{@link DoddleAsync} that resolves to the number of matching elements. */ count(predicate: ASeq.Predicate<T>): DoddleAsync<number> count(predicate?: ASeq.Predicate<T>): DoddleAsync<number> { predicate ??= () => true predicate = chk(this.count).predicate(predicate) return lazyOperator(this, async function count(input) { let index = 0 let count = 0 for await (const element of input) { if (await pull(predicate(element, index++))) { count++ } } return count }) } /** * Calls an action function as each element in `this` is iterated over. Calls the function * before or after yielding the element, or both. * * @param action The action function to invoke for each element. * @param stage The **stage** at which to invoke the function. Can be `"before"`, `"after"`, or * `"both"`. * @returns A new sequence that invokes the action function while being iterated. */ each(action: ASeq.StageIteratee<T, unknown>, stage: EachCallStage = "before") { const c = chk(this.each) c.action(action) c.stage(stage) const myStage = orderedStages.indexOf(stage) return ASeqOperator(this, async function* each(input) { let index = 0 for await (const element of input) { if (myStage & Stage.Before) { await (myStage & Stage.Before && pull(action(element, index, "before"))) } yield element if (myStage & Stage.After) { await (myStage & Stage.After && pull(action(element, index, "after"))) } index++ } }) } /** * 🦥**Lazily** checks if all elements in `this` sequence match the given predicate. * * ⚠️ May iterate over the entire sequence. * * @param predicate The predicate. * @returns A 🦥{@link DoddleAsync} that yields `true` if all elements match, or `false` * otherwise. */ every(predicate: ASeq.Predicate<T>): DoddleAsync<boolean> { predicate = chk(this.every).predicate(predicate) return lazyOperator(this, async function every(input) { let index = 0 for await (const element of input) { if (!(await pull(predicate(element, index++)))) { return false } } return true }) } /** * Filters the elements of `this` sequence based on the given type predicate, narrowing the type * of the elements in the resulting sequence. * * @param predicate The predicate to filter elements. * @returns A new sequence with the filtered elements, its type narrowed based on the predicate. */ filter<S extends T>(predicate: Seq.TypePredicate<T, S>): ASeq<S> /** * Filters the elements of `this` sequence based on the given predicate. * * @param predicate The predicate to filter elements. * @returns A new sequence with the filtered elements. */ filter(predicate: ASeq.Predicate<T>): ASeq<T> filter(predicate: ASeq.Predicate<T>) { predicate = chk(this.filter).predicate(predicate) return ASeqOperator(this, async function* filter(input) { let index = 0 for await (const element of input) { if (await pull(predicate(element, index++))) { yield element } } }) } /** * 🦥**Lazily** finds the first element in `this` sequence, or `undefined` if it's empty. * * @returns A 🦥{@link DoddleAsync} that resolves to the first element or the alternative value. */ first(): DoddleAsync<T | undefined> /** * 🦥**Lazily** finds the first element in `this` sequence that matches the given predicate. * * ⚠️ May iterate over the entire sequence. * * @param predicate The predicate used to find the element. * @param alt The value to return if no element matches the predicate. Defaults to `undefined`. */ first<const Alt = undefined>(predicate: ASeq.Predicate<T>, alt?: Alt): DoddleAsync<T | Alt> first<Alt = T>(predicate?: ASeq.Predicate<T>, alt?: Alt) { predicate = predicate || (() => true) chk(this.first).predicate(predicate) return lazyOperator(this, async function first(input) { let index = 0 for await (const element of input) { if (await pull(predicate(element, index++))) { return element as T | Alt } } return alt as T | Alt }) } /** * Groups the elements of `this` sequence by key, resulting in a sequence of pairs where the * first element is the key and the second is a sequence of values. * * @param keyProjection The projection used to determine the key for each element. * @returns A sequence of pairs. */ groupBy<K>(keyProjection: ASeq.NoIndexIteratee<T, K>): ASeq<ASeq.Group<K, T>> { chk(this.groupBy).keyProjection(keyProjection) return ASeqOperator(this, async function* groupBy(input) { const map = new Map<K, [T, ...T[]]>() const keys: K[] = [] const shared = input .map(async v => { const key = (await pull(keyProjection(v))) as K const group = map.get(key) if (group) { group.push(v) } else { keys.push(key) map.set(key, [v]) } }) .share() async function* getGroupIterable(key: K): AsyncIterable<T> { const group = map.get(key)! for (let i = 0; ; i++) { if (i < group.length) { yield group[i] continue } for await (const _ of shared) { if (i < group.length) break } if (i >= group.length) return i-- } } for (let i = 0; ; i++) { if (i < keys.length) { const key = keys[i] yield [key, aseq(() => getGroupIterable(key))] continue } for await (const _ of shared) { if (i < keys.length) break } if (i >= keys.length) return i-- } }) } /** * 🦥**Lazily** checks if `this` sequence includes one or more values by iterating over it. * * ⚠️ May iterate over the entire sequence. * * @param values The values to check for inclusion. */ includes<T extends S, S>(this: ASeq<T>, ...values: S[]): DoddleAsync<boolean> /** * 🦥**Lazily** checks if `this` sequence includes one or more values. * * ⚠️ May iterate over the entire sequence. * * @param values The values to check for inclusion. */ includes<S extends T>(...values: S[]): DoddleAsync<boolean> includes<S extends T>(..._values: S[]): DoddleAsync<boolean> { const values = new Set(_values) return lazyOperator(this, async function includes(input) { for await (const element of input) { values.delete(element as S) if (values.size === 0) { return true } } return false }) } /** * 🦥**Lazily** joins the elements of `this` sequence into a single string, separated by the * given separator. * * ⚠️ Requires iterating over the entire sequence. * * @param separator The string to use as a separator between elements. * @returns A 🦥{@link DoddleAsync} that resolves to the joined string. */ join(separator = ","): DoddleAsync<string> { chk(this.join).separator(separator) return lazyOperator(this, async function join(input) { const results = [] for await (const x of input) { results.push(x) } return results.join(separator) }) } /** * 🦥**Lazily** gets the last element in `this` sequence, or `undefined`. * * @returns A 🦥{@link DoddleAsync} that resolves to the last element in `this` sequence, or * `undefined`. */ last(): DoddleAsync<T | undefined> /** * 🦥**Lazily** finds the last element in `this` sequence that matches the given predicate. * * ⚠️ Requires iterating over the entire sequence. * * @param predicate The predicate for testing each element. * @param alt Optionally, the value to return if no matching value is found. Defaults to * `undefined`. * @returns A 🦥{@link DoddleAsync} that resolves to the last matching element or the alternative * value. */ last<const Alt = undefined>(predicate: ASeq.Predicate<T>, alt?: Alt): DoddleAsync<T | Alt> last<Alt = undefined>(predicate?: ASeq.Predicate<T>, alt?: Alt) { predicate ??= () => true chk(this.last).predicate(predicate) return lazyOperator(this, async function last(input) { let last: T | Alt = alt as Alt let index = 0 for await (const element of input) { if (await pull(predicate(element, index++))) { last = element } } return last as T | Alt }) } /** * Applies a projection to each element of `this` sequence. * * @param projection The projection to apply to each element. * @returns A new sequence with the projected elements. */ map<S>(projection: ASeq.Iteratee<T, S>): ASeq<S> { chk(this.map).projection(projection) return ASeqOperator(this, async function* map(input) { let index = 0 for await (const element of input) { yield pull(projection(element, index++)) as any } }) } /** * 🦥**Lazily** finds the maximum element in `this` sequence by key, or the given alternative * value if the sequence is empty. * * @param projection Projects each element into a key so it can be compared. * @param alt The value to return if the sequence is empty. Defaults to `undefined`. */ maxBy<K, const Alt = undefined>( projection: ASeq.Iteratee<T, K>, alt?: Alt ): DoddleAsync<T | Alt> { chk(this.maxBy).projection(projection) return lazyOperator(this, async function maxBy(input) { let curMax = alt as Alt | T let curMaxKey = undefined as K let index = 0 for await (const element of input) { const curKey = (await pull(projection(element, index++))) as K if (index === 1 || curKey > curMaxKey) { curMax = element curMaxKey = curKey continue } } return curMax }) } /** * 🦥**Lazily** finds the minimum element in `this` sequence by key, or the given alternative * value if the sequence is empty. * * @param projection Projects each element into a key so it can be compared. * @param alt The value to return if the sequence is empty. Defaults to `undefined`. * @returns A 🦥{@link DoddleAsync} that resolves to the element with the minimum key, or `alt` * if the sequence is empty. */ minBy<K, const Alt = undefined>( projection: ASeq.Iteratee<T, K>, alt?: Alt ): DoddleAsync<T | Alt> minBy<K>(projection: ASeq.Iteratee<T, K>, alt?: any) { chk(this.minBy).projection(projection) return lazyOperator(this, async function minBy(input) { let curMin = alt as any let curMinKey = undefined as K let index = 0 for await (const element of input) { const curKey = (await pull(projection(element, index++))) as K if (index === 1 || curKey < curMinKey) { curMin = element curMinKey = curKey continue } } return curMin }) } /** * Orders the elements of `this` sequence by key, using the given key projection. * * ⚠️ Has to iterate over the entire sequence. * * @param projection A projection that returns a key to order by. * @param descending Whether to use descending order. * @returns A new sequence with the elements ordered by the given key. */ orderBy<S>(projection: ASeq.NoIndexIteratee<T, S>, descending?: boolean): ASeq<T> /** * Orders the elements of `this` using the given mutli-key tuple projection. * * ℹ️ The keys are compared in the order they appear.\ * ⚠️ Has to iterate over the entire sequence. * * @param projection A projection function that returns a tuple of keys to order by. * @param descending Whether to use descending order. * @returns A new sequence with the elements ordered by the given keys. */ orderBy<K extends [unknown, ...unknown[]]>( projection: ASeq.NoIndexIteratee<T, K>, descending?: boolean ): ASeq<T> orderBy<S>(projection: ASeq.NoIndexIteratee<T, S>, descending = false): ASeq<T> { const c = chk(this.orderBy) c.projection(projection) c.descending(descending) const compareKey = createCompareKey(descending) return ASeqOperator(this, async function* orderBy(input) { const kvps = [] as [any, T][] for await (const element of input) { const key = (await pull(projection(element))) as any kvps.push([key, element]) } kvps.sort(compareKey) for (const [_, value] of kvps) { yield value } }) } /** * Returns a cartesian product of `this` sequence with one or more other sequences, optionally * applying an N-ary projection to each combination of elements. * * The product of `N` sequences is the collection of all possible sets of elements from each * sequence. * * For example, the product of `[1, 2]` and `[3, 4]` is: * * ```ts * ;[ * [1, 3], * [1, 4], * [2, 3], * [2, 4] * ] * ``` * * @example * aseq([1, 2]).product([3, 4]) * // => [[1, 3], [1, 4], [2, 3], [2, 4]] * aseq([]).product([3, 4]) * // => [] * aseq([1, 2]).product([3, 4], (a, b) => a + b) * // => [4, 5, 5, 6] * * @param others One or more sequence-like inputs for the product. * @param projection Optionally, an N-ary projection to apply to each combination of elements. * If not given, each combination is yielded as an array. * @returns A new sequence. */ product<Xs extends any[], R = [T, ...Xs]>( _others: { [K in keyof Xs]: ASeq.Input<Xs[K]> }, projection?: (...args: [T, ...Xs]) => R ): ASeq<R> { const others = _others.map(aseq).map(x => x.cache()) projection ??= (...args: any[]) => args as any chk(this.product).projection(projection) return ASeqOperator(this, async function* product(input) { let partialProducts = [[]] as any[][] for (const iterable of [input, ...others].reverse()) { const oldPartialProducts = partialProducts partialProducts = [] for await (const item of iterable) { partialProducts = partialProducts.concat( oldPartialProducts.map(x => [item, ...x]) ) } } yield* partialProducts.map(x => pull(projection.apply(null, x as any))) as any }) } /** * 🦥**Lazily** reduces `this` sequence to a single value by applying the given reduction. * * ℹ️ Uses the first element as the initial value. * * @param reduction The reduction function to apply to each element. * @returns A 🦥{@link DoddleAsync} that resolves to the reduced value. */ reduce(reducer: ASeq.Reduction<T, T>): DoddleAsync<T> /** * 🦥**Lazily** reduces `this` sequence to a single value by applying the given reduction. * * ℹ️ You need to supply an initial value. * * @param reducer The reduction to apply to each element. * @param initial The initial value to start the reduction with. */ reduce<Acc>(reducer: ASeq.Reduction<T, Acc>, initial: Acc): DoddleAsync<Acc> reduce<Acc>(reducer: ASeq.Reduction<T, Acc>, initial?: Acc): any { chk(this.reduce).reducer(reducer) return lazyOperator(this, async function reduce(input) { let acc = initial ?? (SPECIAL as any) let index = 0 for await (const element of input) { if (acc === SPECIAL) { acc = element continue } acc = (await pull(reducer(acc, element, index++))) as any } if (acc === SPECIAL) { throw new DoddleError(reduceOnEmptyError) } return acc }) as any } /** * Reverses `this` sequence. * * ⚠️ Requires iterating over the entire sequence. * * @returns A new sequence with the elements in reverse order. */ reverse(): ASeq<T> { return ASeqOperator(this, async function* reverse(input) { const elements: T[] = [] for await (const element of input) { elements.push(element) } yield* elements.reverse() }) } /** * Applies a reduction to each element of `this` sequence. Returns a new sequence that yields * the accumulated value at each step. * * ℹ️ The first element is used as the initial value. * * @param reduction The reduction function to apply. * @returns A new sequence with the accumulated values. * @throws If `this` is empty. */ scan(reducer: ASeq.Reduction<T, T>): ASeq<T> /** * Applies a reduction to each element of `this` sequence. Returns a new sequence that yields * the accumulated value at each step. * * ℹ️ You need to supply an initial value. * * @param reduction The reduction to apply. * @param initial The initial value to start the reduction with. */ scan<Acc>(reducer: ASeq.Reduction<T, Acc>, initial: Acc): ASeq<Acc> scan<Acc>(reducer: ASeq.Reduction<T, Acc>, initial?: Acc): ASeq<any> { chk(this.scan).reducer(reducer) return ASeqOperator(this, async function* scan(input) { let hasAcc = initial !== undefined let acc = initial as any let index = 0 if (hasAcc) { yield acc } for await (const element of input) { if (!hasAcc) { acc = element as any hasAcc = true } else { acc = (await pull(reducer(acc, element, index++))) as any } yield acc } }) } /** * 🦥**Lazily** checks if `this` sequence is equal to the `input` sequence. * * ℹ️ For two sequences to be equal, their elements must be equal and be in the same order. * * @param input The sequence-like input to compare with. * @param projection The projection function that determines the key for comparison. * @returns A 🦥{@link DoddleAsync} that resolves to `true` if all elements are equal, or `false` */ seqEquals<T extends S, S>(this: AsyncIterable<T>, input: ASeq.Input<S>): DoddleAsync<boolean> /** * 🦥**Lazily** checks if `this` sequence is equal to the `input` sequence. * * ℹ️ For two sequences to be equal, their elements must be equal and be in the same order. * * @param input The sequence-like input to compare with. * @param projection The projection function that determines the key for comparison. * @returns A 🦥{@link DoddleAsync} that resolves to `true` if all elements are equal, or `false` */ seqEquals<S extends T>(input: ASeq.Input<S>): DoddleAsync<boolean> /** * 🦥**Lazily** checks if `this` sequence is equal to the `input` sequence. * * The elements are compared by key, using the given key projection. * * @param input The sequential input to compare with. * @param projection The projection function that determines the key for comparison. * @returns A 🦥{@link DoddleAsync} that resolves to `true` if all elements are equal, or `false` * otherwise. */ seqEquals<K, S = T>( input: ASeq.Input<S>, projection: ASeq.NoIndexIteratee<S | T, K> ): DoddleAsync<boolean> seqEquals<K, S = T>( input: ASeq.Input<S>, projection: ASeq.NoIndexIteratee<S | T, K> = x => x as K ): DoddleAsync<boolean> { projection ??= x => x as K const other = aseq(input) return lazyOperator(this, async function seqEquals(input) { const otherIterator = _aiter(other) try { for await (const element of input) { const otherElement = await otherIterator.next() const keyThis = await pull(projection(element as any)) const keyOther = await pull(projection(otherElement.value)) if (otherElement.done || keyThis !== keyOther) { return false } } return !!(await otherIterator.next()).done } finally { await otherIterator.return?.() } }) } /** * 🦥**Lazily** checks if `this` sequence contains the same elements as the input sequence, * without regard to order. * * ⚠️ Requires iterating over the entire sequence. * * @param input The sequence-like input to compare with. * @returns A 🦥{@link DoddleAsync} that resolves to `true` if `this` is set-equal to the input, * or `false` otherwise. */ setEquals<S extends T>(input: ASeq.Input<S>): DoddleAsync<boolean> /** * 🦥**Lazily** checks if `this` sequence contains the same elements as the input sequence, * without regard to order. * * ⚠️ Requires iterating over the entire sequence. * * @param input The sequence-like input to compare with. * @returns A 🦥{@link DoddleAsync} that resolves to `true` if `this` is set-equal to the input, * or `false` otherwise. */ setEquals<T extends S, S>(this: AsyncIterable<T>, input: ASeq.Input<S>): DoddleAsync<boolean> /** * 🦥**Lazily** checks if `this` sequence contains the same elements as the input sequence, * without regard to order. * * ℹ️ The elements are compared by key, using the given key projection. * * @param input The sequence-like input to compare with. * @param projection The projection function that determines the key for comparison. * @returns A 🦥{@link DoddleAsync} that resolves to `true` if `this` is set-equal to the input, * or `false` otherwise. */ setEquals<K, S = T>( input: ASeq.Input<NoInfer<S>>, projection?: ASeq.NoIndexIteratee<S | T, K> ): DoddleAsync<boolean> setEquals<K, S = T>( _other: ASeq.Input<S>, projection: ASeq.NoIndexIteratee<S | T, K> = x => x as K ): DoddleAsync<boolean> { projection ??= x => x as K const other = aseq(_other) return lazyOperator(this, async function setEquals(input) { const set = new Set() for await (const element of other) { set.add(await pull(projection(element))) } for await (const element of input) { if (!set.delete(await pull(projection(element)))) { return false } } return set.size === 0 }) } /** * Returns a new sequence that shares its iterator state. This allows different loops to iterate * over it, sharing progress. * * ⚠️ Can be iterated over exactly once, and will be empty afterwards. * * @returns A new, shared iterable sequence that can be iterated over exactly once. */ share(): ASeq<T> { const iter = doddle(() => _aiter(this)) return ASeqOperator(this, async function* share() { while (true) { const { done, value } = await iter.pull().next() if (done) { return } yield value } }) } /** * Shuffles the elements of `this` sequence randomly. * * ⚠️ Requires iterating over the entire sequence. * * @returns A new sequence with the shuffled elements. */ shuffle(): ASeq<T> { return ASeqOperator(this, async function* shuffle(input) { const array = await aseq(input).toArray().pull() shuffleArray(array) yield* array }) } /** * Skips the first `count` elements of `this` sequence, yielding the rest. * * ℹ️ If `count` is negative, skips the final elements instead (e.g. `skipLast`) * * @param count The number of elements to skip. * @returns A new sequence without the skipped elements. */ skip(count: number): ASeq<T> { chk(this.skip).count(count) return ASeqOperator(this, async function* skip(input) { let myCount = count if (myCount < 0) { myCount = -myCount yield* aseq(input) .window(myCount + 1, (...window) => { if (window.length === myCount + 1) { return window[0] } return SPECIAL }) .filter(x => x !== SPECIAL) } else { for await (const x of input) { if (myCount > 0) { myCount-- continue } yield x } } }) as any } /** * Skips elements from `this` sequence while the given predicate is true, and yields the rest. * * ℹ️ You can use the `options` argument to skip the first element that returns `false`. * * @param predicate The predicate to determine whether to continue skipping. * @param options Options for skipping behavior. * @returns A new sequence without the skipped elements. */ skipWhile(predicate: ASeq.Predicate<T>, options?: SkipWhileOptions): ASeq<T> { predicate = chk(this.skipWhile).predicate(predicate) return ASeqOperator(this, async function* skipWhile(input) { let prevMode = SkippingMode.None as SkippingMode let index = 0 for await (const element of input) { if (prevMode === SkippingMode.NotSkipping) { yield element continue } const newSkipping: boolean = await pull(predicate(element, index++)) if (!newSkipping) { if (prevMode !== SkippingMode.Skipping || !options?.skipFinal) { yield element } } prevMode = newSkipping ? SkippingMode.Skipping : SkippingMode.NotSkipping } }) as any } /** * 🦥**Lazily** checks if any element in `this` sequence matches the given predicate, by * iterating over it until a match is found. * * @param predicate The predicate to match the element. * @returns A 🦥{@link DoddleAsync} that resolves to `true` if any element matches, or `false` * otherwise. */ some(predicate: ASeq.Predicate<T>): DoddleAsync<boolean> { predicate = chk(this.some).predicate(predicate) return lazyOperator(this, async function some(input) { let index = 0 for await (const element of input) { if (await pull(predicate(element, index++))) { return true } } return false }) } /** * 🦥**Lazily** sums the elements of `this` sequence by iterating over it, applying the given * projection to each element. * * @param projection The projection function to apply to each element. * @returns A 🦥{@link DoddleAsync} that resolves to the sum of the projected elements. */ sumBy(projection: ASeq.Iteratee<T, number>): DoddleAsync<number> { chk(this.sumBy).projection(projection) return lazyOperator(this, async function sumBy(input) { let cur = 0 let index = 0 for await (const element of input) { cur += (await pull(projection(element, index++))) as number } return cur }) } /** * Yields the first `count` elements of `this` sequence. * * ℹ️ If `count` is negative, yields the last `-count` elements instead.\ * ℹ️ If the sequence is smaller than `count`, it yields all elements. * * @param count The number of elements to yield. * @returns A new sequence with the yielded elements. */ take(count: number): ASeq<T> { chk(this.take).count(count) return ASeqOperator(this, async function* take(input) { let myCount = count if (myCount === 0) { return } if (myCount < 0) { myCount = -myCount const results = (await aseq(input) .concat([SPECIAL]) .window(myCount + 1, (...window) => { if (window[window.length - 1] === SPECIAL) { window.pop() return window as T[] } return undefined }) .filter(x => x !== undefined) .first() .pull()) as T[] yield* results } else { for await (const element of input) { yield element myCount-- if (myCount <= 0) { return } } } }) as any } /** * Yields the first elements of `this` sequence while the given predicate is true and skips the * rest. * * ℹ️ If the sequence is too small, the result will be empty.\ * ℹ️ The `options` argument lets you keep the first element for which the predicate returns * `false`. * * @param predicate The predicate to determine whether to continue yielding. * @param options Extra options. * @returns A new sequence with the yielded elements. */ takeWhile(predicate: ASeq.Predicate<T>, specifier?: TakeWhileOptions): ASeq<T> { chk(this.takeWhile).predicate(predicate) return ASeqOperator(this, async function* takeWhile(input) { let index = 0 for await (const element of input) { if (await pull(predicate(element, index++))) { yield element } else { if (specifier?.takeFinal) { yield element } return } } }) as any } /** * 🦥**Lazily** converts `this` sequence into an array. * * ⚠️ Has to iterate over the entire sequence. * * @returns A 🦥{@link DoddleAsync} that resolves to an array of the elements in the sequence. */ toArray(): DoddleAsync<T[]> { return lazyOperator(this, async function toArray(input) { const result: T[] = [] for await (const element of input) { result.push(element) } return result }) } /** * Returns `this` sequence as an {@link AsyncIterable}. * * @returns An {@link AsyncIterable} of the elements in this sequence. */ toIterable(): AsyncIterable<T> { return this } /** * 🦥**Lazily** converts `this` sequence into a Map. * * ⚠️ Has to iterate over the entire sequence. * * @param kvpProjection A function that takes an element and returns a key-value pair. * @returns A 🦥{@link DoddleAsync} that resolves to a Map of the elements in the sequence. */ toMap<Pair extends readonly [any, any]>( kvpProjection: ASeq.Iteratee<T, Pair> ): DoddleAsync<Map<Pair[0], Pair[1]>> { kvpProjection = chk(this.toMap).kvpProjection(kvpProjection) return lazyOperator(this, async function toMap(input) { const m = new Map<Pair[0], Pair[1]>() let index = 0 for await (const element of input) { const [key, value] = (await pull(kvpProjection(element, index++))) as Pair m.set(key, value) } return m }) } /** * 🦥**Lazily** converts `this` sequence into a plain JS object. Uses the given `kvpProjection` * to determine each key-value pair. * * @param kvpProjection A function that takes an element and returns a key-value pair. Each key * must be a valid PropertyKey. * @returns A 🦥{@link DoddleAsync} that resolves to a plain JS object. */ toRecord<const Key extends PropertyKey, Value>( kvpProjection: ASeq.Iteratee<T, readonly [Key, Value]> ): DoddleAsync<Record<Key, Value>> { chk(this.toRecord).kvpProjection(kvpProjection) return lazyOperator(this, async function toObject(input) { const o = {} as any let index = 0 for await (const element of input) { const [key, value] = (await pull(kvpProjection(element, index++))) as readonly [ Key, Value ] o[key] = value } return o }) } /** * **Lazily** converts `this` async sequence into a synchronous {@link Seq} sequence. * * ⚠️ Has to iterate over the entire sequence. * * @returns A 🦥{@link DoddleAsync} that resolves to a synchronous {@link Seq} sequence. */ toSeq(): DoddleAsync<Seq<T>> { return lazyOperator(this, async function toSeq(input) { const all = await aseq(input).toArray().pull() return seq(all) }) } /** * 🦥**Lazily** converts `this` sequence into a Set. * * ⚠️ Has to iterate over the entire sequence. * * @returns A 🦥{@link DoddleAsync} that resolves to a Set of the elements in the sequence. */ toSet(): DoddleAsync<Set<T>> { return lazyOperator(this, async function toSet(input) { const result = new Set<T>() for await (const element of input) { result.add(element) } return result }) } /** * Filters out duplicate elements from `this` sequence, optionally using a key projection. * * ℹ️ **Doesn't** need to iterate over the entire sequence.\ * ⚠️ Needs to cache the sequence as it's iterated over. * * @param keyProjection A function that takes an element and returns a key used to check for * uniqueness. * @returns A sequence of unique elements. */ uniq(keyProjection: ASeq.NoIndexIteratee<T, any> = x => x): ASeq<T> { chk(this.uniq).projection(keyProjection) return ASeqOperator(this, async function* uniq(input) { const seen = new Set<any>() for await (const element of input) { const key = await pull(keyProjection(element)) if (!seen.has(key)) { seen.add(key) yield element } } }) as any } /** * Splits `this` async sequence into overlapping windows of fixed size, applying a projection to * each window. * * ℹ️ If the sequence is smaller than the window size, one smaller window will yielded. * * @param size The size of each window. * @param projection A function to project each window to a value. * @returns A new sequence of windows or projected results. */ window<L extends number, S>( size: L, projection: (...window: getWindowArgsType<T, L>) => Doddle.MaybePromised<S> ): ASeq<S> /** * Splits `this` async sequence into overlapping windows of fixed size. * * ℹ️ If the sequence is smaller than the window size, one smaller window will yielded. * * @param size The size of each window. * @returns A new sequence of windows. */ window<L extends number>(size: L): ASeq<getWindowOutputType<T, L>> /** * Splits `this` async sequence into overlapping windows of fixed size, optionally applying a * projection to each window. * * @param size The size of each window. T