Array.range() for generating sequences

It is reasonably common to need an array populated by a simple sequence of numbers, usually to manipulate them later. Uses include:

  • Generating page numbers (<a href="page1.html">1</a>)
  • Retries (n attempts to login before being locked out)
  • Mathematics (any ranged sum or product, combinatorics, Taylor/Maclaurin series, partial harmonic series, Fourier series...)

Currently these can be created imperatively via for loop:

function range(start, stop, step){
  let arr = [];
  for(let i = 0; i < start+stop; i++){
    arr[i] = start + (i * step);
  }
  return arr;
}

(Of course, one could elect to make any manipulations they wanted inside the for loop, rather than generating the array and then manipulating it.)

One can also create such an array more functionally using array methods in various ways:

const range = (start, stop, step) => Array((stop - start) / step)
                                      .fill(0)
                                      .map((_,i) => start + (i * step));
// or
const range = (start, stop, step) => Array(stop - start)
                                      .fill(0)
                                      .map((_,i) => start + i)
                                      .filter(v => (v-start) % step === 0)

The for loop solution has detractors simply due to its use of for loops, but in any case it uses a fair amount of code for a fairly simple concept.

The same problem applies to the Array method approach -- an array created via Array(n) cannot be meaningfully mapped over until it is filled with something either via for loop or with Array.prototype.fill(), which can only fill with a constant value. Meaning a third step (at least) is required to generate the final array, and at least two passes over the entire array.

Additionally, the Array methods approach has a lot of potential pitfalls (there are issues with both of my demo implementations, edge cases where they don't work, etc).

I think it would be useful to have a static method on Array, tentatively called Array.range, with the following semantics:

  • Array.range(stop: number) -- for any positive stop return an array of integers in the interval [0, Math.ceil(stop) - 1] inclusive, ordered from least to greatest. For any negative stop return an array of integers in the interval [Math.floor(stop) + 1, 0] inclusive, tentatively ordered from greatest to least (though I'd be open to arguments either way on that). For stop === 0, return [], or possibly [0], it's up for discussion.
  • Array.range(start: number, stop: number, step = 1) -- If start is less than stop, return an array of the set of all n_k = start + k*Math.abs(step) where k is a non-negative integer and n_k is in the interval [start, stop) (note that it is inclusive on the low end, exclusive on the high end a la Array.prototype.slice()). If start is greater than stop, returns an array of the set of all n_k = start - k * Math.abs(step) where k is a non-negative integer and n_k is in the interval [start, stop) with the same restrictions as the prior case. Throw an error (RangeError?) on step === 0. Either throw an error, or return [] or [start] on start === stop.
  • Possibly? Array.range(start: number, stop: number, step: number, modifier: (value: number index: number)=> number) with the same logic as the prior case, but the function modifier is mapped across the resultant array in the same pass as creation (reducing the need to then immediately call .map() on the array, which would also introduce an additional pass over the array)

I think that having this baked into the Array class would allow for some optimizations at the interpreter level that would be more difficult or impossible to do in a library, such as making the creation slightly more lazy in some cases.

Is this (or a version of this) something that anyone else would support?

Hi David,

This seems like a natural extension for https://github.com/tc39/proposal-slice-notation?

There's also a much older proposal, http://array.build, which could be relevant.

Yes! Please! Array.build seems wonderful.

const arr1 = [1:6]; // [1, 2, 3, 4, 5, 6]
const arr2 = [1:7:2]; // [1, 3, 5, 7]
const arr3 = [1:4, 10:20:5, 4, 2:4]; // [1, 2, 3, 4, 10, 15, 20, 4, 2, 3, 4]

for(const n of [4:8]) { ... }

[10:100:10][0:4]; // [10, 20, 30, 40]

There is another proposal for it https://github.com/Jack-Works/proposal-Number.range

1 Like