Learn ES6 with Advent of Code-Day 2
Yesterday, I solved the puzzle from day 1 of this year’s Advent of Code challenge as a way of demonstrating how to get started with Node.js and ES6. Today, we’ll solve the second challenge, which will give us an opportunity to learn a couple more ES6 features – destructuring and generator functions.
Parsing the Input
The puzzle solving framework we created yesterday already reads the puzzle input from a text file and loads it into an array of strings. But today’s puzzle gave us tab separated numbers on each line, so we need to convert every row from a tab separated string "4\t3\t7"
to an array of numbers [4,3,7]
. We can do that by mapping the input array like this.
function solve(input, part) {
const spreadsheet = input.map(r => r.split('\t').map(s => Number(s)))
// ...
Min and Max
The “checksum” we need to calculate for the spreadsheet involves summing values for each row. For part 1 the value we need to find is the difference between the largest and smallest values on each row. What we need is a helpful max
function that given an array like [2,5,3]
will return 5.
Now it is possible to do this with clever use of the built in Math.max
function.
Math.max.apply(null,[2,5,3]) // returns 5
But that’s a little bit ugly, and actually I’ve already created my own min
and max
functions that operate on an array (or iterable) in my utils.js module. I created these utils when solving the 2015 problems in JavaScript as preparation for this year’s challenge.
So we need access to the min
and max
functions from the utils module and this gives us an opportunity to get our first look at ES6 destructuring.
To import the utils module we could do this:
const utils = require('../../utils/utils')
That would mean we’d need to say utils.max
and utils.min
every time we wanted to use these helper functions. So we might do this to let us just use min
and max
:
const utils = require('../../utils/utils');
const max = utils.max;
const min = utils.min;
What destructuring lets us do is simplify that code down into one line:
const {max,min} = require('../../utils/utils');
It’s nice and succinct and it has one other benefit here. Utils contains lots of functions, and by taking this approach I advertise which functions from utils I actually need. This would also help with “tree shaking” if we were trying to minimise a module bundle.
Now we have min
and max
helpers that work on arrays we can solve the part 1 challenge. I’ve defined a checksum
function that takes the spreadsheet (an array of rows which are arrays of integers) and a function to calculate the value for that row (rowFn
). Like yesterday, we use reduce
to add the values together.
function checksum(spreadsheet, rowFn) {
return spreadsheet.map(rowFn)
.reduce((a,b) => a+b);
}
We can call this checksum
method making use of min
and max
to solve part 1:
function solve(input, part) {
const spreadsheet = input.map(r => r.split('\t').map(s => Number(s)))
if (part === 1) {
return checksum(spreadsheet, r => max(r) - min(r));
}
}
Combinations
For part 2, the calculation for each row changed. We needed to find the pair of numbers that were evenly divisible and return the result of dividing them. A simplistic approach to solving this problem would be to loop through and check every pair like this:
function findDivisors(row) {
for (let x = 0; x < row.length; x++) {
for (let y = x+1; y < row.length; y++) {
if (row[x]%row[y] === 0) {
return row[x] / row[y];
}
if (row[y]%row[x] === 0) {
return row[y] / row[x];
}
}
}
throw new Error("no divisors found in row");
}
That allows us to solve part 2 like this:
return checksum(spreadsheet, findDivisors);
So that’s nice and easy, but I think we can improve on the findDivisors
function. It has two distinct responsibilities. First it is iterating through all pairs of numbers in the row. And then it is testing those pairs to see if they are divisible. What if we had a helper function that could return all pairs from an array? We can use another ES6 feature to help us with that:
Generator Functions
If you’re a C# programmer who enjoys using LINQ, you probably know the benefit of functions that return an IEnumerable<T>
using the yield
keyword. It can be much more efficient than returning a whole array as the consumer might not need to iterate the whole way through.
In ES6, you can create generator functions, which work very similarly. You declare them with function*
, and you use the yield
keyword to return each element in the sequence. Here’s my function that returns all pairs (both ways round) as two element arrays. So the array [a,b,c]
would return a sequence of [a,b] [a,c] [b,a], [b,c], [c,a], [c,b]
:
function* allPairs(arr) {
for (let x = 0; x < arr.length; x++) {
for (let y = 0; y < arr.length; y++) {
if (x !== y) yield [arr[x],arr[y]];
}
}
}
The way you consume the output of these functions is with the for…of loop we discussed yesterday. And we can combine this with the destructuring syntax. Since each element in the sequence is a two element array, I can destructure it into the a
and b
variables using this syntax:
function findDivisors(row) {
for (let[a,b] of allPairs(row)) {
if(a%b === 0) return a/b;
}
throw new Error("no divisors found in row");
}
Combining Map and Reduce
As we discussed yesterday, there is a fine balance between conciseness and readability in code. Code that is too verbose is hard to follow, but so is code that has been “code golfed”. However, I think that the checksum
function which is just a map
and a reduce
could be simplified slightly from:
function checksum(spreadsheet, rowFn) {
return spreadsheet.map(rowFn)
.reduce((a,b) => a+b);
}
to a form where we just use reduce
. We do need to pass in 0 as the initial value as the values in the arrays are not numbers so the rowFn
must be applied to each one:
const checksum = (spreadsheet, rowFn) => spreadsheet.reduce((a,b) => a+rowFn(b),0);
Of course its a matter of taste as to whether optimisations like this are helpful or make the code more confusing. As you become more familiar with functional programming concepts, code like this will become easier to understand.
Anyway, hope you found that interesting, you can see my full solution for day 2 here , and why not see if you can solve it in your language of choice.