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"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
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-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
|
||||
[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
|
||||
[profile.dev.package.backtrace]
|
||||
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
|
||||
|
||||
Rusty edition.
|
||||
Rusty and over-engineered edition.
|
||||
|
||||
## 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.
|
||||
|
||||
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 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,
|
||||
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_STARTING_POSITION: i32 = 50;
|
||||
@@ -30,7 +30,7 @@ impl FromStr for Direction {
|
||||
}
|
||||
|
||||
#[instrument(skip(input))]
|
||||
fn part1(input: &str) -> Result<i32> {
|
||||
pub fn part1(input: &str) -> Result<i32> {
|
||||
let mut dial = LOCK_STARTING_POSITION;
|
||||
let mut visited_zero_count = 0;
|
||||
for line in input.trim().split('\n') {
|
||||
@@ -57,7 +57,7 @@ fn part1(input: &str) -> Result<i32> {
|
||||
}
|
||||
|
||||
#[instrument(skip(input))]
|
||||
fn part2(input: &str) -> Result<i32> {
|
||||
pub fn part2(input: &str) -> Result<i32> {
|
||||
let mut dial = LOCK_STARTING_POSITION;
|
||||
let mut visited_zero_count = 0;
|
||||
for line in input.trim().split('\n') {
|
||||
@@ -98,18 +98,6 @@ fn part2(input: &str) -> Result<i32> {
|
||||
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)]
|
||||
mod tests {
|
||||
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 tracing::info;
|
||||
use tracing_error::ErrorLayer;
|
||||
@@ -7,6 +10,21 @@ use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
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<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
@@ -20,10 +38,12 @@ fn main() -> Result<()> {
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
info!("Advent of Code 2025");
|
||||
{
|
||||
let _span = tracing::info_span!("aoc").entered();
|
||||
day01::solve()?;
|
||||
}
|
||||
let _span = tracing::info_span!("aoc").entered();
|
||||
|
||||
run_days(args.day, args.part)?;
|
||||
|
||||
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