Advent of Code solutions in Rust

feat: day 4 of AoC 2025

+220 -2
+1
Cargo.lock
··· 150 150 "aoc_utils", 151 151 "itertools", 152 152 "num-traits", 153 + "thiserror", 153 154 "tokio", 154 155 ] 155 156
+1
aoc_2025/Cargo.toml
··· 11 11 anyhow = { workspace = true } 12 12 itertools = { workspace = true } 13 13 num-traits = { workspace = true } 14 + thiserror = { workspace = true } 14 15 tokio = { workspace = true }
+216
aoc_2025/src/day04.rs
··· 1 + use std::collections::HashSet; 2 + 3 + use aoc_companion::prelude::*; 4 + use aoc_utils::geometry::{ParseMapError, Point}; 5 + use aoc_utils::linalg::Vector; 6 + use itertools::Itertools; 7 + 8 + pub(crate) struct Door { 9 + rolls: HashSet<Vector<usize, 2>>, 10 + } 11 + 12 + impl<'input> Solution<'input> for Door { 13 + fn parse(input: &'input str) -> Result<Self, ParseMapError<ParseTileError>> { 14 + Ok(Door { 15 + rolls: parse_roll_locations(input)?, 16 + }) 17 + } 18 + 19 + fn part1(&self) -> usize { 20 + accessible_roll_locations(&self.rolls).count() 21 + } 22 + 23 + fn part2(&self) -> usize { 24 + self.rolls.len() - non_removable_roll_locations(self.rolls.clone()).len() 25 + } 26 + } 27 + 28 + #[derive(Debug, thiserror::Error)] 29 + pub(crate) enum ParseTileError { 30 + #[error("invalid map tile: {tile:?}")] 31 + InvalidTile { tile: u8 }, 32 + } 33 + 34 + fn parse_roll_locations( 35 + input: &str, 36 + ) -> Result<HashSet<Vector<usize, 2>>, ParseMapError<ParseTileError>> { 37 + let map = aoc_utils::geometry::try_parse_map(input, |tile| match tile { 38 + b'@' => Ok(true), 39 + b'.' => Ok(false), 40 + _ => Err(ParseTileError::InvalidTile { tile }), 41 + })?; 42 + 43 + Ok(map 44 + .indexed_iter() 45 + .filter(|(_, is_roll)| **is_roll) 46 + .map(|((y, x), _)| Vector([x, y])) 47 + .collect()) 48 + } 49 + 50 + fn accessible_roll_locations( 51 + rolls: &HashSet<Vector<usize, 2>>, 52 + ) -> impl Iterator<Item = Vector<usize, 2>> { 53 + rolls 54 + .iter() 55 + .filter(|r| r.neighbors().filter(|n| rolls.contains(n)).count() < 4) 56 + .copied() 57 + } 58 + 59 + fn with_accessible_rolls_removed(rolls: &HashSet<Vector<usize, 2>>) -> HashSet<Vector<usize, 2>> { 60 + rolls 61 + .difference(&accessible_roll_locations(rolls).collect()) 62 + .copied() 63 + .collect() 64 + } 65 + 66 + fn non_removable_roll_locations(rolls: HashSet<Vector<usize, 2>>) -> HashSet<Vector<usize, 2>> { 67 + std::iter::successors(Some(rolls), |prev| { 68 + Some(with_accessible_rolls_removed(prev)) 69 + }) 70 + .tuple_windows() 71 + .find_map(|(lhs, rhs)| (lhs == rhs).then_some(lhs)) 72 + .unwrap() 73 + } 74 + 75 + #[cfg(test)] 76 + mod tests { 77 + use itertools::Itertools; 78 + 79 + use super::*; 80 + 81 + const EXAMPLE_INPUT: &str = "\ 82 + ..@@.@@@@. 83 + @@@.@.@.@@ 84 + @@@@@.@.@@ 85 + @.@@@@..@. 86 + @@.@@@@.@@ 87 + .@@@@@@@.@ 88 + .@.@.@.@@@ 89 + @.@@@.@@@@ 90 + .@@@@@@@@. 91 + @.@.@@@.@."; 92 + 93 + const EXAMPLE_ACCESSIBLE_ROLLS: &str = "\ 94 + ..@@.@@.@. 95 + @......... 96 + ......@... 97 + .......... 98 + @........@ 99 + .......... 100 + .......... 101 + @......... 102 + .......... 103 + @.@.....@."; 104 + 105 + const EXAMPLE_NON_REMOVABLE_ROLLS: &str = "\ 106 + .......... 107 + .......... 108 + .......... 109 + ....@@.... 110 + ...@@@@... 111 + ...@@@@@.. 112 + ...@.@.@@. 113 + ...@@.@@@. 114 + ...@@@@@.. 115 + ....@@@..."; 116 + 117 + const EXAMPLE_ROLL_LOCATIONS: &[Vector<usize, 2>] = &[ 118 + Vector([0, 1]), 119 + Vector([0, 2]), 120 + Vector([0, 3]), 121 + Vector([0, 4]), 122 + Vector([0, 7]), 123 + Vector([0, 9]), 124 + Vector([1, 1]), 125 + Vector([1, 2]), 126 + Vector([1, 4]), 127 + Vector([1, 5]), 128 + Vector([1, 6]), 129 + Vector([1, 8]), 130 + Vector([2, 0]), 131 + Vector([2, 1]), 132 + Vector([2, 2]), 133 + Vector([2, 3]), 134 + Vector([2, 5]), 135 + Vector([2, 7]), 136 + Vector([2, 8]), 137 + Vector([2, 9]), 138 + Vector([3, 0]), 139 + Vector([3, 2]), 140 + Vector([3, 3]), 141 + Vector([3, 4]), 142 + Vector([3, 5]), 143 + Vector([3, 6]), 144 + Vector([3, 7]), 145 + Vector([3, 8]), 146 + Vector([4, 1]), 147 + Vector([4, 2]), 148 + Vector([4, 3]), 149 + Vector([4, 4]), 150 + Vector([4, 5]), 151 + Vector([4, 7]), 152 + Vector([4, 8]), 153 + Vector([4, 9]), 154 + Vector([5, 0]), 155 + Vector([5, 3]), 156 + Vector([5, 4]), 157 + Vector([5, 5]), 158 + Vector([5, 6]), 159 + Vector([5, 8]), 160 + Vector([5, 9]), 161 + Vector([6, 0]), 162 + Vector([6, 1]), 163 + Vector([6, 2]), 164 + Vector([6, 4]), 165 + Vector([6, 5]), 166 + Vector([6, 7]), 167 + Vector([6, 8]), 168 + Vector([6, 9]), 169 + Vector([7, 0]), 170 + Vector([7, 5]), 171 + Vector([7, 6]), 172 + Vector([7, 7]), 173 + Vector([7, 8]), 174 + Vector([8, 0]), 175 + Vector([8, 1]), 176 + Vector([8, 2]), 177 + Vector([8, 3]), 178 + Vector([8, 4]), 179 + Vector([8, 6]), 180 + Vector([8, 7]), 181 + Vector([8, 8]), 182 + Vector([8, 9]), 183 + Vector([9, 1]), 184 + Vector([9, 2]), 185 + Vector([9, 4]), 186 + Vector([9, 5]), 187 + Vector([9, 6]), 188 + Vector([9, 7]), 189 + ]; 190 + 191 + #[test] 192 + fn parse_example_roll_locations() { 193 + itertools::assert_equal( 194 + parse_roll_locations(EXAMPLE_INPUT).unwrap().iter().sorted(), 195 + EXAMPLE_ROLL_LOCATIONS, 196 + ); 197 + } 198 + 199 + #[test] 200 + fn accessible_rolls_in_example() { 201 + assert_eq!( 202 + HashSet::from_iter(accessible_roll_locations( 203 + &parse_roll_locations(EXAMPLE_INPUT).unwrap() 204 + )), 205 + parse_roll_locations(EXAMPLE_ACCESSIBLE_ROLLS).unwrap() 206 + ); 207 + } 208 + 209 + #[test] 210 + fn non_removable_rolls_in_example() { 211 + assert_eq!( 212 + non_removable_roll_locations(parse_roll_locations(EXAMPLE_INPUT).unwrap()), 213 + parse_roll_locations(EXAMPLE_NON_REMOVABLE_ROLLS).unwrap() 214 + ) 215 + } 216 + }
+2 -2
aoc_2025/src/main.rs
··· 3 3 mod day01; 4 4 mod day02; 5 5 mod day03; 6 - // mod day04; 6 + mod day04; 7 7 // mod day05; 8 8 // mod day06; 9 9 // mod day07; ··· 21 21 door!(2025-12-01 ~> day01), 22 22 door!(2025-12-02 ~> day02), 23 23 door!(2025-12-03 ~> day03), 24 - // door!(2025-12-04 ~> day04), 24 + door!(2025-12-04 ~> day04), 25 25 // door!(2025-12-05 ~> day05), 26 26 // door!(2025-12-06 ~> day06), 27 27 // door!(2025-12-07 ~> day07),