From 86e451cbb606486fe9b4a68f3d7b969ab240f7c5 Mon Sep 17 00:00:00 2001 From: Arvid Nicolaas Date: Sun, 2 Oct 2022 11:17:43 +0200 Subject: [PATCH] feat(common): add reducer combineObj method and rename old combine to combineArr BREAKING CHANGE: reducer and asyncReducer combine method has been renamed to combineArr --- deno_dist/common/async-reducer.ts | 127 +++++++++++++++++++-- deno_dist/common/reducer.ts | 119 +++++++++++++++++-- packages/common/src/async-reducer.ts | 127 +++++++++++++++++++-- packages/common/src/reducer.ts | 119 +++++++++++++++++-- packages/common/test-d/reducer.test.ts | 27 +++++ packages/common/test/async-reducer.test.ts | 69 ++++++++++- packages/common/test/reducer.test.ts | 67 ++++++++++- 7 files changed, 605 insertions(+), 50 deletions(-) create mode 100644 packages/common/test-d/reducer.test.ts diff --git a/deno_dist/common/async-reducer.ts b/deno_dist/common/async-reducer.ts index 40798c82f..793095c6f 100644 --- a/deno_dist/common/async-reducer.ts +++ b/deno_dist/common/async-reducer.ts @@ -574,15 +574,15 @@ export namespace AsyncReducer { * ``` */ export const count: { - (): AsyncReducer; + (): AsyncReducer; (pred: (value: T, index: number) => MaybePromise): AsyncReducer< T, number >; } = ( pred?: (value: any, index: number) => MaybePromise - ): AsyncReducer => { - if (undefined === pred) return createMono(0, (_, __, i): number => i + 1); + ): AsyncReducer => { + if (undefined === pred) return createOutput(0, (_, __, i): number => i + 1); return createOutput(0, async (state, next, i): Promise => { if (await pred?.(next, i)) return state + 1; @@ -843,7 +843,7 @@ export namespace AsyncReducer { * // => false * ``` */ - export const isEmpty = createOutput( + export const isEmpty = createOutput( true, (_, __, ___, halt): false => { halt(); @@ -859,7 +859,7 @@ export namespace AsyncReducer { * // => true * ``` */ - export const nonEmpty = createOutput( + export const nonEmpty = createOutput( false, (_, __, ___, halt): true => { halt(); @@ -956,16 +956,18 @@ export namespace AsyncReducer { } /** - * Returns a `Reducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in an array. + * Returns an `AsyncReducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in an array. * @param reducers - 2 or more reducers to combine * @example * ```ts - * const red = Reducer.combine(Reducer.sum, Reducer.average) - * console.log(Stream.range({amount: 9 }).reduce(red)) + * const red = AsyncReducer.combineArr(AsyncReducer.sum, AsyncReducer.average) + * + * await AsyncStream.from(Stream.range({ amount: 9 })) + * .reduce(red) * // => [36, 4] * ``` */ - export function combine< + export function combineArr< T, R extends readonly [unknown, unknown, ...unknown[]] >( @@ -1036,4 +1038,111 @@ export namespace AsyncReducer { ) as any ); } + + /** + * Returns an `AsyncReducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in the shape of the given object. + * @typeparam T - the input type for all the reducers + * @typeparam R - the result object shape + * @param reducerObj - an object of keys, and reducers corresponding to those keys + * @example + * ```ts + * const red = AsyncReducer.combineObj({ + * theSum: Reducer.sum, + * theAverage: Reducer.average + * }); + * + * await AsyncStream.from(Stream.range({ amount: 9 })) + * .reduce(red)); + * // => { theSum: 36, theAverage: 4 } + * ``` + */ + export function combineObj( + reducerObj: { readonly [K in keyof R]: AsyncReducer } & Record< + string, + AsyncReducer + > + ): AsyncReducer { + const createState = async (): Promise< + Record< + keyof R, + { + reducer: AsyncReducer; + halted: boolean; + halt(): void; + state: unknown; + } + > + > => { + const entries = await Promise.all( + Object.entries(reducerObj).map(async ([key, reducer]) => { + const result = { + reducer, + halted: false, + halt(): void { + result.halted = true; + }, + state: await AsyncOptLazy.toMaybePromise(reducer.init), + }; + + return [key, result] as const; + }) + ); + + return Object.fromEntries(entries) as any; + }; + + return create< + T, + R, + Record< + keyof R, + { + reducer: AsyncReducer; + halted: boolean; + halt(): void; + state: unknown; + } + > + >( + createState, + async (allState, next, index, halt) => { + let anyNotHalted = false; + + await Promise.all( + Object.values(allState).map(async (red) => { + if (red.halted) { + return; + } + + red.state = await red.reducer.next( + red.state, + next, + index, + red.halt + ); + + if (!red.halted) { + anyNotHalted = true; + } + }) + ); + + if (!anyNotHalted) { + halt(); + } + + return allState; + }, + async (allState) => { + const entries = await Promise.all( + Object.entries(allState).map( + async ([key, st]) => + [key, await st.reducer.stateToResult(st.state)] as const + ) + ); + + return Object.fromEntries(entries) as any; + } + ); + } } diff --git a/deno_dist/common/reducer.ts b/deno_dist/common/reducer.ts index 1520cbd71..e92e5e1fb 100644 --- a/deno_dist/common/reducer.ts +++ b/deno_dist/common/reducer.ts @@ -501,10 +501,12 @@ export namespace Reducer { * ``` */ export const count: { - (): Reducer; + (): Reducer; (pred: (value: T, index: number) => boolean): Reducer; - } = (pred?: (value: any, index: number) => boolean): Reducer => { - if (undefined === pred) return createMono(0, (_, __, i): number => i + 1); + } = ( + pred?: (value: unknown, index: number) => boolean + ): Reducer => { + if (undefined === pred) return createOutput(0, (_, __, i): number => i + 1); return createOutput(0, (state, next, i): number => { if (pred?.(next, i)) return state + 1; @@ -749,7 +751,7 @@ export namespace Reducer { * // => false * ``` */ - export const isEmpty = createOutput( + export const isEmpty = createOutput( true, (_, __, ___, halt): false => { halt(); @@ -765,7 +767,7 @@ export namespace Reducer { * // => true * ``` */ - export const nonEmpty = createOutput( + export const nonEmpty = createOutput( false, (_, __, ___, halt): true => { halt(); @@ -866,12 +868,12 @@ export namespace Reducer { * @param reducers - 2 or more reducers to combine * @example * ```ts - * const red = Reducer.combine(Reducer.sum, Reducer.average) + * const red = Reducer.combineArr(Reducer.sum, Reducer.average) * console.log(Stream.range({amount: 9 }).reduce(red)) * // => [36, 4] * ``` */ - export function combine< + export function combineArr< T, R extends readonly [unknown, unknown, ...unknown[]] >( @@ -904,16 +906,24 @@ export namespace Reducer { let i = -1; const len = allState.length; + while (++i < len) { const red = allState[i]; - if (red.halted) continue; + if (red.halted) { + continue; + } red.state = red.reducer.next(red.state, next, index, red.halt); - if (!red.halted) anyNotHalted = true; + + if (!red.halted) { + anyNotHalted = true; + } } - if (!anyNotHalted) halt(); + if (!anyNotHalted) { + halt(); + } return allState; }, @@ -921,4 +931,93 @@ export namespace Reducer { allState.map((st) => st.reducer.stateToResult(st.state)) as any ); } + + /** + * Returns a `Reducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in the shape of the given object. + * @typeparam T - the input type for all the reducers + * @typeparam R - the result object shape + * @param reducerObj - an object of keys, and reducers corresponding to those keys + * @example + * ```ts + * const red = Reducer.combineObj({ + * theSum: Reducer.sum, + * theAverage: Reducer.average + * }); + * + * Stream.range({ amount: 9 }).reduce(red); + * // => { theSum: 36, theAverage: 4 } + * ``` + */ + export function combineObj( + reducerObj: { readonly [K in keyof R]: Reducer } & Record< + string, + Reducer + > + ): Reducer { + const createState = (): Record< + keyof R, + { + reducer: Reducer; + halted: boolean; + halt(): void; + state: unknown; + } + > => { + const allState: any = {}; + + for (const key in reducerObj) { + const reducer = reducerObj[key]; + + const result = { + reducer, + halted: false, + halt(): void { + result.halted = true; + }, + state: OptLazy(reducer.init), + }; + + allState[key] = result; + } + + return allState; + }; + + return create>( + createState, + (allState, next, index, halt) => { + let anyNotHalted = false; + + for (const key in allState) { + const red = allState[key]; + + if (red.halted) { + continue; + } + + red.state = red.reducer.next(red.state, next, index, red.halt); + + if (!red.halted) { + anyNotHalted = true; + } + } + + if (!anyNotHalted) { + halt(); + } + + return allState; + }, + (allState) => { + const result: any = {}; + + for (const key in allState) { + const st = allState[key]; + result[key] = st.reducer.stateToResult(st.state); + } + + return result; + } + ); + } } diff --git a/packages/common/src/async-reducer.ts b/packages/common/src/async-reducer.ts index e1220eab0..afcbc4229 100644 --- a/packages/common/src/async-reducer.ts +++ b/packages/common/src/async-reducer.ts @@ -574,15 +574,15 @@ export namespace AsyncReducer { * ``` */ export const count: { - (): AsyncReducer; + (): AsyncReducer; (pred: (value: T, index: number) => MaybePromise): AsyncReducer< T, number >; } = ( pred?: (value: any, index: number) => MaybePromise - ): AsyncReducer => { - if (undefined === pred) return createMono(0, (_, __, i): number => i + 1); + ): AsyncReducer => { + if (undefined === pred) return createOutput(0, (_, __, i): number => i + 1); return createOutput(0, async (state, next, i): Promise => { if (await pred?.(next, i)) return state + 1; @@ -843,7 +843,7 @@ export namespace AsyncReducer { * // => false * ``` */ - export const isEmpty = createOutput( + export const isEmpty = createOutput( true, (_, __, ___, halt): false => { halt(); @@ -859,7 +859,7 @@ export namespace AsyncReducer { * // => true * ``` */ - export const nonEmpty = createOutput( + export const nonEmpty = createOutput( false, (_, __, ___, halt): true => { halt(); @@ -956,16 +956,18 @@ export namespace AsyncReducer { } /** - * Returns a `Reducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in an array. + * Returns an `AsyncReducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in an array. * @param reducers - 2 or more reducers to combine * @example * ```ts - * const red = Reducer.combine(Reducer.sum, Reducer.average) - * console.log(Stream.range({amount: 9 }).reduce(red)) + * const red = AsyncReducer.combineArr(AsyncReducer.sum, AsyncReducer.average) + * + * await AsyncStream.from(Stream.range({ amount: 9 })) + * .reduce(red) * // => [36, 4] * ``` */ - export function combine< + export function combineArr< T, R extends readonly [unknown, unknown, ...unknown[]] >( @@ -1036,4 +1038,111 @@ export namespace AsyncReducer { ) as any ); } + + /** + * Returns an `AsyncReducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in the shape of the given object. + * @typeparam T - the input type for all the reducers + * @typeparam R - the result object shape + * @param reducerObj - an object of keys, and reducers corresponding to those keys + * @example + * ```ts + * const red = AsyncReducer.combineObj({ + * theSum: Reducer.sum, + * theAverage: Reducer.average + * }); + * + * await AsyncStream.from(Stream.range({ amount: 9 })) + * .reduce(red)); + * // => { theSum: 36, theAverage: 4 } + * ``` + */ + export function combineObj( + reducerObj: { readonly [K in keyof R]: AsyncReducer } & Record< + string, + AsyncReducer + > + ): AsyncReducer { + const createState = async (): Promise< + Record< + keyof R, + { + reducer: AsyncReducer; + halted: boolean; + halt(): void; + state: unknown; + } + > + > => { + const entries = await Promise.all( + Object.entries(reducerObj).map(async ([key, reducer]) => { + const result = { + reducer, + halted: false, + halt(): void { + result.halted = true; + }, + state: await AsyncOptLazy.toMaybePromise(reducer.init), + }; + + return [key, result] as const; + }) + ); + + return Object.fromEntries(entries) as any; + }; + + return create< + T, + R, + Record< + keyof R, + { + reducer: AsyncReducer; + halted: boolean; + halt(): void; + state: unknown; + } + > + >( + createState, + async (allState, next, index, halt) => { + let anyNotHalted = false; + + await Promise.all( + Object.values(allState).map(async (red) => { + if (red.halted) { + return; + } + + red.state = await red.reducer.next( + red.state, + next, + index, + red.halt + ); + + if (!red.halted) { + anyNotHalted = true; + } + }) + ); + + if (!anyNotHalted) { + halt(); + } + + return allState; + }, + async (allState) => { + const entries = await Promise.all( + Object.entries(allState).map( + async ([key, st]) => + [key, await st.reducer.stateToResult(st.state)] as const + ) + ); + + return Object.fromEntries(entries) as any; + } + ); + } } diff --git a/packages/common/src/reducer.ts b/packages/common/src/reducer.ts index 84a6f7c1b..874d0d0ca 100644 --- a/packages/common/src/reducer.ts +++ b/packages/common/src/reducer.ts @@ -501,10 +501,12 @@ export namespace Reducer { * ``` */ export const count: { - (): Reducer; + (): Reducer; (pred: (value: T, index: number) => boolean): Reducer; - } = (pred?: (value: any, index: number) => boolean): Reducer => { - if (undefined === pred) return createMono(0, (_, __, i): number => i + 1); + } = ( + pred?: (value: unknown, index: number) => boolean + ): Reducer => { + if (undefined === pred) return createOutput(0, (_, __, i): number => i + 1); return createOutput(0, (state, next, i): number => { if (pred?.(next, i)) return state + 1; @@ -749,7 +751,7 @@ export namespace Reducer { * // => false * ``` */ - export const isEmpty = createOutput( + export const isEmpty = createOutput( true, (_, __, ___, halt): false => { halt(); @@ -765,7 +767,7 @@ export namespace Reducer { * // => true * ``` */ - export const nonEmpty = createOutput( + export const nonEmpty = createOutput( false, (_, __, ___, halt): true => { halt(); @@ -866,12 +868,12 @@ export namespace Reducer { * @param reducers - 2 or more reducers to combine * @example * ```ts - * const red = Reducer.combine(Reducer.sum, Reducer.average) + * const red = Reducer.combineArr(Reducer.sum, Reducer.average) * console.log(Stream.range({amount: 9 }).reduce(red)) * // => [36, 4] * ``` */ - export function combine< + export function combineArr< T, R extends readonly [unknown, unknown, ...unknown[]] >( @@ -904,16 +906,24 @@ export namespace Reducer { let i = -1; const len = allState.length; + while (++i < len) { const red = allState[i]; - if (red.halted) continue; + if (red.halted) { + continue; + } red.state = red.reducer.next(red.state, next, index, red.halt); - if (!red.halted) anyNotHalted = true; + + if (!red.halted) { + anyNotHalted = true; + } } - if (!anyNotHalted) halt(); + if (!anyNotHalted) { + halt(); + } return allState; }, @@ -921,4 +931,93 @@ export namespace Reducer { allState.map((st) => st.reducer.stateToResult(st.state)) as any ); } + + /** + * Returns a `Reducer` that combines multiple input `reducers` by providing input values to all of them and collecting the outputs in the shape of the given object. + * @typeparam T - the input type for all the reducers + * @typeparam R - the result object shape + * @param reducerObj - an object of keys, and reducers corresponding to those keys + * @example + * ```ts + * const red = Reducer.combineObj({ + * theSum: Reducer.sum, + * theAverage: Reducer.average + * }); + * + * Stream.range({ amount: 9 }).reduce(red); + * // => { theSum: 36, theAverage: 4 } + * ``` + */ + export function combineObj( + reducerObj: { readonly [K in keyof R]: Reducer } & Record< + string, + Reducer + > + ): Reducer { + const createState = (): Record< + keyof R, + { + reducer: Reducer; + halted: boolean; + halt(): void; + state: unknown; + } + > => { + const allState: any = {}; + + for (const key in reducerObj) { + const reducer = reducerObj[key]; + + const result = { + reducer, + halted: false, + halt(): void { + result.halted = true; + }, + state: OptLazy(reducer.init), + }; + + allState[key] = result; + } + + return allState; + }; + + return create>( + createState, + (allState, next, index, halt) => { + let anyNotHalted = false; + + for (const key in allState) { + const red = allState[key]; + + if (red.halted) { + continue; + } + + red.state = red.reducer.next(red.state, next, index, red.halt); + + if (!red.halted) { + anyNotHalted = true; + } + } + + if (!anyNotHalted) { + halt(); + } + + return allState; + }, + (allState) => { + const result: any = {}; + + for (const key in allState) { + const st = allState[key]; + result[key] = st.reducer.stateToResult(st.state); + } + + return result; + } + ); + } } diff --git a/packages/common/test-d/reducer.test.ts b/packages/common/test-d/reducer.test.ts new file mode 100644 index 000000000..312079130 --- /dev/null +++ b/packages/common/test-d/reducer.test.ts @@ -0,0 +1,27 @@ +import { Stream } from '@rimbu/stream'; +import { Reducer } from '@rimbu/common'; +import { expectType } from 'tsd'; + +expectType>( + Reducer.combineArr(Reducer.toArray(), Reducer.sum) +); + +expectType<[number[], number]>( + Stream.of(1, 2).reduce(Reducer.combineArr(Reducer.toArray(), Reducer.sum)) +); + +expectType>( + Reducer.combineObj({ + a: Reducer.toArray(), + s: Reducer.sum, + }) +); + +expectType<{ a: number[]; s: number }>( + Stream.of(1, 2).reduce( + Reducer.combineObj({ + a: Reducer.toArray(), + s: Reducer.sum, + }) + ) +); diff --git a/packages/common/test/async-reducer.test.ts b/packages/common/test/async-reducer.test.ts index 6054e3d62..06855615a 100644 --- a/packages/common/test/async-reducer.test.ts +++ b/packages/common/test/async-reducer.test.ts @@ -515,8 +515,8 @@ describe('AsyncReducers', () => { ).toEqual([0, 1, 2]); }); - it('AsyncReducer.combine', async () => { - const r = AsyncReducer.combine(AsyncReducer.sum, AsyncReducer.average); + it('AsyncReducer.combineArr', async () => { + const r = AsyncReducer.combineArr(AsyncReducer.sum, AsyncReducer.average); expect(await AsyncStream.empty().reduce(r)).toEqual([0, 0]); expect(await AsyncStream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ @@ -531,8 +531,8 @@ describe('AsyncReducers', () => { ]); }); - it('AsyncReducer.combine with halt', async () => { - const r = AsyncReducer.combine(AsyncReducer.sum, AsyncReducer.product); + it('AsyncReducer.combineArr with halt', async () => { + const r = AsyncReducer.combineArr(AsyncReducer.sum, AsyncReducer.product); expect(await AsyncStream.empty().reduce(r)).toEqual([0, 1]); expect(await AsyncStream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ @@ -547,8 +547,8 @@ describe('AsyncReducers', () => { ]); }); - it('AsyncReducer.combine with stateToResult', async () => { - const r = AsyncReducer.combine( + it('AsyncReducer.combineArr with stateToResult', async () => { + const r = AsyncReducer.combineArr( AsyncReducer.sum.mapOutput(async (v) => v + 1), AsyncReducer.product ); @@ -566,6 +566,63 @@ describe('AsyncReducers', () => { ]); }); + it('AsyncReducer.combineObj', async () => { + const r = AsyncReducer.combineObj({ + sum: AsyncReducer.sum, + avg: AsyncReducer.average, + }); + + expect(await AsyncStream.empty().reduce(r)).toEqual({ sum: 0, avg: 0 }); + expect(await AsyncStream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ + { sum: 0, avg: 0 }, + { sum: 0, avg: 0 }, + { sum: 0, avg: 0 }, + ]); + expect(await AsyncStream.of(0, 2, 4).reduceStream(r).toArray()).toEqual([ + { sum: 0, avg: 0 }, + { sum: 2, avg: 1 }, + { sum: 6, avg: 2 }, + ]); + }); + + it('AsyncReducer.combineObj with halt', async () => { + const r = AsyncReducer.combineObj({ + sum: AsyncReducer.sum, + prod: AsyncReducer.product, + }); + + expect(await AsyncStream.empty().reduce(r)).toEqual({ sum: 0, prod: 1 }); + expect(await AsyncStream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ + { sum: 0, prod: 0 }, + { sum: 0, prod: 0 }, + { sum: 0, prod: 0 }, + ]); + expect(await AsyncStream.of(0, 2, 4).reduceStream(r).toArray()).toEqual([ + { sum: 0, prod: 0 }, + { sum: 2, prod: 0 }, + { sum: 6, prod: 0 }, + ]); + }); + + it('AsyncReducer.combineObj with stateToResult', async () => { + const r = AsyncReducer.combineObj({ + sum: AsyncReducer.sum.mapOutput(async (v) => v + 1), + prod: AsyncReducer.product, + }); + + expect(await AsyncStream.empty().reduce(r)).toEqual({ sum: 1, prod: 1 }); + expect(await AsyncStream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ + { sum: 1, prod: 0 }, + { sum: 1, prod: 0 }, + { sum: 1, prod: 0 }, + ]); + expect(await AsyncStream.of(0, 2, 4).reduceStream(r).toArray()).toEqual([ + { sum: 1, prod: 0 }, + { sum: 3, prod: 0 }, + { sum: 7, prod: 0 }, + ]); + }); + it('AsyncReducer.contains', async () => { expect(await AsyncStream.empty().reduce(AsyncReducer.contains(6))).toBe( false diff --git a/packages/common/test/reducer.test.ts b/packages/common/test/reducer.test.ts index 8829cfe7c..9dae93475 100644 --- a/packages/common/test/reducer.test.ts +++ b/packages/common/test/reducer.test.ts @@ -312,8 +312,8 @@ describe('Reducers', () => { ]); }); - it('Reducer.combine', () => { - const r = Reducer.combine( + it('Reducer.combineArr', () => { + const r = Reducer.combineArr( Reducer.sum, Reducer.average, Reducer.toArray() @@ -332,8 +332,8 @@ describe('Reducers', () => { ]); }); - it('Reducer.combine with halt', () => { - const r = Reducer.combine(Reducer.sum, Reducer.product); + it('Reducer.combineArr with halt', () => { + const r = Reducer.combineArr(Reducer.sum, Reducer.product); expect(Stream.empty().reduce(r)).toEqual([0, 1]); expect(Stream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ @@ -348,8 +348,8 @@ describe('Reducers', () => { ]); }); - it('Reducer.combine with stateToResult', () => { - const r = Reducer.combine( + it('Reducer.combineArr with stateToResult', () => { + const r = Reducer.combineArr( Reducer.sum.mapOutput((v) => v + 1), Reducer.product ); @@ -367,6 +367,61 @@ describe('Reducers', () => { ]); }); + it('Reducer.combineObj', () => { + const r = Reducer.combineObj({ + sum: Reducer.sum, + avg: Reducer.average, + arr: Reducer.toArray(), + }); + + expect(Stream.empty().reduce(r)).toEqual({ sum: 0, avg: 0, arr: [] }); + expect(Stream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ + { sum: 0, avg: 0, arr: [0] }, + { sum: 0, avg: 0, arr: [0, 0] }, + { sum: 0, avg: 0, arr: [0, 0, 0] }, + ]); + expect(Stream.of(0, 2, 4).reduceStream(r).toArray()).toEqual([ + { sum: 0, avg: 0, arr: [0] }, + { sum: 2, avg: 1, arr: [0, 2] }, + { sum: 6, avg: 2, arr: [0, 2, 4] }, + ]); + }); + + it('Reducer.combineObj with halt', () => { + const r = Reducer.combineObj({ sum: Reducer.sum, prod: Reducer.product }); + + expect(Stream.empty().reduce(r)).toEqual({ sum: 0, prod: 1 }); + expect(Stream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ + { sum: 0, prod: 0 }, + { sum: 0, prod: 0 }, + { sum: 0, prod: 0 }, + ]); + expect(Stream.of(0, 2, 4).reduceStream(r).toArray()).toEqual([ + { sum: 0, prod: 0 }, + { sum: 2, prod: 0 }, + { sum: 6, prod: 0 }, + ]); + }); + + it('Reducer.combineObj with stateToResult', () => { + const r = Reducer.combineObj({ + sum: Reducer.sum.mapOutput((v) => v + 1), + prod: Reducer.product, + }); + + expect(Stream.empty().reduce(r)).toEqual({ sum: 1, prod: 1 }); + expect(Stream.of(0, 0, 0).reduceStream(r).toArray()).toEqual([ + { sum: 1, prod: 0 }, + { sum: 1, prod: 0 }, + { sum: 1, prod: 0 }, + ]); + expect(Stream.of(0, 2, 4).reduceStream(r).toArray()).toEqual([ + { sum: 1, prod: 0 }, + { sum: 3, prod: 0 }, + { sum: 7, prod: 0 }, + ]); + }); + it('Reducer.contains', () => { expect(Stream.empty().reduce(Reducer.contains(6))).toBe(false); expect(Stream.of(1, 2, 3).reduce(Reducer.contains(6))).toBe(false);