Advent of Code in Rust for the Rest of Us
I’m not a competitive programmer – far from it. I never received formal education in advanced algorithms and data structures. In fact, I can’t even implement Dijkstra’s algorithm without referring to Wikipedia. Yet, I still participate in Advent of Code competitions. I’ve never made it onto an official leaderboard; I’m too slow for that. Completing all the puzzles a month behind schedule is pretty standard for me, and my solutions are rarely, if ever, the best in class. Sometimes, I compare them to those shared on GitHub, Reddit, or X, and mine are always longer, less efficient, and more ad hoc. But I still enjoy solving those puzzles. This blog post is for people like me who love the process of figuring out a solution and want to use RustRover, the JetBrains Rust IDE, to make that experience even better. Plus, there’s now the added incentive of winning a prize!
Why participate?
People are often motivated to challenge themselves in intellectual competitions, such as solving coding puzzles, for reasons that go beyond winning. Many find intrinsic satisfaction in the process, driven by curiosity, the desire for mastery, and the enjoyment of tackling tough problems. These challenges also promote personal growth, helping individuals overcome limits, track progress, and gain valuable feedback. Social connection plays a role too, as participants bond with like-minded communities, seek recognition, or collaborate with others. Additionally, these activities can serve practical purposes, like sharpening career-relevant skills or enhancing resumes, while also offering emotional rewards like the dopamine rush of solving a difficult problem. For many, it’s not about the outcome but the journey – learning, improving, and finding meaning in the struggle itself.
Participating in challenges like Advent of Code is not only a fun way to tackle interesting problems but also a practical opportunity to enhance technical skills. It’s an opportunity to learn a new programming language, exploring its standard library and the most important external libraries through hands-on problem-solving. These puzzles also help you refresh (or gain!) knowledge of algorithms and data structures, often pushing you to think creatively or even experiment with tools like Bevy Game Engine for visualizing solutions. Equally important is mastering the practical skills of using an IDE effectively – leveraging features like debugging, testing, refactoring, and code navigation to streamline your workflow. This blend of language learning, algorithmic thinking, and tooling expertise makes solving Advent of Code puzzles both educational and deeply rewarding.
Advent of Code trivia
Starting December 1, for 25 consecutive days, coding puzzles are published at Advent of Code. Each puzzle follows a storyline where a protagonist is tasked with solving technical issues. Every puzzle consists of two parts. Initially, only the first part is available, providing a problem description and input. The goal is to find a solution, usually an integer. To assist with testing, a smaller sample input and its correct result are provided. Once the solution to the actual input is ready, you submit it on the website. If the answer is correct, you gain access to the second part of the puzzle, which can feel like flipping the script – it often requires you to significantly reconsider your approach to the same input. The second part is typically more challenging. When the solution to the second part is ready and correctly submitted, you can officially call it a day (even if it’s already March).
Solving puzzles in RustRover
I’ve prepared this lightweight template to help you start solving Advent of Code puzzles in Rust using RustRover. There are two options for using this template:
- Using GitHub’s templating feature: Simply click the Use this template button on the repository page, create a new repository, and then open it in RustRover by selecting File | New | Project From Version Control….
- Adding the template to RustRover: You can integrate the template directly into RustRover and use the regular New Project wizard, as shown in the screenshot below.
Yes, I do consider solving Advent of Code puzzles my work.
The essence of the template is illustrated in the following screenshot:
Your input files should be named 01.txt, 02.txt, and so on, located in the input folder. Your solutions are expected to be written in files named 01.rs, 02.rs, and so on, within the src/bin folder. To create a new solution file quickly, you can simply copy and paste an existing template file (e.g. using Cmd+C and Cmd+V on macOS) and rename it appropriately:
Once you have a task for the day, you can copy your input to the corresponding input file, create a new solution file, and fill in the necessary details, such as the challenge day number, sample input, and expected answer for testing. I recommend checking my TODO
comments using RustRover’s TODO tool window (View | Tool Windows | TODO): the Current File tab will guide you to the relevant code fragments.
Here’s a fragment of my day puzzle starter code:
const DAY: &str = "NN"; // TODO: Fill the day const INPUT_FILE: &str = concatcp!("input/", DAY, ".txt"); const TEST: &str = "\ <TEST-INPUT> "; // TODO: Add the test input fn main() -> Result<()> { start_day(DAY); //region Part 1 println!("=== Part 1 ==="); fn part1<R: BufRead>(reader: R) -> Result<usize> { // TODO: Solve Part 1 of the puzzle let answer = reader.lines().flatten().count(); Ok(answer) } // TODO: Set the expected answer for the test input assert_eq!(0, part1(BufReader::new(TEST.as_bytes()))?); let input_file = BufReader::new(File::open(INPUT_FILE)?); let result = time_snippet!(part1(input_file)?); println!("Result = {}", result); //endregion //Part 2 Ok(()) }
The solution’s starter code reflects my personal preferences when solving Advent of Code puzzles:
- Input management: I keep the test input in the source file (so I can look at it quickly) and store the actual input in a separate data file. To read the input uniformly, I use
BufReader
to handle different data sources (e.g.&str
for a sample input or an actual file for the real input). - Independent parts: Although it’s often fine to parse the input once and use an intermediate representation for both parts of the puzzle, I prefer to treat the solutions for each part independently. This means both
part1
andpart2
start by reading the source fromBufReader
. I also like to keep solutions for both parts in the file. - Code folding: I rely heavily on RustRover’s code folding features to hide irrelevant details. Using
//region Part
and//endregion
comments creates easily foldable code sections in RustRover. Once I finish the first part, I collapse it and focus entirely on the second. - Common functions: The
start_day
function simply prints a puzzle header. It’s defined in src/lib.rs and serves as an example of a reusable function that can be used in any solution. By the end of December, there’s a good chance there will be additional common functions that are shared across multiple solutions. - Solution structure: Each part follows the same structured approach:
- Printing a header.
- Implementing a nested function with the complete solution logic. This is usually concise and calls helper functions for reading, processing, and computing the answer. These functions are defined below the main function.
- Adding assertions for one or two sample inputs provided in the puzzle description.
- Creating a data source for the actual input.
- Computing the puzzle answer (using a timing macro to measure and display computation time).
- Printing the computed answer.
I’ve added several dependencies to Cargo.toml, including:
- anyhow: This simplifies error handling when dealing with incorrect data formats or other issues. For Advent of Code puzzles, I believe it’s morally justifiable to largely ignore error handling for simplicity.
- code-timing-macros: This crate provides the convenient
time_snippet!
macro for lightweight and straightforward time measurement. - const_format: This makes it easier to initialize constants before the
main
function.
See the following section for my discussion and advice on using additional external libraries in your solutions.
I don’t usually use any special tooling for accessing puzzle descriptions and input files – my web browser does the job just fine. However, if you prefer working from the terminal, check out the aoc-cli crate, which provides a command-line interface to Advent of Code. If you’re looking for an even more heavyweight approach to Advent of Code, take a look at this template. I find it better suited for professionals, as it goes deeper into source code generation, benchmarking, and profiling your solutions. That said, it might feel like overkill for the rest of us.
Tips and tricks
Always use version control
Even if you don’t publish your solutions on GitHub, version control is essential. I typically commit my solutions multiple times throughout the process. Here’s my general workflow:
- Create a solution file for the day
- Commit
- Solve the first part
- Commit
- Refactor if needed
- Commit
- Solve the second part
- Commit
- Refactor if needed
- Commit
Why? Having the code committed means I can do anything with it. I can experiment, I can break it, and I can always return to a fixed state. Version control brings the joy of freedom when making changes to your code. The version history might not be the cleanest, but we’re not here for a perfect commit history, right?
Debug interactively
It’s tempting to rely on println!
or dbg!
for debugging all the time – they’re quick and easy, right? Maybe not. As I’ve already mentioned, Advent of Code is a great opportunity to master new tools, so why not explore interactive debugging? RustRover provides this functionality out of the box. Set a breakpoint in your code, perhaps inside a loop, and run your program in debug mode. When the program hits the breakpoint, it will pause execution, allowing you to explore variables and their values, evaluate expressions, and even use Step In or Step Over to track execution step by step. You can also mutate variables and replay the same code fragment without restarting the program. Need more? Set another breakpoint and continue execution. The program will suspend again when it reaches the new point. Interactive debugging is faster and more efficient because you don’t need to recompile or restart your program every time you add or adjust a println!
. Master interactive debugging, and you’ll never go back to relying on debug output.
Write tests to save time
Having a suite of tests for your Advent of Code solutions allows you to quickly validate changes, experiment with new approaches, and catch mistakes early. With tests in place, you can confidently refactor your code or try out optimizations without fear of breaking existing functionality. It’s an investment that pays off every time you need to debug, extend, or revisit your solution.
Compile with the release profile when time is an issue
Debug builds are great for development, but they come with overhead that can slow down execution, especially for more complex Advent of Code puzzles. Switching to the release profile optimizes your code for speed, ensuring your solutions run efficiently. It’s a simple adjustment that can save precious seconds – or even minutes – when dealing with resource-intensive computations.
It’s also easy with RustRover! Simply select release from the Cargo Profile dropdown and run your program again. This quick setup ensures your code runs with maximum performance, which is ideal for time-sensitive Advent of Code solutions.
Use external libraries for the boring stuff
Using external libraries for the less exciting parts of the challenge allows you to focus your energy on solving the actual puzzles. Many Advent of Code tasks involve repetitive or tedious operations, like parsing input or handling edge cases. Instead of reinventing the wheel, leverage crates from Rust’s rich ecosystem to handle these tasks efficiently. Libraries like regex or itertools can simplify your code and save valuable time. The less time you spend on boilerplate, the more time you can dedicate to crafting elegant solutions.
Iterators are, in general, a very powerful concept and highly suitable for Advent of Code puzzles. I recommend reading through my three-part series on iterators to gain a deeper understanding beyond the basics, learn best practices for structuring code with iterators, and discover some lesser-known iterator functionalities that can be incredibly useful for the tasks at hand. The itertools crate adds even more functionality for processing serial data.
As for regular expressions, RustRover recognizes them in your code and makes it easy to compose and verify them. Simply select your regex, then press Alt+Enter (Windows/Linux) or ⌥↵ (macOS) to access helpful tools for editing and testing:
In some puzzles, you might need a simulation or an algorithm that relies on randomized input, making the rand crate a valuable addition to your Cargo.toml.
If your Advent of Code solutions involve advanced number manipulation, the num
crate is your go-to solution. It extends Rust’s standard numeric capabilities with features like big integers, rationals, complex numbers, and more. Using num saves you from reinventing the wheel, so you can focus on solving the puzzle instead of building number utilities from scratch.
For puzzles involving linear algebra, matrices, or vector computations, the nalgebra crate is a lifesaver. Instead of writing custom code for mathematical operations, you can leverage this powerful library to handle even the most complex calculations. It’s perfect for tasks requiring geometric transformations or higher-dimensional data structures, allowing you to concentrate on the logic rather than the math.
When a problem involves graph structures – such as navigating nodes, finding shortest paths, or detecting cycles – the petgraph crate is a must. It provides efficient data structures and algorithms for working with graphs, eliminating the need to build your own graph utilities. With petgraph, you can tackle even the trickiest graph-related puzzles with ease. You can even visualize your graphs, which can be extremely helpful for finding solutions in some cases.
Want to bring a new dimension to your Advent of Code solutions? The bevy crate allows you to visualize solutions with a powerful game engine, making them not only functional but also visually engaging. Alternatively, for a lightweight and terminal-based approach, ratatui offers a simple way to build interactive user interfaces in the terminal. Whether you’re crafting stunning visualizations or enhancing interactivity, these crates add flair and fun to your solutions.
Some might ask: why spend time learning these crates instead of crafting simpler solutions manually? Others might even argue that using external libraries is unfair because they make solving the problem too easy. But remember why you’re doing this. If it’s about learning, then embrace the opportunity to learn. If it’s about fun, go ahead and implement everything yourself. It’s entirely up to you. Freedom!
AI or no AI?
Some might wonder: should you use AI tools to solve Advent of Code puzzles, or is that missing the point? While full-blown cloud-based code completion models can sometimes solve an entire puzzle outright, that might take away the fun and challenge. Instead, consider using more limited, local code completion tools to help you brainstorm or debug without bypassing the puzzle-solving process entirely.
RustRover runs local models on your code, providing useful suggestions even without the cloud-based AI Assistant plugin. If you prefer, you can disable this functionality. Simply go to Settings | Editor | General | Inline Completion and choose the setup that works best for you. The choice is yours!
AI can also be a great assistant for handling the boring parts of the puzzle, like implementing standard algorithms such as Dijkstra’s.Finally, there’s no need to keep revisiting Wikipedia for that! Let the AI handle the routine, so that you can focus on the creative and challenging aspects of the puzzle. Ultimately, the choice is yours. Strike a balance that makes the experience both rewarding and enjoyable!
Conclusion
Advent of Code is more than just a series of coding challenges – it’s an opportunity to grow as a programmer, explore new tools and techniques, and have fun along the way. Whether you’re solving puzzles to learn Rust, master algorithms, or simply enjoy the creative problem-solving process, there’s something for everyone. The journey may be challenging, but that’s exactly what makes it worthwhile.
From using RustRover to streamline your workflow to leveraging powerful crates and even exploring AI-assisted coding, Advent of Code encourages you to experiment and find the approach that works best for you. It’s not about writing perfect code or making the leaderboard. It’s about the joy of discovery, the thrill of overcoming tough problems, and the satisfaction of watching your skills evolve.
So, dive in! Whether you finish all the puzzles by December or months later, each one is a chance to learn something new and celebrate small victories. Grab your keyboard, fire up RustRover, and let’s make this Advent of Code a season of coding joy. You’ve got this!