Implementing a Pythonic range() function in JavaScript.

Creating a JavaScript array of say, years 2009 to 2050, can be naively done as follows:

var years = [2009, 2010, 2011, 2012, ... 2050];

If you’re lazier (and I don’t mean that’s a bad thing; Perl aficionados admire the virtue of being lazy), you’d do something along the lines of:

var years = [];
for (var year = 2009; year < 2051; year++){
    years.push(year);
}

And you wouldn’t give it much thought. You’d throw that piece into your code and chug along like a good rabbit. JavaScript 1.7 introduces a cool new feature called an array comprehension. I smell Python. Python has a similar construct, called a list comprehension, that must have been some source of inspiration for the JavaScript standardizing committee. Anyway, here’s what an array comprehension allows you to do:

var years = [year for each (year in range(2009, 2051))];

Sweet. One line that does all of that. But wait. JavaScript doesn’t come with a range() method, so you can very naively implement one using another cool new feature that comes with JavaScript 1.7: generators. We’ll get into much more detail about those in another article perhaps, but here’s an oversimplified explanation about what they are. They’re your usual functions with an orange-y twist. You can pause a generator function right in the middle using the yield keyword and have it give you a value, finish whatever you want to do with that value and ask that generator function to resume where it left off possibly to yield another value to you or run to completion.

function range(begin, end){
    for (let i = begin; i < end; ++i){
        yield i;
    }
}

I cannot possibly take credit for that piece of code because I simply lifted it from the Mozilla Developer Wiki. You may be wondering, why would you ever want to use a generator to do that? Well, the real difference between a generator and an ordinary function that returns a list is the very efficient use of memory. Generators don’t have to return an entire list which may be so long your end-user might simply quit using your application. They just return (more correctly, yield) as much as you need and let you go along your way. Now, generators are shiny and all, but there’s one little problem. What about when you really need that list of numbers, say from 20 to 400? That’s when you would do this:

/**
 * Behaves just like the python range() built-in function.
 * Arguments:   [start,] stop[, step]
 *
 * @start   Number  start value
 * @stop    Number  stop value (excluded from result)
 * @step    Number  skip values by this step size
 *
 * Number.range() -> error: needs more arguments
 * Number.range(4) -> [0, 1, 2, 3]
 * Number.range(0) -> []
 * Number.range(0, 4) -> [0, 1, 2, 3]
 * Number.range(0, 4, 1) -> [0, 1, 2, 3]
 * Number.range(0, 4, -1) -> []
 * Number.range(4, 0, -1) -> [4, 3, 2, 1]
 * Number.range(0, 4, 5) -> [0]
 * Number.range(5, 0, 5) -> []
 *   Number.range(5, 4, 1) -> []
 * Number.range(0, 1, 0) -> error: step cannot be zero
 * Number.range(0.2, 4.0) -> [0, 1, 2, 3]
 */
Number.range = function() {
  var start, end, step;
  var array = [];

  switch(arguments.length){
    case 0:
      throw new Error('range() expected at least 1 argument, got 0 - must be specified as [start,] stop[, step]');
      return array;
    case 1:
      start = 0;
      end = Math.floor(arguments[0]) - 1;
      step = 1;
      break;
    case 2:
    case 3:
    default:
      start = Math.floor(arguments[0]);
      end = Math.floor(arguments[1]) - 1;
      var s = arguments[2];
      if (typeof s === 'undefined'){
        s = 1;
      }
      step = Math.floor(s) || (function(){ throw new Error('range() step argument must not be zero'); })();
      break;
   }

  if (step > 0){
    for (var i = start; i <= end; i += step){
      array.push(i);
    }
  } else if (step < 0) {
    step = -step;
    if (start > end){
      for (var i = start; i > end + 1; i -= step){
        array.push(i);
      }
    }
  }
  return array;
}

And you could now simply do:

var years = Number.range(2009, 2051);

to get a list of years from 2009 to 2050 (inclusive).

Released under the you-may-do-whatever-the-fuck-you-want-with-that-code license.

About these ads

About this entry