From 97a17208b283c380b4311e4d78dc3e9690216283 Mon Sep 17 00:00:00 2001 From: Kir_Antipov Date: Sat, 31 Dec 2022 10:46:04 +0000 Subject: [PATCH] Implemented array-like methods for iterables --- src/utils/collections/iterable.ts | 2023 +++++++++++++++++ tests/unit/utils/collections/iterable.spec.ts | 1606 +++++++++++++ 2 files changed, 3629 insertions(+) create mode 100644 src/utils/collections/iterable.ts create mode 100644 tests/unit/utils/collections/iterable.spec.ts diff --git a/src/utils/collections/iterable.ts b/src/utils/collections/iterable.ts new file mode 100644 index 0000000..fe1d75a --- /dev/null +++ b/src/utils/collections/iterable.ts @@ -0,0 +1,2023 @@ +import { Comparer, createDefaultComparer, createDefaultEqualityComparer, EqualityComparer } from "@/utils/comparison"; +import { ArrayMap } from "./map"; +import { ArraySet } from "./set"; + +/** + * Determines whether a value is iterable.H + * + * @template T - The type of elements in the iterable. + * + * @param iterable - The value to check. + * + * @returns `true` if the value is iterable; otherwise, `false`. + */ +export function isIterable(iterable: unknown): iterable is Iterable { + return typeof iterable?.[Symbol.iterator] === "function"; +} + +/** + * Returns the elements of an iterable that meet the condition specified in a callback function. + * + * @template T - The type of the elements in the iterable. + * @template S - The type of the elements in the resulting iterable. + * + * @param iterable - The iterable to filter. + * @param predicate - A function to test each element of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns An iterable that contains the elements from the input iterable that satisfy the condition specified by the predicate function. + */ +export function filter(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): Iterable; + +/** + * Returns the elements of an iterable that meet the condition specified in a callback function. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to filter. + * @param predicate - A function to test each element of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns An iterable that contains the elements from the input iterable that satisfy the condition specified by the predicate function. + */ +export function filter(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): Iterable; + +/** + * Returns the elements of an iterable that meet the condition specified in a callback function. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to filter. + * @param predicate - A function to test each element of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns An iterable that contains the elements from the input iterable that satisfy the condition specified by the predicate function. + */ +export function* filter(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): Iterable { + predicate = thisArg === undefined ? predicate : predicate.bind(thisArg); + + let i = 0; + for (const value of iterable) { + if (predicate(value, i++, iterable)) { + yield value; + } + } +} + +/** + * Returns an iterable that contains only the distinct elements of the input iterable. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to filter. + * @param comparer - An optional function to compare values for equality. + * + * @returns An iterable containing only the distinct elements of the input iterable. + */ +export function distinct(iterable: Iterable, comparer?: EqualityComparer): Iterable { + return comparer ? new ArraySet(iterable, comparer) : new Set(iterable); +} + +/** + * Returns a new iterable that contains only the distinct elements of the input iterable, based on the selected property. + * + * @template T - The type of the elements in the iterable. + * @template U - The type of the property used for comparison. + * + * @param iterable - The iterable to filter. + * @param selector - A function to select the property used for comparison. + * @param comparer - An optional function to compare values for equality. + * + * @returns An iterable containing the distinct elements of the input iterable based on the selected property. + */ +export function distinctBy(iterable: Iterable, selector: (value: T) => U, comparer?: EqualityComparer): Iterable { + if (comparer) { + const valueComparer = (a: T, b: T) => comparer(selector(a), selector(b)); + return new ArraySet(iterable, valueComparer); + } + + return new Map(map(iterable, x => [selector(x), x] as const)).values(); +} + +/** + * Executes a provided function on every element of the iterable and returns the results in a new iterable. + * + * @template T - The type of the elements in the input iterable. + * @template U - The type of the elements in the resulting iterable. + * + * @param iterable - The iterable to map. + * @param callbackFn - The function to apply to each element in the input iterable. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns A new iterable containing the results of applying the callback function to each element in the input iterable. + */ +export function* map(iterable: Iterable, callbackFn: (value: T, index: number, iterable: Iterable) => U, thisArg?: unknown): Iterable { + callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg); + + let i = 0; + for (const value of iterable) { + yield callbackFn(value, i++, iterable); + } +} + +/** + * Executes a provided function on every element of the iterable and flattens the results into a new iterable. + * + * @template T - The type of the elements in the input iterable. + * @template U - The type of the elements in the resulting iterable. + * + * @param iterable - The iterable to flat map. + * @param callbackFn - The function to apply to each element in the input iterable. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns A new iterable containing the flattened results of applying the callback function to each element in the input iterable. + */ +export function* flatMap(iterable: Iterable, callbackFn: (value: T, index: number, iterable: Iterable) => Iterable, thisArg?: unknown): Iterable { + callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg); + + let i = 0; + for (const value of iterable) { + yield* callbackFn(value, i++, iterable); + } +} + +/** + * Applies a provided function to each element of the iterable, ultimately reducing the iterable to a single value. + * + * @template T - The type of the elements in the input iterable. + * @template U - The type of the accumulator and the resulting single value. + * + * @param iterable - The iterable to reduce. + * @param callbackFn - The function to apply to each element in the input iterable and the accumulator. + * @param initialValue - The initial value to use as the accumulator. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns The accumulated single value resulting from applying the callback function to each element in the input iterable. + */ +export function reduce(iterable: Iterable, callbackFn: (accumulator: U, value: T, index: number, iterable: Iterable) => U, initialValue: NonNullable, thisArg?: unknown): U; + +/** + * Applies a provided function to each element of the iterable, ultimately reducing the iterable to a single value. + * + * @template T - The type of the elements in the input iterable. + * @template U - The type of the accumulator and the resulting single value. + * + * @param iterable - The iterable to reduce. + * @param callbackFn - The function to apply to each element in the input iterable and the accumulator. + * @param initialValue - The initial value to use as the accumulator. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns The accumulated single value resulting from applying the callback function to each element in the input iterable. + */ +export function reduce(iterable: Iterable, callbackFn: (accumulator: T, value: T, index: number, iterable: Iterable) => T, initialValue?: T, thisArg?: unknown): T; + +/** + * Applies a provided function to each element of the iterable, ultimately reducing the iterable to a single value. + * + * @template T - The type of the elements in the input iterable. + * @template U - The type of the accumulator and the resulting single value. + * + * @param iterable - The iterable to reduce. + * @param callbackFn - The function to apply to each element in the input iterable and the accumulator. + * @param initialValue - The initial value to use as the accumulator. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns The accumulated single value resulting from applying the callback function to each element in the input iterable. + */ +export function reduce(iterable: Iterable, callbackFn: (accumulator: U, value: T, index: number, iterable: Iterable) => U, initialValue?: U, thisArg?: unknown): U { + callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg); + + let accumulator = initialValue; + let i = 0; + for (const value of iterable) { + if (accumulator === undefined && i === 0) { + accumulator = value as unknown as U; + } else { + accumulator = callbackFn(accumulator, value, i, iterable); + } + ++i; + } + + return accumulator; +} + +/** + * Returns an iterable that skips the first `count` elements of the input iterable. + * + * @template T - The type of elements in the input iterable. + * + * @param iterable - The input iterable. + * @param count - The number of elements to skip. Must be a non-negative integer. + * + * @returns An iterable that contains the remaining elements after skipping `count` elements. + */ +export function* skip(iterable: Iterable, count: number): Iterable { + const it = iterable[Symbol.iterator](); + for (let i = 0; i < count; ++i) { + const { done } = it.next(); + if (done) { + return; + } + } + yield* { [Symbol.iterator]: () => it }; +} + +/** + * Returns an iterable that contains the first `count` elements of the input iterable. + * + * @template T - The type of elements in the input iterable. + * + * @param iterable - The input iterable. + * @param count - The number of elements to take. Must be a non-negative integer. + * + * @returns An iterable that contains the first `count` elements of the input iterable. + */ +export function* take(iterable: Iterable, count: number): Iterable { + let i = 0; + for (const value of iterable) { + if (++i > count) { + return; + } + + yield value; + } +} + +/** + * Returns an iterable containing the last `count` elements of the input iterable. + * + * @template T - The type of elements in the input iterable. + * + * @param iterable - The input iterable. + * @param count - The number of elements to include in the output iterable. + * + * @returns An iterable containing the last `count` elements of the input iterable. + */ +export function takeLast(iterable: Iterable, count: number): Iterable { + const buffer = [] as T[]; + for (const item of iterable) { + buffer.push(item); + if (buffer.length > count) { + buffer.shift(); + } + } + return buffer; +} + +/** + * Returns an iterable that contains a subset of the elements in the input iterable. + * + * @template T - The type of elements in the input iterable. + * + * @param iterable - The input iterable. + * @param start - The starting index *(inclusive)*. If omitted, defaults to `0`. + * @param end - The ending index *(exclusive)*. If omitted, returns all elements after the `start` index. + * + * @returns An iterable that contains a subset of the elements in the input iterable. + */ +export function slice(iterable: Iterable, start?: number, end?: number): Iterable { + if (end === 0) { + return []; + } + + const isRelative = start < 0 || end < 0; + if (isRelative) { + return asArray(iterable).slice(start, end); + } + + start ||= 0; + const skipped = start === 0 ? iterable : skip(iterable, start); + const took = end === undefined ? skipped : take(skipped, end - start); + return took; +} + +/** + * Returns a new array with the elements of the input iterable in reverse order. + * + * @remarks + * + * This function will eagerly iterate over the input iterable and return an array with its elements in reverse order. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to reverse. + * + * @returns A new array with the elements of the input iterable in reverse order. + */ +export function reverse(iterable: Iterable): T[] { + return [...iterable].reverse(); +} + +/** + * Returns a new array with the elements of the input iterable sorted according to the specified comparer function. + * + * @remarks + * + * This function will eagerly iterate over the input iterable and return a new array with its elements sorted in ascending order. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to sort. + * @param comparer - An optional function that compares two elements and returns a number indicating their relative order. + * + * @returns A new array with the elements of the input iterable sorted according to the specified comparer function. + */ +export function sort(iterable: Iterable, comparer?: Comparer): T[] { + return [...iterable].sort(comparer || createDefaultComparer()); +} + +/** + * Checks whether all elements of an iterable satisfy a specific condition. + * + * @template T - The type of the elements in the input iterable. + * @template S - The type of the elements in the resulting iterable. + * + * @param iterable - The iterable to check. + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `false` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if every element of the iterable satisfies the condition; otherwise, `false`. + */ +export function every(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): iterable is Iterable; + +/** + * Checks whether all elements of an iterable satisfy a specific condition. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to check. + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `false` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if every element of the iterable satisfies the condition; otherwise, `false`. + */ +export function every(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): boolean; + +/** + * Checks whether all elements of an iterable satisfy a specific condition. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to check. + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `false` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if every element of the iterable satisfies the condition; otherwise, `false`. +*/ +export function every(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): boolean { + predicate = thisArg === undefined ? predicate : predicate.bind(thisArg); + + let i = 0; + for (const value of iterable) { + if (!predicate(value, i++, iterable)) { + return false; + } + } + return true; +} + +/** + * Checks whether any element of an iterable satisfies a specific condition. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to check. + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `true` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if any element of the iterable satisfies the condition; otherwise, `false`. + */ +export function some(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): boolean { + predicate = thisArg === undefined ? predicate : predicate.bind(thisArg); + + let i = 0; + for (const value of iterable) { + if (predicate(value, i++, iterable)) { + return true; + } + } + return false; +} + +/** + * A comparison function that compares two values in an iterable and returns a number indicating their relative order. + * + * @param left - The first value to compare. + * @param right - The second value to compare. + * @param leftIndex - The index of the first value in the iterable. + * @param rightIndex - The index of the second value in the iterable. + * @param iterable - The iterable being compared. + * + * @returns A number indicating the relative order of the two values. If the first value is less than the second, returns a negative number. If the first value is greater than the second, returns a positive number. If the values are equal, returns 0. + */ +interface IterableComparer { + (left: T, right: T, leftIndex: number, rightIndex: number, iterable: Iterable): number; +} + +/** + * Returns the minimum value in an iterable based on a specified comparison function. + * + * @param iterable - The iterable from which to find the minimum value. + * @param comparer - An optional comparison function that determines the order of the elements. If not provided, the default comparison function will be used. + * @param thisArg - An optional object to use as `this` when executing the comparison function. + * + * @returns The minimum value in the iterable, or `undefined` if the iterable is empty. + */ +export function min(iterable: Iterable, comparer?: IterableComparer, thisArg?: unknown): T | undefined { + return extremum(iterable, -1, comparer, thisArg); +} + +/** + * Returns the maximum value in an iterable based on a specified comparison function. + * + * @param iterable - The iterable from which to find the maximum value. + * @param comparer - An optional comparison function that determines the order of the elements. If not provided, the default comparison function will be used. + * @param thisArg - An optional object to use as `this` when executing the comparison function. + * + * @returns The maximum value in the iterable, or `undefined` if the iterable is empty. + */ +export function max(iterable: Iterable, comparer?: IterableComparer, thisArg?: unknown): T | undefined { + return extremum(iterable, 1, comparer, thisArg); +} + +/** + * Finds the extreme value in an iterable based on a specified comparison sign and comparison function. + * + * @param iterable - The iterable from which to find the extreme value. + * @param comparisonSign - A positive number to indicate maximum search; a negative number to indicate minimum search. + * @param comparer - An optional comparison function that determines the order of the elements. If not provided, the default comparison function will be used. + * @param thisArg - An optional object to use as `this` when executing the comparison function. + * + * @returns The extreme value in the iterable, or `undefined` if the iterable is empty. + */ +function extremum(iterable: Iterable, comparisonSign: 1 | -1, comparer?: IterableComparer, thisArg?: unknown): T | undefined { + comparer ||= createDefaultComparer(); + comparer = thisArg === undefined ? comparer : comparer.bind(thisArg); + + let currentValue = undefined; + let currentValueIndex = -1; + let i = -1; + for (const value of iterable) { + ++i; + + if (currentValueIndex === -1) { + currentValue = value; + currentValueIndex = i; + continue; + } + + if (Math.sign(comparer(value, currentValue, i, currentValueIndex, iterable)) === comparisonSign) { + currentValue = value; + currentValueIndex = i; + } + } + return currentValue; +} + +/** + * Counts the number of elements in an iterable that satisfy a specific condition. + * + * @remarks + * + * If no predicate function is provided, this method returns the length of the iterable. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to check. + * @param predicate - The count method calls the predicate function for each element in the iterable and increments the counter if the predicate returns a value which is coercible to the `true` boolean value. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns The number of elements in the iterable that satisfy the condition. + */ +export function count(iterable: Iterable, predicate?: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): number { + if (!predicate && Array.isArray(iterable)) { + return iterable.length; + } + + let count = 0; + if (predicate) { + predicate = thisArg === undefined ? predicate : predicate.bind(thisArg); + let i = 0; + for (const value of iterable) { + if (predicate(value, i++, iterable)) { + ++count; + } + } + } else { + for (const _value of iterable) { + ++count; + } + } + return count; +} + +/** + * Returns the index of the first occurrence of a specified value in an iterable object. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable object to search for the specified value. + * @param searchElement - The value to search for in the iterable object. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal; otherwise, `false`. + * + * @returns The index of the first occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ +export function indexOf(iterable: Iterable, searchElement: T, comparer?: EqualityComparer): number; + +/** + * Returns the index of the first occurrence of a specified value in an iterable object, starting the search at a specified index. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable object to search for the specified value. + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index to start the search at. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the first occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ +export function indexOf(iterable: Iterable, searchElement: T, fromIndex?: number, comparer?: EqualityComparer): number; + +/** + * Returns the index of the first occurrence of a specified value in an iterable object, starting the search at a specified index. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable object to search for the specified value. + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index to start the search at. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the first occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ +export function indexOf(iterable: Iterable, searchElement: T, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): number { + if (typeof fromIndex !== "number") { + comparer = fromIndex; + fromIndex = 0; + } + + fromIndex ??= 0; + comparer ??= createDefaultEqualityComparer(); + + let i = 0; + for (const value of iterable) { + if (i>= fromIndex && comparer(searchElement, value)) { + return i; + } + ++i; + } + return -1; +} + +/** + * Returns the index of the last occurrence of a specified value in an iterable object. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable object to search for the specified value. + * @param searchElement - The value to search for in the iterable object. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal; otherwise, `false`. + * + * @returns The index of the last occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ +export function lastIndexOf(iterable: Iterable, searchElement: T, comparer?: EqualityComparer): number; + +/** + * Returns the index of the last occurrence of a specified value in an iterable object, starting the search at a specified index. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable object to search for the specified value. + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index at which to begin searching backward. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the last occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ +export function lastIndexOf(iterable: Iterable, searchElement: T, fromIndex?: number, comparer?: EqualityComparer): number; + +/** + * Returns the index of the last occurrence of a specified value in an iterable object, starting the search at a specified index. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable object to search for the specified value. + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index at which to begin searching backward. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the last occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ +export function lastIndexOf(iterable: Iterable, searchElement: T, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): number { + if (typeof fromIndex !== "number") { + comparer = fromIndex; + fromIndex = Infinity; + } + + fromIndex ??= Infinity; + comparer ??= createDefaultEqualityComparer(); + + let i = 0; + let lastIndex = -1; + for (const value of iterable) { + if (i >= fromIndex) { + break; + } + + if (comparer(searchElement, value)) { + lastIndex = i; + } + ++i; + } + return lastIndex; +} + +/** + * Determines whether an iterable includes a certain element, returning `true` or `false` as appropriate. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search for the element. + * @param searchElement - The element to search for. + * @param comparer - An optional function to use for comparing elements. + * + * @returns A boolean indicating whether the element was found in the iterable. + */ +export function includes(iterable: Iterable, searchElement: T, comparer?: EqualityComparer): boolean; + +/** + * Determines whether an iterable includes a certain element, returning `true` or `false` as appropriate. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search for the element. + * @param searchElement - The element to search for. + * @param fromIndex - The position in the iterable at which to begin searching for the element. + * @param comparer - An optional function to use for comparing elements. + * + * @returns A boolean indicating whether the element was found in the iterable. + */ +export function includes(iterable: Iterable, searchElement: T, fromIndex?: number, comparer?: EqualityComparer): boolean; + +/** + * Determines whether an iterable includes a certain element, returning `true` or `false` as appropriate. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search for the element. + * @param searchElement - The element to search for. + * @param fromIndex - The position in the iterable at which to begin searching for the element. + * @param comparer - An optional function to use for comparing elements. + * + * @returns A boolean indicating whether the element was found in the iterable. + */ +export function includes(iterable: Iterable, searchElement: T, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): boolean { + return indexOf(iterable, searchElement, fromIndex as number, comparer) !== -1; +} + +/** + * Checks if two iterables are equal, element by element, using an optional custom comparer. + * + * @template T - The type of the elements in the iterable. + * + * @param first - The first iterable to compare. + * @param second - The second iterable to compare. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterables are equal. + */ +export function sequenceEqual(first: Iterable, second: Iterable, comparer?: EqualityComparer): boolean { + comparer ??= createDefaultEqualityComparer(); + + const firstIterator = first[Symbol.iterator](); + const secondIterator = second[Symbol.iterator](); + + let firstCurrentElement = firstIterator.next(); + let secondCurrentElement = secondIterator.next(); + + while (!firstCurrentElement.done && !secondCurrentElement.done) { + if (!comparer(firstCurrentElement.value, secondCurrentElement.value)) { + return false; + } + + firstCurrentElement = firstIterator.next(); + secondCurrentElement = secondIterator.next(); + } + + return firstCurrentElement.done && secondCurrentElement.done; +} + +/** + * Checks if an iterable starts with the specified search elements, using an optional custom comparer. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to search. + * @param searchElements - The elements to search for at the start of the iterable. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable starts with the search elements. + */ +export function startsWith(iterable: Iterable, searchElements: Iterable, comparer?: EqualityComparer): boolean; + +/** + * Checks if an iterable starts with the specified search elements, using an optional custom comparer. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to search. + * @param searchElements - The elements to search for at the start of the iterable. + * @param fromIndex - An optional index to start the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable starts with the search elements. + */ +export function startsWith(iterable: Iterable, searchElements: Iterable, fromIndex?: number, comparer?: EqualityComparer): boolean; + +/** + * Checks if an iterable starts with the specified search elements, using an optional custom comparer. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to search. + * @param searchElements - The elements to search for at the start of the iterable. + * @param fromIndex - An optional index to start the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable starts with the search elements. + */ +export function startsWith(iterable: Iterable, searchElements: Iterable, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): boolean { + if (typeof fromIndex !== "number") { + comparer = fromIndex; + fromIndex = 0; + } + fromIndex ||= 0; + comparer ||= createDefaultEqualityComparer(); + + const iterableIterator = skip(iterable, fromIndex || 0)[Symbol.iterator](); + const searchElementsIterator = searchElements[Symbol.iterator](); + + let iterableElement = iterableIterator.next(); + let searchElement = searchElementsIterator.next(); + + while (!searchElement.done) { + if (iterableElement.done || !comparer(iterableElement.value, searchElement.value)) { + return false; + } + + iterableElement = iterableIterator.next(); + searchElement = searchElementsIterator.next(); + } + + return true; +} + +/** + * Checks if an iterable ends with the specified search elements, using an optional custom comparer. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to search. + * @param searchElements - The elements to search for at the end of the iterable. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable ends with the search elements. + */ +export function endsWith(iterable: Iterable, searchElements: Iterable, comparer?: EqualityComparer): boolean; + +/** + * Checks if an iterable ends with the specified search elements, using an optional custom comparer. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to search. + * @param searchElements - The elements to search for at the end of the iterable. + * @param toIndex - An optional index to end the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable ends with the search elements. + */ +export function endsWith(iterable: Iterable, searchElements: Iterable, toIndex?: number, comparer?: EqualityComparer): boolean; + +/** + * Checks if an iterable ends with the specified search elements, using an optional custom comparer. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to search. + * @param searchElements - The elements to search for at the end of the iterable. + * @param toIndex - An optional index to end the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable ends with the search elements. + */ +export function endsWith(iterable: Iterable, searchElements: Iterable, toIndex?: number | EqualityComparer, comparer?: EqualityComparer): boolean { + if (typeof toIndex !== "number") { + comparer = toIndex; + toIndex = undefined; + } + + const searchElementsBuffered = asArray(searchElements); + const limitedIterable = typeof toIndex === "number" ? take(iterable, toIndex) : iterable; + const lastElements = takeLast(limitedIterable, searchElementsBuffered.length); + return sequenceEqual(lastElements, searchElementsBuffered, comparer); +} + +/** + * Returns the index of the first element in an iterable that satisfies the provided predicate function. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search. + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The index of the first element in the iterable that satisfies the provided predicate function, or `-1` if none are found. + */ +export function findIndex(iterable: Iterable, predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): number { + predicate = thisArg === undefined ? predicate : predicate.bind(thisArg); + + let i = 0; + for (const value of iterable) { + if (predicate(value, i, iterable)) { + return i; + } + ++i; + } + return -1; +} + +/** + * Returns the first element in an iterable that satisfies the provided type guard predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the type predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @template T - The type of the elements in the input iterable. + * @template S - The type of the resulting element. + * + * @param iterable - The iterable to search. + * @param predicate - A type guard function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided type guard predicate function, or `undefined` if none are found. + */ +export function first(iterable: Iterable, predicate?: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): S | undefined; + +/** + * Returns the first element in an iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search. + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ +export function first(iterable: Iterable, predicate?: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined; + +/** + * Returns the first element in an iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search. + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ +export function first(iterable: Iterable, predicate?: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined { + if (!predicate) { + for (const value of iterable) { + return value; + } + return undefined + } + + predicate = thisArg === undefined ? predicate : predicate.bind(thisArg); + let i = 0; + for (const value of iterable) { + if (predicate(value, i++, iterable)) { + return value; + } + } + return undefined; +} + +/** + * Returns the last element in an iterable that satisfies the provided type guard predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the last element in the iterable for which the type predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the last element in the iterable, or `undefined` if the iterable is empty. + * + * @template T - The type of the elements in the input iterable. + * @template S - The type of the resulting element. + * + * @param iterable - The iterable to search. + * @param predicate - A type guard function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The last element in the iterable that satisfies the provided type guard predicate function, or `undefined` if none are found. + */ +export function last(iterable: Iterable, predicate?: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): S | undefined; + +/** + * Returns the last element in an iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the last element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the last element in the iterable, or `undefined` if the iterable is empty. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search. + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The last element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ +export function last(iterable: Iterable, predicate?: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined; + +/** + * Returns the last element in an iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the last element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the last element in the iterable, or `undefined` if the iterable is empty. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable to search. + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The last element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ +export function last(iterable: Iterable, predicate?: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined { + if (!predicate) { + let lastValue = undefined; + for (const value of iterable) { + lastValue = value; + } + return lastValue; + } + + predicate = thisArg === undefined ? predicate : predicate.bind(thisArg); + let i = 0; + let lastValue = undefined; + for (const value of iterable) { + if (predicate(value, i++, iterable)) { + lastValue = value; + } + } + return lastValue; +} + +/** + * Returns the element at the specified index in an iterable object. + * + * @template T - The type of elements in the iterable object. + * + * @param iterable - The iterable object to get the element from. + * @param index - The zero-based index of the element to get. + * + * @returns The element at the specified index or `undefined` if the index is out of range or the iterable is empty. + */ +export function at(iterable: Iterable, index: number): T | undefined { + if (Array.isArray(iterable)) { + return iterable.at(index); + } + + const isRelative = index < 0; + if (isRelative) { + return first(takeLast(iterable, -index)); + } + + return first(skip(iterable, index)); +} + +/** + * Concatenates the elements in an iterable object using a specified separator between each element. + * + * @param iterable - The iterable object to concatenate. + * @param separator - The string to use as a separator. If omitted, a comma (`,`) is used. + * + * @returns The concatenated string. + */ +export function join(iterable: Iterable, separator?: string): string { + return asArray(iterable).join(separator); +} + +/** + * Concatenates multiple iterable objects into a single iterable object. + * + * @template T - The type of elements in the iterable objects. + * + * @param iterables - The iterable objects to concatenate. + * + * @returns An iterable object that contains all the elements of the input iterable objects in the order they were passed in. + */ +export function* concat(...iterables: Iterable[]): Iterable { + for (const iterable of iterables) { + yield* iterable; + } +} + +/** + * Prepends the specified value to an iterable and returns a new iterable. + * + * @param iterable - The iterable to prepend the value to. + * @param value - The value to prepend to the iterable. + * + * @returns A new iterable with the specified value prepended. + */ +export function* prepend(iterable: Iterable, value: T): Iterable { + yield value; + yield* iterable; +} + +/** + * Appends the specified value to an iterable and returns a new iterable. + * + * @param iterable - The iterable to append the value to. + * @param value - The value to append to the iterable. + * + * @returns A new iterable with the specified value appended. + */ +export function* append(iterable: Iterable, value: T): Iterable { + yield* iterable; + yield value; +} + +/** + * Removes the last element from the input iterable and returns that element and a new iterable without the removed element. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable from which to remove the last element. + * + * @returns A tuple containing the removed element and a new iterable without the removed element. + */ +export function pop(iterable: Iterable): [T, Iterable] { + const buffer = [...iterable]; + const value = buffer.pop(); + return [value, buffer]; +} + +/** + * Removes the first element from the input iterable and returns that element and a new iterable without the removed element. + * + * @template T - The type of the elements in the input iterable. + * + * @param iterable - The iterable from which to remove the first element. + * + * @returns A tuple containing the removed element and a new iterable without the removed element. + */ +export function shift(iterable: Iterable): [T, Iterable] { + const iterator = iterable[Symbol.iterator](); + const firstIteration = iterator.next(); + const firstElement = firstIteration.done ? undefined : firstIteration.value; + return [firstElement, { [Symbol.iterator]: () => iterator }]; +} + +/** + * Calls a function for each element in an iterable object. + * + * @template T - The type of elements in the iterable object. + * + * @param iterable - The iterable object to iterate over. + * @param callbackFn - A function to call for each element in the iterable object. + * @param thisArg - An object to use as `this` when executing the `callbackFn` function. + */ +export function forEach(iterable: Iterable, callbackFn: (value: T, index: number, iterable: Iterable) => void, thisArg?: unknown): void { + callbackFn = thisArg === undefined ? callbackFn : callbackFn.bind(thisArg); + + let i = 0; + for (const value of iterable) { + callbackFn(value, i++, iterable); + } +} + +/** + * Converts an iterable to an array. + * + * If the iterable is already an array, a reference to the same array will be returned. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to convert to an array. + * + * @returns An array containing all the elements of the iterable, or a reference to the same array if it is already an array. + */ +export function asArray(iterable: Iterable): T[] { + return Array.isArray(iterable) ? iterable : [...iterable]; +} + +/** + * Converts an iterable to an array or an {@link ArrayLikeIterable}. + * + * If the iterable is already an array, a reference to the same array will be returned. + * If the iterable is not an array, an {@link ArrayLikeIterable} object will be returned. + * + * @template T - The type of the elements in the iterable. + * + * @param iterable - The iterable to convert to an array or an {@link ArrayLikeIterable}. + * + * @returns A reference to the same array if it is already an array, or an {@link ArrayLikeIterable} object if the iterable is not an array. + */ +export function asArrayLike(iterable: Iterable): T[] | ArrayLikeIterable { + return Array.isArray(iterable) ? iterable : $i(iterable); +} + +/** + * Wraps an iterable and adds array-like functionality to it. + * + * @template T - The type of elements in the iterable. + * + * @param iterable - The iterable to wrap. + * + * @returns A new instance of the {@link ArrayLikeIterable} class. + */ +export function $i(iterable: Iterable): ArrayLikeIterable { + return iterable instanceof ArrayLikeIterable ? iterable : ArrayLikeIterable.from(iterable); +} + +/** + * Wraps an iterable and adds array-like functionality to it. + * + * @template T - The type of elements in the iterable. + */ +export class ArrayLikeIterable implements Iterable { + /** + * The original iterable, wrapped by this instance. + */ + private readonly _iterable: Iterable; + + /** + * Creates a new instance of the {@link ArrayLikeIterable} class. + * + * @param iterable - The iterable to wrap. + */ + private constructor(iterable: Iterable) { + this._iterable = iterable; + } + + /** + * Creates a new instance of the {@link ArrayLikeIterable} class from an iterable. + * + * @template T - The type of elements in the iterable. + * + * @param iterable - The iterable to wrap. + * + * @returns A new instance of the {@link ArrayLikeIterable} class. + */ + static from(iterable: Iterable): ArrayLikeIterable { + return new ArrayLikeIterable(iterable); + } + + /** + * Creates a new instance of the {@link ArrayLikeIterable} class from an iterator. + * + * @template T - The type of elements in the iterable. + * + * @param iterator - The iterator to wrap. + * + * @returns A new instance of the {@link ArrayLikeIterable} class. + */ + static of(iterator: Iterator): ArrayLikeIterable { + return new ArrayLikeIterable({ [Symbol.iterator]: () => iterator }); + } + + /** + * Returns the number of elements in this iterable. + * + * @remarks + * + * Accessing this property will cause the iterable to be fully evaluated, + * which may and definitely will result in performance overhead for large iterables. + */ + get length(): number { + return this.count(); + } + + /** + * Returns the elements of the iterable that meet the condition specified in a callback function. + * + * @template S - The type of the elements in the resulting iterable. + * + * @param predicate - A function to test each element of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns An iterable that contains the elements from the input iterable that satisfy the condition specified by the predicate function. + */ + filter(predicate: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): ArrayLikeIterable; + + /** + * Returns the elements of the iterable that meet the condition specified in a callback function. + * + * @param predicate - A function to test each element of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns An iterable that contains the elements from the input iterable that satisfy the condition specified by the predicate function. + */ + filter(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): ArrayLikeIterable; + + /** + * Returns the elements of the iterable that meet the condition specified in a callback function. + * + * @param predicate - A function to test each element of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns An iterable that contains the elements from the input iterable that satisfy the condition specified by the predicate function. + */ + filter(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): ArrayLikeIterable { + return ArrayLikeIterable.from(filter(this._iterable, predicate, thisArg)); + } + + /** + * Executes a provided function on every element of the iterable and returns the results in a new iterable. + * + * @template U - The type of the elements in the resulting iterable. + * + * @param callbackFn - The function to apply to each element in the input iterable. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns A new iterable containing the results of applying the callback function to each element in the input iterable. + */ + map(callbackFn: (value: T, index: number, iterable: Iterable) => U, thisArg?: unknown): ArrayLikeIterable { + return ArrayLikeIterable.from(map(this._iterable, callbackFn, thisArg)); + } + + /** + * Executes a provided function on every element of the iterable and flattens the results into a new iterable. + * + * @template U - The type of the elements in the resulting iterable. + * + * @param callbackFn - The function to apply to each element in the input iterable. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns A new iterable containing the flattened results of applying the callback function to each element in the input iterable. + */ + flatMap(callbackFn: (value: T, index: number, iterable: Iterable) => Iterable, thisArg?: unknown): ArrayLikeIterable { + return ArrayLikeIterable.from(flatMap(this._iterable, callbackFn, thisArg)); + } + + /** + * Applies a provided function to each element of the iterable, ultimately reducing the iterable to a single value. + * + * @template U - The type of the accumulator and the resulting single value. + * + * @param callbackFn - The function to apply to each element in the input iterable and the accumulator. + * @param initialValue - The initial value to use as the accumulator. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns The accumulated single value resulting from applying the callback function to each element in the input iterable. + */ + reduce(callbackFn: (accumulator: U, value: T, index: number, iterable: Iterable) => U, initialValue: NonNullable, thisArg?: unknown): U; + + /** + * Applies a provided function to each element of the iterable, ultimately reducing the iterable to a single value. + * + * @template U - The type of the accumulator and the resulting single value. + * + * @param callbackFn - The function to apply to each element in the input iterable and the accumulator. + * @param initialValue - The initial value to use as the accumulator. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns The accumulated single value resulting from applying the callback function to each element in the input iterable. + */ + reduce(callbackFn: (accumulator: T, value: T, index: number, iterable: Iterable) => T, initialValue?: T, thisArg?: unknown): T; + + /** + * Applies a provided function to each element of the iterable, ultimately reducing the iterable to a single value. + * + * @template U - The type of the accumulator and the resulting single value. + * + * @param callbackFn - The function to apply to each element in the input iterable and the accumulator. + * @param initialValue - The initial value to use as the accumulator. + * @param thisArg - The value to use as `this` when executing the callback function. + * + * @returns The accumulated single value resulting from applying the callback function to each element in the input iterable. + */ + reduce(callbackFn: (accumulator: U, value: T, index: number, iterable: Iterable) => U, initialValue?: U, thisArg?: unknown): U { + return reduce(this._iterable, callbackFn, initialValue, thisArg); + } + + /** + * Returns an iterable that skips the first `count` elements of the input iterable. + * + * @param count - The number of elements to skip. Must be a non-negative integer. + * + * @returns An iterable that contains the remaining elements after skipping `count` elements. + */ + skip(count: number): ArrayLikeIterable { + return ArrayLikeIterable.from(skip(this._iterable, count)); + } + + /** + * Returns an iterable that contains the first `count` elements of the input iterable. + * + * @param count - The number of elements to take. Must be a non-negative integer. + * + * @returns An iterable that contains the first `count` elements of the input iterable. + */ + take(count: number): ArrayLikeIterable { + return ArrayLikeIterable.from(take(this._iterable, count)); + } + + /** + * Returns an iterable containing the last `count` elements of the input iterable. + * + * @param count - The number of elements to include in the output iterable. + * + * @returns An iterable containing the last `count` elements of the input iterable. + */ + takeLast(count: number): ArrayLikeIterable { + return ArrayLikeIterable.from(takeLast(this._iterable, count)); + } + + /** + * Returns an iterable that contains a subset of the elements in the input iterable. + * + * @param start - The starting index *(inclusive)*. If omitted, defaults to `0`. + * @param end - The ending index *(exclusive)*. If omitted, returns all elements after the `start` index. + * + * @returns An iterable that contains a subset of the elements in the input iterable. + */ + slice(start?: number, end?: number): ArrayLikeIterable { + return ArrayLikeIterable.from(slice(this._iterable, start, end)); + } + + /** + * Returns a new iterable with the elements of the input iterable in reverse order. + * + * @returns A new iterable with the elements of the input iterable in reverse order. + */ + reverse(): ArrayLikeIterable { + return ArrayLikeIterable.from(reverse(this._iterable)); + } + + /** + * Returns a new iterable with the elements of the input iterable sorted according to the specified comparer function. + * + * @param comparer - An optional function that compares two elements and returns a number indicating their relative order. + * + * @returns A new iterable with the elements of the input iterable sorted according to the specified comparer function. + */ + sort(comparer?: Comparer): ArrayLikeIterable { + return ArrayLikeIterable.from(sort(this._iterable, comparer)); + } + + /** + * Checks whether all elements of an iterable satisfy a specific condition. + * + * @template S - The type of the elements in the resulting iterable. + * + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `false` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if every element of the iterable satisfies the condition; otherwise, `false`. + */ + every(predicate: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): this is ArrayLikeIterable; + + /** + * Checks whether all elements of an iterable satisfy a specific condition. + * + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `false` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if every element of the iterable satisfies the condition; otherwise, `false`. + */ + every(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): boolean; + + /** + * Checks whether all elements of an iterable satisfy a specific condition. + * + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `false` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if every element of the iterable satisfies the condition; otherwise, `false`. + */ + every(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): boolean { + return every(this._iterable, predicate, thisArg); + } + + /** + * Checks whether any element of the iterable satisfies a specific condition. + * + * @param predicate - This function will be called for each element in the iterable until it returns a value which is coercible to the `true` boolean value or until the end of the iterable. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns `true` if any element of the iterable satisfies the condition; otherwise, `false`. + */ + some(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): boolean { + return some(this._iterable, predicate, thisArg); + } + + /** + * Returns the minimum value in the iterable based on a specified comparison function. + * + * @param comparer - An optional comparison function that determines the order of the elements. If not provided, the default comparison function will be used. + * @param thisArg - An optional object to use as `this` when executing the comparison function. + * + * @returns The minimum value in the iterable, or `undefined` if the iterable is empty. + */ + min(comparer?: IterableComparer, thisArg?: unknown): T | undefined { + return min(this._iterable, comparer, thisArg); + } + + /** + * Returns the maximum value in the iterable based on a specified comparison function. + * + * @param comparer - An optional comparison function that determines the order of the elements. If not provided, the default comparison function will be used. + * @param thisArg - An optional object to use as `this` when executing the comparison function. + * + * @returns The maximum value in the iterable, or `undefined` if the iterable is empty. + */ + max(comparer?: IterableComparer, thisArg?: unknown): T | undefined { + return max(this._iterable, comparer, thisArg); + } + + /** + * Counts the number of elements in an iterable that satisfy a specific condition. + * + * @remarks + * + * If no predicate function is provided, this method returns the length of the iterable. + * + * @param predicate - The count method calls the predicate function for each element in the iterable and increments the counter if the predicate returns a value which is coercible to the `true` boolean value. + * @param thisArg - An object to which the `this` keyword can refer in the `predicate` function. + * + * @returns The number of elements in the iterable that satisfy the condition. + */ + count(predicate?: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): number { + return count(this._iterable, predicate, thisArg); + } + + /** + * Returns the index of the first occurrence of a specified value in the iterable object. + * + * @param searchElement - The value to search for in the iterable object. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal; otherwise, `false`. + * + * @returns The index of the first occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ + indexOf(searchElement: T, comparer?: EqualityComparer): number; + + /** + * Returns the index of the first occurrence of a specified value in the iterable object, starting the search at a specified index. + * + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index to start the search at. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the first occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ + indexOf(searchElement: T, fromIndex?: number, comparer?: EqualityComparer): number; + + /** + * Returns the index of the first occurrence of a specified value in the iterable object, starting the search at a specified index. + * + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index to start the search at. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the first occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ + indexOf(searchElement: T, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): number { + return indexOf(this._iterable, searchElement, fromIndex as number, comparer); + } + + /** + * Returns the index of the last occurrence of a specified value in the iterable object. + * + * @param searchElement - The value to search for in the iterable object. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal; otherwise, `false`. + * + * @returns The index of the last occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ + lastIndexOf(searchElement: T, comparer?: EqualityComparer): number; + + /** + * Returns the index of the last occurrence of a specified value in the iterable object, starting the search at a specified index. + * + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index to start the search at. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the last occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ + lastIndexOf(searchElement: T, fromIndex?: number, comparer?: EqualityComparer): number; + + /** + * Returns the index of the last occurrence of a specified value in the iterable object, starting the search at a specified index. + * + * @param searchElement - The value to search for in the iterable object. + * @param fromIndex - The index to start the search at. + * @param comparer - An optional function used to compare equality of values. Returns `true` if the values are equal, otherwise `false`. + * + * @returns The index of the last occurrence of the specified value in the iterable object, or `-1` if it is not found. + */ + lastIndexOf(searchElement: T, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): number { + return lastIndexOf(this._iterable, searchElement, fromIndex as number, comparer); + } + + /** + * Determines whether the iterable includes a certain element, returning `true` or `false` as appropriate. + * + * @param searchElement - The element to search for. + * @param comparer - An optional function to use for comparing elements. + * + * @returns A boolean indicating whether the element was found in the iterable. + */ + includes(searchElement: T, comparer?: EqualityComparer): boolean; + + /** + * Determines whether the iterable includes a certain element, returning `true` or `false` as appropriate. + * + * @param searchElement - The element to search for. + * @param fromIndex - The position in the iterable at which to begin searching for the element. + * @param comparer - An optional function to use for comparing elements. + * + * @returns A boolean indicating whether the element was found in the iterable. + */ + includes(searchElement: T, fromIndex?: number, comparer?: EqualityComparer): boolean; + + /** + * Determines whether the iterable includes a certain element, returning `true` or `false` as appropriate. + * + * @param searchElement - The element to search for. + * @param fromIndex - The position in the iterable at which to begin searching for the element. + * @param comparer - An optional function to use for comparing elements. + * + * @returns A boolean indicating whether the element was found in the iterable. + */ + includes(searchElement: T, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): boolean { + return includes(this._iterable, searchElement, fromIndex as number, comparer); + } + + /** + * Checks if two iterables are equal, element by element, using an optional custom comparer. + * + * @param second - The second iterable to compare. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterables are equal. + */ + sequenceEqual(second: Iterable, comparer?: EqualityComparer): boolean { + return sequenceEqual(this._iterable, second, comparer); + } + + /** + * Checks if the iterable starts with the specified search elements, using an optional custom comparer. + * + * @param searchElements - The elements to search for at the start of the iterable. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable starts with the search elements. + */ + startsWith(searchElements: Iterable, comparer?: EqualityComparer): boolean; + + /** + * Checks if the iterable starts with the specified search elements, using an optional custom comparer. + * + * @param searchElements - The elements to search for at the start of the iterable. + * @param fromIndex - An optional index to start the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable starts with the search elements. + */ + startsWith(searchElements: Iterable, fromIndex?: number, comparer?: EqualityComparer): boolean; + + /** + * Checks if the iterable starts with the specified search elements, using an optional custom comparer. + * + * @param searchElements - The elements to search for at the start of the iterable. + * @param fromIndex - An optional index to start the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable starts with the search elements. + */ + startsWith(searchElements: Iterable, fromIndex?: number | EqualityComparer, comparer?: EqualityComparer): boolean { + return startsWith(this._iterable, searchElements, fromIndex as number, comparer); + } + + /** + * Checks if the iterable ends with the specified search elements, using an optional custom comparer. + * + * @param searchElements - The elements to search for at the end of the iterable. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable ends with the search elements. + */ + endsWith(searchElements: Iterable, comparer?: EqualityComparer): boolean; + + /** + * Checks if the iterable ends with the specified search elements, using an optional custom comparer. + * + * @param searchElements - The elements to search for at the end of the iterable. + * @param toIndex - An optional index to end the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable ends with the search elements. + */ + endsWith(searchElements: Iterable, toIndex?: number, comparer?: EqualityComparer): boolean; + + /** + * Checks if the iterable ends with the specified search elements, using an optional custom comparer. + * + * @param searchElements - The elements to search for at the end of the iterable. + * @param toIndex - An optional index to end the search. + * @param comparer - An optional function for comparing elements for equality. + * + * @returns A boolean indicating whether the iterable ends with the search elements. + */ + endsWith(searchElements: Iterable, toIndex?: number | EqualityComparer, comparer?: EqualityComparer): boolean { + return endsWith(this._iterable, searchElements, toIndex as number, comparer); + } + + /** + * Returns the index of the first element in the iterable that satisfies the provided predicate function. + * + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The index of the first element in the iterable that satisfies the provided predicate function, or `-1` if none are found. + */ + findIndex(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): number { + return findIndex(this._iterable, predicate, thisArg); + } + + /** + * Returns the first element in the iterable that satisfies the provided type guard predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the type predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @template S - The type of the resulting element. + * + * @param predicate - A type guard function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided type guard predicate function, or `undefined` if none are found. + */ + find(predicate: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): S | undefined; + + /** + * Returns the first element in the iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ + find(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined; + + /** + * Returns the first element in the iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ + find(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined { + return this.first(predicate, thisArg); + } + + /** + * Returns the first element in the iterable that satisfies the provided type guard predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the type predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @template S - The type of the resulting element. + * + * @param predicate - A type guard function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided type guard predicate function, or `undefined` if none are found. + */ + first(predicate: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): S | undefined; + + /** + * Returns the first element in the iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ + first(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined; + + /** + * Returns the first element in the iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the first element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the first element in the iterable, or `undefined` if the iterable is empty. + * + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The first element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ + first(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined { + return first(this._iterable, predicate, thisArg); + } + + /** + * Returns the last element in the iterable that satisfies the provided type guard predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the last element in the iterable for which the type predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the last element in the iterable, or `undefined` if the iterable is empty. + * + * @template S - The type of the resulting element. + * + * @param predicate - A type guard function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The last element in the iterable that satisfies the provided type guard predicate function, or `undefined` if none are found. + */ + last(predicate: (value: T, index: number, iterable: Iterable) => value is S, thisArg?: unknown): S | undefined; + + /** + * Returns the last element in the iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the last element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the last element in the iterable, or `undefined` if the iterable is empty. + * + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The last element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ + last(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined; + + /** + * Returns the last element in the iterable that satisfies the provided predicate function. + * + * @remarks + * - If the `predicate` is passed, this function returns the last element in the iterable for which the predicate returns `true`, or `undefined` if none are found. + * - If the `predicate` is not passed, this function returns the last element in the iterable, or `undefined` if the iterable is empty. + * + * @param predicate - A function to test each element for a condition. + * @param thisArg - An optional object to use as `this` when executing the `predicate`. + * + * @returns The last element in the iterable that satisfies the provided predicate function, or `undefined` if none are found. + */ + last(predicate: (value: T, index: number, iterable: Iterable) => unknown, thisArg?: unknown): T | undefined { + return last(this._iterable, predicate, thisArg); + } + + /** + * Returns the element at the specified index in an iterable object. + * + * @param index - The zero-based index of the element to get. + * + * @returns The element at the specified index or `undefined` if the index is out of range or the iterable is empty. + */ + at(index: number): T | undefined { + return at(this._iterable, index); + } + + /** + * Concatenates the elements in an iterable object using a specified separator between each element. + * + * @param separator - The string to use as a separator. If omitted, a comma (`,`) is used. + * + * @returns The concatenated string. + */ + join(separator?: string): string { + return join(this._iterable, separator); + } + + /** + * Concatenates multiple iterable objects into a single iterable object. + * + * @param iterables - The iterable objects to concatenate. + * + * @returns An iterable object that contains all the elements of the input iterable objects in the order they were passed in. + */ + concat(...iterables: Iterable[]): ArrayLikeIterable { + return ArrayLikeIterable.from(concat(this._iterable, ...iterables)); + } + + /** + * Prepends the specified value to this iterable and returns a new iterable. + * + * @param value - The value to prepend to the iterable. + * + * @returns A new iterable with the specified value prepended. + */ + prepend(value: T): ArrayLikeIterable { + return ArrayLikeIterable.from(prepend(this._iterable, value)); + } + + /** + * Appends the specified value to this iterable and returns a new iterable. + * + * @param value - The value to append to the iterable. + * + * @returns A new iterable with the specified value appended. + */ + append(value: T): ArrayLikeIterable { + return ArrayLikeIterable.from(append(this._iterable, value)); + } + + /** + * Removes the first element from the input iterable and returns that element and a new iterable without the removed element. + * + * @returns A tuple containing the removed element and a new iterable without the removed element. + */ + shift(): [T, ArrayLikeIterable] { + const [value, iterable] = shift(this._iterable); + return [value, ArrayLikeIterable.from(iterable)]; + } + + /** + * Prepends the specified value to this iterable and returns a new iterable. + * + * @param value - The value to prepend to the iterable. + * + * @returns A new iterable with the specified value prepended. + */ + unshift(value: T): ArrayLikeIterable { + return this.prepend(value); + } + + /** + * Appends the specified value to this iterable and returns a new iterable. + * + * @param value - The value to append to the iterable. + * + * @returns A new iterable with the specified value appended. + */ + push(value: T): ArrayLikeIterable { + return this.append(value); + } + + /** + * Removes the last element from the input iterable and returns that element and a new iterable without the removed element. + * + * @returns A tuple containing the removed element and a new iterable without the removed element. + */ + pop(): [T, ArrayLikeIterable] { + const [value, iterable] = pop(this._iterable); + return [value, ArrayLikeIterable.from(iterable)]; + } + + /** + * Returns an iterable of indices in the iterable. + */ + keys(): Iterable { + return map(this._iterable, (_value, i) => i); + } + + /** + * Returns an iterable of values in the iterable. + */ + values(): Iterable { + return this._iterable; + } + + /** + * Returns an iterable of index, value pairs for every entry in the iterable. + */ + entries(): Iterable<[number, T]> { + return map(this._iterable, (value, i) => [i, value]); + } + + /** + * Calls a function for each element in an iterable object. + * + * @param callbackFn - A function to call for each element in the iterable object. + * @param thisArg - An object to use as `this` when executing the `callbackFn` function. + */ + forEach(callbackFn: (value: T, index: number, iterable: Iterable) => void, thisArg?: unknown): void { + return forEach(this._iterable, callbackFn, thisArg); + } + + /** + * Converts the iterable to an array. + * + * If the iterable is already an array, a reference to the same array will be returned. + */ + asArray(): readonly T[] { + return asArray(this._iterable); + } + + /** + * Returns an array containing all elements of this iterable. + */ + toArray(): T[] { + return [...this._iterable]; + } + + /** + * Converts the iterable of key-value pairs into a Map. + * + * @template K - The type of the keys in the key-value pairs. + * @template V - The type of the values in the key-value pairs. + * + * @param comparer - Optional custom equality comparer for the keys. + * + * @returns A Map containing the key-value pairs from this iterable. + */ + toMap(this: ArrayLikeIterable, comparer?: EqualityComparer): Map { + return comparer ? new ArrayMap(this._iterable, comparer) : new Map(this._iterable); + } + + /** + * Converts the iterable into a Set. + * + * @param comparer - Optional custom equality comparer for the values. + * + * @returns A Set containing the values from this iterable. + */ + toSet(comparer?: EqualityComparer): Set { + return comparer ? new ArraySet(this._iterable, comparer) : new Set(this._iterable); + } + + /** + * Converts the iterable of key-value pairs into a Record. + * + * @template K - The type of the keys in the key-value pairs. + * @template V - The type of the values in the key-value pairs. + * + * @returns A Record containing the key-value pairs from this iterable. + */ + toRecord(this: ArrayLikeIterable): Record { + return reduce(this._iterable, (record, [key, value]) => { + record[key] = value; + return record; + }, { } as Record); + } + + /** + * Returns an iterable that contains only the distinct elements of the current iterable. + * + * @param comparer - An optional function to compare values for equality. + * + * @returns An iterable containing only the distinct elements of the current iterable. + */ + distinct(comparer?: EqualityComparer): ArrayLikeIterable { + return ArrayLikeIterable.from(distinct(this._iterable, comparer)); + } + + /** + * Returns a new iterable that contains only the distinct elements of the current iterable, based on the selected property. + * + * @template U - The type of the property used for comparison. + * + * @param selector - A function to select the property used for comparison. + * @param comparer - An optional function to compare values for equality. + * + * @returns An iterable containing the distinct elements of the current iterable based on the selected property. + */ + distinctBy(selector: (value: T) => U, comparer?: EqualityComparer): ArrayLikeIterable { + return ArrayLikeIterable.from(distinctBy(this._iterable, selector, comparer)); + } + + /** + * Returns an iterator for this iterable. + */ + [Symbol.iterator](): Iterator { + return this._iterable[Symbol.iterator](); + } + + /** + * Returns a string representation of this object. + */ + get [Symbol.toStringTag](): string { + return "Iterable"; + } +} diff --git a/tests/unit/utils/collections/iterable.spec.ts b/tests/unit/utils/collections/iterable.spec.ts new file mode 100644 index 0000000..213ae76 --- /dev/null +++ b/tests/unit/utils/collections/iterable.spec.ts @@ -0,0 +1,1606 @@ +import { IGNORE_CASE_COMPARER } from "@/utils/comparison/string-comparer"; +import { IGNORE_CASE_EQUALITY_COMPARER } from "@/utils/comparison/string-equality-comparer"; +import * as Iterable from "@/utils/collections/iterable"; + +describe("isIterable", () => { + test("returns true for iterable objects", () => { + expect(Iterable.isIterable([1, 2, 3])).toBe(true); + expect(Iterable.isIterable(new Set())).toBe(true); + expect(Iterable.isIterable(new Map())).toBe(true); + expect(Iterable.isIterable("test")).toBe(true); + }); + + test("returns false for non-iterable objects", () => { + expect(Iterable.isIterable(123)).toBe(false); + expect(Iterable.isIterable({ key: "value" })).toBe(false); + expect(Iterable.isIterable(undefined)).toBe(false); + expect(Iterable.isIterable(null)).toBe(false); + }); +}); + +describe("filter", () => { + test("filters out elements not matching the predicate", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Array.from(Iterable.filter(iterable, x => x > 3)); + + expect(result).toEqual([4, 5]); + }); + + test("binds thisArg to the predicate", () => { + const iterable = [1, 2, 3, 4, 5]; + const thisArg = { threshold: 3 }; + + const result = Array.from(Iterable.filter(iterable, function (x) { return x > this.threshold; }, thisArg)); + + expect(result).toEqual([4, 5]); + }); +}); + +describe("distinct", () => { + test("removes duplicate elements", () => { + const iterable = [1, 2, 3, 2, 1]; + + const result = Array.from(Iterable.distinct(iterable)); + + expect(result).toEqual([1, 2, 3]); + }); + + test("uses provided comparer for equality check", () => { + const iterable = [{ id: 1 }, { id: 2 }, { id: 1 }, { id: 2 }]; + + const result = Array.from(Iterable.distinct(iterable, (a, b) => a.id === b.id)); + + expect(result).toEqual([{ id: 1 }, { id: 2 }]); + }); +}); + +describe("distinctBy", () => { + test("removes elements with duplicate selected property", () => { + const iterable = [{ id: 1 }, { id: 2 }, { id: 1 }, { id: 2 }]; + + const result = Array.from(Iterable.distinctBy(iterable, x => x.id)); + + expect(result).toEqual([{ id: 1 }, { id: 2 }]); + }); + + test("uses provided comparer for equality check of selected property", () => { + const iterable = [{ id: "a" }, { id: "b" }, { id: "A" }, { id: "B" }]; + + const result = Array.from(Iterable.distinctBy(iterable, x => x.id, IGNORE_CASE_EQUALITY_COMPARER)); + + expect(result).toEqual([{ id: "A" }, { id: "B" }]); + }); +}); + +describe("map", () => { + test("applies callback function to each element", () => { + const iterable = [1, 2, 3]; + + const result = Array.from(Iterable.map(iterable, x => x * 2)); + + expect(result).toEqual([2, 4, 6]); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [1, 2, 3]; + const thisArg = { factor: 2 }; + + const result = Array.from(Iterable.map(iterable, function (x) { return x * this.factor; }, thisArg)); + + expect(result).toEqual([2, 4, 6]); + }); +}); + +describe("flatMap", () => { + test("applies callback function to each element and flattens the result", () => { + const iterable = [1, 2, 3]; + + const result = Array.from(Iterable.flatMap(iterable, x => [x, x * 2])); + + expect(result).toEqual([1, 2, 2, 4, 3, 6]); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [1, 2, 3]; + const thisArg = { factor: 2 }; + + const result = Array.from(Iterable.flatMap(iterable, function (x) { return [x, x * this.factor]; }, thisArg)); + + expect(result).toEqual([1, 2, 2, 4, 3, 6]); + }); +}); + +describe("reduce", () => { + test("reduces iterable to a single value", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.reduce(iterable, (acc, curr) => acc + curr, 0); + + expect(result).toBe(15); + }); + + test("uses the first value as a seed when one was not provided", () => { + const callback = jest.fn().mockImplementation((acc, cur) => acc + cur); + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.reduce(iterable, callback); + + expect(result).toBe(15); + expect(callback).toHaveBeenCalledTimes(4); + // 1st (ie 0th) call - 1 as accumulator, 2 as value, 1 as current index + expect(callback).toHaveBeenNthCalledWith(1, 1, 2, 1, iterable); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [1, 2, 3, 4, 5]; + const thisArg = { factor: 2 }; + + const result = Iterable.reduce(iterable, function (acc, curr) { return acc + curr * this.factor; }, 0, thisArg); + + expect(result).toBe(30); + }); +}); + +describe("skip", () => { + test("skips the first n elements", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Array.from(Iterable.skip(iterable, 2)); + + expect(result).toEqual([3, 4, 5]); + }); + + test("does not skip elements if n <= 0", () => { + expect(Array.from(Iterable.skip([1, 2], 0))).toEqual([1, 2]); + expect(Array.from(Iterable.skip([1, 2], -1))).toEqual([1, 2]); + }); +}); + +describe("take", () => { + test("takes the first n elements", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Array.from(Iterable.take(iterable, 3)); + + expect(result).toEqual([1, 2, 3]); + }); + + test("returns an empty iterable if n <= 0", () => { + expect(Array.from(Iterable.take([1, 2], 0))).toEqual([]); + expect(Array.from(Iterable.take([1, 2], -1))).toEqual([]); + }); +}); + +describe("takeLast", () => { + test("takes the last n elements", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Array.from(Iterable.takeLast(iterable, 2)); + + expect(result).toEqual([4, 5]); + }); + + test("returns an empty iterable if n <= 0", () => { + expect(Array.from(Iterable.takeLast([1, 2], 0))).toEqual([]); + expect(Array.from(Iterable.takeLast([1, 2], -1))).toEqual([]); + }); +}); + +describe("slice", () => { + test("slices the elements between start and end", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Array.from(Iterable.slice(iterable, 1, 4)); + + expect(result).toEqual([2, 3, 4]); + }); + + test("slices the elements using relative indices", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Array.from(Iterable.slice(iterable, -3, -1)); + + expect(result).toEqual([3, 4]); + }); +}); + +describe("reverse", () => { + test("reverses the order of elements", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Array.from(Iterable.reverse(iterable)); + + expect(result).toEqual([5, 4, 3, 2, 1]); + }); + + test("returns an empty iterable if the input is already empty", () => { + expect(Array.from(Iterable.reverse([]))).toEqual([]); + }); +}); + +describe("sort", () => { + test("sorts elements in ascending order by default", () => { + const iterable = [5, 3, 1, 4, 2]; + + const result = Array.from(Iterable.sort(iterable)); + + expect(result).toEqual([1, 2, 3, 4, 5]); + }); + + test("sorts elements according to comparer function", () => { + const iterable = ["Apple", "Pear", "banana", "mango", "Cherry"]; + + const result = Array.from(Iterable.sort(iterable, IGNORE_CASE_COMPARER)); + + expect(result).toEqual(["Apple", "banana", "Cherry", "mango", "Pear"]); + }); +}); + +describe("every", () => { + test("returns true if all elements meet the condition", () => { + const iterable = [2, 4, 6, 8]; + + const result = Iterable.every(iterable, (value) => value % 2 === 0); + + expect(result).toBe(true); + }); + + test("returns false if any element does not meet the condition", () => { + const iterable = [2, 4, 5, 8]; + + const result = Iterable.every(iterable, (value) => value % 2 === 0); + + expect(result).toBe(false); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [2, 4, 6, 8]; + const thisArg = { factor: 2 }; + + const result = Iterable.every(iterable, function (x) { return x % this.factor === 0; }, thisArg); + + expect(result).toBe(true); + }); +}); + +describe("some", () => { + test("returns true if any element meets the condition", () => { + const iterable = [1, 3, 4, 7]; + + const result = Iterable.some(iterable, (value) => value % 2 === 0); + + expect(result).toBe(true); + }); + + test("returns false if no element meets the condition", () => { + const iterable = [1, 3, 5, 7]; + + const result = Iterable.some(iterable, (value) => value % 2 === 0); + + expect(result).toBe(false); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [1, 3, 4, 7]; + const thisArg = { factor: 2 }; + + const result = Iterable.some(iterable, function (x) { return x % this.factor === 0; }, thisArg); + + expect(result).toBe(true); + }); +}); + +describe("min", () => { + test("returns the minimum value in an iterable", () => { + const iterable = [3, 1, 4, 2]; + + const result = Iterable.min(iterable); + + expect(result).toBe(1); + }); + + test("returns the minimum value in an iterable with custom comparer", () => { + const iterable = ["apple", "banana", "cherry"]; + + const result = Iterable.min(iterable, (a, b) => a.length - b.length); + + expect(result).toBe("apple"); + }); + + test("returns undefined for an empty iterable", () => { + const iterable = []; + + const result = Iterable.min(iterable); + + expect(result).toBeUndefined(); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [3, 1, 4, 2]; + const thisArg = { sign: -1 }; + + const result = Iterable.min(iterable, function (a, b) { return (a - b) * this.sign; }, thisArg); + + expect(result).toBe(4); + }); +}); + +describe("max", () => { + test("returns the maximum value in an iterable", () => { + const iterable = [3, 1, 4, 2]; + const result = Iterable.max(iterable); + expect(result).toBe(4); + }); + + test("returns the maximum value in an iterable with custom comparer", () => { + const iterable = ["apple", "banana", "cherry"]; + const result = Iterable.max(iterable, (a, b) => a.length - b.length); + expect(result).toBe("banana"); + }); + + test("returns undefined for an empty iterable", () => { + const iterable = []; + const result = Iterable.max(iterable); + expect(result).toBeUndefined(); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [3, 1, 4, 2]; + const thisArg = { sign: -1 }; + + const result = Iterable.max(iterable, function (a, b) { return (a - b) * this.sign; }, thisArg); + + expect(result).toBe(1); + }); +}); + +describe("count", () => { + test("returns the count of elements that meet the condition", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.count(iterable, x => x > 2); + + expect(result).toBe(3); + }); + + test("returns the length of the iterable if no predicate is provided", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.count(iterable); + + expect(result).toBe(5); + }); + + test("returns 0 for empty iterables", () => { + expect(Iterable.count([])).toBe(0); + expect(Iterable.count(new Set())).toBe(0); + expect(Iterable.count(new Map(), x => x)).toBe(0); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [1, 2, 3, 4, 5]; + const thisArg = { min: 2 }; + + const result = Iterable.count(iterable, function (x) { return x > this.min; }, thisArg); + + expect(result).toBe(3); + }); +}); + +describe("indexOf", () => { + test("returns the index of the first occurrence of a specified value", () => { + const iterable = [1, 2, 3, 2, 4]; + + const result = Iterable.indexOf(iterable, 2); + + expect(result).toBe(1); + }); + + test("returns -1 if the iterable does not include a certain element", () => { + const iterable = [1, 2, 3, 2, 4]; + + const result = Iterable.indexOf(iterable, 5); + + expect(result).toBe(-1); + }); + + test("returns the index of the first occurrence from the given index", () => { + const iterable = [1, 2, 3, 2, 4]; + + const result = Iterable.indexOf(iterable, 2, 2); + + expect(result).toBe(3); + }); + + test("returns the index of the first occurrence with custom comparer", () => { + const iterable = ["a", "b", "c", "B", "A"]; + + const result = Iterable.indexOf(iterable, "B", IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(1); + }); + + test("returns the index of the first occurrence with custom comparer from the given index", () => { + const iterable = ["a", "b", "c", "B", "A"]; + + const result = Iterable.indexOf(iterable, "b", 2, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(3); + }); +}); + +describe("lastIndexOf", () => { + test("returns the index of the last occurrence of a specified value", () => { + const iterable = [1, 2, 3, 2, 4]; + + const result = Iterable.lastIndexOf(iterable, 2); + + expect(result).toBe(3); + }); + + test("returns -1 if the iterable does not include a certain element", () => { + const iterable = [1, 2, 3, 2, 4]; + + const result = Iterable.lastIndexOf(iterable, 5); + + expect(result).toBe(-1); + }); + + test("returns the index of the last occurrence with custom comparer", () => { + const iterable = ["a", "b", "c", "B", "A"]; + + const result = Iterable.lastIndexOf(iterable, "b", IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(3); + }); + + test("returns the index of the last occurrence with custom comparer from the given index", () => { + const iterable = ["a", "b", "c", "B", "A"]; + + const result = Iterable.lastIndexOf(iterable, "B", 2, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(1); + }); +}); + +describe("includes", () => { + test("returns true if the iterable includes a certain element", () => { + const iterable = [1, 2, 3, 4]; + + const result = Iterable.includes(iterable, 2); + + expect(result).toBe(true); + }); + + test("returns false if the iterable does not include a certain element", () => { + const iterable = [1, 2, 3, 4]; + + const result = Iterable.includes(iterable, 5); + + expect(result).toBe(false); + }); + + test("returns true if the iterable includes a certain element from the given index", () => { + const iterable = ["a", "b", "c", "B"]; + + const result = Iterable.includes(iterable, "B", 2); + + expect(result).toBe(true); + }); + + test("returns true if the iterable includes a certain element with custom comparer", () => { + const iterable = ["a", "b", "c"]; + + const result = Iterable.includes(iterable, "B", IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(true); + }); + + test("returns true if the iterable includes a certain element with custom comparer from the given index", () => { + const iterable = ["a", "b", "c", "B"]; + + const result = Iterable.includes(iterable, "b", 2, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(true); + }); +}); + +describe("sequenceEqual", () => { + test("returns true if two iterables are equal", () => { + const first = [1, 2, 3, 4]; + const second = [1, 2, 3, 4]; + + const result = Iterable.sequenceEqual(first, second); + + expect(result).toBe(true); + }); + + test("returns false if two iterables are not equal", () => { + const first = [1, 2, 3, 4]; + const second = [1, 2, 3, 5]; + + const result = Iterable.sequenceEqual(first, second); + + expect(result).toBe(false); + }); + + test("returns true if two iterables are equal using custom comparer", () => { + const first = ["a", "b", "c"]; + const second = ["A", "B", "C"]; + + const result = Iterable.sequenceEqual(first, second, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(true); + }); + + test("returns false if two iterables are not equal using custom comparer", () => { + const first = ["a", "b", "c"]; + const second = ["a", "b", "d"]; + + const result = Iterable.sequenceEqual(first, second, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(false); + }); +}); + +describe("startsWith", () => { + test("returns true if an iterable starts with the specified search elements", () => { + const iterable = [1, 2, 3, 4]; + const searchElements = [1, 2]; + + const result = Iterable.startsWith(iterable, searchElements); + + expect(result).toBe(true); + }); + + test("returns false if an iterable does not start with the specified search elements", () => { + const iterable = [1, 2, 3, 4]; + const searchElements = [2, 3]; + + const result = Iterable.startsWith(iterable, searchElements); + + expect(result).toBe(false); + }); + + test("returns true if an iterable starts with the specified search elements from a specific index", () => { + const iterable = [1, 2, 3, 4]; + const searchElements = [3, 4]; + + const result = Iterable.startsWith(iterable, searchElements, 2); + + expect(result).toBe(true); + }); + + test("returns true if an iterable starts with the specified search elements using custom comparer", () => { + const iterable = ["a", "b", "c", "d"]; + const searchElements = ["A", "B"]; + + const result = Iterable.startsWith(iterable, searchElements, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(true); + }); + + test("returns true if an iterable starts with the specified search elements using custom comparer from the given index", () => { + const iterable = ["a", "b", "c", "d"]; + const searchElements = ["C", "D"]; + + const result = Iterable.startsWith(iterable, searchElements, 2, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(true); + }); +}); + +describe("endsWith", () => { + test("returns true if an iterable ends with the specified search elements", () => { + const iterable = [1, 2, 3, 4]; + const searchElements = [3, 4]; + + const result = Iterable.endsWith(iterable, searchElements); + + expect(result).toBe(true); + }); + + test("returns true if an iterable does not end with the specified search elements", () => { + const iterable = [1, 2, 3, 4]; + const searchElements = [2, 3]; + + const result = Iterable.endsWith(iterable, searchElements); + + expect(result).toBe(false); + }); + + + test("returns true if an iterable ends with the specified search elements up to a specific index", () => { + const iterable = [1, 2, 3, 4]; + const searchElements = [2, 3]; + + const result = Iterable.endsWith(iterable, searchElements, 3); + + expect(result).toBe(true); + }); + + test("returns true if an iterable ends with the specified search elements using custom comparer", () => { + const iterable = ["a", "b", "c", "d"]; + const searchElements = ["C", "D"]; + + const result = Iterable.endsWith(iterable, searchElements, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(true); + }); + + test("returns true if an iterable ends with the specified search elements using custom comparer up to a specific index", () => { + const iterable = ["a", "b", "c", "d"]; + const searchElements = ["B", "C"]; + + const result = Iterable.endsWith(iterable, searchElements, 3, IGNORE_CASE_EQUALITY_COMPARER); + + expect(result).toBe(true); + }); +}); + +describe("findIndex", () => { + test("returns the index of the first element that satisfies the predicate", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.findIndex(iterable, value => value > 3); + + expect(result).toBe(3); + }); + + test("returns -1 if no elements satisfy the predicate", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.findIndex(iterable, value => value > 5); + + expect(result).toBe(-1); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [1, 2, 3, 4, 5]; + const thisArg = { target: 3 }; + + const result = Iterable.findIndex(iterable, function (x) { return x === this.target; }, thisArg); + + expect(result).toBe(2); + }); +}); + +describe("first", () => { + test("returns the first element that satisfies the predicate", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + + const result = Iterable.first(iterable, x => x.value === "b"); + + expect(result).toEqual({ id: 2, value: "b" }); + }); + + test("returns undefined if no elements satisfy the predicate", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + + const result = Iterable.first(iterable, x => x.value === "c"); + + expect(result).toBeUndefined(); + }); + + test("returns the first element if no predicate is provided", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + + const result = Iterable.first(iterable); + + expect(result).toEqual({ id: 1, value: "a" }); + }); + + test("returns undefined if iterable is empty", () => { + expect(Iterable.first([])).toBeUndefined(); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + const thisArg = { target: "b" }; + + const result = Iterable.first(iterable, function (x) { return x.value === this.target; }, thisArg); + + expect(result).toEqual({ id: 2, value: "b" }); + }); +}); + +describe("last", () => { + test("returns the last element that satisfies the predicate", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + + const result = Iterable.last(iterable, x => x.value === "a"); + + expect(result).toEqual({ id: 3, value: "a" }); + }); + + test("returns undefined if no elements satisfy the predicate", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + + const result = Iterable.last(iterable, x => x.value === "c"); + + expect(result).toBeUndefined(); + }); + + test("returns the last element if no predicate is provided", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + + const result = Iterable.last(iterable); + + expect(result).toEqual({ id: 4, value: "b" }); + }); + + test("returns undefined if iterable is empty", () => { + expect(Iterable.last([])).toBeUndefined(); + }); + + test("binds thisArg to the callback function", () => { + const iterable = [{ id: 1, value: "a" }, { id: 2, value: "b" }, { id: 3, value: "a" }, { id: 4, value: "b" }]; + const thisArg = { target: "a" }; + + const result = Iterable.last(iterable, function (x) { return x.value === this.target; }, thisArg); + + expect(result).toEqual({ id: 3, value: "a" }); + }); +}); + +describe("at", () => { + test("returns the element at the specified index", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.at(iterable, 2); + + expect(result).toBe(3); + }); + + test("returns undefined if the index is out of range", () => { + const iterable = [1, 2, 3, 4, 5]; + + const result = Iterable.at(iterable, 5); + + expect(result).toBeUndefined(); + }); + + test("returns undefined if the iterable is empty", () => { + const iterable = []; + + const result = Iterable.at(iterable, 1); + + expect(result).toBeUndefined(); + }); + + test("handles relative indices", () => { + const iterable = [1, 2, 3, 4, 5]; + + expect(Iterable.at(iterable, -1)).toBe(5); + expect(Iterable.at(iterable, -2)).toBe(4); + expect(Iterable.at(iterable, -3)).toBe(3); + expect(Iterable.at(iterable, -4)).toBe(2); + expect(Iterable.at(iterable, -5)).toBe(1); + expect(Iterable.at(iterable, -6)).toBeUndefined(); + }); +}); + +describe("join", () => { + test("joins elements with the specified separator", () => { + const iterable = [1, 2, 3]; + + const result = Iterable.join(iterable, "-"); + + expect(result).toBe("1-2-3"); + }); + + test("joins elements with a comma if no separator is provided", () => { + const iterable = [1, 2, 3]; + + const result = Iterable.join(iterable); + + expect(result).toBe("1,2,3"); + }); +}); + +describe("concat", () => { + test("concatenates multiple iterables", () => { + const iterable1 = [1, 2, 3]; + const iterable2 = [4, 5, 6]; + const iterable3 = [7, 8, 9]; + + const result = Array.from(Iterable.concat(iterable1, iterable2, iterable3)); + + expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]); + }); +}); + +describe("prepend", () => { + test("prepends a value to an iterable", () => { + const iterable = [1, 2, 3]; + + const result = Array.from(Iterable.prepend(iterable, 0)); + + expect(result).toEqual([0, 1, 2, 3]); + }); +}); + +describe("append", () => { + test("appends a value to an iterable", () => { + const iterable = [1, 2, 3]; + + const result = Array.from(Iterable.append(iterable, 4)); + + expect(result).toEqual([1, 2, 3, 4]); + }); +}); + +describe("pop", () => { + test("removes the last element from an iterable", () => { + const iterable = [1, 2, 3]; + + const [value, rest] = Iterable.pop(iterable); + + expect(value).toBe(3); + expect(Array.from(rest)).toEqual([1, 2]); + }); + + test("returns undefined and empty iterable when the input iterable is empty", () => { + const iterable = []; + + const [value, rest] = Iterable.pop(iterable); + + expect(value).toBeUndefined(); + expect(Array.from(rest)).toEqual([]); + }); +}); + +describe("shift", () => { + test("removes the first element from an iterable", () => { + const iterable = [1, 2, 3]; + + const [value, rest] = Iterable.shift(iterable); + + expect(value).toBe(1); + expect(Array.from(rest)).toEqual([2, 3]); + }); + + test("returns undefined and empty iterable when the input iterable is empty", () => { + const iterable = []; + const [value, rest] = Iterable.shift(iterable); + + expect(value).toBeUndefined(); + expect(Array.from(rest)).toEqual([]); + }); +}); + +describe("forEach", () => { + test("executes a function for each element in an iterable", () => { + const iterable = [1, 2, 3]; + const mockFn = jest.fn(); + + Iterable.forEach(iterable, mockFn); + + expect(mockFn).toHaveBeenCalledTimes(3); + expect(mockFn).toHaveBeenNthCalledWith(1, 1, 0, iterable); + expect(mockFn).toHaveBeenNthCalledWith(2, 2, 1, iterable); + expect(mockFn).toHaveBeenNthCalledWith(3, 3, 2, iterable); + }); +}); + +describe("asArray", () => { + test("converts an iterable to an array", () => { + const iterable = new Set([1, 2, 3]); + + const result = Iterable.asArray(iterable); + + expect(result).toEqual([1, 2, 3]); + }); + + test("returns the same array if the iterable is already an array", () => { + const iterable = [1, 2, 3]; + + const result = Iterable.asArray(iterable); + + expect(result).toBe(iterable); + }); +}); + +describe("asArrayLike", () => { + test("converts an iterable to an array-like", () => { + const iterable = new Set([1, 2, 3]); + + const result = Iterable.asArrayLike(iterable); + + expect(result).toBeInstanceOf(Iterable.ArrayLikeIterable); + expect(Array.from(result)).toEqual([1, 2, 3]); + }); + + test("returns the same array if the iterable is already an array", () => { + const iterable = [1, 2, 3]; + + const result = Iterable.asArrayLike(iterable); + + expect(result).toBe(iterable); + }); + + test("returns the same array-like if the iterable is already an array-like", () => { + const iterable = Iterable.ArrayLikeIterable.from([1, 2, 3]); + + const result = Iterable.asArrayLike(iterable); + + expect(result).toBe(iterable); + }); +}); + +describe("$i", () => { + test("converts an iterable to an ArrayLikeIterable", () => { + const iterable = new Set([1, 2, 3]); + + const result = Iterable.$i(iterable); + + expect(result).toBeInstanceOf(Iterable.ArrayLikeIterable); + expect(Array.from(result)).toEqual([1, 2, 3]); + }); + + test("returns the same ArrayLikeIterable if the iterable is already an ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([1, 2, 3]); + + const result = Iterable.$i(iterable); + + expect(result).toBe(iterable); + }); +}); + +describe("ArrayLikeIterable", () => { + describe("from", () => { + test("creates a new instance from an iterable", () => { + const array = [1, 2, 3]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const values = Array.from(iterable.values()); + + expect(values).toEqual(array); + }); + }); + + describe("of", () => { + test("creates a new instance from an iterator", () => { + const array = [1, 2, 3]; + const arrayIterator = array[Symbol.iterator](); + const iterable = Iterable.ArrayLikeIterable.of(arrayIterator); + + const values = Array.from(iterable.values()); + + expect(values).toEqual(array); + }); + }); + + describe("length", () => { + test("returns the number of elements in the iterable", () => { + const array = [1, 2, 3]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + expect(iterable.length).toBe(array.length); + }); + }); + + describe("toArray", () => { + test("returns an array containing all elements of the iterable", () => { + const array = [1, 2, 3]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const newArray = iterable.toArray(); + + expect(newArray).toEqual(array); + }); + + test("new array is not the same as one used to create ArrayLikeIterable", () => { + const array = [1, 2, 3]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const newArray = iterable.toArray(); + + expect(newArray).not.toBe(array); + }); + }); + + describe("toMap", () => { + test("converts the iterable of key-value pairs into a map", () => { + const entries = [["zero", 0], ["one", 1], ["two", 2]] as const; + const iterable = Iterable.ArrayLikeIterable.from(entries); + + const map = iterable.toMap(); + + expect(Array.from(map.entries())).toEqual(entries); + }); + + test("converts the iterable of key-value pairs into a map with custom comparer", () => { + const entries = [["zero", 0], ["one", 1], ["two", 2], ["ONE", -1]] as const; + const iterable = Iterable.ArrayLikeIterable.from(entries); + + const map = iterable.toMap(IGNORE_CASE_EQUALITY_COMPARER); + + expect(Array.from(map.entries())).toEqual([["zero", 0], ["ONE", -1], ["two", 2]]); + }); + }); + + describe("toSet", () => { + test("converts the iterable into a set", () => { + const array = ["zero", "one", "two"]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const set = iterable.toSet(); + + expect(Array.from(set)).toEqual(array); + }); + + test("converts the iterable into a set with custom comparer", () => { + const array = ["zero", "one", "two", "ONE"]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const set = iterable.toSet(IGNORE_CASE_EQUALITY_COMPARER); + + expect(Array.from(set)).toEqual(["zero", "ONE", "two"]); + }); + }); + + describe("toRecord", () => { + test("converts the iterable of key-value pairs into a record", () => { + const entries = [["zero", 0], ["one", 1], ["two", 2]] as const; + const iterable = Iterable.ArrayLikeIterable.from(entries); + + const record = iterable.toRecord(); + + expect(record).toEqual({ "zero": 0, "one": 1, "two": 2 }); + }); + }); + + describe("keys", () => { + test("returns an iterable of indices in the iterable", () => { + const array = ["zero", "one", "two"]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const keys = Array.from(iterable.keys()); + + expect(keys).toEqual([0, 1, 2]); + }); + }); + + describe("values", () => { + test("returns an iterable of values in the iterable", () => { + const array = [1, 2, 3]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const values = Array.from(iterable.values()); + + expect(values).toEqual(array); + }); + }); + + describe("entries", () => { + test("returns an iterable of index-value pairs for every entry in the iterable", () => { + const iterable = Iterable.ArrayLikeIterable.from(["zero", "one", "two"]); + + const entries = Array.from(iterable.entries()); + + expect(entries).toEqual([[0, "zero"], [1, "one"], [2, "two"]]); + }); + }); + + describe("[Symbol.iterator]", () => { + test("returns an iterator over the values", () => { + const array = ["zero", "one", "two"]; + const iterable = Iterable.ArrayLikeIterable.from(array); + + const values = [...iterable]; + + expect(values).toEqual(array); + }); + }); + + describe("[Symbol.toStringTag]", () => { + test("returns 'Iterable'", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable[Symbol.toStringTag]).toBe("Iterable"); + }); + }); + + describe("filter", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.filter(x => x)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'filter'", () => { + expect(Iterable.ArrayLikeIterable.prototype.filter.toString()).toMatch(/\Wfilter\W/); + expect(Iterable.ArrayLikeIterable.prototype.filter.toString()).not.toMatch(/\wfilter\w/); + }); + }); + + describe("map", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.map(x => x)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'map'", () => { + expect(Iterable.ArrayLikeIterable.prototype.map.toString()).toMatch(/\Wmap\W/); + expect(Iterable.ArrayLikeIterable.prototype.map.toString()).not.toMatch(/\wmap\w/); + }); + }); + + describe("flatMap", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.flatMap(x => x)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'flatMap'", () => { + expect(Iterable.ArrayLikeIterable.prototype.flatMap.toString()).toMatch(/\WflatMap\W/); + expect(Iterable.ArrayLikeIterable.prototype.flatMap.toString()).not.toMatch(/\wflatMap\w/); + }); + }); + + describe("reduce", () => { + test("returns a value", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.reduce(x => x)).toBeUndefined(); + }); + + test("redirects the call to 'reduce'", () => { + expect(Iterable.ArrayLikeIterable.prototype.reduce.toString()).toMatch(/\Wreduce\W/); + expect(Iterable.ArrayLikeIterable.prototype.reduce.toString()).not.toMatch(/\wreduce\w/); + }); + }); + + describe("skip", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.skip(0)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'skip'", () => { + expect(Iterable.ArrayLikeIterable.prototype.skip.toString()).toMatch(/\Wskip\W/); + expect(Iterable.ArrayLikeIterable.prototype.skip.toString()).not.toMatch(/\wskip\w/); + }); + }); + + describe("take", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.take(0)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'take'", () => { + expect(Iterable.ArrayLikeIterable.prototype.take.toString()).toMatch(/\Wtake\W/); + expect(Iterable.ArrayLikeIterable.prototype.take.toString()).not.toMatch(/\wtake\w/); + }); + }); + + describe("takeLast", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.takeLast(0)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'takeLast'", () => { + expect(Iterable.ArrayLikeIterable.prototype.takeLast.toString()).toMatch(/\WtakeLast\W/); + expect(Iterable.ArrayLikeIterable.prototype.takeLast.toString()).not.toMatch(/\wtakeLast\w/); + }); + }); + + describe("slice", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.slice(0)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'slice'", () => { + expect(Iterable.ArrayLikeIterable.prototype.slice.toString()).toMatch(/\Wslice\W/); + expect(Iterable.ArrayLikeIterable.prototype.slice.toString()).not.toMatch(/\wslice\w/); + }); + }); + + describe("reverse", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.reverse()).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'reverse'", () => { + expect(Iterable.ArrayLikeIterable.prototype.reverse.toString()).toMatch(/\Wreverse\W/); + expect(Iterable.ArrayLikeIterable.prototype.reverse.toString()).not.toMatch(/\wreverse\w/); + }); + }); + + describe("sort", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.sort()).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'sort'", () => { + expect(Iterable.ArrayLikeIterable.prototype.sort.toString()).toMatch(/\Wsort\W/); + expect(Iterable.ArrayLikeIterable.prototype.sort.toString()).not.toMatch(/\wsort\w/); + }); + }); + + describe("every", () => { + test("returns boolean", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.every(x => x)).toBe("boolean"); + }); + + test("redirects the call to 'every'", () => { + expect(Iterable.ArrayLikeIterable.prototype.every.toString()).toMatch(/\Wevery\W/); + expect(Iterable.ArrayLikeIterable.prototype.every.toString()).not.toMatch(/\wevery\w/); + }); + }); + + describe("some", () => { + test("returns boolean", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.some(x => x)).toBe("boolean"); + }); + + test("redirects the call to 'some'", () => { + expect(Iterable.ArrayLikeIterable.prototype.some.toString()).toMatch(/\Wsome\W/); + expect(Iterable.ArrayLikeIterable.prototype.some.toString()).not.toMatch(/\wsome\w/); + }); + }); + + describe("min", () => { + test("returns a value", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.min()).toBeUndefined(); + }); + + test("redirects the call to 'min'", () => { + expect(Iterable.ArrayLikeIterable.prototype.min.toString()).toMatch(/\Wmin\W/); + expect(Iterable.ArrayLikeIterable.prototype.min.toString()).not.toMatch(/\wmin\w/); + }); + }); + + describe("max", () => { + test("returns a value", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.max()).toBeUndefined(); + }); + + test("redirects the call to 'max'", () => { + expect(Iterable.ArrayLikeIterable.prototype.max.toString()).toMatch(/\Wmax\W/); + expect(Iterable.ArrayLikeIterable.prototype.max.toString()).not.toMatch(/\wmax\w/); + }); + }); + + describe("count", () => { + test("returns number", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.count()).toBe("number"); + }); + + test("redirects the call to 'count'", () => { + expect(Iterable.ArrayLikeIterable.prototype.count.toString()).toMatch(/\Wcount\W/); + expect(Iterable.ArrayLikeIterable.prototype.count.toString()).not.toMatch(/\wcount\w/); + }); + }); + + describe("indexOf", () => { + test("returns number", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.indexOf({})).toBe("number"); + }); + + test("redirects the call to 'indexOf'", () => { + expect(Iterable.ArrayLikeIterable.prototype.indexOf.toString()).toMatch(/\WindexOf\W/); + expect(Iterable.ArrayLikeIterable.prototype.indexOf.toString()).not.toMatch(/\windexOf\w/); + }); + }); + + describe("lastIndexOf", () => { + test("returns number", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.lastIndexOf({})).toBe("number"); + }); + + test("redirects the call to 'lastIndexOf'", () => { + expect(Iterable.ArrayLikeIterable.prototype.lastIndexOf.toString()).toMatch(/\WlastIndexOf\W/); + expect(Iterable.ArrayLikeIterable.prototype.lastIndexOf.toString()).not.toMatch(/\wlastIndexOf\w/); + }); + }); + + describe("includes", () => { + test("returns boolean", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.includes({})).toBe("boolean"); + }); + + test("redirects the call to 'includes'", () => { + expect(Iterable.ArrayLikeIterable.prototype.includes.toString()).toMatch(/\Wincludes\W/); + expect(Iterable.ArrayLikeIterable.prototype.includes.toString()).not.toMatch(/\wincludes\w/); + }); + }); + + describe("sequenceEqual", () => { + test("returns boolean", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.sequenceEqual([])).toBe("boolean"); + }); + + test("redirects the call to 'sequenceEqual'", () => { + expect(Iterable.ArrayLikeIterable.prototype.sequenceEqual.toString()).toMatch(/\WsequenceEqual\W/); + expect(Iterable.ArrayLikeIterable.prototype.sequenceEqual.toString()).not.toMatch(/\wsequenceEqual\w/); + }); + }); + + describe("startsWith", () => { + test("returns boolean", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.startsWith([])).toBe("boolean"); + }); + + test("redirects the call to 'startsWith'", () => { + expect(Iterable.ArrayLikeIterable.prototype.startsWith.toString()).toMatch(/\WstartsWith\W/); + expect(Iterable.ArrayLikeIterable.prototype.startsWith.toString()).not.toMatch(/\wstartsWith\w/); + }); + }); + + describe("endsWith", () => { + test("returns boolean", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.endsWith([])).toBe("boolean"); + }); + + test("redirects the call to 'endsWith'", () => { + expect(Iterable.ArrayLikeIterable.prototype.endsWith.toString()).toMatch(/\WendsWith\W/); + expect(Iterable.ArrayLikeIterable.prototype.endsWith.toString()).not.toMatch(/\wendsWith\w/); + }); + }); + + describe("findIndex", () => { + test("returns number", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.findIndex(x => x)).toBe("number"); + }); + + test("redirects the call to 'findIndex'", () => { + expect(Iterable.ArrayLikeIterable.prototype.findIndex.toString()).toMatch(/\WfindIndex\W/); + expect(Iterable.ArrayLikeIterable.prototype.findIndex.toString()).not.toMatch(/\wfindIndex\w/); + }); + }); + + describe("find", () => { + test("returns a value", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.find(x => x)).toBeUndefined(); + }); + + test("redirects the call to 'first'", () => { + expect(Iterable.ArrayLikeIterable.prototype.find.toString()).toMatch(/\Wfirst\W/); + expect(Iterable.ArrayLikeIterable.prototype.find.toString()).not.toMatch(/\wfirst\w/); + }); + }); + + describe("first", () => { + test("returns a value", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.first(x => x)).toBeUndefined(); + }); + + test("redirects the call to 'first'", () => { + expect(Iterable.ArrayLikeIterable.prototype.first.toString()).toMatch(/\Wfirst\W/); + expect(Iterable.ArrayLikeIterable.prototype.first.toString()).not.toMatch(/\wfirst\w/); + }); + }); + + describe("last", () => { + test("returns a value", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.last(x => x)).toBeUndefined(); + }); + + test("redirects the call to 'last'", () => { + expect(Iterable.ArrayLikeIterable.prototype.last.toString()).toMatch(/\Wlast\W/); + expect(Iterable.ArrayLikeIterable.prototype.last.toString()).not.toMatch(/\wlast\w/); + }); + }); + + describe("at", () => { + test("returns a value", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.at(0)).toBeUndefined(); + }); + + test("redirects the call to 'at'", () => { + expect(Iterable.ArrayLikeIterable.prototype.at.toString()).toMatch(/\Wat\W/); + expect(Iterable.ArrayLikeIterable.prototype.at.toString()).not.toMatch(/\wat\w/); + }); + }); + + describe("join", () => { + test("returns string", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(typeof iterable.join()).toBe("string"); + }); + + test("redirects the call to 'join'", () => { + expect(Iterable.ArrayLikeIterable.prototype.join.toString()).toMatch(/\Wjoin\W/); + expect(Iterable.ArrayLikeIterable.prototype.join.toString()).not.toMatch(/\wjoin\w/); + }); + }); + + describe("concat", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.concat([])).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'concat'", () => { + expect(Iterable.ArrayLikeIterable.prototype.concat.toString()).toMatch(/\Wconcat\W/); + expect(Iterable.ArrayLikeIterable.prototype.concat.toString()).not.toMatch(/\wconcat\w/); + }); + }); + + describe("prepend", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.prepend([])).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'prepend'", () => { + expect(Iterable.ArrayLikeIterable.prototype.prepend.toString()).toMatch(/\Wprepend\W/); + expect(Iterable.ArrayLikeIterable.prototype.prepend.toString()).not.toMatch(/\wprepend\w/); + }); + }); + + describe("append", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.append([])).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'append'", () => { + expect(Iterable.ArrayLikeIterable.prototype.append.toString()).toMatch(/\Wappend\W/); + expect(Iterable.ArrayLikeIterable.prototype.append.toString()).not.toMatch(/\wappend\w/); + }); + }); + + describe("shift", () => { + test("returns [value, ArrayLikeIterable]", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + const [value, newIterable] = iterable.shift(); + + expect(value).toBeUndefined(); + expect(newIterable).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'shift'", () => { + expect(Iterable.ArrayLikeIterable.prototype.shift.toString()).toMatch(/\Wshift\W/); + expect(Iterable.ArrayLikeIterable.prototype.shift.toString()).not.toMatch(/\wshift\w/); + }); + }); + + describe("unshift", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.unshift({})).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'prepend'", () => { + expect(Iterable.ArrayLikeIterable.prototype.unshift.toString()).toMatch(/\Wprepend\W/); + expect(Iterable.ArrayLikeIterable.prototype.unshift.toString()).not.toMatch(/\wprepend\w/); + }); + }); + + describe("push", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.push({})).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'append'", () => { + expect(Iterable.ArrayLikeIterable.prototype.push.toString()).toMatch(/\Wappend\W/); + expect(Iterable.ArrayLikeIterable.prototype.push.toString()).not.toMatch(/\wappend\w/); + }); + }); + + describe("pop", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + const [value, newIterable] = iterable.pop(); + + expect(value).toBeUndefined(); + expect(newIterable).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'pop'", () => { + expect(Iterable.ArrayLikeIterable.prototype.pop.toString()).toMatch(/\Wpop\W/); + expect(Iterable.ArrayLikeIterable.prototype.pop.toString()).not.toMatch(/\wpop\w/); + }); + }); + + describe("forEach", () => { + test("returns nothing", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.forEach(x => x)).toBeUndefined(); + }); + + test("redirects the call to 'forEach'", () => { + expect(Iterable.ArrayLikeIterable.prototype.forEach.toString()).toMatch(/\WforEach\W/); + expect(Iterable.ArrayLikeIterable.prototype.forEach.toString()).not.toMatch(/\wforEach\w/); + }); + }); + + describe("asArray", () => { + test("returns Array", () => { + const array = []; + const iterable = Iterable.ArrayLikeIterable.from(array); + + expect(iterable.asArray()).toBe(array); + }); + + test("redirects the call to 'asArray'", () => { + expect(Iterable.ArrayLikeIterable.prototype.asArray.toString()).toMatch(/\WasArray\W/); + expect(Iterable.ArrayLikeIterable.prototype.asArray.toString()).not.toMatch(/\wasArray\w/); + }); + }); + + describe("distinct", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.distinct()).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'distinct'", () => { + expect(Iterable.ArrayLikeIterable.prototype.distinct.toString()).toMatch(/\Wdistinct\W/); + expect(Iterable.ArrayLikeIterable.prototype.distinct.toString()).not.toMatch(/\wdistinct\w/); + }); + }); + + describe("distinctBy", () => { + test("returns ArrayLikeIterable", () => { + const iterable = Iterable.ArrayLikeIterable.from([]); + + expect(iterable.distinctBy(x => x)).toBeInstanceOf(Iterable.ArrayLikeIterable); + }); + + test("redirects the call to 'distinctBy'", () => { + expect(Iterable.ArrayLikeIterable.prototype.distinctBy.toString()).toMatch(/\WdistinctBy\W/); + expect(Iterable.ArrayLikeIterable.prototype.distinctBy.toString()).not.toMatch(/\wdistinctBy\w/); + }); + }); +});