This year I participated the first time in the Advent of Code, a series of programming puzzles before Christmas. This post is an experience report of my 25+ day journey in December.
Spoiler alert: If you're still working or planning to work on the puzzles stop reading now.
From December 1 to December 25, a programming puzzle appears each day at Advent of Code website. To understand each day's two-part puzzle, go to the website and read the full description (for example Day 1: Report Repair).
I decided to write all solutions in Rust because I wanted to learn the language. This was my only "hard" requirement, the rest was just to have fun. After solving the first couple of puzzles I set the following boundaries for myself:
- Language: I write the solutions in Rust with minimal external dependencies. I used only the regex and itertools crates.
- Independent programs: Each day's solution is a separate, independent executable with no code reuse among the solutions.
- Tests: I always transform the provided examples into automated tests. Sometimes I add more tests, but I'm not strict about them. GitHub Actions run the tests on every commit.
- Explore: I rewrite code as I learn new features of the Rust language and its standard library. The code what you see now in the GitHub repository is often not my first attempt.
I also decided what were not my goals:
- No code golf: I wanted readable and concise solutions, but in no way I tried to minimize the lines of source code.
- Coding speed: I had no ambition (or chance) competing on the global leaderboard. I solved the problems at my own speed, next to my full-time job. Often it took me days to finish a puzzle.
- Code performance: I didn't measure or optimize my code's performance in any way. Nevertheless, the execution time of every solution is less than a few seconds.
Variations on Conway's Game of Life appeared on multiple days: on day 11 the cells evolved on a fixed grid, on day 17 in three and four dimensions, and on day 24 the solution was expected on a hexagonal grid. I enjoyed these problems and learned a lot about Rust iterators and iterator adapters.
Day 18 was about a small expression evaluator with custom precedence and associativity rules. I learned about Dijkstra's shunting yard algorithm and I used it to transform the expressions specified in infix notation to reverse Polish notation.
The second part of the puzzle on day 13 caused me headaches. Someone on a Reddit thread suggested that the Chinese remainder theorem is relevant here. I learned the basics of this theorem and recognized how to use it in the solution.
In the weeds
There were also days when the problem itself was not particularly difficult, but I still struggled. On these days I ended up with code that sort of works, but not particularly nice:
- Day 19: The problem input represented a language akin to regular expressions. My parsing code became quite messy, maybe I should have used actual regular expressions to parse the input file.
- Day 20: I made many mistakes during the implementation, the solution was tedious with much bookkeeping and little reward.
- Day 21: I couldn't connect to the problem because I found it too convoluted. After I looked at other people's solution it was clear that I was missing the underlying search algorithm therefore my implementation turned out to be less than great.
Solving these puzzles was great for learning Rust. From these last three weeks I'd underline these topics:
- Cargo: The cargo package manager works well and easy to use. The project was easy to start and the file layout is clear and simple.
- Testing: Rust has a built-in testing framework that integrates well with cargo. It's easy to add tests and maintain tests because the test code is close to the application code.
- Cargo watch: Cargo Watch can run the tests every time a change occurs in project's source. I only introduced this tool at day 16. I should have done it right at the beginning.
- Vec: For almost every puzzle I used Vec from the standard library.
- Iterators: I learned to use methods of the
Iterator trait. I
countalmost every day. Long iterator chains take a bit of getting used to, but they are idiomatic in Rust.
- itertools crate:
multi_cartesian_productfrom the itertools crate was a life-saver for implementing Game of Life in arbitrary dimensions.
- Regex crate: Well, regular expressions are just everywhere.
- enum: In Rust
enumis a real sum type where the variants can contain data too. I used an
enumas a central type on days 4, 8, 14, 18 and 19.
Completing the Advent of Code and learning a new programming language was challenging. I learned a lot in these last 25 days about puzzles, algorithms and about the Rust language.
I particularly enjoyed the puzzles where knowing an algorithm is rewarding and makes your code concise and readable. When my code is long and messy I know that I'm missing something: a data structure, an algorithm or a language feature. The hard part is to figure out which one.
The source code of all solutions are available on GitHub.