DEV Community 👩‍💻👨‍💻

Jason Yu
Jason Yu

Posted on • Updated on

JavaScript Range How to create range in Javascript

range is a function that basically takes in a starting index and ending index then return a list of all integers from start to end.

The most obvious way would be using a for loop.

function range(start, end) {
    var ans = [];
    for (let i = start; i <= end; i++) {
        ans.push(i);
    }
    return ans;
}
Enter fullscreen mode Exit fullscreen mode

As a fan of FP, let's come up with an recursive solution. So the base case is obviously when the start and end are the same, the answer would simply be [start].

function range(start, end) {
    if(start === end) return [start];
    // recursive case
}
Enter fullscreen mode Exit fullscreen mode

Now take the leap of faith, assume that range(start, end) will just work. Then how do we solve the problem range(start, end)? Simple! Just do [start, ...range(start + 1, end)].

So combining both, we get

function range(start, end) {
    if(start === end) return [start];
    return [start, ...range(start + 1, end)];
}
Enter fullscreen mode Exit fullscreen mode

A lot more elegant than the for-loop solution in my opinion. But we could even go further if we use new Array(n) which creates an array with n elements.

If we have an n element list, we could build a range from it by mapping each element to its index, i.e. arr.map((_, i) => i).

However, according to https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map#Description, map will not call for unassigned element. This mean we need to initialise the new Array(n) before mapping. One standard technique is to use fill. The final result is the following.

function range(start, end) {
    return (new Array(end - start + 1)).fill(undefined).map((_, i) => i + start);
}
Enter fullscreen mode Exit fullscreen mode

We could also make use of Array.from to create range:

function range(start, end) {
  return Array.from({ length: end - start + 1 }, (_, i) => i)
}
Enter fullscreen mode Exit fullscreen mode

Thank you Step for mentioning about efficiency when handling large ranges, which essentially build a huge array. We could have a more efficient way of doing this by using generators.

function* range(start, end) {
    for (let i = start; i <= end; i++) {
        yield i;
    }
}
Enter fullscreen mode Exit fullscreen mode

We could use this generator in a for...of loop (which would be very efficient) or use an array spread to retrieve all values (note that this essentially builds the array which is essentially the same as the non-generator approaches.)

for (i of range(1, 5)) {
    console.log(i);
}
/* Output
 * 1 2 3 4 5 */

[...range(1, 5)] // [1, 2, 3, 4, 5]
Enter fullscreen mode Exit fullscreen mode

Since I always try to avoid for loops, we could also define the generator recursively as follows.

function* range(start, end) {
    yield start;
    if (start === end) return;
    yield* range(start + 1, end);
}
Enter fullscreen mode Exit fullscreen mode

Could you think of some cooler method to achieve this?

Top comments (55)

Collapse
 
namirsab profile image
Namir • Edited on

I like this way:

const range = (start, end) => {
    const length = end - start;
    return Array.from({ length }, (_, i) => start + i);
}
Enter fullscreen mode Exit fullscreen mode

As array from works with arrayLike structures and receives a second optional argument mapFn, it's a nice candidate to build range.

Collapse
 
johnboy5358 profile image
John

Yes, excellent solution.

This, slimmed down, version also works...

const range = (start, end, length = end - start) =>
  Array.from({ length }, (_, i) => start + i)

Collapse
 
mbrookes profile image
Matt

Almost.

Try:

const range = (start, end, length = end - start + 1) =>
  Array.from({ length }, (_, i) => start + i)
Enter fullscreen mode Exit fullscreen mode
> range(0,2)
[ 0, 1, 2 ]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
acatalfano profile image
Adam Catalfano

as satisfying as this is, you're exposing a third argument that anyone can override. In a modern tech stack, this code is hesitantly admissible only if you can guarantee that absolutely no one can set length to be anything but end - start, otherwise you need to invest in a much more complex testing framework, likely outweighing the expense of adding a second line to this function.

Collapse
 
kerafyrm02 profile image
kerafyrm02 • Edited on

const range = (start, end) => Array.from({length: end}, (_, i) => start + 1);

Thread Thread
 
johnboy5358 profile image
John

Sorry kerafyrm02, but that does not produce a range.

If I run range with range(1,50) I get [2,2,2,2,2,2,2,2,...]

const range = (start, end) => Array.from({length: end}, (_, i) => start + 1); console.log(range(0,20))
Thread Thread
 
kerafyrm02 profile image
kerafyrm02 • Edited on

let r = (s, e) => Array.from('x'.repeat(e - s), (_, i) => s + i);

Ok here ya go... even shorter. :D

Thread Thread
 
johnboy5358 profile image
John

Rock, paper, scissors... 68 chars, you've nailed it!

Thread Thread
 
kerafyrm02 profile image
kerafyrm02

lol., made range to r.. so even shorter :D

Thread Thread
 
johnboy5358 profile image
John • Edited on

Trust me to bump into a js golfer.

