Compare commits

..

31 Commits

Author SHA1 Message Date
185c082764
chore: run benchmarks 2025-12-11 21:32:17 +02:00
ea4cfa28eb
Finish part-2 2025-12-11 21:31:33 +02:00
a2ccf9b271
Finish day-11 part-1 2025-12-11 20:55:46 +02:00
d236d75bf4
chore: run benchmarks 2025-12-10 20:16:55 +02:00
d54f976f07
Finish part-1 2025-12-10 20:12:28 +02:00
51ca0e1215
Init day-10 2025-12-10 10:07:29 +02:00
18dd729a49
chore: run benchmarks 2025-12-09 09:13:52 +02:00
9d78fedb86
fix: benchmarking files 2025-12-09 09:13:43 +02:00
ecdd78ed14
Finish part-1 2025-12-09 09:05:11 +02:00
838f24ea5d
Init day-09 2025-12-09 08:11:32 +02:00
056a1a1208
Finish part-2 2025-12-08 08:46:09 +02:00
43c57b58df
Finish day-08 part-1 2025-12-08 08:42:26 +02:00
6cac42e2df
Finish part-2 2025-12-07 14:28:55 +02:00
41d1feffc2
Finish part-1 2025-12-07 14:21:58 +02:00
770d09d42b
Init day-07 2025-12-07 13:53:01 +02:00
92ef093b85
Finish day-06 part-1 2025-12-06 08:30:25 +02:00
0ab0cf8bf8
Finish part-2 2025-12-05 20:07:19 +02:00
3deee94f8a
Finish day-05 part-1 2025-12-05 19:50:31 +02:00
71ac0a4c35
Finish part-2 2025-12-04 16:31:31 +02:00
4a259655e2
Finish day-04 part1 2025-12-04 11:56:55 +02:00
3cfc40324c
Finish part-2 2025-12-03 17:47:18 +02:00
6330105eeb
Finish part-1 2025-12-03 17:21:19 +02:00
2ae6b63d6d
Init day-03 2025-12-03 16:25:09 +02:00
3449dba83e
Finish part-2 2025-12-02 14:48:38 +02:00
007a9fabd5
Finish part-1 2025-12-02 14:44:40 +02:00
05863f1bf8
fix: update year 2025-12-02 11:29:40 +02:00
c62b63462c
Init day-02 2025-12-02 11:27:51 +02:00
1c4b2188f1
Finish part-01 2025-12-01 19:44:15 +02:00
1dcdb86bed
Finish part-1 2025-12-01 19:16:34 +02:00
2d34e14f7d
Init day-01 2025-12-01 13:50:14 +02:00
dc25916f7b
Init 2025 2025-12-01 13:50:08 +02:00
102 changed files with 4539 additions and 0 deletions

3
2025/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
.env
input*.txt

1038
2025/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

50
2025/Cargo.toml Normal file
View File

@ -0,0 +1,50 @@
[workspace]
members = [
"day-*",
"day-01",
"day-02",
"day-03",
"day-04",
"day-05",
"day-06",
"day-07",
"day-08",
"day-09",
"day-10",
"day-11",
]
default-members = ["day-*"]
resolver = "2"
[workspace.dependencies]
divan = "0.1"
glam = "0.30"
itertools = "0.14"
nom = "8.0"
nom-supreme = "0.8"
nom_locate = "5.0"
rayon = "1.11"
rstest = "0.26"
rstest_reuse = "0.7"
thiserror = "2.0"
tracing = "0.1"
[workspace.dependencies.miette]
version = "7.6"
features = ["fancy"]
[workspace.dependencies.test-log]
version = "0.2"
features = ["trace"]
default-features = false
[workspace.dependencies.tracing-subscriber]
version = "0.3"
features = [
"fmt",
"env-filter",
]
[workspace.lints.clippy]
nursery = "warn"
pedantic = "warn"

113
2025/benchmarks.txt Normal file
View File

@ -0,0 +1,113 @@
running 2 tests
ii
test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
day_01_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 102.2 µs │ 158 µs │ 104.7 µs │ 107.5 µs │ 100 │ 100
╰─ part2 112.4 µs │ 142.1 µs │ 113.3 µs │ 114.5 µs │ 100 │ 100
running 22 tests
iiiiiiiiiiiiiiiiiiiiii
test result: ok. 0 passed; 0 failed; 22 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
day_02_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 95.12 ms │ 98.88 ms │ 95.83 ms │ 96.01 ms │ 100 │ 100
╰─ part2 94.67 ms │ 99.18 ms │ 95.81 ms │ 96.01 ms │ 100 │ 100
running 2 tests
ii
test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
day_03_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 199.8 µs │ 227.2 µs │ 211.5 µs │ 212 µs │ 100 │ 100
╰─ part2 183.5 µs │ 315.2 µs │ 199.9 µs │ 201.5 µs │ 100 │ 100
running 2 tests
ii
test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
day_04_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 315 µs │ 328.9 µs │ 322.7 µs │ 322.6 µs │ 100 │ 100
╰─ part2 13.57 ms │ 15.01 ms │ 14.44 ms │ 14.45 ms │ 100 │ 100
running 2 tests
ii
test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
day_05_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 54.38 µs │ 111.6 µs │ 57.5 µs │ 58.85 µs │ 100 │ 100
╰─ part2 6.472 µs │ 12.59 µs │ 6.631 µs │ 6.707 µs │ 100 │ 100
running 2 tests
ii
test result: ok. 0 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
day_06_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 46.83 µs │ 94.14 µs │ 47.75 µs │ 49.44 µs │ 100 │ 100
╰─ part2

View File

@ -0,0 +1,27 @@
[package]
name = "{{project-name}}"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "{{project-name}}-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

View File

@ -0,0 +1,15 @@
use {{crate_name}}::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

View File

@ -0,0 +1,12 @@
use {{crate_name}}::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use {{crate_name}}::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

View File

@ -0,0 +1,22 @@
use miette::miette;
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
todo!("day xx - part 1");
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "";
todo!("haven't built test yet");
let result = 0;
assert_eq!(process(input)?, result);
Ok(())
}
}

View File

@ -0,0 +1,23 @@
use miette::miette;
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
todo!("day xx - part 2");
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "";
todo!("haven't built test yet");
let result = 0;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-01/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-01"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-01-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-01/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_01_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 128.6 µs │ 641 µs │ 129 µs │ 146.8 µs │ 100 │ 100
╰─ part2 138.3 µs │ 471.2 µs │ 139.5 µs │ 161.4 µs │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_01::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_01::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_01::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-01/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

106
2025/day-01/src/part1.rs Normal file
View File

