This is an helper crate to help you solve [https://adventofcode.com/](Advent Of Code) problems in Rust. It contains utilities to run various repetitive tasks, such as input parsing, benchmarking, or testing.
The installation involves a few steps that need to be done once.
Add the following dependencies to your Rust project's Cargo.toml:
[dependencies]
aoc = { git = "https://github.com/evenfurther/aoc" }
[build-dependencies]
aoc-build = { git = "https://github.com/evenfurther/aoc" }In the same directory as your Cargo.toml file, create a build.rs file containing:
fn main() {
aoc_build::build().expect("Build error");
}Create src/main.rs, which takes care of running all solutions, or a selection of solutions, depending on the command line arguments:
fn main() -> eyre::Result<()> {
aoc::run(aoc2023::register::register_runners)
}Create src/lib.rs, which will include register.rs built by the build script:
#[macro_use]
extern crate aoc;
pub mod register {
include!(concat!(env!("OUT_DIR"), "/register.rs"));
}Let's implement the solution for day 1 of the current year.
Retrieve your input, and place it into input/day1.txt. This is where the runner will look for it.
At the end of src/lib.rs, add a new module day1:
pub mod day1;Create a new src/day1.rs file, in which you will place your solution.
Add the solution for part 1 of day 1, using the #aoc attribute to indicate that you are doing so. Behind the scenes, it will register the corresponding runner:
#[aoc(day1, part1)]
fn part1(input: &str) -> usize {
input.len()
}In our case, it assumes that the solution to the first problem is to give the length of the input. You can now run it:
$ cargo run
Day 1 - part 1: 42That's it. By default, it runs the solution for the current day. If you are working on the day 1 solution at a later time, you can specify the day to run:
$ cargo run -- --day 1
Day 1 - part 1: 42The -- is necessary to distinguish between arguments to cargo run and arguments to your program.
Of course, you might want to add --release if your solution takes time to compute:
$ cargo run --release -- --day 1
Day 1 - part 1: 42Note that --release must appear before --.
Once you have implemented your solution, you can add the expected outcome to a expected.txt file. This file will then be compared to the real execution when running cargo test (or cargo test --release if your algorithms take time).
When you add a new solution, the expected.txt file will be out-of-date. You can update it during cargo test by setting the RECORD_RESULTS environment variable to 1:
$ cargo test
running 1 test
test check_expected ... FAILED
failures:
---- check_expected stdout ----
Actual does not meet expected:
--- expected.txt 2023-12-04 11:32:54.051878291 +0100
+++ /tmp/5b5ff539bb01470198c092bad5f444ab 2023-12-04 11:32:56.410914647 +0100
@@ -0,0 +1,10 @@
+Day 1 - part 1: 232
$ RECORD_RESULTS=1 cargo test
running 1 test
test check_expected ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
$ cargo test
running 1 test
test check_expected ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sYou can then commit expected.txt to your version control system, and run the tests in your continuous integration framework if you use one.
In our example, part1() returns a usize. You can return any type implementing the Display trait. You can even return a Result as long as the error variant implements the Error trait:
Let us assume that we have added the eyre crate to our project:
#[aoc(day1, part1)]
fn part1(input: &str) -> eyre::Result<u32> {
Ok(input.parse()? * 2) // Return twice the number contained in the input
}The runner will automatically extract the result if everything goes well, or print the error otherwise.
By default, programs for day N will receive input retrieved from input/dayN.txt. You can choose another input using the --input command line argument.
For example, if you have stored the example given in the problem text into input/sample1.txt, you can run:
$ cargo run -- --input input/sample1.txt
Day 1 - Part 1: 17In our example, our solver receives the input as a &str, that is a single string containing the content of the input file input/day1.txt. However, this crate is able to split the input into lines automatically if the signature of your solver requires it:
#[aoc(day1, part1)]
fn part1(input: &[&str]) -> usize {
input.len() // Returns the number of lines
}Also, you can ask for any type implementing the FromStr trait. For example, let's assume we have an input file with two integers separated by "/" on every line:
struct Game {
left: u32,
right: u32
}
impl FromStr for Game {
type Error = eyre::Report;
fn from_str(line: &str) -> Result<Self, Self::Err> {
let Some((l, r)) = line.split_once('/')
else { eyre::bail!("unable to parse {}", line) };
Ok(Game {
left: l.parse()?,
right: r.parse()?,
})
}
}
#[aoc(day1, part1)]
fn part1(games: &[Game]) -> u32 {
// Return the sum of product of left and right for each game
games.iter().map(|g| g.left * g.right).sum()
}You don't have to take care of parsing the various Game structures found on every line, this framework will parse them for you.
In addition, inputs can be given as String, Vec<_> instead of slices, and so on. You can even mark them as mutable. For example, if you intend to work on the input strings and mangle them, you can do something like:
#[aoc(day1, part1)]
fn part1(mut input: Vec<String>) -> usize {
// Add a dummy last line, as directed in the instructions
input.push(String::from("Hello, world"));
// Add a 'x' at the end of every line
input.iter_mut().for_each(|l| l.push('x'));
// Do something
input.into_iter().map(|s| s.len()).sum()
}You get the idea.
You can get (very) basic timing information by using the --timing flag on the command line:
$ cargo run --release -- --timing --day 1
Day 1 - part 1 (23.96 µs): 42You might want to implement alternative ways of implementing a part, as shown in dummy-year/src/day1.rs:
#[aoc(day1, part1)]
fn part1(input: &str) -> usize {
2 * bytecount::count(input.as_bytes(), b'(') - input.trim().len()
}
#[aoc(day1, part1, str_slice)]
fn part1_string_slice(input: &[&str]) -> usize {
input.iter().copied().map(part1).sum()
}This will show up in runs:
$ cargo run
Day 1 - part 1: 232
Day 1 - part 1 — str_slice: 232You can use --main-only if you do not want to see the alternatives.
You can get help by using --help (don't forget the --, or you'll get cargo run's help message):
$ cargo run -- --help
Advent of Code
Usage: dummy-year [OPTIONS]
Options:
-a, --all Run all days
-d, --day <DAY> Use a specific day
-p, --part <PART> Restrict running to one part (1 or 2)
-t, --timing Show timing information
-m, --main-only Skip running any alternate version
-i, --input <INPUT> Use alternate input (file or string)
-h, --help Print help
-V, --version Print versionYou are welcome to contribute by sending bug reports and pull requests directed to the repository.