It’s been quite some time since I’ve taken time to enjoy coding challenges primarily for the fun of it. Over the next months, I’m keen to work my way through the 2023 Advent of Code - a fun set of problems created by Eric Wastl strung together with a seasonal story - and write up a bit about my own learning and fun.
For me it’s a chance to keep improving my fluency with various Rust libraries and tools (start learning the Rust programming language here!). And the first day of the 2023 Advent of Code is a parsing exercise - a good opportunity to re-familiarise myself with Rust’s nom parsing library as well as re-enforce in my head that confirmation bias is strong in this one (me).
For this first problem, day 1, I’m just going to outline the final solutions that I arrived at and note, for my own learning, where I was tripped up, whereas I’ll plan my commits a little better for the rest of the problems to learn also from the path I’ve taken (thanks to Amos at fasterthanli.me for the inspiration to start writing about what I learn again).
So first, the actual binary main functions for parts 1 and 2 are similar and straight-forward:
|
|
differing only in that part2 calls parse_calibration_part2()
instead.
Part 1 - A calibration value using the first and last digits
The first part of the challenge is to parse the first and last digit when given a string of alphanumeric characters. So a string such as “a1b2c3d4e45f” is parsed as the number 15. Each such line gives a calibration value, with the desired result being the summation of all calibrations for all lines of your input.
I find it useful to add an initial implementation with a todo!()
:
|
|
so that I can go ahead and write a bunch of failing test cases before hitting confirmation bias as I implement and test further (and no, I didn’t come up with all of these before writing - the last one is the result of noticing that I’d missed this explicit case even though it’d been highlighted in the instructions):
|
|
Part 1 Step 1 - parsing the numbers amongst alpha characters
Nom is a parsing library that allows building combinators of parsers. In this case, we want to:
- capture at least one but possibly many numbers that may be preceeded or terminated with zero or more alpha characters to get a result such as
vec!["26", "45", "98"]
(line 6 below), - map that result through a function to take the first digit of the first number and the last digit of the last number, eg.
28
for the example above (line 7 below)
|
|
In this case I just moved my todo();
into a stubbed first_n_last
function.
Part 1 Step 2 - first and list digits
Extracting the first and last digits of a vec of numbers to form a new base 10 number is just an exercise in learning/using rust’s excellent built-in methods:
|
|
Tests pass, the example input works, and when run with my own input, I get my first star. Happy days, until…
Part 2 - including digits spelled out with letters
OK, I admit, initially I thought part 2 would be a trivial addition but I failed to take into account my brain’s tendancy to confirm the first solution it finds.
I knew I wanted to reuse my first_n_last
function, and simply have a new parse_digits
function which would parse both digits and spelled-out words representing digits, so I started with a stubbed (todo!()
) parse_digits
function and a few test cases of what I expect to see:
|
|
Note the last test case above, which is me thinking my genius has realised the trick here: sevenine
will need to be parsed as vec!["7", "9"]
. Being very self-satisfied at noticing this, I scanned the other words and didn’t immediately see any other exceptions like this and continued on. This cost me time and frustration later when I couldn’t get the correct result for my actual input, even though it worked for the example input. Lesson for me to re-learn? When you find an exception, always check all permutations explicitly for other exceptions, or even better, find a generalisation that removes the need for exceptions. As it turns out, I missed the following additional test-cases initially:
|
|
Part 2 Step 1: parsing words as well as digits
I added a small function to alternately parse either a word digit or a numerical digit:
|
|
Note that this function (and the one below) needs to return a Vec<&str>
rather than just a single &str
because input such as sevenine
results in two numbers being returned by parse_word_digit
(though I could have also chosen to return "79"
I guess).
The actual implementation of parse_word_digit
is then pattern matching for quite a few alternatives (updated to include the extra alternatives I’d missed originally):
|
|
Part 2 Step 2: Checking for digits one char at a time…
This step introduced me to a new nom
function that I needed: many_till
which will continue consuming from the input (using the first provided argument) until the second provided argument is able to parse something. For this problem, I’m simply consuming a single character until I’m able to parse a word or number digit:
|
|
Note that we need to flatten the returned vector (that is, it’s a vector of vectors of calibrations and we want to flatten that result to a simple vector of calibrations) only because parse_word_or_num_digit
itself returns a vec of calibrations (for the exception cases of sevenine
etc).
With that we can then write a small parse_calibration_part2
function that calls parse_digits
and maps the result through first_n_last
to get the first and last digits the same as in part 1:
|
|
Aaaand with that, tests pass again and my part2 binary (virtually the same as the part1 one at the beginning) gives the correct answer.
What did I learn here?
So the main point that caused issues for me in this exercise was confirmation bias when I found what I thought was the (single) trick of overlapping word-digits (sevenine
) and checked too casually for other similar overlapping words, rather than exhaustively checking through the ten options. I need to reinforce that when I discover an interesting or unexpected case, that I exhaustively check for other instances of the same.
And I got to use a new nom
function, many_till
which will be incredibly helpful for other parsing situations here in the advent of code, I’m sure.