Posted in:

In today’s Advent of Code puzzle, we needed to parse a long string, counting how deep into nested brackets we were, and following rules to ignore “garbage”.

I was able to create one function that solved both parts of the puzzle by looping through all characters in the input string, counting how many “garbage” characters we’d seen, and tracking how deep our nesting level was so we could keep a “score” up to date:

function parseInput(input) {
    let nestingLevel = 0;
    let score = 0;
    let garbageChars = 0;
    let inGarbage = false;
    for(let n = 0; n < input.length; n++) {
        let c = input[n]
        if(c === '!') n++;
        else if(c === '>') inGarbage = false;
        else if(inGarbage) garbageChars++;
        else if(c === '<') inGarbage = true;
        else if(!inGarbage && c === '{') score += ++nestingLevel;
        else if(!inGarbage && c === '}') nestingLevel--;
    }
    return [score,garbageChars];
}

Testing with Jasmine

I decided again to create some Jasmine tests to check my answers against the test cases. I decided to implement my own very simplistic data driven tests, and one nice feature of the Jasmine test runner, is that it will keep running all the expects, rather than stopping after the first failure, so just one run of these tests is needed to discover all failing test cases.

const { parseInput } = require("../2017/09/solve")

const testScores = [ ['{}', 1 ],
    [ '{{{}}}', 6 ],
    [ '{{},{}}', 5 ],
    [ '{{{},{},{{}}}}', 16 ],
    [ '{<a>,<a>,<a>,<a>}', 1 ],
    [ '{{<ab>},{<ab>},{<ab>},{<ab>}}', 9 ],
    [ '{{<!!>},{<!!>},{<!!>},{<!!>}}', 9 ],
    [ '{{<a!>},{<a!>},{<a!>},{<ab>}}', 3 ] ]

const garbageCounts = [
    [ '<>', 0 ],
    [ '<random characters>', 17 ],
    [ '<<<<>', 3 ],
    [ '<{!>}>', 2 ],
    [ '<!!>', 0 ],
    [ '<!!!>>', 0 ],
    [ '<{o"i!a,<{i<a>', 10 ],
];

describe("2017 day 9", function() {
    it ("scores the tests correctly", function() {
        for (let [s,n] of testScores)
            expect(parseInput(s)[0]).toBe(n);
    })

    it ("counts garbage chars correctly", function() {
        for (let [s,n] of garbageCounts)
            expect(parseInput(s)[1]).toBe(n);
    })
});

Having these unit tests was hugely valuable. My first attempt at solving part 2 failed because I’d ordered my else if checks wrong, so I moved one up a line and that solved part 2, but introduced a regression for part 1. That’s where taking the extra time to create unit tests really pays off. I might not have noticed I’d broken part 1’s answer without them. But it was third time lucky as I eventually got these three else if clauses into the right order:

else if(c === '>') inGarbage = false;
else if(inGarbage) garbageChars++;
else if(c === '<') inGarbage = true;

Clean up your code with ESLint

I’ve not yet mentioned another development tool I’m using and that’s ESLint. To get it working I installed the ESLint extension for VS Code and then I installed ESLint in my project with npm install eslint.

What happened next was initially quite frustrating as you need to configure ESLint with an .eslintrc.js configuration file. This can be set up to “extend” a set of rules, and so I chose to extend the “eslint:recommended” rules which seems to be a good starting point.

However, I found it soon complained about me using undeclared symbols. To fix that I needed to tell it about my “environment”. I’m using node, so I can use things like require, I’m using ES6 so I can use things like Map, and my tests are using Jasmine, so I can use things like describe. I also overrode the default no-console rule.

Here’s the config file I ended up with:

module.exports = {
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaVersion": 6
      },
      "env": {
        "browser": false,
        "node": true,
        "es6": true,
        "jasmine": true
    },
    "rules": {
        "no-console": "off"
    }
};

With this in place, it very helpfully warns me about unused parameters, undeclared variables or other places where I’m not following good JavaScript coding practices. After configuring it, I went through and fixed all ESLint warnings for my 2015 answers, and you can see here the sorts of changes I made. On the whole most of them I thought were steps in the right direction. There were a few you could legitimately quibble with, but you can always add additional rules to your ESLint config file to customise it to your taste.

I definitely recommend getting a setup where the linter is showing you issues while you’re in your editor, encouraging you to fix up style problems as you go rather than leaving it to the end.

As usual, you can explore my solutions on GitHub

Comments

Comment by Mikhail Shilkov

Ordering problem probably shows that your conditions are not specific enough and depend on (a combination of) previous conditions. You could improve it by changing to "if(!inGarbage && c === '<') inGarbage = true;". Not sure if it's worth it.
In more advanced languages, you would use pattern matching instead of if-else chain, which is usually quite robust to ordering (you get a compiler warning in most screwed cases).

Mikhail Shilkov