Learn ES6 with Advent of Code–Day 1
Regular followers of my blog will know that the last two years I’ve attempted the daily coding challenges at Eric Wastl’s superb Advent of Code website. In 2015, I posted daily videos, solving each challenge with LINQ in C# and in F#. In 2016 I focussed on F#.
Solving coding challenges is a great way to improve your skills in any language, and this year I’ve decided that I could do with becoming more familiar with JavaScript, and in particular the new ES6 language features.
Getting Started with ES6
Probably the easiest way to get started if you’ve not played around with ES6 is to install node.js. This gives you access to the node JavaScript runner, which means you can run code in an index.js
file simply by saying node index.js
. It also installs the npm package manager, which allows you to take advantage of the rich ecosystem of npm packages.
With node.js installed, a good starting point is creating a package.json
file by calling npm init
. You can accept the defaults, and you’ll end up with something like this:
{
"name": "myapp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC"
}
Creating a Solver Framework – index.js
I’ve set up a GitHub repository for my JavaScript solutions to this year’s Advent of Code challenges. The entry point is index.js which does a few things for me:
- Parses command line arguments so I can run a specific challenge or all of them
- Reads the data from the input.txt file for each challenge into an array with a line per string
- Outputs the results unit test style showing whether the answers returned match the correct answers for my input
My index.js
file has dependencies on two modules. One is the built-in fs module, which gives us file system access. And the other is the chalk npm package, giving us an easy way to output coloured text in the console.
To install the chalk package, I need to enter npm install chalk
at the command line and my package.json
file will get updated with chalk as a dependency:
"dependencies": {
"chalk": "^2.3.0"
}
Now I can load those modules in my JavaScript file with a call to require
, passing in the module name:
let chalk = require('chalk');
let fs = require('fs');
Notice that we’re using our first ES6 feature here – the let
keyword is the recommended replacement everywhere you used to use var
. In fact, in this case it would be better to use the const
keyword which is for variables that won’t be reassigned. I’m still getting in the habit of this, so forgive me for missing some opportunities to use it!
Next up, I’ll parse the command line arguments, and we can access them in the process.argv
array. The first two are the node executable itself and the index.js
filename, so I skip those with slice(2)
.
let year = 2017;
let startDay = 1;
let endDay = 25;
let args = process.argv.slice(2);
for(let arg of args) {
let n = Number(arg);
if (n > 2000) {
year = n;
}
else if (n > 0) {
startDay = n;
endDay = n;
}
}
This is just some quick and dirty code, but it does introduce another ES6 feature. The for … of
loop is a bit like C#’s foreach
loop – we can use it to iterate through each element in an array, or sequence. JavaScript already had a for … in
loop but that returns keys, not values, so the for … of loop is really handy in situations like this.
Next up, we’ll actually run the tests. I’ll loop through each day, and see if there is a solver for that year, by using the fs.existsSync
method (we’ll save async for another time).
for(let day = startDay; day <= endDay; day++) {
let path = `./${year}/${("0" + day).slice(-2)}`;
if (!fs.existsSync(path)) {
console.log(chalk.red(`${year} day ${day} not found`));
break;
}
// ...
}
I’m using another ES6 feature here to format the path. By surrounding the string in back-ticks I can use a ${…}
syntax for string interpolation which is a nice feature very similar to string interpolation in C#.
You can also see I’m using the chalk npm package to emit some red text if we don’t find the solver for this day.
Next, we read the input from the input.txt file with the fs.readFileSync
method. To make life easier for me, I know I want the input file in the format of an array of strings, one per line in the file, and with blank lines removed. So I use this LINQ-like statement to split the string into lines, use a regex to strip out unwanted carriage returns and a filter to strip out empty lines:
let text = fs.readFileSync(path + `/input.txt`)
.toString()
.split('\n')
.map(s => s.replace(/\r$/, ''))
.filter(s => s.length > 0);
Next we need to load the solver. It’s in a solve.js
file, so I can use require to load it like this:
let solver = require(path +`/solve`);
Now we’re nearly there. Each solver module in my project exports a solve
method and an expected
method. We run the solve
method passing in the input array and part number (each Advent of Code problem has two parts) and then call the expected
method, passing in the part to get the expected answer. We compare the two and output success in green and failure in red:
for(let part of [1,2]) {
let answer = solver.solve(text, part);
let expected = solver.expected(part);
if (answer === expected) {
console.log(chalk.green(`${year} day ${day} part ${part}: ${answer}`));
}
else {
console.log(chalk.red(`${year} day ${day} part ${part}: ${answer} - expected ${expected}`));
}
}
Solving the Day 1 Challenge
So that’s our test runner framework set up. Now we need to actually solve the puzzle for day 1, and thankfully the first challenge is relatively kind.
For each day, I create a solve.js
file with the following boilerplate:
function solve(input, part) {
// TODO
}
const expected = part => part === 1 ? -1 : -1;
module.exports = {solve,expected};
The expected
function uses the new ES6 arrow functions syntax that C# programmers will feel right at home with. Since I don’t know the right answer yet for the solution, I’ll just return –1
for each part, but update it with my answers once I’ve solved the challenge.
I also need to export the solve
and expected
functions to allow them to be called from consumers of my module.
So, that was a lot of setup. The puzzle for day 1 required us to loop through an array of numbers, and add them together, but only if a certain condition was met. I won’t go into too great detail on the solution for today since this blog post is getting quite long, but I will point out a couple of features.
First, to turn a string into an array of characters that we can call map on, we can use the ES6 “spread syntax”, which means[..."abc"]
becomes ["a",b","c"]
.
Second, although there isn’t a convenient “sum” method on Array, we can just use reduce
to simply sum the numbers together.
function solve(input, part) {
const nextChar = (ch,n) => ch[(n + 1) % ch.length];
const halfWay = (ch,n) => ch[(n + (ch.length / 2)) % ch.length];
return sumMatching([...input[0]], part === 1 ? nextChar: halfWay);
}
function sumMatching(chars, fn) {
return chars.map((c,n) => c === fn(chars,n) ? Number(c) : 0).reduce((a,b) => a+b);
}
The solution could be reduced further by combining the map
and reduce
steps, but I think this strikes a nice balance between conciseness and readability, so I’m leaving it here.
Anyway, hopefully I’ve persuaded you to give Advent of Code and ES6 a try. Or at least pick a language you’d like to improve in and tackle the daily puzzles.
I can’t promise I’ll have time to blog all the solutions this year, but feel free to follow my GitHub repo, and I’ll hopefully share a few more ES6/node tips and tricks I pick up along the way.