Ruby Ranges in JavaScript
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.
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.
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.
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.
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.
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.
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.
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]
_
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.
# 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.
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.
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.
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! 😎