Skip to the code
cf.

Zip iterables

Write a generator function zip(...iterables). It yields arrays: the first value of every iterable, then the second value of every iterable, and so on. It takes any number of iterables of any kind, endless ones included. With no iterables, it yields nothing.

It stops as soon as one of the iterables runs out.

It's lazy: it pulls nothing until you ask for the first array.

When it stops, because an iterable ran out or because the caller stopped early (with break, or by calling return()), it closes the iterators that haven't finished: it calls their return() method, if they have one, so a generator's finally blocks run. Array and string iterators have no return().

[...zip([1, 2, 3], ["a", "b", "c"])]; // [[1, "a"], [2, "b"], [3, "c"]]
[...zip([1, 2, 3], "ab")];            // [[1, "a"], [2, "b"]]
[...zip([1], [2], [3])];              // [[1, 2, 3]]
[...zip()];                           // []

What the tests check

  1. yields arrays of the values at each position
    expect([...zip([1, 2, 3], ["a", "b", "c"])]).toEqual([
      [1, "a"],
      [2, "b"],
      [3, "c"],
    ]);
    
  2. stops when the shortest iterable runs out
    expect([...zip([1, 2, 3], "ab")]).toEqual([
      [1, "a"],
      [2, "b"],
    ]);
    expect([...zip([1, 2], [])]).toEqual([]);
    
  3. takes any number of iterables, or none
    expect([...zip([1], [2], [3])]).toEqual([[1, 2, 3]]);
    expect([...zip([1, 2])]).toEqual([[1], [2]]);
    expect(zip().next()).toEqual({ value: undefined, done: true });
    
  4. works with Sets, Maps, and endless generators
    // Endless for lazy code; eager code gets an error instead of a frozen page.
    function* naturals() {
      for (let n = 0; n < 1000; n++) yield n;
      throw new Error("read 1000 values from an endless generator: something isn't lazy");
    }
    expect([...zip(naturals(), new Set(["a", "b"]))]).toEqual([
      [0, "a"],
      [1, "b"],
    ]);
    expect([...zip(new Map([["k", 1]]), [true])]).toEqual([[["k", 1], true]]);
    
  5. pulls nothing until the first next()
    let pulls = 0;
    const upToThree = () => {
      let n = 0;
      return {
        [Symbol.iterator]() {
          return this;
        },
        next() {
          pulls += 1;
          n += 1;
          return n > 3 ? { value: undefined, done: true } : { value: n, done: false };
        },
      };
    };
    const zipped = zip(upToThree(), upToThree());
    expect(pulls).toBe(0);
    expect(zipped.next()).toEqual({ value: [1, 1], done: false });
    expect(pulls).toBe(2);
    
  6. closes the other iterators when one runs out
    let closed = false;
    function* letters() {
      try {
        yield "a";
        yield "b";
        yield "c";
      } finally {
        closed = true;
      }
    }
    expect([...zip([1], letters())]).toEqual([[1, "a"]]);
    expect(closed).toBe(true);
    
  7. closes every iterator when the caller stops early
    const closed = [];
    function* source(name) {
      try {
        yield name + 1;
        yield name + 2;
      } finally {
        closed.push(name);
      }
    }
    const pairs = [];
    for (const pair of zip(source("a"), source("b"))) {
      pairs.push(pair);
      break;
    }
    expect(pairs).toEqual([["a1", "b1"]]);
    expect(closed.sort()).toEqual(["a", "b"]);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).