diff --git a/examples/day_01_secret_entrance.proptest-regressions b/examples/day_01_secret_entrance.proptest-regressions new file mode 100644 index 0000000..3cdf791 --- /dev/null +++ b/examples/day_01_secret_entrance.proptest-regressions @@ -0,0 +1,10 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 49c016dc4fd11fa9bbfb8bbf0f07e752bc534c486ad5b54b7bb24b97f083aab4 # shrinks to s = ":" +cc 8492ad45bd0ba7b1c687c413c52779c63bb91de1c2ec36d4c5e58cffdeb07e9c # shrinks to s = "R2200000000" +cc 99935ba16841a9db1a7e192bec5734770069262016d0e91724af515fdd5fb798 # shrinks to acc = 0, delta = 0 +cc 406bf951122ca7019f2f56865072c4edf7f09d75c44fab42846bcea9cbb6278a # shrinks to acc = 16, delta = -6916 diff --git a/examples/day_01_secret_entrance.rs b/examples/day_01_secret_entrance.rs new file mode 100644 index 0000000..4804dca --- /dev/null +++ b/examples/day_01_secret_entrance.rs @@ -0,0 +1,182 @@ +fn main() { + aoc2025::run( + "Day 1: Secret Entrance", + "inputs/day_01.txt", + parse_input, + part_1, + part_2, + ) + .unwrap(); +} + +/// Parses the input into a list of signed integers. +fn parse_input(input: &str) -> Vec { + input + .split('\n') + .filter(|x| !x.is_empty()) + .filter_map(|x| parse_line(x)) + .collect() +} + +/// Parses the given line into a signed integer +fn parse_line(line: &str) -> Option { + let line = line.replace("L", "-").replace("R", ""); + i32::from_str_radix(line.as_str(), 10).ok() +} + +fn part_1(deltas: Vec) -> usize { + // The input file contains a list of rotations in the form `XYY`, + // where `X` is a direction (`L` for left or `R` for right) and + // `YY` is a non-zero-padded number of clicks. + // + // The dial consists of numbers 0-99. Turning left decreases the + // number, and turning right increases it. Turning too far will + // overflow to the opposite end of the range. + // + // The dial starts at 50. Run the input file against this dial and + // record the number of times it leaves it pointing at 0. + + // Iterate through the list and count the number of times the + // accumulated sum equals zero. + (deltas.into_iter()) + .scan(50, |position, delta| { + *position += delta; + Some(*position) + }) + .filter(|position| position % 100 == 0) + .count() +} + +fn part_2(deltas: Vec) -> i32 { + (deltas.into_iter()) + .scan(50, |prev, delta| { + let (next, hits) = step(*prev, delta); + *prev = next; + Some(hits) + }) + .sum() +} + +fn step(start: i32, delta: i32) -> (i32, i32) { + let mut hits = (delta / 100).abs(); + + let remainder = delta % 100; + if start + remainder <= 0 && start != 0 || 99 < start + remainder { + hits += 1; + } + + ((start + delta).rem_euclid(100), hits) +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + prop_compose! { + fn line_input()(s in -999_i32..999) -> (i32, String) { + (s, format!("{}{}", if s < 0 { "L" } else { "R" }, s.abs())) + } + } + + mod parse_line { + use super::*; + + #[test] + fn ignores_gibberish() { + assert_eq!(None, parse_line(":")); + } + + proptest! { + #[test] + fn doesnt_crash(s in "\\PC*") { + parse_line(&s); + } + + #[test] + fn parses_lines_correctly((s, line) in line_input()) { + prop_assert_eq!(s, parse_line(&line).unwrap()) + } + } + } + + mod parse_input { + use super::*; + + proptest! { + #[test] + fn doesnt_crash(s in "\\PC+") { + parse_input(&s); + } + + #[test] + fn ignores_empty_lines(s in "\\n+") { + prop_assert!(parse_input(&s).is_empty()); + } + + #[test] + fn parses_files(s in prop::collection::vec(line_input(), 1..100)) { + let (values, input): (Vec, Vec) = s.into_iter().unzip(); + prop_assert_eq!(values, parse_input(&input.join("\n"))); + } + } + } + + mod part_1 { + use super::*; + + proptest! { + #[test] + fn counts_halfstep_deltas(s in prop::collection::vec(Just(50_i32), 0..100)) { + prop_assert_eq!((s.len() + 1) / 2, part_1(s)); + } + } + } + + mod part_2 { + use super::*; + + // Slow, naive step implementation to compare fast version against. + fn step_reference(start: i32, delta: i32) -> (i32, i32) { + let mut pos = start; + let dir = if delta >= 0 { 1 } else { -1 }; + let mut hits = 0; + + for _ in 0..delta.abs() { + pos += dir; + if pos.rem_euclid(100) == 0 { + hits += 1; + } + } + + let final_acc = pos.rem_euclid(100); + (final_acc, hits) + } + + proptest! { + #[test] + fn counts_perfect_deltas(s in prop::collection::vec(Just(100), 0..100)) { + prop_assert_eq!(s.len() as i32, part_2(s)); + } + + #[test] + fn counts_perfect_multiple_deltas(s in prop::collection::vec(0..9, 0..100)) { + prop_assert_eq!( + s.clone().into_iter().sum::(), + part_2(s.into_iter().map(|x| x * 100).collect()) + ); + } + + #[test] + fn steps_correctly( + start in 0i32..100, + delta in -10_000_i32..-1 + ) { + let reference_hits = step_reference(start, delta); + let real_hits = step(start, delta); + + prop_assert_eq!(reference_hits, real_hits); + } + } + } +}