diff --git a/async/chain.ts b/async/chain.ts new file mode 100644 index 0000000..cb1a99f --- /dev/null +++ b/async/chain.ts @@ -0,0 +1,24 @@ +/** + * Chains multiple iterables together. + * + * @param iterables The iterables to chain. + * @returns The chained iterable. + * @example + * + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { chain } from "@core/iterutil/async/chain"; + * + * const iter = chain([1, 2], [3, 4]); + * console.log(await toArray(iter)); // [1, 2, 3, 4] + * ``` + */ +export async function* chain( + ...iterables: Iterable[] | AsyncIterable[] +): AsyncIterable { + for await (const iterable of iterables) { + for await (const value of iterable) { + yield value; + } + } +} diff --git a/async/chain_test.ts b/async/chain_test.ts new file mode 100644 index 0000000..df7393b --- /dev/null +++ b/async/chain_test.ts @@ -0,0 +1,25 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { chain } from "./chain.ts"; + +Deno.test("chain", async (t) => { + await t.step("with async iterable", async () => { + const result = chain( + toAsyncIterable([1, 2]), + toAsyncIterable([3, 4]), + toAsyncIterable([5]), + ); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with iterable", async () => { + const result = chain([1, 2], [3, 4], [5]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); +}); diff --git a/async/chunked.ts b/async/chunked.ts new file mode 100644 index 0000000..e9cd288 --- /dev/null +++ b/async/chunked.ts @@ -0,0 +1,32 @@ +/** + * Chunks an iterable into arrays of a given size. + * + * @param iterable The iterable to chunk. + * @param size The size of each chunk. + * @returns The chunked iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { chunked } from "@core/iterutil/async/chunked"; + * + * const iter = chunked([1, 2, 3, 4, 5], 2); + * console.log(await toArray(iter)); // [[1, 2], [3, 4], [5]] + * ``` + */ +export async function* chunked( + iterable: Iterable | AsyncIterable, + size: number, +): AsyncIterable { + let chunk = []; + for await (const item of iterable) { + chunk.push(item); + if (chunk.length === size) { + yield chunk; + chunk = []; + } + } + if (chunk.length > 0) { + yield chunk; + } +} diff --git a/async/chunked_test.ts b/async/chunked_test.ts new file mode 100644 index 0000000..9b38a15 --- /dev/null +++ b/async/chunked_test.ts @@ -0,0 +1,67 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { chunked } from "./chunked.ts"; + +Deno.test("chunked", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("the length is divisible by the size", async () => { + const result = chunked(toAsyncIterable([1, 2, 3, 4, 5, 6]), 2); + const expected = [[1, 2], [3, 4], [5, 6]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the length is not divisible by the size", async () => { + const result = chunked(toAsyncIterable([1, 2, 3, 4, 5]), 2); + const expected = [[1, 2], [3, 4], [5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the length is equal to the size", async () => { + const result = chunked(toAsyncIterable([1, 2, 3, 4, 5]), 5); + const expected = [[1, 2, 3, 4, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the length is less than the size", async () => { + const result = chunked(toAsyncIterable([1, 2, 3, 4, 5]), 6); + const expected = [[1, 2, 3, 4, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("the length is divisible by the size", async () => { + const result = chunked([1, 2, 3, 4, 5, 6], 2); + const expected = [[1, 2], [3, 4], [5, 6]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the length is not divisible by the size", async () => { + const result = chunked([1, 2, 3, 4, 5], 2); + const expected = [[1, 2], [3, 4], [5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the length is equal to the size", async () => { + const result = chunked([1, 2, 3, 4, 5], 5); + const expected = [[1, 2, 3, 4, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the length is less than the size", async () => { + const result = chunked([1, 2, 3, 4, 5], 6); + const expected = [[1, 2, 3, 4, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/compact.ts b/async/compact.ts new file mode 100644 index 0000000..92ec059 --- /dev/null +++ b/async/compact.ts @@ -0,0 +1,24 @@ +/** + * Removes all nullish values from an iterable. + * + * @param iterable The iterable to compact. + * @returns The compacted iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { compact } from "@core/iterutil/async/compact"; + * + * const iter = compact([1, undefined, 2, null, 3]); + * console.log(await toArray(iter)); // [1, 2, 3] + * ``` + */ +export async function* compact( + iterable: Iterable | AsyncIterable, +): AsyncIterable> { + for await (const value of iterable) { + if (value != null) { + yield value; + } + } +} diff --git a/async/compact_test.ts b/async/compact_test.ts new file mode 100644 index 0000000..f0204f0 --- /dev/null +++ b/async/compact_test.ts @@ -0,0 +1,91 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { compact } from "./compact.ts"; + +Deno.test("compact", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("without undefined/null", async () => { + const result = compact(toAsyncIterable([1, 2, 3, 4, 5])); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with undefined", async () => { + const result = compact(toAsyncIterable([ + undefined, + 1, + 2, + undefined, + 3, + undefined, + 4, + 5, + undefined, + ])); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with null", async () => { + const result = compact( + toAsyncIterable([null, 1, 2, null, 3, null, 4, 5, null]), + ); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with undefined/null", async () => { + const result = compact( + toAsyncIterable([undefined, 1, 2, null, 3, undefined, 4, 5, null]), + ); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("without undefined/null", async () => { + const result = compact([1, 2, 3, 4, 5]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with undefined", async () => { + const result = compact([ + undefined, + 1, + 2, + undefined, + 3, + undefined, + 4, + 5, + undefined, + ]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with null", async () => { + const result = compact([null, 1, 2, null, 3, null, 4, 5, null]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with undefined/null", async () => { + const result = compact([undefined, 1, 2, null, 3, undefined, 4, 5, null]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/compress.ts b/async/compress.ts new file mode 100644 index 0000000..9d75e04 --- /dev/null +++ b/async/compress.ts @@ -0,0 +1,39 @@ +/** + * Compress an iterable by selecting elements using a selector iterable. + * + * @param iterable The iterable to compress. + * @param selectors The selectors to use. + * @returns The compressed iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { compress } from "@core/iterutil/async/compress"; + * + * const iter = compress([1, 2, 3, 4, 5], [true, false, true, false, true]); + * console.log(await toArray(iter)); // [1, 3, 5] + * ``` + */ +export async function* compress( + iterable: Iterable | AsyncIterable, + selectors: Iterable | AsyncIterable, +): AsyncIterable { + const it1 = Symbol.iterator in iterable + ? iterable[Symbol.iterator]() + : iterable[Symbol.asyncIterator](); + const it2 = Symbol.iterator in selectors + ? selectors[Symbol.iterator]() + : selectors[Symbol.asyncIterator](); + while (true) { + const [ + { done: done1, value: value1 }, + { done: done2, value: value2 }, + ] = await Promise.all([it1.next(), it2.next()]); + if (done1 || done2) { + break; + } + if (value2) { + yield value1; + } + } +} diff --git a/async/compress_test.ts b/async/compress_test.ts new file mode 100644 index 0000000..b368caa --- /dev/null +++ b/async/compress_test.ts @@ -0,0 +1,68 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { compress } from "./compress.ts"; + +Deno.test("compress", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("the iterable and the selectors are same length", async () => { + const result = compress( + toAsyncIterable([1, 2, 3, 4, 5]), + toAsyncIterable([true, false, true, false, true]), + ); + const expected = [1, 3, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the iterable is larger than the selectors", async () => { + const result = compress( + toAsyncIterable([1, 2, 3, 4, 5]), + toAsyncIterable([true, false, true]), + ); + const expected = [1, 3]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the iterable is smaller than the selector", async () => { + const result = compress( + toAsyncIterable([1, 2, 3]), + toAsyncIterable([true, false, true, false, true]), + ); + const expected = [1, 3]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("the iterable and the selectors are same length", async () => { + const result = compress([1, 2, 3, 4, 5], [ + true, + false, + true, + false, + true, + ]); + const expected = [1, 3, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the iterable is larger than the selectors", async () => { + const result = compress([1, 2, 3, 4, 5], [true, false, true]); + const expected = [1, 3]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("the iterable is smaller than the selector", async () => { + const result = compress([1, 2, 3], [true, false, true, false, true]); + const expected = [1, 3]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/cycle.ts b/async/cycle.ts new file mode 100644 index 0000000..66a4867 --- /dev/null +++ b/async/cycle.ts @@ -0,0 +1,30 @@ +/** + * Returns an infinite iterable that cycles through the given iterable. + * + * @param iterable The iterable to cycle. + * @returns The cycled iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { cycle } from "@core/iterutil/async/cycle"; + * import { take } from "@core/iterutil/async/take"; + * + * const iter = cycle([1, 2, 3]); + * console.log(await toArray(take(iter, 5))); // [1, 2, 3, 1, 2] + * ``` + */ +export async function* cycle( + iterable: Iterable | AsyncIterable, +): AsyncIterable { + const array: T[] = []; + for await (const item of iterable) { + array.push(item); + } + if (array.length === 0) { + return; + } + while (true) { + yield* array; + } +} diff --git a/async/cycle_test.ts b/async/cycle_test.ts new file mode 100644 index 0000000..069deab --- /dev/null +++ b/async/cycle_test.ts @@ -0,0 +1,54 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { take } from "./take.ts"; +import { cycle } from "./cycle.ts"; + +Deno.test("cycle", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = cycle(toAsyncIterable([0, 1, 2])); + const expected = [0, 1, 2, 0, 1]; + assertEquals(await toArray(take(result, 5)), expected); + assertType>>(true); + }); + + await t.step("with single value iterable", async () => { + const result = cycle(toAsyncIterable([0])); + const expected = [0, 0, 0, 0, 0]; + assertEquals(await toArray(take(result, 5)), expected); + assertType>>(true); + }); + + await t.step("with empty iterable", async () => { + const result = cycle(toAsyncIterable([] as number[])); + const expected: number[] = []; + assertEquals(await toArray(take(result, 5)), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = cycle([0, 1, 2]); + const expected = [0, 1, 2, 0, 1]; + assertEquals(await toArray(take(result, 5)), expected); + assertType>>(true); + }); + + await t.step("with single value iterable", async () => { + const result = cycle([0]); + const expected = [0, 0, 0, 0, 0]; + assertEquals(await toArray(take(result, 5)), expected); + assertType>>(true); + }); + + await t.step("with empty iterable", async () => { + const result = cycle([] as number[]); + const expected: number[] = []; + assertEquals(await toArray(take(result, 5)), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/drop.ts b/async/drop.ts new file mode 100644 index 0000000..e979d27 --- /dev/null +++ b/async/drop.ts @@ -0,0 +1,36 @@ +/** + * Drops the first `limit` items from the iterable. + * + * It throws an error if `limit` is less than 0. + * + * @param iterable The iterable to drop items from. + * @param limit The number of items to drop. + * @returns The iterable with the first `limit` items dropped. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { drop } from "@core/iterutil/async/drop"; + * + * const iter = drop([1, 2, 3, 4, 5], 2); + * console.log(await toArray(iter)); // [3, 4, 5] + * ``` + */ +export function drop( + iterable: Iterable | AsyncIterable, + limit: number, +): AsyncIterable { + if (limit < 0) { + throw new Error( + `limit argument must be greater than or equal to 0, but got ${limit}.`, + ); + } + return async function* () { + let i = 0; + for await (const item of iterable) { + if (i++ >= limit) { + yield item; + } + } + }(); +} diff --git a/async/drop_test.ts b/async/drop_test.ts new file mode 100644 index 0000000..0223e2d --- /dev/null +++ b/async/drop_test.ts @@ -0,0 +1,59 @@ +import { assertEquals, assertThrows } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { drop } from "./drop.ts"; + +Deno.test("drop", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with positive limit", async () => { + const result = drop(toAsyncIterable([0, 1, 2, 3, 4]), 2); + const expected = [2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with negative limit", () => { + assertThrows( + () => { + drop([0, 1, 2, 3, 4], -2); + }, + Error, + "limit argument must be greater than or equal to 0, but got -2.", + ); + }); + + await t.step("with 0 limit", async () => { + const result = drop(toAsyncIterable([0, 1, 2, 3, 4]), 0); + const expected = [0, 1, 2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with positive limit", async () => { + const result = drop([0, 1, 2, 3, 4], 2); + const expected = [2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with negative limit", () => { + assertThrows( + () => { + drop([0, 1, 2, 3, 4], -2); + }, + Error, + "limit argument must be greater than or equal to 0, but got -2.", + ); + }); + + await t.step("with 0 limit", async () => { + const result = drop([0, 1, 2, 3, 4], 0); + const expected = [0, 1, 2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/drop_while.ts b/async/drop_while.ts new file mode 100644 index 0000000..efc04c0 --- /dev/null +++ b/async/drop_while.ts @@ -0,0 +1,32 @@ +/** + * Drops elements from the iterable while the predicate returns true. + * + * The first element that does not match the predicate is included in the output. + * If the predicate never returns false, the output will be an empty iterable. + * + * @param iterable The iterable to drop elements from. + * @param fn The predicate function to drop elements with. + * @returns The iterable with elements dropped while the predicate returns true. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { dropWhile } from "@core/iterutil/async/drop-while"; + * + * const iter = dropWhile([1, 2, 3, 4, 5], (x) => x < 3); + * console.log(await toArray(iter)); // [3, 4, 5] + * ``` + */ +export async function* dropWhile( + iterable: Iterable | AsyncIterable, + fn: (value: T) => boolean | Promise, +): AsyncIterable { + let dropping = true; + for await (const value of iterable) { + if (dropping && await fn(value)) { + continue; + } + dropping = false; + yield value; + } +} diff --git a/async/drop_while_test.ts b/async/drop_while_test.ts new file mode 100644 index 0000000..b16b19a --- /dev/null +++ b/async/drop_while_test.ts @@ -0,0 +1,70 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { dropWhile } from "./drop_while.ts"; + +Deno.test("dropWhile", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with some true", async () => { + const result = dropWhile(toAsyncIterable([0, 1, 2, 3, 4]), (v) => v < 2); + const expected = [2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with some promise true", async () => { + const result = dropWhile( + toAsyncIterable([0, 1, 2, 3, 4]), + (v) => Promise.resolve(v < 2), + ); + const expected = [2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all true", async () => { + const result = dropWhile(toAsyncIterable([0, 1, 2, 3, 4]), () => true); + const expected: number[] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all false", async () => { + const result = dropWhile(toAsyncIterable([0, 1, 2, 3, 4]), () => false); + const expected = [0, 1, 2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with some true", async () => { + const result = dropWhile([0, 1, 2, 3, 4], (v) => v < 2); + const expected = [2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with some promise true", async () => { + const result = dropWhile([0, 1, 2, 3, 4], (v) => Promise.resolve(v < 2)); + const expected = [2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all true", async () => { + const result = dropWhile([0, 1, 2, 3, 4], () => true); + const expected: number[] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all false", async () => { + const result = dropWhile([0, 1, 2, 3, 4], () => false); + const expected = [0, 1, 2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/enumerate.ts b/async/enumerate.ts new file mode 100644 index 0000000..c92e9ed --- /dev/null +++ b/async/enumerate.ts @@ -0,0 +1,27 @@ +/** + * Enumerate an iterable. + * + * @param iterable The iterable to enumerate. + * @param start The starting index. + * @returns An iterable of index-value pairs. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { enumerate } from "@core/iterutil/async/enumerate"; + * + * const iter = enumerate(["a", "b", "c"]); + * console.log(await toArray(iter)); // [[0, "a"], [1, "b"], [2, "c"]] + * ``` + */ +export async function* enumerate( + iterable: Iterable | AsyncIterable, + start: number = 0, + step: number = 1, +): AsyncIterable<[number, T]> { + let i = start; + for await (const value of iterable) { + yield [i, value]; + i += step; + } +} diff --git a/async/enumerate_test.ts b/async/enumerate_test.ts new file mode 100644 index 0000000..dd8428e --- /dev/null +++ b/async/enumerate_test.ts @@ -0,0 +1,53 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { enumerate } from "./enumerate.ts"; + +Deno.test("enumerate", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("default", async () => { + const result = enumerate(toAsyncIterable([0, 1, 2])); + const expected = [[0, 0], [1, 1], [2, 2]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with start", async () => { + const result = enumerate(toAsyncIterable([0, 1, 2]), 1); + const expected = [[1, 0], [2, 1], [3, 2]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with start/step", async () => { + const result = enumerate(toAsyncIterable([0, 1, 2]), 1, 2); + const expected = [[1, 0], [3, 1], [5, 2]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("default", async () => { + const result = enumerate([0, 1, 2]); + const expected = [[0, 0], [1, 1], [2, 2]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with start", async () => { + const result = enumerate([0, 1, 2], 1); + const expected = [[1, 0], [2, 1], [3, 2]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with start/step", async () => { + const result = enumerate([0, 1, 2], 1, 2); + const expected = [[1, 0], [3, 1], [5, 2]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/every.ts b/async/every.ts new file mode 100644 index 0000000..7ffdd86 --- /dev/null +++ b/async/every.ts @@ -0,0 +1,27 @@ +/** + * Returns true if every element in the iterable satisfies the provided testing function. + * + * @param iterable The iterable to test. + * @param fn The function to test with. + * @returns True if every element in the iterable satisfies the provided testing function, otherwise false. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { every } from "@core/iterutil/async/every"; + * + * console.log(await every([1, 2, 3], (value) => value > 0)); // true + * console.log(await every([1, 2, 3], (value) => value > 1)); // false + * ``` + */ +export async function every( + iterable: Iterable | AsyncIterable, + fn: (value: T) => boolean | Promise, +): Promise { + for await (const value of iterable) { + if (!await fn(value)) { + return false; + } + } + return true; +} diff --git a/async/every_test.ts b/async/every_test.ts new file mode 100644 index 0000000..5fe0b32 --- /dev/null +++ b/async/every_test.ts @@ -0,0 +1,84 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { every } from "./every.ts"; + +Deno.test("every", async (t) => { + await t.step("with iterable", async (t) => { + await t.step("true", async () => { + const result = await every( + toAsyncIterable([1, 2, 3, 4, 5]), + (v) => v > 0, + ); + const expected = true; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("false", async () => { + const result = await every( + toAsyncIterable([1, 2, 3, 4, 5]), + (v) => v > 1, + ); + const expected = false; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("promise true", async () => { + const result = await every( + toAsyncIterable([1, 2, 3, 4, 5]), + (v) => Promise.resolve(v > 0), + ); + const expected = true; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("promise false", async () => { + const result = await every( + toAsyncIterable([1, 2, 3, 4, 5]), + (v) => Promise.resolve(v > 1), + ); + const expected = false; + assertEquals(result, expected); + assertType>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("true", async () => { + const result = await every([1, 2, 3, 4, 5], (v) => v > 0); + const expected = true; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("false", async () => { + const result = await every([1, 2, 3, 4, 5], (v) => v > 1); + const expected = false; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("promise true", async () => { + const result = await every( + [1, 2, 3, 4, 5], + (v) => Promise.resolve(v > 0), + ); + const expected = true; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("promise false", async () => { + const result = await every( + [1, 2, 3, 4, 5], + (v) => Promise.resolve(v > 1), + ); + const expected = false; + assertEquals(result, expected); + assertType>(true); + }); + }); +}); diff --git a/async/filter.ts b/async/filter.ts new file mode 100644 index 0000000..9ad9b58 --- /dev/null +++ b/async/filter.ts @@ -0,0 +1,27 @@ +/** + * Filters an iterable based on a function. + * + * @params iterable The iterable to filter. + * @params fn The function to filter with. + * @returns The filtered iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { filter } from "@core/iterutil/async/filter"; + * + * const iter = filter([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * console.log(await toArray(iter)); // [2, 4] + * ``` + */ +export async function* filter( + iterable: Iterable | AsyncIterable, + fn: (value: T, index: number) => boolean | Promise, +): AsyncIterable { + let index = 0; + for await (const value of iterable) { + if (await fn(value, index++)) { + yield value; + } + } +} diff --git a/async/filter_test.ts b/async/filter_test.ts new file mode 100644 index 0000000..647cab7 --- /dev/null +++ b/async/filter_test.ts @@ -0,0 +1,77 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { filter } from "./filter.ts"; + +Deno.test("filter", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("without promise", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = filter( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return value % 2 === 0; + }, + ); + const expected = [2, 4]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("with promise", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = filter( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value % 2 === 0); + }, + ); + const expected = [2, 4]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("without promise", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = filter([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return value % 2 === 0; + }); + const expected = [2, 4]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("with promise", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = filter([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value % 2 === 0); + }); + const expected = [2, 4]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + }); +}); diff --git a/async/find.ts b/async/find.ts new file mode 100644 index 0000000..03b0426 --- /dev/null +++ b/async/find.ts @@ -0,0 +1,28 @@ +/** + * Returns the first element in the iterable that satisfies the provided + * testing function. Otherwise, undefined is returned. + * + * @param iterable The iterable to search. + * @param fn The function to test with. + * @returns The first element that satisfies the provided testing function. + * + * @example + * ```ts + * import { find } from "@core/iterutil/async/find"; + * + * const value = await find([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * console.log(value); // 2 + * ``` + */ +export async function find( + iterable: Iterable | AsyncIterable, + fn: (value: T, index: number) => boolean | Promise, +): Promise { + let index = 0; + for await (const value of iterable) { + if (await fn(value, index++)) { + return value; + } + } + return undefined; +} diff --git a/async/find_test.ts b/async/find_test.ts new file mode 100644 index 0000000..d741de8 --- /dev/null +++ b/async/find_test.ts @@ -0,0 +1,90 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { find } from "./find.ts"; + +Deno.test("find", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("found", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = await find( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return value % 2 === 0; + }, + ); + const expected = 2; + assertEquals(result, expected); + assertEquals(values, [1, 2]); + assertEquals(indices, [0, 1]); + assertType>(true); + }); + + await t.step("promise found", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = await find( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value % 2 === 0); + }, + ); + const expected = 2; + assertEquals(result, expected); + assertEquals(values, [1, 2]); + assertEquals(indices, [0, 1]); + assertType>(true); + }); + + await t.step("not found", async () => { + const result = await find(toAsyncIterable([1, 2, 3, 4, 5]), () => false); + const expected = undefined; + assertEquals(result, expected); + assertType>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("found", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = await find([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return value % 2 === 0; + }); + const expected = 2; + assertEquals(result, expected); + assertEquals(values, [1, 2]); + assertEquals(indices, [0, 1]); + assertType>(true); + }); + + await t.step("promise found", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = await find([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value % 2 === 0); + }); + const expected = 2; + assertEquals(result, expected); + assertEquals(values, [1, 2]); + assertEquals(indices, [0, 1]); + assertType>(true); + }); + + await t.step("not found", async () => { + const result = await find([1, 2, 3, 4, 5], () => false); + const expected = undefined; + assertEquals(result, expected); + assertType>(true); + }); + }); +}); diff --git a/async/first.ts b/async/first.ts new file mode 100644 index 0000000..831c8aa --- /dev/null +++ b/async/first.ts @@ -0,0 +1,23 @@ +/** + * Returns the first element of an iterable. + * If the iterable is empty, returns `undefined`. + * + * @param iterable The iterable to get the first element from. + * @returns The first element of the iterable, or `undefined` if the iterable is empty. + * + * @example + * ```ts + * import { first } from "@core/iterutil/async/first"; + * + * const value = await first([1, 2, 3]); + * console.log(value); // 1 + * ``` + */ +export async function first( + iterable: Iterable | AsyncIterable, +): Promise { + for await (const value of iterable) { + return value; + } + return undefined; +} diff --git a/async/first_test.ts b/async/first_test.ts new file mode 100644 index 0000000..6163f9e --- /dev/null +++ b/async/first_test.ts @@ -0,0 +1,38 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { first } from "./first.ts"; + +Deno.test("first", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = await first(toAsyncIterable([1, 2, 3, 4, 5])); + const expected = 1; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("with empty iterable", async () => { + const result = await first(toAsyncIterable([] as number[])); + const expected = undefined; + assertEquals(result, expected); + assertType>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = await first([1, 2, 3, 4, 5]); + const expected = 1; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("with empty iterable", async () => { + const result = await first([] as number[]); + const expected = undefined; + assertEquals(result, expected); + assertType>(true); + }); + }); +}); diff --git a/async/flat_map.ts b/async/flat_map.ts new file mode 100644 index 0000000..15c3e0b --- /dev/null +++ b/async/flat_map.ts @@ -0,0 +1,30 @@ +/** + * Maps each value in an iterable to an iterable, then flattens the result. + * + * @param iterable The iterable to flat map. + * @param fn The function to map with. + * @returns The flat mapped iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { flatMap } from "@core/iterutil/async/flat-map"; + * + * const iter = flatMap([1, 2, 3], (value) => [value, value]); + * console.log(await toArray(iter)); // [1, 1, 2, 2, 3, 3] + * ``` + */ +export async function* flatMap( + iterable: Iterable | AsyncIterable, + fn: ( + value: T, + index: number, + ) => Iterable | AsyncIterable | Promise>, +): AsyncIterable { + let index = 0; + for await (const value of iterable) { + for await (const item of await fn(value, index++)) { + yield item; + } + } +} diff --git a/async/flat_map_test.ts b/async/flat_map_test.ts new file mode 100644 index 0000000..9ad312d --- /dev/null +++ b/async/flat_map_test.ts @@ -0,0 +1,124 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { flatMap } from "./flat_map.ts"; + +Deno.test("flatMap", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("single nest", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = flatMap( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return [value, value]; + }, + ); + const expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("single async iterable nest", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = flatMap( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return toAsyncIterable([value, value]); + }, + ); + const expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("single promise nest", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = flatMap( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve([value, value]); + }, + ); + const expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("multi nest", async () => { + const result = flatMap(toAsyncIterable([1, 2, 3, 4, 5]), (v) => [[v, v]]); + const expected = [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("single nest", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = flatMap([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return [value, value]; + }); + const expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("single async iterable nest", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = flatMap([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return toAsyncIterable([value, value]); + }); + const expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("single promise nest", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = flatMap([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve([value, value]); + }); + const expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("multi nest", async () => { + const result = flatMap([1, 2, 3, 4, 5], (v) => [[v, v]]); + const expected = [[1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/flatten.ts b/async/flatten.ts new file mode 100644 index 0000000..d33fe0e --- /dev/null +++ b/async/flatten.ts @@ -0,0 +1,26 @@ +/** + * Flattens an iterable of iterables into a single iterable. + * + * @param iterable The iterable to flatten. + * @returns The flattened iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { flatten } from "@core/iterutil/async/flatten"; + * + * const iter = flatten([[1, 2], [3, 4], [5]]); + * console.log(await toArray(iter)); // [1, 2, 3, 4, 5] + * ``` + */ +export async function* flatten( + iterable: + | Iterable | AsyncIterable | Promise>> + | AsyncIterable | AsyncIterable | Promise>>, +): AsyncIterable { + for await (const innerIterable of iterable) { + for await (const value of innerIterable) { + yield value; + } + } +} diff --git a/async/flatten_test.ts b/async/flatten_test.ts new file mode 100644 index 0000000..376b0ef --- /dev/null +++ b/async/flatten_test.ts @@ -0,0 +1,45 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { flatten } from "./flatten.ts"; + +Deno.test("flatten", async (t) => { + await t.step("with iterable", async (t) => { + await t.step("single nest", async () => { + const result = flatten([[1, 2], [3, 4], [5]]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("single async iterable nest", async () => { + const result = flatten([ + toAsyncIterable([1, 2]), + toAsyncIterable([3, 4]), + toAsyncIterable([5]), + ]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("single promise nest", async () => { + const result = flatten([ + Promise.resolve([1, 2]), + Promise.resolve([3, 4]), + Promise.resolve([5]), + ]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("multi nest", async () => { + const result = flatten([[[1, 2], [3, 4]], [[5]]]); + const expected = [[1, 2], [3, 4], [5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/for_each.ts b/async/for_each.ts new file mode 100644 index 0000000..fb78a2c --- /dev/null +++ b/async/for_each.ts @@ -0,0 +1,23 @@ +/** + * Calls a function for each value in an iterable. + * + * @param iterable The iterable to iterate over. + * @param fn The function to call for each value. + * + * @example + * ```ts + * import { forEach } from "@core/iterutil/async/for-each"; + * await forEach([1, 2, 3], console.log); + * // 1 + * // 2 + * // 3 + */ +export async function forEach( + iterable: Iterable | AsyncIterable, + fn: (value: T, index: number) => void | Promise, +): Promise { + let index = 0; + for await (const value of iterable) { + await fn(value, index++); + } +} diff --git a/async/for_each_test.ts b/async/for_each_test.ts new file mode 100644 index 0000000..a8402ab --- /dev/null +++ b/async/for_each_test.ts @@ -0,0 +1,51 @@ +import { assertEquals } from "@std/assert"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { forEach } from "./for_each.ts"; + +Deno.test("forEach", async (t) => { + await t.step("with async iterable", async () => { + const values: number[] = []; + const indices: number[] = []; + await forEach(toAsyncIterable([1, 2, 3, 4, 5]), (value, index) => { + values.push(value); + indices.push(index); + }); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + }); + + await t.step("with async iterable (promise)", async () => { + const values: number[] = []; + const indices: number[] = []; + await forEach(toAsyncIterable([1, 2, 3, 4, 5]), (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(); + }); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + }); + + await t.step("with iterable", async () => { + const values: number[] = []; + const indices: number[] = []; + await forEach([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + }); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + }); + + await t.step("with iterable (promise)", async () => { + const values: number[] = []; + const indices: number[] = []; + await forEach([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(); + }); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + }); +}); diff --git a/async/iter.ts b/async/iter.ts new file mode 100644 index 0000000..633363b --- /dev/null +++ b/async/iter.ts @@ -0,0 +1,34 @@ +/** + * Convert an iterable to an iterator. + * + * @param iterable The iterable to convert. + * @returns The iterator. + * + * @example + * ```ts + * import { iter } from "@core/iterutil/async/iter"; + * + * const it = iter([1, 2, 3, 4, 5]); + * console.log(await it.next()); // { value: 1, done: false } + * console.log(await it.next()); // { value: 2, done: false } + * + * for await (const value of it) { + * console.log(value); + * } + * // 3 + * // 4 + * // 5 + * ``` + */ +export function iter( + iterable: Iterable | AsyncIterable, +): AsyncIterableIterator { + const inner = Symbol.iterator in iterable + ? iterable[Symbol.iterator]() + : iterable[Symbol.asyncIterator](); + const it = { + next: async () => await inner.next(), + [Symbol.asyncIterator]: () => it, + }; + return it; +} diff --git a/async/iter_test.ts b/async/iter_test.ts new file mode 100644 index 0000000..b995602 --- /dev/null +++ b/async/iter_test.ts @@ -0,0 +1,23 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { iter } from "./iter.ts"; + +Deno.test("iter", async (t) => { + await t.step("with async iterable", async () => { + const it = iter(toAsyncIterable([0, 1, 2, 3, 4, 5])); + assertEquals(await it.next(), { done: false, value: 0 }); + assertEquals(await it.next(), { done: false, value: 1 }); + assertEquals(await toArray(it), [2, 3, 4, 5]); + assertType>>(true); + }); + + await t.step("with iterable", async () => { + const it = iter([0, 1, 2, 3, 4, 5]); + assertEquals(await it.next(), { done: false, value: 0 }); + assertEquals(await it.next(), { done: false, value: 1 }); + assertEquals(await toArray(it), [2, 3, 4, 5]); + assertType>>(true); + }); +}); diff --git a/async/last.ts b/async/last.ts new file mode 100644 index 0000000..4de6bb5 --- /dev/null +++ b/async/last.ts @@ -0,0 +1,23 @@ +/** + * Returns the last element of an iterable. + * + * @param iterable The iterable to get the last element of. + * @returns The last element of the iterable, or `undefined` if the iterable is empty. + * + * @example + * ```ts + * import { last } from "@core/iterutil/async/last"; + * + * console.log(await last([1, 2, 3])); // 3 + * console.log(await last([])); // undefined + * ``` + */ +export async function last( + iterable: Iterable | AsyncIterable, +): Promise { + let lastValue: T | undefined = undefined; + for await (const value of iterable) { + lastValue = value; + } + return lastValue; +} diff --git a/async/last_test.ts b/async/last_test.ts new file mode 100644 index 0000000..0f32e5b --- /dev/null +++ b/async/last_test.ts @@ -0,0 +1,38 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { last } from "./last.ts"; + +Deno.test("last", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = await last(toAsyncIterable([1, 2, 3, 4, 5])); + const expected = 5; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("with empty iterable", async () => { + const result = await last(toAsyncIterable([] as number[])); + const expected = undefined; + assertEquals(result, expected); + assertType>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = await last([1, 2, 3, 4, 5]); + const expected = 5; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("with empty iterable", async () => { + const result = await last([] as number[]); + const expected = undefined; + assertEquals(result, expected); + assertType>(true); + }); + }); +}); diff --git a/async/map.ts b/async/map.ts new file mode 100644 index 0000000..fc1f2a5 --- /dev/null +++ b/async/map.ts @@ -0,0 +1,25 @@ +/** + * Maps an iterable with a function. + * + * @param iterable The iterable to map. + * @param fn The function to map with. + * @returns The mapped iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { map } from "@core/iterutil/async/map"; + * + * const iter = map([1, 2, 3], (value) => value * 2); + * console.log(await toArray(iter)); // [2, 4, 6] + * ``` + */ +export async function* map( + iterable: Iterable | AsyncIterable, + fn: (value: T, index: number) => U | Promise, +): AsyncIterable { + let index = 0; + for await (const value of iterable) { + yield await fn(value, index++); + } +} diff --git a/async/map_test.ts b/async/map_test.ts new file mode 100644 index 0000000..12a8b02 --- /dev/null +++ b/async/map_test.ts @@ -0,0 +1,67 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { map } from "./map.ts"; + +Deno.test("map", async (t) => { + await t.step("with async iterable", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = map(toAsyncIterable([1, 2, 3, 4, 5]), (value, index) => { + values.push(value); + indices.push(index); + return value * 2; + }); + const expected = [2, 4, 6, 8, 10]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("with iterable (promise)", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = map(toAsyncIterable([1, 2, 3, 4, 5]), (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value * 2); + }); + const expected = [2, 4, 6, 8, 10]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("with iterable", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = map([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return value * 2; + }); + const expected = [2, 4, 6, 8, 10]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); + + await t.step("with iterable (promise)", async () => { + const values: number[] = []; + const indices: number[] = []; + const result = map([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value * 2); + }); + const expected = [2, 4, 6, 8, 10]; + assertEquals(await toArray(result), expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>>(true); + }); +}); diff --git a/async/mod.ts b/async/mod.ts new file mode 100644 index 0000000..d99226c --- /dev/null +++ b/async/mod.ts @@ -0,0 +1,28 @@ +export * from "./chain.ts"; +export * from "./chunked.ts"; +export * from "./compact.ts"; +export * from "./compress.ts"; +export * from "./cycle.ts"; +export * from "./drop.ts"; +export * from "./drop_while.ts"; +export * from "./enumerate.ts"; +export * from "./every.ts"; +export * from "./filter.ts"; +export * from "./find.ts"; +export * from "./first.ts"; +export * from "./flat_map.ts"; +export * from "./flatten.ts"; +export * from "./for_each.ts"; +export * from "./iter.ts"; +export * from "./last.ts"; +export * from "./map.ts"; +export * from "./pairwise.ts"; +export * from "./partition.ts"; +export * from "./reduce.ts"; +export * from "./some.ts"; +export * from "./take.ts"; +export * from "./take_while.ts"; +export * from "./to_array.ts"; +export * from "./to_async_iterable.ts"; +export * from "./uniq.ts"; +export * from "./zip.ts"; diff --git a/async/mod_test.ts b/async/mod_test.ts new file mode 100644 index 0000000..3409c34 --- /dev/null +++ b/async/mod_test.ts @@ -0,0 +1,59 @@ +import { assertArrayIncludes } from "@std/assert"; +import { basename, globToRegExp, join } from "@std/path"; +import { ensure, is } from "@core/unknownutil"; +import { parse } from "@std/jsonc"; + +const excludes = [ + "mod.ts", + "*_test.ts", + "*_bench.ts", +]; + +Deno.test("mod.ts must exports all exports in public modules", async () => { + const modExports = await listModExports("./mod.ts"); + const pubExports = []; + for await (const name of iterPublicModules(".")) { + pubExports.push(...await listModExports(`./${name}.ts`)); + } + assertArrayIncludes(modExports, pubExports); +}); + +Deno.test("JSR exports must have all exports in mod.ts", async () => { + const jsrExportEntries = await listJsrExportEntries(); + const modExportEntries: [string, string][] = []; + for await (const name of iterPublicModules(".")) { + modExportEntries.push([ + `./async/${name.replaceAll("_", "-")}`, + `./async/${name}.ts`, + ]); + } + assertArrayIncludes(jsrExportEntries, modExportEntries); +}); + +async function* iterPublicModules(relpath: string): AsyncIterable { + const patterns = excludes.map((p) => globToRegExp(p)); + const root = join(import.meta.dirname!, relpath); + for await (const entry of Deno.readDir(root)) { + if (!entry.isFile || !entry.name.endsWith(".ts")) continue; + if (patterns.some((p) => p.test(entry.name))) continue; + yield basename(entry.name, ".ts"); + } +} + +async function listModExports(path: string): Promise { + const mod = await import(import.meta.resolve(path)); + return [...Object.keys(mod)]; +} + +async function listJsrExportEntries(): Promise<[string, string][]> { + const text = await Deno.readTextFile( + new URL(import.meta.resolve("../deno.jsonc")), + ); + const json = ensure( + parse(text), + is.ObjectOf({ + exports: is.RecordOf(is.String, is.String), + }), + ); + return Object.entries(json.exports); +} diff --git a/async/pairwise.ts b/async/pairwise.ts new file mode 100644 index 0000000..85499d3 --- /dev/null +++ b/async/pairwise.ts @@ -0,0 +1,34 @@ +/** + * Returns an iterable that pairs adjacent elements from the input iterable. + * + * When the input iterable has a finite number of items `n`, the output iterable will have `n - 1` items. + * + * @param iterable The iterable to pair elements from. + * @returns The paired iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { pairwise } from "@core/iterutil/async/pairwise"; + * + * const iter = pairwise([1, 2, 3, 4, 5]); + * console.log(await toArray(iter)); // [[1, 2], [2, 3], [3, 4], [4, 5]] + * ``` + */ +export async function* pairwise( + iterable: Iterable | AsyncIterable, +): AsyncIterable<[T, T]> { + const it = Symbol.iterator in iterable + ? iterable[Symbol.iterator]() + : iterable[Symbol.asyncIterator](); + let result = await it.next(); + while (!result.done) { + const first = result.value; + result = await it.next(); + if (result.done) { + break; + } + const second = result.value; + yield [first, second]; + } +} diff --git a/async/pairwise_test.ts b/async/pairwise_test.ts new file mode 100644 index 0000000..892d28c --- /dev/null +++ b/async/pairwise_test.ts @@ -0,0 +1,53 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { pairwise } from "./pairwise.ts"; + +Deno.test("pairwise", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = pairwise(toAsyncIterable([1, 2, 3, 4, 5])); + const expected = [[1, 2], [2, 3], [3, 4], [4, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with single value iterable", async () => { + const result = pairwise(toAsyncIterable([1])); + const expected: number[][] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with empty iterable", async () => { + const result = pairwise(toAsyncIterable([] as number[])); + const expected: number[][] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const result = pairwise([1, 2, 3, 4, 5]); + const expected = [[1, 2], [2, 3], [3, 4], [4, 5]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with single value iterable", async () => { + const result = pairwise([1]); + const expected: number[][] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with empty iterable", async () => { + const result = pairwise([] as number[]); + const expected: number[][] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/partition.ts b/async/partition.ts new file mode 100644 index 0000000..35ea1b4 --- /dev/null +++ b/async/partition.ts @@ -0,0 +1,31 @@ +/** + * Partitions an iterable into two arrays based on a selector function. + * + * @param iterable The iterable to partition. + * @param selector The function to partition with. + * @returns The partitioned arrays. + * + * @example + * ```ts + * import { partition } from "@core/iterutil/partition"; + * + * const [even, odd] = await partition([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * console.log(even); // [2, 4] + * console.log(odd); // [1, 3, 5] + * ``` + */ +export async function partition( + iterable: Iterable | AsyncIterable, + selector: (value: T, index: number) => boolean | Promise, +): Promise<[T[], T[]]> { + let index = 0; + const [a, b]: [T[], T[]] = [[], []]; + for await (const value of iterable) { + if (await selector(value, index++)) { + a.push(value); + } else { + b.push(value); + } + } + return [a, b]; +} diff --git a/async/partition_test.ts b/async/partition_test.ts new file mode 100644 index 0000000..9d13ff2 --- /dev/null +++ b/async/partition_test.ts @@ -0,0 +1,99 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { partition } from "./partition.ts"; + +Deno.test("partition", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const values: number[] = []; + const indices: number[] = []; + const [left, right] = await partition( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return value % 2 === 0; + }, + ); + assertEquals(left, [2, 4]); + assertEquals(right, [1, 3, 5]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + assertType>(true); + }); + + await t.step("with non empty iterable (promise)", async () => { + const values: number[] = []; + const indices: number[] = []; + const [left, right] = await partition( + toAsyncIterable([1, 2, 3, 4, 5]), + (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value % 2 === 0); + }, + ); + assertEquals(left, [2, 4]); + assertEquals(right, [1, 3, 5]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + assertType>(true); + }); + + await t.step("with empty iterable", async () => { + const [left, right] = await partition( + toAsyncIterable([] as number[]), + (v) => v % 2 === 0, + ); + assertEquals(left, []); + assertEquals(right, []); + assertType>(true); + assertType>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with non empty iterable", async () => { + const values: number[] = []; + const indices: number[] = []; + const [left, right] = await partition([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return value % 2 === 0; + }); + assertEquals(left, [2, 4]); + assertEquals(right, [1, 3, 5]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + assertType>(true); + }); + + await t.step("with non empty iterable (promise)", async () => { + const values: number[] = []; + const indices: number[] = []; + const [left, right] = await partition([1, 2, 3, 4, 5], (value, index) => { + values.push(value); + indices.push(index); + return Promise.resolve(value % 2 === 0); + }); + assertEquals(left, [2, 4]); + assertEquals(right, [1, 3, 5]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + assertType>(true); + }); + + await t.step("with empty iterable", async () => { + const [left, right] = await partition([] as number[], (v) => v % 2 === 0); + assertEquals(left, []); + assertEquals(right, []); + assertType>(true); + assertType>(true); + }); + }); +}); diff --git a/async/reduce.ts b/async/reduce.ts new file mode 100644 index 0000000..86fc2d9 --- /dev/null +++ b/async/reduce.ts @@ -0,0 +1,69 @@ +/** + * Reduces an iterable into a single value. + * + * @param iterable The iterable to reduce. + * @param fn The function to reduce with. + * @returns The reduced value. + * + * @example + * ```ts + * import { reduce } from "@core/iterutil/async/reduce"; + * + * const sum = await reduce([1, 2, 3, 4, 5], (acc, value) => acc + value); + * console.log(sum); // 15 + * ``` + */ +export function reduce( + iterable: Iterable | AsyncIterable, + fn: (acc: T, value: T, index: number) => T | Promise, +): Promise; + +/** + * Reduces an iterable into a single value. + * + * @param iterable The iterable to reduce. + * @param fn The function to reduce with. + * @param initial The initial value to start reducing with. + * @returns The reduced value. + * + * @example + * ```ts + * import { reduce } from "@core/iterutil/async/reduce"; + * + * const joined = await reduce([1, 2, 3, 4, 5], (acc, value) => acc + value.toString(), ""); + * console.log(joined); // 12345 + * ``` + */ +export function reduce( + iterable: Iterable | AsyncIterable, + fn: (acc: U, value: T, index: number) => U | Promise, + initial: U, +): Promise; + +export async function reduce( + iterable: Iterable | AsyncIterable, + fn: (acc: U, value: T, index: number) => U | Promise, + initial?: U, +): Promise { + const it = Symbol.iterator in iterable + ? iterable[Symbol.iterator]() + : iterable[Symbol.asyncIterator](); + let index = 0; + if (initial == null) { + const { done, value } = await it.next(); + if (done) { + return undefined; + } + initial = value as unknown as U; + index = 1; + } + let acc: U = initial; + while (true) { + const { done, value } = await it.next(); + if (done) { + break; + } + acc = await fn(acc, value as T, index++); + } + return acc; +} diff --git a/async/reduce_test.ts b/async/reduce_test.ts new file mode 100644 index 0000000..fa3628e --- /dev/null +++ b/async/reduce_test.ts @@ -0,0 +1,175 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { reduce } from "./reduce.ts"; + +Deno.test("reduce", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with initial", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce( + toAsyncIterable([1, 2, 3, 4, 5]), + (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return acc + value; + }, + 5, + ); + assertEquals(result, 20); + assertEquals(accumulators, [5, 6, 8, 11, 15]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("with initial promise", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce( + toAsyncIterable([1, 2, 3, 4, 5]), + (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return Promise.resolve(acc + value); + }, + 5, + ); + assertEquals(result, 20); + assertEquals(accumulators, [5, 6, 8, 11, 15]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("without initial", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce( + toAsyncIterable([1, 2, 3, 4, 5]), + (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return acc + value; + }, + ); + assertEquals(result, 15); + assertEquals(accumulators, [1, 3, 6, 10]); + assertEquals(values, [2, 3, 4, 5]); + assertEquals(indices, [1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("without initial promise", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce( + toAsyncIterable([1, 2, 3, 4, 5]), + (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return Promise.resolve(acc + value); + }, + ); + assertEquals(result, 15); + assertEquals(accumulators, [1, 3, 6, 10]); + assertEquals(values, [2, 3, 4, 5]); + assertEquals(indices, [1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("without initial / with empty iterable", async () => { + const result = await reduce( + toAsyncIterable([] as number[]), + (acc, v) => acc + v, + ); + assertEquals(result, undefined); + assertType>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with initial", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce([1, 2, 3, 4, 5], (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return acc + value; + }, 5); + assertEquals(result, 20); + assertEquals(accumulators, [5, 6, 8, 11, 15]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("with initial promise", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce([1, 2, 3, 4, 5], (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return Promise.resolve(acc + value); + }, 5); + assertEquals(result, 20); + assertEquals(accumulators, [5, 6, 8, 11, 15]); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("without initial", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce([1, 2, 3, 4, 5], (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return acc + value; + }); + assertEquals(result, 15); + assertEquals(accumulators, [1, 3, 6, 10]); + assertEquals(values, [2, 3, 4, 5]); + assertEquals(indices, [1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("without initial promise", async () => { + const accumulators: number[] = []; + const values: number[] = []; + const indices: number[] = []; + const result = await reduce([1, 2, 3, 4, 5], (acc, value, index) => { + accumulators.push(acc); + values.push(value); + indices.push(index); + return Promise.resolve(acc + value); + }); + assertEquals(result, 15); + assertEquals(accumulators, [1, 3, 6, 10]); + assertEquals(values, [2, 3, 4, 5]); + assertEquals(indices, [1, 2, 3, 4]); + assertType>(true); + }); + + await t.step("without initial / with empty iterable", async () => { + const result = await reduce([] as number[], (acc, v) => acc + v); + assertEquals(result, undefined); + assertType>(true); + }); + }); +}); diff --git a/async/some.ts b/async/some.ts new file mode 100644 index 0000000..1cab821 --- /dev/null +++ b/async/some.ts @@ -0,0 +1,26 @@ +/** + * Returns true if at least one element in the iterable satisfies the provided + * + * @param iterable The iterable to check. + * @param fn The function to check with. + * @returns True if at least one element satisfies the provided function. + * + * @example + * ```ts + * import { some } from "@core/iterutil/async/some"; + * + * console.log(await some([1, 2, 3], (value) => value % 2 === 0)); // true + * console.log(await some([1, 3, 5], (value) => value % 2 === 0)); // false + * ``` + */ +export async function some( + iterable: Iterable | AsyncIterable, + fn: (value: T) => boolean | Promise, +): Promise { + for await (const value of iterable) { + if (await fn(value)) { + return true; + } + } + return false; +} diff --git a/async/some_test.ts b/async/some_test.ts new file mode 100644 index 0000000..16c1687 --- /dev/null +++ b/async/some_test.ts @@ -0,0 +1,38 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { some } from "./some.ts"; + +Deno.test("some", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("true", async () => { + const result = await some(toAsyncIterable([1, 2, 3, 4, 5]), (v) => v > 4); + const expected = true; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("false", async () => { + const result = await some(toAsyncIterable([1, 2, 3, 4, 5]), (v) => v > 5); + const expected = false; + assertEquals(result, expected); + assertType>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("true", async () => { + const result = await some([1, 2, 3, 4, 5], (v) => v > 4); + const expected = true; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("false", async () => { + const result = await some([1, 2, 3, 4, 5], (v) => v > 5); + const expected = false; + assertEquals(result, expected); + assertType>(true); + }); + }); +}); diff --git a/async/take.ts b/async/take.ts new file mode 100644 index 0000000..6ee014d --- /dev/null +++ b/async/take.ts @@ -0,0 +1,37 @@ +/** + * Take the first `limit` items from the iterable. + * + * It throws an error if `limit` is less than 0. + * + * @param iterable The iterable to take items from. + * @param limit The number of items to take. + * @returns The iterable with the first `limit` items taken. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { take } from "@core/iterutil/async/take"; + * + * const iter = take([1, 2, 3, 4, 5], 2); + * console.log(await toArray(iter)); // [1, 2] + * ``` + */ +export function take( + iterable: Iterable | AsyncIterable, + limit: number, +): AsyncIterable { + if (limit < 0) { + throw new Error( + `limit argument must be greater than or equal to 0, but got ${limit}.`, + ); + } + return async function* () { + let i = 0; + for await (const item of iterable) { + if (i++ >= limit) { + break; + } + yield item; + } + }(); +} diff --git a/async/take_test.ts b/async/take_test.ts new file mode 100644 index 0000000..1d0752e --- /dev/null +++ b/async/take_test.ts @@ -0,0 +1,59 @@ +import { assertEquals, assertThrows } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { take } from "./take.ts"; + +Deno.test("take", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with positive limit", async () => { + const result = take(toAsyncIterable([0, 1, 2, 3, 4]), 2); + const expected = [0, 1]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with negative limit", () => { + assertThrows( + () => { + take([0, 1, 2, 3, 4], -2); + }, + Error, + "limit argument must be greater than or equal to 0, but got -2.", + ); + }); + + await t.step("with 0 limit", async () => { + const result = take(toAsyncIterable([0, 1, 2, 3, 4]), 0); + const expected: number[] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with positive limit", async () => { + const result = take([0, 1, 2, 3, 4], 2); + const expected = [0, 1]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with negative limit", () => { + assertThrows( + () => { + take([0, 1, 2, 3, 4], -2); + }, + Error, + "limit argument must be greater than or equal to 0, but got -2.", + ); + }); + + await t.step("with 0 limit", async () => { + const result = take([0, 1, 2, 3, 4], 0); + const expected: number[] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/take_while.ts b/async/take_while.ts new file mode 100644 index 0000000..07bd64f --- /dev/null +++ b/async/take_while.ts @@ -0,0 +1,28 @@ +/** + * Take elements from the iterable while the predicate is true. + * + * @param iterable The iterable to take elements from. + * @param fn The predicate to take elements with. + * @returns The taken iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { takeWhile } from "@core/iterutil/async/take-while"; + * + * const iter = takeWhile([1, 2, 3, 4, 5], (value) => value < 4); + * console.log(await toArray(iter)); // [1, 2, 3] + * ``` + */ +export async function* takeWhile( + iterable: Iterable | AsyncIterable, + fn: (value: T, index: number) => boolean | Promise, +): AsyncIterable { + let index = 0; + for await (const value of iterable) { + if (!await fn(value, index++)) { + break; + } + yield value; + } +} diff --git a/async/take_while_test.ts b/async/take_while_test.ts new file mode 100644 index 0000000..7377a83 --- /dev/null +++ b/async/take_while_test.ts @@ -0,0 +1,70 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { takeWhile } from "./take_while.ts"; + +Deno.test("takeWhile", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("with some true", async () => { + const result = takeWhile(toAsyncIterable([0, 1, 2, 3, 4]), (v) => v < 2); + const expected = [0, 1]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with some promise true", async () => { + const result = takeWhile( + toAsyncIterable([0, 1, 2, 3, 4]), + (v) => Promise.resolve(v < 2), + ); + const expected = [0, 1]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all true", async () => { + const result = takeWhile(toAsyncIterable([0, 1, 2, 3, 4]), () => true); + const expected = [0, 1, 2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all false", async () => { + const result = takeWhile(toAsyncIterable([0, 1, 2, 3, 4]), () => false); + const expected: number[] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("with some true", async () => { + const result = takeWhile([0, 1, 2, 3, 4], (v) => v < 2); + const expected = [0, 1]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with some promise true", async () => { + const result = takeWhile([0, 1, 2, 3, 4], (v) => Promise.resolve(v < 2)); + const expected = [0, 1]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all true", async () => { + const result = takeWhile([0, 1, 2, 3, 4], () => true); + const expected = [0, 1, 2, 3, 4]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with all false", async () => { + const result = takeWhile([0, 1, 2, 3, 4], () => false); + const expected: number[] = []; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/to_array.ts b/async/to_array.ts new file mode 100644 index 0000000..3d4adf8 --- /dev/null +++ b/async/to_array.ts @@ -0,0 +1,34 @@ +/** + * Converts an iterable or async iterable to an array. + * + * @param iterable The iterable or async iterable to convert. + * @returns The array. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * + * const arr1 = await toArray(function*() { + * yield 1; + * yield 2; + * yield 3; + * }()); + * console.log(arr1); // [1, 2, 3] + * + * const arr2 = await toArray(async function*() { + * yield 1; + * yield 2; + * yield 3; + * }()); + * console.log(arr2); // [1, 2, 3] + * ``` + */ +export async function toArray( + iterable: Iterable | AsyncIterable, +): Promise { + const result: T[] = []; + for await (const value of iterable) { + result.push(value); + } + return result; +} diff --git a/async/to_array_test.ts b/async/to_array_test.ts new file mode 100644 index 0000000..a86807c --- /dev/null +++ b/async/to_array_test.ts @@ -0,0 +1,27 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toArray } from "./to_array.ts"; + +Deno.test("toArray", async (t) => { + await t.step("with async iterable", async () => { + const result = await toArray(async function* () { + yield 1 as number; + yield 2 as number; + yield 3 as number; + }()); + const expected = [1, 2, 3]; + assertEquals(result, expected); + assertType>(true); + }); + + await t.step("with iterable", async () => { + const result = await toArray(function* () { + yield 1 as number; + yield 2 as number; + yield 3 as number; + }()); + const expected = [1, 2, 3]; + assertEquals(result, expected); + assertType>(true); + }); +}); diff --git a/async/to_async_iterable.ts b/async/to_async_iterable.ts new file mode 100644 index 0000000..a195860 --- /dev/null +++ b/async/to_async_iterable.ts @@ -0,0 +1,20 @@ +/** + * Converts an iterable to an async iterable. + * + * @param iterable The iterable to convert. + * @returns The async iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { toAsyncIterable } from "@core/iterutil/async/to-async-iterable"; + * + * const iter = toAsyncIterable([1, 2, 3]); + * console.log(await toArray(iter)); // [1, 2, 3] + * ``` + */ +export async function* toAsyncIterable( + iterable: Iterable, +): AsyncIterable { + yield* iterable; +} diff --git a/async/to_async_iterable_test.ts b/async/to_async_iterable_test.ts new file mode 100644 index 0000000..a82f3d4 --- /dev/null +++ b/async/to_async_iterable_test.ts @@ -0,0 +1,11 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toArray } from "./to_array.ts"; +import { toAsyncIterable } from "./to_async_iterable.ts"; + +Deno.test("toAsyncIterable", async () => { + const result = toAsyncIterable([1, 2, 3, 4, 5]); + const expected = [1, 2, 3, 4, 5]; + assertEquals(await toArray(result), expected); + assertType>>(true); +}); diff --git a/async/uniq.ts b/async/uniq.ts new file mode 100644 index 0000000..cc6eb15 --- /dev/null +++ b/async/uniq.ts @@ -0,0 +1,41 @@ +/** + * Returns an iterable that yields the unique elements of the input iterable. + * + * @param iterable The iterable to get the unique elements of. + * @param identify An optional function to transform the elements before checking for uniqueness. + * @returns An iterable that yields the unique elements of the input iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { uniq } from "@core/iterutil/async/uniq"; + * + * const iter = uniq([1, 2, 2, 3, 3, 3]); + * console.log(await toArray(iter)); // [1, 2, 3] + * ``` + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { uniq } from "@core/iterutil/async/uniq"; + * + * const iter = uniq( + * [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], + * (v) => Math.floor(v / 10), + * ); + * console.log(await toArray(iter)); // [1, 10, 20, 30] + * ``` + */ +export async function* uniq( + iterable: Iterable | AsyncIterable, + identify: (v: T) => unknown | Promise = (v) => v, +): AsyncIterable { + const set = new Set(); + for await (const item of iterable) { + const identity = await identify(item); + if (!set.has(identity)) { + set.add(identity); + yield item; + } + } +} diff --git a/async/uniq_test.ts b/async/uniq_test.ts new file mode 100644 index 0000000..4ed8498 --- /dev/null +++ b/async/uniq_test.ts @@ -0,0 +1,67 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { uniq } from "./uniq.ts"; + +Deno.test("uniq", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("default", async () => { + const result = uniq( + toAsyncIterable([1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31]), + ); + const expected = [1, 2, 3, 10, 20, 30, 11, 21, 31]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with identify", async () => { + const result = uniq( + toAsyncIterable([1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31]), + (v) => Math.floor(v / 10), + ); + const expected = [1, 10, 20, 30]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with identify promise", async () => { + const result = uniq( + toAsyncIterable([1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31]), + (v) => Promise.resolve(Math.floor(v / 10)), + ); + const expected = [1, 10, 20, 30]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("default", async () => { + const result = uniq([1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31]); + const expected = [1, 2, 3, 10, 20, 30, 11, 21, 31]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with identify", async () => { + const result = uniq( + [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], + (v) => Math.floor(v / 10), + ); + const expected = [1, 10, 20, 30]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("with identify promise", async () => { + const result = uniq( + [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], + (v) => Promise.resolve(Math.floor(v / 10)), + ); + const expected = [1, 10, 20, 30]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + }); +}); diff --git a/async/zip.ts b/async/zip.ts new file mode 100644 index 0000000..474758e --- /dev/null +++ b/async/zip.ts @@ -0,0 +1,39 @@ +/** + * Zips multiple iterables into a single iterable. + * + * @param iterables The iterables to zip. + * @returns The zipped iterable. + * + * @example + * ```ts + * import { toArray } from "@core/iterutil/async/to-array"; + * import { zip } from "@core/iterutil/async/zip"; + * + * const iter = zip([1, 2, 3], ["a", "b", "c"]); + * console.log(await toArray(iter)); // [[1, "a"], [2, "b"], [3, "c"]] + * ``` + */ +export async function* zip< + U extends (Iterable | AsyncIterable)[], +>( + ...iterables: U +): AsyncIterable> { + const its = iterables.map((iterable) => + Symbol.iterator in iterable + ? iterable[Symbol.iterator]() + : iterable[Symbol.asyncIterator]() + ); + while (true) { + const rs = await Promise.all(its.map((it) => it.next())); + if (rs.find(({ done }) => !!done)) { + break; + } + yield rs.map(({ value }) => value) as Zip; + } +} + +export type Zip | AsyncIterable)[]> = { + [P in keyof T]: T[P] extends Iterable ? U + : T[P] extends AsyncIterable ? U + : never; +}; diff --git a/async/zip_test.ts b/async/zip_test.ts new file mode 100644 index 0000000..beb936d --- /dev/null +++ b/async/zip_test.ts @@ -0,0 +1,97 @@ +import { assertEquals } from "@std/assert"; +import { assertType, type IsExact } from "@std/testing/types"; +import { toAsyncIterable } from "./to_async_iterable.ts"; +import { toArray } from "./to_array.ts"; +import { zip } from "./zip.ts"; + +Deno.test("zip", async (t) => { + await t.step("with async iterable", async (t) => { + await t.step("zip with two iterables", async () => { + const result = zip( + toAsyncIterable([1, 2, 3]), + toAsyncIterable(["a", "b", "c"]), + ); + const expected = [[1, "a"], [2, "b"], [3, "c"]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("zip with two in-balanced iterables", async () => { + const result = zip( + toAsyncIterable([1, 2, 3, 4, 5]), + toAsyncIterable(["a", "b", "c"]), + ); + const expected = [[1, "a"], [2, "b"], [3, "c"]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("zip with three iterables", async () => { + const result = zip( + toAsyncIterable([1, 2, 3]), + toAsyncIterable(["a", "b", "c"]), + [true, false, true], + ); + const expected = [[1, "a", true], [2, "b", false], [3, "c", true]]; + assertEquals(await toArray(result), expected); + assertType< + IsExact> + >( + true, + ); + }); + + await t.step("zip with three in-balanced iterables", async () => { + const result = zip( + toAsyncIterable([1, 2, 3, 4, 5]), + toAsyncIterable(["a", "b", "c"]), + [true, false, true], + ); + const expected = [[1, "a", true], [2, "b", false], [3, "c", true]]; + assertEquals(await toArray(result), expected); + assertType< + IsExact> + >( + true, + ); + }); + }); + + await t.step("with iterable", async (t) => { + await t.step("zip with two iterables", async () => { + const result = zip([1, 2, 3], ["a", "b", "c"]); + const expected = [[1, "a"], [2, "b"], [3, "c"]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("zip with two in-balanced iterables", async () => { + const result = zip([1, 2, 3, 4, 5], ["a", "b", "c"]); + const expected = [[1, "a"], [2, "b"], [3, "c"]]; + assertEquals(await toArray(result), expected); + assertType>>(true); + }); + + await t.step("zip with three iterables", async () => { + const result = zip([1, 2, 3], ["a", "b", "c"], [true, false, true]); + const expected = [[1, "a", true], [2, "b", false], [3, "c", true]]; + assertEquals(await toArray(result), expected); + assertType< + IsExact> + >( + true, + ); + }); + + await t.step("zip with three in-balanced iterables", async () => { + const result = zip([1, 2, 3, 4, 5], ["a", "b", "c"], [true, false, true]); + const expected = [[1, "a", true], [2, "b", false], [3, "c", true]]; + assertEquals(await toArray(result), expected); + assertType< + IsExact> + >( + true, + ); + }); + }); +}); diff --git a/deno.jsonc b/deno.jsonc index c2a18e9..88a0ab3 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -3,6 +3,37 @@ "version": "0.0.0", "exports": { ".": "./mod.ts", + "./async": "./async/mod.ts", + "./async/chain": "./async/chain.ts", + "./async/chunked": "./async/chunked.ts", + "./async/compact": "./async/compact.ts", + "./async/compress": "./async/compress.ts", + "./async/count": "./async/count.ts", + "./async/cycle": "./async/cycle.ts", + "./async/drop": "./async/drop.ts", + "./async/drop-while": "./async/drop_while.ts", + "./async/enumerate": "./async/enumerate.ts", + "./async/every": "./async/every.ts", + "./async/filter": "./async/filter.ts", + "./async/find": "./async/find.ts", + "./async/first": "./async/first.ts", + "./async/flat-map": "./async/flat_map.ts", + "./async/flatten": "./async/flatten.ts", + "./async/for-each": "./async/for_each.ts", + "./async/iter": "./async/iter.ts", + "./async/last": "./async/last.ts", + "./async/map": "./async/map.ts", + "./async/pairwise": "./async/pairwise.ts", + "./async/partition": "./async/partition.ts", + "./async/range": "./async/range.ts", + "./async/reduce": "./async/reduce.ts", + "./async/some": "./async/some.ts", + "./async/take": "./async/take.ts", + "./async/take-while": "./async/take_while.ts", + "./async/to-array": "./async/to_array.ts", + "./async/to-async-iterable": "./async/to_async_iterable.ts", + "./async/uniq": "./async/uniq.ts", + "./async/zip": "./async/zip.ts", "./chain": "./chain.ts", "./chunked": "./chunked.ts", "./compact": "./compact.ts", @@ -31,7 +62,7 @@ "./take-while": "./take_while.ts", "./to-array": "./to_array.ts", "./uniq": "./uniq.ts", - "./zip": "./zip.ts" + "./zip": "./zip.ts", }, "exclude": [ ".coverage/**" @@ -50,6 +81,37 @@ }, "imports": { "@core/iterutil": "./mod.ts", + "@core/iterutil/async": "./async/mod.ts", + "@core/iterutil/async/chain": "./async/chain.ts", + "@core/iterutil/async/chunked": "./async/chunked.ts", + "@core/iterutil/async/compact": "./async/compact.ts", + "@core/iterutil/async/compress": "./async/compress.ts", + "@core/iterutil/async/count": "./async/count.ts", + "@core/iterutil/async/cycle": "./async/cycle.ts", + "@core/iterutil/async/drop": "./async/drop.ts", + "@core/iterutil/async/drop-while": "./async/drop_while.ts", + "@core/iterutil/async/enumerate": "./async/enumerate.ts", + "@core/iterutil/async/every": "./async/every.ts", + "@core/iterutil/async/filter": "./async/filter.ts", + "@core/iterutil/async/find": "./async/find.ts", + "@core/iterutil/async/first": "./async/first.ts", + "@core/iterutil/async/flat-map": "./async/flat_map.ts", + "@core/iterutil/async/flatten": "./async/flatten.ts", + "@core/iterutil/async/for-each": "./async/for_each.ts", + "@core/iterutil/async/iter": "./async/iter.ts", + "@core/iterutil/async/last": "./async/last.ts", + "@core/iterutil/async/map": "./async/map.ts", + "@core/iterutil/async/pairwise": "./async/pairwise.ts", + "@core/iterutil/async/partition": "./async/partition.ts", + "@core/iterutil/async/range": "./async/range.ts", + "@core/iterutil/async/reduce": "./async/reduce.ts", + "@core/iterutil/async/some": "./async/some.ts", + "@core/iterutil/async/take": "./async/take.ts", + "@core/iterutil/async/take-while": "./async/take_while.ts", + "@core/iterutil/async/to-array": "./async/to_array.ts", + "@core/iterutil/async/to-async-iterable": "./async/to_async_iterable.ts", + "@core/iterutil/async/uniq": "./async/uniq.ts", + "@core/iterutil/async/zip": "./async/zip.ts", "@core/iterutil/chain": "./chain.ts", "@core/iterutil/chunked": "./chunked.ts", "@core/iterutil/compact": "./compact.ts",