@ -0,0 +1,106 @@
use miette::Result;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
Left,
Right,
}
impl FromStr for Direction {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().trim() {
"l" => Ok(Self::Left),
"r" => Ok(Self::Right),
_ => Err("Wrong value".to_string()),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Rotation {
amount: i32,
direction: Direction,
}
impl Rotation {
const fn right(self, dial: i32) -> i32 {
(dial + self.amount).rem_euclid(100)
}
const fn left(self, dial: i32) -> i32 {
(dial - self.amount).rem_euclid(100)
}
}
impl FromStr for Rotation {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let trimmed = s.trim();
let (direction_str, amount_str) = trimmed.split_at(1);
let direction = direction_str.parse()?;
let amount = amount_str.parse::<i32>().map_err(|e| e.to_string())?;
Ok(Self { amount, direction })
}
}
#[derive(Debug, Clone)]
struct Sequence {
rotations: Vec<Rotation>,
}
impl FromStr for Sequence {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let rotations = s
.trim()
.lines()
.map(|line| line.parse::<Rotation>().unwrap())
.collect::<Vec<_>>();
Ok(Self { rotations })
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> Result<usize> {
let sequence = input.parse::<Sequence>().unwrap();
let mut count = 0;
sequence.rotations.iter().fold(50, |acc, rotation| {
let number = match rotation.direction {
Direction::Left => rotation.left(acc),
Direction::Right => rotation.right(acc),
};
if number == 0 {
count += 1;
}
number
});
Ok(count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> Result<()> {
let input = "L68
L30
R48
L5
R60
L55
L1
L99
R14
L82
";
let result = 3;
assert_eq!(process(input)?, result);
Ok(())
}
}

124
2025/day-01/src/part2.rs Normal file
View File

@ -0,0 +1,124 @@
use miette::Result;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq)]
enum Direction {
Left,
Right,
}
impl FromStr for Direction {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().trim() {
"l" => Ok(Self::Left),
"r" => Ok(Self::Right),
_ => Err("Wrong value".to_string()),
}
}
}
#[derive(Debug, Clone, Copy)]
struct Rotation {
amount: i32,
direction: Direction,
}
impl FromStr for Rotation {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let trimmed = s.trim();
let (direction_str, amount_str) = trimmed.split_at(1);
let direction = direction_str.parse()?;
let amount = amount_str.parse::<i32>().map_err(|e| e.to_string())?;
Ok(Self { amount, direction })
}
}
#[derive(Debug, Clone)]
struct Sequence {
rotations: Vec<Rotation>,
}
impl FromStr for Sequence {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let rotations = s
.trim()
.lines()
.map(|line| line.parse::<Rotation>().unwrap())
.collect::<Vec<_>>();
Ok(Self { rotations })
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> Result<usize> {
let sequence = input.parse::<Sequence>().unwrap();
let (_, total_zeros) = sequence
.rotations
.iter()
.fold((50, 0), |(dial, total), rotation| {
let new_dial = match rotation.direction {
Direction::Left => (dial - rotation.amount).rem_euclid(100),
Direction::Right => (dial + rotation.amount).rem_euclid(100),
};
let zeros_crossed = count_zeros_crossed(dial, rotation.amount, rotation.direction);
let new_total = total + zeros_crossed;
(new_dial, new_total)
});
Ok(total_zeros.try_into().unwrap())
}
const fn count_zeros_crossed(pos: i32, amount: i32, direction: Direction) -> i32 {
match direction {
Direction::Left => {
if pos == 0 {
amount / 100
} else if amount >= pos {
1 + (amount - pos) / 100
} else {
0
}
}
Direction::Right => {
if pos == 0 {
amount / 100
} else {
let target = 100 - pos;
if amount >= target {
1 + (amount - target) / 100
} else {
0
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> Result<()> {
let input = "L68
L30
R48
L5
R60
L55
L1
L99
R14
L82
";
let result = 6;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-02/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-02"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-02-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-02/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_02_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 131.4 ms │ 141.6 ms │ 134.1 ms │ 134.5 ms │ 100 │ 100
╰─ part2 134.5 ms │ 141.5 ms │ 137.7 ms │ 137.6 ms │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_02::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_02::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_02::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-02/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

102
2025/day-02/src/part1.rs Normal file
View File

@ -0,0 +1,102 @@
use miette::Result;
use std::str::FromStr;
#[derive(Debug)]
struct Id(usize);
impl FromStr for Id {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let val = s.parse::<usize>().map_err(|e| e.to_string())?;
Ok(Self(val))
}
}
#[derive(Debug)]
struct Range {
start: Id,
end: Id,
}
impl Range {
fn find_invalid(&self) -> Vec<Id> {
(self.start.0..=self.end.0)
.filter_map(|x| {
if has_repeating_sequence(x) {
Some(Id(x))
} else {
None
}
})
.collect()
}
}
fn has_repeating_sequence(num: usize) -> bool {
let s = num.to_string();
let len = s.len();
(1..=len / 2).any(|pattern_len| {
let repeats = len / pattern_len;
repeats >= 2
&& len.is_multiple_of(pattern_len)
&& s[0..pattern_len].repeat(len / pattern_len) == s
})
}
impl FromStr for Range {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut trimmed = s.trim().split('-');
let start = trimmed.next().unwrap_or_default().parse()?;
let end = trimmed.next_back().unwrap_or_default().parse()?;
Ok(Self { start, end })
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> Result<usize> {
let result = input
.trim()
.split(',')
.map(Range::from_str)
.flat_map(|range| range.unwrap().find_invalid())
.map(|x| x.0)
.sum();
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn test_process() -> Result<()> {
let input = "11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124";
let result = 1_227_775_554;
assert_eq!(process(input)?, result);
Ok(())
}
#[rstest]
#[case(11)]
#[case(22)]
#[case(1010)]
#[case(1_188_511_885)]
#[case(222_222)]
#[case(446_446)]
#[case(38_593_859)]
fn repeating(#[case] num: usize) {
assert!(has_repeating_sequence(num));
}
#[rstest]
#[case(111)]
#[case(222)]
#[case(222_222_222)]
fn not_repeating(#[case] num: usize) {
assert!(!has_repeating_sequence(num));
}
}

94
2025/day-02/src/part2.rs Normal file
View File

@ -0,0 +1,94 @@
use miette::Result;
use std::str::FromStr;
#[derive(Debug)]
struct Id(usize);
impl FromStr for Id {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let val = s.parse::<usize>().map_err(|e| e.to_string())?;
Ok(Self(val))
}
}
#[derive(Debug)]
struct Range {
start: Id,
end: Id,
}
impl Range {
fn find_invalid(&self) -> Vec<Id> {
(self.start.0..=self.end.0)
.filter_map(|x| {
if has_repeating_sequence(x) {
Some(Id(x))
} else {
None
}
})
.collect()
}
}
fn has_repeating_sequence(num: usize) -> bool {
let s = num.to_string();
let len = s.len();
(1..=len / 2).any(|pattern_len| {
len.is_multiple_of(pattern_len) && s[0..pattern_len].repeat(len / pattern_len) == s
})
}
impl FromStr for Range {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut trimmed = s.trim().split('-');
let start = trimmed.next().unwrap_or_default().parse()?;
let end = trimmed.next_back().unwrap_or_default().parse()?;
Ok(Self { start, end })
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> Result<usize> {
let result = input
.trim()
.split(',')
.map(Range::from_str)
.flat_map(|range| range.unwrap().find_invalid())
.map(|x| x.0)
.sum();
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[test]
fn test_process() -> Result<()> {
let input = "11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124";
let result = 4_174_379_265;
assert_eq!(process(input)?, result);
Ok(())
}
#[rstest]
#[case(11)]
#[case(22)]
#[case(1010)]
#[case(1_188_511_885)]
#[case(222_222)]
#[case(446_446)]
#[case(38_593_859)]
#[case(111)]
#[case(222)]
#[case(222_222_222)]
fn repeating(#[case] num: usize) {
assert!(has_repeating_sequence(num));
}
}

27
2025/day-03/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-03"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-03-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-03/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_03_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 308.2 µs │ 565.8 µs │ 316.1 µs │ 323.9 µs │ 100 │ 100
╰─ part2 328.5 µs │ 1.456 ms │ 336 µs │ 353 µs │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_03::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_03::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_03::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-03/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

90
2025/day-03/src/part1.rs Normal file
View File

@ -0,0 +1,90 @@
use itertools::Itertools;
use std::{ops::Add, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Joltage(u8);
impl Add for Joltage {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 * 10 + rhs.0)
}
}
impl Add for &Joltage {
type Output = Joltage;
fn add(self, rhs: Self) -> Self::Output {
Joltage(self.0 * 10 + rhs.0)
}
}
impl From<Joltage> for usize {
fn from(value: Joltage) -> Self {
Self::from(value.0)
}
}
impl TryFrom<char> for Joltage {
type Error = String;
fn try_from(ch: char) -> std::result::Result<Self, Self::Error> {
let s = ch.to_string();
let value = s.parse::<u8>().map_err(|e| e.to_string())?;
Ok(Self(value))
}
}
#[derive(Debug, Clone)]
struct Bank(Vec<Joltage>);
impl Bank {
fn get_max_value(&self) -> usize {
let mut clone = self.0.clone();
clone.truncate(self.0.len() - 1);
let max1 = clone.iter().max().unwrap();
let (pos1, _) = self.0.iter().find_position(|&x| x == max1).unwrap();
let list = self.0.clone().split_off(pos1 + 1);
let max2 = list.iter().max().unwrap();
(max1 + max2).into()
}
}
impl FromStr for Bank {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let bank = s
.trim()
.chars()
.map(Joltage::try_from)
.collect::<Result<Vec<_>, String>>()?;
Ok(Self(bank))
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let banks = input
.lines()
.map(|line| Bank::from_str(line).unwrap())
.map(|bank| bank.get_max_value())
.sum();
Ok(banks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "987654321111111
811111111111119
234234234234278
818181911112111
";
let result = 357;
assert_eq!(process(input)?, result);
Ok(())
}
}

109
2025/day-03/src/part2.rs Normal file
View File

@ -0,0 +1,109 @@
use std::{ops::Add, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Joltage(usize);
impl Add for Joltage {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 * 10 + rhs.0)
}
}
impl Add for &Joltage {
type Output = Joltage;
fn add(self, rhs: Self) -> Self::Output {
Joltage(self.0 * 10 + rhs.0)
}
}
impl From<Joltage> for usize {
fn from(value: Joltage) -> Self {
value.0
}
}
impl TryFrom<char> for Joltage {
type Error = String;
fn try_from(ch: char) -> std::result::Result<Self, Self::Error> {
let s = ch.to_string();
let value = s.parse::<usize>().map_err(|e| e.to_string())?;
Ok(Self(value))
}
}
#[derive(Debug, Clone)]
struct Bank(Vec<Joltage>);
impl Bank {
const LEN: usize = 12;
fn get_max_value(&self) -> usize {
if self.0.len() < Self::LEN {
return self.0.iter().map(|x| x.0).sum();
}
let mut result = Joltage(0);
let mut start = 0;
let mut remaining = Self::LEN;
while remaining > 0 {
let search_end = self.0.len() - remaining + 1;
let mut max_joltage = self.0[start];
let mut max_pos = start;
for i in start..search_end {
if self.0[i] > max_joltage {
max_joltage = self.0[i];
max_pos = i;
}
}
result = result + max_joltage;
start = max_pos + 1;
remaining -= 1;
}
result.into()
}
}
impl FromStr for Bank {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let bank = s
.trim()
.chars()
.map(Joltage::try_from)
.collect::<Result<Vec<_>, String>>()?;
Ok(Self(bank))
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let banks = input
.lines()
.map(|line| Bank::from_str(line).unwrap())
.map(|bank| bank.get_max_value())
.sum();
Ok(banks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "987654321111111
811111111111119
234234234234278
818181911112111
";
let result = 3_121_910_778_619;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-04/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-04"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-04-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-04/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_04_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 497.7 µs │ 571.3 µs │ 501.7 µs │ 503.6 µs │ 100 │ 100
╰─ part2 22.84 ms │ 28.42 ms │ 22.91 ms │ 23.24 ms │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_04::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_04::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_04::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-04/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

134
2025/day-04/src/part1.rs Normal file
View File

@ -0,0 +1,134 @@
use std::{convert::Infallible, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Space {
Roll,
Empty,
}
impl From<char> for Space {
fn from(ch: char) -> Self {
match ch {
'@' => Self::Roll,
_ => Self::Empty,
}
}
}
#[derive(Debug, Clone)]
struct Row(Vec<Space>);
impl FromStr for Row {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let row = s.chars().map(Space::from).collect();
Ok(Self(row))
}
}
#[derive(Debug, Clone)]
struct Grid(Vec<Row>);
impl FromStr for Grid {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let grid = s
.lines()
.map(Row::from_str)
.collect::<Result<Vec<_>, Infallible>>()
.unwrap();
Ok(Self(grid))
}
}
#[derive(Debug)]
struct Coords {
x: isize,
y: isize,
}
impl Coords {
const fn new(x: isize, y: isize) -> Self {
Self { x, y }
}
const fn offset(&self, coords: &Self) -> Self {
Self {
x: self.x + coords.x,
y: self.y + coords.y,
}
}
}
impl Grid {
fn find_accessible(&self) -> usize {
self.0
.iter()
.enumerate()
.flat_map(|(x, row)| row.0.iter().enumerate().map(move |(y, _)| (x, y)))
.filter(|(x, y)| {
let coords = Coords::new((*x).try_into().unwrap(), (*y).try_into().unwrap());
let adjacent = self.get_adjacent(&coords);
adjacent < 4 && matches!(self.get(&coords), Space::Roll)
})
.count()
}
fn get_adjacent(&self, coords: &Coords) -> usize {
let max_x = self.0.len() - 1;
let max_y = self.0[0].0.len() - 1;
(-1..=1)
.flat_map(|x| (-1..=1).map(move |y| (x, y)))
.filter(|(x, y)| {
!(*x == 0 && *y == 0)
&& !((coords.x == 0 && *x == -1)
|| (coords.y == 0 && *y == -1)
|| (usize::try_from(coords.x).unwrap() == max_x && *x == 1)
|| (usize::try_from(coords.y).unwrap() == max_y && *y == 1))
})
.filter(|(x, y)| {
let coord = coords.offset(&Coords::new(
(*x).try_into().unwrap(),
(*y).try_into().unwrap(),
));
matches!(self.get(&coord), Space::Roll)
})
.count()
}
fn get(&self, coords: &Coords) -> Space {
self.0[usize::try_from(coords.x).unwrap()].0[usize::try_from(coords.y).unwrap()]
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let grid = Grid::from_str(input)?;
Ok(grid.find_accessible())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "..@@.@@@@.
@@@.@.@.@@
@@@@@.@.@@
@.@@@@..@.
@@.@@@@.@@
.@@@@@@@.@
.@.@.@.@@@
@.@@@.@@@@
.@@@@@@@@.
@.@.@@@.@.
";
let result = 13;
assert_eq!(process(input)?, result);
Ok(())
}
}

146
2025/day-04/src/part2.rs Normal file
View File

@ -0,0 +1,146 @@
use std::{convert::Infallible, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Space {
Roll,
Empty,
}
impl From<char> for Space {
fn from(ch: char) -> Self {
match ch {
'@' => Self::Roll,
_ => Self::Empty,
}
}
}
#[derive(Debug, Clone)]
struct Row(Vec<Space>);
impl FromStr for Row {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let row = s.chars().map(Space::from).collect();
Ok(Self(row))
}
}
#[derive(Debug, Clone)]
struct Grid(Vec<Row>);
impl FromStr for Grid {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let grid = s
.lines()
.map(Row::from_str)
.collect::<Result<Vec<_>, Infallible>>()
.unwrap();
Ok(Self(grid))
}
}
#[derive(Debug)]
struct Coords {
x: isize,
y: isize,
}
impl Coords {
const fn new(x: isize, y: isize) -> Self {
Self { x, y }
}
const fn offset(&self, coords: &Self) -> Self {
Self {
x: self.x + coords.x,
y: self.y + coords.y,
}
}
}
impl Grid {
fn find_accessible(&mut self) -> usize {
let mut total_removed = 0;
loop {
let accessible = self
.0
.iter()
.enumerate()
.flat_map(|(x, row)| row.0.iter().enumerate().map(move |(y, _)| (x, y)))
.filter(|(x, y)| {
let coords = Coords::new((*x).try_into().unwrap(), (*y).try_into().unwrap());
let adjacent = self.get_adjacent(&coords);
adjacent < 4 && matches!(self.get(&coords), Space::Roll)
})
.collect::<Vec<_>>();
if accessible.is_empty() {
break;
}
for (x, y) in &accessible {
self.0[*x].0[*y] = Space::Empty;
}
total_removed += accessible.len();
}
total_removed
}
fn get_adjacent(&self, coords: &Coords) -> usize {
let max_x = self.0.len() - 1;
let max_y = self.0[0].0.len() - 1;
(-1..=1)
.flat_map(|x| (-1..=1).map(move |y| (x, y)))
.filter(|(x, y)| {
!(*x == 0 && *y == 0)
&& !((coords.x == 0 && *x == -1)
|| (coords.y == 0 && *y == -1)
|| (usize::try_from(coords.x).unwrap() == max_x && *x == 1)
|| (usize::try_from(coords.y).unwrap() == max_y && *y == 1))
})
.filter(|(x, y)| {
let coord = coords.offset(&Coords::new(
(*x).try_into().unwrap(),
(*y).try_into().unwrap(),
));
matches!(self.get(&coord), Space::Roll)
})
.count()
}
fn get(&self, coords: &Coords) -> Space {
self.0[usize::try_from(coords.x).unwrap()].0[usize::try_from(coords.y).unwrap()]
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let mut grid = Grid::from_str(input)?;
Ok(grid.find_accessible())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "..@@.@@@@.
@@@.@.@.@@
@@@@@.@.@@
@.@@@@..@.
@@.@@@@.@@
.@@@@@@@.@
.@.@.@.@@@
@.@@@.@@@@
.@@@@@@@@.
@.@.@@@.@.
";
let result = 43;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-05/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-05"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-05-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-05/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_05_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 103.9 µs │ 151.8 µs │ 105.6 µs │ 107.3 µs │ 100 │ 100
╰─ part2 9.687 µs │ 14.12 µs │ 9.787 µs │ 9.895 µs │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_05::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_05::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_05::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-05/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

106
2025/day-05/src/part1.rs Normal file
View File

@ -0,0 +1,106 @@
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Id(usize);
impl From<usize> for Id {
fn from(value: usize) -> Self {
Self(value)
}
}
impl FromStr for Id {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let num = s.trim().parse::<usize>().map_err(|e| e.to_string())?;
Ok(Self(num))
}
}
#[derive(Debug, Clone, Copy)]
struct Range {
start: Id,
end: Id,
}
impl Range {
fn contains(&self, x: Id) -> bool {
x >= self.start && x <= self.end
}
}
impl FromStr for Range {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (start_str, end_str) = s.trim().split_once('-').ok_or("`-` not found")?;
let start = start_str.parse::<Id>()?;
let end = end_str.parse::<Id>()?;
Ok(Self { start, end })
}
}
#[derive(Debug, Clone)]
struct DB {
ranges: Vec<Range>,
ids: Vec<Id>,
}
impl DB {
fn contains(&self, x: Id) -> bool {
self.ranges.iter().any(|range| range.contains(x))
}
fn count_fresh(&self) -> usize {
self.ids.iter().filter(|&&id| self.contains(id)).count()
}
}
impl FromStr for DB {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (ranges_section, ids_section) = s
.split_once("\n\n")
.ok_or("No blank line separator found")?;
let ranges = ranges_section
.lines()
.map(Range::from_str)
.collect::<Result<Vec<_>, _>>()?;
let ids = ids_section
.lines()
.map(Id::from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { ranges, ids })
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let db = DB::from_str(input).unwrap();
Ok(db.count_fresh())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "3-5
10-14
16-20
12-18
1
5
8
11
17
32
";
let result = 3;
assert_eq!(process(input)?, result);
Ok(())
}
}

107
2025/day-05/src/part2.rs Normal file
View File

@ -0,0 +1,107 @@
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Id(usize);
impl From<usize> for Id {
fn from(value: usize) -> Self {
Self(value)
}
}
impl FromStr for Id {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let num = s.trim().parse::<usize>().map_err(|e| e.to_string())?;
Ok(Self(num))
}
}
#[derive(Debug, Clone, Copy)]
struct Range {
start: Id,
end: Id,
}
impl FromStr for Range {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (start_str, end_str) = s.trim().split_once('-').ok_or("`-` not found")?;
let start = start_str.parse::<Id>()?;
let end = end_str.parse::<Id>()?;
Ok(Self { start, end })
}
}
#[derive(Debug, Clone)]
struct DB {
ranges: Vec<Range>,
}
impl DB {
fn count_range_ids(&self) -> usize {
let mut sorted_ranges = self.ranges.clone();
sorted_ranges.sort_by_key(|r| r.start.0);
let (total, start, end) =
sorted_ranges
.into_iter()
.fold((0, None, None), |(total, s, e), range| match (s, e) {
(Some(s), Some(e)) if range.start.0 <= e + 1 => {
(total, Some(s), Some(range.end.0.max(e)))
}
(Some(s), Some(e)) => {
(total + (e - s + 1), Some(range.start.0), Some(range.end.0))
}
_ => (total, Some(range.start.0), Some(range.end.0)),
});
total + start.zip(end).map_or(0, |(s, e)| e - s + 1)
}
}
impl FromStr for DB {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (ranges_section, _) = s
.split_once("\n\n")
.ok_or("No blank line separator found")?;
let ranges = ranges_section
.lines()
.map(Range::from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self { ranges })
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let db = DB::from_str(input).unwrap();
Ok(db.count_range_ids())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "3-5
10-14
16-20
12-18
1
5
8
11
17
32
";
let result = 14;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-06/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-06"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-06-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

3
2025/day-06/bench.txt Normal file
View File

@ -0,0 +1,3 @@
day_06_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 69.96 µs │ 141.7 µs │ 70.27 µs │ 73 µs │ 100 │ 100
╰─ part2

View File

@ -0,0 +1,15 @@
use day_06::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_06::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_06::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-06/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

102
2025/day-06/src/part1.rs Normal file
View File

@ -0,0 +1,102 @@
use miette::miette;
use std::{fmt::Display, str::FromStr, vec};
#[derive(Debug, Clone)]
struct Row<T>(Vec<T>);
impl<T> FromStr for Row<T>
where
T: FromStr,
T::Err: Display,
{
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let row = s
.split_whitespace()
.map(str::parse::<T>)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(Self(row))
}
}
#[derive(Debug, Clone)]
struct Grid<T>(Vec<T>);
impl<T: Clone> Grid<Row<T>> {
fn to_transposed(&self) -> Self {
if self.0.is_empty() {
return Self(Vec::new());
}
let num_cols = self.0[0].0.len();
let mut transposed = vec![Vec::new(); num_cols];
for row in &self.0 {
for (col_idx, val) in row.0.iter().enumerate() {
if col_idx < transposed.len() {
transposed[col_idx].push(val.clone());
}
}
}
Self(transposed.into_iter().map(Row).collect())
}
}
impl<T> FromStr for Grid<T>
where
T: FromStr,
T::Err: Display,
{
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let rows = s
.lines()
.map(T::from_str)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.to_string())?;
Ok(Self(rows))
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let (rest, last_line) = match input.trim_end().rsplit_once('\n') {
Some((r, l)) => (r, l),
None => ("", ""),
};
let digits = rest
.parse::<Grid<Row<usize>>>()
.map_err(|e| miette!("{e}"))?;
let operators = last_line.parse::<Row<char>>().map_err(|e| miette!("{e}"))?;
let result = operators
.0
.iter()
.zip(digits.to_transposed().0)
.map(|(operator, row)| match operator {
'+' => row.0.iter().sum(),
'*' => row.0.iter().product::<usize>(),
op => panic!("Unknown operator: {op}"),
})
.sum();
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "123 328 51 64
45 64 387 23
6 98 215 314
* + * + ";
let result = 4_277_556;
assert_eq!(process(input)?, result);
Ok(())
}
}

