Skip to the code
cf.

Iterable Range class

Write a class Range. new Range(from, to, step) covers the numbers from from up to to, both included, counting by step (1 by default; you can assume it's positive). Make its instances iterable, so they work with for...of, spread, Array.from, and destructuring.

A range is reusable: every [Symbol.iterator]() call returns a new iterator with its own position, so you can spread a range twice, or loop over it inside another loop over the same range. When from is greater than to, the range is empty.

Write the iterator by hand, as an object with a next() method, not with a generator: that's what this problem practices. Like the built-in iterators, it's iterable itself: its [Symbol.iterator]() returns the iterator. Once it's done, next() keeps returning { value: undefined, done: true }.

[...new Range(1, 4)];     // [1, 2, 3, 4]
[...new Range(0, 10, 5)]; // [0, 5, 10]
for (const n of new Range(1, 3)) console.log(n); // 1, 2, 3
 
const it = new Range(1, 3)[Symbol.iterator]();
it.next(); // { value: 1, done: false }
[...it];   // [2, 3]

What the tests check

  1. spreads to the numbers between the bounds, both included
    expect([...new Range(1, 4)]).toEqual([1, 2, 3, 4]);
    expect([...new Range(-1, 1)]).toEqual([-1, 0, 1]);
    
  2. counts by step
    expect([...new Range(0, 10, 5)]).toEqual([0, 5, 10]);
    expect([...new Range(1, 6, 2)]).toEqual([1, 3, 5]);
    
  3. is empty when from is greater than to
    expect([...new Range(3, 2)]).toEqual([]);
    expect(Array.from(new Range(10, 0))).toEqual([]);
    
  4. can be spread twice
    const range = new Range(1, 3);
    expect([...range]).toEqual([1, 2, 3]);
    expect([...range]).toEqual([1, 2, 3]);
    
  5. nested loops over the same range don't interfere
    const range = new Range(1, 2);
    const pairs = [];
    for (const a of range) {
      for (const b of range) pairs.push(a * 10 + b);
    }
    expect(pairs).toEqual([11, 12, 21, 22]);
    
  6. next() returns { value, done } and stays done at the end
    const it = new Range(7, 7)[Symbol.iterator]();
    expect(it.next()).toEqual({ value: 7, done: false });
    expect(it.next()).toEqual({ value: undefined, done: true });
    expect(it.next()).toEqual({ value: undefined, done: true });
    
  7. the iterator is iterable itself
    const it = new Range(1, 3)[Symbol.iterator]();
    expect(it[Symbol.iterator]()).toBe(it);
    it.next();
    expect([...it]).toEqual([2, 3]);
    
  8. the iterator is written by hand, not a generator
    const it = new Range(1, 3)[Symbol.iterator]();
    expect(typeof it.next).toBe("function");
    // A generator shows as [object Generator]; an object or class instance (even one built on Iterator) passes.
    expect(Object.prototype.toString.call(it)).toMatch(/^\[object (Object|Iterator)\]$/);
    

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).