Advent of Code Day 4-Anagrams, Sets and Jasmine
Today’s Advent of Code challenge gives us a good excuse to use another ES6 feature: the Set object. We’ll also see a nice trick for testing if one word is an anagram of another and learn how to set up Jasmine unit tests for our project.
Part 1 – Unique Sets
Today’s puzzle required us to validate “passphrases” by checking whether a string contained any repeated words. This can easily be done by splitting the passphrase into words, and then adding each word to a Set
, which we can do by passing the word array into its constructor. A Set
object cannot contain duplicate strings, so if its size doesn’t match the length of the word array that means there are duplicates in the array. This allows us to make a simple isValid
function:
function isValid(passphrase) {
const words = passphrase.split(' ');
return words.length === new Set(words).size;
}
And we can count valid passphrases by filtering the input to only contain valid passphrases and checking the length:
function solve(input, part) {
return input.filter(isValid).length;
}
Part 2 – Anagrams
For part 2, words were considered equal if they were anagrams of each other. But how can we check if one word is an anagram of another? There’s actually a simple trick we can use. If we rearrange the letters in a word into alphabetical order then any two words which are anagrams of each other will produce the same result. So “star” becomes “arst” and “rats” also becomes “arst”. So we can just sort the letters and then use the same Set
trick we used in part 1 to check for duplicates.
We’ll first of all make our isValid
function more generic by allowing each word to be mapped before the check:
function isValid(passphrase, wordMap) {
const words = passphrase.split(' ').map(wordMap);
return words.length === new Set(words).size;
}
And now we can sort the letters in a word into alphabetical order by using the ES6 spread operator to explode a string into an array of characters, sort
it, and then join
it back into a string:
const isValid2 = p => isValid(p, w => [...w].sort().join(''));
Now all that remains is to define a countValid
function that can use a different validator depending on whether we are solving part 1 or 2:
const countValid = (phrases, validator) => phrases.filter(validator).length;
My full solution for day 4 can be seen here.
Testing with Jasmine
One nice feature of the Advent of Code puzzles is that each day we’re given some simple test cases that we can use to prove out our solution before submitting our answer. Today we were given some example valid and invalid passphrases for each part. What if we wanted to write some unit tests to run through those test cases?
Well, we can use a JavaScript unit testing framework, and one popular choice is Jasmine. To get started, we first install it with npm
:
npm install --save-dev jasmine
And then we can get it to set up a configuration file for us by calling jasmine init
. The jasmine CLI tool will be available in the node_modules/.bin
folder, so we can run it like this:
./node_modules/.bin/jasmine init
This will create a jasmine.json
file with sensible defaults, and our tests should be put inside the spec
folder. Each test module should have a name ending in Spec
, so I called my tests for day 4 “2017-04-Spec.js
”
In the spec file, we first require the functions we want to test. I’m testing the isValid1
and isValid2
functions so I can require
them using ES6 destructuring like this:
let { isValid1,isValid2 } = require("../2017/04/solve")
Of course, for this to work, I must remember to export those functions in the solve.js
file for day 4:
module.exports = {solve,expected,isValid1,isValid2};
In a Jasmine spec file we use the describe
function to describe the expected behaviour of the system under test. Each test is defined in an it
function, describing something that we expect the code to do. There is a fluent expectation syntax we are using which takes the form expect(actual).toBe(expected)
. Here’s a couple of tests for today’s challenge:
describe("2017 day 4", function() {
it ("aa bb cc dd ee is valid part 1 passphrase", function() {
const valid = isValid1("aa bb cc dd ee");
expect(valid).toBe(true);
})
it ("abcde xyz ecdab is invalid part 2 passphrase", function() {
const valid = isValid2("abcde xyz ecdab");
expect(valid).toBe(false);
});
});
To run the tests, we simply need to call the jasmine
CLI tool we used earlier. Calling it without any parameters will cause it to look for tests and run them. But we can also add a test entry to the scripts section of our package.json
file like this:
"scripts": {
"test": "jasmine"
},
That way, we can launch the tests simply by saying npm test
. We see a simple representation of the tests that ran, and if there’s a problem, we’ll get a detailed breakdown of which test failed at which line. But fortunately my tests are all passing…
For today’s puzzle, the unit tests were perhaps overkill as it was a fairly simple solution, but as the puzzles get harder, creating unit tests to verify the behaviour of individual functions can be an invaluable tool to have at your disposal.
One improvement my tests could do with is the elimination of some duplication as they’re quite repetitive. I’d like to use data-driven tests a bit like you can in NUnit with the [TestData]
attribute. It looks like there are some Jasmine extensions that can do this, so hopefully on a future day, I’ll find some time to wire one in.
Comments
I wasn't aware that JavaScript had its own Set object in ES6. Instead, I used an object acting as a dictionary, with a memoized function to keep track of the words that had already been read.
pmarfleet