Next you'll be telling me that you're dropping the let keyword and just leaving r on the global object!!

Thread Thread
 
ycmjason profile image
Jason Yu Author

haha! nice solution John!

Thread Thread
 
johnboy5358 profile image
John

Well, thank you so much for saying!

Collapse
 
jclemens24 profile image
Jordan Clemens

This is how I went about creating a different solution although it is only really beneficial by calling (value in target) and really only counts as a "typeof range" :) Just bored and playing around with proxies.

console.log(...generateRange());

let range = {
    start: 1,
    end: 10,
};

range = new Proxy(range, {
    has(target: typeof range, prop: string | symbol | number) {
        if (typeof prop === 'number') {
            return prop >= target.start && prop <= target.end;
        } else {
            return Number(prop) >= target.start && Number(prop) <= target.end;
        }
    },
});

console.log(5 in range);
console.log(50 in range);

Enter fullscreen mode Exit fullscreen mode
Collapse
 
perrydbucs profile image
Perry Donham • Edited on

If you're going to use => might as well go all in... (line break for readability on dev.to)

const range = (start, end) => new Array(end - start + 1)
.fill(undefined).map((_, i) => i + start)

Collapse
 
ycmjason profile image
Jason Yu Author

I am quite against writing all functions as arrow functions. The reason being that arrow functions are not bind-able and it refers to the this from the outer scope. I feel that it is designed to prevent us writing var self = this; and refer to self in a callback function. So I tend to write arrow functions only when it is a callback or when it is a one-off function. Also if we use arrow function instead of proper function declaration in this case, we will lose the advantage which function hoisting provides. This means range might not be accessible by some earlier parts of our code. But it is all down to personal preference, do tell me what you think.

Collapse
 
perrydbucs profile image
Perry Donham

I don't like ambiguity in code. When I'm reviewing code and have to stop and figure out what this is referring to, it makes me frown; with arrow functions I always know exactly what this is. The 'swap this and that' pattern in JS always struck me as a hack. One of the big wins of arrow functions is eliminating the this ambiguity.

I have a similar feeling for hoisting. I understand why it is part of the language, but it always feels like a path to ruin when I have to rely on some behind-the-scenes reshuffling by the interpreter to get my code to run.

All that said, you are correct that there are situations in which arrow functions are not appropriate, which also make me crazy. Having two ways to write functions just adds confusion. "We recommend using arrow functions everyhwere. Oh, sorry, was that a constructor? Write that one this way instead..."

I can't wait to start seeing code in review that's built with templated classes. Maybe ES7 will introduce header files...

Thread Thread
 
ycmjason profile image
Jason Yu Author • Edited on

Haha! I totally understand what you mean. But still since ES6 introduces class we should really move away from using function as a constructor. As soon as it is consistent in a team then it's fine.

Collapse
 
perrydbucs profile image
Perry Donham • Edited on

Also, I'm a big fan of using variable names that describe what they are (even for internal functions), so (line break for readability on dev.to)
const range = (start, end) => new Array(end - start + 1)
.fill(undefined).map((value, index) => index + start)

Collapse
 
ycmjason profile image
Jason Yu Author

On the other hand, I am a big fan of Haskell. And we tend to use _ to refer to variables which we don't use in the function. But I do agree with you that having meaningful names is a good practice and could help readability.

Thread Thread
 
perrydbucs profile image
Perry Donham • Edited on

