Building a range generator in JavaScript – Part I

I came across a very near trick in Python, for creating quick enums. Instead of assigning values one by one to a set of variables, it uses range() generator to assign them.

Normal code
# Assigning variables one at a time
RED = 1
GREEN = 1
BLUE = 2
print ( 'RED={}, GREEN={}, BLUE={}'
  .format(RED, GREEN, BLUE)
Using range generator
# Use Python's multi variable assignment
RED, GREEN, BLUE = range(3)
print ( 'RED={}, GREEN={}, BLUE={}'
  .format(RED, GREEN, BLUE)

Sometimes, small little things like this give joy! So, I wanted to do the same thing in JavaScript

// Use javaScript destructuring to multiple assignments
var [RED, GREEN, BLUE] = range(3);

// However, there is no built-in range()

No problems, lets write it ourselves… It should be easy!

/*
 * Module: range() generator
 * Author: Sanjay Vyas
 *

// Generators should be function*
function *range(end) {
  for (var value = 0;
       value < end;
       value++) {
    // generator magic
    // Yield create a "state meachine"
    yield value;
  }
}

// Prints 0, 1, 2
for (var x of range(3))
  console.log(x); 

var [RED, GREEN, BLUE] = range(3);

// Prints 0, 1, 2
console.log(RED, GREEN, BLUE); 

Yay! We have just written our own range generator in JavaScript. But wait a minute.. Python allows the following

# range(start, end, number_to_skip)
# range stops BEFORE end, never returns end
range(5) # 0, 1, 2, 3, 4
range(3, 6) # 3, 4, 5
range(0, 6, 2) # 0, 2, 3, 4
range(6, 3, -1) # 6, 4
range(10, 0, -2) # 10, 8, 6, 4, 2
Alright! Let's have a go at it again
function* range(start, end, skip) {

  // If end not provided, assume 0..start
  // e.g. range(5) will become range(0. 5)
  if (end == undefined) {
    end = start;
    start = 0;
  }

  // Reject wrong skips
  // like range(0, 5, -1)
  // or range(6, 0, +1)
  let ascending = start < end;
  if (ascending && skip <= 0 || 
     ! ascending && skip >= 0)
    return null;
  
  // If user did not give skip
  // range(0, 5) -> skip = +1
  // range(6, 0) -> skip = -1
  if (skip == undefined)
    skip = ascending ? +1 : -1;

  // Now 'yield' values in a loop
  if (ascending) {
    for (var value  = start;
      start<end;
      value += skip)
      yield value;
  } else {
    for (var value  = start;
         start<end;
         value += skip)
      yield value;
}

There! We are done!
But is this optimised? We have for loop twice and, once for ascending and once more for descending

Ummm.. let's try to optimise it!
function* range(start, end, skip) {

  // If end not provided, assume 0..start
  end == undefined && ([end, start] = [start, 0]);

  // Check order and set skip, if needed
  let ascending = start < end;

  if (ascending && skip <= 0 || 
     ! ascending && skip >= 0)
    return null;
  
  // Use "truthy" short-circuiting
  // and ternary assignment
  !skip && (skip = ascending ? +1 : -1);

  // Replace 2 fors with 1
  // Use lambda to check condition in for
  const condition = 
       ascending 
           ? ((value, end) => value < end) 
           : ((value, end) => value > end);

  // Now 'yield' values in a loop
  for (var value = start;
    condition(value, end);
    value += skip)
    yield value;
}

Tada!! Finally done. Let's use it and see
// Should print [ 0, 1, 2, 3, 4 ]
console.log("range(5): ", 
  [...range(5)]);

// Should print [ 0, 1, 2 ]
console.log("range(0, 3): ",
  [...range(0, 3)]);

// Should print 6, 5, 4
console.log("range(6, 3): ",
  [...range(6, 3)]);

// Should print 6, 4, 2
console.log("range(6, 0, -2): ",
  [...range(6, 0, -2)]);

// Create enum using range(3)
var [RED, GREEN, BLUE] = range(3);
console.log(`RED=${RED}, GREEN=${GREEN}, BLUE=${BLUE}`);

// Should print 10, 8, 6, 4, 2
console.log("range(10, 0, -2): ",
  [...range(10, 0, -2)]);

// Should not print anything
console.log("range(): ",
  [...range()]);

// Should not print anything
console.log("range(0): ",
  [...range(0)]);

// Should print 0, 2, 4, 6, 8
console.log("range(0, 10, 2): ",
  [...range(0, 10, 2)]);

// Should not print anything
console.log("range(10, undefined, 2): ",
  [...range(10, undefined, 2)]);

// Should print 5, 4, 3, 2, 1
console.log("range(5, 0): ",
  [...range(5, 0)]);

And here is the output
range(5): [ 0, 1, 2, 3, 4 ]
range(0, 3): [ 0, 1, 2 ]
range(6, 3): [ 6, 5, 4 ]
range(6, 0, -2): [ 6, 4, 2 ]
RED=0, GREEN=1, BLUE=2
range(10, 0, -2): [ 10, 8, 6, 4, 2 ]
range(): []
range(0): []
range(0, 10, 2): [ 0, 2, 4, 6, 8 ]
range(10, undefined, 2): [ 0, 2, 4, 6, 8 ]
range(5, 0): [ 5, 4, 3, 2, 1 ]

Is that it? YES! This is a short and sweet implementation of range() generator in JavaScript.
Can we do it in a better way? Well, we can do it using recursion and array expansion with map.

Those will be Part II and III

Range Generator Series

  1. Building a range generator in JavaScript – Part I (iterative – for loop)
  2. Building a range generator in JavaScript – Part II (recursive)
  3. Building a range generator in JavaScript – Part III (iterative – no for loop)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s