Rust crates I used for Advent of Code 2023

Posted by Martin Vilcans on 27 December 2023

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.

My results on Advent of Code 2023

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 with str::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 on str only work with a single char, not a string such as " -> ",

  • Day 24 [-]?\d+ with Regex::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 chars 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

Comments disabled on this post.