23
2025/day-06/src/part2.rs Normal file
View File

@ -0,0 +1,23 @@
use miette::Result;
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> Result<usize> {
todo!("day xx - part 2");
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> Result<()> {
let input = "";
todo!("haven't built test yet");
let result = 0;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-07/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-07"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-07-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-07/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_07_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 366.1 µs │ 407.2 µs │ 369.8 µs │ 370.9 µs │ 100 │ 100
╰─ part2 273.4 µs │ 291.4 µs │ 277.4 µs │ 278.5 µs │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_07::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_07::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_07::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-07/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

92
2025/day-07/src/part1.rs Normal file
View File

@ -0,0 +1,92 @@
use std::collections::HashSet;
use tracing::info;
type Column = usize;
type Row = usize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Splitter {
column: Column,
row: Row,
}
#[derive(Debug)]
struct SimulationState {
active_positions: HashSet<Column>,
splitters: HashSet<Splitter>,
}
impl SimulationState {
fn new(start_column: Column) -> Self {
let mut active_positions = HashSet::new();
active_positions.insert(start_column);
Self {
active_positions,
splitters: HashSet::new(),
}
}
fn process_row(&mut self, row: Row, line: &str) {
let mut next_positions = HashSet::new();
for col in &self.active_positions {
if line.as_bytes()[*col] == b'^' {
info!(?col, "split at");
next_positions.insert(col - 1);
next_positions.insert(col + 1);
next_positions.insert(col + 1);
self.splitters.insert(Splitter { column: *col, row });
} else {
next_positions.insert(*col);
}
}
self.active_positions = next_positions;
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let mut lines = input.lines().enumerate();
let (_, first_line) = lines.next().unwrap();
let start_column = first_line.chars().position(|ch| ch == 'S').unwrap();
let mut state = SimulationState::new(start_column);
for (row, line) in lines {
state.process_row(row, line);
}
Ok(state.splitters.len())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = ".......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............
";
let result = 21;
assert_eq!(process(input)?, result);
Ok(())
}
}

82
2025/day-07/src/part2.rs Normal file
View File

@ -0,0 +1,82 @@
use std::collections::HashMap;
use tracing::info;
type Column = usize;
type PathCount = usize;
#[derive(Debug, Clone)]
struct PathCounts {
positions: HashMap<Column, PathCount>,
}
impl PathCounts {
fn new(start_column: Column) -> Self {
let mut positions = HashMap::new();
positions.insert(start_column, 1);
Self { positions }
}
fn apply_row(&self, line: &str) -> Self {
let mut new_positions = HashMap::new();
for (&column, &count) in &self.positions {
if line.as_bytes()[column] == b'^' {
info!(column, "split at");
*new_positions.entry(column - 1).or_insert(0) += count;
*new_positions.entry(column + 1).or_insert(0) += count;
} else {
*new_positions.entry(column).or_insert(0) += count;
}
}
Self {
positions: new_positions,
}
}
fn total_paths(&self) -> usize {
self.positions.values().sum()
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let mut lines = input.lines().enumerate();
let (_, first_line) = lines.next().unwrap();
let start_column = first_line.chars().position(|ch| ch == 'S').unwrap();
let final_state = lines.fold(PathCounts::new(start_column), |state, (_, line)| {
state.apply_row(line)
});
Ok(final_state.total_paths())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = ".......S.......
...............
.......^.......
...............
......^.^......
...............
.....^.^.^.....
...............
....^.^...^....
...............
...^.^...^.^...
...............
..^...^.....^..
...............
.^.^.^.^.^...^.
...............
";
let result = 40;
assert_eq!(process(input)?, result);
Ok(())
}
}

28
2025/day-08/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "day-08"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
glam = { workspace = true, features = ["mint"] }
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-08-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-08/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_08_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 24.77 ms │ 29.97 ms │ 25.17 ms │ 25.63 ms │ 100 │ 100
╰─ part2 24.89 ms │ 33.97 ms │ 25.34 ms │ 25.74 ms │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_08::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt")), 1000).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_08::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file, 1000).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_08::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-08/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

142
2025/day-08/src/part1.rs Normal file
View File

@ -0,0 +1,142 @@
use glam::Vec3;
use miette::miette;
use std::{collections::HashSet, str::FromStr};
fn vec3_from_str(s: &str) -> Result<Vec3, String> {
let coords = s
.trim()
.split(',')
.map(|x| {
x.parse::<f32>()
.map_err(|_| "Invalid coordinate value".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
if coords.len() == 3 {
let vec = Vec3::new(coords[0], coords[1], coords[2]);
return Ok(vec);
}
Err("Expected exactly 3 coordinates".to_string())
}
#[derive(Debug, Clone)]
struct UnionFind {
parent: Vec<usize>,
size: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
size: vec![1; n],
}
}
fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
self.parent[x] = self.find(self.parent[x]);
}
self.parent[x]
}
fn union(&mut self, mut x: usize, mut y: usize) {
x = self.find(x);
y = self.find(y);
if x == y {
return;
}
if self.size[x] < self.size[y] {
(x, y) = (y, x);
}
self.parent[y] = x;
self.size[x] += self.size[y];
}
}
#[derive(Debug, Clone)]
struct Coordinates(Vec<Vec3>);
impl Coordinates {
fn solve(&self, size: usize) -> usize {
let n = self.0.len();
let mut pairs = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let dist = self.0[i].distance(self.0[j]);
pairs.push((dist, i, j));
}
}
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let mut uf = UnionFind::new(n);
for (_, i, j) in pairs.iter().take(size) {
if uf.find(*i) != uf.find(*j) {
uf.union(*i, *j);
}
}
let mut roots = HashSet::new();
for i in 0..n {
roots.insert(uf.find(i));
}
let mut sizes = roots.iter().map(|&root| uf.size[root]).collect::<Vec<_>>();
sizes.sort_by(|a, b| b.cmp(a));
sizes[0] * sizes[1] * sizes[2]
}
}
impl FromStr for Coordinates {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let coords = s
.lines()
.map(vec3_from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(coords))
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str, size: usize) -> miette::Result<usize> {
let coords = Coordinates::from_str(input).map_err(|e| miette!("{e}"))?;
Ok(coords.solve(size))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "162,817,812
57,618,57
906,360,560
592,479,940
352,342,300
466,668,158
542,29,236
431,825,988
739,650,466
52,470,668
216,146,977
819,987,18
117,168,530
805,96,715
346,949,466
970,615,88
941,993,340
862,61,35
984,92,344
425,690,689";
let result = 40;
assert_eq!(process(input, 10)?, result);
Ok(())
}
}

