// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { getOwn } from './getOwn.std.ts'; export function isIterable(value: unknown): value is Iterable { return ( (typeof value === 'object' && value != null && Symbol.iterator in value) || typeof value === 'string' ); } export function size(iterable: Iterable): number { // We check for common types as an optimization. if (typeof iterable === 'string' || Array.isArray(iterable)) { return iterable.length; } if (iterable instanceof Set || iterable instanceof Map) { return iterable.size; } const iterator = iterable[Symbol.iterator](); let result = -1; for (let done = false; !done; result += 1) { done = Boolean(iterator.next().done); } return result; } export function concat( ...iterables: ReadonlyArray> ): Iterable { return new ConcatIterable(iterables); } class ConcatIterable implements Iterable { readonly #iterables: ReadonlyArray>; constructor(iterables: ReadonlyArray>) { this.#iterables = iterables; } *[Symbol.iterator](): Iterator { for (const iterable of this.#iterables) { yield* iterable; } } } export function filter( iterable: Iterable, predicate: (value: T) => value is S ): Iterable; export function filter( iterable: Iterable, predicate: (value: T) => unknown ): Iterable; export function filter( iterable: Iterable, predicate: (value: T) => unknown ): Iterable { return new FilterIterable(iterable, predicate); } // oxlint-disable-next-line max-classes-per-file class FilterIterable implements Iterable { readonly #iterable: Iterable; readonly #predicate: (value: T) => unknown; constructor(iterable: Iterable, predicate: (value: T) => unknown) { this.#iterable = iterable; this.#predicate = predicate; } [Symbol.iterator](): Iterator { return new FilterIterator( this.#iterable[Symbol.iterator](), this.#predicate ); } } class FilterIterator implements Iterator { readonly #iterator: Iterator; readonly #predicate: (value: T) => unknown; constructor(iterator: Iterator, predicate: (value: T) => unknown) { this.#iterator = iterator; this.#predicate = predicate; } next(): IteratorResult { // oxlint-disable-next-line no-constant-condition while (true) { const nextIteration = this.#iterator.next(); if (nextIteration.done || this.#predicate(nextIteration.value)) { return nextIteration; } } } } /** * Filter and transform (map) that produces a new type * useful when traversing through fields that might be undefined */ export function collect( iterable: Iterable, fn: (value: T) => S | undefined ): Iterable { return new CollectIterable(iterable, fn); } class CollectIterable implements Iterable { readonly #iterable: Iterable; readonly #fn: (value: T) => S | undefined; constructor(iterable: Iterable, fn: (value: T) => S | undefined) { this.#iterable = iterable; this.#fn = fn; } [Symbol.iterator](): Iterator { return new CollectIterator(this.#iterable[Symbol.iterator](), this.#fn); } } class CollectIterator implements Iterator { readonly #iterator: Iterator; readonly #fn: (value: T) => S | undefined; constructor(iterator: Iterator, fn: (value: T) => S | undefined) { this.#iterator = iterator; this.#fn = fn; } next(): IteratorResult { // oxlint-disable-next-line no-constant-condition while (true) { const nextIteration = this.#iterator.next(); if (nextIteration.done) { return nextIteration; } const nextValue = this.#fn(nextIteration.value); if (nextValue !== undefined) { return { done: false, value: nextValue, }; } } } } export function find( iterable: Iterable, predicate: (value: T) => unknown ): undefined | T { for (const value of iterable) { if (predicate(value)) { return value; } } return undefined; } export function groupBy( iterable: Iterable, fn: (value: T) => string ): Record> { const result: Record> = Object.create(null); for (const value of iterable) { const key = fn(value); const existingGroup = getOwn(result, key); if (existingGroup) { existingGroup.push(value); } else { result[key] = [value]; } } return result; } export const isEmpty = (iterable: Iterable): boolean => Boolean(iterable[Symbol.iterator]().next().done); export function join(iterable: Iterable, separator: string): string { let hasProcessedFirst = false; let result = ''; for (const value of iterable) { // oxlint-disable-next-line typescript/no-base-to-string const stringifiedValue = value == null ? '' : String(value); if (hasProcessedFirst) { result += separator + stringifiedValue; } else { result = stringifiedValue; } hasProcessedFirst = true; } return result; } export function map( iterable: Iterable, fn: (value: T) => ResultT ): Iterable { return new MapIterable(iterable, fn); } class MapIterable implements Iterable { readonly #iterable: Iterable; readonly #fn: (value: T) => ResultT; constructor(iterable: Iterable, fn: (value: T) => ResultT) { this.#iterable = iterable; this.#fn = fn; } [Symbol.iterator](): Iterator { return new MapIterator(this.#iterable[Symbol.iterator](), this.#fn); } } class MapIterator implements Iterator { readonly #iterator: Iterator; readonly #fn: (value: T) => ResultT; constructor(iterator: Iterator, fn: (value: T) => ResultT) { this.#iterator = iterator; this.#fn = fn; } next(): IteratorResult { const nextIteration = this.#iterator.next(); if (nextIteration.done) { return nextIteration; } return { done: false, value: this.#fn(nextIteration.value), }; } } export function reduce( iterable: Iterable, fn: (result: TResult, value: T) => TResult, accumulator: TResult ): TResult { let result = accumulator; for (const value of iterable) { result = fn(result, value); } return result; } export function repeat(value: T): Iterable { return new RepeatIterable(value); } export function* chunk( iterable: Iterable, chunkSize: number ): Iterable> { let aChunk: Array = []; for (const item of iterable) { aChunk.push(item); if (aChunk.length === chunkSize) { yield aChunk; aChunk = []; } } if (aChunk.length > 0) { yield aChunk; } } class RepeatIterable implements Iterable { readonly #value: T; constructor(value: T) { this.#value = value; } [Symbol.iterator](): Iterator { return new RepeatIterator(this.#value); } } class RepeatIterator implements Iterator { readonly #iteratorResult: IteratorResult; constructor(value: Readonly) { this.#iteratorResult = { done: false, value, }; } next(): IteratorResult { return this.#iteratorResult; } } export function take(iterable: Iterable, amount: number): Iterable { return new TakeIterable(iterable, amount); } class TakeIterable implements Iterable { readonly #iterable: Iterable; readonly #amount: number; constructor(iterable: Iterable, amount: number) { this.#iterable = iterable; this.#amount = amount; } [Symbol.iterator](): Iterator { return new TakeIterator(this.#iterable[Symbol.iterator](), this.#amount); } } class TakeIterator implements Iterator { readonly #iterator: Iterator; #amount: number; constructor(iterator: Iterator, amount: number) { this.#iterator = iterator; this.#amount = amount; } next(): IteratorResult { const nextIteration = this.#iterator.next(); if (nextIteration.done || this.#amount === 0) { return { done: true, value: undefined }; } this.#amount -= 1; return nextIteration; } } // In the future, this could support number and symbol property names. export function zipObject( props: Iterable, values: Iterable ): Record { const result: Record = {}; const propsIterator = props[Symbol.iterator](); const valuesIterator = values[Symbol.iterator](); // oxlint-disable-next-line no-constant-condition while (true) { const propIteration = propsIterator.next(); if (propIteration.done) { break; } const valueIteration = valuesIterator.next(); if (valueIteration.done) { break; } result[propIteration.value] = valueIteration.value; } return result; }