Skip to content

Commit

Permalink
feat: implement reduceLazy (#239)
Browse files Browse the repository at this point in the history
  • Loading branch information
Phryxia committed Feb 17, 2024
1 parent 308cca9 commit 57e3e8c
Show file tree
Hide file tree
Showing 4 changed files with 219 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Lazy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import pipeLazy from "./pipeLazy";
import pluck from "./pluck";
import prepend from "./prepend";
import range from "./range";
import reduceLazy from "./reduceLazy";
import reject from "./reject";
import repeat from "./repeat";
import reverse from "./reverse";
Expand Down Expand Up @@ -70,6 +71,7 @@ export {
prepend,
range,
reject,
reduceLazy,
repeat,
reverse,
scan,
Expand Down
63 changes: 63 additions & 0 deletions src/Lazy/reduceLazy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import reduce from "../reduce";
import type IterableInfer from "../types/IterableInfer";
import type { AsyncReducer, SyncReducer } from "../types/Reducer";
import type ReturnValueType from "../types/ReturnValueType";

type InferCarrier<T> = T extends AsyncIterable<infer R>
? AsyncIterable<R>
: T extends Iterable<unknown>
? T
: never;

// DO NOT change the order of signatures prematurely.
// See `reduceLazy.test.ts` for the reason

function reduceLazy<T extends Iterable<unknown> | AsyncIterable<unknown>, Acc>(
f: SyncReducer<Acc, IterableInfer<T>> | AsyncReducer<Acc, IterableInfer<T>>,
seed: Acc,
): (iterable: InferCarrier<T>) => ReturnValueType<T, Acc>;

function reduceLazy<T extends Iterable<unknown> | AsyncIterable<unknown>>(
f:
| AsyncReducer<IterableInfer<T>, IterableInfer<T>>
| SyncReducer<IterableInfer<T>, IterableInfer<T>>,
seed?: IterableInfer<T>,
): (iterable: InferCarrier<T>) => ReturnValueType<T, IterableInfer<T>>;

function reduceLazy<T, Acc>(
f: SyncReducer<Acc, T>,
seed: Acc,
): <C extends Iterable<T> | AsyncIterable<T>>(
iterable: C,
) => ReturnValueType<C, Acc>;

function reduceLazy<T>(
f: SyncReducer<T, T>,
seed?: T,
): <C extends Iterable<T> | AsyncIterable<T>>(
iterable: C,
) => ReturnValueType<C>;

function reduceLazy<T, Acc>(
f: AsyncReducer<Acc, T>,
seed: Acc,
): (iterable: Iterable<T> | AsyncIterable<T>) => Promise<Acc>;

function reduceLazy<T>(
f: AsyncReducer<T, T>,
seed?: T,
): (iterable: Iterable<T> | AsyncIterable<T>) => Promise<T>;

function reduceLazy<T extends Iterable<unknown> | AsyncIterable<unknown>, Acc>(
f: SyncReducer<Acc, IterableInfer<T>>,
seed?: Acc,
) {
if (seed === undefined) {
return (iterable: Iterable<unknown> | AsyncIterable<unknown>) =>
reduce(f, iterable as any);
}
return (iterable: Iterable<unknown> | AsyncIterable<unknown>) =>
reduce(f, seed, iterable as any);
}

export default reduceLazy;
79 changes: 79 additions & 0 deletions test/Lazy/reduceLazy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { filter, map, pipe, range, reduceLazy, toAsync } from "../../src";

const addNumber = (a: number, b: number) => a + b;
const addNumberAsync = async (a: number, b: number) => a + b;

// these tests are almost identical to `reduce.spec.ts`
describe("reduceLazy", () => {
describe("sync", () => {
it("should return initial value when the given `iterable` is empty array", () => {
const reduce = reduceLazy((a, b) => a + b, "seed");
expect(reduce([])).toEqual("seed");
});

it("should be occured error when the given `iterable` is an empty array and initial value is absent", () => {
const reduce = reduceLazy((a: number, b: number) => a + b);
expect(() => reduce([])).toThrow();
});

it("should work given it is initial value", () => {
const reduce = reduceLazy(addNumber, 10);
expect(reduce(range(1, 6))).toEqual(25);
});

it("should use the first value as the initial value if initial value is absent", () => {
const reduce = reduceLazy(addNumber);
expect(reduce(range(1, 6))).toEqual(15);
});

it("should be able to be used as a curried function in the pipeline", () => {
const res = pipe(
["1", "2", "3", "4", "5"],
map((a) => Number(a)),
filter((a) => a % 2),
reduceLazy(addNumber),
);
expect(res).toEqual(1 + 3 + 5);
});
});

describe("async", () => {
it("should reduce `iterable` by the callback", async () => {
const reduce = reduceLazy(addNumber, 10);
expect(await reduce(toAsync(range(1, 6)))).toEqual(25);
});

it("should use the first value as the initial value if initial value is absent", async () => {
const reduce = reduceLazy(addNumber);
expect(await reduce(toAsync(range(1, 6)))).toEqual(15);
});

it("should reduce `AsyncIterable` by the callback with initial value", async () => {
const reduce = reduceLazy(addNumberAsync, 10);
expect(await reduce(toAsync(range(1, 6)))).toEqual(25);
});

it("should reduce 'AsyncIterable' by the callback", async () => {
const reduce = reduceLazy(addNumberAsync);
expect(await reduce(toAsync(range(1, 6)))).toEqual(15);
});

it("should be able to be used as a curried function in the pipeline", async () => {
const res1 = await pipe(
toAsync(["1", "2", "3", "4", "5"]),
map((a) => Number(a)),
filter((a) => a % 2),
reduceLazy(addNumber),
);
// async callback
const res2 = await pipe(
toAsync(["1", "2", "3", "4", "5"]),
map((a) => Number(a)),
filter((a) => a % 2),
reduceLazy(addNumberAsync),
);
expect(res1).toEqual(1 + 3 + 5);
expect(res2).toEqual(1 + 3 + 5);
});
});
});
75 changes: 75 additions & 0 deletions type-check/Lazy/reduceLazy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { pipe, toAsync } from "../../src";
import reduceLazy from "../../src/Lazy/reduceLazy";
import * as Test from "../../src/types/Test";

const { checks, check } = Test;

const homoSyncFn = reduceLazy((acc: number, value: number) => acc + value);
const homoSyncFnIterable = homoSyncFn([1, 2, 3]);
const homoSyncFnAsyncIterable = homoSyncFn(toAsync([1, 2, 3]));

const homoAsyncFn = reduceLazy(
async (acc: number, value: number) => acc + value,
);
const homoAsyncFnIterable = homoAsyncFn([1, 2, 3]);
const homoAsyncFnAsyncIterable = homoAsyncFn(toAsync([1, 2, 3]));

const heteroSyncFn = reduceLazy(
(acc: number, value: string) => acc + Number(value),
0,
);
const heteroSyncFnIterable = heteroSyncFn(["1", "2", "3"]);
const heteroSyncFnAsyncIterable = heteroSyncFn(toAsync(["1", "2", "3"]));

const heteroAsyncFn = reduceLazy(
async (acc: number, value: string) => acc + Number(value),
0,
);
const heteroAsyncFnIterable = heteroAsyncFn(["1", "2", "3"]);
const heteroAsyncFnAsyncIterable = heteroAsyncFn(toAsync(["1", "2", "3"]));

const homoPipe = pipe(
[1, 2, 3],
reduceLazy((acc, value) => acc + value),
);
const heteroPipe = pipe(
["1", "2", "3"],
reduceLazy((acc, value) => acc + Number(value), 0),
);
const homoPipeAsync = pipe(
[1, 2, 3],
toAsync,
reduceLazy((acc, value) => acc + value),
);
const heteroPipeAsync = pipe(
["1", "2", "3"],
toAsync,
reduceLazy((acc, value) => acc + Number(value), 0),
);
const homoPipeAsyncPromise = pipe(
[1, 2, 3],
toAsync,
reduceLazy(async (acc, value) => acc + value),
);
const heteroPipePromise = pipe(
["1", "2", "3"],
toAsync,
reduceLazy(async (acc, value) => acc + Number(value), 0),
);

checks([
check<typeof homoSyncFnIterable, number, Test.Pass>(),
check<typeof homoSyncFnAsyncIterable, Promise<number>, Test.Pass>(),
check<typeof homoAsyncFnIterable, Promise<number>, Test.Pass>(),
check<typeof homoAsyncFnAsyncIterable, Promise<number>, Test.Pass>(),
check<typeof heteroSyncFnIterable, number, Test.Pass>(),
check<typeof heteroSyncFnAsyncIterable, Promise<number>, Test.Pass>(),
check<typeof heteroAsyncFnIterable, Promise<number>, Test.Pass>(),
check<typeof heteroAsyncFnAsyncIterable, Promise<number>, Test.Pass>(),
check<typeof homoPipe, number, Test.Pass>(),
check<typeof heteroPipe, number, Test.Pass>(),
check<typeof homoPipeAsync, Promise<number>, Test.Pass>(),
check<typeof heteroPipeAsync, Promise<number>, Test.Pass>(),
check<typeof homoPipeAsyncPromise, Promise<number>, Test.Pass>(),
check<typeof heteroPipePromise, Promise<number>, Test.Pass>(),
]);

0 comments on commit 57e3e8c

Please sign in to comment.