142
2025/day-08/src/part2.rs Normal file
View File

@ -0,0 +1,142 @@
use glam::Vec3;
use miette::miette;
use std::str::FromStr;
fn vec3_from_str(s: &str) -> Result<Vec3, String> {
let coords = s
.trim()
.split(',')
.map(|x| {
x.parse::<f32>()
.map_err(|_| "Invalid coordinate value".to_string())
})
.collect::<Result<Vec<_>, _>>()?;
if coords.len() == 3 {
let vec = Vec3::new(coords[0], coords[1], coords[2]);
return Ok(vec);
}
Err("Expected exactly 3 coordinates".to_string())
}
#[derive(Debug, Clone)]
struct UnionFind {
parent: Vec<usize>,
size: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
Self {
parent: (0..n).collect(),
size: vec![1; n],
}
}
fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
self.parent[x] = self.find(self.parent[x]);
}
self.parent[x]
}
fn union(&mut self, mut x: usize, mut y: usize) {
x = self.find(x);
y = self.find(y);
if x == y {
return;
}
if self.size[x] < self.size[y] {
(x, y) = (y, x);
}
self.parent[y] = x;
self.size[x] += self.size[y];
}
}
#[derive(Debug, Clone)]
struct Coordinates(Vec<Vec3>);
impl Coordinates {
fn solve(&self) -> usize {
let n = self.0.len();
let mut pairs = Vec::new();
for i in 0..n {
for j in (i + 1)..n {
let dist = self.0[i].distance(self.0[j]);
pairs.push((dist, i, j));
}
}
pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let mut uf = UnionFind::new(n);
let mut last_pair = (0, 0);
let mut num_circuits = n;
for (_, i, j) in &pairs {
if uf.find(*i) != uf.find(*j) {
uf.union(*i, *j);
last_pair = (*i, *j);
num_circuits -= 1;
if num_circuits == 1 {
break;
}
}
}
(self.0[last_pair.0].x as usize) * (self.0[last_pair.1].x as usize)
}
}
impl FromStr for Coordinates {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let coords = s
.lines()
.map(vec3_from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(coords))
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let coords = Coordinates::from_str(input).map_err(|e| miette!("{e}"))?;
Ok(coords.solve())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "162,817,812
57,618,57
906,360,560
592,479,940
352,342,300
466,668,158
542,29,236
431,825,988
739,650,466
52,470,668
216,146,977
819,987,18
117,168,530
805,96,715
346,949,466
970,615,88
941,993,340
862,61,35
984,92,344
425,690,689";
let result = 25272;
assert_eq!(process(input)?, result);
Ok(())
}
}

