@byloth/core
Version:
An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧
1,254 lines (1,166 loc) • 44.4 kB
text/typescript
import { SmartAsyncIterator } from "../iterators/index.js";
import type {
GeneratorFunction,
AsyncGeneratorFunction,
MaybeAsyncGeneratorFunction,
MaybeAsyncIteratorLike
} from "../iterators/types.js";
import type { MaybePromise } from "../types.js";
import ReducedIterator from "./reduced-iterator.js";
import type { MaybeAsyncKeyedIteratee, MaybeAsyncKeyedReducer } from "./types.js";
/**
* A class representing an iterator that aggregates elements in a lazy and optimized way.
*
* It's part of the {@link SmartAsyncIterator} implementation,
* providing a way to group elements of an iterable by key.
* For this reason, it isn't recommended to instantiate this class directly
* (although it's still possible), but rather use the {@link SmartAsyncIterator.groupBy} method.
*
* It isn't directly iterable like its parent class but rather needs to specify on what you want to iterate.
* See the {@link AggregatedAsyncIterator.keys}, {@link AggregatedAsyncIterator.entries}
* & {@link AggregatedAsyncIterator.values} methods.
* It does, however, provide the same set of methods to perform
* operations and transformations on the elements of the iterator,
* having also the knowledge and context of the groups to which
* they belong, allowing to handle them in a grouped manner.
*
* This is particularly useful when you need to group elements and
* then perform specific operations on the groups themselves.
*
* ---
*
* @example
* ```ts
* const elements = fetch([...]); // Promise<[-3, -1, 0, 2, 3, 5, 6, 8]>;
* const results = new SmartAsyncIterator(elements)
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .count();
*
* console.log(await results.toObject()); // { odd: 4, even: 4 }
* ```
*
* ---
*
* @template K The type of the keys used to group the elements.
* @template T The type of the elements to aggregate.
*/
export default class AggregatedAsyncIterator<K extends PropertyKey, T>
{
/**
* The internal {@link SmartAsyncIterator} object that holds the elements to aggregate.
*/
protected readonly _elements: SmartAsyncIterator<[K, T]>;
/**
* Initializes a new instance of the {@link AggregatedAsyncIterator} class.
*
* ---
*
* @example
* ```ts
* const iterator = new AggregatedAsyncIterator<string, number>([["A", 1], ["B", 2], ["A", 3], ["C", 4], ["B", 5]]);
* ```
*
* ---
*
* @param iterable The iterable to aggregate.
*/
public constructor(iterable: Iterable<[K, T]>);
/**
* Initializes a new instance of the {@link AggregatedAsyncIterator} class.
*
* ---
*
* @example
* ```ts
* const elements = fetch([...]); // Promise<[["A", 1], ["B", 2], ["A", 3], ["C", 4], ["B", 5]]>
* const iterator = new AggregatedAsyncIterator<string, number>(elements);
* ```
*
* ---
*
* @param iterable The iterable to aggregate.
*/
public constructor(iterable: AsyncIterable<[K, T]>);
/**
* Initializes a new instance of the {@link AggregatedAsyncIterator} class.
*
* ---
*
* @example
* ```ts
* import { Random } from "@byloth/core";
*
* const iterator = new AggregatedAsyncIterator<string, number>({
* _index: 0,
* next: () =>
* {
* if (this._index >= 5) { return { done: true, value: undefined }; }
* this._index += 1;
*
* return { done: false, value: [Random.Choice(["A", "B", "C"]), (this._index + 1)] };
* }
* });
* ```
*
* ---
*
* @param iterator The iterator to aggregate.
*/
public constructor(iterator: Iterator<[K, T]>);
/**
* Initializes a new instance of the {@link AggregatedAsyncIterator} class.
*
* ---
*
* @example
* ```ts
* import { Random } from "@byloth/core";
*
* const iterator = new AggregatedAsyncIterator<string, number>({
* _index: 0,
* next: async () =>
* {
* if (this._index >= 5) { return { done: true, value: undefined }; }
* this._index += 1;
*
* return { done: false, value: [Random.Choice(["A", "B", "C"]), (this._index + 1)] };
* }
* });
* ```
*
* ---
*
* @param iterator The iterator to aggregate.
*/
public constructor(iterator: AsyncIterator<[K, T]>);
/**
* Initializes a new instance of the {@link AggregatedAsyncIterator} class.
*
* ---
*
* @example
* ```ts
* import { range, Random } from "@byloth/core";
*
* const iterator = new AggregatedAsyncIterator<string, number>(function* ()
* {
* for (const index of range(5))
* {
* yield [Random.Choice(["A", "B", "C"]), (index + 1)];
* }
* });
* ```
*
* ---
*
* @param generatorFn The generator function to aggregate.
*/
public constructor(generatorFn: GeneratorFunction<[K, T]>);
/**
* Initializes a new instance of the {@link AggregatedAsyncIterator} class.
*
* ---
*
* @example
* ```ts
* import { range, Random } from "@byloth/core";
*
* const iterator = new AggregatedAsyncIterator<string, number>(async function* ()
* {
* for await (const index of range(5))
* {
* yield [Random.Choice(["A", "B", "C"]), (index + 1)];
* }
* });
* ```
*
* ---
*
* @param generatorFn The generator function to aggregate.
*/
public constructor(generatorFn: AsyncGeneratorFunction<[K, T]>);
/**
* Initializes a new instance of the {@link AggregatedAsyncIterator} class.
*
* ---
*
* @example
* ```ts
* const iterator = new AggregatedAsyncIterator(asyncKeyedValues);
* ```
*
* ---
*
* @param argument The iterable, iterator or generator function to aggregate.
*/
public constructor(argument: MaybeAsyncIteratorLike<[K, T]> | MaybeAsyncGeneratorFunction<[K, T]>);
public constructor(argument: MaybeAsyncIteratorLike<[K, T]> | MaybeAsyncGeneratorFunction<[K, T]>)
{
this._elements = new SmartAsyncIterator(argument);
}
/**
* Determines whether all elements of each group of the iterator satisfy a given condition.
* See also {@link AggregatedAsyncIterator.some}.
* This method will consume the entire iterator in the process.
*
* It will iterate over all elements of the iterator checjing if they satisfy the condition.
* Once a single element of one group doesn't satisfy the condition,
* the result for the respective group will be `false`.
*
* Eventually, it will return a new {@link ReducedIterator}
* object that will contain all the boolean results for each group.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .every(async (key, value) => value >= 0);
*
* console.log(await results.toObject()); // { odd: false, even: true }
* ```
*
* ---
*
* @param predicate The condition to check for each element of the iterator.
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing the boolean results for each group.
*/
public async every(predicate: MaybeAsyncKeyedIteratee<K, T, boolean>): Promise<ReducedIterator<K, boolean>>
{
const values = new Map<K, [number, boolean]>();
for await (const [key, element] of this._elements)
{
const [index, result] = values.get(key) ?? [0, true];
if (!(result)) { continue; }
values.set(key, [index + 1, await predicate(key, element, index)]);
}
return new ReducedIterator(function* ()
{
for (const [key, [_, result]] of values) { yield [key, result]; }
});
}
/**
* Determines whether any element of each group of the iterator satisfies a given condition.
* See also {@link AggregatedAsyncIterator.every}.
* This method will consume the entire iterator in the process.
*
* It will iterate over all elements of the iterator checjing if they satisfy the condition.
* Once a single element of one group satisfies the condition,
* the result for the respective group will be `true`.
*
* Eventually, it will return a new {@link ReducedIterator}
* object that will contain all the boolean results for each group.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-5, -4, -3, -2, -1, 0])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .some(async (key, value) => value >= 0);
*
* console.log(await results.toObject()); // { odd: false, even: true }
* ```
*
* ---
*
* @param predicate The condition to check for each element of the iterator.
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing the boolean results for each group.
*/
public async some(predicate: MaybeAsyncKeyedIteratee<K, T, boolean>): Promise<ReducedIterator<K, boolean>>
{
const values = new Map<K, [number, boolean]>();
for await (const [key, element] of this._elements)
{
const [index, result] = values.get(key) ?? [0, false];
if (result) { continue; }
values.set(key, [index + 1, await predicate(key, element, index)]);
}
return new ReducedIterator(function* ()
{
for (const [key, [_, result]] of values) { yield [key, result]; }
});
}
/**
* Filters the elements of the iterator based on a given condition.
*
* This method will iterate over all elements of the iterator checking if they satisfy the condition.
* If the condition is met, the element will be included in the new iterator.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .filter(async (key, value) => value >= 0);
*
* console.log(await results.toObject()); // { odd: [3, 5], even: [0, 2, 6, 8] }
* ```
*
* ---
*
* @param predicate The condition to check for each element of the iterator.
*
* @returns A new {@link AggregatedAsyncIterator} containing the elements that satisfy the condition.
*/
public filter(predicate: MaybeAsyncKeyedIteratee<K, T, boolean>): AggregatedAsyncIterator<K, T>;
/**
* Filters the elements of the iterator based on a given condition.
*
* This method will iterate over all elements of the iterator checking if they satisfy the condition.
* If the condition is met, the element will be included in the new iterator.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, "-1", 0, "2", "3", 5, 6, "8"])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .filter<number>(async (key, value) => typeof value === "number");
*
* console.log(await results.toObject()); // { odd: [-3, 5], even: [0, 6] }
* ```
*
* ---
*
* @template S
* The type of the elements that satisfy the condition.
* This allows the type-system to infer the correct type of the new iterator.
*
* It must be a subtype of the original type of the elements.
*
* @param predicate The type guard condition to check for each element of the iterator.
*
* @returns A new {@link AggregatedAsyncIterator} containing the elements that satisfy the condition.
*/
public filter<S extends T>(predicate: MaybeAsyncKeyedIteratee<K, T, boolean>): AggregatedAsyncIterator<K, S>;
public filter(predicate: MaybeAsyncKeyedIteratee<K, T, boolean>): AggregatedAsyncIterator<K, T>
{
const elements = this._elements;
return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[K, T]>
{
const indexes = new Map<K, number>();
for await (const [key, element] of elements)
{
const index = indexes.get(key) ?? 0;
if (await predicate(key, element, index)) { yield [key, element]; }
indexes.set(key, index + 1);
}
});
}
/**
* Maps the elements of the iterator using a given transformation function.
*
* This method will iterate over all elements of the iterator applying the condition.
* The result of each transformation will be included in the new iterator.
*
* Since the iterator is lazy, the mapping process will
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .map(async (key, value) => Math.abs(value));
*
* console.log(await results.toObject()); // { odd: [3, 1, 3, 5], even: [0, 2, 6, 8] }
* ```
*
* ---
*
* @template V The type of the elements after the transformation.
*
* @param iteratee The transformation function to apply to each element of the iterator.
*
* @returns A new {@link AggregatedAsyncIterator} containing the transformed elements.
*/
public map<V>(iteratee: MaybeAsyncKeyedIteratee<K, T, V>): AggregatedAsyncIterator<K, V>
{
const elements = this._elements;
return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[K, V]>
{
const indexes = new Map<K, number>();
for await (const [key, element] of elements)
{
const index = indexes.get(key) ?? 0;
yield [key, await iteratee(key, element, index)];
indexes.set(key, index + 1);
}
});
}
/**
* Reduces the elements of the iterator using a given reducer function.
* This method will consume the entire iterator in the process.
*
* It will iterate over all elements of the iterator applying the reducer function.
* The result of each iteration will be passed as the accumulator to the next one.
*
* The first accoumulator value will be the first element of the iterator.
* The last accumulator value will be the final result of the reduction.
*
* Eventually, it will return a new {@link ReducedIterator}
* object that will contain all the reduced results for each group.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .reduce(async (key, accumulator, value) => accumulator + value);
*
* console.log(await results.toObject()); // { odd: 4, even: 16 }
* ```
*
* ---
*
* @param reducer The reducer function to apply to each element of the iterator.
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing the reduced results for each group.
*/
public async reduce(reducer: MaybeAsyncKeyedReducer<K, T, T>): Promise<ReducedIterator<K, T>>;
/**
* Reduces the elements of the iterator using a given reducer function.
* This method will consume the entire iterator in the process.
*
* It will iterate over all elements of the iterator applying the reducer function.
* The result of each iteration will be passed as the accumulator to the next one.
*
* The first accoumulator value will be the provided initial value.
* The last accumulator value will be the final result of the reduction.
*
* Eventually, it will return a new {@link ReducedIterator}
* object that will contain all the reduced results for each group.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .reduce(async (key, accumulator, value) => accumulator + value, 0);
*
* console.log(await results.toObject()); // { odd: 4, even: 16 }
* ```
*
* ---
*
* @template A The type of the accumulator value which will also be the final result of the reduction.
*
* @param reducer The reducer function to apply to each element of the iterator.
* @param initialValue The initial value for the accumulator.
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing the reduced results for each group.
*/
public async reduce<A extends PropertyKey>(
reducer: MaybeAsyncKeyedReducer<K, T, A>, initialValue: MaybePromise<A>
): Promise<ReducedIterator<K, A>>;
/**
* Reduces the elements of the iterator using a given reducer function.
* This method will consume the entire iterator in the process.
*
* It will iterate over all elements of the iterator applying the reducer function.
* The result of each iteration will be passed as the accumulator to the next one.
*
* The first accoumulator value will be the provided initial value by the given function.
* The last accumulator value will be the final result of the reduction.
*
* Eventually, it will return a new {@link ReducedIterator}
* object that will contain all the reduced results for each group.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .reduce(async (key, { value }, currentValue) => ({ value: value + currentValue }), (key) => ({ value: 0 }));
*
* console.log(await results.toObject()); // { odd: { value: 4 }, even: { value: 16 } }
* ```
*
* ---
*
* @template A The type of the accumulator value which will also be the final result of the reduction.
*
* @param reducer The reducer function to apply to each element of the iterator.
* @param initialValue The function that provides the initial value for the accumulator.
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing the reduced results for each group.
*/
public async reduce<A>(
reducer: MaybeAsyncKeyedReducer<K, T, A>, initialValue: (key: K) => MaybePromise<A>
): Promise<ReducedIterator<K, A>>;
public async reduce<A>(
reducer: MaybeAsyncKeyedReducer<K, T, A>, initialValue?: MaybePromise<A> | ((key: K) => MaybePromise<A>)
): Promise<ReducedIterator<K, A>>
{
const values = new Map<K, [number, A]>();
for await (const [key, element] of this._elements)
{
let index: number;
let accumulator: A;
if (values.has(key)) { [index, accumulator] = values.get(key)!; }
else if (initialValue !== undefined)
{
index = 0;
if (initialValue instanceof Function) { accumulator = await initialValue(key); }
else { accumulator = await initialValue; }
}
else
{
values.set(key, [0, (element as unknown) as A]);
continue;
}
values.set(key, [index + 1, await reducer(key, accumulator, element, index)]);
}
return new ReducedIterator(function* ()
{
for (const [key, [_, accumulator]] of values) { yield [key, accumulator]; }
});
}
/**
* Flattens the elements of the iterator using a given transformation function.
*
* This method will iterate over all elements of the iterator applying the transformation function.
* The result of each transformation will be included in the new iterator.
*
* Since the iterator is lazy, the mapping process will
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([[-3, -1], 0, 2, 3, 5, [6, 8]])
* .groupBy(async (values) =>
* {
* const value = values instanceof Array ? values[0] : values;
* return value % 2 === 0 ? "even" : "odd";
* })
* .flatMap(async (key, values) => values);
*
* console.log(await results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] }
* ```
*
* ---
*
* @template V The type of the elements after the transformation.
*
* @param iteratee The transformation function to apply to each element of the iterator.
*
* @returns A new {@link AggregatedAsyncIterator} containing the transformed elements.
*/
public flatMap<V>(iteratee: MaybeAsyncKeyedIteratee<K, T, V | readonly V[]>): AggregatedAsyncIterator<K, V>
{
const elements = this._elements;
return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[K, V]>
{
const indexes = new Map<K, number>();
for await (const [key, element] of elements)
{
const index = indexes.get(key) ?? 0;
const values = await iteratee(key, element, index);
if (values instanceof Array)
{
for (const value of values) { yield [key, value]; }
}
else { yield [key, values]; }
indexes.set(key, index + 1);
}
});
}
/**
* Drops a given number of elements from the beginning of each group of the iterator.
* The remaining elements will be included in the new iterator.
* See also {@link AggregatedAsyncIterator.take}.
*
* Since the iterator is lazy, the dropping process will
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .drop(2);
*
* console.log(await results.toObject()); // { odd: [3, 5], even: [6, 8] }
* ```
*
* ---
*
* @param count The number of elements to drop from the beginning of each group.
*
* @returns A new {@link AggregatedAsyncIterator} containing the remaining elements.
*/
public drop(count: number): AggregatedAsyncIterator<K, T>
{
const elements = this._elements;
return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[K, T]>
{
const indexes = new Map<K, number>();
for await (const [key, element] of elements)
{
const index = indexes.get(key) ?? 0;
if (index < count)
{
indexes.set(key, index + 1);
continue;
}
yield [key, element];
}
});
}
/**
* Takes a given number of elements from the beginning of each group of the iterator.
* The elements will be included in the new iterator.
* See also {@link AggregatedAsyncIterator.drop}.
*
* Since the iterator is lazy, the taking process will
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .take(2);
*
* console.log(await results.toObject()); // { odd: [-3, -1], even: [0, 2] }
* ```
*
* ---
*
* @param limit The number of elements to take from the beginning of each group.
*
* @returns A new {@link AggregatedAsyncIterator} containing the taken elements.
*/
public take(limit: number): AggregatedAsyncIterator<K, T>
{
const elements = this._elements;
return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[K, T]>
{
const indexes = new Map<K, number>();
for await (const [key, element] of elements)
{
const index = indexes.get(key) ?? 0;
if (index >= limit) { continue; }
yield [key, element];
indexes.set(key, index + 1);
}
});
}
/**
* Finds the first element of each group of the iterator that satisfies a given condition.
* This method will consume the entire iterator in the process.
*
* It will iterate over all elements of the iterator checking if they satisfy the condition.
* Once the first element of one group satisfies the condition,
* the result for the respective group will be the element itself.
*
* Eventually, it will return a new {@link ReducedIterator}
* object that will contain the first element that satisfies the condition for each group.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .find(async (key, value) => value > 0);
*
* console.log(await results.toObject()); // { odd: 3, even: 2 }
* ```
*
* ---
*
* @param predicate The condition to check for each element of the iterator.
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing
* the first element that satisfies the condition for each group.
*/
public async find(predicate: MaybeAsyncKeyedIteratee<K, T, boolean>): Promise<ReducedIterator<K, T | undefined>>;
/**
* Finds the first element of each group of the iterator that satisfies a given condition.
* This method will consume the entire iterator in the process.
*
* It will iterate over all elements of the iterator checking if they satisfy the condition.
* Once the first element of one group satisfies the condition,
* the result for the respective group will be the element itself.
*
* Eventually, it will return a new {@link ReducedIterator}
* object that will contain the first element that satisfies the condition for each group.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number | string>([-3, "-1", 0, "2", "3", 5, 6, "8"])
* .groupBy(async (value) => Number(value) % 2 === 0 ? "even" : "odd")
* .find<number>(async (key, value) => typeof value === "number");
*
* console.log(await results.toObject()); // { odd: -3, even: 0 }
* ```
*
* ---
*
* @template S
* The type of the elements that satisfy the condition.
* This allows the type-system to infer the correct type of the new iterator.
*
* It must be a subtype of the original type of the elements.
*
* @param predicate The type guard condition to check for each element of the iterator.
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing
* the first element that satisfies the condition for each group.
*/
public async find<S extends T>(
predicate: MaybeAsyncKeyedIteratee<K, T, boolean>
): Promise<ReducedIterator<K, S | undefined>>;
public async find(predicate: MaybeAsyncKeyedIteratee<K, T, boolean>): Promise<ReducedIterator<K, T | undefined>>
{
const values = new Map<K, [number, T | undefined]>();
for await (const [key, element] of this._elements)
{
let [index, finding] = values.get(key) ?? [0, undefined];
if (finding !== undefined) { continue; }
if (await predicate(key, element, index)) { finding = element; }
values.set(key, [index + 1, finding]);
}
return new ReducedIterator(function* ()
{
for (const [key, [_, finding]] of values) { yield [key, finding]; }
});
}
/**
* Enumerates the elements of the iterator.
* Each element is paired with its index within the group in the new iterator.
*
* Since the iterator is lazy, the enumeration process will
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, 0, 2, -1, 3])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .enumerate();
*
* console.log(results.toObject()); // { odd: [[0, -3], [1, -1], [2, 3]], even: [[0, 0], [1, 2]] }
* ```
*
* ---
*
* @returns A new {@link AggregatedAsyncIterator} containing the enumerated elements.
*/
public enumerate(): AggregatedAsyncIterator<K, [number, T]>
{
return this.map((key, value, index) => [index, value]);
}
/**
* Removes all duplicate elements from within each group of the iterator.
* The first occurrence of each element will be included in the new iterator.
*
* Since the iterator is lazy, the deduplication process will
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 6, -3, -1, 0, 5, 6, 8, 0, 2])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .unique();
*
* console.log(await results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] }
* ```
*
* ---
*
* @returns A new {@link AggregatedAsyncIterator} containing only the unique elements.
*/
public unique(): AggregatedAsyncIterator<K, T>
{
const elements = this._elements;
return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[K, T]>
{
const keys = new Map<K, Set<T>>();
for await (const [key, element] of elements)
{
const values = keys.get(key) ?? new Set<T>();
if (values.has(element)) { continue; }
values.add(element);
keys.set(key, values);
yield [key, element];
}
});
}
/**
* Counts the number of elements within each group of the iterator.
* This method will consume the entire iterator in the process.
*
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .count();
*
* console.log(await results.toObject()); // { odd: 4, even: 4 }
* ```
*
* ---
*
* @returns
* A {@link Promise} resolving to a new {@link ReducedIterator} containing the number of elements for each group.
*/
public async count(): Promise<ReducedIterator<K, number>>
{
const counters = new Map<K, number>();
for await (const [key] of this._elements)
{
const count = counters.get(key) ?? 0;
counters.set(key, count + 1);
}
return new ReducedIterator(function* ()
{
for (const [key, count] of counters) { yield [key, count]; }
});
}
/**
* Iterates over the elements of the iterator.
* The elements are passed to the given iteratee function along with their key and index within the group.
*
* This method will consume the entire iterator in the process.
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const aggregator = new SmartAsyncIterator<number>([-3, 0, 2, -1, 3])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd");
*
* await aggregator.forEach(async (key, value, index) =>
* {
* console.log(`${index}: ${value}`); // "0: -3", "0: 0", "1: 2", "1: -1", "2: 3"
* };
* ```
*
* ---
*
* @param iteratee The function to execute for each element of the iterator.
*
* @returns A {@link Promise} that will resolve once the iteration is complete.
*/
public async forEach(iteratee: MaybeAsyncKeyedIteratee<K, T>): Promise<void>
{
const indexes = new Map<K, number>();
for await (const [key, element] of this._elements)
{
const index = indexes.get(key) ?? 0;
await iteratee(key, element, index);
indexes.set(key, index + 1);
}
}
/**
* Changes the key of each element on which the iterator is aggregated.
* The new key is determined by the given iteratee function.
*
* Since the iterator is lazy, the reorganization process will
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const results = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .map(async (key, value, index) => index % 2 === 0 ? value : -value)
* .reorganizeBy(async (key, value) => value >= 0 ? "+" : "-");
*
* console.log(await results.toObject()); // { "+": [1, 0, 3, 6], "-": [-3, -2, -5, -8] }
* ```
*
* ---
*
* @template J The type of the new key.
*
* @param iteratee The function to determine the new key for each element of the iterator.
*
* @returns A new {@link AggregatedAsyncIterator} containing the elements reorganized by the new keys.
*/
public reorganizeBy<J extends PropertyKey>(
iteratee: MaybeAsyncKeyedIteratee<K, T, J>
): AggregatedAsyncIterator<J, T>
{
const elements = this._elements;
return new AggregatedAsyncIterator(async function* (): AsyncGenerator<[J, T]>
{
const indexes = new Map<K, number>();
for await (const [key, element] of elements)
{
const index = indexes.get(key) ?? 0;
yield [await iteratee(key, element, index), element];
indexes.set(key, index + 1);
}
});
}
/**
* An utility method that returns a new {@link SmartAsyncIterator}
* object containing all the keys of the iterator.
*
* Since the iterator is lazy, the keys will be extracted
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const keys = new SmartAsyncIterator([-3, Symbol(), "A", { }, null, [1 , 2, 3], false])
* .groupBy(async (value) => typeof value)
* .keys();
*
* console.log(await keys.toArray()); // ["number", "symbol", "string", "object", "boolean"]
* ```
*
* ---
*
* @returns A new {@link SmartAsyncIterator} containing all the keys of the iterator.
*/
public keys(): SmartAsyncIterator<K>
{
const elements = this._elements;
return new SmartAsyncIterator<K>(async function* ()
{
const keys = new Set<K>();
for await (const [key] of elements)
{
if (keys.has(key)) { continue; }
keys.add(key);
yield key;
}
});
}
/**
* An utility method that returns a new {@link SmartAsyncIterator}
* object containing all the entries of the iterator.
* Each entry is a tuple containing the key and the element.
*
* Since the iterator is lazy, the entries will be extracted
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const entries = new SmartAsyncIterator<number>([-3, 0, 2, -1, 3])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .entries();
*
* console.log(await entries.toArray()); // [["odd", -3], ["even", 0], ["even", 2], ["odd", -1], ["odd", 3]]
* ```
*
* ---
*
* @returns A new {@link SmartAsyncIterator} containing all the entries of the iterator.
*/
public entries(): SmartAsyncIterator<[K, T]>
{
return this._elements;
}
/**
* An utility method that returns a new {@link SmartAsyncIterator}
* object containing all the values of the iterator.
*
* Since the iterator is lazy, the values will be extracted
* be executed once the resulting iterator is materialized.
*
* A new iterator will be created, holding the reference to the original one.
* This means that the original iterator won't be consumed until the
* new one is and that consuming one of them will consume the other as well.
*
* ---
*
* @example
* ```ts
* const values = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd")
* .values();
*
* console.log(await values.toArray()); // [-3, -1, 0, 2, 3, 5, 6, 8]
* ```
*
* ---
*
* @returns A new {@link SmartAsyncIterator} containing all the values of the iterator.
*/
public values(): SmartAsyncIterator<T>
{
const elements = this._elements;
return new SmartAsyncIterator<T>(async function* ()
{
for await (const [_, element] of elements) { yield element; }
});
}
/**
* Materializes the iterator into an array of arrays.
* This method will consume the entire iterator in the process.
*
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const aggregator = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd");
*
* console.log(await aggregator.toArray()); // [[-3, -1, 3, 5], [0, 2, 6, 8]]
* ```
*
* ---
*
* @returns A {@link Promise} resolving to an {@link Array} containing all the values of the iterator.
*/
public async toArray(): Promise<T[][]>
{
const map = await this.toMap();
return Array.from(map.values());
}
/**
* Materializes the iterator into a map.
* This method will consume the entire iterator in the process.
*
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const aggregator = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd");
*
* console.log(await aggregator.toMap()); // Map(2) { "odd" => [-3, -1, 3, 5], "even" => [0, 2, 6, 8] }
* ```
*
* ---
*
* @returns A {@link Promise} resolving to a {@link Map} containing all the entries of the iterator.
*/
public async toMap(): Promise<Map<K, T[]>>
{
const groups = new Map<K, T[]>();
for await (const [key, element] of this._elements)
{
const value = groups.get(key) ?? [];
value.push(element);
groups.set(key, value);
}
return groups;
}
/**
* Materializes the iterator into an object.
* This method will consume the entire iterator in the process.
*
* If the iterator is infinite, the method will never return.
*
* ---
*
* @example
* ```ts
* const aggregator = new SmartAsyncIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8])
* .groupBy(async (value) => value % 2 === 0 ? "even" : "odd");
*
* console.log(await aggregator.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] }
* ```
*
* ---
*
* @returns A {@link Promise} resolving to an object containing all the entries of the iterator.
*/
public async toObject(): Promise<Record<K, T[]>>
{
const groups = { } as Record<K, T[]>;
for await (const [key, element] of this._elements)
{
const value = groups[key] ?? [];
value.push(element);
groups[key] = value;
}
return groups;
}
public readonly [Symbol.toStringTag]: string = "AggregatedAsyncIterator";
}