Skip to the code
cf.

Interleave iterables round-robin

Write a generator function interleave(...iterables). It yields the first value of the first iterable, then the first value of the second, and so on, then goes back to the first iterable for its second value: round-robin. An iterable that runs out drops out of the rotation, and the others carry on until every one is done. With no iterables, it yields nothing.

It's lazy: it pulls nothing until you ask for the first value, and then pulls from one iterable at a time, only until it has the next value to yield. So it works with endless iterables.

Once an iterable's iterator has said it's done, interleave never calls its next() again.

When the caller stops early (with break, or by calling return()), it closes the iterators that haven't finished: it calls their return() method, if they have one.

[...interleave([1, 2, 3], "ab", [true])];
// [1, "a", true, 2, "b", 3]
 
function* naturals() {
  for (let n = 0; ; n++) yield n;
}
const mixed = interleave(naturals(), ["a"]);
mixed.next().value; // 0
mixed.next().value; // "a"
mixed.next().value; // 1
mixed.next().value; // 2 (the array has run out)

What the tests check

  1. takes one value from each iterable in turn
    expect([...interleave([1, 2], ["a", "b"])]).toEqual([1, "a", 2, "b"]);
    
  2. drops iterables that run out and carries on with the rest
    expect([...interleave([1, 2, 3], "ab", [true])]).toEqual([1, "a", true, 2, "b", 3]);
    expect([...interleave("abc", [1], "xy")]).toEqual(["a", 1, "x", "b", "y", "c"]);
    expect([...interleave([], [1, 2], [])]).toEqual([1, 2]);
    
  3. yields nothing for no iterables, or only empty ones
    expect([...interleave()]).toEqual([]);
    expect([...interleave([], new Set(), "")]).toEqual([]);
    
  4. works with endless iterables
    // 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");
    }
    const mixed = interleave(naturals(), ["a"]);
    const first = [];
    for (let i = 0; i < 5; i++) first.push(mixed.next().value);
    expect(first).toEqual([0, "a", 1, 2, 3]);
    
  5. pulls only as far as the next value
    const pulls = [0, 0];
    const upToTen = (index) => ({
      [Symbol.iterator]() {
        return this;
      },
      next() {
        if (pulls[index] === 10) return { value: undefined, done: true };
        pulls[index] += 1;
        return { value: index, done: false };
      },
    });
    const mixed = interleave(upToTen(0), upToTen(1));
    expect(pulls).toEqual([0, 0]);
    mixed.next();
    expect(pulls).toEqual([1, 0]);
    mixed.next();
    expect(pulls).toEqual([1, 1]);
    mixed.next();
    expect(pulls).toEqual([2, 1]);
    
  6. never calls next() again on an iterator that's done
    const strict = (values) => {
      let index = 0;
      let finished = false;
      return {
        [Symbol.iterator]() {
          return this;
        },
        next() {
          if (finished) throw new Error("next() was called after the iterator said it was done");
          if (index < values.length) return { value: values[index++], done: false };
          finished = true;
          return { value: undefined, done: true };
        },
      };
    };
    expect([...interleave(strict([1]), strict(["a", "b", "c"]))]).toEqual([1, "a", "b", "c"]);
    
  7. closes the unfinished iterators when the caller stops early
    const closed = [];
    function* source(name) {
      try {
        yield name + 1;
        yield name + 2;
      } finally {
        closed.push(name);
      }
    }
    const mixed = interleave(source("a"), source("b"), [1]);
    expect(mixed.next().value).toBe("a1");
    expect(mixed.next().value).toBe("b1");
    mixed.return();
    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).