DEV Community

loading...
Cover image for Implementing ranges in JavaScript

Implementing ranges in JavaScript

Antonio Villagra De La Cruz
・3 min read

Ranges are natively supported by a few (popular) programming languages. They allow for iteration over a defined space, while not have a linear increase in their memory footprint (all ranges always store a similar amount of data).

Let's try adding a similar idiom to JavaScript!


One way to approach this challenge is to write a plugin for a transpiler (for example a babel plugin) that would allow the following syntax:

const range = (0..5)
for (let i of range){
  console.log(i)
  // 0, 1, 2, 3, 4
}
Enter fullscreen mode Exit fullscreen mode

Instead, we will provide a similar functionality with vanilla JavaScript.

for (let i of range(0, 5)) {
  console.log(i)
  // 0, 1, 2, 3, 4
}
Enter fullscreen mode Exit fullscreen mode

We does the range stop at 4 instead of 5? This is a design choice, and as most languages that have a built-in range as not inclusive of their last value, the range utility that we will build will follow a similar convention by default.

The above syntax also allows us to pass a third argument to the function to control the step in between each iteration:

for (let i of range(0, 10, 2)) {
  console.log(i)
  // 0, 2, 4, 6, 8
}
Enter fullscreen mode Exit fullscreen mode

To start, let's create a class Range which will host the data needed for a range:

class Range {
  constructor(start, stop, step = 1) {
    this._start = Number(start);
    this._stop = Number(stop);
    this._step = Number(step);

    // Initialise a counter for iteration
    this.i = Number(start);
  }

  first() {
    return this._start;
  }

  last() {
    return this._stop;
  }

  step() {
    return this._step;
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now create a very basic (and not very useful) range:

const range = new Range(0, 10);

range.first(); // 0
range.last(); // 10
range.step(); // 1 (by default)
Enter fullscreen mode Exit fullscreen mode

One of the main reasons we want ranges though is to iterate over them ... so let's implement iteration protocols in our Range class!

To do so, we need to implement a next() method, as well as a [Symbol.iterator] method.

class Range {
  constructor(start, stop, step = 1) {
    ...

    // Initialise a counter for iteration
    this.i = Number(start);
  }

  first() { ... }
  last() { ... }
  step() { ... }

  next() {
    if (this.i < this._stop) {
      const value = this.i;
      this.i += this._step;
      return { value, done: false };
    }

    return { value: undefined, done: true };
  }

  [Symbol.iterator]() {
    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

Great! Now we can use our ranges as follow:

const range = new Range(0, 5)

for(let i of range) {
  console.log(i)
  // 0, 1, 2, 3, 4
}
Enter fullscreen mode Exit fullscreen mode

or

const range = new Range(0, 5)

range.next() // { value: 0, done: false }
range.next() // { value: 1, done: false }
range.next() // { value: 2, done: false }
range.next() // { value: 3, done: false }
range.next() // { value: 4, done: false }
range.next() // { value: undefined, done: true }
Enter fullscreen mode Exit fullscreen mode

There is one issue with our current implementation though, and that is that the range is depleted after a single iteration. We cannot reuse the same range in multiple consecutive loops.

Luckily, there is a one line fix to support that:

class Range {
  constructor(start, stop, step = 1) {
    ...

    // Initialise a counter for iteration
    this.i = Number(start);
  }

  first() { ... }
  last() { ... }
  step() { ... }

  next() {
    if (this.i < this._stop) {
      const value = this.i;
      this.i += this._step;
      return { value, done: false };
    }

    // We reset the value once we have iterated over all values so that
    // ranges are reusable.
    this.i = this._start;

    return { value: undefined, done: true };
  }

  [Symbol.iterator]() {
    return this;
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, to achieve the semantics we defined at the beginning, we need to wrap our class creation in a function:

class Range { ... }

function range(start, stop, step = 1) {
  return new Range(start, stop, step);
}

for (let i of range(0, 5)) {
  console.log(i)
  // 0, 1, 2, 3, 4
}
Enter fullscreen mode Exit fullscreen mode

Again, inspired by this blog post, I decided to build a library with the aforementioned features and much more! Check it out:

GitHub logo AntonioVdlC / range

⛰ - Implement ranges in JavaScript

range

version issues downloads license

Implement ranges in JavaScript.

Installation

This package is distributed via npm:

npm install @antoniovdlc/range

Motivation

Ranges are natively supported by a few (popular) programming languages. They allow for iteration over a defined space, while not having a linear increase in their memory footprint (all ranges always store a similar amount of data).

Usage

You can use this library either as an ES module or a CommonJS package:

import range from "@antoniovdlc
/range";
Enter fullscreen mode Exit fullscreen mode

- or -

const range = require("@antoniovdlc
/range");
Enter fullscreen mode Exit fullscreen mode

To create a range:

const start = 0;
const stop = 10;
const step = 2; // Defaults to `1` if not passed
const inclusive = true; // Defaults to `false` if not passed

const r = range(start, stop, step, inclusive);
Enter fullscreen mode Exit fullscreen mode

Getters

You can use the following getters on ranges:

.start

Returns the start value…

Discussion (0)