28
2025/day-09/Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[package]
name = "day-09"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
glam.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-09-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

3
2025/day-09/bench.txt Normal file
View File

@ -0,0 +1,3 @@
day_09_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 267.7 µs │ 279.6 µs │ 268.8 µs │ 269.8 µs │ 100 │ 100
╰─ part2

View File

@ -0,0 +1,15 @@
use day_09::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_09::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_09::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-09/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

93
2025/day-09/src/part1.rs Normal file
View File

@ -0,0 +1,93 @@
use glam::{USizeVec2, usize};
use miette::miette;
use std::str::FromStr;
#[derive(Debug, Clone, Copy)]
struct Tile(USizeVec2);
impl Tile {
const fn x(&self) -> usize {
self.0.x
}
const fn y(&self) -> usize {
self.0.y
}
const fn area(&self, other: Self) -> usize {
(self.x().abs_diff(other.x()) + 1) * (self.y().abs_diff(other.y()) + 1)
}
}
impl FromStr for Tile {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let str = s.trim().split(',').collect::<Vec<_>>();
if str.len() < 2 {
return Err("Missing coords".to_string());
}
Ok(Self(USizeVec2 {
x: str_to_usize(str[0])?,
y: str_to_usize(str[1])?,
}))
}
}
fn str_to_usize(s: &str) -> Result<usize, String> {
s.parse::<usize>().map_err(|e| e.to_string())
}
#[derive(Debug, Clone)]
struct Grid(Vec<Tile>);
impl FromStr for Grid {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let grid = s
.trim()
.lines()
.map(Tile::from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(grid))
}
}
impl Grid {
fn get_max_area(&self) -> usize {
self.area_list().max().unwrap_or(0)
}
fn area_list(&self) -> impl Iterator<Item = usize> {
self.0
.iter()
.flat_map(|&a| self.0.iter().map(move |&b| a.area(b)))
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let grid = Grid::from_str(input).map_err(|e| miette!("{e}"))?;
Ok(grid.get_max_area())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "7,1
11,1
11,7
9,7
9,5
2,5
2,3
7,3";
let result = 50;
assert_eq!(process(input)?, result);
Ok(())
}
}

