diff --git a/README.md b/README.md index c97c97b..9f96fdc 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,15 @@ Chains multiple iterables together. ```ts import { chain } from "@core/iterutil/chain"; -const iter = chain([1, 2], [3, 4]); -console.log(Array.from(iter)); // [1, 2, 3, 4] +const iter = chain([1, 2], ["a", "b"], [true, false]); +console.log(Array.from(iter)); // [1, 2, "a", "b", true, false] ``` ```ts import { chain } from "@core/iterutil/async/chain"; -const iter = chain([1, 2], [3, 4]); -console.log(await Array.fromAsync(iter)); // [1, 2, 3, 4] +const iter = chain([1, 2], ["a", "b"], [true, false]); +console.log(await Array.fromAsync(iter)); // [1, 2, "a", "b", true, false] ``` ### chunked @@ -75,14 +75,20 @@ Compresses an iterable by selecting elements using a selector iterable. ```ts import { compress } from "@core/iterutil/compress"; -const iter = compress([1, 2, 3, 4, 5], [true, false, true, false, true]); +const iter = compress( + [1, 2, 3, 4, 5], + [true, false, true, false, true], +); console.log(Array.from(iter)); // [1, 3, 5] ``` ```ts import { compress } from "@core/iterutil/async/compress"; -const iter = compress([1, 2, 3, 4, 5], [true, false, true, false, true]); +const iter = compress( + [1, 2, 3, 4, 5], + [true, false, true, false, true], +); console.log(await Array.fromAsync(iter)); // [1, 3, 5] ``` @@ -144,14 +150,20 @@ Drops elements from the iterable while the predicate returns true. ```ts import { dropWhile } from "@core/iterutil/drop-while"; -const iter = dropWhile([1, 2, 3, 4, 5], (x) => x < 3); +const iter = dropWhile( + [1, 2, 3, 4, 5], + (v) => v < 3, +); console.log(Array.from(iter)); // [3, 4, 5] ``` ```ts import { dropWhile } from "@core/iterutil/async/drop-while"; -const iter = dropWhile([1, 2, 3, 4, 5], (x) => x < 3); +const iter = dropWhile( + [1, 2, 3, 4, 5], + (v) => v < 3, +); console.log(await Array.fromAsync(iter)); // [3, 4, 5] ``` @@ -181,15 +193,15 @@ function. ```ts import { every } from "@core/iterutil/every"; -console.log(every([1, 2, 3], (value) => value > 0)); // true -console.log(every([1, 2, 3], (value) => value > 1)); // false +console.log(every([1, 2, 3], (v) => v > 0)); // true +console.log(every([1, 2, 3], (v) => v > 1)); // false ``` ```ts 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 +console.log(await every([1, 2, 3], (v) => v > 0)); // true +console.log(await every([1, 2, 3], (v) => v > 1)); // false ``` ### filter @@ -199,14 +211,20 @@ Filters an iterable based on a function. ```ts import { filter } from "@core/iterutil/filter"; -const iter = filter([1, 2, 3, 4, 5], (value) => value % 2 === 0); +const iter = filter( + [1, 2, 3, 4, 5], + (v) => v % 2 === 0, +); console.log(Array.from(iter)); // [2, 4] ``` ```ts import { filter } from "@core/iterutil/async/filter"; -const iter = filter([1, 2, 3, 4, 5], (value) => value % 2 === 0); +const iter = filter( + [1, 2, 3, 4, 5], + (v) => v % 2 === 0, +); console.log(await Array.fromAsync(iter)); // [2, 4] ``` @@ -218,14 +236,20 @@ function. Otherwise, undefined is returned. ```ts import { find } from "@core/iterutil/find"; -const value = find([1, 2, 3, 4, 5], (value) => value % 2 === 0); +const value = find( + [1, 2, 3, 4, 5], + (v) => v % 2 === 0, +); console.log(value); // 2 ``` ```ts import { find } from "@core/iterutil/async/find"; -const value = await find([1, 2, 3, 4, 5], (value) => value % 2 === 0); +const value = await find( + [1, 2, 3, 4, 5], + (v) => v % 2 === 0, +); console.log(value); // 2 ``` @@ -237,15 +261,15 @@ Returns the first element of an iterable. If the iterable is empty, returns ```ts import { first } from "@core/iterutil/first"; -const value = first([1, 2, 3]); -console.log(value); // 1 +const result = first([1, 2, 3]); +console.log(result); // 1 ``` ```ts import { first } from "@core/iterutil/async/first"; -const value = await first([1, 2, 3]); -console.log(value); // 1 +const result = await first([1, 2, 3]); +console.log(result); // 1 ``` ### flatMap @@ -255,14 +279,20 @@ Maps each value in an iterable to an iterable, then flattens the result. ```ts import { flatMap } from "@core/iterutil/flat-map"; -const iter = flatMap([1, 2, 3], (value) => [value, value]); +const iter = flatMap( + [1, 2, 3], + (v) => [v, v], +); console.log(Array.from(iter)); // [1, 1, 2, 2, 3, 3] ``` ```ts import { flatMap } from "@core/iterutil/async/flat-map"; -const iter = flatMap([1, 2, 3], (value) => [value, value]); +const iter = flatMap( + [1, 2, 3], + (v) => [v, v], +); console.log(await Array.fromAsync(iter)); // [1, 1, 2, 2, 3, 3] ``` @@ -290,7 +320,7 @@ Calls a function for each value in an iterable. ```ts import { forEach } from "@core/iterutil/for-each"; -forEach([1, 2, 3], console.log); +forEach([1, 2, 3], (v) => console.log(v)); // 1 // 2 // 3 @@ -298,7 +328,7 @@ forEach([1, 2, 3], console.log); ```ts import { forEach } from "@core/iterutil/async/for-each"; -await forEach([1, 2, 3], console.log); +await forEach([1, 2, 3], (v) => console.log(v)); // 1 // 2 // 3 @@ -363,14 +393,20 @@ Maps an iterable with a function. ```ts import { map } from "@core/iterutil/map"; -const iter = map([1, 2, 3], (value) => value * 2); +const iter = map( + [1, 2, 3], + (v) => v * 2, +); console.log(Array.from(iter)); // [2, 4, 6] ``` ```ts import { map } from "@core/iterutil/async/map"; -const iter = map([1, 2, 3], (value) => value * 2); +const iter = map( + [1, 2, 3], + (v) => v * 2, +); console.log(await Array.fromAsync(iter)); // [2, 4, 6] ``` @@ -399,7 +435,10 @@ Partitions an iterable into two arrays based on a selector function. ```ts import { partition } from "@core/iterutil/partition"; -const [even, odd] = partition([1, 2, 3, 4, 5], (value) => value % 2 === 0); +const [even, odd] = partition( + [1, 2, 3, 4, 5], + (v) => v % 2 === 0, +); console.log(even); // [2, 4] console.log(odd); // [1, 3, 5] ``` @@ -409,7 +448,7 @@ import { partition } from "@core/iterutil/async/partition"; const [even, odd] = await partition( [1, 2, 3, 4, 5], - (value) => value % 2 === 0, + (v) => v % 2 === 0, ); console.log(even); // [2, 4] console.log(odd); // [1, 3, 5] @@ -422,7 +461,7 @@ Generates a range of numbers. ```ts import { range } from "@core/iterutil/range"; -console.log(Array.from(range(3))); // [0, 1, 2] +console.log(Array.from(range(1, 3))); // [1, 2, 3] console.log(Array.from(range(1, 6, 2))); // [1, 3, 5] ``` @@ -433,12 +472,15 @@ Reduces an iterable into a single value. ```ts import { reduce } from "@core/iterutil/reduce"; -const sum = reduce([1, 2, 3, 4, 5], (acc, value) => acc + value); +const sum = reduce( + [1, 2, 3, 4, 5], + (acc, v) => acc + v, +); console.log(sum); // 15 const joined = reduce( [1, 2, 3, 4, 5], - (acc, value) => acc + value.toString(), + (acc, v) => acc + v, "", ); console.log(joined); // 12345 @@ -447,12 +489,15 @@ console.log(joined); // 12345 ```ts import { reduce } from "@core/iterutil/async/reduce"; -const sum = await reduce([1, 2, 3, 4, 5], (acc, value) => acc + value); +const sum = await reduce( + [1, 2, 3, 4, 5], + (acc, v) => acc + v, +); console.log(sum); // 15 const joined = await reduce( [1, 2, 3, 4, 5], - (acc, value) => acc + value.toString(), + (acc, v) => acc + v, "", ); console.log(joined); // 12345 @@ -465,15 +510,15 @@ Returns true if at least one element in the iterable satisfies the provided ```ts import { some } from "@core/iterutil/some"; -console.log(some([1, 2, 3], (value) => value % 2 === 0)); // true -console.log(some([1, 3, 5], (value) => value % 2 === 0)); // false +console.log(some([1, 2, 3], (v) => v % 2 === 0)); // true +console.log(some([1, 3, 5], (v) => v % 2 === 0)); // false ``` ```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 +console.log(await some([1, 2, 3], (v) => v % 2 === 0)); // true +console.log(await some([1, 3, 5], (v) => v % 2 === 0)); // false ``` ### take @@ -501,14 +546,20 @@ Takes elements from the iterable while the predicate is true. ```ts import { takeWhile } from "@core/iterutil/take-while"; -const iter = takeWhile([1, 2, 3, 4, 5], (value) => value < 4); +const iter = takeWhile( + [1, 2, 3, 4, 5], + (v) => v < 4, +); console.log(Array.from(iter)); // [1, 2, 3] ``` ```ts import { takeWhile } from "@core/iterutil/async/take-while"; -const iter = takeWhile([1, 2, 3, 4, 5], (value) => value < 4); +const iter = takeWhile( + [1, 2, 3, 4, 5], + (v) => v < 4, +); console.log(await Array.fromAsync(iter)); // [1, 2, 3] ``` @@ -534,10 +585,10 @@ const iter1 = uniq([1, 2, 2, 3, 3, 3]); console.log(Array.from(iter1)); // [1, 2, 3] const iter2 = uniq( - [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], - (v) => Math.floor(v / 10), + [1, 2, 3, 4, 5, 6, 7, 8, 9], + (v) => v % 4, ); -console.log(Array.from(iter2)); // [1, 10, 20, 30] +console.log(Array.from(iter2)); // [1, 2, 3, 4] ``` ```ts @@ -547,10 +598,10 @@ const iter1 = uniq([1, 2, 2, 3, 3, 3]); console.log(await Array.fromAsync(iter1)); // [1, 2, 3] const iter2 = uniq( - [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], - (v) => Math.floor(v / 10), + [1, 2, 3, 4, 5, 6, 7, 8, 9], + (v) => v % 4, ); -console.log(await Array.fromAsync(iter2)); // [1, 10, 20, 30] +console.log(await Array.fromAsync(iter2)); // [1, 2, 3, 4] ``` ### zip diff --git a/async/chain.ts b/async/chain.ts index ec4fbd4..6fcab42 100644 --- a/async/chain.ts +++ b/async/chain.ts @@ -1,5 +1,11 @@ /** - * Chains multiple iterables together. + * Chains multiple iterables and returns the chained iterable. + * + * The chained iterable will yield the elements of the first iterable, then the + * elements of the second iterable, and so on. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/zip zip} to zip iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/chain chain} to chain iterables synchronously. * * @param iterables The iterables to chain. * @returns The chained iterable. @@ -8,20 +14,20 @@ * ```ts * import { chain } from "@core/iterutil/async/chain"; * - * const iter = chain([1, 2], [3, 4]); - * console.log(await Array.fromAsync(iter)); // [1, 2, 3, 4] - * ``` - * - * @example With malformed iterables - * ```ts - * import { chain } from "@core/iterutil/async/chain"; - * - * const iter = chain([1, 2], ["a", "b"], [true]); - * console.log(await Array.fromAsync(iter)); // [1, 2, "a", "b", true] + * const iter = chain( + * [1, 2, 3], + * ["a", "b"], + * [true] + * ); + * console.log(await Array.fromAsync(iter)); // [1, 2, 3, "a", "b", true] * ``` */ export async function* chain< - T extends (Iterable | AsyncIterable)[], + T extends readonly [ + Iterable | AsyncIterable, + Iterable | AsyncIterable, + ...(Iterable | AsyncIterable)[], + ], >( ...iterables: T ): AsyncIterable> { diff --git a/async/chain_test.ts b/async/chain_test.ts index b049758..ca8ab91 100644 --- a/async/chain_test.ts +++ b/async/chain_test.ts @@ -4,45 +4,35 @@ import { toAsyncIterable } from "./to_async_iterable.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 Array.fromAsync(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]; + await t.step("with empty iterables", async () => { + const result = chain([] as number[], [] as string[]); + const expected = [] as (number | string)[]; assertEquals(await Array.fromAsync(result), expected); - assertType>>(true); + assertType>>(true); }); - await t.step("with mixed iterable", async () => { + await t.step("with iterables", async () => { const result = chain( - toAsyncIterable([1, 2]), - [3, 4], - toAsyncIterable([5]), + [1, 2, 3], + toAsyncIterable(["a", "b"]), ); - const expected = [1, 2, 3, 4, 5]; + const expected = [1, 2, 3, "a", "b"]; assertEquals(await Array.fromAsync(result), expected); - assertType>>(true); + assertType>>(true); }); - await t.step("with malform iterable", async () => { + await t.step("with multiple iterables", async () => { const result = chain( - toAsyncIterable([1, 2]), + toAsyncIterable([1, 2, 3]), ["a", "b"], toAsyncIterable([true]), ); - const expected = [1, 2, "a", "b", true]; + const expected = [1, 2, 3, "a", "b", true]; assertEquals(await Array.fromAsync(result), expected); assertType< IsExact> - >(true); + >( + true, + ); }); }); diff --git a/async/chunked.ts b/async/chunked.ts index 61d5808..fb25052 100644 --- a/async/chunked.ts +++ b/async/chunked.ts @@ -1,31 +1,47 @@ /** - * Chunks an iterable into arrays of a given size. + * Chunks the iterable into the iterable of arrays of `size`. + * + * The last chunk may have less than `size` elements. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/flatten flatten} to flatten the chunks. + * Use {@linkcode https://jsr.io/@core/iterutil/chunked chunked} to chunk iterables synchronously. * * @param iterable The iterable to chunk. * @param size The size of each chunk. * @returns The chunked iterable. + * @throws {RangeError} if `size` is not a positive safe integer. * * @example * ```ts * import { chunked } from "@core/iterutil/async/chunked"; * - * const iter = chunked([1, 2, 3, 4, 5], 2); + * const iter = chunked( + * [1, 2, 3, 4, 5], + * 2, + * ); * console.log(await Array.fromAsync(iter)); // [[1, 2], [3, 4], [5]] * ``` */ -export async function* chunked( +export function chunked( iterable: Iterable | AsyncIterable, size: number, ): AsyncIterable { - let chunk = []; - for await (const item of iterable) { - chunk.push(item); - if (chunk.length === size) { + if (size <= 0 || !Number.isSafeInteger(size)) { + throw new RangeError( + `size must be a positive safe integer, but got ${size}.`, + ); + } + return async function* () { + let chunk = []; + for await (const item of iterable) { + chunk.push(item); + if (chunk.length === size) { + yield chunk; + chunk = []; + } + } + if (chunk.length > 0) { yield chunk; - chunk = []; } - } - if (chunk.length > 0) { - yield chunk; - } + }(); } diff --git a/async/chunked_test.ts b/async/chunked_test.ts index a95f46a..7519b8b 100644 --- a/async/chunked_test.ts +++ b/async/chunked_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { toAsyncIterable } from "./to_async_iterable.ts"; import { chunked } from "./chunked.ts"; @@ -32,6 +32,17 @@ Deno.test("chunked", async (t) => { assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the length is not positive safe integer", () => { + assertThrows(() => chunked([], NaN), RangeError); + assertThrows(() => chunked([], Infinity), RangeError); + assertThrows(() => chunked([], -Infinity), RangeError); + assertThrows(() => chunked([], -1), RangeError); + assertThrows(() => chunked([], 1.1), RangeError); + assertThrows(() => chunked([], 0), RangeError); + }); + }); }); await t.step("with iterable", async (t) => { @@ -62,5 +73,16 @@ Deno.test("chunked", async (t) => { assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the length is not positive safe integer", () => { + assertThrows(() => chunked([], NaN), RangeError); + assertThrows(() => chunked([], Infinity), RangeError); + assertThrows(() => chunked([], -Infinity), RangeError); + assertThrows(() => chunked([], -1), RangeError); + assertThrows(() => chunked([], 1.1), RangeError); + assertThrows(() => chunked([], 0), RangeError); + }); + }); }); }); diff --git a/async/compact.ts b/async/compact.ts index b0814e2..725e16a 100644 --- a/async/compact.ts +++ b/async/compact.ts @@ -1,6 +1,10 @@ /** * Removes all nullish (`null` or `undefined`) values from an iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/compress compress} to remove values based on an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to remove values based on a function. + * Use {@linkcode https://jsr.io/@core/iterutil/compact compact} to compact iterables synchronously. + * * @param iterable The iterable to compact. * @returns The compacted iterable. * diff --git a/async/compress.ts b/async/compress.ts index 4ef02f1..c1a820e 100644 --- a/async/compress.ts +++ b/async/compress.ts @@ -1,6 +1,19 @@ /** * Compresses an iterable by selecting elements using a selector iterable. * + * The compressed iterable will yield the elements of the iterable for which the + * corresponding selector is true. + * + * If the selector iterable is shorter than the input iterable, the output will + * be truncated to the length of the selector iterable. + * + * If the input iterable is shorter than the selector iterable, the output will + * be truncated to the length of the input iterable. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/compact compact} to remove nullish values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to remove values based on a function. + * Use {@linkcode https://jsr.io/@core/iterutil/compress compress} to compress iterables synchronously. + * * @param iterable The iterable to compress. * @param selectors The selectors to use. * @returns The compressed iterable. @@ -9,7 +22,10 @@ * ```ts * import { compress } from "@core/iterutil/async/compress"; * - * const iter = compress([1, 2, 3, 4, 5], [true, false, true, false, true]); + * const iter = compress( + * [1, 2, 3, 4, 5], + * [true, false, true, false, true], + * ); * console.log(await Array.fromAsync(iter)); // [1, 3, 5] * ``` */ diff --git a/async/cycle.ts b/async/cycle.ts index 8a92714..9fb9d82 100644 --- a/async/cycle.ts +++ b/async/cycle.ts @@ -1,6 +1,9 @@ /** * Returns an infinite iterable that cycles through the given iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/take take} to limit the number of items of the cycled iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/cycle cycle} to cycle the iterable synchronously. + * * @param iterable The iterable to cycle. * @returns The cycled iterable. * @@ -16,10 +19,7 @@ export async function* cycle( iterable: Iterable | AsyncIterable, ): AsyncIterable { - const array: T[] = []; - for await (const item of iterable) { - array.push(item); - } + const array = await Array.fromAsync(iterable); if (array.length === 0) { return; } diff --git a/async/drop.ts b/async/drop.ts index 99af176..bc9f0b8 100644 --- a/async/drop.ts +++ b/async/drop.ts @@ -1,10 +1,14 @@ /** * Drops the first `limit` items from the iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/drop-while dropWhile} to drop items while a condition is met. + * Use {@linkcode https://jsr.io/@core/iterutil/async/take take} to take a specific number of items from an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/drop drop} to drop items synchronously. + * * @param iterable The iterable to drop items from. * @param limit The number of items to drop. It must be 0 or positive safe integer. * @returns The iterable with the first `limit` items dropped. - * @throws {DropLimitError} If `limit` is less than 0 or non safe integer. + * @throws {RangeError} if `limit` is less than 0 or non safe integer. * * @example * ```ts @@ -19,7 +23,14 @@ export function drop( limit: number, ): AsyncIterable { if (limit < 0 || !Number.isSafeInteger(limit)) { - throw new DropLimitError(limit); + throw new RangeError( + `limit must be 0 or positive safe integer, but got ${limit}.`, + ); + } + if (limit === 0) { + return async function* () { + yield* iterable; + }(); } return async function* () { let i = 0; @@ -30,12 +41,3 @@ export function drop( } }(); } - -/** - * Error thrown when the 'limit' is negative or not a safe integer. - */ -export class DropLimitError extends Error { - constructor(limit: number) { - super(`The 'limit' must be 0 or positive safe integer, but got ${limit}.`); - } -} diff --git a/async/drop_test.ts b/async/drop_test.ts index 899fd16..245fbef 100644 --- a/async/drop_test.ts +++ b/async/drop_test.ts @@ -1,7 +1,7 @@ import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { toAsyncIterable } from "./to_async_iterable.ts"; -import { drop, DropLimitError } from "./drop.ts"; +import { drop } from "./drop.ts"; Deno.test("drop", async (t) => { await t.step("with async iterable", async (t) => { @@ -12,21 +12,22 @@ Deno.test("drop", async (t) => { assertType>>(true); }); - await t.step("with negative limit", () => { - assertThrows( - () => { - drop([0, 1, 2, 3, 4], -2); - }, - DropLimitError, - ); - }); - 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 Array.fromAsync(result), expected); assertType>>(true); }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the limit is not 0 nor positive safe integer", () => { + assertThrows(() => drop([], NaN), RangeError); + assertThrows(() => drop([], Infinity), RangeError); + assertThrows(() => drop([], -Infinity), RangeError); + assertThrows(() => drop([], -1), RangeError); + assertThrows(() => drop([], 1.1), RangeError); + }); + }); }); await t.step("with iterable", async (t) => { @@ -37,20 +38,21 @@ Deno.test("drop", async (t) => { assertType>>(true); }); - await t.step("with negative limit", () => { - assertThrows( - () => { - drop([0, 1, 2, 3, 4], -2); - }, - DropLimitError, - ); - }); - 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 Array.fromAsync(result), expected); assertType>>(true); }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the limit is not 0 nor positive safe integer", () => { + assertThrows(() => drop([], NaN), RangeError); + assertThrows(() => drop([], Infinity), RangeError); + assertThrows(() => drop([], -Infinity), RangeError); + assertThrows(() => drop([], -1), RangeError); + assertThrows(() => drop([], 1.1), RangeError); + }); + }); }); }); diff --git a/async/drop_while.ts b/async/drop_while.ts index 56608f7..b678dc1 100644 --- a/async/drop_while.ts +++ b/async/drop_while.ts @@ -4,6 +4,10 @@ * 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. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/drop drop} to drop a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/async/take take} to take a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/drop-while dropWhile} to drop elements synchronously. + * * @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. @@ -12,17 +16,21 @@ * ```ts * import { dropWhile } from "@core/iterutil/async/drop-while"; * - * const iter = dropWhile([1, 2, 3, 4, 5], (x) => x < 3); + * const iter = dropWhile( + * [1, 2, 3, 4, 5], + * (v) => v < 3 + * ); * console.log(await Array.fromAsync(iter)); // [3, 4, 5] * ``` */ export async function* dropWhile( iterable: Iterable | AsyncIterable, - fn: (value: T) => boolean | Promise, + fn: (value: T, index: number) => boolean | Promise, ): AsyncIterable { let dropping = true; + let index = 0; for await (const value of iterable) { - if (dropping && await fn(value)) { + if (dropping && await fn(value, index++)) { continue; } dropping = false; diff --git a/async/drop_while_test.ts b/async/drop_while_test.ts index 062fcd0..d3f6e41 100644 --- a/async/drop_while_test.ts +++ b/async/drop_while_test.ts @@ -6,9 +6,17 @@ 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]; + const values: number[] = []; + const indices: number[] = []; + const result = dropWhile(toAsyncIterable([1, 2, 3, 4, 5]), (v, index) => { + values.push(v); + indices.push(index); + return v < 3; + }); + const expected = [3, 4, 5]; assertEquals(await Array.fromAsync(result), expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>>(true); }); @@ -39,9 +47,17 @@ Deno.test("dropWhile", async (t) => { 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]; + const values: number[] = []; + const indices: number[] = []; + const result = dropWhile([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v < 3; + }); + const expected = [3, 4, 5]; assertEquals(await Array.fromAsync(result), expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>>(true); }); diff --git a/async/enumerate.ts b/async/enumerate.ts index a64293e..67c2b28 100644 --- a/async/enumerate.ts +++ b/async/enumerate.ts @@ -1,26 +1,53 @@ /** - * Enumerates an iterable. + * Returns an iterable of index-value pairs. + * + * The index starts at `start` and increments by `step` for each value. + * If `start` is not provided, it defaults to `0`. + * If `step` is not provided, it defaults to `1`. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/zip zip} to zip iterable with other iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/async/count count} to generate an infinite sequence of numbers. + * Use {@linkcode https://jsr.io/@core/iterutil/enumerate enumerate} to enumerate synchronously. * * @param iterable The iterable to enumerate. * @param start The starting index. + * @param step The step between indices. * @returns An iterable of index-value pairs. + * @throws {RangeError} if `start` or `step` is not finite or `step` is 0. * * @example * ```ts * import { enumerate } from "@core/iterutil/async/enumerate"; * - * const iter = enumerate(["a", "b", "c"]); - * console.log(await Array.fromAsync(iter)); // [[0, "a"], [1, "b"], [2, "c"]] + * const iter1 = enumerate(["a", "b", "c"]); + * console.log(await Array.fromAsync(iter1)); // [[0, "a"], [1, "b"], [2, "c"]] + * + * const iter2 = enumerate(["a", "b", "c"], 1); + * console.log(await Array.fromAsync(iter2)); // [[1, "a"], [2, "b"], [3, "c"]] + * + * const iter3 = enumerate(["a", "b", "c"], 1, 2); + * console.log(await Array.fromAsync(iter3)); // [[1, "a"], [3, "b"], [5, "c"]] * ``` */ -export async function* enumerate( +export 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; + if (!Number.isFinite(start)) { + throw new RangeError(`start must be finite, but got ${start}.`); + } + if (!Number.isFinite(step)) { + throw new RangeError(`step must be finite, but got ${step}.`); + } + if (step === 0) { + throw new RangeError(`step must not be 0.`); } + return async function* () { + 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 index 0bba424..bb14a82 100644 --- a/async/enumerate_test.ts +++ b/async/enumerate_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { toAsyncIterable } from "./to_async_iterable.ts"; import { enumerate } from "./enumerate.ts"; @@ -12,19 +12,77 @@ Deno.test("enumerate", async (t) => { assertType>>(true); }); - await t.step("with start", async () => { + await t.step("with positive start", async () => { const result = enumerate(toAsyncIterable([0, 1, 2]), 1); const expected = [[1, 0], [2, 1], [3, 2]]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); - await t.step("with start/step", async () => { + await t.step("with negative start", async () => { + const result = enumerate(toAsyncIterable([0, 1, 2]), -1); + const expected = [[-1, 0], [0, 1], [1, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("with float start", async () => { + const result = enumerate(toAsyncIterable([0, 1, 2]), 1.1); + const expected = [[1.1, 0], [2.1, 1], [3.1, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("with start and positive step", async () => { const result = enumerate(toAsyncIterable([0, 1, 2]), 1, 2); const expected = [[1, 0], [3, 1], [5, 2]]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); + + await t.step("with start and negative step", async () => { + const result = enumerate(toAsyncIterable([0, 1, 2]), 1, -1); + const expected = [[1, 0], [0, 1], [-1, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("with start and float step", async () => { + const result = enumerate(toAsyncIterable([0, 1, 2]), 1, 0.2); + const expected = [[1, 0], [1.2, 1], [1.4, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the start is not finite", () => { + assertThrows(() => enumerate(toAsyncIterable([]), NaN), RangeError); + assertThrows( + () => enumerate(toAsyncIterable([]), Infinity), + RangeError, + ); + assertThrows( + () => enumerate(toAsyncIterable([]), -Infinity), + RangeError, + ); + }); + + await t.step("if the step is not finite", () => { + assertThrows(() => enumerate(toAsyncIterable([]), 0, NaN), RangeError); + assertThrows( + () => enumerate(toAsyncIterable([]), 0, Infinity), + RangeError, + ); + assertThrows( + () => enumerate(toAsyncIterable([]), 0, -Infinity), + RangeError, + ); + }); + + await t.step("if the step is 0", () => { + assertThrows(() => enumerate(toAsyncIterable([]), 0, 0), RangeError); + }); + }); }); await t.step("with iterable", async (t) => { @@ -35,18 +93,76 @@ Deno.test("enumerate", async (t) => { assertType>>(true); }); - await t.step("with start", async () => { + await t.step("with positive start", async () => { const result = enumerate([0, 1, 2], 1); const expected = [[1, 0], [2, 1], [3, 2]]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); - await t.step("with start/step", async () => { + await t.step("with negative start", async () => { + const result = enumerate([0, 1, 2], -1); + const expected = [[-1, 0], [0, 1], [1, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("with float start", async () => { + const result = enumerate([0, 1, 2], 1.1); + const expected = [[1.1, 0], [2.1, 1], [3.1, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("with start and positive step", async () => { const result = enumerate([0, 1, 2], 1, 2); const expected = [[1, 0], [3, 1], [5, 2]]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); + + await t.step("with start and negative step", async () => { + const result = enumerate([0, 1, 2], 1, -1); + const expected = [[1, 0], [0, 1], [-1, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("with start and float step", async () => { + const result = enumerate([0, 1, 2], 1, 0.2); + const expected = [[1, 0], [1.2, 1], [1.4, 2]]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the start is not finite", () => { + assertThrows(() => enumerate([], NaN), RangeError); + assertThrows( + () => enumerate([], Infinity), + RangeError, + ); + assertThrows( + () => enumerate([], -Infinity), + RangeError, + ); + }); + + await t.step("if the step is not finite", () => { + assertThrows(() => enumerate([], 0, NaN), RangeError); + assertThrows( + () => enumerate([], 0, Infinity), + RangeError, + ); + assertThrows( + () => enumerate([], 0, -Infinity), + RangeError, + ); + }); + + await t.step("if the step is 0", () => { + assertThrows(() => enumerate([], 0, 0), RangeError); + }); + }); }); }); diff --git a/async/every.ts b/async/every.ts index 389da48..e26c1da 100644 --- a/async/every.ts +++ b/async/every.ts @@ -1,5 +1,15 @@ /** - * Returns true if every element in the iterable satisfies the provided testing function. + * Returns true if every element in the iterable satisfies the provided function. + * Otherwise, returns false. + * + * The function is called for each element in the iterable until one of them + * returns false. If the function returns false for any element, this function + * returns false immediately and does not iterate over the remaining elements. + * + * If the iterable is empty, this function returns true. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/some some} to check if any element satisfies the function. + * Use {@linkcode https://jsr.io/@core/iterutil/every every} to check synchronously. * * @param iterable The iterable to test. * @param fn The function to test with. @@ -9,16 +19,20 @@ * ```ts * 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 + * const result = await every( + * [1, 2, 3], + * (v) => v > 0, + * ); + * console.log(result); // true * ``` */ export async function every( iterable: Iterable | AsyncIterable, - fn: (value: T) => boolean | Promise, + fn: (value: T, index: number) => boolean | Promise, ): Promise { + let index = 0; for await (const value of iterable) { - if (!await fn(value)) { + if (!await fn(value, index++)) { return false; } } diff --git a/async/every_test.ts b/async/every_test.ts index 5fe0b32..9eaa47e 100644 --- a/async/every_test.ts +++ b/async/every_test.ts @@ -4,80 +4,144 @@ 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("with async iterable", async (t) => { await t.step("true", async () => { + const values: number[] = []; + const indices: number[] = []; const result = await every( toAsyncIterable([1, 2, 3, 4, 5]), - (v) => v > 0, + (v, index) => { + values.push(v); + indices.push(index); + return v < 6; + }, ); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); assertType>(true); }); await t.step("false", async () => { + const values: number[] = []; + const indices: number[] = []; const result = await every( toAsyncIterable([1, 2, 3, 4, 5]), - (v) => v > 1, + (v, index) => { + values.push(v); + indices.push(index); + return v < 3; + }, ); const expected = false; assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>(true); }); await t.step("promise true", async () => { + const values: number[] = []; + const indices: number[] = []; const result = await every( toAsyncIterable([1, 2, 3, 4, 5]), - (v) => Promise.resolve(v > 0), + (v, index) => { + values.push(v); + indices.push(index); + return Promise.resolve(v < 6); + }, ); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); assertType>(true); }); await t.step("promise false", async () => { + const values: number[] = []; + const indices: number[] = []; const result = await every( toAsyncIterable([1, 2, 3, 4, 5]), - (v) => Promise.resolve(v > 1), + (v, index) => { + values.push(v); + indices.push(index); + return Promise.resolve(v < 3); + }, ); const expected = false; assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); 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 values: number[] = []; + const indices: number[] = []; + const result = await every([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v < 6; + }); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); assertType>(true); }); await t.step("false", async () => { - const result = await every([1, 2, 3, 4, 5], (v) => v > 1); + const values: number[] = []; + const indices: number[] = []; + const result = await every([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v < 3; + }); const expected = false; assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>(true); }); await t.step("promise true", async () => { + const values: number[] = []; + const indices: number[] = []; const result = await every( [1, 2, 3, 4, 5], - (v) => Promise.resolve(v > 0), + (v, index) => { + values.push(v); + indices.push(index); + return Promise.resolve(v < 6); + }, ); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); assertType>(true); }); await t.step("promise false", async () => { + const values: number[] = []; + const indices: number[] = []; const result = await every( [1, 2, 3, 4, 5], - (v) => Promise.resolve(v > 1), + (v, index) => { + values.push(v); + indices.push(index); + return Promise.resolve(v < 3); + }, ); const expected = false; assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>(true); }); }); diff --git a/async/filter.ts b/async/filter.ts index 9b27b68..1d52210 100644 --- a/async/filter.ts +++ b/async/filter.ts @@ -1,6 +1,12 @@ /** * Filters an iterable based on a function. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/compact compact} to remove nullish values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/compress compress} to remove values based on an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/map map} to transform the values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/reduce reduce} to reduce the values. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter synchronously. + * * @params iterable The iterable to filter. * @params fn The function to filter with. * @returns The filtered iterable. @@ -9,7 +15,10 @@ * ```ts * import { filter } from "@core/iterutil/async/filter"; * - * const iter = filter([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * const iter = filter( + * [1, 2, 3, 4, 5], + * (v) => v % 2 === 0, + * ); * console.log(await Array.fromAsync(iter)); // [2, 4] * ``` */ diff --git a/async/find.ts b/async/find.ts index 03b0426..d92c177 100644 --- a/async/find.ts +++ b/async/find.ts @@ -2,6 +2,11 @@ * Returns the first element in the iterable that satisfies the provided * testing function. Otherwise, undefined is returned. * + * Use {@link https://jsr.io/@core/iterutil/async/first first} to get the first element. + * Use {@link https://jsr.io/@core/iterutil/async/last last} to get the last element. + * Use {@link https://jsr.io/@core/iterutil/async/filter filter} to filter elements. + * Use {@link https://jsr.io/@core/iterutil/find find} to find elements synchronously. + * * @param iterable The iterable to search. * @param fn The function to test with. * @returns The first element that satisfies the provided testing function. @@ -10,7 +15,10 @@ * ```ts * import { find } from "@core/iterutil/async/find"; * - * const value = await find([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * const value = await find( + * [1, 2, 3, 4, 5], + * (v) => v % 2 === 0, + * ); * console.log(value); // 2 * ``` */ diff --git a/async/find_test.ts b/async/find_test.ts index d741de8..0cf2b81 100644 --- a/async/find_test.ts +++ b/async/find_test.ts @@ -10,10 +10,10 @@ Deno.test("find", async (t) => { const indices: number[] = []; const result = await find( toAsyncIterable([1, 2, 3, 4, 5]), - (value, index) => { - values.push(value); + (v, index) => { + values.push(v); indices.push(index); - return value % 2 === 0; + return v % 2 === 0; }, ); const expected = 2; @@ -28,10 +28,10 @@ Deno.test("find", async (t) => { const indices: number[] = []; const result = await find( toAsyncIterable([1, 2, 3, 4, 5]), - (value, index) => { - values.push(value); + (v, index) => { + values.push(v); indices.push(index); - return Promise.resolve(value % 2 === 0); + return Promise.resolve(v % 2 === 0); }, ); const expected = 2; @@ -53,10 +53,10 @@ Deno.test("find", 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); + const result = await find([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return value % 2 === 0; + return v % 2 === 0; }); const expected = 2; assertEquals(result, expected); @@ -68,10 +68,10 @@ Deno.test("find", async (t) => { 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); + const result = await find([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return Promise.resolve(value % 2 === 0); + return Promise.resolve(v % 2 === 0); }); const expected = 2; assertEquals(result, expected); diff --git a/async/first.ts b/async/first.ts index b2133c6..6e017dc 100644 --- a/async/first.ts +++ b/async/first.ts @@ -1,6 +1,10 @@ /** * Returns the first element of an iterable. If the iterable is empty, returns `undefined`. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/last last} to get the last element of an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/find find} to get the first element that matches a predicate. + * Use {@linkcode https://jsr.io/@core/iterutil/first first} to get the first element synchronously. + * * @param iterable The iterable to get the first element from. * @returns The first element of the iterable, or `undefined` if the iterable is empty. * diff --git a/async/flat_map.ts b/async/flat_map.ts index 65097e7..eb39f1d 100644 --- a/async/flat_map.ts +++ b/async/flat_map.ts @@ -1,6 +1,11 @@ /** * Maps each value in an iterable to an iterable, then flattens the result. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/map map} to map values to iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/flatten flatten} to flatten an iterable of iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/flat-map flatMap} to flat map an iterable synchronously. + * * @param iterable The iterable to flat map. * @param fn The function to map with. * @returns The flat mapped iterable. @@ -9,7 +14,10 @@ * ```ts * import { flatMap } from "@core/iterutil/async/flat-map"; * - * const iter = flatMap([1, 2, 3], (value) => [value, value]); + * const iter = flatMap( + * [1, 2, 3], + * (v) => [v, v], + * ); * console.log(await Array.fromAsync(iter)); // [1, 1, 2, 2, 3, 3] * ``` */ diff --git a/async/flatten.ts b/async/flatten.ts index 0c627a0..1e1c6a5 100644 --- a/async/flatten.ts +++ b/async/flatten.ts @@ -1,6 +1,11 @@ /** * Flattens an iterable of iterables into a single iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/map map} to map values to iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/flat-map flatMap} to flat map an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/flatten flatten} to flatten an iterable of iterables synchronously. + * * @param iterable The iterable to flatten. * @returns The flattened iterable. * @@ -18,8 +23,6 @@ export async function* flatten( | AsyncIterable | AsyncIterable | Promise>>, ): AsyncIterable { for await (const innerIterable of iterable) { - for await (const value of innerIterable) { - yield value; - } + yield* innerIterable; } } diff --git a/async/for_each.ts b/async/for_each.ts index 407d422..4be7ec9 100644 --- a/async/for_each.ts +++ b/async/for_each.ts @@ -1,6 +1,10 @@ /** * Calls a function for each value in an iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/map map} to transform values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/for-each forEach} to iterate synchronously. + * * @param iterable The iterable to iterate over. * @param fn The function to call for each value. * @@ -8,7 +12,7 @@ * ```ts * import { forEach } from "@core/iterutil/async/for-each"; * - * await forEach([1, 2, 3], console.log); + * await forEach([1, 2, 3], (v) => console.log(v)); * // 1 * // 2 * // 3 diff --git a/async/iter.ts b/async/iter.ts index 2ffb7bc..b12bdd7 100644 --- a/async/iter.ts +++ b/async/iter.ts @@ -1,8 +1,11 @@ /** - * Converts an iterable to an iterator. + * Converts an iterable to an iterable iterator. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/to-async-iterable toAsyncIterable} for converting an async iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/iter iter} for `IterableIterator`. * * @param iterable The iterable to convert. - * @returns The iterator. + * @returns The iterable iterator. * * @example * ```ts diff --git a/async/last.ts b/async/last.ts index 4de6bb5..0ba8130 100644 --- a/async/last.ts +++ b/async/last.ts @@ -1,6 +1,10 @@ /** * Returns the last element of an iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/first first} to get the first element. + * Use {@linkcode https://jsr.io/@core/iterutil/async/find find} to find an element. + * Use {@linkcode https://jsr.io/@core/iterutil/last last} to get the last element synchronously. + * * @param iterable The iterable to get the last element of. * @returns The last element of the iterable, or `undefined` if the iterable is empty. * diff --git a/async/map.ts b/async/map.ts index 95e869a..25f8b37 100644 --- a/async/map.ts +++ b/async/map.ts @@ -1,6 +1,12 @@ /** * Maps an iterable with a function. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/for-each forEach} to call a function for each value. + * Use {@linkcode https://jsr.io/@core/iterutil/async/flat-map flatMap} to flat map an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/flatten flatten} to flatten an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/map map} to map synchronously. + * * @param iterable The iterable to map. * @param fn The function to map with. * @returns The mapped iterable. @@ -9,7 +15,10 @@ * ```ts * import { map } from "@core/iterutil/async/map"; * - * const iter = map([1, 2, 3], (value) => value * 2); + * const iter = map( + * [1, 2, 3], + * (v) => v * 2, + * ); * console.log(await Array.fromAsync(iter)); // [2, 4, 6] * ``` */ diff --git a/async/map_test.ts b/async/map_test.ts index a8362ca..8c20471 100644 --- a/async/map_test.ts +++ b/async/map_test.ts @@ -7,10 +7,10 @@ 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); + const result = map(toAsyncIterable([1, 2, 3, 4, 5]), (v, index) => { + values.push(v); indices.push(index); - return value * 2; + return v * 2; }); const expected = [2, 4, 6, 8, 10]; assertEquals(await Array.fromAsync(result), expected); @@ -22,10 +22,10 @@ Deno.test("map", async (t) => { 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); + const result = map(toAsyncIterable([1, 2, 3, 4, 5]), (v, index) => { + values.push(v); indices.push(index); - return Promise.resolve(value * 2); + return Promise.resolve(v * 2); }); const expected = [2, 4, 6, 8, 10]; assertEquals(await Array.fromAsync(result), expected); @@ -37,10 +37,10 @@ Deno.test("map", async (t) => { 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); + const result = map([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return value * 2; + return v * 2; }); const expected = [2, 4, 6, 8, 10]; assertEquals(await Array.fromAsync(result), expected); @@ -52,10 +52,10 @@ Deno.test("map", async (t) => { 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); + const result = map([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return Promise.resolve(value * 2); + return Promise.resolve(v * 2); }); const expected = [2, 4, 6, 8, 10]; assertEquals(await Array.fromAsync(result), expected); diff --git a/async/pairwise.ts b/async/pairwise.ts index e79d767..24301ba 100644 --- a/async/pairwise.ts +++ b/async/pairwise.ts @@ -3,6 +3,8 @@ * * When the input iterable has a finite number of items `n`, the output iterable will have `n - 1` items. * + * Use {@linkcode https://jsr.io/@core/iterutil/pairwise pairwise} to pair elements from an iterable synchronously. + * * @param iterable The iterable to pair elements from. * @returns The paired iterable. * diff --git a/async/partition.ts b/async/partition.ts index c075eae..af28b97 100644 --- a/async/partition.ts +++ b/async/partition.ts @@ -1,6 +1,8 @@ /** * Partitions an iterable into two arrays based on a selector function. * + * Use {@linkcode https://jsr.io/@core/iterutil/partition partition} to partition synchronously. + * * @param iterable The iterable to partition. * @param selector The function to partition with. * @returns The partitioned arrays. @@ -9,7 +11,10 @@ * ```ts * import { partition } from "@core/iterutil/async/partition"; * - * const [even, odd] = await partition([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * const [even, odd] = await partition( + * [1, 2, 3, 4, 5], + * (v) => v % 2 === 0, + * ); * console.log(even); // [2, 4] * console.log(odd); // [1, 3, 5] * ``` diff --git a/async/reduce.ts b/async/reduce.ts index 86fc2d9..69d2ec3 100644 --- a/async/reduce.ts +++ b/async/reduce.ts @@ -1,6 +1,12 @@ /** * Reduces an iterable into a single value. * + * The first element is used as the initial value. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/map map} to transform values of the iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to filter values of the iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/reduce reduce} to reduce an iterable synchronously. + * * @param iterable The iterable to reduce. * @param fn The function to reduce with. * @returns The reduced value. @@ -9,8 +15,11 @@ * ```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 + * const result = await reduce( + * [1, 2, 3, 4, 5], + * (a, v) => a + v, + * ); + * console.log(result); // 15 * ``` */ export function reduce( @@ -30,8 +39,12 @@ export function reduce( * ```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 + * const result = await reduce( + * [1, 2, 3, 4, 5], + * (a, v) => a + v, + * "" + * ); + * console.log(result); // 12345 * ``` */ export function reduce( diff --git a/async/some.ts b/async/some.ts index 1cab821..4cda776 100644 --- a/async/some.ts +++ b/async/some.ts @@ -1,24 +1,39 @@ /** * Returns true if at least one element in the iterable satisfies the provided + * function. Otherwise, returns false. + * + * The function is called for each element in the iterable until one of them + * returns true. If the function returns true for any element, this function + * returns true immediately and does not iterate over the remaining elements. + * + * If the iterable is empty, this function returns false. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/every every} to check if every + * element satisfies the provided function. + * Use {@linkcode https://jsr.io/@core/iterutil/some some} to check synchronously. * * @param iterable The iterable to check. * @param fn The function to check with. - * @returns True if at least one element satisfies the provided function. + * @returns True if at least one element satisfies the provided function, otherwise false. * * @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 + * const result = await some( + * [1, 2, 3], + * (v) => v % 2 === 0, + * ); + * console.log(result); // true * ``` */ export async function some( iterable: Iterable | AsyncIterable, - fn: (value: T) => boolean | Promise, + fn: (value: T, index: number) => boolean | Promise, ): Promise { + let index = 0; for await (const value of iterable) { - if (await fn(value)) { + if (await fn(value, index++)) { return true; } } diff --git a/async/some_test.ts b/async/some_test.ts index 16c1687..c20568c 100644 --- a/async/some_test.ts +++ b/async/some_test.ts @@ -6,32 +6,70 @@ 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 values: number[] = []; + const indices: number[] = []; + const result = await some( + toAsyncIterable([1, 2, 3, 4, 5]), + (v, index) => { + values.push(v); + indices.push(index); + return v > 2; + }, + ); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>(true); }); await t.step("false", async () => { - const result = await some(toAsyncIterable([1, 2, 3, 4, 5]), (v) => v > 5); + const values: number[] = []; + const indices: number[] = []; + const result = await some( + toAsyncIterable([1, 2, 3, 4, 5]), + (v, index) => { + values.push(v); + indices.push(index); + return v > 5; + }, + ); const expected = false; assertEquals(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("true", async () => { - const result = await some([1, 2, 3, 4, 5], (v) => v > 4); + const values: number[] = []; + const indices: number[] = []; + const result = await some([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v > 2; + }); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>(true); }); await t.step("false", async () => { - const result = await some([1, 2, 3, 4, 5], (v) => v > 5); + const values: number[] = []; + const indices: number[] = []; + const result = await some([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v > 5; + }); const expected = false; assertEquals(result, expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); assertType>(true); }); }); diff --git a/async/take.ts b/async/take.ts index 118b5ab..f4eec4d 100644 --- a/async/take.ts +++ b/async/take.ts @@ -1,10 +1,16 @@ /** * Takes the first `limit` items from the iterable. * + * Note that it will stop consuming the iterable once `limit` items are taken. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/take-while takeWhile} to take items while the predicate returns true. + * Use {@linkcode https://jsr.io/@core/iterutil/async/drop drop} to drop items from the beginning. + * Use {@linkcode https://jsr.io/@core/iterutil/take take} to take items synchronously. + * * @param iterable The iterable to take items from. * @param limit The number of items to take. It must be 0 or positive safe integer. * @returns The iterable with the first `limit` items taken. - * @throws {TakeLimitError} If `limit` is less than 0 or non safe integer. + * @throws {RangeError} if `limit` is less than 0 or non safe integer. * * @example * ```ts @@ -19,24 +25,20 @@ export function take( limit: number, ): AsyncIterable { if (limit < 0 || !Number.isSafeInteger(limit)) { - throw new TakeLimitError(limit); + throw new RangeError( + `limit must be 0 or positive safe integer, but got ${limit}.`, + ); + } + if (limit === 0) { + return async function* () {}(); } return async function* () { - let i = 0; + let i = 1; for await (const item of iterable) { + yield item; if (i++ >= limit) { break; } - yield item; } }(); } - -/** - * Error thrown when the 'limit' is negative or not a safe integer. - */ -export class TakeLimitError extends Error { - constructor(limit: number) { - super(`The 'limit' must be 0 or positive safe integer, but got ${limit}.`); - } -} diff --git a/async/take_test.ts b/async/take_test.ts index d9a273d..fd52b79 100644 --- a/async/take_test.ts +++ b/async/take_test.ts @@ -1,7 +1,8 @@ import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { toAsyncIterable } from "./to_async_iterable.ts"; -import { take, TakeLimitError } from "./take.ts"; +import { iter } from "./iter.ts"; +import { take } from "./take.ts"; Deno.test("take", async (t) => { await t.step("with async iterable", async (t) => { @@ -12,15 +13,6 @@ Deno.test("take", async (t) => { assertType>>(true); }); - await t.step("with negative limit", () => { - assertThrows( - () => { - take([0, 1, 2, 3, 4], -2); - }, - TakeLimitError, - ); - }); - await t.step("with 0 limit", async () => { const result = take(toAsyncIterable([0, 1, 2, 3, 4]), 0); const expected: number[] = []; @@ -37,15 +29,6 @@ Deno.test("take", async (t) => { assertType>>(true); }); - await t.step("with negative limit", () => { - assertThrows( - () => { - take([0, 1, 2, 3, 4], -2); - }, - TakeLimitError, - ); - }); - await t.step("with 0 limit", async () => { const result = take([0, 1, 2, 3, 4], 0); const expected: number[] = []; @@ -53,4 +36,24 @@ Deno.test("take", async (t) => { assertType>>(true); }); }); + + await t.step("will stop consuming once limit items is taken", async () => { + const it = iter([0, 1, 2, 3, 4]); + const result = take(it, 3); + const expected: number[] = [0, 1, 2]; + assertEquals(await Array.fromAsync(result), expected); + assertType>>(true); + // Ensure the iterator is NOT fully consumed + assertEquals(await Array.fromAsync(it), [3, 4]); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the limit is not 0 nor positive safe integer", () => { + assertThrows(() => take([], NaN), RangeError); + assertThrows(() => take([], Infinity), RangeError); + assertThrows(() => take([], -Infinity), RangeError); + assertThrows(() => take([], -1), RangeError); + assertThrows(() => take([], 1.1), RangeError); + }); + }); }); diff --git a/async/take_while.ts b/async/take_while.ts index 3871bc8..f687ea8 100644 --- a/async/take_while.ts +++ b/async/take_while.ts @@ -1,6 +1,10 @@ /** * Takes elements from the iterable while the predicate is true. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/take take} to take a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/async/drop drop} to drop a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/take-while take} to take elements synchronously. + * * @param iterable The iterable to take elements from. * @param fn The predicate to take elements with. * @returns The taken iterable. @@ -9,7 +13,10 @@ * ```ts * import { takeWhile } from "@core/iterutil/async/take-while"; * - * const iter = takeWhile([1, 2, 3, 4, 5], (value) => value < 4); + * const iter = takeWhile( + * [1, 2, 3, 4, 5], + * (v) => v < 4 + * ); * console.log(await Array.fromAsync(iter)); // [1, 2, 3] * ``` */ diff --git a/async/uniq.ts b/async/uniq.ts index 93dcf4b..0431a54 100644 --- a/async/uniq.ts +++ b/async/uniq.ts @@ -18,21 +18,22 @@ * 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), + * [1, 2, 3, 4, 5, 6, 7, 8, 9], + * (v) => v % 4, * ); - * console.log(await Array.fromAsync(iter)); // [1, 10, 20, 30] + * console.log(await Array.fromAsync(iter)); // [1, 2, 3, 4] * ``` */ export async function* uniq( iterable: Iterable | AsyncIterable, - identify: (v: T) => unknown | Promise = (v) => v, + identify: (v: T, index: number) => unknown | Promise = (v) => v, ): AsyncIterable { const set = new Set(); + let index = 0; for await (const item of iterable) { - const identity = await identify(item); - if (!set.has(identity)) { - set.add(identity); + const id = await identify(item, index++); + if (!set.has(id)) { + set.add(id); yield item; } } diff --git a/async/uniq_test.ts b/async/uniq_test.ts index 055c752..74afd0d 100644 --- a/async/uniq_test.ts +++ b/async/uniq_test.ts @@ -7,29 +7,41 @@ 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]), + toAsyncIterable([1, 2, 2, 3, 3, 3]), ); - const expected = [1, 2, 3, 10, 20, 30, 11, 21, 31]; + const expected = [1, 2, 3]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); await t.step("with identify", async () => { + const values: number[] = []; + const indices: number[] = []; + const identities: number[] = []; const result = uniq( - toAsyncIterable([1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31]), - (v) => Math.floor(v / 10), + toAsyncIterable([1, 2, 3, 4, 5, 6, 7, 8, 9]), + (v, index) => { + values.push(v); + indices.push(index); + const id = v % 4; + identities.push(id); + return id; + }, ); - const expected = [1, 10, 20, 30]; + const expected = [1, 2, 3, 4]; assertEquals(await Array.fromAsync(result), expected); + assertEquals(values, [1, 2, 3, 4, 5, 6, 7, 8, 9]); + assertEquals(indices, [0, 1, 2, 3, 4, 5, 6, 7, 8]); + assertEquals(identities, [1, 2, 3, 0, 1, 2, 3, 0, 1]); 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)), + toAsyncIterable([1, 2, 3, 4, 5, 6, 7, 8, 9]), + (v) => Promise.resolve(v % 4), ); - const expected = [1, 10, 20, 30]; + const expected = [1, 2, 3, 4]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); @@ -37,28 +49,40 @@ Deno.test("uniq", async (t) => { 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]; + const result = uniq([1, 2, 2, 3, 3, 3]); + const expected = [1, 2, 3]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); await t.step("with identify", async () => { + const values: number[] = []; + const indices: number[] = []; + const identities: number[] = []; const result = uniq( - [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], - (v) => Math.floor(v / 10), + [1, 2, 3, 4, 5, 6, 7, 8, 9], + (v, index) => { + values.push(v); + indices.push(index); + const id = v % 4; + identities.push(id); + return id; + }, ); - const expected = [1, 10, 20, 30]; + const expected = [1, 2, 3, 4]; assertEquals(await Array.fromAsync(result), expected); + assertEquals(values, [1, 2, 3, 4, 5, 6, 7, 8, 9]); + assertEquals(indices, [0, 1, 2, 3, 4, 5, 6, 7, 8]); + assertEquals(identities, [1, 2, 3, 0, 1, 2, 3, 0, 1]); 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)), + [1, 2, 3, 4, 5, 6, 7, 8, 9], + (v) => Promise.resolve(v % 4), ); - const expected = [1, 10, 20, 30]; + const expected = [1, 2, 3, 4]; assertEquals(await Array.fromAsync(result), expected); assertType>>(true); }); diff --git a/async/zip.ts b/async/zip.ts index b6fb2c1..5db9581 100644 --- a/async/zip.ts +++ b/async/zip.ts @@ -1,6 +1,16 @@ /** * Zips multiple iterables into a single iterable. * + * The resulting iterable will yield arrays of elements from the input iterables. + * The first array will contain the first element of each input iterable, the second array will contain the second element of each input iterable, and so on. + * + * If the input iterables have different lengths, the resulting iterable will stop when the shortest input iterable is exhausted. + * The remaining elements from the longer input iterables will be ignored. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/chain chain} to chain iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/async/enumerate enumerate} to zip with indices. + * Use {@linkcode https://jsr.io/@core/iterutil/zip zip} to zip iterables synchronously. + * * @param iterables The iterables to zip. * @returns The zipped iterable. * @@ -8,33 +18,41 @@ * ```ts * import { zip } from "@core/iterutil/async/zip"; * - * const iter = zip([1, 2, 3], ["a", "b", "c"]); - * console.log(await Array.fromAsync(iter)); // [[1, "a"], [2, "b"], [3, "c"]] + * const iter = zip( + * [1, 2, 3], + * ["a", "b", "c"], + * [true, false, true], + * ); + * console.log(await Array.fromAsync(iter)); // [[1, "a", true], [2, "b", false], [3, "c", true]] * ``` */ export async function* zip< - U extends (Iterable | AsyncIterable)[], + U extends readonly [ + Iterable | AsyncIterable, + Iterable | AsyncIterable, + ...(Iterable | AsyncIterable)[], + ], >( ...iterables: U ): AsyncIterable> { - const its = iterables.map((iterable) => - Symbol.iterator in iterable - ? iterable[Symbol.iterator]() - : iterable[Symbol.asyncIterator]() + const iterators = iterables.map((it) => + Symbol.iterator in it ? it[Symbol.iterator]() : it[Symbol.asyncIterator]() ); while (true) { - const rs = await Promise.all(its.map((it) => it.next())); - if (rs.find(({ done }) => !!done)) { + const results = await Promise.all(iterators.map((it) => it.next())); + if (results.find(({ done }) => !!done)) { break; } - yield rs.map(({ value }) => value) as Zip; + yield results.map(({ value }) => value) as Zip; } } /** * @internal */ -export type Zip | AsyncIterable)[]> = { +export type Zip< + T extends readonly (Iterable | AsyncIterable)[], +> = { [P in keyof T]: T[P] extends Iterable ? U : T[P] extends AsyncIterable ? U : never; diff --git a/chain.ts b/chain.ts index 9e8fac1..6552a86 100644 --- a/chain.ts +++ b/chain.ts @@ -1,5 +1,11 @@ /** - * Chains multiple iterables together. + * Chains multiple iterables and returns the chained iterable. + * + * The chained iterable will yield the elements of the first iterable, then the + * elements of the second iterable, and so on. + * + * Use {@linkcode https://jsr.io/@core/iterutil/zip zip} to zip iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/async/chain chain} to chain iterables asynchronously. * * @param iterables The iterables to chain. * @returns The chained iterable. @@ -8,19 +14,21 @@ * ```ts * import { chain } from "@core/iterutil/chain"; * - * const iter = chain([1, 2], [3, 4]); - * console.log(Array.from(iter)); // [1, 2, 3, 4] - * ``` - * - * @example With malformed iterables - * ```ts - * import { chain } from "@core/iterutil/chain"; - * - * const iter = chain([1, 2], ["a", "b"], [true]); - * console.log(Array.from(iter)); // [1, 2, "a", "b", true] + * const iter = chain( + * [1, 2, 3], + * ["a", "b"], + * [true] + * ); + * console.log(Array.from(iter)); // [1, 2, 3, "a", "b", true] * ``` */ -export function* chain[]>( +export function* chain< + T extends readonly [ + Iterable, + Iterable, + ...Iterable[], + ], +>( ...iterables: T ): Iterable> { for (const iterable of iterables) { diff --git a/chain_test.ts b/chain_test.ts index d361a12..8ff6566 100644 --- a/chain_test.ts +++ b/chain_test.ts @@ -3,16 +3,30 @@ import { assertType, type IsExact } from "@std/testing/types"; import { chain } from "./chain.ts"; Deno.test("chain", async (t) => { - await t.step("uniform iterables", () => { - const result = chain([1, 2], [3, 4], [5]); - const expected = [1, 2, 3, 4, 5]; + await t.step("with empty iterables", () => { + const result = chain([] as number[], [] as string[]); + const expected = [] as (number | string)[]; assertEquals(Array.from(result), expected); - assertType>>(true); + assertType>>(true); }); - await t.step("malform iterables", () => { - const result = chain([1, 2], ["a", "b"], [true]); - const expected = [1, 2, "a", "b", true]; + await t.step("with iterables", () => { + const result = chain( + [1, 2, 3], + ["a", "b"], + ); + const expected = [1, 2, 3, "a", "b"]; + assertEquals(Array.from(result), expected); + assertType>>(true); + }); + + await t.step("with multiple iterables", () => { + const result = chain( + [1, 2, 3], + ["a", "b"], + [true], + ); + const expected = [1, 2, 3, "a", "b", true]; assertEquals(Array.from(result), expected); assertType>>( true, diff --git a/chunked.ts b/chunked.ts index 23c2294..7bde7b4 100644 --- a/chunked.ts +++ b/chunked.ts @@ -1,31 +1,47 @@ /** - * Chunks an iterable into arrays of a given size. + * Chunks the iterable into the iterable of arrays of `size`. + * + * The last chunk may have less than `size` elements. + * + * Use {@linkcode https://jsr.io/@core/iterutil/flatten flatten} to flatten the chunks. + * Use {@linkcode https://jsr.io/@core/iterutil/async/chunked chunked} to chunk iterables asynchronously. * * @param iterable The iterable to chunk. * @param size The size of each chunk. * @returns The chunked iterable. + * @throws {RangeError} if `size` is not a positive safe integer. * * @example * ```ts * import { chunked } from "@core/iterutil/chunked"; * - * const iter = chunked([1, 2, 3, 4, 5], 2); + * const iter = chunked( + * [1, 2, 3, 4, 5], + * 2, + * ); * console.log(Array.from(iter)); // [[1, 2], [3, 4], [5]] * ``` */ -export function* chunked( +export function chunked( iterable: Iterable, size: number, ): Iterable { - let chunk = []; - for (const item of iterable) { - chunk.push(item); - if (chunk.length === size) { + if (size <= 0 || !Number.isSafeInteger(size)) { + throw new RangeError( + `size must be a positive safe integer, but got ${size}.`, + ); + } + return function* () { + let chunk = []; + for (const item of iterable) { + chunk.push(item); + if (chunk.length === size) { + yield chunk; + chunk = []; + } + } + if (chunk.length > 0) { yield chunk; - chunk = []; } - } - if (chunk.length > 0) { - yield chunk; - } + }(); } diff --git a/chunked_test.ts b/chunked_test.ts index 0c096e5..df35a94 100644 --- a/chunked_test.ts +++ b/chunked_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { chunked } from "./chunked.ts"; @@ -30,4 +30,15 @@ Deno.test("chunked", async (t) => { assertEquals(Array.from(result), expected); assertType>>(true); }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the length is not positive safe integer", () => { + assertThrows(() => chunked([], NaN), RangeError); + assertThrows(() => chunked([], Infinity), RangeError); + assertThrows(() => chunked([], -Infinity), RangeError); + assertThrows(() => chunked([], -1), RangeError); + assertThrows(() => chunked([], 1.1), RangeError); + assertThrows(() => chunked([], 0), RangeError); + }); + }); }); diff --git a/compact.ts b/compact.ts index ac42adc..40b5f1c 100644 --- a/compact.ts +++ b/compact.ts @@ -1,6 +1,10 @@ /** * Removes all nullish (`null` or `undefined`) values from an iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/compress compress} to remove values based on an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to remove values based on a function. + * Use {@linkcode https://jsr.io/@core/iterutil/async/compact compact} to compact iterables asynchronously. + * * @param iterable The iterable to compact. * @returns The compacted iterable. * diff --git a/compress.ts b/compress.ts index 2549739..68b48a7 100644 --- a/compress.ts +++ b/compress.ts @@ -1,15 +1,31 @@ /** * Compresses an iterable by selecting elements using a selector iterable. * + * The compressed iterable will yield the elements of the iterable for which the + * corresponding selector is true. + * + * If the selector iterable is shorter than the input iterable, the output will + * be truncated to the length of the selector iterable. + * + * If the input iterable is shorter than the selector iterable, the output will + * be truncated to the length of the input iterable. + * + * Use {@linkcode https://jsr.io/@core/iterutil/compact compact} to remove nullish values. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to remove values based on a function. + * Use {@linkcode https://jsr.io/@core/iterutil/async/compress compress} to compress iterables asynchronously. + * * @param iterable The iterable to compress. - * @param selectors The selectors to use. + * @param selectors The selectors iterable to use. * @returns The compressed iterable. * * @example * ```ts * import { compress } from "@core/iterutil/compress"; * - * const iter = compress([1, 2, 3, 4, 5], [true, false, true, false, true]); + * const iter = compress( + * [1, 2, 3, 4, 5], + * [true, false, true, false, true], + * ); * console.log(Array.from(iter)); // [1, 3, 5] * ``` */ diff --git a/count.ts b/count.ts index d724fc8..a6996db 100644 --- a/count.ts +++ b/count.ts @@ -1,9 +1,17 @@ /** * Generates an infinite sequence of numbers starting from `start` with a step of `step`. * + * The sequence starts from `start` and increments by `step` for each number. + * If `start` is not provided, it defaults to `0`. + * If `step` is not provided, it defaults to `1`. + * + * Use {@linkcode https://jsr.io/@core/iterutil/take take} to limit the number of items. + * Use {@linkcode https://jsr.io/@core/iterutil/range range} to generate a sequence of numbers within a range. + * * @param start The number to start the sequence from. * @param step The step between each number in the sequence. * @returns The count iterable. + * @throws {RangeError} if `start` or `step` is not finite or `step` is 0. * * @example * ```ts @@ -14,10 +22,21 @@ * console.log(Array.from(take(iter, 5))); // [1, 3, 5, 7, 9] * ``` */ -export function* count(start: number = 0, step: number = 1): Iterable { - let i = start; - while (true) { - yield i; - i += step; +export function count(start: number = 0, step: number = 1): Iterable { + if (!Number.isFinite(start)) { + throw new RangeError(`start must be finite, but got ${start}.`); + } + if (!Number.isFinite(step)) { + throw new RangeError(`step must be finite, but got ${step}.`); + } + if (step === 0) { + throw new RangeError(`step must not be 0.`); } + return function* () { + let i = start; + while (true) { + yield i; + i += step; + } + }(); } diff --git a/count_test.ts b/count_test.ts index cecdf48..da031fe 100644 --- a/count_test.ts +++ b/count_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { take } from "./take.ts"; import { count } from "./count.ts"; @@ -11,17 +11,63 @@ Deno.test("count", async (t) => { assertType>>(true); }); - await t.step("with start", () => { - const result = count(2); - const expected = [2, 3, 4]; + await t.step("with positive start", () => { + const result = count(1); + const expected = [1, 2, 3]; assertEquals(Array.from(take(result, 3)), expected); assertType>>(true); }); - await t.step("with start/step", () => { - const result = count(2, 2); - const expected = [2, 4, 6]; + await t.step("with negative start", () => { + const result = count(-1); + const expected = [-1, 0, 1]; assertEquals(Array.from(take(result, 3)), expected); assertType>>(true); }); + + await t.step("with float start", () => { + const result = count(1.1); + const expected = [1.1, 2.1, 3.1]; + assertEquals(Array.from(take(result, 3)), expected); + assertType>>(true); + }); + + await t.step("with start and positive step", () => { + const result = count(1, 2); + const expected = [1, 3, 5]; + assertEquals(Array.from(take(result, 3)), expected); + assertType>>(true); + }); + + await t.step("with start and negative step", () => { + const result = count(1, -1); + const expected = [1, 0, -1]; + assertEquals(Array.from(take(result, 3)), expected); + assertType>>(true); + }); + + await t.step("with start and float step", () => { + const result = count(1, 0.2); + const expected = [1.0, 1.2, 1.4]; + assertEquals(Array.from(take(result, 3)), expected); + assertType>>(true); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the start is not finite", () => { + assertThrows(() => count(NaN), RangeError); + assertThrows(() => count(Infinity), RangeError); + assertThrows(() => count(-Infinity), RangeError); + }); + + await t.step("if the step is not finite", () => { + assertThrows(() => count(0, NaN), RangeError); + assertThrows(() => count(0, Infinity), RangeError); + assertThrows(() => count(0, -Infinity), RangeError); + }); + + await t.step("if the step is 0", () => { + assertThrows(() => count(0, 0), RangeError); + }); + }); }); diff --git a/cycle.ts b/cycle.ts index 2921a9c..a0c1872 100644 --- a/cycle.ts +++ b/cycle.ts @@ -1,6 +1,9 @@ /** * Returns an infinite iterable that cycles through the given iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/take take} to limit the number of items of the cycled iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/cycle cycle} to cycle the iterable asynchronously. + * * @param iterable The iterable to cycle. * @returns The cycled iterable. * diff --git a/drop.ts b/drop.ts index 72c37a3..9bec0a2 100644 --- a/drop.ts +++ b/drop.ts @@ -1,10 +1,14 @@ /** * Drops the first `limit` items from the iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/drop-while dropWhile} to drop items while a condition is met. + * Use {@linkcode https://jsr.io/@core/iterutil/take take} to take a specific number of items from an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/drop drop} to drop items asynchronously. + * * @param iterable The iterable to drop items from. * @param limit The number of items to drop. It must be 0 or positive safe integer. * @returns The iterable with the first `limit` items dropped. - * @throws {DropLimitError} If `limit` is less than 0 or non safe integer. + * @throws {RangeError} if `limit` is less than 0 or non safe integer. * * @example * ```ts @@ -16,7 +20,12 @@ */ export function drop(iterable: Iterable, limit: number): Iterable { if (limit < 0 || !Number.isSafeInteger(limit)) { - throw new DropLimitError(limit); + throw new RangeError( + `limit must be 0 or positive safe integer, but got ${limit}.`, + ); + } + if (limit === 0) { + return iterable; } return function* () { let i = 0; @@ -27,12 +36,3 @@ export function drop(iterable: Iterable, limit: number): Iterable { } }(); } - -/** - * Error thrown when the 'limit' is negative or not a safe integer. - */ -export class DropLimitError extends Error { - constructor(limit: number) { - super(`The 'limit' must be 0 or positive safe integer, but got ${limit}.`); - } -} diff --git a/drop_test.ts b/drop_test.ts index 85a8151..8a717c8 100644 --- a/drop_test.ts +++ b/drop_test.ts @@ -1,6 +1,6 @@ import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; -import { drop, DropLimitError } from "./drop.ts"; +import { drop } from "./drop.ts"; Deno.test("drop", async (t) => { await t.step("with positive limit", () => { @@ -10,19 +10,20 @@ Deno.test("drop", async (t) => { assertType>>(true); }); - await t.step("with negative limit", () => { - assertThrows( - () => { - drop([0, 1, 2, 3, 4], -2); - }, - DropLimitError, - ); - }); - await t.step("with 0 limit", () => { const result = drop([0, 1, 2, 3, 4], 0); const expected = [0, 1, 2, 3, 4]; assertEquals(Array.from(result), expected); assertType>>(true); }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the limit is not 0 nor positive safe integer", () => { + assertThrows(() => drop([], NaN), RangeError); + assertThrows(() => drop([], Infinity), RangeError); + assertThrows(() => drop([], -Infinity), RangeError); + assertThrows(() => drop([], -1), RangeError); + assertThrows(() => drop([], 1.1), RangeError); + }); + }); }); diff --git a/drop_while.ts b/drop_while.ts index df358af..7ce1578 100644 --- a/drop_while.ts +++ b/drop_while.ts @@ -4,6 +4,10 @@ * 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. * + * Use {@linkcode https://jsr.io/@core/iterutil/drop drop} to drop a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/take take} to take a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/async/drop-while dropWhile} to drop elements asynchronously. + * * @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. @@ -12,17 +16,21 @@ * ```ts * import { dropWhile } from "@core/iterutil/drop-while"; * - * const iter = dropWhile([1, 2, 3, 4, 5], (x) => x < 3); + * const iter = dropWhile( + * [1, 2, 3, 4, 5], + * (v) => v < 3, + * ); * console.log(Array.from(iter)); // [3, 4, 5] * ``` */ export function* dropWhile( iterable: Iterable, - fn: (value: T) => boolean, + fn: (value: T, index: number) => boolean, ): Iterable { let dropping = true; + let index = 0; for (const value of iterable) { - if (dropping && fn(value)) { + if (dropping && fn(value, index++)) { continue; } dropping = false; diff --git a/drop_while_test.ts b/drop_while_test.ts index 9f1bdea..e269a29 100644 --- a/drop_while_test.ts +++ b/drop_while_test.ts @@ -4,22 +4,30 @@ import { dropWhile } from "./drop_while.ts"; Deno.test("dropWhile", async (t) => { await t.step("with some true", () => { - const result = dropWhile([0, 1, 2, 3, 4], (v) => v < 2); - const expected = [2, 3, 4]; + const values: number[] = []; + const indices: number[] = []; + const result = dropWhile([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v < 3; + }); + const expected = [3, 4, 5]; assertEquals(Array.from(result), expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>>(true); }); await t.step("with all true", () => { - const result = dropWhile([0, 1, 2, 3, 4], () => true); + const result = dropWhile([1, 2, 3, 4, 5], () => true); const expected: number[] = []; assertEquals(Array.from(result), expected); assertType>>(true); }); await t.step("with all false", () => { - const result = dropWhile([0, 1, 2, 3, 4], () => false); - const expected = [0, 1, 2, 3, 4]; + const result = dropWhile([1, 2, 3, 4, 5], () => false); + const expected = [1, 2, 3, 4, 5]; assertEquals(Array.from(result), expected); assertType>>(true); }); diff --git a/enumerate.ts b/enumerate.ts index a2b4e62..c6b959e 100644 --- a/enumerate.ts +++ b/enumerate.ts @@ -1,26 +1,53 @@ /** - * Enumerates an iterable. + * Returns an iterable of index-value pairs. + * + * The index starts at `start` and increments by `step` for each value. + * If `start` is not provided, it defaults to `0`. + * If `step` is not provided, it defaults to `1`. + * + * Use {@linkcode https://jsr.io/@core/iterutil/zip zip} to zip iterable with other iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/count count} to generate an infinite sequence of numbers. + * Use {@linkcode https://jsr.io/@core/iterutil/async/enumerate enumerate} to enumerate asynchronously. * * @param iterable The iterable to enumerate. * @param start The starting index. + * @param step The step between indices. * @returns An iterable of index-value pairs. + * @throws {RangeError} if `start` or `step` is not finite or `step` is 0. * * @example * ```ts * import { enumerate } from "@core/iterutil/enumerate"; * - * const iter = enumerate(["a", "b", "c"]); - * console.log(Array.from(iter)); // [[0, "a"], [1, "b"], [2, "c"]] + * const iter1 = enumerate(["a", "b", "c"]); + * console.log(Array.from(iter1)); // [[0, "a"], [1, "b"], [2, "c"]] + * + * const iter2 = enumerate(["a", "b", "c"], 1); + * console.log(Array.from(iter2)); // [[1, "a"], [2, "b"], [3, "c"]] + * + * const iter3 = enumerate(["a", "b", "c"], 1, 2); + * console.log(Array.from(iter3)); // [[1, "a"], [3, "b"], [5, "c"]] * ``` */ -export function* enumerate( +export function enumerate( iterable: Iterable, start: number = 0, step: number = 1, ): Iterable<[number, T]> { - let i = start; - for (const value of iterable) { - yield [i, value]; - i += step; + if (!Number.isFinite(start)) { + throw new RangeError(`start must be finite, but got ${start}.`); + } + if (!Number.isFinite(step)) { + throw new RangeError(`step must be finite, but got ${step}.`); + } + if (step === 0) { + throw new RangeError(`step must not be 0.`); } + return function* () { + let i = start; + for (const value of iterable) { + yield [i, value]; + i += step; + } + }(); } diff --git a/enumerate_test.ts b/enumerate_test.ts index 91d94a9..45e0900 100644 --- a/enumerate_test.ts +++ b/enumerate_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { enumerate } from "./enumerate.ts"; @@ -10,17 +10,63 @@ Deno.test("enumerate", async (t) => { assertType>>(true); }); - await t.step("with start", () => { + await t.step("with positive start", () => { const result = enumerate([0, 1, 2], 1); const expected = [[1, 0], [2, 1], [3, 2]]; assertEquals(Array.from(result), expected); assertType>>(true); }); - await t.step("with start/step", () => { + await t.step("with negative start", () => { + const result = enumerate([0, 1, 2], -1); + const expected = [[-1, 0], [0, 1], [1, 2]]; + assertEquals(Array.from(result), expected); + assertType>>(true); + }); + + await t.step("with float start", () => { + const result = enumerate([0, 1, 2], 1.1); + const expected = [[1.1, 0], [2.1, 1], [3.1, 2]]; + assertEquals(Array.from(result), expected); + assertType>>(true); + }); + + await t.step("with start and positive step", () => { const result = enumerate([0, 1, 2], 1, 2); const expected = [[1, 0], [3, 1], [5, 2]]; assertEquals(Array.from(result), expected); assertType>>(true); }); + + await t.step("with start and negative step", () => { + const result = enumerate([0, 1, 2], 1, -1); + const expected = [[1, 0], [0, 1], [-1, 2]]; + assertEquals(Array.from(result), expected); + assertType>>(true); + }); + + await t.step("with start and float step", () => { + const result = enumerate([0, 1, 2], 1, 0.2); + const expected = [[1, 0], [1.2, 1], [1.4, 2]]; + assertEquals(Array.from(result), expected); + assertType>>(true); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the start is not finite", () => { + assertThrows(() => enumerate([], NaN), RangeError); + assertThrows(() => enumerate([], Infinity), RangeError); + assertThrows(() => enumerate([], -Infinity), RangeError); + }); + + await t.step("if the step is not finite", () => { + assertThrows(() => enumerate([], 0, NaN), RangeError); + assertThrows(() => enumerate([], 0, Infinity), RangeError); + assertThrows(() => enumerate([], 0, -Infinity), RangeError); + }); + + await t.step("if the step is 0", () => { + assertThrows(() => enumerate([], 0, 0), RangeError); + }); + }); }); diff --git a/every.ts b/every.ts index b3ed7e4..99a9342 100644 --- a/every.ts +++ b/every.ts @@ -1,24 +1,38 @@ /** - * Returns true if every element in the iterable satisfies the provided testing function. + * Returns true if every element in the iterable satisfies the provided function. + * Otherwise, returns false. * - * @param iterable The iterable to test. - * @param fn The function to test with. + * The function is called for each element in the iterable until one of them + * returns false. If the function returns false for any element, this function + * returns false immediately and does not iterate over the remaining elements. + * + * If the iterable is empty, this function returns true. + * + * Use {@linkcode https://jsr.io/@core/iterutil/some some} to check if any element satisfies the function. + * Use {@linkcode https://jsr.io/@core/iterutil/async/every every} to check asynchronously. + * + * @param iterable The iterable to check. + * @param fn The function to check with. * @returns True if every element in the iterable satisfies the provided testing function, otherwise false. * * @example * ```ts * import { every } from "@core/iterutil/every"; * - * console.log(every([1, 2, 3], (value) => value > 0)); // true - * console.log(every([1, 2, 3], (value) => value > 1)); // false + * const result = every( + * [1, 2, 3], + * (v) => v > 0 + * ); + * console.log(result); // true * ``` */ export function every( iterable: Iterable, - fn: (value: T) => boolean, + fn: (value: T, index: number) => boolean, ): boolean { + let index = 0; for (const value of iterable) { - if (!fn(value)) { + if (!fn(value, index++)) { return false; } } diff --git a/every_test.ts b/every_test.ts index c0b4a7e..35f91ac 100644 --- a/every_test.ts +++ b/every_test.ts @@ -3,17 +3,36 @@ import { assertType, type IsExact } from "@std/testing/types"; import { every } from "./every.ts"; Deno.test("every", async (t) => { - await t.step("true", () => { - const result = every([1, 2, 3, 4, 5], (v) => v > 0); + await t.step("returns true if all elements satisfy the function", () => { + const values: number[] = []; + const indices: number[] = []; + const result = every([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v < 6; + }); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); assertType>(true); }); - await t.step("false", () => { - const result = every([1, 2, 3, 4, 5], (v) => v > 1); - const expected = false; - assertEquals(result, expected); - assertType>(true); - }); + await t.step( + "returns false if not all elements satisfy the function", + () => { + const values: number[] = []; + const indices: number[] = []; + const result = every([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v < 3; + }); + const expected = false; + assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); + assertType>(true); + }, + ); }); diff --git a/filter.ts b/filter.ts index adb9be0..6d94524 100644 --- a/filter.ts +++ b/filter.ts @@ -1,6 +1,12 @@ /** * Filters an iterable based on a function. * + * Use {@linkcode https://jsr.io/@core/iterutil/compact compact} to remove nullish values. + * Use {@linkcode https://jsr.io/@core/iterutil/compress compress} to remove values based on an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/map map} to transform the values. + * Use {@linkcode https://jsr.io/@core/iterutil/reduce reduce} to reduce the values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/filter filter} to filter asynchronously. + * * @params iterable The iterable to filter. * @params fn The function to filter with. * @returns The filtered iterable. @@ -9,7 +15,10 @@ * ```ts * import { filter } from "@core/iterutil/filter"; * - * const iter = filter([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * const iter = filter( + * [1, 2, 3, 4, 5], + * (v) => v % 2 === 0 + * ); * console.log(Array.from(iter)); // [2, 4] * ``` */ diff --git a/filter_test.ts b/filter_test.ts index 8cca723..76891e7 100644 --- a/filter_test.ts +++ b/filter_test.ts @@ -5,10 +5,10 @@ import { filter } from "./filter.ts"; Deno.test("filter", () => { const values: number[] = []; const indices: number[] = []; - const result = filter([1, 2, 3, 4, 5], (value, index) => { - values.push(value); + const result = filter([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return value % 2 === 0; + return v % 2 === 0; }); const expected = [2, 4]; assertEquals(Array.from(result), expected); diff --git a/find.ts b/find.ts index 643ca6d..860bc6c 100644 --- a/find.ts +++ b/find.ts @@ -2,6 +2,11 @@ * Returns the first element in the iterable that satisfies the provided * testing function. Otherwise, undefined is returned. * + * Use {@linkcode https://jsr.io/@core/iterutil/first first} to get the first element. + * Use {@linkcode https://jsr.io/@core/iterutil/last last} to get the last element. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter elements. + * Use {@linkcode https://jsr.io/@core/iterutil/async/find find} to find elements asynchronously. + * * @param iterable The iterable to search. * @param fn The function to test with. * @returns The first element that satisfies the provided testing function. @@ -10,7 +15,10 @@ * ```ts * import { find } from "@core/iterutil/find"; * - * const value = find([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * const value = find( + * [1, 2, 3, 4, 5], + * (v) => v % 2 === 0, + * ); * console.log(value); // 2 * ``` */ diff --git a/find_test.ts b/find_test.ts index 975e9ee..68a2009 100644 --- a/find_test.ts +++ b/find_test.ts @@ -6,10 +6,10 @@ Deno.test("find", async (t) => { await t.step("found", () => { const values: number[] = []; const indices: number[] = []; - const result = find([1, 2, 3, 4, 5], (value, index) => { - values.push(value); + const result = find([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return value % 2 === 0; + return v % 2 === 0; }); const expected = 2; assertEquals(result, expected); diff --git a/first.ts b/first.ts index 894d76d..f01c251 100644 --- a/first.ts +++ b/first.ts @@ -1,6 +1,10 @@ /** * Returns the first element of an iterable. If the iterable is empty, returns `undefined`. * + * Use {@linkcode https://jsr.io/@core/iterutil/last last} to get the last element of an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/find find} to get the first element that matches a predicate. + * Use {@linkcode https://jsr.io/@core/iterutil/async/first first} to get the first element asynchronously. + * * @param iterable The iterable to get the first element from. * @returns The first element of the iterable, or `undefined` if the iterable is empty. * @@ -8,8 +12,8 @@ * ```ts * import { first } from "@core/iterutil/first"; * - * const value = first([1, 2, 3]); - * console.log(value); // 1 + * const result = first([1, 2, 3]); + * console.log(result); // 1 * ``` */ export function first(iterable: Iterable): T | undefined { diff --git a/flat_map.ts b/flat_map.ts index 77f9d05..d765438 100644 --- a/flat_map.ts +++ b/flat_map.ts @@ -1,6 +1,11 @@ /** * Maps each value in an iterable to an iterable, then flattens the result. * + * Use {@linkcode https://jsr.io/@core/iterutil/map map} to map values to iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/flatten flatten} to flatten an iterable of iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/async/flat-map flatMap} to flat map an iterable asynchronously. + * * @param iterable The iterable to flat map. * @param fn The function to map with. * @returns The flat mapped iterable. @@ -9,7 +14,10 @@ * ```ts * import { flatMap } from "@core/iterutil/flat-map"; * - * const iter = flatMap([1, 2, 3], (value) => [value, value]); + * const iter = flatMap( + * [1, 2, 3], + * (v) => [v, v], + * ); * console.log(Array.from(iter)); // [1, 1, 2, 2, 3, 3] * ``` */ diff --git a/flat_map_test.ts b/flat_map_test.ts index 9815c38..5488ac6 100644 --- a/flat_map_test.ts +++ b/flat_map_test.ts @@ -6,10 +6,10 @@ Deno.test("flatMap", async (t) => { await t.step("single nest", () => { const values: number[] = []; const indices: number[] = []; - const result = flatMap([1, 2, 3, 4, 5], (value, index) => { - values.push(value); + const result = flatMap([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return [value, value]; + return [v, v]; }); const expected = [1, 1, 2, 2, 3, 3, 4, 4, 5, 5]; assertEquals(Array.from(result), expected); diff --git a/flatten.ts b/flatten.ts index 13ff76d..08c40a8 100644 --- a/flatten.ts +++ b/flatten.ts @@ -1,6 +1,11 @@ /** * Flattens an iterable of iterables into a single iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/map map} to map values to iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/flat-map flatMap} to flat map an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/flatten flatten} to flatten an iterable of iterables asynchronously. + * * @param iterable The iterable to flatten. * @returns The flattened iterable. * diff --git a/for_each.ts b/for_each.ts index c75014f..2788a68 100644 --- a/for_each.ts +++ b/for_each.ts @@ -1,6 +1,10 @@ /** * Calls a function for each value in an iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/map map} to transform values. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/async/for-each forEach} to iterate asynchronously. + * * @param iterable The iterable to iterate over. * @param fn The function to call for each value. * @@ -8,7 +12,7 @@ * ```ts * import { forEach } from "@core/iterutil/for-each"; * - * forEach([1, 2, 3], console.log); + * forEach([1, 2, 3], (v) => console.log(v)); * // 1 * // 2 * // 3 diff --git a/for_each_test.ts b/for_each_test.ts index 75326bd..c9da733 100644 --- a/for_each_test.ts +++ b/for_each_test.ts @@ -4,8 +4,8 @@ import { forEach } from "./for_each.ts"; Deno.test("forEach", () => { const values: number[] = []; const indices: number[] = []; - forEach([1, 2, 3, 4, 5], (value, index) => { - values.push(value); + forEach([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); }); assertEquals(values, [1, 2, 3, 4, 5]); diff --git a/iter.ts b/iter.ts index 4ac7d22..8f72727 100644 --- a/iter.ts +++ b/iter.ts @@ -1,8 +1,10 @@ /** - * Converts an iterable to an iterator. + * Converts an iterable to an iterable iterator. + * + * Use {@linkcode https://jsr.io/@core/iterutil/async/iter iter} for `AsyncIterableIterator`. * * @param iterable The iterable to convert. - * @returns The iterator. + * @returns The iterable iterator. * * @example * ```ts diff --git a/last.ts b/last.ts index f27fb8a..3577f1d 100644 --- a/last.ts +++ b/last.ts @@ -1,6 +1,10 @@ /** * Returns the last element of an iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/first first} to get the first element. + * Use {@linkcode https://jsr.io/@core/iterutil/find find} to find an element. + * Use {@linkcode https://jsr.io/@core/iterutil/async/last last} to get the last element asynchronously. + * * @param iterable The iterable to get the last element of. * @returns The last element of the iterable, or `undefined` if the iterable is empty. * diff --git a/map.ts b/map.ts index e1ca6e1..ab93f92 100644 --- a/map.ts +++ b/map.ts @@ -1,6 +1,12 @@ /** * Maps an iterable with a function. * + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter values. + * Use {@linkcode https://jsr.io/@core/iterutil/for-each forEach} to call a function for each value. + * Use {@linkcode https://jsr.io/@core/iterutil/flat-map flatMap} to map and flatten the result. + * Use {@linkcode https://jsr.io/@core/iterutil/flatten flatten} to flatten an iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/map map} to map asynchronously. + * * @param iterable The iterable to map. * @param fn The function to map with. * @returns The mapped iterable. @@ -9,7 +15,10 @@ * ```ts * import { map } from "@core/iterutil/map"; * - * const iter = map([1, 2, 3], (value) => value * 2); + * const iter = map( + * [1, 2, 3], + * (v) => v * 2, + * ); * console.log(Array.from(iter)); // [2, 4, 6] * ``` */ diff --git a/map_test.ts b/map_test.ts index 6d68e80..d83b624 100644 --- a/map_test.ts +++ b/map_test.ts @@ -5,10 +5,10 @@ import { map } from "./map.ts"; Deno.test("map", () => { const values: number[] = []; const indices: number[] = []; - const result = map([1, 2, 3, 4, 5], (value, index) => { - values.push(value); + const result = map([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return value * 2; + return v * 2; }); const expected = [2, 4, 6, 8, 10]; assertEquals(Array.from(result), expected); diff --git a/pairwise.ts b/pairwise.ts index 248abdf..34d4b5c 100644 --- a/pairwise.ts +++ b/pairwise.ts @@ -3,6 +3,8 @@ * * When the input iterable has a finite number of items `n`, the output iterable will have `n - 1` items. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/pairwise pairwise} to pair elements from an iterable asynchronously. + * * @param iterable The iterable to pair elements from. * @returns The paired iterable. * diff --git a/partition.ts b/partition.ts index c92293f..737c126 100644 --- a/partition.ts +++ b/partition.ts @@ -1,6 +1,8 @@ /** * Partitions an iterable into two arrays based on a selector function. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/partition partition} to partition asynchronously. + * * @param iterable The iterable to partition. * @param selector The function to partition with. * @returns The partitioned arrays. @@ -9,7 +11,10 @@ * ```ts * import { partition } from "@core/iterutil/partition"; * - * const [even, odd] = partition([1, 2, 3, 4, 5], (value) => value % 2 === 0); + * const [even, odd] = partition( + * [1, 2, 3, 4, 5], + * (v) => v % 2 === 0 + * ); * console.log(even); // [2, 4] * console.log(odd); // [1, 3, 5] * ``` diff --git a/partition_test.ts b/partition_test.ts index c6fc528..d49bc26 100644 --- a/partition_test.ts +++ b/partition_test.ts @@ -6,10 +6,10 @@ Deno.test("partition", async (t) => { await t.step("with non empty iterable", () => { const values: number[] = []; const indices: number[] = []; - const [left, right] = partition([1, 2, 3, 4, 5], (value, index) => { - values.push(value); + const [left, right] = partition([1, 2, 3, 4, 5], (v, index) => { + values.push(v); indices.push(index); - return value % 2 === 0; + return v % 2 === 0; }); assertEquals(left, [2, 4]); assertEquals(right, [1, 3, 5]); diff --git a/range.ts b/range.ts index ae08313..fb7ed0c 100644 --- a/range.ts +++ b/range.ts @@ -1,28 +1,19 @@ /** * Generate a range of numbers. * - * @param stop The end of the range. - * @returns The range of numbers. - * - * @example - * ```ts - * import { range } from "@core/iterutil/range"; - * - * console.log(Array.from(range(3))); // [0, 1, 2] - * ``` - */ -export function range(stop: number): Iterable; -/** - * Generate a range of numbers. + * Use {@linkcode https://jsr.io/@core/iterutil/count count} to generate an infinite range. * * @param start The start of the range. * @param stop The end of the range. - * @returns The range of numbers. + * @param step The step between each number in the range. + * @returns The range iterable. + * @throws {RangeError} if `start`, `stop`, or `step` is not finite or `step` is 0, or `start` is greater than `stop` for positive step or `start` is less than `stop` for negative step. * * @example * ```ts * import { range } from "@core/iterutil/range"; * + * console.log(Array.from(range(1, 3))); // [1, 2, 3] * console.log(Array.from(range(1, 6, 2))); // [1, 3, 5] * ``` */ @@ -30,17 +21,35 @@ export function range( start: number, stop: number, step?: number, -): Iterable; -export function* range( - startOrStop: number, - stop?: number, - step: number = 1, ): Iterable { - if (!stop) { - yield* range(0, startOrStop); - } else { - for (let i = startOrStop; i < stop; i += step) { - yield i; - } + step ??= start > stop ? -1 : 1; + if (!Number.isFinite(start)) { + throw new RangeError(`start must be finite, but got ${start}.`); + } + if (!Number.isFinite(step)) { + throw new RangeError(`step must be finite, but got ${step}.`); + } + if (step === 0) { + throw new RangeError(`step must not be 0.`); } + if (!Number.isFinite(stop)) { + throw new RangeError(`stop must be finite, but got ${stop}.`); + } + if (step > 0 && start > stop) { + throw new RangeError(`start must be less than stop for positive step.`); + } + if (step < 0 && start < stop) { + throw new RangeError(`start must be greater than stop for negative step.`); + } + return function* () { + if (step >= 0) { + for (let i = start; i <= stop; i += step) { + yield i; + } + } else { + for (let i = start; i >= stop; i += step) { + yield i; + } + } + }(); } diff --git a/range_test.ts b/range_test.ts index ddce440..5f24330 100644 --- a/range_test.ts +++ b/range_test.ts @@ -1,23 +1,139 @@ -import { assertEquals } from "@std/assert"; +import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; import { range } from "./range.ts"; Deno.test("range", async (t) => { - await t.step("default", () => { - const result = range(5); - assertEquals(Array.from(result), [0, 1, 2, 3, 4]); - assertType>>(true); - }); + await t.step("without step", async (t) => { + await t.step("with 0 to 0", () => { + const result = range(0, 0); + assertEquals(Array.from(result), [0]); + assertType>>(true); + }); + + await t.step("with 0 to 2", () => { + const result = range(0, 2); + assertEquals(Array.from(result), [0, 1, 2]); + assertType>>(true); + }); + + await t.step("with 0 to -2", () => { + const result = range(0, -2); + assertEquals(Array.from(result), [0, -1, -2]); + assertType>>(true); + }); + + await t.step("with 2 to 0", () => { + const result = range(2, 0); + assertEquals(Array.from(result), [2, 1, 0]); + assertType>>(true); + }); + + await t.step("with -2 to 0", () => { + const result = range(-2, 0); + assertEquals(Array.from(result), [-2, -1, 0]); + assertType>>(true); + }); - await t.step("with start/stop", () => { - const result = range(1, 5); - assertEquals(Array.from(result), [1, 2, 3, 4]); - assertType>>(true); + await t.step("with -2 to 2", () => { + const result = range(-2, 2); + assertEquals(Array.from(result), [-2, -1, 0, 1, 2]); + assertType>>(true); + }); + + await t.step("with 2 to -2", () => { + const result = range(2, -2); + assertEquals(Array.from(result), [2, 1, 0, -1, -2]); + assertType>>(true); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the start is not finite", () => { + assertThrows(() => range(NaN, 0), RangeError); + assertThrows(() => range(Infinity, 0), RangeError); + assertThrows(() => range(-Infinity, 0), RangeError); + }); + + await t.step("if the end is not finite", () => { + assertThrows(() => range(0, NaN), RangeError); + assertThrows(() => range(0, Infinity), RangeError); + assertThrows(() => range(0, -Infinity), RangeError); + }); + }); }); - await t.step("with start/stop/step", () => { - const result = range(1, 10, 2); - assertEquals(Array.from(result), [1, 3, 5, 7, 9]); - assertType>>(true); + await t.step("with step", async (t) => { + await t.step("with 0 to 0", () => { + const result = range(0, 0, 2); + assertEquals(Array.from(result), [0]); + assertType>>(true); + }); + + await t.step("with 0 to 2", () => { + const result = range(0, 2, 2); + assertEquals(Array.from(result), [0, 2]); + assertType>>(true); + }); + + await t.step("with 0 to -2", () => { + const result = range(0, -2, -2); + assertEquals(Array.from(result), [0, -2]); + assertType>>(true); + }); + + await t.step("with 2 to 0", () => { + const result = range(2, 0, -2); + assertEquals(Array.from(result), [2, 0]); + assertType>>(true); + }); + + await t.step("with -2 to 0", () => { + const result = range(-2, 0, 2); + assertEquals(Array.from(result), [-2, 0]); + assertType>>(true); + }); + + await t.step("with -2 to 2", () => { + const result = range(-2, 2, 2); + assertEquals(Array.from(result), [-2, 0, 2]); + assertType>>(true); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the start is not finite", () => { + assertThrows(() => range(NaN, 0, 2), RangeError); + assertThrows(() => range(Infinity, 0, 2), RangeError); + assertThrows(() => range(-Infinity, 0, 2), RangeError); + }); + + await t.step("if the end is not finite", () => { + assertThrows(() => range(0, NaN, 2), RangeError); + assertThrows(() => range(0, Infinity, 2), RangeError); + assertThrows(() => range(0, -Infinity, 2), RangeError); + }); + + await t.step("if the step is not finite", () => { + assertThrows(() => range(0, 0, NaN), RangeError); + assertThrows(() => range(0, 0, Infinity), RangeError); + assertThrows(() => range(0, 0, -Infinity), RangeError); + }); + + await t.step("if the step is 0", () => { + assertThrows(() => range(0, 0, 0), RangeError); + }); + + await t.step( + "if the start is greater than stop for positive step", + () => { + assertThrows(() => range(1, 0, 1), RangeError); + }, + ); + + await t.step( + "if the start is less than stop for negative step", + () => { + assertThrows(() => range(0, 1, -1), RangeError); + }, + ); + }); }); }); diff --git a/reduce.ts b/reduce.ts index 8a63d7a..203e674 100644 --- a/reduce.ts +++ b/reduce.ts @@ -1,6 +1,12 @@ /** * Reduces an iterable into a single value. * + * The first value of the iterable is used as the initial value. + * + * Use {@linkcode https://jsr.io/@core/iterutil/map map} to transform values of the iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter values of the iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/reduce reduce} to reduce an iterable asynchronously. + * * @param iterable The iterable to reduce. * @param fn The function to reduce with. * @returns The reduced value. @@ -9,8 +15,11 @@ * ```ts * import { reduce } from "@core/iterutil/reduce"; * - * const sum = reduce([1, 2, 3, 4, 5], (acc, value) => acc + value); - * console.log(sum); // 15 + * const result = reduce( + * [1, 2, 3, 4, 5], + * (a, v) => a + v, + * ); + * console.log(result); // 15 * ``` */ export function reduce( @@ -21,6 +30,10 @@ export function reduce( /** * Reduces an iterable into a single value. * + * Use {@linkcode https://jsr.io/@core/iterutil/map map} to transform values of the iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/filter filter} to filter values of the iterable. + * Use {@linkcode https://jsr.io/@core/iterutil/async/reduce reduce} to reduce an iterable asynchronously. + * * @param iterable The iterable to reduce. * @param fn The function to reduce with. * @param initial The initial value to start reducing with. @@ -30,8 +43,12 @@ export function reduce( * ```ts * import { reduce } from "@core/iterutil/reduce"; * - * const joined = reduce([1, 2, 3, 4, 5], (acc, value) => acc + value.toString(), ""); - * console.log(joined); // 12345 + * const result = reduce( + * [1, 2, 3, 4, 5], + * (a, v) => a + v, + * "", + * ); + * console.log(result); // 12345 * ``` */ export function reduce( diff --git a/some.ts b/some.ts index 4b5cf88..be16348 100644 --- a/some.ts +++ b/some.ts @@ -1,24 +1,39 @@ /** * Returns true if at least one element in the iterable satisfies the provided + * function. Otherwise, returns false. + * + * The function is called for each element in the iterable until one of them + * returns true. If the function returns true for any element, this function + * returns true immediately and does not iterate over the remaining elements. + * + * If the iterable is empty, this function returns false. + * + * Use {@linkcode https://jsr.io/@core/iterutil/every every} to check if every + * element satisfies the provided function. + * Use {@linkcode https://jsr.io/@core/iterutil/async/some some} to check asynchronously. * * @param iterable The iterable to check. * @param fn The function to check with. - * @returns True if at least one element satisfies the provided function. + * @returns True if at least one element satisfies the provided function, otherwise false. * * @example * ```ts * import { some } from "@core/iterutil/some"; * - * console.log(some([1, 2, 3], (value) => value % 2 === 0)); // true - * console.log(some([1, 3, 5], (value) => value % 2 === 0)); // false + * const result = some( + * [1, 2, 3], + * (v) => v % 2 === 0, + * ); + * console.log(result); // true * ``` */ export function some( iterable: Iterable, - fn: (value: T) => boolean, + fn: (value: T, index: number) => boolean, ): boolean { + let index = 0; for (const value of iterable) { - if (fn(value)) { + if (fn(value, index++)) { return true; } } diff --git a/some_test.ts b/some_test.ts index c7d3d01..7d93d2f 100644 --- a/some_test.ts +++ b/some_test.ts @@ -3,17 +3,36 @@ import { assertType, type IsExact } from "@std/testing/types"; import { some } from "./some.ts"; Deno.test("some", async (t) => { - await t.step("true", () => { - const result = some([1, 2, 3, 4, 5], (v) => v > 4); + await t.step("returns true if some elements satisfy the function", () => { + const values: number[] = []; + const indices: number[] = []; + const result = some([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v > 2; + }); const expected = true; assertEquals(result, expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>(true); }); - await t.step("false", () => { - const result = some([1, 2, 3, 4, 5], (v) => v > 5); - const expected = false; - assertEquals(result, expected); - assertType>(true); - }); + await t.step( + "returns false if no elements satisfy the function", + () => { + const values: number[] = []; + const indices: number[] = []; + const result = some([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v > 5; + }); + const expected = false; + assertEquals(result, expected); + assertEquals(values, [1, 2, 3, 4, 5]); + assertEquals(indices, [0, 1, 2, 3, 4]); + assertType>(true); + }, + ); }); diff --git a/take.ts b/take.ts index 3194679..a3471a5 100644 --- a/take.ts +++ b/take.ts @@ -1,10 +1,16 @@ /** * Takes the first `limit` items from the iterable. * + * Note that it will stop consuming the iterable once `limit` items are taken. + * + * Use {@linkcode https://jsr.io/@core/iterutil/take-while takeWhile} to take items while the predicate returns true. + * Use {@linkcode https://jsr.io/@core/iterutil/drop drop} to drop items from the beginning. + * Use {@linkcode https://jsr.io/@core/iterutil/async/take take} to take items asynchronously. + * * @param iterable The iterable to take items from. * @param limit The number of items to take. It must be 0 or positive safe integer. * @returns The iterable with the first `limit` items taken. - * @throws {TakeLimitError} If `limit` is less than 0 or non safe integer. + * @throws {RangeError} if `limit` is less than 0 or non safe integer. * * @example * ```ts @@ -16,24 +22,20 @@ */ export function take(iterable: Iterable, limit: number): Iterable { if (limit < 0 || !Number.isSafeInteger(limit)) { - throw new TakeLimitError(limit); + throw new RangeError( + `limit must be 0 or positive safe integer, but got ${limit}.`, + ); + } + if (limit === 0) { + return []; } return function* () { - let i = 0; + let i = 1; for (const item of iterable) { + yield item; if (i++ >= limit) { break; } - yield item; } }(); } - -/** - * Error thrown when the 'limit' is negative or not a safe integer. - */ -export class TakeLimitError extends Error { - constructor(limit: number) { - super(`The 'limit' must be 0 or positive safe integer, but got ${limit}.`); - } -} diff --git a/take_test.ts b/take_test.ts index ddac214..607f2cb 100644 --- a/take_test.ts +++ b/take_test.ts @@ -1,6 +1,7 @@ import { assertEquals, assertThrows } from "@std/assert"; import { assertType, type IsExact } from "@std/testing/types"; -import { take, TakeLimitError } from "./take.ts"; +import { iter } from "./iter.ts"; +import { take } from "./take.ts"; Deno.test("take", async (t) => { await t.step("with positive limit", () => { @@ -10,19 +11,30 @@ Deno.test("take", async (t) => { assertType>>(true); }); - await t.step("with negative limit", () => { - assertThrows( - () => { - take([0, 1, 2, 3, 4], -2); - }, - TakeLimitError, - ); - }); - await t.step("with 0 limit", () => { const result = take([0, 1, 2, 3, 4], 0); const expected: number[] = []; assertEquals(Array.from(result), expected); assertType>>(true); }); + + await t.step("will stop consuming once limit items is taken", () => { + const it = iter([0, 1, 2, 3, 4]); + const result = take(it, 3); + const expected: number[] = [0, 1, 2]; + assertEquals(Array.from(result), expected); + assertType>>(true); + // Ensure the iterator is NOT fully consumed + assertEquals(Array.from(it), [3, 4]); + }); + + await t.step("throws RangeError", async (t) => { + await t.step("if the limit is not 0 nor positive safe integer", () => { + assertThrows(() => take([], NaN), RangeError); + assertThrows(() => take([], Infinity), RangeError); + assertThrows(() => take([], -Infinity), RangeError); + assertThrows(() => take([], -1), RangeError); + assertThrows(() => take([], 1.1), RangeError); + }); + }); }); diff --git a/take_while.ts b/take_while.ts index 3c5cd22..3edf5d6 100644 --- a/take_while.ts +++ b/take_while.ts @@ -1,6 +1,10 @@ /** * Takes elements from the iterable while the predicate is true. * + * Use {@linkcode https://jsr.io/@core/iterutil/take take} to take a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/drop drop} to drop a specific number of elements. + * Use {@linkcode https://jsr.io/@core/iterutil/async/take-while takeWhile} to take elements asynchronously. + * * @param iterable The iterable to take elements from. * @param fn The predicate to take elements with. * @returns The taken iterable. @@ -9,7 +13,10 @@ * ```ts * import { takeWhile } from "@core/iterutil/take-while"; * - * const iter = takeWhile([1, 2, 3, 4, 5], (value) => value < 4); + * const iter = takeWhile( + * [1, 2, 3, 4, 5], + * (v) => v < 4, + * ); * console.log(Array.from(iter)); // [1, 2, 3] * ``` */ diff --git a/take_while_test.ts b/take_while_test.ts index b90e64a..203ee53 100644 --- a/take_while_test.ts +++ b/take_while_test.ts @@ -4,21 +4,29 @@ import { takeWhile } from "./take_while.ts"; Deno.test("takeWhile", async (t) => { await t.step("with some true", () => { - const result = takeWhile([0, 1, 2, 3, 4], (v) => v < 2); - const expected = [0, 1]; + const values: number[] = []; + const indices: number[] = []; + const result = takeWhile([1, 2, 3, 4, 5], (v, index) => { + values.push(v); + indices.push(index); + return v < 3; + }); + const expected = [1, 2]; assertEquals(Array.from(result), expected); + assertEquals(values, [1, 2, 3]); + assertEquals(indices, [0, 1, 2]); assertType>>(true); }); await t.step("with all true", () => { - const result = takeWhile([0, 1, 2, 3, 4], () => true); - const expected = [0, 1, 2, 3, 4]; + const result = takeWhile([1, 2, 3, 4, 5], () => true); + const expected = [1, 2, 3, 4, 5]; assertEquals(Array.from(result), expected); assertType>>(true); }); await t.step("with all false", () => { - const result = takeWhile([0, 1, 2, 3, 4], () => false); + const result = takeWhile([1, 2, 3, 4, 5], () => false); const expected: number[] = []; assertEquals(Array.from(result), expected); assertType>>(true); diff --git a/uniq.ts b/uniq.ts index c4bf01e..8f0ab24 100644 --- a/uniq.ts +++ b/uniq.ts @@ -1,6 +1,8 @@ /** * Returns an iterable that yields the unique elements of the input iterable. * + * Use {@linkcode https://jsr.io/@core/iterutil/async/uniq uniq} to get the unique elements asynchronously. + * * @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. @@ -18,21 +20,22 @@ * import { uniq } from "@core/iterutil/uniq"; * * const iter = uniq( - * [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], - * (v) => Math.floor(v / 10), + * [1, 2, 3, 4, 5, 6, 7, 8, 9], + * (v) => v % 4, * ); - * console.log(Array.from(iter)); // [1, 10, 20, 30] + * console.log(Array.from(iter)); // [1, 2, 3, 4] * ``` */ export function* uniq( iterable: Iterable, - identify: (v: T) => unknown = (v) => v, + identify: (value: T, index: number) => unknown = (v) => v, ): Iterable { const set = new Set(); + let index = 0; for (const item of iterable) { - const identity = identify(item); - if (!set.has(identity)) { - set.add(identity); + const id = identify(item, index++); + if (!set.has(id)) { + set.add(id); yield item; } } diff --git a/uniq_test.ts b/uniq_test.ts index 68d4ce8..9100ec0 100644 --- a/uniq_test.ts +++ b/uniq_test.ts @@ -4,19 +4,31 @@ import { uniq } from "./uniq.ts"; Deno.test("uniq", async (t) => { await t.step("default", () => { - 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]; + const result = uniq([1, 2, 2, 3, 3, 3]); + const expected = [1, 2, 3]; assertEquals(Array.from(result), expected); assertType>>(true); }); await t.step("with identify", () => { + const values: number[] = []; + const indices: number[] = []; + const identities: number[] = []; const result = uniq( - [1, 2, 3, 1, 2, 3, 10, 20, 30, 11, 21, 31], - (v) => Math.floor(v / 10), + [1, 2, 3, 4, 5, 6, 7, 8, 9], + (v, index) => { + values.push(v); + indices.push(index); + const id = v % 4; + identities.push(id); + return id; + }, ); - const expected = [1, 10, 20, 30]; + const expected = [1, 2, 3, 4]; assertEquals(Array.from(result), expected); + assertEquals(values, [1, 2, 3, 4, 5, 6, 7, 8, 9]); + assertEquals(indices, [0, 1, 2, 3, 4, 5, 6, 7, 8]); + assertEquals(identities, [1, 2, 3, 0, 1, 2, 3, 0, 1]); assertType>>(true); }); }); diff --git a/zip.ts b/zip.ts index 72207a1..8514a59 100644 --- a/zip.ts +++ b/zip.ts @@ -1,6 +1,16 @@ /** * Zips multiple iterables into a single iterable. * + * The resulting iterable will yield arrays of elements from the input iterables. + * The first array will contain the first element of each input iterable, the second array will contain the second element of each input iterable, and so on. + * + * If the input iterables have different lengths, the resulting iterable will stop when the shortest input iterable is exhausted. + * The remaining elements from the longer input iterables will be ignored. + * + * Use {@linkcode https://jsr.io/@core/iterutil/chain chain} to chain iterables. + * Use {@linkcode https://jsr.io/@core/iterutil/enumerate enumerate} to zip with indices. + * Use {@linkcode https://jsr.io/@core/iterutil/async/zip async zip} to zip asynchronously.;w + * * @param iterables The iterables to zip. * @returns The zipped iterable. * @@ -8,26 +18,36 @@ * ```ts * import { zip } from "@core/iterutil/zip"; * - * const iter = zip([1, 2, 3], ["a", "b", "c"]); - * console.log(Array.from(iter)); // [[1, "a"], [2, "b"], [3, "c"]] + * const iter = zip( + * [1, 2, 3], + * ["a", "b", "c"], + * [true, false, true], + * ); + * console.log(Array.from(iter)); // [[1, "a", true], [2, "b", false], [3, "c", true]] * ``` */ -export function* zip[]>( +export function* zip< + U extends readonly [ + Iterable, + Iterable, + ...Iterable[], + ], +>( ...iterables: U ): Iterable> { - const its = iterables.map((iterable) => iterable[Symbol.iterator]()); + const iterators = iterables.map((it) => it[Symbol.iterator]()); while (true) { - const rs = its.map((it) => it.next()); - if (rs.find(({ done }) => !!done)) { + const results = iterators.map((it) => it.next()); + if (results.find(({ done }) => !!done)) { break; } - yield rs.map(({ value }) => value) as Zip; + yield results.map(({ value }) => value) as Zip; } } /** * @internal */ -export type Zip[]> = { +export type Zip[]> = { [P in keyof T]: T[P] extends Iterable ? U : never; };