Skip to content

feat(collections/unstable): add zip truncate option to allow zipping until end of longest input #6733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

lionel-rowe
Copy link
Contributor

Closes #6732

Diffs stable vs. unstable
diff --git a/collections/zip.ts b/collections/unstable_zip.ts
index 61677b77..7bb6ea25 100644
--- a/collections/zip.ts
+++ b/collections/unstable_zip.ts
@@ -1,12 +1,61 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 // This module is browser compatible.
 
-import { minOf } from "./min_of.ts";
+/** Options for zipping arrays */
+export type ZipOptions = {
+  /**
+   * Determines how to handle arrays of different lengths.
+   * - `'shortest'`: stops zipping when the shortest array ends.
+   * - `'longest'`: continues zipping until the longest array ends, filling missing values with `undefined`.
+   * @default {'shortest'}
+   */
+  truncate?: "shortest" | "longest";
+};
 
 /**
  * Builds N-tuples of elements from the given N arrays with matching indices,
  * stopping when the smallest array's end is reached.
  *
+ * @experimental **UNSTABLE**: New API, yet to be vetted.
+ *
+ * @typeParam T the type of the tuples produced by this function.
+ * @typeParam O the type of the options passed.
+ *
+ * @param options Options for zipping arrays.
+ * @param arrays The arrays to zip.
+ *
+ * @returns A new array containing N-tuples of elements from the given arrays.
+ *
+ * @example Usage with options
+ * ```ts
+ * import { zip } from "@std/collections/zip";
+ * import { assertEquals } from "@std/assert";
+ *
+ * const numbers = [1, 2, 3];
+ * const letters = ["a", "b", "c", "d"];
+ *
+ * assertEquals(
+ *   zip({ truncate: "shortest" }, numbers, letters),
+ *   [[1, "a"], [2, "b"], [3, "c"]],
+ * );
+ * assertEquals(
+ *   zip({ truncate: "longest" }, numbers, letters),
+ *   [[1, "a"], [2, "b"], [3, "c"], [undefined, "d"]],
+ * );
+ * ```
+ */
+export function zip<T extends unknown[], O extends ZipOptions>(
+  options: O,
+  ...arrays: { [K in keyof T]: ReadonlyArray<T[K]> }
+): {
+  [K in keyof T]: O extends { truncate: "longest" } ? T[K] | undefined : T[K];
+}[];
+/**
+ * Builds N-tuples of elements from the given N arrays with matching indices,
+ * stopping when the smallest array's end is reached.
+ *
+ * @experimental **UNSTABLE**: New API, yet to be vetted.
+ *
  * @typeParam T the type of the tuples produced by this function.
  *
  * @param arrays The arrays to zip.
@@ -35,14 +84,23 @@ import { minOf } from "./min_of.ts";
  */
 export function zip<T extends unknown[]>(
   ...arrays: { [K in keyof T]: ReadonlyArray<T[K]> }
-): T[] {
-  const minLength = minOf(arrays, (element) => element.length) ?? 0;
+): T[];
+export function zip(...args: unknown[]): unknown[] {
+  const [options, arrays] = args.length === 0 || Array.isArray(args[0])
+    ? [{}, args as unknown[][]]
+    : [args[0] as ZipOptions, args.slice(1) as unknown[][]];
+
+  if (arrays.length === 0) return [];
+
+  const minLength = Math[options.truncate === "longest" ? "max" : "min"](
+    ...arrays.map((x) => x.length),
+  );
 
-  const result: T[] = new Array(minLength);
+  const result = new Array(minLength);
 
   for (let i = 0; i < minLength; i += 1) {
     const arr = arrays.map((it) => it[i]);
-    result[i] = arr as T;
+    result[i] = arr;
   }
 
   return result;
diff --git a/collections/zip_test.ts b/collections/unstable_zip_test.ts
index 2bcd9994..48083180 100644
--- a/collections/zip_test.ts
+++ b/collections/unstable_zip_test.ts
@@ -1,25 +1,24 @@
 // Copyright 2018-2025 the Deno authors. MIT license.
 
 import { assertEquals } from "@std/assert";
-import { zip } from "./zip.ts";
+import { zip, type ZipOptions } from "./unstable_zip.ts";
+import { assertType, type IsExact } from "@std/testing/types";
 
-function zip1Test<T>(
-  input: [Array<T>],
-  expected: Array<[T]>,
-  message?: string,
-) {
-  const actual = zip(...input);
-  assertEquals(actual, expected, message);
-}
+const EMPTY_OPTIONS: ZipOptions = {};
 
-assertEquals(zip([]), []);
+Deno.test({
+  name: "zip() handles zero arrays",
+  fn() {
+    assertEquals(zip([]), []);
+    assertEquals(zip(EMPTY_OPTIONS, []), []);
+  },
+});
 
 Deno.test({
   name: "zip() handles one array",
   fn() {
-    zip1Test([
-      [1, 2, 3],
-    ], [[1], [2], [3]]);
+    assertEquals(zip([1, 2, 3]), [[1], [2], [3]]);
+    assertEquals(zip(EMPTY_OPTIONS, [1, 2, 3]), [[1], [2], [3]]);
   },
 });
 
@@ -166,3 +165,43 @@ Deno.test({
     );
   },
 });
+
+Deno.test("zip() handles truncate option", async (t) => {
+  const arrays: [number[], string[], bigint[]] = [[1, 2, 3], [
+    "a",
+    "b",
+    "c",
+    "d",
+  ], [88n]];
+
+  await t.step('truncate: "shortest" (default)', () => {
+    const explicit = zip({ truncate: "shortest" }, ...arrays);
+    const implicit = zip(...arrays);
+
+    assertEquals(explicit, [[1, "a", 88n]]);
+    assertEquals(explicit, implicit);
+
+    assertType<IsExact<typeof explicit, [number, string, bigint][]>>(true);
+    assertType<IsExact<typeof implicit, [number, string, bigint][]>>(true);
+  });
+
+  await t.step('truncate: "longest"', () => {
+    const zipped = zip({ truncate: "longest" }, ...arrays);
+    assertEquals(
+      zipped,
+      [
+        [1, "a", 88n],
+        [2, "b", undefined],
+        [3, "c", undefined],
+        [undefined, "d", undefined],
+      ],
+    );
+
+    assertType<
+      IsExact<
+        typeof zipped,
+        [number | undefined, string | undefined, bigint | undefined][]
+      >
+    >(true);
+  });
+});

Copy link

codecov bot commented Jun 21, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 94.65%. Comparing base (2258bf2) to head (7a1720b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6733      +/-   ##
==========================================
- Coverage   94.65%   94.65%   -0.01%     
==========================================
  Files         576      577       +1     
  Lines       47065    47082      +17     
  Branches     6610     6615       +5     
==========================================
+ Hits        44549    44565      +16     
- Misses       2473     2474       +1     
  Partials       43       43              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

@std/collections zip with customizable handling of where to truncate (longest vs shortest)
1 participant