Ruby Ranges in JavaScript

Published: Monday, June 6, 2022

Greetings, friends! If you come from a Ruby background, then you may have seen Ranges. We can use a Range to create arrays of integers very quickly.

ruby
Copied! ⭐️
new_range = (1..5).to_a

p new_range # [1, 2, 3, 4, 5]

In JavaScript, we don't have special syntax that helps us create ranges. We'll have to create a function that accepts parameters instead. We can use the native Array.from method to create a range function in JavaScript.

javascript
Copied! ⭐️
function range(start, end) {
  return Array.from({ length: end - start + 1 }, (_, index) => index + start);
}

const newRange = range(1, 5);

console.log(newRange); // [1, 2, 3, 4, 5]

If we look at the function definition for Array.from, we can see that it accepts three parameters: an array-like or iterable object, a map function, and a value to use as the this context when executing the map function. The last parameter is rarely used.

javascript
Copied! ⭐️
Array.from(arrayLike, mapFn, thisArg)

If you're unfamiliar with array-like objects, please check out my array-like objects tutorial. I also have a three-part series on iterables.

It turns out that an object with a length property meets the requirement of being considered "array-like." Therefore, we can create an array using Array.from and passing in an object with a length property.

javascript
Copied! ⭐️
const newArray = Array.from({length: 3});

console.log(newArray);
// OUTPUT: [undefined, undefined, undefined]

When we don't pass a map function, mapFn, as a second parameter, then the array will only contain elements of undefined. The map function is a callback function that can accept two parameters: the current element of the array and the index of that element. We can expand the function definition of Array.from to include parameters of the map function.

javascript
Copied! ⭐️
Array.from(arrayLike, (element, index) => { /* ... */ }, thisArg)

The value returned by the map function will then replace each undefined we saw in the code snippet above. In the range function we created earlier, we can see that the map function is using the index of the array to generate numbers one through five. Arrays in JavaScript start with zero, so we need to add a value of one when setting the length property to include the end value.

javascript
Copied! ⭐️
function range(start, end) {
  return Array.from({ length: end - start + 1 }, (_, index) => index + start);
}

const newRange = range(1, 5);

console.log(newRange); // [1, 2, 3, 4, 5]

We can simplify the example above by using arrow functions.

javascript
Copied! ⭐️
const range = (start, end) => Array.from({ length: end - start + 1 }, (_, index) => index + start);

const newRange = range(1, 5);

console.log(newRange); // [1, 2, 3, 4, 5]
tip
It's common to use an underscore _ for parameters that aren't used in a callback function. It'll stop linters such as ESLint from complaining too!

Our code looks much shorter now! You can now use our custom range function to create ranges similar to Ruby!

We can also customize the range function for being inclusive versus exclusive. The Range utility in Ruby lets you use two dots .. to create an inclusive range, or three dots ... to create an exclusive range.

ruby
Copied! ⭐️
# inclusive - includes the last number
rangeInclusive = (1..5).to_a

# exclusive - excludes the last number
rangeExclusive = (1...5).to_a

p rangeInclusive # [1, 2, 3, 4, 5]
p rangeExclusive # [1, 2, 3, 4]

In JavaScript, we can add an extra parameter to determine whether the range should be inclusive or exclusive. We can use a default parameter to make sure this parameter is false when it's not used.

javascript
Copied! ⭐️
const range = (start, end, isExclusive = false) => {
  const length = isExclusive ? end - start : end - start + 1;
  return Array.from({ length }, (_, index) => index + start);
};

const rangeInclusive = range(1, 5);
const rangeExclusive = range(1, 5, true);

console.log(rangeInclusive); // [1, 2, 3, 4, 5]
console.log(rangeExclusive); // [1, 2, 3, 4]

In the example above, I'm using the shorthand property name for length, so we don't have to write { length: length }.

What's nice about using Array.from is that a negative length will result in an empty array.

javascript
Copied! ⭐️
const range = (start, end, isExclusive = false) => {
  const length = isExclusive ? end - start : end - start + 1;
  return Array.from({ length }, (_, index) => index + start);
};

const rangeInclusive = range(1, -3);
const rangeExclusive = range(1, -3, true);

console.log(rangeInclusive); // [ ]
console.log(rangeExclusive); // [ ]

This may be the behavior you want, but you can always change it if you prefer a different result. In Ruby, an empty array is returned when the difference between the end value and start value is a negative number.

ruby
Copied! ⭐️
rangeInclusive = (1..-3).to_a
rangeExclusive = (1...-3).to_a

p rangeInclusive # [ ]
p rangeExclusive # [ ]

Looks like our implementation of range behaves very similar to Ruby! Awesome! 😎

Resources