Even in Haskell, _ is a bad idea :) Think about the poor intern trying to learn the language while debugging your code. (And it isn't just Haskell...I did a lot of Perl back in the day, which at it best can look like line noise).

BTW since you are a fan of Haskell, I ran across this article the other day that you might enjoy, describing how to structure JS function with Haskell-styl currying.

Collapse
 
nycdude777 profile image
Rick Ellis • Edited on

I liked your recursive generator. How about something like this:

Number.prototype.to = function* (end) {
  const start = this;
  const step = end > start ? 1 : -1;
  const fn = function* (n) {
    let next = start + step * n;
    yield next;
    if (next === end) return;
    yield* fn(n + 1);
  };
  yield start;
  yield* fn(1);   
}

const asc = [...(1).to(5)];
const dsc = [...(5).to(1)];

console.log(asc); // [1, 2, 3, 4, 5]
console.log(dsc); // [5, 4, 3, 2, 1]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pinksynth profile image
Sammy Taylor

This is great, I wish this would make its way into the spec.

Collapse
 
ycmjason profile image
Jason Yu Author

Haha! I like this idea!

Collapse
 
teraspora profile image
John Lynch

I just use a one-liner recursive arrow function in ES6; note, it doesn't check but you should only pass it integers else it will never get to a == b so it will blow the stack!



let range = (a, b) => a>b ? range(b, a).reverse() : (a==b ? [a] : range(a, b-1).concat(b));
Collapse
 
ycmjason profile image
Comment marked as low quality/non-constructive by the community. View Code of Conduct
Jason Yu Author

Are you actually using this in production code? 😂😂

You realise how inefficient this is right? 😂😂

Collapse
 
ycmjason profile image
Jason Yu Author

I have adapted Namir's method (see comments). And it is probably the most efficient.

Collapse
 
josefrichter profile image
Josef Richter

The recursive one is really nice. Can be shortened to:

const range = (s, e) => e > s ? [s, ...range(s + 1, e)] : [s];
Enter fullscreen mode Exit fullscreen mode

Also this is another approach, + easier to handle descending sequences too:

const range = (s,e) => e > s ? Array(e - s + 1).fill(0).map((x, y) => y + s) : Array(s - e + 1).fill(0).map((x, y) => - y + s);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
danielboll profile image
Daniel Boll

I'm a little late for the discussion 😅 but I found the different ways interesting, I ended up getting this one. Is it valid?

const range = (start, end, length = end - start + 1) => [...Array(length).keys()].map(d => d + start) 
Collapse
 
ycmjason profile image
Jason Yu Author

Looks great to me!

Collapse
 
jesseighor profile image
Ighor Jesse • Edited on

This is an Awesome string of code you've written. I'm partial to Python3 behavior of range though. Made a few minor adjustments yours. Better to steal like an artist then build from scratch.

function* range(start=0, end=undefined, step=1) {    
    if( arguments.length === 1) {end = start, start = 0}    

    [...arguments].forEach(arg => {    
        if( typeof arg !== 'number') {throw new TypeError("Invalid argument")}                               
    })    
    if( arguments.length === 0) {throw new TypeError("range requires at least 1 argument, got 0")}    

    if(start >= end) return                                                                                                                                     
    yield start    
    yield* range(parseInt(start + step), end, step)    
}

// Use Cases
console.log([...range(5)])

console.log([...range(2, 5)])

console.log([...range(2, 5, 2)])
Collapse
 
davidchase profile image
David Chase

Why not get really wild and crazy :P ?

1st define a unfoldr

const unfoldr = (f, seed, xs = [], next = f(seed)) =>
   next ? unfoldr(f, next[1], xs.concat(next[0])) : xs

then define a range in terms of unfold

const range = (from, to) =>
   unfoldr(seed => seed > to ? false : [seed, seed + 1], from)

excellent article btw, keep them coming :)

Collapse
 
chronaeon profile image
Micah Dameron

Thanks for this article. I find your first answer to be the most intuitive, and it works great for my situation.

function loopASequenceAgain(start, end) {
  // create a loop which loops from start to end
  var ans = [];
  for (let i = start; i <= end; i++) {
  // log current value to console
      console.log(i); 
      ans.push(i);
  }
  return ans;
}
Collapse
 
johnboy5358 profile image
John • Edited on

It rather depends, Jason, on what you mean by cooler.

If you only needed range to handle approx. 6000 iterations then I think a recursive range function is pretty cool, but slow:

const range = (s,e) => (s === e) ? [s] : [s, ...range(s+1,e)]
Enter fullscreen mode Exit fullscreen mode

But, if you need a much bigger range and fast executuion then do something like this:

const range = (s=0,e=10,r=[]) =>
  {while(e >= s){ r.push(s); (e>=s)? s++ : s--; } return r }
Enter fullscreen mode Exit fullscreen mode

And if you don't like the arrow function, then it's easy to convert to a standard es5 style function.

Collapse
 
bryistheguy profile image
Bryan Ryan

The for loop is by far the easiest to read and also by far the most efficient. Your last example was literally more than 15 times slower than the for loop. Far too many developers these days are trying to get all fancy when the simple solution is much better.

Collapse
 
stepchud profile image
Step Chud

Might be inefficient to create an array for large ranges, and only supports range of integers (no dates). Have you seen Ruby's Range class? ruby-doc.org/core-2.2.0/Range.html
You could still implement toArray() and offer a covers() function.

Collapse
 
ycmjason profile image
Jason Yu Author

Yes! You are right! I will add a generator implementation as well which should solve this problem! ;D

Collapse
 
stepchud profile image
Step Chud

I'd like to see the generator implementation!

Thread Thread
 
ycmjason profile image
Jason Yu Author

What do you think about it? I have implemented.

Collapse
 
mihailoff profile image
George Mihailov

Why nobody is wondering why this is not part of the standard library?

php.net/manual/en/function.range.php
ruby-doc.org/core-2.5.1/Range.html
docs.python.org/3.3/library/stdtyp...

Collapse
 
furf profile image
Dave Furfero • Edited on

let's go both ways!

function range(start, end) {
  const sign = start > end ? -1 : 1;
  return Array.from(
    { length: Math.abs(end - start) + 1 },
    (_, i) => start + i * sign
  );
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ycmjason profile image
Jason Yu Author

oooooo maybe with Math.sign?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

About Real-time

Join DEV and MongoDB to build a front-end application using MongoDB Atlas. Change streams to display live updates as your database changes for your entry in the DEV x MongoDB Atlas Hackathon 2022.

Join the Hackathon