use std::ops::RangeInclusive; fn main() { aoc2025::run( "Day 2: Gift Shop", "inputs/day_02.txt", parse_input, part_1, part_2, ) .unwrap(); } fn parse_chunk(chunk: &str) -> Option> { let chunk = chunk.trim(); let (start, end) = chunk.find('-').map(|i| chunk.split_at(i))?; let start = start.parse::().ok(); let end = end.trim_start_matches('-').parse::().ok(); start.zip(end).map(|(start, end)| start..=end) } fn parse_input(input: &str) -> Vec> { (input.split(',')) .filter_map(|ch| parse_chunk(ch)) .collect::>() } fn repeats_once(value: u64) -> bool { let value = value.to_string(); if value.len() % 2 != 0 { return false; } let (start, end) = value.split_at(value.len() / 2); start == end } fn factors(x: usize) -> Vec { (1..=x) .filter_map(|i| if x % i == 0 { Some(i) } else { None }) .collect::>() } fn repeats_multiple_times(value: u64) -> bool { let value = value.to_string(); let value = value.as_bytes(); if value.len() <= 1 { return false; } for l in factors(value.len()) { if l == value.len() { continue; } let first = &value[0..l]; if value.chunks(l).all(|c| c == first) { return true; } } false } fn part_1(ranges: Vec>) -> u64 { (ranges.into_iter()) .map(|range| range.filter(|x| repeats_once(*x)).sum::()) .sum() } fn part_2(ranges: Vec>) -> u64 { (ranges.into_iter()) .map(|range| range.filter(|x| repeats_multiple_times(*x)).sum::()) .sum() } #[cfg(test)] mod tests { use std::ops::RangeInclusive; use super::*; use proptest::prelude::*; prop_compose! { fn chunk()( x in any::(), y in any::(), w0 in "\\s*", w1 in "\\s*" ) -> (RangeInclusive, String) { let start = x.min(y); let end = x.max(y); (start..=end, format!("{}{}-{}{}", w0, start, end, w1)) } } prop_compose! { fn input()(v in prop::collection::vec(chunk(), 1..100)) -> (Vec>, String) { let (ranges, chunks): (Vec<_>, Vec<_>) = v.into_iter().unzip(); (ranges, chunks.join(",")) } } mod parse_chunk { use super::*; proptest! { #[test] fn doesnt_crash(s in "\\PC+") { parse_chunk(s.as_str()); } #[test] fn parses_chunks(ch in chunk()) { let (expected, input) = ch; prop_assert_eq!(Some(expected), parse_chunk(&input)); } } } mod parse_input { use super::*; proptest! { #[test] fn doesnt_crash(s in "\\PC+") { parse_input(s.as_str()); } #[test] fn ignores_empty_chunks(s in ",+") { prop_assert_eq!(Vec::>::new(), parse_input(&s)); } #[test] fn parses_input_correctly(s in input()) { let (ranges, input) = s; prop_assert_eq!(ranges, parse_input(&input)); } } } mod repeats_once { use super::*; fn odd_length() -> impl Strategy { any::().prop_filter("must be odd length", |x| x.to_string().len() % 2 != 0) } fn even_length_nonrepeating() -> impl Strategy { any::() .prop_filter("must be even length", |x| x.to_string().len() % 2 == 0) .prop_filter("must be nonrepeating", |x| { let x = x.to_string(); let (a, b) = x.split_at(x.len() / 2); a != b }) } const MAX_SUBNUMBER: u64 = 1_844_674_407; prop_compose! { fn even_length_repeating()(x in 0_u64..MAX_SUBNUMBER) -> u64 { format!("{}{}", x, x).parse::().unwrap() } } proptest! { #[test] fn doesnt_crash(x in any::()) { repeats_once(x); } #[test] fn odd_lengths_cant_repeat(x in odd_length()) { prop_assert!(!repeats_once(x)); } #[test] fn detects_nonrepeating(x in even_length_nonrepeating()) { prop_assert!(!repeats_once(x)); } #[test] fn detects_repeating_once(x in even_length_repeating()) { prop_assert!(repeats_once(x)); } } } mod factors { use super::*; // We can test smaller ranges here because `factors` will only be used // on the length of the string representation of a number, not the // number itself. proptest! { #[test] fn doesnt_crash(x in 1_usize..=20) { factors(x); } #[test] fn always_includes_one(x in 1_usize..=20) { prop_assert!(factors(x).contains(&1)) } #[test] fn always_includes_input(x in 1_usize..=20) { prop_assert!(factors(x).contains(&x)) } #[test] fn factors_correctly(x in 1_usize..=10, y in 1_usize..=10) { let f = factors(x * y); prop_assert!(f.contains(&x)); prop_assert!(f.contains(&y)); } } } mod repeats_multiple_times { use proptest::string; use super::*; // Any string that repeats X times must be a multiple of that length // Therefore, find the factors of len(s) and get the substring chunks // If all are equal for any factor, return true const PRIME_LENGTHS: &[usize] = &[2, 3, 5, 7, 11, 13, 17, 19]; // Doesn't cover all nonrepeating numbers but that's a lot of work and // this should be good enough fn nonrepeating() -> impl Strategy { prop::sample::select(PRIME_LENGTHS.to_vec()).prop_flat_map(|len| { let pattern = format!("[1-9][0-9]{{{}}}", len - 1); string::string_regex(pattern.as_str()) .unwrap() .prop_filter_map("not all digits identical", move |s| { let mut chars = s.chars(); let first = chars.next().unwrap(); if chars.all(|c| c == first) { return None; } s.parse::().ok() }) }) } fn repeating() -> impl Strategy { (2_usize..10, 1_usize..10) .prop_filter("total length must fit within u64", |(x, y)| x * y < 20) .prop_flat_map(|(n, length)| { let pattern = match length { 1 => "[1-9]".to_string(), l => format!("[1-9][0-9]{{{}}}", l - 1), }; (string::string_regex(&pattern).unwrap()) .prop_filter_map("parseable u64", move |chunk| { chunk.repeat(n).parse::().ok() }) }) } proptest! { #[test] fn doesnt_crash(x in any::()) { repeats_multiple_times(x); } #[test] fn single_digits_cant_repeat(x in 0_u64..10) { prop_assert!(!repeats_multiple_times(x)); } #[test] fn detects_nonrepeating_numbers(x in nonrepeating()) { prop_assert!(!repeats_multiple_times(x)); } #[test] fn detects_repeating_numbers(x in repeating()) { prop_assert!(repeats_multiple_times(x)) } } } const EXAMPLE_INPUT: &str = "11-22,95-115,998-1012,1188511880-1188511890,222220-222224,1698522-1698528,446443-446449,38593856-38593862,565653-565659,824824821-824824827,2121212118-2121212124"; mod part_1 { use super::*; const EXAMPLE_ANSWER: u64 = 1227775554; #[test] fn solves_example() { let input = parse_input(EXAMPLE_INPUT); assert_eq!(EXAMPLE_ANSWER, part_1(input)); } } mod part_2 { use super::*; const EXAMPLE_ANSWER: u64 = 4174379265; #[test] fn solves_example() { let input = parse_input(EXAMPLE_INPUT); assert_eq!(EXAMPLE_ANSWER, part_2(input)); } } }