Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions collections/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
84 changes: 84 additions & 0 deletions collections/unstable_zip.ts
Original file line number Diff line number Diff line change
@@ -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<T extends unknown[]>(
...iterables: { [K in keyof T]: Iterable<T[K]> }
): 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;
}
191 changes: 191 additions & 0 deletions collections/unstable_zip_test.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
input: [Array<T>],
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<T, U>(
input: [ReadonlyArray<T>, ReadonlyArray<U>],
expected: Array<[T, U]>,
message?: string,
) {
const actual = zip(...input);
assertEquals(actual, expected, message);
}

function zip3Test<T, U, V>(
input: [Array<T>, Array<U>, Array<V>],
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"]],
);
},
});
Loading