It's that time of the year when it's time to look back and think about how you did on this year's Advent of Code.
I got 43 stars, and I'm letting that be enough for this year. Some of the tasks towards the end were quite difficult and I don't want to spend too much time on it.
I chose to solve the problems in Rust. While Rust is my favorite language, it's not new to me anymore, and I know it quite well. The challenges were in figuring out the algorithms, not struggling with the language. I noticed that Rust's strictness worked well for me. The compiler noticed any mistakes I made, so once I got the code to run, it almost always did what I intended it to do. For many of the simpler tasks, that meant that I got the correct result the first time I ran the code.
I didn't go hardcore and implemented all of the algorithms myself. Instead I used some packages from Rust's rich ecosystem of crates (which existance is one of the reasons I like working with Rust to begin with).
Here are the ones I used. This list may be useful for you if there are crates here that you didn't know about. And it may be useful for me if you can suggest better alternatives if there are any. Please comment!
euclid
: 2D and 3D vectors
Euclid provides types such as
Point2D
and
Vector2D
,
Point3D
and
Vector3D
that are generic over the scalar type, so you can use them for both integer and floating point vectors.
In tasks where you work with coordinates, it's convenient to pass these types around
instead of tuples of (x, y) or (x, y, z).
They have the expected set of functionality like adding, subtracting and scaling
so you save some time and make the code clearer.
imgref
: 2D maps
When working with rectangular blocks of pixels,
it's common to store the colors of the pixels in a 1D vector,
and access them by the index [x + y * width]
.
The types in the imgref
crate wrap a Vec
(or other type of your choice)
together with the width and height,
so you don't have to pass around the Vec
, width, and height as separate arguments.
It also provides access to the pixels by x and y coordinates, with bounds checks,
so the code for accessing the pixels is more robust and clean.
While the primary intended use case for imgref
apparently is to keep track of pixel colors,
it works well for any type of rectangular data.
I used it whenever the input was some kind of 2D map.
Sometimes I used it to contain char
values taken directly from the input,
or — when I wanted the code to be a bit cleaner — with a custom enum.
itertools
: iterators and combinatorics
Itertools provides some functionality for iterators,
some of which you might expect to be built into the standard library,
as well as functions for iterating over combinations or permutations of
the data in an iterator.
It's a bit similar to its namesake itertools
in Python's standard library.
It has group_by
which allowed me to group the cards by their rank on day 7.
and
combinations_with_replacement
that let me easily try out different replacements for the joker cards.
The combinations
method provided a convenient way to iterate over all pairs of galaxies on
day 11.
I also used unique
in a couple of places. It filters out duplicates from an iterator,
without having you to sort the data first.
Lastly, itertools provides the method collect_vec()
as a faster way to write
collect::<Vec<_>>()
.
For Advent of Code it's pretty nice to save a few keystrokes here and there.
num
: numeric types
I haven't explored the num
crate. I only used it for the function
lcm
to calculate the lowest common multiple of the repeated lengths
on day 8:
repeat_lengths.iter().cloned().reduce(num::integer::lcm)
pathfinding
: Dijkstra and A*
This crates implements some common pathfinding algorithms.
I used
dijkstra_reach
to follow the pipe loop on
day 10,
and again on
day 21
to find where the garden elf could go in a given number of steps.
I also used it on
day 23
to find the longest path, but part 2 took over a minute to run,
so there surely must have been a better way to implement it than the way I did.
The crate naturally also provides astar
, which I used to navigate the factory city on
day 17.
regex
: parsing input
I often prefer splitting up the input lines with
split
,
split_once
,
split_ascii_whitespace
and other methods that work on strings instead of immediately reaching for regular expressions.
It's often less work that way and the resulting code is cleaner.
But for good or bad I used the following regular expressions:
-
Day 3:
\d+
and[^0123456789.]
to find parts and symbols respectively, -
Day 4:
^Card\s+(\d+): (.+) \| (.+)$
to parse a whole line, then splitting it up withstr::split_ascii_whitespace
, as you don't have to use regular expressions for everything, -
Day 5:
(.+)-to-(.+) map:
to parse a whole line, which turned out to be unnecessary, -
Day 8:
(...) = \((...), (...)\)
to parse the node instructions, -
Day 19:
([xmas])([<>])(\d+):(.+)
to parse the instructions, a case where the regular expression really felt like it helped, -
Day 20:
(.+) -> (.+)
to parse a line, where a regular expression looks overkill, but the split methods onstr
only work with a single char, not a string such as" -> "
, -
Day 24
[-]?\d+
withRegex::find_iter
to find all numbers in a line, skipping over any number of spaces or other characters. In production code I would have made it more robust by ensuring there were commas and@
signs in the expected places in the input. Probably by splitting on'@'
and then by the regular expression,\s*
.
A crate idea: Character map
Among the crates I wished existed but didn't find is something that would help me work with map data. In Advent of Code, a 2D map is typically provided in some kind of ASCII art, where one line provides one row of map data, and each character on that line is one tile in the map, like this map from day 21:
...........
.....###.#.
.###.##..#.
..#.#...#..
....#.#....
.##..S####.
.##..#...#.
.......##..
.##.#.####.
.##..##.##.
...........
It's easy to read this into a Vec<String>
but String
is not a very convenient type
to use for map data. As strings may contain UTF-8 encoded characters, getting a specific
character is not as simple as directly indexing into the raw bytes of the string.
You can do it, as the input is always ASCII, but Rust makes it a little bit inconvenient.
You can read the map as Vec<Vec<u8>>
instead, but then if you debug-print it,
it will come out as character codes in decimal instead of characters.
Converting it to Vec<Vec<char>>
doesn't give you an easy way to print it either.
I copied around a function called print_map
between the days, and adjusted it
This little hassle could be overcome with some helper library.
If it could map back and forth between readable/printable char
s and a custom enum, that would be great.
Maybe this will be something I'll work on in the coming year, in preparation for Advent of Code 2024.
Previous: Rust/C# hybrid in game project