Compare commits
17 Commits
db47fe7947
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9081354ce8 | |||
| 7a3463e8cc | |||
| 1311cb830b | |||
| 74c28c9344 | |||
| 9de4d77a63 | |||
| 8f4bc5b802 | |||
| de1e4b5b4f | |||
| df96d619b9 | |||
| 3a305e1bce | |||
| 7a3c36000c | |||
| 8e163be240 | |||
| 3d001c690f | |||
| 5433430628 | |||
| cdc0225ca8 | |||
| 9cce1d7fdb | |||
| e490bc99f5 | |||
| 952a851b41 |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[build]
|
||||||
|
rustflags = ["-C", "target-cpu=native", "-Cforce-frame-pointers=yes"]
|
||||||
1063
Cargo.lock
generated
1063
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@@ -4,14 +4,38 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
color-eyre = "0.6"
|
color-eyre = "0.6"
|
||||||
tracing = "0.1"
|
itertools = "0.14"
|
||||||
|
rayon = "1.11"
|
||||||
|
tracing = { version = "0.1", features = ["release_max_level_info"] }
|
||||||
tracing-error = "0.2"
|
tracing-error = "0.2"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
test-log = {version = "0.2", default-features = false, features = ["trace"]}
|
criterion = "0.5"
|
||||||
|
pprof = { version = "0.15" , features = ["flamegraph", "criterion"] }
|
||||||
|
test-log = { version = "0.2", default-features = false, features = ["trace"] }
|
||||||
|
|
||||||
# Improve perf on debug builds: https://docs.rs/color-eyre/latest/color_eyre/#improving-perf-on-debug-builds
|
# Improve perf on debug builds: https://docs.rs/color-eyre/latest/color_eyre/#improving-perf-on-debug-builds
|
||||||
[profile.dev.package.backtrace]
|
[profile.dev.package.backtrace]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
|
# Gotta go fast
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = "fat"
|
||||||
|
panic = "abort"
|
||||||
|
debug = false # set true for profiling
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "aoc"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "aoc_bin"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "aoc"
|
||||||
|
harness = false
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -1,15 +1,50 @@
|
|||||||
# Advent of Code 2025
|
# Advent of Code 2025
|
||||||
|
|
||||||
Rusty edition.
|
Rusty and over-engineered edition.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
By request of AoC creator, I haven't included the input files (e.g. src/input/day01.txt). Log into the Advent of Code site and save the inputs there to the src/input/ folder.
|
By request of AoC creator, I haven't included the input files (e.g. src/input/day01.txt). Log into the Advent of Code site and save the inputs there to the src/input/ folder.
|
||||||
|
|
||||||
Then to run: `cargo run`.
|
To run all days: `cargo run`.
|
||||||
|
|
||||||
|
To run a specific day and/or part: `cargo run -- --day 1 --part 1`.
|
||||||
|
|
||||||
To run in super-fast prod mode: `cargo run --release`.
|
To run in super-fast prod mode: `cargo run --release`.
|
||||||
|
|
||||||
To run with debug logs enabled: `RUST_LOG=debug cargo run`.
|
To run with debug logs enabled: `RUST_LOG=debug cargo run`.
|
||||||
|
|
||||||
To run the tests against included test input files: `RUST_LOG=debug cargo test -- --no-capture`.
|
To run all the tests against included test input files: `RUST_LOG=debug cargo test -- --no-capture`.
|
||||||
|
|
||||||
|
To run the tests for a specific day and/or part: `RUST_LOG=debug cargo test day01::test::test_part1 -- --no-capture`.
|
||||||
|
|
||||||
|
## Benchmarks
|
||||||
|
|
||||||
|
Because this is over-engineered, I've included benchmarks for each day's solution. Because, why not?
|
||||||
|
|
||||||
|
To run benchmarks: `cargo bench`. Or a specific day and/or part: `cargo bench -- "day02 part1"`.
|
||||||
|
|
||||||
|
### Results
|
||||||
|
|
||||||
|
These were all run on my personal machine, an AMD Ryzen 9 3900X 12-Core Processor with 32 GB RAM, on Linux (WSL), with nightly rust.
|
||||||
|
|
||||||
|
Timings are given as: [lower-bound **best-estimate** upper-bound]
|
||||||
|
|
||||||
|
| Day | Part 1 | Part 2 |
|
||||||
|
|-----|--------|--------|
|
||||||
|
| 01 | [79.998 µs **80.349 µs** 80.721 µs] | [76.289 µs **76.616 µs** 76.950 µs] |
|
||||||
|
| 02 | [2.0386 ms **2.0483 ms** 2.0584 ms] | [2.0823 ms **2.0918 ms** 2.1015 ms] |
|
||||||
|
| 03 | [45.711 µs **45.937 µs** 46.177 µs] | [267.18 µs **267.95 µs** 268.75 µs] |
|
||||||
|
| 04 | [143.40 µs **144.00 µs** 144.73 µs] | [1.6165 ms **1.6258 ms** 1.6355 ms] |
|
||||||
|
| 05 | [187.25 µs **188.93 µs** 190.74 µs] | [63.809 µs **64.204 µs** 64.606 µs] |
|
||||||
|
| 06 | [128.44 µs **129.44 µs** 130.52 µs] | [165.05 µs **165.70 µs** 166.36 µs] |
|
||||||
|
|
||||||
|
## Profiling
|
||||||
|
|
||||||
|
To aid in increasing performance, the `pprof` crate can be used to generate flamegraphs off of the benchmarks.
|
||||||
|
|
||||||
|
To run profiling across all benchmarks: `cargo bench --bench aoc -- --profile-time 10`.
|
||||||
|
|
||||||
|
To run profile the benchmark for a specific day and/or part: `cargo bench --bench aoc -- --profile-time 30 "day01 part1"`.
|
||||||
|
|
||||||
|
The flamegraphs will be generated in `target/criterion/<benchmark_name>/profile/flamegraph.svg`.
|
||||||
|
|||||||
36
benches/aoc.rs
Normal file
36
benches/aoc.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use pprof::criterion::{Output, PProfProfiler};
|
||||||
|
|
||||||
|
const PPROF_SAMPLING_FREQ_HZ: i32 = 997;
|
||||||
|
|
||||||
|
macro_rules! bench_days {
|
||||||
|
($($day_num:literal => $day_mod:ident),* $(,)?) => {
|
||||||
|
$(
|
||||||
|
mod $day_mod {
|
||||||
|
use super::*;
|
||||||
|
use aoc::$day_mod;
|
||||||
|
|
||||||
|
pub fn part1(c: &mut Criterion) {
|
||||||
|
c.bench_function(concat!(stringify!($day_mod), " part1"), |b| {
|
||||||
|
b.iter(|| $day_mod::part1($day_mod::INPUT))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn part2(c: &mut Criterion) {
|
||||||
|
c.bench_function(concat!(stringify!($day_mod), " part2"), |b| {
|
||||||
|
b.iter(|| $day_mod::part2($day_mod::INPUT))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
|
||||||
|
criterion_group! {
|
||||||
|
name = benches;
|
||||||
|
config = Criterion::default().with_profiler(PProfProfiler::new(PPROF_SAMPLING_FREQ_HZ, Output::Flamegraph(None)));
|
||||||
|
targets = $($day_mod::part1, $day_mod::part2),*
|
||||||
|
}
|
||||||
|
criterion_main!(benches);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
aoc::all_days!(bench_days);
|
||||||
@@ -4,9 +4,9 @@ use color_eyre::{
|
|||||||
Result,
|
Result,
|
||||||
eyre::{Error, eyre},
|
eyre::{Error, eyre},
|
||||||
};
|
};
|
||||||
use tracing::{debug, info, instrument};
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
const INPUT: &str = include_str!("input/input.txt");
|
pub const INPUT: &str = include_str!("input/input.txt");
|
||||||
|
|
||||||
const LOCK_SIZE: i32 = 100;
|
const LOCK_SIZE: i32 = 100;
|
||||||
const LOCK_STARTING_POSITION: i32 = 50;
|
const LOCK_STARTING_POSITION: i32 = 50;
|
||||||
@@ -30,7 +30,7 @@ impl FromStr for Direction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(input))]
|
#[instrument(skip(input))]
|
||||||
fn part1(input: &str) -> Result<i32> {
|
pub fn part1(input: &str) -> Result<i32> {
|
||||||
let mut dial = LOCK_STARTING_POSITION;
|
let mut dial = LOCK_STARTING_POSITION;
|
||||||
let mut visited_zero_count = 0;
|
let mut visited_zero_count = 0;
|
||||||
for line in input.trim().split('\n') {
|
for line in input.trim().split('\n') {
|
||||||
@@ -57,7 +57,7 @@ fn part1(input: &str) -> Result<i32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip(input))]
|
#[instrument(skip(input))]
|
||||||
fn part2(input: &str) -> Result<i32> {
|
pub fn part2(input: &str) -> Result<i32> {
|
||||||
let mut dial = LOCK_STARTING_POSITION;
|
let mut dial = LOCK_STARTING_POSITION;
|
||||||
let mut visited_zero_count = 0;
|
let mut visited_zero_count = 0;
|
||||||
for line in input.trim().split('\n') {
|
for line in input.trim().split('\n') {
|
||||||
@@ -98,18 +98,6 @@ fn part2(input: &str) -> Result<i32> {
|
|||||||
Ok(visited_zero_count)
|
Ok(visited_zero_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn solve() -> Result<()> {
|
|
||||||
info!("Day 1");
|
|
||||||
{
|
|
||||||
let _span = tracing::info_span!("day01").entered();
|
|
||||||
let p1 = part1(INPUT)?;
|
|
||||||
info!("Part 1: {}", p1);
|
|
||||||
let p2 = part2(INPUT)?;
|
|
||||||
info!("Part 2: {}", p2);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
1
src/day02/input/test1.txt
Normal file
1
src/day02/input/test1.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124
|
||||||
185
src/day02/mod.rs
Normal file
185
src/day02/mod.rs
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use color_eyre::{
|
||||||
|
Result,
|
||||||
|
eyre::{Error, eyre},
|
||||||
|
};
|
||||||
|
use rayon::prelude::*;
|
||||||
|
use tracing::{debug, debug_span, instrument};
|
||||||
|
|
||||||
|
pub const INPUT: &str = include_str!("input/input.txt");
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ProductRange(std::ops::RangeInclusive<i64>);
|
||||||
|
|
||||||
|
impl FromStr for ProductRange {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = s.split('-');
|
||||||
|
let start = parts
|
||||||
|
.next()
|
||||||
|
.ok_or(eyre!("Invalid product range: no start"))?
|
||||||
|
.parse::<i64>()?;
|
||||||
|
let end = parts
|
||||||
|
.next()
|
||||||
|
.ok_or(eyre!("Invalid product range: no end"))?
|
||||||
|
.parse::<i64>()?;
|
||||||
|
if parts.next().is_some() {
|
||||||
|
return Err(eyre!("Invalid product range: too many parts"));
|
||||||
|
}
|
||||||
|
Ok(ProductRange(start..=end))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ProductRange {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}-{}", self.0.start(), self.0.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for ProductRange {
|
||||||
|
type Item = i64;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.0.next()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.0.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProductRange {
|
||||||
|
fn invalid_ids(self) -> Result<Vec<i64>> {
|
||||||
|
let start = *self.0.start();
|
||||||
|
let end = *self.0.end();
|
||||||
|
|
||||||
|
let mut invalid_ids = Vec::new();
|
||||||
|
|
||||||
|
// Determine digit ranges we need to check
|
||||||
|
let start_digits = if start == 0 { 1 } else { start.ilog10() + 1 };
|
||||||
|
let end_digits = if end == 0 { 1 } else { end.ilog10() + 1 };
|
||||||
|
|
||||||
|
for num_digits in start_digits..=end_digits {
|
||||||
|
// Skip odd digit counts - they're all valid
|
||||||
|
if num_digits % 2 != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let half_digits = num_digits / 2;
|
||||||
|
let half_min = 10_i64.pow(half_digits - 1);
|
||||||
|
let half_max = 10_i64.pow(half_digits) - 1;
|
||||||
|
let multiplier = 10_i64.pow(half_digits) + 1; // Pre-calculate: half * multiplier = AABB pattern
|
||||||
|
|
||||||
|
// Generate all patterns where first half == second half
|
||||||
|
for half in half_min..=half_max {
|
||||||
|
let id = half * multiplier;
|
||||||
|
if id >= start && id <= end {
|
||||||
|
invalid_ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Invalid IDs: {:?}", &invalid_ids);
|
||||||
|
Ok(invalid_ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid_ids2(self) -> Result<Vec<i64>> {
|
||||||
|
let start = *self.0.start();
|
||||||
|
let end = *self.0.end();
|
||||||
|
|
||||||
|
let mut invalid_ids = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
// Determine digit ranges we need to check
|
||||||
|
let start_digits = if start == 0 { 1 } else { start.ilog10() + 1 };
|
||||||
|
let end_digits = if end == 0 { 1 } else { end.ilog10() + 1 };
|
||||||
|
|
||||||
|
for num_digits in start_digits..=end_digits {
|
||||||
|
// Try all possible chunk sizes that divide evenly
|
||||||
|
for chunk_size in 1..=num_digits / 2 {
|
||||||
|
if num_digits % chunk_size != 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let num_chunks = num_digits / chunk_size;
|
||||||
|
if num_chunks < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate all possible chunk patterns
|
||||||
|
let chunk_min = 10_i64.pow(chunk_size - 1);
|
||||||
|
let chunk_max = 10_i64.pow(chunk_size) - 1;
|
||||||
|
let chunk_power = 10_i64.pow(chunk_size);
|
||||||
|
|
||||||
|
// Calculate multiplier for repeating pattern
|
||||||
|
// For ABCABC: chunk * (10^6 + 10^3 + 1) = chunk * 1001001
|
||||||
|
let mut multiplier = 0_i64;
|
||||||
|
for i in 0..num_chunks {
|
||||||
|
multiplier += chunk_power.pow(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for chunk in chunk_min..=chunk_max {
|
||||||
|
let id = chunk * multiplier;
|
||||||
|
|
||||||
|
if id >= start && id <= end {
|
||||||
|
invalid_ids.insert(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let invalid_ids: Vec<i64> = invalid_ids.into_iter().collect();
|
||||||
|
|
||||||
|
debug!("Invalid IDs: {:?}", &invalid_ids);
|
||||||
|
Ok(invalid_ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part1(input: &str) -> Result<i64> {
|
||||||
|
input
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|range| {
|
||||||
|
let _span = debug_span!("range", range = %range).entered();
|
||||||
|
let range: ProductRange = range.parse()?;
|
||||||
|
Ok(range.invalid_ids()?.iter().sum::<i64>())
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part2(input: &str) -> Result<i64> {
|
||||||
|
input
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_par_iter()
|
||||||
|
.map(|range| {
|
||||||
|
let _span = debug_span!("range", range = %range).entered();
|
||||||
|
let range: ProductRange = range.parse()?;
|
||||||
|
Ok(range.invalid_ids2()?.iter().sum::<i64>())
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
const TEST_INPUT1: &str = include_str!("input/test1.txt");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part1() {
|
||||||
|
assert_eq!(part1(TEST_INPUT1).unwrap(), 1227775554);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part2() {
|
||||||
|
assert_eq!(part2(TEST_INPUT1).unwrap(), 4174379265);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/day03/input/test1.txt
Normal file
4
src/day03/input/test1.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
987654321111111
|
||||||
|
811111111111119
|
||||||
|
234234234234278
|
||||||
|
818181911112111
|
||||||
72
src/day03/mod.rs
Normal file
72
src/day03/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use color_eyre::Result;
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
pub const INPUT: &str = include_str!("input/input.txt");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct Battery {
|
||||||
|
column: usize,
|
||||||
|
joltage: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Battery {
|
||||||
|
fn default() -> Self {
|
||||||
|
Battery {
|
||||||
|
column: 0,
|
||||||
|
joltage: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn largest_output_joltage<const N: usize>(input: &str) -> Result<u64> {
|
||||||
|
let mut output_joltage: u64 = 0;
|
||||||
|
for line in input.trim().split('\n') {
|
||||||
|
let mut batteries: [Battery; N] = [Battery::default(); N];
|
||||||
|
let line_len = line.len();
|
||||||
|
for (column, joltage) in line.bytes().map(|c| c - b'0').enumerate() {
|
||||||
|
let min = N.saturating_sub(line_len - column);
|
||||||
|
for i in min..N {
|
||||||
|
if joltage > batteries[i].joltage {
|
||||||
|
batteries[i].column = column;
|
||||||
|
batteries[i].joltage = joltage;
|
||||||
|
batteries[i + 1..].fill(Battery::default());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let line_joltage = batteries
|
||||||
|
.iter()
|
||||||
|
.fold(0u64, |acc, &b| acc * 10 + b.joltage as u64);
|
||||||
|
debug!(line, line_joltage);
|
||||||
|
output_joltage += line_joltage;
|
||||||
|
}
|
||||||
|
Ok(output_joltage)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part1(input: &str) -> Result<u64> {
|
||||||
|
largest_output_joltage::<2>(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part2(input: &str) -> Result<u64> {
|
||||||
|
largest_output_joltage::<12>(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
const TEST_INPUT1: &str = include_str!("input/test1.txt");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part1() {
|
||||||
|
assert_eq!(part1(TEST_INPUT1).unwrap(), 357);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part2() {
|
||||||
|
assert_eq!(part2(TEST_INPUT1).unwrap(), 3121910778619);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/day04/input/test1.txt
Normal file
10
src/day04/input/test1.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
..@@.@@@@.
|
||||||
|
@@@.@.@.@@
|
||||||
|
@@@@@.@.@@
|
||||||
|
@.@@@@..@.
|
||||||
|
@@.@@@@.@@
|
||||||
|
.@@@@@@@.@
|
||||||
|
.@.@.@.@@@
|
||||||
|
@.@@@.@@@@
|
||||||
|
.@@@@@@@@.
|
||||||
|
@.@.@@@.@.
|
||||||
172
src/day04/mod.rs
Normal file
172
src/day04/mod.rs
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use color_eyre::{Result, eyre::eyre};
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
pub const INPUT: &str = include_str!("input/input.txt");
|
||||||
|
|
||||||
|
const ADJACENT_DELTAS: [(isize, isize); 8] = [
|
||||||
|
(-1, -1),
|
||||||
|
(-1, 0),
|
||||||
|
(-1, 1),
|
||||||
|
(0, -1),
|
||||||
|
(0, 1),
|
||||||
|
(1, -1),
|
||||||
|
(1, 0),
|
||||||
|
(1, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum Cell {
|
||||||
|
Empty,
|
||||||
|
Paper,
|
||||||
|
AccessiblePaper,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Cell {
|
||||||
|
type Err = color_eyre::Report;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"." => Ok(Cell::Empty),
|
||||||
|
"@" => Ok(Cell::Paper),
|
||||||
|
"x" => Ok(Cell::AccessiblePaper),
|
||||||
|
_ => Err(eyre!("Invalid cell character: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell {
|
||||||
|
fn from_byte(b: u8) -> Result<Self, color_eyre::Report> {
|
||||||
|
match b {
|
||||||
|
b'.' => Ok(Cell::Empty),
|
||||||
|
b'@' => Ok(Cell::Paper),
|
||||||
|
b'x' => Ok(Cell::AccessiblePaper),
|
||||||
|
_ => Err(eyre!("Invalid cell byte: {}", b)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Grid<const R: usize, const C: usize> {
|
||||||
|
cells: [[Cell; C]; R],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const R: usize, const C: usize> FromStr for Grid<R, C> {
|
||||||
|
type Err = color_eyre::Report;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut cells = [[Cell::Empty; C]; R];
|
||||||
|
|
||||||
|
for (row, line) in s.lines().enumerate() {
|
||||||
|
for (col, byte) in line.bytes().enumerate() {
|
||||||
|
cells[row][col] = Cell::from_byte(byte)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Grid { cells })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const R: usize, const C: usize> Display for Grid<R, C> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for row in 0..R {
|
||||||
|
for col in 0..C {
|
||||||
|
let symbol = match self.cells[row][col] {
|
||||||
|
Cell::Empty => '.',
|
||||||
|
Cell::Paper => '@',
|
||||||
|
Cell::AccessiblePaper => 'x',
|
||||||
|
};
|
||||||
|
write!(f, "{}", symbol)?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const R: usize, const C: usize> Grid<R, C> {
|
||||||
|
fn count_accessible_papers(&mut self, replace_with: Cell) -> usize {
|
||||||
|
let mut count = 0;
|
||||||
|
for row in 0..R {
|
||||||
|
for col in 0..C {
|
||||||
|
if self.cells[row][col] != Cell::Paper {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut adjacent_papers = 0;
|
||||||
|
for &(dr, dc) in &ADJACENT_DELTAS {
|
||||||
|
let adj_row = row as isize + dr;
|
||||||
|
let adj_col = col as isize + dc;
|
||||||
|
if adj_row >= 0
|
||||||
|
&& adj_col >= 0
|
||||||
|
&& (adj_row as usize) < R
|
||||||
|
&& (adj_col as usize) < C
|
||||||
|
{
|
||||||
|
let adjacent = self.cells[adj_row as usize][adj_col as usize];
|
||||||
|
if matches!(adjacent, Cell::Paper | Cell::AccessiblePaper) {
|
||||||
|
adjacent_papers += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if adjacent_papers < 4 {
|
||||||
|
self.cells[row][col] = replace_with;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solve_part1<const R: usize, const C: usize>(input: &str) -> Result<usize> {
|
||||||
|
let mut grid = Grid::<R, C>::from_str(input)?;
|
||||||
|
debug!("Parsed grid:\n{}", grid);
|
||||||
|
let count = grid.count_accessible_papers(Cell::AccessiblePaper);
|
||||||
|
debug!("Processed grid:\n{}", grid);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part1(input: &str) -> Result<usize> {
|
||||||
|
solve_part1::<135, 135>(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solve_part2<const R: usize, const C: usize>(input: &str) -> Result<usize> {
|
||||||
|
let mut grid = Grid::<R, C>::from_str(input)?;
|
||||||
|
debug!("Parsed grid:\n{}", grid);
|
||||||
|
let mut count = 0;
|
||||||
|
loop {
|
||||||
|
let removed = grid.count_accessible_papers(Cell::Empty);
|
||||||
|
if removed == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
debug!("Removed {} in grid:\n{}", removed, grid);
|
||||||
|
count += removed;
|
||||||
|
}
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part2(input: &str) -> Result<usize> {
|
||||||
|
solve_part2::<135, 135>(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
const TEST_INPUT1: &str = include_str!("input/test1.txt");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part1() {
|
||||||
|
assert_eq!(solve_part1::<10, 10>(TEST_INPUT1).unwrap(), 13);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part2() {
|
||||||
|
assert_eq!(solve_part2::<10, 10>(TEST_INPUT1).unwrap(), 43);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/day05/input/test1.txt
Normal file
11
src/day05/input/test1.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
3-5
|
||||||
|
10-14
|
||||||
|
16-20
|
||||||
|
12-18
|
||||||
|
|
||||||
|
1
|
||||||
|
5
|
||||||
|
8
|
||||||
|
11
|
||||||
|
17
|
||||||
|
32
|
||||||
125
src/day05/mod.rs
Normal file
125
src/day05/mod.rs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
|
use color_eyre::{
|
||||||
|
Result,
|
||||||
|
eyre::{Error, eyre},
|
||||||
|
};
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
pub const INPUT: &str = include_str!("input/input.txt");
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct FreshRange(pub std::ops::RangeInclusive<i64>);
|
||||||
|
|
||||||
|
impl FromStr for FreshRange {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut parts = s.split('-');
|
||||||
|
let start = parts
|
||||||
|
.next()
|
||||||
|
.ok_or(eyre!("Invalid fresh range: no start"))?
|
||||||
|
.parse::<i64>()?;
|
||||||
|
let end = parts
|
||||||
|
.next()
|
||||||
|
.ok_or(eyre!("Invalid fresh range: no end"))?
|
||||||
|
.parse::<i64>()?;
|
||||||
|
if parts.next().is_some() {
|
||||||
|
return Err(eyre!("Invalid fresh range: too many parts"));
|
||||||
|
}
|
||||||
|
Ok(FreshRange(start..=end))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FreshRange {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}-{}", self.0.start(), self.0.end())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part1(input: &str) -> Result<usize> {
|
||||||
|
let mut processing_ranges = true;
|
||||||
|
let mut fresh_ranges = Vec::new();
|
||||||
|
let mut fresh_ingredients = 0;
|
||||||
|
for line in input.trim().lines() {
|
||||||
|
if line.is_empty() {
|
||||||
|
processing_ranges = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if processing_ranges {
|
||||||
|
let range = line.parse::<FreshRange>()?;
|
||||||
|
debug!(range = %range);
|
||||||
|
fresh_ranges.push(range);
|
||||||
|
} else {
|
||||||
|
let ingredient = line.parse::<i64>()?;
|
||||||
|
debug!(ingredient);
|
||||||
|
for range in &mut fresh_ranges {
|
||||||
|
if range.0.contains(&ingredient) {
|
||||||
|
fresh_ingredients += 1;
|
||||||
|
debug!(fresh_ingredients, "fresh!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(fresh_ingredients)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part2(input: &str) -> Result<usize> {
|
||||||
|
let mut fresh_ranges: Vec<Option<FreshRange>> = Vec::new();
|
||||||
|
for line in input.trim().lines() {
|
||||||
|
if line.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let range = line.parse::<FreshRange>()?;
|
||||||
|
let mut overlap_range = range.clone();
|
||||||
|
debug!(range = %range);
|
||||||
|
for range_slot in fresh_ranges.iter_mut() {
|
||||||
|
if let Some(existing_range) = range_slot {
|
||||||
|
if overlap_range.0.end() < existing_range.0.start()
|
||||||
|
|| overlap_range.0.start() > existing_range.0.end()
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let start = *overlap_range.0.start().min(existing_range.0.start());
|
||||||
|
let end = *overlap_range.0.end().max(existing_range.0.end());
|
||||||
|
if start <= end {
|
||||||
|
overlap_range = FreshRange(start..=end);
|
||||||
|
debug!(overlap_range = %overlap_range, existing_range = %existing_range, "merging existing range");
|
||||||
|
*range_slot = None; // this existing range is now completely merged with the current range
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fresh_ranges.push(Some(overlap_range));
|
||||||
|
}
|
||||||
|
Ok(fresh_ranges
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.map(|r| (r.0.end() - r.0.start() + 1) as usize)
|
||||||
|
.sum())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
const TEST_INPUT1: &str = include_str!("input/test1.txt");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part1() {
|
||||||
|
assert_eq!(part1(TEST_INPUT1).unwrap(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part2() {
|
||||||
|
assert_eq!(part2(TEST_INPUT1).unwrap(), 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part2_triple_overlap() {
|
||||||
|
assert_eq!(part2("3-4\n2-5\n1-6").unwrap(), 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/day06/input/test1.txt
Normal file
4
src/day06/input/test1.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
123 328 51 64
|
||||||
|
45 64 387 23
|
||||||
|
6 98 215 314
|
||||||
|
* + * +
|
||||||
212
src/day06/mod.rs
Normal file
212
src/day06/mod.rs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use color_eyre::{
|
||||||
|
Result,
|
||||||
|
eyre::{Context, Error, OptionExt, eyre},
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use tracing::{debug, instrument};
|
||||||
|
|
||||||
|
pub const INPUT: &str = include_str!("input/input.txt");
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Operation {
|
||||||
|
Add,
|
||||||
|
Multiply,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Operation {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"+" => Ok(Operation::Add),
|
||||||
|
"*" => Ok(Operation::Multiply),
|
||||||
|
_ => Err(eyre!("invalid operation: {}", s)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Operation {
|
||||||
|
fn from_byte(byte: u8) -> Result<Self> {
|
||||||
|
match byte {
|
||||||
|
b'+' => Ok(Operation::Add),
|
||||||
|
b'*' => Ok(Operation::Multiply),
|
||||||
|
_ => Err(eyre!("invalid operation byte: {}", byte)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Problem {
|
||||||
|
numbers: Vec<u64>,
|
||||||
|
operation: Operation,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct CephalopodProblem {
|
||||||
|
columns: Vec<Vec<u8>>,
|
||||||
|
operation: Operation,
|
||||||
|
width: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part1(input: &str) -> Result<u64> {
|
||||||
|
let mut lines = input.trim().lines();
|
||||||
|
let first_numbers: Vec<u64> = lines
|
||||||
|
.next()
|
||||||
|
.ok_or_eyre("no first line in input")?
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|s| s.parse::<u64>().context("parsing number"))
|
||||||
|
.collect::<Result<Vec<u64>>>()?;
|
||||||
|
let mut problems = first_numbers
|
||||||
|
.into_iter()
|
||||||
|
.map(|n| Problem {
|
||||||
|
numbers: vec![n],
|
||||||
|
operation: Operation::Add,
|
||||||
|
})
|
||||||
|
.collect::<Vec<Problem>>();
|
||||||
|
for line in lines {
|
||||||
|
let first_byte = line.bytes().nth(0).ok_or_eyre("empty line in input")?;
|
||||||
|
if matches!(first_byte, b'*' | b'+') {
|
||||||
|
for (index, op_result) in line.split_whitespace().map(|s| s.parse()).enumerate() {
|
||||||
|
let op = op_result?;
|
||||||
|
problems[index].operation = op;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (index, num_result) in line.split_whitespace().map(|s| s.parse()).enumerate() {
|
||||||
|
let num = num_result?;
|
||||||
|
problems[index].numbers.push(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(problems
|
||||||
|
.into_iter()
|
||||||
|
.map(|problem| match problem.operation {
|
||||||
|
Operation::Add => {
|
||||||
|
let sum = problem.numbers.iter().sum::<u64>();
|
||||||
|
debug!("{} = {}", problem.numbers.iter().join(" + "), sum);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
Operation::Multiply => {
|
||||||
|
let product = problem.numbers.iter().product::<u64>();
|
||||||
|
debug!("{} = {}", problem.numbers.iter().join(" * "), product);
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
pub fn part2(input: &str) -> Result<usize> {
|
||||||
|
let mut lines = input.lines();
|
||||||
|
let last_line = lines.next_back().ok_or_eyre("no last line in input")?;
|
||||||
|
let mut problems = Vec::new();
|
||||||
|
let mut spaces: u8 = 1;
|
||||||
|
for byte in last_line.bytes().rev() {
|
||||||
|
if byte.is_ascii_whitespace() {
|
||||||
|
spaces += 1;
|
||||||
|
} else {
|
||||||
|
problems.push(CephalopodProblem {
|
||||||
|
columns: (0..spaces).map(|_| Vec::new()).collect(),
|
||||||
|
operation: Operation::from_byte(byte)?,
|
||||||
|
width: spaces,
|
||||||
|
});
|
||||||
|
spaces = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!(last_problem = ?problems[0]);
|
||||||
|
debug!(second_last_problem = ?problems[1]);
|
||||||
|
problems.reverse();
|
||||||
|
for line in lines {
|
||||||
|
let bytes = line.as_bytes();
|
||||||
|
let mut offset = 0;
|
||||||
|
for i in 0..problems.len() {
|
||||||
|
if offset + (problems[i].width as usize) > bytes.len() {
|
||||||
|
return Err(eyre!("line too short for problem columns"));
|
||||||
|
}
|
||||||
|
for (cell_col, &byte) in bytes[offset..offset + problems[i].width as usize]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
if !byte.is_ascii_whitespace() {
|
||||||
|
problems[i].columns[cell_col].push(byte - b'0');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
offset += problems[i].width as usize + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!(first_problem = ?problems[0]);
|
||||||
|
debug!(second_problem = ?problems[1]);
|
||||||
|
Ok(problems
|
||||||
|
.iter()
|
||||||
|
.map(|problem| match problem.operation {
|
||||||
|
Operation::Add => {
|
||||||
|
let sum: usize = problem
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.map(|col| {
|
||||||
|
col.iter()
|
||||||
|
.fold(0usize, |acc, &digit| acc * 10 + (digit as usize))
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
|
debug!(
|
||||||
|
"{} = {}",
|
||||||
|
problem
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.map(|col| {
|
||||||
|
col.iter()
|
||||||
|
.fold(0usize, |acc, &digit| acc * 10 + (digit as usize))
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.join(" + "),
|
||||||
|
sum
|
||||||
|
);
|
||||||
|
sum
|
||||||
|
}
|
||||||
|
Operation::Multiply => {
|
||||||
|
let product: usize = problem
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.map(|col| {
|
||||||
|
col.iter()
|
||||||
|
.fold(0usize, |acc, &digit| acc * 10 + (digit as usize))
|
||||||
|
})
|
||||||
|
.product();
|
||||||
|
debug!(
|
||||||
|
"{} = {}",
|
||||||
|
problem
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.map(|col| {
|
||||||
|
col.iter()
|
||||||
|
.fold(0usize, |acc, &digit| acc * 10 + (digit as usize))
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
.join(" * "),
|
||||||
|
product
|
||||||
|
);
|
||||||
|
product
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use test_log::test;
|
||||||
|
|
||||||
|
const TEST_INPUT1: &str = include_str!("input/test1.txt");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part1() {
|
||||||
|
assert_eq!(part1(TEST_INPUT1).unwrap(), 4277556);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_part2() {
|
||||||
|
assert_eq!(part2(TEST_INPUT1).unwrap(), 3263827);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/days.rs
Normal file
16
src/days.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Single source of truth for all implemented days
|
||||||
|
// Add new days here and they'll automatically be available in both the runner and benchmarks
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! all_days {
|
||||||
|
($macro_name:path) => {
|
||||||
|
$macro_name! {
|
||||||
|
1 => day01,
|
||||||
|
2 => day02,
|
||||||
|
3 => day03,
|
||||||
|
4 => day04,
|
||||||
|
5 => day05,
|
||||||
|
6 => day06,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
7
src/lib.rs
Normal file
7
src/lib.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod day01;
|
||||||
|
pub mod day02;
|
||||||
|
pub mod day03;
|
||||||
|
pub mod day04;
|
||||||
|
pub mod day05;
|
||||||
|
pub mod day06;
|
||||||
|
pub mod days;
|
||||||
30
src/main.rs
30
src/main.rs
@@ -1,5 +1,8 @@
|
|||||||
pub mod day01;
|
mod days;
|
||||||
|
mod runner;
|
||||||
|
|
||||||
|
use aoc::*;
|
||||||
|
use clap::Parser;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
use tracing_error::ErrorLayer;
|
use tracing_error::ErrorLayer;
|
||||||
@@ -7,6 +10,21 @@ use tracing_subscriber::EnvFilter;
|
|||||||
use tracing_subscriber::fmt::format::FmtSpan;
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "Advent of Code 2025")]
|
||||||
|
#[command(about = "Solutions for Advent of Code 2025", long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Day to run (1-25). If not specified, runs all days.
|
||||||
|
#[arg(short, long)]
|
||||||
|
day: Option<u8>,
|
||||||
|
|
||||||
|
/// Part to run (1 or 2). If not specified, runs all parts.
|
||||||
|
#[arg(short, long)]
|
||||||
|
part: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
all_days!(runner::days);
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
color_eyre::install()?;
|
color_eyre::install()?;
|
||||||
|
|
||||||
@@ -20,10 +38,12 @@ fn main() -> Result<()> {
|
|||||||
)
|
)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
info!("Advent of Code 2025");
|
info!("Advent of Code 2025");
|
||||||
{
|
let _span = tracing::info_span!("aoc").entered();
|
||||||
let _span = tracing::info_span!("aoc").entered();
|
|
||||||
day01::solve()?;
|
run_days(args.day, args.part)?;
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/runner.rs
Normal file
57
src/runner.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use color_eyre::Result;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
macro_rules! days {
|
||||||
|
($($day_num:literal => $day_mod:ident),* $(,)?) => {
|
||||||
|
pub fn run_days(day: Option<u8>, part: Option<u8>) -> Result<()> {
|
||||||
|
match day {
|
||||||
|
$(
|
||||||
|
Some($day_num) => $crate::runner::run_day($day_num, part, $day_mod::part1, $day_mod::part2, $day_mod::INPUT)?,
|
||||||
|
)*
|
||||||
|
Some(d) => color_eyre::eyre::bail!("Day {} is not yet implemented", d),
|
||||||
|
None => {
|
||||||
|
$(
|
||||||
|
$crate::runner::run_day($day_num, None, $day_mod::part1, $day_mod::part2, $day_mod::INPUT)?;
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use days;
|
||||||
|
|
||||||
|
pub fn run_day<T1, T2>(
|
||||||
|
day: u8,
|
||||||
|
part: Option<u8>,
|
||||||
|
part1_fn: fn(&str) -> Result<T1>,
|
||||||
|
part2_fn: fn(&str) -> Result<T2>,
|
||||||
|
input: &str,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
T1: std::fmt::Display,
|
||||||
|
T2: std::fmt::Display,
|
||||||
|
{
|
||||||
|
info!("Day {}", day);
|
||||||
|
let day_name = format!("{:02}", day);
|
||||||
|
let _span = tracing::info_span!("day", day = %day_name).entered();
|
||||||
|
|
||||||
|
if part.is_none() || part == Some(1) {
|
||||||
|
let result = part1_fn(input)?;
|
||||||
|
info!("Part 1: {}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if part.is_none() || part == Some(2) {
|
||||||
|
let result = part2_fn(input)?;
|
||||||
|
info!("Part 2: {}", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(p) = part {
|
||||||
|
if p != 1 && p != 2 {
|
||||||
|
color_eyre::eyre::bail!("Part {} is invalid. Must be 1 or 2.", p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user