23
2025/day-09/src/part2.rs Normal file
View File

@ -0,0 +1,23 @@
use miette::Result;
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> Result<usize> {
todo!("day xx - part 2");
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> Result<()> {
let input = "";
todo!("haven't built test yet");
let result = 0;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-10/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-10"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-10-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

3
2025/day-10/bench.txt Normal file
View File

@ -0,0 +1,3 @@
day_10_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 7.256 ms │ 8.22 ms │ 7.328 ms │ 7.348 ms │ 100 │ 100
╰─ part2

View File

@ -0,0 +1,15 @@
use day_10::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_10::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_10::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-10/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

207
2025/day-10/src/part1.rs Normal file
View File

@ -0,0 +1,207 @@
use miette::miette;
use std::{collections::HashSet, slice::Iter, str::FromStr, vec};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Light {
On,
Off,
}
impl TryFrom<char> for Light {
type Error = String;
fn try_from(ch: char) -> Result<Self, Self::Error> {
match ch {
'#' => Ok(Self::On),
'.' => Ok(Self::Off),
_ => Err("Unknown Light status".to_string()),
}
}
}
impl Light {
const fn toggle(&self) -> Self {
match self {
Self::On => Self::Off,
Self::Off => Self::On,
}
}
}
// [.##.]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct IndicatorLight(Vec<Light>);
impl FromStr for IndicatorLight {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let digits_str = trimmed
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.unwrap_or("");
let lights = digits_str
.chars()
.map(Light::try_from)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(lights))
}
}
impl IndicatorLight {
fn toggle(&mut self, button: &Button) {
for &btn in button.iter() {
let light = self.0.get_mut(btn).expect("light exsists");
*light = light.toggle();
}
}
const fn len(&self) -> usize {
self.0.len()
}
fn with_capacity(capacity: usize) -> Self {
Self(vec![Light::Off; capacity])
}
}
// (3) (1,3) (2) (2,3) (0,2) (0,1)
#[derive(Debug, Clone)]
struct Button(Vec<usize>);
impl FromStr for Button {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let digits_str = trimmed
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.unwrap_or(trimmed);
let button = digits_str
.split(',')
.map(|s| s.parse::<usize>().map_err(|e| e.to_string()))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(button))
}
}
impl Button {
fn iter(&self) -> Iter<'_, usize> {
self.0.iter()
}
}
// {3,5,4,7}
#[derive(Debug, Clone)]
struct Joltage(Vec<usize>);
impl FromStr for Joltage {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let digits_str = trimmed
.strip_prefix('{')
.and_then(|s| s.strip_suffix('}'))
.unwrap_or(trimmed);
let joltage = digits_str
.split(',')
.map(|s| s.parse::<usize>().map_err(|e| e.to_string()))
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(joltage))
}
}
// [.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
#[derive(Debug, Clone)]
struct Machine {
final_state: IndicatorLight, // [.##.]
current_state: IndicatorLight,
buttons: Vec<Button>, // (3) (1,3) (2) (2,3) (0,2) (0,1)
_joltage: Joltage, // {3,5,4,7}
}
impl FromStr for Machine {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let bracket_start = trimmed.find('[').ok_or("Missing '['")?;
let bracket_end = trimmed.find(']').ok_or("Missing ']'")?;
let final_state = trimmed[bracket_start..=bracket_end].parse::<IndicatorLight>()?;
let state_len = final_state.len();
let brace_start = trimmed.find('{').ok_or("Missing '{'")?;
let brace_end = trimmed.find('}').ok_or("Missing '}'")?;
let joltage = trimmed[brace_start..=brace_end].parse()?;
let buttons_str = trimmed[bracket_end + 1..brace_start].trim();
let buttons = buttons_str
.split_whitespace()
.map(Button::from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self {
final_state,
current_state: IndicatorLight::with_capacity(state_len),
buttons,
_joltage: joltage,
})
}
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let machines = input
.lines()
.map(Machine::from_str)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| miette!("{e}"))?;
let result = machines
.iter()
.map(|machine| {
let mut set = HashSet::new();
set.insert(machine.current_state.clone());
let mut i = 0;
loop {
set = set
.into_iter()
.flat_map(|state| {
machine.buttons.iter().map(move |btn| {
let mut new_state = state.clone();
new_state.toggle(btn);
new_state
})
})
.collect();
i += 1;
if set.contains(&machine.final_state) {
break;
}
}
i
})
.sum();
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}";
let result = 7;
assert_eq!(process(input)?, result);
Ok(())
}
}

