diff --git a/2024/day-2/Cargo.toml b/2024/day-2/Cargo.toml new file mode 100644 index 0000000..4892e58 --- /dev/null +++ b/2024/day-2/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "day-2" +version.workspace = true +edition.workspace = true +authors.workspace = true +repository.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + + +[dependencies] +nom.workspace = true +itertools.workspace = true +log.workspace = true +error-stack.workspace = true +thiserror.workspace = true +dhat.workspace = true + +[dev-dependencies] +test-log.workspace = true +rstest.workspace = true + +[features] +dhat-heap = [] diff --git a/2024/day-2/src/lib.rs b/2024/day-2/src/lib.rs new file mode 100644 index 0000000..3fafe8d --- /dev/null +++ b/2024/day-2/src/lib.rs @@ -0,0 +1,4 @@ +pub mod part1; +pub use crate::part1::*; +pub mod part2; +pub use crate::part2::*; diff --git a/2024/day-2/src/main.rs b/2024/day-2/src/main.rs new file mode 100644 index 0000000..452473b --- /dev/null +++ b/2024/day-2/src/main.rs @@ -0,0 +1,31 @@ +#![warn(clippy::all, clippy::pedantic)] + +use day_2::part1; +use day_2::part2; + +use error_stack::{Result, ResultExt}; +use thiserror::Error; + +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +#[derive(Debug, Error)] +enum Day2Error { + #[error("Part 1 failed")] + Part1Error, + #[error("Part 2 failed")] + Part2Error, +} + +fn main() -> Result<(), Day2Error> { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + + let input = include_str!("./input.txt"); + let part1_result = part1(input).change_context(Day2Error::Part1Error)?; + println!("part 1: {part1_result}"); + let part2_result = part2(input).change_context(Day2Error::Part2Error)?; + println!("part 2: {part2_result}"); + Ok(()) +} diff --git a/2024/day-2/src/part1.rs b/2024/day-2/src/part1.rs new file mode 100644 index 0000000..65a198a --- /dev/null +++ b/2024/day-2/src/part1.rs @@ -0,0 +1,99 @@ +#![warn(clippy::all, clippy::pedantic)] + +use error_stack::{Report, Result, ResultExt}; +use nom::{character::complete, multi::separated_list1, IResult}; +use std::cmp::Ordering; +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq)] +enum Safety { + Safe, + UnSafe, +} + +#[derive(Debug, PartialEq, Eq)] +struct XmasReport { + levels: Vec, +} + +impl XmasReport { + pub fn is_safe(&self) -> Safety { + let mut dir = Ordering::Equal; + for i in 1..self.levels.len() { + if !(1_u32..=3).contains(&(self.levels[i - 1].abs_diff(self.levels[i]))) { + return Safety::UnSafe; + } + let new_dir = self.levels[i - 1].cmp(&self.levels[i]); + if dir != Ordering::Equal && dir != new_dir { + return Safety::UnSafe; + } + dir = new_dir; + } + Safety::Safe + } +} + +// day-2 +#[derive(Debug, Error)] +pub enum Day2Part1Error { + #[error("Problem parsing Day 2")] + ParseError, +} + +/// Day-2 Part 1 for 2024 advent of code +/// Problem can be found here: +/// +/// # Errors +/// - `ParseError` there was an issue with the parser +pub fn part1(input: &str) -> Result { + let (_, reports) = parse_input(input) + .map_err(|x| Report::from(x.to_owned())) + .change_context(Day2Part1Error::ParseError)?; + Ok(reports + .iter() + .filter(|x| x.is_safe() == Safety::Safe) + .count() + .to_string()) +} + +fn parse_level(input: &str) -> IResult<&str, XmasReport> { + let (input, v) = separated_list1(complete::space1, complete::u32)(input)?; + Ok((input, XmasReport { levels: v })) +} + +fn parse_input(input: &str) -> IResult<&str, Vec> { + separated_list1(complete::line_ending, parse_level)(input) +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("7 6 4 2 1", Safety::Safe)] + #[case("1 2 7 8 9", Safety::UnSafe)] + #[case("9 7 6 2 1", Safety::UnSafe)] + #[case("1 3 2 4 5", Safety::UnSafe)] + #[case("8 6 4 4 1", Safety::UnSafe)] + #[case("1 3 6 7 9", Safety::Safe)] + fn part1_report_safety(#[case] input: &str, #[case] expected: Safety) { + let (_, tester) = parse_level(input).expect("should be valid input"); + assert_eq!(tester.is_safe(), expected); + } + + const INPUT: &str = "7 6 4 2 1 +1 2 7 8 9 +9 7 6 2 1 +1 3 2 4 5 +8 6 4 4 1 +1 3 6 7 9"; + + #[test_log::test] + #[test_log(default_log_filter = "trace")] + fn part1_works() { + let result = part1(INPUT).unwrap(); + assert_eq!(result, "2".to_string()); + } +} + diff --git a/2024/day-2/src/part2.rs b/2024/day-2/src/part2.rs new file mode 100644 index 0000000..35774f2 --- /dev/null +++ b/2024/day-2/src/part2.rs @@ -0,0 +1,117 @@ +#![warn(clippy::all, clippy::pedantic)] + +use error_stack::{Report, Result, ResultExt}; +use nom::{character::complete, multi::separated_list1, IResult}; +use std::cmp::Ordering; +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq)] +enum Safety { + Safe, + UnSafe, +} + +#[derive(Debug, PartialEq, Eq)] +struct XmasReport { + levels: Vec, +} + +impl XmasReport { + pub fn is_safe(&self) -> Safety { + fn internal_check(levels: &[u32]) -> Safety { + let mut dir = Ordering::Equal; + for i in 1..levels.len() { + if !(1_u32..=3).contains(&(levels[i - 1].abs_diff(levels[i]))) { + return Safety::UnSafe; + } + let new_dir = levels[i - 1].cmp(&levels[i]); + if dir != Ordering::Equal && dir != new_dir { + return Safety::UnSafe; + } + dir = new_dir; + } + Safety::Safe + } + let ret = internal_check(&self.levels); + if ret == Safety::Safe { + return ret; + } + for i in 0..self.levels.len() { + let mut attempt = self.levels.clone(); + let _ = attempt.remove(i); + let ret = internal_check(&attempt); + if ret == Safety::Safe { + return ret; + } + } + Safety::UnSafe + } +} + +// day-2 +#[derive(Debug, Error)] +pub enum Day2Part2Error { + #[error("Problem parsing Day 2")] + ParseError, +} + +/// Day-2 Part 2 for 2024 advent of code +/// Problem can be found here: +/// +/// # Errors +/// - `ParseError` there was an issue with the parser +pub fn part2(input: &str) -> Result { + let (_, reports) = parse_input(input) + .map_err(|x| Report::from(x.to_owned())) + .change_context(Day2Part2Error::ParseError)?; + Ok(reports + .iter() + .filter(|x| x.is_safe() == Safety::Safe) + .count() + .to_string()) +} + +fn parse_level(input: &str) -> IResult<&str, XmasReport> { + let (input, v) = separated_list1(complete::space1, complete::u32)(input)?; + Ok((input, XmasReport { levels: v })) +} + +fn parse_input(input: &str) -> IResult<&str, Vec> { + separated_list1(complete::line_ending, parse_level)(input) +} + +#[cfg(test)] +mod test { + use super::*; + use rstest::rstest; + + #[rstest] + #[case("7 6 4 2 1", Safety::Safe)] + #[case("1 2 7 8 9", Safety::UnSafe)] + #[case("9 7 6 2 1", Safety::UnSafe)] + #[case("1 3 2 4 5", Safety::Safe)] + #[case("8 6 4 4 1", Safety::Safe)] + #[case("1 3 6 7 9", Safety::Safe)] + #[case("5 3 6 7 9", Safety::Safe)] + #[case("5 3 6 7 6", Safety::UnSafe)] + #[case("8 6 4 6 4", Safety::UnSafe)] + fn part1_report_safety(#[case] input: &str, #[case] expected: Safety) { + let (_, tester) = parse_level(input).expect("should be valid input"); + assert_eq!(tester.is_safe(), expected); + } + + const INPUT: &str = "7 6 4 2 1 +1 2 7 8 9 +9 7 6 2 1 +1 3 2 4 5 +8 6 4 4 1 +1 3 6 7 9"; + + #[test_log::test] + #[test_log(default_log_filter = "trace")] + fn part2_works() { + let result = part2(INPUT).unwrap(); + assert_eq!(result, "4".to_string()); + } +} +