From 7787c45500b44829225acd584af02781fc25962e Mon Sep 17 00:00:00 2001 From: Tomas Zijdemans Date: Tue, 28 Apr 2026 13:17:59 +0200 Subject: [PATCH] feat(collections/unstable): add `zip` that accepts `Iterable` Mirrors the iterable-based `interleave` API so `zip` can accept any `Iterable`, not just arrays. Non-array iterables are consumed eagerly via `Array.from`, then the existing index-based zip-to-shortest logic runs. Adds a two-iterable fast path matching `interleave`. Lives alongside stable `zip` to bake before any decision about replacing the stable version. Made-with: Cursor --- collections/deno.json | 1 + collections/unstable_zip.ts | 84 ++++++++++++++ collections/unstable_zip_test.ts | 191 +++++++++++++++++++++++++++++++ 3 files changed, 276 insertions(+) create mode 100644 collections/unstable_zip.ts create mode 100644 collections/unstable_zip_test.ts diff --git a/collections/deno.json b/collections/deno.json index 42cad94e11a5..4624f3e92923 100644 --- a/collections/deno.json +++ b/collections/deno.json @@ -65,6 +65,7 @@ "./unstable-sum-of": "./unstable_sum_of.ts", "./unstable-take-last-while": "./unstable_take_last_while.ts", "./unstable-take-while": "./unstable_take_while.ts", + "./unstable-zip": "./unstable_zip.ts", "./unzip": "./unzip.ts", "./without-all": "./without_all.ts", "./zip": "./zip.ts" diff --git a/collections/unstable_zip.ts b/collections/unstable_zip.ts new file mode 100644 index 000000000000..c595833ad956 --- /dev/null +++ b/collections/unstable_zip.ts @@ -0,0 +1,84 @@ +// Copyright 2018-2026 the Deno authors. MIT license. +// This module is browser compatible. + +/** + * Builds N-tuples of elements from the given N iterables with matching + * indices, stopping when the shortest iterable is exhausted. All input + * iterables are consumed eagerly. + * + * @experimental **UNSTABLE**: New API, yet to be vetted. + * + * @typeParam T The tuple of element types in the input iterables. + * + * @param iterables The iterables to zip. + * + * @returns A new array containing N-tuples of elements from the given + * iterables. + * + * @example Basic usage + * ```ts + * import { zip } from "@std/collections/unstable-zip"; + * import { assertEquals } from "@std/assert"; + * + * const numbers = [1, 2, 3, 4]; + * const letters = ["a", "b", "c", "d"]; + * const pairs = zip(numbers, letters); + * + * assertEquals( + * pairs, + * [ + * [1, "a"], + * [2, "b"], + * [3, "c"], + * [4, "d"], + * ], + * ); + * ``` + * + * @example With iterables + * ```ts + * import { zip } from "@std/collections/unstable-zip"; + * import { assertEquals } from "@std/assert"; + * + * assertEquals( + * zip(new Set([1, 2, 3]), ["a", "b", "c"]), + * [[1, "a"], [2, "b"], [3, "c"]], + * ); + * ``` + */ +export function zip( + ...iterables: { [K in keyof T]: Iterable } +): T[] { + const arrayCount = iterables.length; + if (arrayCount === 0) return []; + + const arrays = iterables.map((it) => Array.isArray(it) ? it : Array.from(it)); + + let minLength = arrays[0]!.length; + for (let i = 1; i < arrayCount; ++i) { + const len = arrays[i]!.length; + if (len < minLength) minLength = len; + } + + const result: T[] = new Array(minLength); + + // Fast path for two iterables + if (arrayCount === 2) { + const a = arrays[0]!; + const b = arrays[1]!; + for (let i = 0; i < minLength; ++i) { + result[i] = [a[i], b[i]] as T; + } + return result; + } + + for (let i = 0; i < minLength; ++i) { + const tuple: unknown[] = new Array(arrayCount); + for (let j = 0; j < arrayCount; ++j) { + tuple[j] = arrays[j]![i]; + } + result[i] = tuple as T; + } + + return result; +} diff --git a/collections/unstable_zip_test.ts b/collections/unstable_zip_test.ts new file mode 100644 index 000000000000..59e35b3bdb5b --- /dev/null +++ b/collections/unstable_zip_test.ts @@ -0,0 +1,191 @@ +// Copyright 2018-2026 the Deno authors. MIT license. + +import { assertEquals } from "@std/assert"; +import { zip } from "./unstable_zip.ts"; + +function zip1Test( + input: [Array], + expected: Array<[T]>, + message?: string, +) { + const actual = zip(...input); + assertEquals(actual, expected, message); +} + +assertEquals(zip([]), []); + +Deno.test({ + name: "zip() handles one array", + fn() { + zip1Test([ + [1, 2, 3], + ], [[1], [2], [3]]); + }, +}); + +function zipTest( + input: [ReadonlyArray, ReadonlyArray], + expected: Array<[T, U]>, + message?: string, +) { + const actual = zip(...input); + assertEquals(actual, expected, message); +} + +function zip3Test( + input: [Array, Array, Array], + expected: Array<[T, U, V]>, + message?: string, +) { + const actual = zip(...input); + assertEquals(actual, expected, message); +} + +Deno.test({ + name: "zip() handles three arrays", + fn() { + zip3Test([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ], [[1, 4, 7], [2, 5, 8], [3, 6, 9]]); + }, +}); + +Deno.test({ + name: "zip() handles three arrays when the first is the shortest", + fn() { + zip3Test([ + [1, 2], + [4, 5, 6], + [7, 8, 9], + ], [[1, 4, 7], [2, 5, 8]]); + }, +}); + +Deno.test({ + name: "zip() handles no mutation", + fn() { + const arrayA = [1, 4, 5]; + const arrayB = ["foo", "bar"]; + zip(arrayA, arrayB); + + assertEquals(arrayA, [1, 4, 5]); + assertEquals(arrayB, ["foo", "bar"]); + }, +}); + +Deno.test({ + name: "zip() handles empty input", + fn() { + zipTest( + [[], []], + [], + ); + zipTest( + [[1, 2, 3], []], + [], + ); + zipTest( + [[], [{}, []]], + [], + ); + assertEquals(zip(), []); + }, +}); + +Deno.test({ + name: "zip() handles same length", + fn() { + zipTest( + [ + [1, 4, 5], + ["foo", "bar", "lorem"], + ], + [ + [1, "foo"], + [4, "bar"], + [5, "lorem"], + ], + ); + }, +}); + +Deno.test({ + name: "zip() handles first shorter", + fn() { + zipTest( + [ + [1], + ["foo", "bar", "lorem"], + ], + [[1, "foo"]], + ); + }, +}); + +Deno.test({ + name: "zip() handles second shorter", + fn() { + zipTest( + [ + [1, 4, 5], + ["foo"], + ], + [[1, "foo"]], + ); + }, +}); + +Deno.test({ + name: "zip() handles sparse arrays", + fn() { + // deno-lint-ignore no-sparse-arrays + const sparse = [1, , 3]; + const result = zip(sparse, ["a", "b", "c"]); + assertEquals(result, [ + [1, "a"], + [undefined, "b"], + [3, "c"], + ]); + }, +}); + +Deno.test({ + name: "zip() handles non-array iterables", + fn() { + function* numbers() { + yield 1; + yield 2; + yield 3; + } + assertEquals( + zip(numbers(), ["a", "b", "c"]), + [[1, "a"], [2, "b"], [3, "c"]], + ); + }, +}); + +Deno.test({ + name: "zip() handles Set iterables", + fn() { + assertEquals( + zip(new Set([1, 2, 3]), ["a", "b", "c"]), + [[1, "a"], [2, "b"], [3, "c"]], + ); + }, +}); + +Deno.test({ + name: "zip() handles iterables of different lengths", + fn() { + function* numbers() { + yield 1; + yield 2; + } + assertEquals( + zip(numbers(), ["a", "b", "c"]), + [[1, "a"], [2, "b"]], + ); + }, +});