Advent of Code solutions in Rust

refactor: generalize day 12 solution a bit

Knowing that all problems can be conclusively judged irrespective of the
actual present shapes, it is no longer necessary to restrict the
solution to known shapes by asserting on a specific hash. All that's
needed is to extract the areas occupied by each present.

For different inputs, they might still be problems which cannot be
conclusively judged based on the implemented rules (though I suspect
that all inputs share this quality) but then my solution would still
produce a proper error, rather than a wrong answer.

+103 -16
-1
Cargo.lock
··· 148 148 "anyhow", 149 149 "aoc_companion", 150 150 "aoc_utils", 151 - "fxhash", 152 151 "itertools", 153 152 "ndarray", 154 153 "num-traits",
-1
aoc_2025/Cargo.toml
··· 15 15 rayon = { workspace = true } 16 16 thiserror = { workspace = true } 17 17 tokio = { workspace = true } 18 - fxhash = "0.2.1"
+103 -14
aoc_2025/src/day12.rs
··· 5 5 use itertools::Itertools as _; 6 6 7 7 pub(crate) struct Door { 8 + areas: [usize; 6], 8 9 problems: Vec<Problem>, 9 10 } 10 11 ··· 13 14 let Some((shapes, problems)) = input.rsplit_once("\n\n") else { 14 15 bail!("could not find empty line delimiting shapes and problems"); 15 16 }; 16 - let shapes_hash = fxhash::hash(shapes); 17 - if shapes_hash != 0x2050bd894f01430f { 18 - bail!("solution only works for my input; got different hash: {shapes_hash:x}"); 19 - } 20 - problems 21 - .lines() 22 - .map(str::parse) 23 - .try_collect() 24 - .map(|problems| Door { problems }) 17 + let areas = aoc_utils::array::try_from_iter_exact(shapes.split("\n\n").map(|shape| { 18 + let Some((_, shape)) = shape.split_once(":\n") else { 19 + bail!("missing shape introducer line ending in colon"); 20 + }; 21 + let shape = aoc_utils::geometry::parse_ascii_map(shape) 22 + .with_context(|| anyhow!("failed to parse shape"))?; 23 + if shape.dim() != (3, 3) { 24 + bail!( 25 + "expected shapes of up to 3x3, got {}x{}", 26 + shape.dim().0, 27 + shape.dim().1 28 + ); 29 + } 30 + Ok(shape.iter().filter(|b| **b == b'#').count()) 31 + }))? 32 + .map_err(|v| anyhow!("expected exactly 6 present shapes, got {}", v.len()))?; 33 + let problems = problems.lines().map(str::parse).try_collect()?; 34 + Ok(Door { areas, problems }) 25 35 } 26 36 27 37 fn part1(&self) -> Result<usize> { ··· 29 39 .problems 30 40 .iter() 31 41 .map(|problem| { 32 - rule_out_due_to_insufficient_area(problem) 42 + rule_out_due_to_insufficient_area(problem, &self.areas) 33 43 .or_else(|| verify_with_trivial_packing(problem)) 34 44 .ok_or(problem) 35 45 }) ··· 98 108 } 99 109 } 100 110 101 - fn rule_out_due_to_insufficient_area(problem: &Problem) -> Option<bool> { 102 - const AREA: [usize; 6] = [5, 7, 7, 7, 6, 7]; 103 - 111 + fn rule_out_due_to_insufficient_area(problem: &Problem, areas: &[usize; 6]) -> Option<bool> { 104 112 let required_area: usize = problem 105 113 .presents 106 114 .iter() 107 - .zip(AREA) 115 + .zip(areas) 108 116 .map(|(count, area)| count * area) 109 117 .sum(); 110 118 ··· 118 126 119 127 (total_presents <= available_cells).then_some(true) 120 128 } 129 + 130 + #[cfg(test)] 131 + mod tests { 132 + use super::*; 133 + 134 + const EXAMPLE_INPUT: &str = "\ 135 + 0: 136 + ### 137 + ##. 138 + ##. 139 + 140 + 1: 141 + ### 142 + ##. 143 + .## 144 + 145 + 2: 146 + .## 147 + ### 148 + ##. 149 + 150 + 3: 151 + ##. 152 + ### 153 + ##. 154 + 155 + 4: 156 + ### 157 + #.. 158 + ### 159 + 160 + 5: 161 + ### 162 + .#. 163 + ### 164 + 165 + 4x4: 0 0 0 0 2 0 166 + 12x5: 1 0 1 0 2 2 167 + 12x5: 1 0 1 0 3 2"; 168 + 169 + const EXAMPLE_AREAS: [usize; 6] = [7, 7, 7, 7, 7, 7]; 170 + const EXAMPLE_PROBLEMS: &[Problem] = &[ 171 + Problem { 172 + dimensions: [4, 4], 173 + presents: [0, 0, 0, 0, 2, 0], 174 + }, 175 + Problem { 176 + dimensions: [12, 5], 177 + presents: [1, 0, 1, 0, 2, 2], 178 + }, 179 + Problem { 180 + dimensions: [12, 5], 181 + presents: [1, 0, 1, 0, 3, 2], 182 + }, 183 + ]; 184 + 185 + #[test] 186 + fn parse_example_input() { 187 + let Door { areas, problems } = Door::parse(EXAMPLE_INPUT).unwrap(); 188 + assert_eq!(areas, EXAMPLE_AREAS); 189 + itertools::assert_equal(&problems, EXAMPLE_PROBLEMS); 190 + } 191 + 192 + #[test] 193 + fn none_of_the_example_problems_can_be_ruled_out_due_to_insufficient_area() { 194 + itertools::assert_equal( 195 + EXAMPLE_PROBLEMS 196 + .iter() 197 + .map(|problem| rule_out_due_to_insufficient_area(problem, &EXAMPLE_AREAS)), 198 + [None, None, None], 199 + ); 200 + } 201 + 202 + #[test] 203 + fn none_of_the_example_problems_can_be_verified_with_trivial_packing() { 204 + itertools::assert_equal( 205 + EXAMPLE_PROBLEMS.iter().map(verify_with_trivial_packing), 206 + [None, None, None], 207 + ); 208 + } 209 + }