diff --git a/2024/Cargo.lock b/2024/Cargo.lock index 5fb4b1d..3ede24e 100644 --- a/2024/Cargo.lock +++ b/2024/Cargo.lock @@ -220,6 +220,21 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "day-08" +version = "0.1.0" +dependencies = [ + "divan", + "itertools", + "miette", + "nom", + "rstest", + "test-log", + "thiserror 2.0.3", + "tracing", + "tracing-subscriber", +] + [[package]] name = "divan" version = "0.1.16" diff --git a/2024/day-03/benches/benchmarks.rs b/2024/day-03/benches/benchmarks.rs index a883666..45f2f40 100644 --- a/2024/day-03/benches/benchmarks.rs +++ b/2024/day-03/benches/benchmarks.rs @@ -6,16 +6,10 @@ fn main() { #[divan::bench] fn part1() { - part1::process(divan::black_box(include_str!( - "../input1.txt", - ))) - .unwrap(); + part1::process(divan::black_box(include_str!("../input1.txt",))).unwrap(); } #[divan::bench] fn part2() { - part2::process(divan::black_box(include_str!( - "../input2.txt", - ))) - .unwrap(); + part2::process(divan::black_box(include_str!("../input2.txt",))).unwrap(); } diff --git a/2024/day-03/src/part2.rs b/2024/day-03/src/part2.rs deleted file mode 100644 index b041517..0000000 --- a/2024/day-03/src/part2.rs +++ /dev/null @@ -1,118 +0,0 @@ -use miette::{Diagnostic, Result}; -use regex::Regex; -use thiserror::Error; - -#[derive(Error, Debug, Diagnostic)] -pub enum MultiplicationError { - #[error("Failed to parse first value: {0}")] - #[diagnostic(code(calculator::parse_error::first_value))] - FirstValueParseError(String), - - #[error("Failed to parse second value: {0}")] - #[diagnostic(code(calculator::parse_error::second_value))] - SecondValueParseError(String), - - #[error("Invalid multiplication format")] - #[diagnostic(code(calculator::parse_error::format))] - InvalidFormat, -} - -#[derive(Debug)] -struct Multiplication(usize, usize); - -impl Multiplication { - fn calculate(&self) -> usize { - self.0 * self.1 - } -} - -impl TryFrom<(T, U)> for Multiplication -where - T: AsRef, - U: AsRef, -{ - type Error = MultiplicationError; - fn try_from(value: (T, U)) -> std::result::Result { - let first = - value.0.as_ref().parse().map_err(|_| { - MultiplicationError::FirstValueParseError(value.0.as_ref().to_string()) - }); - let second = - value.1.as_ref().parse().map_err(|_| { - MultiplicationError::SecondValueParseError(value.1.as_ref().to_string()) - }); - Ok(Self(first?, second?)) - } -} - -#[derive(Error, Debug, Diagnostic)] -pub enum CalculatorError { - #[error("Failed to compile regex: {0}")] - #[diagnostic(code(calculator::regex_error))] - RegexError(#[from] regex::Error), - - #[diagnostic(code(calculator::multiplication_error))] - #[error("Failed to parse multiplication: {0}")] - MultiplicationError(#[from] MultiplicationError), - - #[error("Failed to process input: {0}")] - #[diagnostic(code(calculator::process_error))] - ProcessError(String), -} - -#[derive(Debug)] -struct Calculator { - do_re: Regex, - mult_re: Regex, -} - -impl Calculator { - fn new() -> Result { - Ok(Self { - do_re: Regex::new(r"^.*?don't\(\)|do\(\)(.*?)don't\(\)|do\(\).*$")?, - mult_re: Regex::new(r"mul\((\d{1,3}),(\d{1,3})\)")?, - }) - } - - fn extract_multiplications<'a>( - &'a self, - line: &'a str, - ) -> impl Iterator + 'a { - self.mult_re.captures_iter(line).filter_map(|caps| { - Multiplication::try_from((caps.get(1)?.as_str(), caps.get(2)?.as_str())).ok() - }) - } - - fn extract_do<'a>(&'a self, line: &'a str) -> impl Iterator + 'a { - self.do_re - .captures_iter(line) - .filter_map(|caps| caps.get(0).map(|m| m.as_str())) - } - - fn process(&self, input: &str) -> usize { - let value = input.lines().collect::(); - self.extract_do(&value) - .flat_map(|line| self.extract_multiplications(line)) - .map(|mult| mult.calculate()) - .sum() - } -} - -#[tracing::instrument] -pub fn process(input: &str) -> Result { - let calculator = Calculator::new()?; - Ok(calculator.process(input)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_process() -> Result<()> { - let input = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"; - let result = 48; - assert_eq!(process(input)?, result); - Ok(()) - } -} diff --git a/2024/day-08/Cargo.toml b/2024/day-08/Cargo.toml new file mode 100644 index 0000000..915a4d7 --- /dev/null +++ b/2024/day-08/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "day-08" +version = "0.1.0" +edition = "2021" + +# 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-08-bench" +path = "benches/benchmarks.rs" +harness = false + +[lints.clippy] +pedantic = "warn" +nursery = "warn" diff --git a/2024/day-08/benches/benchmarks.rs b/2024/day-08/benches/benchmarks.rs new file mode 100644 index 0000000..0ddf332 --- /dev/null +++ b/2024/day-08/benches/benchmarks.rs @@ -0,0 +1,21 @@ +use day_08::*; + +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(); +} diff --git a/2024/day-08/src/bin/part1.rs b/2024/day-08/src/bin/part1.rs new file mode 100644 index 0000000..002b694 --- /dev/null +++ b/2024/day-08/src/bin/part1.rs @@ -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).context("process part 1")?; + println!("{result}"); + Ok(()) +} diff --git a/2024/day-08/src/bin/part2.rs b/2024/day-08/src/bin/part2.rs new file mode 100644 index 0000000..4d52dbd --- /dev/null +++ b/2024/day-08/src/bin/part2.rs @@ -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(()) +} diff --git a/2024/day-08/src/lib.rs b/2024/day-08/src/lib.rs new file mode 100644 index 0000000..faaf542 --- /dev/null +++ b/2024/day-08/src/lib.rs @@ -0,0 +1,2 @@ +pub mod part1; +pub mod part2; diff --git a/2024/day-08/src/part1.rs b/2024/day-08/src/part1.rs new file mode 100644 index 0000000..3cd4514 --- /dev/null +++ b/2024/day-08/src/part1.rs @@ -0,0 +1,341 @@ +use std::{ + collections::HashMap, + fmt::Display, + ops::{Add, Sub}, + str::FromStr, +}; + +use miette::{Diagnostic, Result}; +use thiserror::Error; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Vec2 { + row: usize, + col: usize, +} + +impl Vec2 { + const fn abs_diff(self, other: Self) -> Self { + Self { + row: self.row.abs_diff(other.row), + col: self.col.abs_diff(other.col), + } + } + + const fn manhattan_distance(self, other: Self) -> usize { + let diff = self.abs_diff(other); + diff.row + diff.col + } +} + +impl From for (usize, usize) { + fn from(value: Vec2) -> Self { + (value.row, value.col) + } +} + +impl Add for Vec2 { + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Self { + row: self.row + rhs.row, + col: self.col + rhs.col, + } + } +} + +impl Sub for Vec2 { + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Vec2 { + row: self.row.saturating_sub(rhs.row), + col: self.col.saturating_sub(rhs.col), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +enum Location { + Antenna(char), + Antinode, + Empty, +} + +impl From for Location { + fn from(value: char) -> Self { + match value { + '.' => Self::Empty, + '#' => Self::Antinode, + ch => Self::Antenna(ch), + } + } +} + +impl Display for Location { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let ch = match self { + Self::Antenna(ch) => *ch, + Self::Antinode => '#', + Self::Empty => '.', + }; + write!(f, "{}", ch) + } +} + +#[derive(Debug, Error, Diagnostic)] +enum RoofError { + #[error("Failed to parse roof")] + ParseError, +} + +fn calculate_diffs(positions: &[Vec2]) -> Vec { + positions + .iter() + .enumerate() + .flat_map(|(idx, &pos1)| { + positions[idx + 1..] + .iter() + .map(move |&pos2| pos1.abs_diff(pos2)) + }) + .collect() +} + +#[derive(Debug, PartialEq, Eq)] +struct Roof(Vec>); + +impl Roof { + fn place_antinodes(&mut self) { + let antenna_positions = self.antenna_positions(); + for (_, pos) in antenna_positions.iter() { + for i in 0..pos.len() { + for j in (i + 1)..pos.len() { + let (pos1, pos2) = (pos[i], pos[j]); + + if let Some(antinode_pos) = self.find_antinode_position(pos1, pos2) { + self.0[antinode_pos.row][antinode_pos.col] = Location::Antinode; + } + } + } + } + } + + fn find_antinode_position(&self, pos1: Vec2, pos2: Vec2) -> Option { + let diff = pos1.abs_diff(pos2); + let total_dist = diff.row + diff.col; + + // Try each step between the antennas + for step in 0..=total_dist { + let row = if pos1.row < pos2.row { + pos1.row + step.min(diff.row) + } else { + pos1.row - step.min(diff.row) + }; + + let col = if pos1.col < pos2.col { + pos1.col + step.min(diff.col) + } else { + pos1.col - step.min(diff.col) + }; + + let candidate = Vec2 { row, col }; + + // Check if position is valid and equidistant + if row < self.0.len() + && col < self.0[0].len() + && matches!(self.0[row][col], Location::Empty) + { + let dist1 = pos1.manhattan_distance(candidate); + let dist2 = pos2.manhattan_distance(candidate); + if dist1 == dist2 { + return Some(candidate); + } + } + } + None + } + + fn antenna_positions(&self) -> HashMap> { + self.0 + .iter() + .enumerate() + .flat_map(|(row_idx, row)| { + row.iter() + .enumerate() + .filter_map(move |(col_idx, col)| match col { + Location::Antenna(ch) => Some(( + *ch, + Vec2 { + row: row_idx, + col: col_idx, + }, + )), + _ => None, + }) + }) + .fold(HashMap::new(), |mut acc, (key, pos)| { + acc.entry(key).or_default().push(pos); + acc + }) + } + + fn count(&self) -> usize { + self.0 + .iter() + .map(|row| { + row.iter() + .filter(|pos| matches!(pos, Location::Antinode)) + .count() + }) + .sum() + } + + fn row_len(&self) -> usize { + self.0.len() + } + + fn col_len(&self) -> usize { + self.0[0].len() + } + + fn is_within_bounds(&self, pos: &Vec2) -> bool { + pos.row < self.row_len() && pos.col < self.col_len() + } +} + +impl FromStr for Roof { + type Err = RoofError; + fn from_str(s: &str) -> std::result::Result { + let roof = s + .lines() + .map(|line| line.chars().map(Location::from).collect::>()) + .collect::>(); + Ok(Self(roof)) + } +} + +impl Display for Roof { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (idx, row) in self.0.iter().enumerate() { + for pos in row { + write!(f, "{}", pos)?; + } + if idx < self.0.len() - 1 { + writeln!(f)?; + } + } + Ok(()) + } +} + +#[tracing::instrument] +pub fn process(input: &str) -> Result { + let mut roof = Roof::from_str(input)?; + roof.place_antinodes(); + println!("{roof}"); + Ok(roof.count()) +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[test] + fn test_process() -> Result<()> { + let input = "............ +........0... +.....0...... +.......0.... +....0....... +......A..... +............ +............ +........A... +.........A.. +............ +............"; + let result = 14; + assert_eq!(process(input)?, result); + Ok(()) + } + + #[rstest] + #[case( + ".......... +.......... +.......... +....a..... +.......... +.....a.... +.......... +.......... +.......... +..........", + ".......... +...#...... +.......... +....a..... +.......... +.....a.... +.......... +......#... +.......... +.........." + )] + /* #[case( + ".......... + .......... + .......... + ....a..... + ........a. + .....a.... + .......... + .......... + .......... + ..........", + ".......... + ...#...... + #......... + ....a..... + ........a. + .....a.... + ..#....... + ......#... + .......... + .........." + )] + #[case( + "............ + ........0... + .....0...... + .......0.... + ....0....... + ......A..... + ............ + ............ + ........A... + .........A.. + ............ + ............", + "......#....# + ...#....0... + ....#0....#. + ..#....0.... + ....0....#.. + .#....A..... + ...#........ + #......#.... + ........A... + .........A.. + ..........#. + ..........#." + )] */ + fn test_layout(#[case] input_str: &str, #[case] output_str: &str) -> Result<()> { + let mut input = Roof::from_str(input_str)?; + input.place_antinodes(); + let output = Roof::from_str(output_str)?; + assert_eq!(input, output); + Ok(()) + } +} diff --git a/2024/day-08/src/part2.rs b/2024/day-08/src/part2.rs new file mode 100644 index 0000000..cbcdee4 --- /dev/null +++ b/2024/day-08/src/part2.rs @@ -0,0 +1,21 @@ +use miette::Result; + +#[tracing::instrument] +pub fn process(input: &str) -> Result { + 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(()) + } +}