23
2025/day-10/src/part2.rs Normal file
View File

@ -0,0 +1,23 @@
use miette::miette;
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
todo!("day xx - part 2");
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "";
todo!("haven't built test yet");
let result = 0;
assert_eq!(process(input)?, result);
Ok(())
}
}

27
2025/day-11/Cargo.toml Normal file
View File

@ -0,0 +1,27 @@
[package]
name = "day-11"
version = "0.1.0"
edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
itertools.workspace = true
nom.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
miette.workspace = true
thiserror.workspace = true
[dev-dependencies]
divan.workspace = true
rstest.workspace = true
test-log.workspace = true
[[bench]]
name = "day-11-bench"
path = "benches/benchmarks.rs"
harness = false
[lints]
workspace = true

4
2025/day-11/bench.txt Normal file
View File

@ -0,0 +1,4 @@
day_11_bench fastest │ slowest │ median │ mean │ samples │ iters
├─ part1 129.6 µs │ 171.1 µs │ 131.1 µs │ 133.9 µs │ 100 │ 100
╰─ part2 320.8 µs │ 370.5 µs │ 333.9 µs │ 335.6 µs │ 100 │ 100

View File

@ -0,0 +1,15 @@
use day_11::{part1, part2};
fn main() {
divan::main();
}
#[divan::bench]
fn part1() {
part1::process(divan::black_box(include_str!("../input1.txt"))).unwrap();
}
#[divan::bench]
fn part2() {
part2::process(divan::black_box(include_str!("../input2.txt"))).unwrap();
}

