UNPKG

@rimbu/stream

Version:

Efficient structure representing a sequence of elements, with powerful operations for TypeScript

1,555 lines (1,402 loc) 60.8 kB
import { RimbuError } from '@rimbu/base'; import { CollectFun, Eq, ErrBase, OptLazy } from '@rimbu/common'; import { Stream, type FastIterator, type StreamSource } from '@rimbu/stream'; /** * A `Reducer` is a stand-alone calculation that takes input values of type I, and, when requested, produces an output value of type O. * @typeparam I - the input value type * @typeparam O - the output value type */ export type Reducer<I, O = I> = Reducer.Impl<I, O, unknown>; function identity<T>(value: T): T { return value; } /** * Combines multiple reducers in an object's values of the same input type into a single reducer that * forwards each incoming value to all reducers, and when output is requested will return an object containing * the corresponding output of each reducer at the matching object property. */ function combineObj<T, R extends { readonly [key: string]: unknown }>( reducerObj: { readonly [K in keyof R]: Reducer<T, R[K]> } & Record< string, Reducer<T, unknown> > ): Reducer<T, R> { return Reducer.create( (initHalt) => { const result: Record<string, Reducer.Instance<T, any>> = {}; let allHalted = true; for (const key in reducerObj) { const instance = reducerObj[key].compile(); allHalted = allHalted && instance.halted; result[key] = instance; } if (allHalted) { initHalt(); } return result; }, (state, elem, index, halt) => { let allHalted = true; for (const key in state) { const reducerInstance = state[key]; if (!reducerInstance.halted) { reducerInstance.next(elem); allHalted = allHalted && reducerInstance.halted; } } if (allHalted) { halt(); } return state; }, (state) => { const result: any = {}; for (const key in state) { result[key] = state[key].getOutput(); } return result as R; } ); } /** * Combines multiple reducers in an array of the same input type into a single reducer that * forwards each incoming value to all reducers, and when output is requested will return an array containing * the corresponding output of each reducer. */ function combineArr<T, R extends readonly [unknown, unknown, ...unknown[]]>( ...reducers: { [K in keyof R]: Reducer<T, R[K]> } & Reducer<T, unknown>[] ): Reducer<T, R> { return Reducer.create<T, any, Reducer.Instance<T, unknown>[]>( (initHalt) => { let allHalted = true; const result = reducers.map((reducer) => { const instance = reducer.compile(); allHalted = allHalted && instance.halted; return instance; }); if (allHalted) { initHalt(); } return result; }, (state, elem, index, halt) => { let allHalted = true; let i = -1; const len = state.length; while (++i < len) { const reducerInstance = state[i]; if (!reducerInstance.halted) { reducerInstance.next(elem); allHalted = allHalted && reducerInstance.halted; } } if (allHalted) { halt(); } return state; }, (state) => state.map((reducerInstance) => reducerInstance.getOutput()) ); } export namespace Reducer { /** * The Implementation interface for a `Reducer`, which also exposes the internal state type. * @typeparam I - the input value type * @typeparam O - the output value type * @typeparam S - the internal state type */ export interface Impl<I, O, S = unknown> { /** * A function that produces the initial state value for the reducer algorithm. * @param initHalt - a callback function that, if called, indicates that the reducer does not accept any input. */ readonly init: (initHalt: () => void) => S; /** * Returns the next state based on the given input values: * @param state - the current state * @param elem - the current input value * @param index - the current input index * @param halt - a function that, when called, ensures no more values are passed to the reducer */ next(state: S, elem: I, index: number, halt: () => void): S; /** * Returns the output value based on the given `state`. * @param state - the current state * @param index - the value index * @param halted - a boolean indicating whether the reducer is halted */ stateToResult(state: S, index: number, halted: boolean): O; /** * Returns a `Reducer` instance that only passes values to the reducer that satisy the given `pred` predicate. * @param pred - a function that returns true if the value should be passed to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index<br/> * - halt: function that, when called, ensures no more new values are passed to the reducer * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate * @note if the predicate is a type guard, the return type is automatically inferred * @example * ```ts * Reducer.sum.filterInput(v => v > 10) * // this reducer will only sum values larger than 10 * ``` */ filterInput<IF extends I>( pred: (value: I, index: number, halt: () => void) => value is IF, options?: { negate?: false | undefined } ): Reducer<IF, O>; filterInput<IF extends I>( pred: (value: I, index: number, halt: () => void) => value is IF, options: { negate: true } ): Reducer<Exclude<I, IF>, O>; filterInput( pred: (value: I, index: number, halt: () => void) => boolean, options?: { negate?: boolean | undefined } ): Reducer<I, O>; /** * Returns a `Reducer` instance that converts its input values using given `mapFun` before passing them to this reducer. * @param mapFun - a function that returns a new value to pass to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index * @typeparam I2 - the resulting reducer input type * @example * ```ts * Reducer.sum.mapInput(v => v * 2) * // this reducer will double all input values before summing them * ``` */ mapInput: <I2>(mapFun: (value: I2, index: number) => I) => Reducer<I2, O>; /** * Returns a `Reducer` instance that converts each output value from some source reducer into an arbitrary number of output values * using given `flatMapFun` before passing them to this reducer. * @typeparam I2 - the resulting reducer input type * @param mapFun - a function that returns a new value to pass to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index * @example * ```ts * Reducer.sum.flatMapInput(v => [v, v + 1]) * // this reducer will add v and v + 1 to the sum for each input value * ``` */ flatMapInput<I2>( flatMapFun: (value: I2, index: number) => StreamSource<I> ): Reducer<I2, O>; /** * Returns a `Reducer` instance that converts or filters its input values using given `collectFun` before passing them to the reducer. * @param collectFun - a function receiving<br/> * - `value`: the next value<br/> * - `index`: the value index<br/> * - `skip`: a token that, when returned, will not add a value to the resulting collection<br/> * - `halt`: a function that, when called, ensures no next elements are passed * @typeparam I2 - the resulting reducer input type * @example * ```ts * Reducer.sum.collectInput((v, _, skip) => v <= 10 ? skip : v * 2) * // this reducer will double all input values larger thant 10 before summing them, * // and will skip all values smaller than 10 * ``` */ collectInput<I2>(collectFun: CollectFun<I2, I>): Reducer<I2, O>; /** * Returns a `Reducer` instance that converts its output values using given `mapFun`. * @typeparam O2 - the resulting reducer output type * @param mapFun - a function that takes the current output value and converts it to a new output value * @typeparam O2 - the new output type * @example * ```ts * Reducer.sum.mapOutput(String) * // this reducer will convert all its results to string before returning them * ``` */ mapOutput<O2>( mapFun: (value: O, index: number, halted: boolean) => O2 ): Reducer<I, O2>; /** * Returns a `Reducer` instance that takes at most the given `amount` of input elements, and will ignore subsequent elements. * @param amount - the amount of elements to accept * @example * ```ts * Stream.range({ end: 10 }).reduce(Reducer.sum.takeInput(2)) * // => 1 * ``` */ takeInput(amount: number): Reducer<I, O>; /** * Returns a `Reducer` instance that skips the first given `amount` of input elements, and will process subsequent elements. * @param amount - the amount of elements to skip * @example * ```ts * Stream.range({ end: 10 }).reduce(Reducer.sum.dropInput(9)) * // => 19 * ``` */ dropInput(amount: number): Reducer<I, O>; /** * Returns a `Reducer` instance that takes given `amount` of elements starting at given `from` index, and ignores other elements. * @param from - (default: 0) the index at which to start processing elements * @param amount - (optional) the amount of elements to process, if not given, processes all elements from the `from` index * @example * ```ts * Stream.range({ end: 10 }).reduce(Reducer.sum.sliceInput(1, 2)) * // => 3 * ``` */ sliceInput(from?: number, amount?: number): Reducer<I, O>; /** * Returns an 'AsyncReducer` instance that produces at most `amount` values. * @param amount - the maximum amount of values to produce. */ takeOutput(amount: number): Reducer<I, O>; /** * Returns a 'Reducer` instance that produces until the given `pred` predicate returns true for * the output value. * @param pred - a function that returns true if the value should be passed to the reducer based on the following inputs:<br/> * - value: the current input value<br/> * - index: the current input index<br/> * - halt: function that, when called, ensures no more new values are passed to the reducer * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate */ takeOutputUntil( pred: (value: O, index: number) => boolean, options?: { negate?: boolean } ): Reducer<I, O>; /** * Returns a reducer that applies this reducer and then the `nextReducers` sequentially on halting of each reducer. * It provides the last output value of the active reducer. * @param nextReducers - an number of reducers consuming and producing the same types as the current reducer. * @example * ```ts * const result = Stream.range({ amount: 6 }) * .reduce( * Reducer.sum * .takeInput(3) * .chain( * [v => v > 10 ? Reducer.product : Reducer.sum] * ) * ) * console.log(result) * // => 21 * ``` */ chain<O2 extends O>( nextReducers: StreamSource<OptLazy<Reducer<I, O2>, [O2]>> ): Reducer<I, O2>; /** * Returns a 'runnable' instance of the current reducer specification. This instance maintains its own state * and indices, so that the instance only needs to be provided the input values, and output values can be * retrieved when needed. The state is kept private. * @example * ```ts * const reducer = Reducer.sum.mapOutput(v => v * 2); * const instance = reducer.compile(); * instance.next(3); * instance.next(5); * console.log(instance.getOutput()); * // => 16 * ``` */ compile(): Reducer.Instance<I, O>; } /** * A base class that can be used to easily create `Reducer` instances. * @typeparam I - the input value type * @typeparam O - the output value type * @typeparam S - the internal state type */ export class Base<I, O, S> implements Reducer.Impl<I, O, S> { constructor( readonly init: (initHalt: () => void) => S, readonly next: (state: S, elem: I, index: number, halt: () => void) => S, readonly stateToResult: (state: S, index: number, halted: boolean) => O ) {} filterInput( pred: (value: I, index: number, halt: () => void) => boolean, options: { negate?: boolean | undefined } = {} ): any { const { negate = false } = options; return create<I, O, Reducer.Instance<I, O>>( () => this.compile(), (state, elem, index, halt) => { if (pred(elem, index, halt) !== negate) { state.next(elem); if (state.halted) { halt(); } } return state; }, (state): O => state.getOutput() ); } mapInput<I2>(mapFun: (value: I2, index: number) => I): Reducer<I2, O> { return create( this.init, (state, elem, index, halt): S => this.next(state, mapFun(elem, index), index, halt), this.stateToResult ); } flatMapInput<I2>( flatMapFun: (value: I2, index: number) => StreamSource<I> ): Reducer<I2, O> { return create<I2, O, Reducer.Instance<I, O>>( () => this.compile(), (state, elem, index, halt) => { if (state.halted) { halt(); return state; } const elems = flatMapFun(elem, index); const iter = Stream.from(elems)[Symbol.iterator](); const done = Symbol(); let value: I | typeof done; while (done !== (value = iter.fastNext(done))) { state.next(value); if (state.halted) { halt(); break; } } return state; }, (state) => state.getOutput() ); } collectInput<I2>(collectFun: CollectFun<I2, I>): Reducer<I2, O> { return create( () => this.compile(), (state, elem, index, halt) => { const nextElem = collectFun(elem, index, CollectFun.Skip, halt); if (CollectFun.Skip !== nextElem) { state.next(nextElem); if (state.halted) { halt(); } } return state; }, (state): O => state.getOutput() ); } mapOutput<O2>( mapFun: (value: O, index: number, halted: boolean) => O2 ): Reducer<I, O2> { return create( this.init, this.next, (state, index, halted): O2 => mapFun(this.stateToResult(state, index, halted), index, halted) ); } takeOutput(amount: number): Reducer<I, O> { if (amount <= 0) { return create( (initHalt) => { initHalt(); return this.init(initHalt); }, this.next, this.stateToResult ); } return create( this.init, (state, next, index, halt) => { if (index >= amount - 1) { halt(); } return this.next(state, next, index, halt); }, this.stateToResult ); } takeOutputUntil( pred: (value: O, index: number) => boolean, options: { negate?: boolean } = {} ): Reducer<I, O> { const { negate = false } = options; return create( this.init, (state, next, index, halt) => { const nextState = this.next(state, next, index, halt); const nextOutput = this.stateToResult(nextState, index, false); if (pred(nextOutput, index) !== negate) { halt(); } return nextState; }, this.stateToResult ); } takeInput(amount: number): Reducer<I, O> { if (amount <= 0) { return create(this.init, identity, this.stateToResult); } return this.filterInput((_, i, halt): boolean => { if (i >= amount - 1) { halt(); } return i < amount; }); } dropInput(amount: number): Reducer<I, O> { if (amount <= 0) { return this; } return this.filterInput((_, i): boolean => i >= amount); } sliceInput(from = 0, amount?: number): Reducer<I, O> { if (undefined === amount) return this.dropInput(from); if (amount <= 0) return create(this.init, identity, this.stateToResult); if (from <= 0) return this.takeInput(amount); return this.takeInput(amount).dropInput(from); } chain<O2 extends O>( nextReducers: StreamSource<OptLazy<Reducer<I, O2>, [O2]>> ): Reducer<I, O2> { return Reducer.create( ( initHalt ): { activeInstance: Reducer.Instance<I, O2>; iterator: FastIterator<OptLazy<Reducer<I, O2>, [O2]>>; } => { const iterator = Stream.from(nextReducers)[Symbol.iterator](); let activeInstance = this.compile() as Reducer.Instance<I, O2>; if (undefined !== activeInstance && activeInstance.halted) { let output = activeInstance.getOutput(); do { const creator = iterator.fastNext(); if (undefined === creator) { initHalt(); return { activeInstance, iterator, }; } activeInstance = OptLazy(creator, output).compile(); output = activeInstance.getOutput(); } while (activeInstance.halted); } return { activeInstance, iterator, }; }, (state, next, index, halt) => { state.activeInstance.next(next); while (state.activeInstance.halted) { const output = state.activeInstance.getOutput(); const creator = state.iterator.fastNext(); if (undefined === creator) { halt(); return state; } state.activeInstance = OptLazy(creator, output).compile(); } return state; }, (state) => state.activeInstance.getOutput() ); } compile(): Reducer.Instance<I, O> { return new Reducer.InstanceImpl(this); } } /** * A reducer instance that manages its own state based on the reducer definition that * was used to create this instance. * @typeparam I - the input element type * @typeparam O - the output element type */ export interface Instance<I, O> { /** * Returns true if the reducer instance does not receive any more values, false otherwise. */ get halted(): boolean; /** * Returns the index of the last received value. */ get index(): number; /** * Method that, when called, halts the reducer instance so that it will no longer receive values. */ halt(): void; /** * Sends a new value into the reducer instance. * @param value - the next input value */ next(value: I): void; /** * Returns the output value based on the current given input values. */ getOutput(): O; } /** * The default `Reducer.Impl` implementation. * @typeparam I - the input element type * @typeparam O - the output element type * @typeparam S - the reducer state type */ export class InstanceImpl<I, O, S> implements Reducer.Instance<I, O> { constructor(readonly reducer: Reducer.Impl<I, O, S>) { this.#state = reducer.init(this.halt); } #state: S; #index = 0; #halted = false; halt = (): void => { this.#halted = true; }; get halted(): boolean { return this.#halted; } get index(): number { return this.#index; } next = (value: I): void => { if (this.#halted) { throw new Reducer.ReducerHaltedError(); } this.#state = this.reducer.next( this.#state, value, this.#index++, this.halt ); }; getOutput(): O { return this.reducer.stateToResult(this.#state, this.index, this.halted); } } /** * Returns a `Reducer` with the given options: * @param init - the initial state value * @param next - returns the next state value based on the given inputs:<br/> * - current: the current state<br/> * - next: the current input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @param stateToResult - a function that converts the current state to an output value * @typeparam I - the input value type * @typeparam O - the output value type * @typeparam S - the internal state type * @example * ```ts * const evenNumberOfOnes = Reducer.create( * true, * (current, value: number) => (value === 1 ? !current : current), * (state) => (state ? 'even' : 'not even') * ); * const result = Stream.of(1, 2, 3, 2, 1).reduce(evenNumberOfOnes); * console.log(result); * // => 'even' * ``` */ export function create<I, O = I, S = O>( init: (initHalt: () => void) => S, next: (current: S, next: I, index: number, halt: () => void) => S, stateToResult: (state: S, index: number, halted: boolean) => O ): Reducer<I, O> { return new Reducer.Base(init, next, stateToResult); } /** * Returns a `Reducer` of which the input, state, and output types are the same. * @param init - the initial state value * @param next - returns the next state value based on the given inputs:<br/> * - current: the current state<br/> * - next: the current input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @param stateToResult - (optional) a function that converts the current state to an output value * @typeparam T - the overall value type * @example * ```ts * const sum = Reducer.createMono( * 0, * (current, value) => current + value * ); * const result = Stream.of(1, 2, 3, 2, 1).reduce(sum); * console.log(result); * // => 9 * ``` */ export function createMono<T>( init: (initHalt: () => void) => T, next: (current: T, next: T, index: number, halt: () => void) => T, stateToResult?: (state: T, index: number, halted: boolean) => T ): Reducer<T> { return create(init, next, stateToResult ?? identity); } /** * Returns a `Reducer` of which the state and output types are the same. * @param init - the initial state value * @param next - returns the next state value based on the given inputs:<br/> * - current: the current state<br/> * - next: the current input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @param stateToResult - (optional) a function that converts the current state to an output value * @typeparam I - the input value type * @typeparam O - the output value type * @example * ```ts * const boolToString = Reducer.createOutput( * '', * (current, value: boolean) => current + (value ? 'T' : 'F') * ); * const result = Stream.of(true, false, true).reduce(boolToString); * console.log(result); * // => 'TFT' * ``` */ export function createOutput<I, O = I>( init: (initHalt: () => void) => O, next: (current: O, next: I, index: number, halt: () => void) => O, stateToResult?: (state: O, index: number, halted: boolean) => O ): Reducer<I, O> { return create(init, next, stateToResult ?? identity); } /** * Returns a `Reducer` that uses the given `init` and `next` values to fold the input values into * result values. * @param init - an (optionally lazy) initial result value * @param next - a function taking the following arguments:<br/> * - current - the current result value<br/> * - value - the next input value<br/> * - index: the input index value<br/> * - halt: function that, when called, ensures no more elements are passed to the reducer * @typeparam T - the input type * @typeparam R - the output type */ export function fold<T, R>( init: OptLazy<R>, next: (current: R, value: T, index: number, halt: () => void) => R ): Reducer<T, R> { return Reducer.createOutput(() => OptLazy(init), next); } /** * A `Reducer` that sums all given numeric input values. * @example * ```ts * console.log(Stream.range({ amount: 5 }).reduce(Reducer.sum)) * // => 10 * ``` */ export const sum = createMono( () => 0, (state, next): number => state + next ); /** * A `Reducer` that calculates the product of all given numeric input values. * @example * ```ts * console.log(Stream.range({ start: 1, amount: 5 }).reduce(product)) * // => 120 * ``` */ export const product = createMono( () => 1, (state, next, _, halt): number => { if (0 === next) halt(); return state * next; } ); /** * A `Reducer` that calculates the average of all given numberic input values. * @example * ```ts * console.log(Stream.range({ amount: 5 }).reduce(Reducer.average)); * // => 2 * ``` */ export const average = createMono( () => 0, (avg, value, index): number => avg + (value - avg) / (index + 1) ); /** * Returns a `Reducer` that remembers the minimum value of the inputs using the given `compFun` to compare input values * @param compFun - a comparison function for two input values, returning 0 when equal, positive when greater, negetive when smaller * @param otherwise - (default: undefineds) a fallback value when there were no input values given * @typeparam T - the element type * @typeparam O - the fallback value type * @example * ```ts * const stream = Stream.of('abc', 'a', 'abcde', 'ab') * console.log(stream.minBy((s1, s2) => s1.length - s2.length)) * // 'a' * ``` */ export const minBy: { <T>(compFun: (v1: T, v2: T) => number): Reducer<T, T | undefined>; <T, O>( compFun: (v1: T, v2: T) => number, otherwise: OptLazy<O> ): Reducer<T, T | O>; } = <T, O>(compFun: (v1: T, v2: T) => number, otherwise?: OptLazy<O>) => { const token = Symbol(); return create<T, T | O, T | typeof token>( () => token, (state, next): T => { if (token === state) { return next; } return compFun(state, next) < 0 ? state : next; }, (state): T | O => (token === state ? OptLazy(otherwise!) : state) ); }; /** * Returns a `Reducer` that remembers the minimum value of the numberic inputs. * @param otherwise - (default: undefined) a fallback value when there were no input values given * @typeparam O - the fallback value type * @example * ```ts * console.log(Stream.of(5, 3, 7, 4).reduce(Reducer.min())) * // => 3 * ``` */ // prettier-ignore export const min: { (): Reducer<number, number | undefined>; <O>(otherwise: OptLazy<O>): Reducer<number, number | O>; } = <O,>(otherwise?: OptLazy<O>) => { return create<number, number | O, number | undefined>( () => undefined, (state, next): number => undefined !== state && state < next ? state : next, (state): number | O => state ?? OptLazy(otherwise!) ); }; /** * Returns a `Reducer` that remembers the maximum value of the inputs using the given `compFun` to compare input values * @param compFun - a comparison function for two input values, returning 0 when equal, positive when greater, negetive when smaller * @param otherwise - (default: undefined) a fallback value when there were no input values given * @typeparam T - the element type * @typeparam O - the fallback value type * @example * ```ts * const stream = Stream.of('abc', 'a', 'abcde', 'ab') * console.log(stream.maxBy((s1, s2) => s1.length - s2.length)) * // 'abcde' * ``` */ export const maxBy: { <T>(compFun: (v1: T, v2: T) => number): Reducer<T, T | undefined>; <T, O>( compFun: (v1: T, v2: T) => number, otherwise: OptLazy<O> ): Reducer<T, T | O>; } = <T, O>( compFun: (v1: T, v2: T) => number, otherwise?: OptLazy<O> ): Reducer<T, T | O> => { const token = Symbol(); return create<T, T | O, T | typeof token>( () => token, (state, next): T => { if (token === state) { return next; } return compFun(state, next) > 0 ? state : next; }, (state): T | O => (token === state ? OptLazy(otherwise!) : state) ); }; /** * Returns a `Reducer` that remembers the maximum value of the numberic inputs. * @param otherwise - (default: undefined) a fallback value when there were no input values given * @typeparam O - the fallback value type * @example * ```ts * console.log(Stream.of(5, 3, 7, 4).reduce(Reducer.max())) * // => 7 * ``` */ // prettier-ignore export const max: { (): Reducer<number, number | undefined>; <O>(otherwise: OptLazy<O>): Reducer<number, number | O>; } = <O,>(otherwise?: OptLazy<O>): Reducer<number, number | O> => { return create<number, number | O, number | undefined>( () => undefined, (state, next): number => undefined !== state && state > next ? state : next, (state): number | O => state ?? OptLazy(otherwise!) ); }; /** * Returns a `Reducer` that joins the given input values into a string using the given options. * @param options - an object containing:<br/> * - sep: (optional) a seperator string value between values in the output<br/> * - start: (optional) a start string to prepend to the output<br/> * - end: (optional) an end string to append to the output<br/> * @typeparam T - the input element type * @example * ```ts * console.log(Stream.of(1, 2, 3).reduce(Reducer.join({ sep: '-' }))) * // => '1-2-3' * ``` */ export function join<T>({ sep = '', start = '', end = '', valueToString = String as (value: T) => string, } = {}): Reducer<T, string> { return create( () => '', (state, next, index) => { const valueString = valueToString(next); if (index <= 0) { return start.concat(valueString); } return state.concat(sep, valueToString(next)); }, (state): string => state.concat(end) ); } /** * A `Reducer` that remembers the amount of input items provided. * @example * ```ts * const stream = Stream.range({ amount: 10 }) * console.log(stream.reduce(Reducer.count)) * // => 10 * ``` */ export const count: Reducer<any, number> = create<any, number, void>( () => { // }, identity, (_, index) => index ); /** * Returns a `Reducer` that remembers the first input value. * @param otherwise - (default: undefined) a fallback value to output if no input value has been provided * @typeparam T - the input value type * @typeparam O - the fallback value type * @example * ```ts * console.log(Stream.range({ amount: 10 }).reduce(Reducer.first())) * // => 0 * ``` */ export const first: { <T>(): Reducer<T, T | undefined>; <T, O>(otherwise: OptLazy<O>): Reducer<T, T | O>; } = <T, O>(otherwise?: OptLazy<O>): Reducer<T, T | O> => { return create<T, T | O, T | undefined>( () => undefined, (state, next, _, halt): T => { halt(); return next; }, (state, index): T | O => (index <= 0 ? OptLazy(otherwise!) : state!) ); }; /** * Returns a `Reducer` that remembers the last input value. * @param otherwise - (default: undefined) a fallback value to output if no input value has been provided * @typeparam T - the input value type * @typeparam O - the fallback value type * @example * ```ts * console.log(Stream.range({ amount: 10 }).reduce(Reducer.last())) * // => 9 * ``` */ export const last: { <T>(): Reducer<T, T | undefined>; <T, O>(otherwise: OptLazy<O>): Reducer<T, T | O>; } = <T, O>(otherwise?: OptLazy<O>): Reducer<T, T | O> => { return create<T, T | O, T | undefined>( () => undefined, (_, next): T => next, (state, index): T | O => (index <= 0 ? OptLazy(otherwise!) : state!) ); }; /** * Returns a Reducer that only produces an output value when having receives exactly one * input value, otherwise will return the `otherwise` value or undefined. * @param otherwise - the fallback value to return when more or less than one value is received. * @typeparam T - the element type * @typeparam O - the fallback value type */ export const single: { <T>(): Reducer<T, T | undefined>; <T, O>(otherwise: OptLazy<O>): Reducer<T, T | O>; } = <T, O>(otherwise?: OptLazy<O>): Reducer<T, T | O> => { return create<T, T | O, T | undefined>( () => undefined, (state, next, index, halt): T => { if (index > 1) { halt(); } return next; }, (state, index): T | O => (index !== 1 ? OptLazy(otherwise!) : state!) ); }; /** * Returns a `Reducer` that ouputs false as long as no input value satisfies given `pred`, true otherwise. * @typeparam T - the element type * @param pred - a function taking an input value and its index, and returning true if the value satisfies the predicate * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate * @example * ```ts * console.log( * Stream.range({ amount: 10 }).reduce(Reducer.some((v) => v > 5)) * ) * // => true * ``` */ export function some<T>( pred: (value: T, index: number) => boolean, options: { negate?: boolean } = {} ): Reducer<T, boolean> { return nonEmpty.filterInput(pred, options); } /** * Returns a `Reducer` that ouputs true as long as all input values satisfy the given `pred`, false otherwise. * @typeparam T - the element type * @param pred - a function taking an input value and its index, and returning true if the value satisfies the predicate * @param options - (optional) an object containing the following properties:<br/> * - negate: (default: false) when true will invert the given predicate * @example * ```ts * console.log( * Stream.range({ amount: 10 }).reduce(Reducer.every((v) => v < 5)) * ) * // => false * ``` */ export function every<T>( pred: (value: T, index: number) => boolean, options: { negate?: boolean } = {} ): Reducer<T, boolean> { const { negate = false } = options; return isEmpty.filterInput(pred, { negate: !negate }); } /** * Returns a `Reducer` that ouputs true when the received elements match the given `other` stream source according to the `eq` instance, false otherwise. * @typeparam T - the element type * @param other - a stream source containg elements to match against * @param options - (optional) an object containing the following properties:<br/> * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements * - negate: (default: false) when true will invert the given predicate */ export function equals<T>( other: StreamSource<T>, options: { eq?: Eq<T>; negate?: boolean } = {} ): Reducer<T, boolean> { const { eq = Eq.objectIs, negate = false } = options; const sliceStream = Stream.from(other); const done = Symbol(); return Reducer.create< T, boolean, { iter: FastIterator<T>; nextSeq: T | typeof done; result: boolean } >( () => { const iter = sliceStream[Symbol.iterator](); const nextSeq = iter.fastNext(done); return { iter, nextSeq, result: false }; }, (state, next, _, halt) => { if (done === state.nextSeq) { halt(); state.result = false; return state; } if (eq(next, state.nextSeq) === negate) { halt(); state.result = false; return state; } state.nextSeq = state.iter.fastNext(done); if (done === state.nextSeq) { state.result = true; } return state; }, (state, index, halted) => !halted && done === state.nextSeq ); } /** * Returns a `Reducer` that outputs false as long as the given `elem` has not been encountered the given `amount` of times in the input values, true otherwise. * @typeparam T - the element type * @param elem - the element to search for * @param options - (optional) an object containing the following properties:<br/> * - amount: (detaulf: 1) the amount of elements to find * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements * - negate: (default: false) when true will invert the given predicate * @example * ```ts * console.log(Stream.range({ amount: 10 }).reduce(Reducer.contains(5))) * // => true * ``` */ export function contains<T, T2 extends T = T>( elem: T2, options: { amount?: number; eq?: Eq<T | T2>; negate?: boolean; } = {} ): Reducer<T, boolean> { const { amount = 1, eq = Object.is, negate = false } = options; return Reducer.create<T, boolean, number>( (initHalt) => { if (amount <= 0) { initHalt(); } return amount; }, (state, next, _, halt): number => { const satisfies = eq(next, elem) !== negate; if (!satisfies) { return state; } const newRemain = state - 1; if (newRemain <= 0) { halt(); } return newRemain; }, (state) => state <= 0 ); } /** * Returns a `Reducer` that returns true if the first input values match the given `slice` values repeated `amount` times. Otherwise, * returns false. * @param slice - a sequence of elements to match against * @param options - (optional) an object containing the following properties:<br/> * - amount: (detaulf: 1) the amount of elements to find * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements */ export function startsWithSlice<T>( slice: StreamSource<T>, options: { eq?: Eq<T> | undefined; amount?: number } = {} ): Reducer<T, boolean> { const sliceStream = Stream.from(slice); const done = Symbol(); const { eq = Eq.objectIs, amount = 1 } = options; return Reducer.create< T, boolean, { sliceIter: FastIterator<T>; sliceValue: T | typeof done; remain: number; } >( (initHalt) => { const sliceIter = sliceStream[Symbol.iterator](); const sliceValue = sliceIter.fastNext(done); if (done === sliceValue || amount <= 0) { initHalt(); return { sliceIter, sliceValue, remain: 0 }; } return { sliceIter, sliceValue: sliceValue, remain: amount, }; }, (state, next, _, halt) => { if (done === state.sliceValue) { RimbuError.throwInvalidStateError(); } if (eq(next, state.sliceValue)) { state.sliceValue = state.sliceIter.fastNext(done); if (done === state.sliceValue) { state.remain--; if (state.remain <= 0) { halt(); } else { state.sliceIter = sliceStream[Symbol.iterator](); state.sliceValue = state.sliceIter.fastNext(done); } } } else { halt(); } return state; }, (state) => state.remain <= 0 ); } /** * Returns a `Reducer` that returns true if the last input values match the given `slice` values repeated `amount` times. Otherwise, * returns false. * @param slice - a sequence of elements to match against * @param options - (optional) an object containing the following properties:<br/> * - amount: (detaulf: 1) the amount of elements to find * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements */ export function endsWithSlice<T>( slice: StreamSource<T>, options: { eq?: Eq<T> | undefined; amount?: number } = {} ): Reducer<T, boolean> { const sliceStream = Stream.from(slice); const done = Symbol(); const newReducerSpec = Reducer.startsWithSlice(slice, options); return Reducer.create<T, boolean, Set<Reducer.Instance<T, boolean>>>( (initHalt) => { const sliceIter = sliceStream[Symbol.iterator](); const sliceValue = sliceIter.fastNext(done); if (done === sliceValue) { initHalt(); } return new Set([newReducerSpec.compile()]); }, (state, nextValue) => { for (const instance of state) { if (instance.halted) { state.delete(instance); } else { instance.next(nextValue); } } const newReducerInstance = newReducerSpec.compile(); newReducerInstance.next(nextValue); state.add(newReducerInstance); return state; }, (state) => state.size === 0 || Stream.from(state).some((instance) => instance.getOutput()) ); } /** * Returns a `Reducer` that returns true if the input values contain the given `slice` sequence `amount` times. Otherwise, * returns false. * @param slice - a sequence of elements to match against * @param options - (optional) an object containing the following properties:<br/> * - amount: (detaulf: 1) the amount of elements to find * - eq: (default: Eq.objectIs) the `Eq` instance to use to compare elements */ export function containsSlice<T>( slice: StreamSource<T>, options: { eq?: Eq<T> | undefined; amount?: number } = {} ): Reducer<T, boolean> { const { eq, amount = 1 } = options; return Reducer.pipe( endsWithSlice(slice, { eq }), Reducer.contains(true, { amount }) ); } /** * A `Reducer` that takes boolean values and outputs true if all input values are true, and false otherwise. * @example * ```ts * console.log(Stream.of(true, false, true)).reduce(Reducer.and)) * // => false * ``` */ export const and = createMono( () => true, (state, next, _, halt): boolean => { if (!next) { halt(); } return next; } ); /** * A `Reducer` that takes boolean values and outputs true if one or more input values are true, and false otherwise. * @example * ```ts * console.log(Stream.of(true, false, true)).reduce(Reducer.or)) * // => true * ``` */ export const or = createMono( () => false, (state, next, _, halt): boolean => { if (next) { halt(); } return next; } ); /** * A `Reducer` that outputs true if no input values are received, false otherwise. * @example * ```ts * console.log(Stream.of(1, 2, 3).reduce(Reducer.isEmpty)) * // => false * ``` */ export const isEmpty = createOutput<any, boolean>( () => true, (_, __, ___, halt): false => { halt(); return false; } ); /** * A `Reducer` that outputs true if one or more input values are received, false otherwise. * @example * ```ts * console.log(Stream.of(1, 2, 3).reduce(Reducer.nonEmpty)) * // => true * ``` */ export const nonEmpty = createOutput<any, boolean>( () => false, (_, __, ___, halt): true => { halt(); return true; } ); /** * Returns a `Reducer` that always outputs the given `value`, and does not accept input values. */ export function constant<T>(value: OptLazy<T>): Reducer<any, T> { return Reducer.create<any, T, void>( (initHalt) => { initHalt(); }, identity, () => OptLazy(value) ); } /** * Returns a `Reducer` that splits the incoming values into two separate outputs based on the given `pred` predicate. Values for which the predicate is true * are fed into the `collectorTrue` reducer, and other values are fed into the `collectorFalse` instance. If no collectors are provided the values are collected * into arrays. * @param pred - a predicate receiving the value and its index * @param options - (optional) an object containing the following properties:<br/> * - collectorTrue: (default: Reducer.toArray()) a reducer that collects the values for which the predicate is true<br/> * - collectorFalse: (default: Reducer.toArray()) a reducer that collects the values for which the predicate is false * @typeparam T - the input element type * @typeparam RT - the reducer result type for the `collectorTrue` value * @typeparam RF - the reducer result type for the `collectorFalse` value * @note if the predicate is a type guard, the return type is automatically inferred * @example * ```ts * Stream.of(1, 2, 3).partition((v) => v % 2 === 0) * // => [[2], [1, 3]] * * Stream.of<number | string>(1, 'a', 'b', 2) * .partition((v): v is string => typeof v === 'string') * // => [['a', 'b'], [1, 2]] * // return type is: [string[], number[]] * * Stream.of(1, 2, 3, 4).partition( * (v) => v % 2 === 0, * { collectorTrue: Reducer.toJSSet(), collectorFalse: Reducer.sum } * ) * // => [Set(2, 4), 4] * ``` */ export const partition: { <T, T2 extends T, RT, RF = RT>( pred: (value: T, index: number) => value is T2, options: { collectorTrue: Reducer<T2, RT>; collectorFalse: Reducer<Exclude<T, T2>, RF>; } ): Reducer<T, [true: RT, false: RF]>; <T, T2 extends T>( pred: (value: T, index: number) => value is T2, options?: { collectorTrue?: undefined; collectorFalse?: undefined; } ): Reducer<T, [true: T2[], false: Exclude<T, T2>[]]>; <T, RT, RF = RT>( pred: (value: T, index: number) => boolean, options: { collectorTrue: Reducer<T, RT>; collectorFalse: Reducer<T, RF>; } ): Reducer<T, [true: RT, false: RF]>; <T>( pred: (value: T, index: number) => boolean, options?: { collectorTrue?: undefined; collectorFalse?: undefined; } ): Reducer<T, [true: T[], false: T[]]>; } = <T, RT, RF = RT>( pred: (value: T, index: number) => boolean, options: any = {} ): any => { const collectorTrue: Reducer<T, RT> = options.collectorTrue ?? Reducer.toArray(); const collectorFalse: Reducer<T, RF> = options.collectorFalse ?? Reducer.toArray(); return Reducer.create< T, [RT, RF], [Reducer.Instance<T, RT>, Reducer.Instance<T, RF>] >( () => [collectorTrue.compile(), collectorFalse.compile()], (state, value, index) => { const instanceIndex = pred(value, index) ? 0 : 1; state[instanceIndex].next(value); return state; }, (state) => state.map((v) => v.getOutput()) as [RT, RF] ); }; /** * Returns a `Reducer` that uses the `valueToKey` function to calculate a key for each value, and feeds the tuple of the key and the value to the * `collector` reducer. Finally, it returns the output of the `collector`. If no collector is given, the default collector will return a JS multimap * of the type `Map<K, V[]>`. * @param valueToKey - function taking a value and its index, and returning the corresponding key * @param options - (optional) an object containing the following properties:<br/> * - collector: (default: Reducer.toArray()) a reducer that collects the incoming tuple of key and value, and provides the output * @typeparam T - the input value type * @typeparam K - the key type * @typeparam R - the collector output type * @example * ```ts * Stream.of(1, 2, 3).groupBy((v) => v % 2) * // => Map {0 => [2], 1 => [1, 3]} * ``` */ export const groupBy: { <T, K, R, T2 extends readonly [K, T] = [K, T]>( valueToKey: (value: T, index: number) => K, options: { collector: Reducer<[K, T] | T2, R>; } ): Reducer<T, R>; <T, K>( valueToKey: (value: T, index: number) => K, options?: { collector?: undefined; } ): Reducer<T, Map<K, T[]>>; } = <T, K, R>( valueToKey: (value: T, index: number) => K, options: { collector?: Reducer<readonly [K, T], R> | undefined; } = {} ): Reducer<T, R> => { const { collector = Reducer.toJSMultiMap() as Reducer<readonly [K, T], R>, } = options; return Reducer.create( () => collector.compile(), (state, value, index) => { const key = valueToKey(value, index); state.next([key, value]); return state; }, (state) => state.getOutput() ); }; /** * Returns a `Reducer` that feeds incoming values to all reducers in the provided `reducers` source, and halts when the first * reducer in the array is halted and returns the output of that reducer. Returns the `otherwise` value if no reducer is yet halted. * @param reducers - a stream source of reducers that will receive the incoming values * @param otherwise - a fallback value to return if none of the reducers has been hal