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