View File

@ -0,0 +1,12 @@
use day_11::part1::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input1.txt");
let result = process(file).context("process part 1")?;
println!("{result}");
Ok(())
}

View File

@ -0,0 +1,12 @@
use day_11::part2::process;
use miette::{Context, Result};
#[tracing::instrument]
fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let file = include_str!("../../input2.txt");
let result = process(file).context("process part 2")?;
println!("{result}");
Ok(())
}

2
2025/day-11/src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod part1;
pub mod part2;

113
2025/day-11/src/part1.rs Normal file
View File

@ -0,0 +1,113 @@
use itertools::Itertools;
use miette::miette;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Name([char; 3]);
impl Name {
const YOU: Self = Self(['y', 'o', 'u']);
const OUT: Self = Self(['o', 'u', 't']);
}
impl FromStr for Name {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let chars = s.trim().chars().collect_vec();
if chars.len() != 3 {
return Err(format!(
"Name must be exactly 3 characters, got {}",
chars.len()
));
}
Ok(Self([chars[0], chars[1], chars[2]]))
}
}
#[derive(Debug, Clone)]
struct Device {
input: Name,
outputs: Vec<Name>,
}
impl FromStr for Device {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let (input_str, output_str) = trimmed.split_once(':').ok_or("Missing `:`")?;
Ok(Self {
input: Name::from_str(input_str)?,
outputs: output_str
.split_whitespace()
.map(Name::from_str)
.collect::<Result<Vec<_>, _>>()?,
})
}
}
#[derive(Debug, Clone)]
struct Rack(Vec<Device>);
impl FromStr for Rack {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let rack = s
.lines()
.map(Device::from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(rack))
}
}
impl Rack {
fn solve(&self) -> usize {
dfs(&Name::YOU, &self.0)
}
}
fn dfs(current: &Name, devices: &[Device]) -> usize {
if current == &Name::OUT {
return 1;
}
devices
.iter()
.find(|d| d.input == *current)
.map_or(0, |device| {
device
.outputs
.iter()
.map(|output| dfs(output, devices))
.sum()
})
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let rack = Rack::from_str(input).map_err(|e| miette!("{e}"))?;
Ok(rack.solve())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "aaa: you hhh
you: bbb ccc
bbb: ddd eee
ccc: ddd eee fff
ddd: ggg
eee: out
fff: out
ggg: out
hhh: ccc fff iii
iii: out";
let result = 5;
assert_eq!(process(input)?, result);
Ok(())
}
}

159
2025/day-11/src/part2.rs Normal file
View File

@ -0,0 +1,159 @@
use itertools::Itertools;
use miette::miette;
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Name([char; 3]);
impl Name {
const OUT: Self = Self(['o', 'u', 't']);
const DAC: Self = Self(['d', 'a', 'c']);
const FFT: Self = Self(['f', 'f', 't']);
const SVR: Self = Self(['s', 'v', 'r']);
}
impl FromStr for Name {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let chars = s.trim().chars().collect_vec();
if chars.len() != 3 {
return Err(format!(
"Name must be exactly 3 characters, got {}",
chars.len()
));
}
Ok(Self([chars[0], chars[1], chars[2]]))
}
}
#[derive(Debug, Clone)]
struct Device {
input: Name,
outputs: Vec<Name>,
}
impl FromStr for Device {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let (input_str, output_str) = trimmed.split_once(':').ok_or("Missing `:`")?;
Ok(Self {
input: Name::from_str(input_str)?,
outputs: output_str
.split_whitespace()
.map(Name::from_str)
.collect::<Result<Vec<_>, _>>()?,
})
}
}
#[derive(Debug, Clone)]
struct Rack(Vec<Device>);
impl FromStr for Rack {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let rack = s
.lines()
.map(Device::from_str)
.collect::<Result<Vec<_>, _>>()?;
Ok(Self(rack))
}
}
impl Rack {
fn solve(&self) -> usize {
let mut memo = HashMap::new();
let mut computing = HashSet::new();
dfs(&Name::SVR, &self.0, false, false, &mut memo, &mut computing)
}
}
fn dfs(
current: &Name,
devices: &[Device],
visited_dac: bool,
visited_fft: bool,
memo: &mut HashMap<(Name, bool, bool), usize>,
computing: &mut HashSet<(Name, bool, bool)>,
) -> usize {
let state = (*current, visited_dac, visited_fft);
if let Some(&result) = memo.get(&state) {
return result;
}
if computing.contains(&state) {
return 0;
}
if current == &Name::OUT {
return usize::from(visited_dac && visited_fft);
}
computing.insert(state);
let new_visited_dac = visited_dac || (current == &Name::DAC);
let new_visited_fft = visited_fft || (current == &Name::FFT);
let result = devices
.iter()
.find(|d| d.input == *current)
.map_or(0, |device| {
device
.outputs
.iter()
.map(|output| {
dfs(
output,
devices,
new_visited_dac,
new_visited_fft,
memo,
computing,
)
})
.sum()
});
computing.remove(&state);
memo.insert(state, result);
result
}
#[tracing::instrument]
#[allow(clippy::missing_panics_doc)]
#[allow(clippy::missing_errors_doc)]
pub fn process(input: &str) -> miette::Result<usize> {
let rack = Rack::from_str(input).map_err(|e| miette!("{e}"))?;
Ok(rack.solve())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "svr: aaa bbb
aaa: fft
fft: ccc
bbb: tty
tty: ccc
ccc: ddd eee
ddd: hub
hub: fff
eee: dac
dac: fff
fff: ggg hhh
ggg: out
hhh: out";
let result = 2;
assert_eq!(process(input)?, result);
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More