diff --git a/Cargo.lock b/Cargo.lock index 21b9825..35b2d33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,7 @@ version = "0.1.0" dependencies = [ "color-eyre", "criterion", + "itertools 0.14.0", "test-log", "tracing", "tracing-error", @@ -180,7 +181,7 @@ dependencies = [ "clap", "criterion-plot", "is-terminal", - "itertools", + "itertools 0.10.5", "num-traits", "once_cell", "oorandom", @@ -201,7 +202,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools", + "itertools 0.10.5", ] [[package]] @@ -300,6 +301,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" diff --git a/Cargo.toml b/Cargo.toml index 47376c3..9af39ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] color-eyre = "0.6" +itertools = "0.14" tracing = "0.1" tracing-error = "0.2" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/README.md b/README.md index cc78147..614d3f3 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ To run the tests against included test input files: `RUST_LOG=debug cargo test - Because this is over-engineered, I've included benchmarks for each day's solution. Because, why not? -To run benchmarks: `cargo bench`. +To run benchmarks: `cargo bench`. Or a specific day and/or part: `cargo bench -- "day02 part1"`. ### Results @@ -29,3 +29,4 @@ Timings are given as: [lower-bound **best-estimate** upper-bound] | Day | Part 1 | Part 2 | |-----|--------|--------| | 01 | [101.34 µs **101.95 µs** 102.61 µs] | [105.90 µs **106.40 µs** 106.95 µs] | +| 02 | [165.59 ms **166.60 ms** 167.65 ms] | [184.17 ms **185.25 ms** 186.42 ms] | diff --git a/benches/aoc.rs b/benches/aoc.rs index e3bd6d5..8eefa4b 100644 --- a/benches/aoc.rs +++ b/benches/aoc.rs @@ -1,4 +1,5 @@ use aoc::day01; +use aoc::day02; use criterion::{Criterion, criterion_group, criterion_main}; fn day01_benchmark(c: &mut Criterion) { @@ -6,5 +7,10 @@ fn day01_benchmark(c: &mut Criterion) { c.bench_function("day01 part2", |b| b.iter(|| day01::part2(day01::INPUT))); } -criterion_group!(benches, day01_benchmark); +fn day02_benchmark(c: &mut Criterion) { + c.bench_function("day02 part1", |b| b.iter(|| day02::part1(day02::INPUT))); + c.bench_function("day02 part2", |b| b.iter(|| day02::part2(day02::INPUT))); +} + +criterion_group!(benches, day01_benchmark, day02_benchmark); criterion_main!(benches); diff --git a/src/day02/input/test1.txt b/src/day02/input/test1.txt new file mode 100644 index 0000000..a3f22ef --- /dev/null +++ b/src/day02/input/test1.txt @@ -0,0 +1 @@ +11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124 diff --git a/src/day02/mod.rs b/src/day02/mod.rs new file mode 100644 index 0000000..0dee14d --- /dev/null +++ b/src/day02/mod.rs @@ -0,0 +1,172 @@ +use std::{fmt::Display, str::FromStr}; + +use color_eyre::{ + Result, + eyre::{Error, OptionExt, eyre}, +}; +use itertools::Itertools; +use tracing::{debug, debug_span, info, info_span, instrument}; + +pub const INPUT: &str = include_str!("input/input.txt"); + +#[derive(Clone, Debug)] +struct ProductRange(std::ops::RangeInclusive); + +impl FromStr for ProductRange { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('-'); + let start = parts + .next() + .ok_or(eyre!("Invalid product range: no start"))? + .parse::()?; + let end = parts + .next() + .ok_or(eyre!("Invalid product range: no end"))? + .parse::()?; + if parts.next().is_some() { + return Err(eyre!("Invalid product range: too many parts")); + } + Ok(ProductRange(start..=end)) + } +} + +impl Display for ProductRange { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}-{}", self.0.start(), self.0.end()) + } +} + +impl Iterator for ProductRange { + type Item = i64; + + fn next(&mut self) -> Option { + self.0.next() + } + + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +fn is_valid_product_id(id: i64) -> Result { + let digits: Vec = id + .to_string() + .chars() + .map(|c| { + c.to_digit(10) + .ok_or_eyre("Invalid product id: contains a non-decimal digit") + }) + .collect::>>()?; + if digits.len() % 2 != 0 { + return Ok(true); + } + Ok(digits[..digits.len() / 2] != digits[digits.len() / 2..]) +} + +fn is_valid_product_id2(id: i64) -> Result { + let digits: Vec = id + .to_string() + .chars() + .map(|c| { + c.to_digit(10) + .ok_or_eyre("Invalid product id: contains a non-decimal digit") + }) + .collect::>>()?; + let mut chunk_size = digits.len() / 2; + loop { + if chunk_size == 0 { + break; + } + if digits + .chunks(chunk_size) + .tuple_windows() + .all(|(a, b)| a == b) + { + return Ok(false); + } + chunk_size -= 1; + } + Ok(true) +} + +impl ProductRange { + fn invalid_ids(self) -> Result> { + let mut invalid_ids = vec![]; + + for id in self { + if !is_valid_product_id(id)? { + invalid_ids.push(id); + } + } + + debug!("Invalid IDs: {:?}", &invalid_ids); + Ok(invalid_ids) + } + + fn invalid_ids2(self) -> Result> { + let mut invalid_ids = vec![]; + + for id in self { + if !is_valid_product_id2(id)? { + invalid_ids.push(id); + } + } + + debug!("Invalid IDs: {:?}", &invalid_ids); + Ok(invalid_ids) + } +} + +#[instrument(skip(input))] +pub fn part1(input: &str) -> Result { + let mut total_invalid = 0; + for range in input.trim().split(',') { + let _span = debug_span!("range", range = %range).entered(); + let range: ProductRange = range.parse()?; + total_invalid += range.invalid_ids()?.iter().sum::(); + } + Ok(total_invalid) +} + +#[instrument(skip(input))] +pub fn part2(input: &str) -> Result { + let mut total_invalid = 0; + for range in input.trim().split(',') { + let _span = debug_span!("range", range = %range).entered(); + let range: ProductRange = range.parse()?; + total_invalid += range.invalid_ids2()?.iter().sum::(); + } + Ok(total_invalid) +} + +pub fn solve() -> Result<()> { + info!("Day 2"); + { + let _span = info_span!("day02").entered(); + let p1 = part1(INPUT)?; + info!("Part 1: {}", p1); + let p2 = part2(INPUT)?; + info!("Part 2: {}", p2); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use test_log::test; + + const TEST_INPUT1: &str = include_str!("input/test1.txt"); + + #[test] + fn test_part1() { + assert_eq!(part1(TEST_INPUT1).unwrap(), 1227775554); + } + + #[test] + fn test_part2() { + assert_eq!(part2(TEST_INPUT1).unwrap(), 4174379265); + } +} diff --git a/src/lib.rs b/src/lib.rs index 12b8f18..28326d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,2 @@ pub mod day01; +pub mod day02; diff --git a/src/main.rs b/src/main.rs index 03aad64..1832392 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ pub mod day01; +pub mod day02; use color_eyre::Result; use tracing::info; @@ -24,6 +25,7 @@ fn main() -> Result<()> { { let _span = tracing::info_span!("aoc").entered(); day01::solve()?; + day02::solve()?; } Ok(()) }