Advent of Code solutions in Rust

feat: day 5 of AoC 2025

+142 -2
+140
aoc_2025/src/day05.rs
··· 1 + use std::{collections::HashSet, ops::RangeInclusive}; 2 + 3 + use anyhow::Context; 4 + use aoc_companion::prelude::*; 5 + use aoc_utils::range::RangeSet; 6 + use itertools::Itertools as _; 7 + 8 + pub(crate) struct Door { 9 + fresh_ranges: Vec<RangeInclusive<u64>>, 10 + available_ingredients: Vec<u64>, 11 + } 12 + 13 + impl<'input> Solution<'input> for Door { 14 + fn parse(input: &'input str) -> Result<Self> { 15 + let Some((fresh, available)) = input.split_once("\n\n") else { 16 + anyhow::bail!("missing empty line separating fresh ranges from ingredients"); 17 + }; 18 + Ok(Door { 19 + fresh_ranges: fresh 20 + .lines() 21 + .map(|r| -> Result<_> { 22 + let (from, to) = r 23 + .split_once('-') 24 + .with_context(|| anyhow::anyhow!("missing dash in ID range"))?; 25 + Ok(from.parse()?..=to.parse()?) 26 + }) 27 + .try_collect()?, 28 + available_ingredients: available.lines().map(|i| i.parse()).try_collect()?, 29 + }) 30 + } 31 + 32 + fn part1(&self) -> usize { 33 + fresh_ingredients(&self.available_ingredients, &self.fresh_ranges).count() 34 + } 35 + 36 + fn part2(&self) -> usize { 37 + disjoint_ranges(&self.fresh_ranges) 38 + .into_iter() 39 + .map(|r| r.count()) 40 + .sum() 41 + } 42 + } 43 + 44 + fn fresh_ingredients<'a, I>( 45 + available_ingredients: impl IntoIterator<Item = &'a u64> + 'a, 46 + fresh_ranges: I, 47 + ) -> impl Iterator<Item = u64> + 'a 48 + where 49 + I: IntoIterator<Item = &'a RangeInclusive<u64>> + 'a, 50 + I::IntoIter: Clone, 51 + { 52 + let fresh_ranges = fresh_ranges.into_iter(); 53 + available_ingredients 54 + .into_iter() 55 + .cloned() 56 + .filter(move |i| fresh_ranges.clone().any(|r| r.contains(i))) 57 + } 58 + 59 + fn disjoint_ranges<'a>( 60 + ranges: impl IntoIterator<Item = &'a RangeInclusive<u64>>, 61 + ) -> HashSet<RangeInclusive<u64>> { 62 + ranges.into_iter().fold( 63 + HashSet::<RangeInclusive<u64>>::new(), 64 + |mut disjoint_ranges, r| { 65 + let overlapping = disjoint_ranges.extract_if(|d| d.try_union(r).is_some()); 66 + let union = overlapping 67 + .map(|o| o.try_union(r).unwrap()) 68 + .reduce(|lhs, rhs| lhs.try_union(&rhs).unwrap()) 69 + .unwrap_or(r.clone()); 70 + disjoint_ranges.insert(union); 71 + disjoint_ranges 72 + }, 73 + ) 74 + } 75 + 76 + #[cfg(test)] 77 + mod tests { 78 + use super::*; 79 + 80 + const EXAMPLE_INPUT: &str = "\ 81 + 3-5 82 + 10-14 83 + 16-20 84 + 12-18 85 + 86 + 1 87 + 5 88 + 8 89 + 11 90 + 17 91 + 32"; 92 + 93 + const EXAMPLE_FRESH_RANGES: &[RangeInclusive<u64>] = &[3..=5, 10..=14, 16..=20, 12..=18]; 94 + 95 + const EXAMPLE_AVAILABLE_INGREDIENTS: &[u64] = &[1, 5, 8, 11, 17, 32]; 96 + 97 + #[test] 98 + fn parse_example_input() { 99 + let Door { 100 + fresh_ranges, 101 + available_ingredients, 102 + } = Door::parse(EXAMPLE_INPUT).unwrap(); 103 + assert_eq!(fresh_ranges, EXAMPLE_FRESH_RANGES); 104 + assert_eq!(available_ingredients, EXAMPLE_AVAILABLE_INGREDIENTS); 105 + } 106 + 107 + #[test] 108 + fn example_fresh_ingredients() { 109 + itertools::equal( 110 + fresh_ingredients(EXAMPLE_AVAILABLE_INGREDIENTS, EXAMPLE_FRESH_RANGES), 111 + [5, 11, 17], 112 + ); 113 + } 114 + 115 + #[test] 116 + fn disjoint_ranges_cover_same_ids() { 117 + assert_eq!( 118 + disjoint_ranges(EXAMPLE_FRESH_RANGES) 119 + .into_iter() 120 + .flatten() 121 + .collect::<HashSet<_>>(), 122 + EXAMPLE_FRESH_RANGES 123 + .iter() 124 + .cloned() 125 + .flatten() 126 + .collect::<HashSet<_>>() 127 + ); 128 + } 129 + 130 + #[test] 131 + fn disjoint_ranges_are_mutually_disjoint() { 132 + assert_eq!( 133 + disjoint_ranges(EXAMPLE_FRESH_RANGES) 134 + .iter() 135 + .tuple_combinations() 136 + .find(|(lhs, rhs)| lhs.intersection(rhs).is_some()), 137 + None 138 + ); 139 + } 140 + }
+2 -2
aoc_2025/src/main.rs
··· 4 4 mod day02; 5 5 mod day03; 6 6 mod day04; 7 - // mod day05; 7 + mod day05; 8 8 // mod day06; 9 9 // mod day07; 10 10 // mod day08; ··· 22 22 door!(2025-12-02 ~> day02), 23 23 door!(2025-12-03 ~> day03), 24 24 door!(2025-12-04 ~> day04), 25 - // door!(2025-12-05 ~> day05), 25 + door!(2025-12-05 ~> day05), 26 26 // door!(2025-12-06 ~> day06), 27 27 // door!(2025-12-07 ~> day07), 28 28 // door!(2025-12-08 ~> day08),