UNPKG

@byloth/core

Version:

An unopinionated collection of useful functions and classes that I use widely in all my projects. 🔧

1,158 lines (1,077 loc) • 40.5 kB
import { SmartIterator } from "../iterators/index.js"; import type { GeneratorFunction, IteratorLike } from "../iterators/types.js"; import ReducedIterator from "./reduced-iterator.js"; import type { KeyedIteratee, KeyedTypeGuardPredicate, KeyedReducer } from "./types.js"; /** * A class representing an iterator that aggregates elements in a lazy and optimized way. * * It's part of the {@link SmartIterator} 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 SmartIterator.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 AggregatedIterator.keys}, {@link AggregatedIterator.entries} * & {@link AggregatedIterator.values} methods. * It does, however, provide the same set of methods to perform * operations and transformation 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 results = new SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .count(); * * console.log(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 AggregatedIterator<K extends PropertyKey, T> { /** * The internal {@link SmartIterator} object that holds the elements to aggregate. */ protected readonly _elements: SmartIterator<[K, T]>; /** * Initializes a new instance of the {@link AggregatedIterator} class. * * --- * * @example * ```ts * const iterator = new AggregatedIterator<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 AggregatedIterator} class. * * --- * * @example * ```ts * import { Random } from "@byloth/core"; * * const iterator = new AggregatedIterator<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 AggregatedIterator} class. * * --- * * @example * ```ts * import { range, Random } from "@byloth/core"; * * const iterator = new AggregatedIterator<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 AggregatedIterator} class. * * --- * * @example * ```ts * const iterator = new AggregatedIterator(keyedValues); * ``` * * --- * * @param argument The iterable, iterator or generator function to aggregate. */ public constructor(argument: IteratorLike<[K, T]> | GeneratorFunction<[K, T]>); public constructor(argument: IteratorLike<[K, T]> | GeneratorFunction<[K, T]>) { this._elements = new SmartIterator(argument); } /** * Determines whether all elements of each group of the iterator satisfy a given condition. * See also {@link AggregatedIterator.some}. * 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 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .every((key, value) => value >= 0); * * console.log(results.toObject()); // { odd: false, even: true } * ``` * * --- * * @param predicate The condition to check for each element of the iterator. * * @returns A new {@link ReducedIterator} containing the boolean results for each group. */ public every(predicate: KeyedIteratee<K, T, boolean>): ReducedIterator<K, boolean> { const values = new Map<K, [number, boolean]>(); for (const [key, element] of this._elements) { const [index, result] = values.get(key) ?? [0, true]; if (!(result)) { continue; } values.set(key, [index + 1, predicate(key, element, index)]); } return new ReducedIterator(function* () { for (const [key, [_, result]] of values) { yield [key, result]; } }); } /** * Determines whether any elements of each group of the iterator satisfy a given condition. * See also {@link AggregatedIterator.every}. * 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 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 SmartIterator<number>([-5, -4, -3, -2, -1, 0]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .some((key, value) => value >= 0); * * console.log(results.toObject()); // { odd: false, even: true } * ``` * * --- * * @param predicate The condition to check for each element of the iterator. * * @returns A {@link ReducedIterator} containing the boolean results for each group. */ public some(predicate: KeyedIteratee<K, T, boolean>): ReducedIterator<K, boolean> { const values = new Map<K, [number, boolean]>(); for (const [key, element] of this._elements) { const [index, result] = values.get(key) ?? [0, false]; if (result) { continue; } values.set(key, [index + 1, predicate(key, element, index)]); } return new ReducedIterator(function* () { for (const [key, [_, result]] of values) { yield [key, result]; } }); } /** * Filters the elements of the iterator using 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. * * Since the iterator is lazy, the filtering 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .filter((key, value) => value >= 0); * * console.log(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 AggregatedIterator} containing only the elements that satisfy the condition. */ public filter(predicate: KeyedIteratee<K, T, boolean>): AggregatedIterator<K, T>; /** * Filters the elements of the iterator using 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. * * Since the iterator is lazy, the filtering 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 SmartIterator<number | string>([-3, "-1", 0, "2", "3", 5, 6, "8"]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .filter<number>((key, value) => typeof value === "number"); * * console.log(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 AggregatedIterator} containing only the elements that satisfy the condition. */ public filter<S extends T>(predicate: KeyedTypeGuardPredicate<K, T, S>): AggregatedIterator<K, S>; public filter(predicate: KeyedIteratee<K, T, boolean>): AggregatedIterator<K, T> { const elements = this._elements; return new AggregatedIterator(function* () { const indexes = new Map<K, number>(); for (const [key, element] of elements) { const index = indexes.get(key) ?? 0; if (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 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .map((key, value) => Math.abs(value)); * * console.log(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 AggregatedIterator} containing the transformed elements. */ public map<V>(iteratee: KeyedIteratee<K, T, V>): AggregatedIterator<K, V> { const elements = this._elements; return new AggregatedIterator(function* () { const indexes = new Map<K, number>(); for (const [key, element] of elements) { const index = indexes.get(key) ?? 0; yield [key, 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 accumulator 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .reduce((key, accumulator, value) => accumulator + value); * * console.log(results.toObject()); // { odd: 4, even: 16 } * ``` * * --- * * @param reducer The reducer function to apply to each element of the iterator. * * @returns A new {@link ReducedIterator} containing the reduced results for each group. */ public reduce(reducer: KeyedReducer<K, T, T>): 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 accumulator 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .reduce((key, accumulator, value) => accumulator + value, 0); * * console.log(results.toObject()); // { odd: 4, even: 16 } * ``` * * --- * * @template A The type of the accumulator value which will also be the type of the final result of the reduction. * * @param reducer The reducer function to apply to each element of the iterator. * @param initialValue The initial value of the accumulator. * * @returns A new {@link ReducedIterator} containing the reduced results for each group. */ public reduce<A extends PropertyKey>(reducer: KeyedReducer<K, T, A>, initialValue: A): 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 accumulator 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .reduce((key, { value }, currentValue) => ({ value: value + currentValue }), (key) => ({ value: 0 })); * * console.log(results.toObject()); // { odd: { value: 4 }, even: { value: 16 } } * ``` * * --- * * @template A The type of the accumulator value which will also be the type of 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 new {@link ReducedIterator} containing the reduced results for each group. */ public reduce<A>(reducer: KeyedReducer<K, T, A>, initialValue: (key: K) => A): ReducedIterator<K, A>; public reduce<A>(reducer: KeyedReducer<K, T, A>, initialValue?: A | ((key: K) => A)): ReducedIterator<K, A> { const values = new Map<K, [number, A]>(); for (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 = initialValue(key); } else { accumulator = initialValue; } } else { values.set(key, [0, (element as unknown) as A]); continue; } values.set(key, [index + 1, 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 flattening 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 SmartIterator<number[]>([[-3, -1], 0, 2, 3, 5, [6, 8]]) * .groupBy((values) => * { * const value = values instanceof Array ? values[0] : values; * return value % 2 === 0 ? "even" : "odd"; * }) * .flatMap((key, values) => values); * * console.log(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 AggregatedIterator} containing the transformed elements. */ public flatMap<V>(iteratee: KeyedIteratee<K, T, V | readonly V[]>): AggregatedIterator<K, V> { const elements = this._elements; return new AggregatedIterator(function* () { const indexes = new Map<K, number>(); for (const [key, element] of elements) { const index = indexes.get(key) ?? 0; const values = 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 AggregatedIterator.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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .drop(2); * * console.log(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 AggregatedIterator} containing the remaining elements. */ public drop(count: number): AggregatedIterator<K, T> { const elements = this._elements; return new AggregatedIterator(function* () { const indexes = new Map<K, number>(); for (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 AggregatedIterator.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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .take(2); * * console.log(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 AggregatedIterator} containing the taken elements. */ public take(limit: number): AggregatedIterator<K, T> { const elements = this._elements; return new AggregatedIterator(function* () { const indexes = new Map<K, number>(); for (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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .find((key, value) => value > 0); * * console.log(results.toObject()); // { odd: 3, even: 2 } * ``` * * --- * * @param predicate The condition to check for each element of the iterator. * * @returns A new {@link ReducedIterator} containing the first element that satisfies the condition for each group. */ public find(predicate: KeyedIteratee<K, T, boolean>): 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 SmartIterator<number | string>([-3, "-1", 0, "2", "3", 5, 6, "8"]) * .groupBy((value) => Number(value) % 2 === 0 ? "even" : "odd") * .find<number>((key, value) => typeof value === "number"); * * console.log(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 new {@link ReducedIterator} containing the first element that satisfies the condition for each group. */ public find<S extends T>(predicate: KeyedTypeGuardPredicate<K, T, S>): ReducedIterator<K, S | undefined>; public find(predicate: KeyedIteratee<K, T, boolean>): ReducedIterator<K, T | undefined> { const values = new Map<K, [number, T | undefined]>(); for (const [key, element] of this._elements) { let [index, finding] = values.get(key) ?? [0, undefined]; if (finding !== undefined) { continue; } if (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 a 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 SmartIterator<number>([-3, 0, 2, -1, 3]) * .groupBy((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 AggregatedIterator} containing the enumerated elements. */ public enumerate(): AggregatedIterator<K, [number, T]> { return this.map((_, 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 SmartIterator<number>([-3, -1, 0, 2, 3, 6, -3, -1, 0, 5, 6, 8, 0, 2]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .unique(); * * console.log(results.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } * ``` * * --- * * @returns A new {@link AggregatedIterator} containing only the unique elements. */ public unique(): AggregatedIterator<K, T> { const elements = this._elements; return new AggregatedIterator(function* () { const keys = new Map<K, Set<T>>(); for (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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .count(); * * console.log(results.toObject()); // { odd: 4, even: 4 } * ``` * * --- * * @returns A new {@link ReducedIterator} containing the number of elements for each group. */ public count(): ReducedIterator<K, number> { const counters = new Map<K, number>(); for (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 SmartIterator<number>([-3, 0, 2, -1, 3]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); * * aggregator.forEach((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. */ public forEach(iteratee: KeyedIteratee<K, T>): void { const indexes = new Map<K, number>(); for (const [key, element] of this._elements) { const index = indexes.get(key) ?? 0; 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .map((key, value, index) => index % 2 === 0 ? value : -value) * .reorganizeBy((key, value) => value >= 0 ? "+" : "-"); * * console.log(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 AggregatedIterator} containing the elements reorganized by the new keys. */ public reorganizeBy<J extends PropertyKey>(iteratee: KeyedIteratee<K, T, J>): AggregatedIterator<J, T> { const elements = this._elements; return new AggregatedIterator(function* () { const indexes = new Map<K, number>(); for (const [key, element] of elements) { const index = indexes.get(key) ?? 0; yield [iteratee(key, element, index), element]; indexes.set(key, index + 1); } }); } /** * An utility method that returns a new {@link SmartIterator} * 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 SmartIterator([-3, Symbol(), "A", { }, null, [1 , 2, 3], false]) * .groupBy((value) => typeof value) * .keys(); * * console.log(keys.toArray()); // ["number", "symbol", "string", "object", "boolean"] * ``` * * --- * * @returns A new {@link SmartIterator} containing all the keys of the iterator. */ public keys(): SmartIterator<K> { const elements = this._elements; return new SmartIterator<K>(function* () { const keys = new Set<K>(); for (const [key] of elements) { if (keys.has(key)) { continue; } keys.add(key); yield key; } }); } /** * An utility method that returns a new {@link SmartIterator} * 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 SmartIterator<number>([-3, 0, 2, -1, 3]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .entries(); * * console.log(entries.toArray()); // [["odd", -3], ["even", 0], ["even", 2], ["odd", -1], ["odd", 3]] * ``` * * --- * * @returns A new {@link SmartIterator} containing all the entries of the iterator. */ public entries(): SmartIterator<[K, T]> { return this._elements; } /** * An utility method that returns a new {@link SmartIterator} * 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd") * .values(); * * console.log(values.toArray()); // [-3, -1, 0, 2, 3, 5, 6, 8] * ``` * * --- * * @returns A new {@link SmartIterator} containing all the values of the iterator. */ public values(): SmartIterator<T> { const elements = this._elements; return new SmartIterator<T>(function* () { for (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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); * * console.log(aggregator.toArray()); // [[-3, -1, 3, 5], [0, 2, 6, 8]] * ``` * * --- * * @returns An {@link Array} of arrays containing the elements of the iterator. */ public toArray(): T[][] { const map = 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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); * * console.log(aggregator.toMap()); // Map(2) { "odd" => [-3, -1, 3, 5], "even" => [0, 2, 6, 8] } * ``` * * --- * * @returns A {@link Map} containing the elements of the iterator. */ public toMap(): Map<K, T[]> { const groups = new Map<K, T[]>(); for (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 SmartIterator<number>([-3, -1, 0, 2, 3, 5, 6, 8]) * .groupBy((value) => value % 2 === 0 ? "even" : "odd"); * * console.log(aggregator.toObject()); // { odd: [-3, -1, 3, 5], even: [0, 2, 6, 8] } * ``` * * --- * * @returns An {@link Object} containing the elements of the iterator. */ public toObject(): Record<K, T[]> { const groups = { } as Record<K, T[]>; for (const [key, element] of this._elements) { const value = groups[key] ?? []; value.push(element); groups[key] = value; } return groups; } public readonly [Symbol.toStringTag]: string = "AggregatedIterator"; }