1use std::cmp;
2use std::fmt::Debug;
3use std::fs;
4
5use hashbrown::HashMap;
6
7type AnyError = Box<dyn std::error::Error>;
8
9#[derive(Debug, Eq, Hash, PartialEq)]
10struct Cuboid {
11 xl: i64,
12 xr: i64,
13 yl: i64,
14 yr: i64,
15 zl: i64,
16 zr: i64,
17}
18
19impl Cuboid {
20 fn new(xl: i64, xr: i64, yl: i64, yr: i64, zl: i64, zr: i64) -> Option<Cuboid> {
21 if xr <= xl || yr <= yl || zr <= zl {
22 return None;
23 }
24 Some(Cuboid {
25 xl,
26 xr,
27 yl,
28 yr,
29 zl,
30 zr,
31 })
32 }
33}
34
35impl Cuboid {
36 fn contains_cuboid(&self, other: &Cuboid) -> bool {
37 self.xl <= other.xl
38 && self.yl <= other.yl
39 && self.zl <= other.zl
40 && other.xr <= self.xr
41 && other.yr <= self.yr
42 && other.zr <= self.zr
43 }
44
45 fn intersection(&self, other: &Cuboid) -> Option<Cuboid> {
46 Cuboid::new(
47 cmp::max(self.xl, other.xl),
48 cmp::min(self.xr, other.xr),
49 cmp::max(self.yl, other.yl),
50 cmp::min(self.yr, other.yr),
51 cmp::max(self.zl, other.zl),
52 cmp::min(self.zr, other.zr),
53 )
54 }
55
56 fn volume(&self) -> i64 {
57 (self.xr - self.xl) * (self.yr - self.yl) * (self.zr - self.zl)
58 }
59}
60
61fn _steps(text: &str) -> Result<Vec<(bool, Cuboid)>, AnyError> {
62 let re = regex::Regex::new(
63 r"(on|off) x=(-?\d+)[.][.](-?\d+),y=(-?\d+)[.][.](-?\d+),z=(-?\d+)[.][.](-?\d+)",
64 )
65 .unwrap();
66 let mut result = Vec::new();
67 for cap in re.captures_iter(text) {
68 let state = match &cap[1] {
69 "on" => true,
70 "off" => false,
71 state => panic!("Unexpected state '{}'", state),
72 };
73 let cuboid = Cuboid::new(
74 cap[2].parse::<i64>()?,
75 cap[3].parse::<i64>()? + 1,
76 cap[4].parse::<i64>()?,
77 cap[5].parse::<i64>()? + 1,
78 cap[6].parse::<i64>()?,
79 cap[7].parse::<i64>()? + 1,
80 )
81 .ok_or_else(|| format!("Not a valid cuboid: {:?}", cap))?;
82 result.push((state, cuboid));
83 }
84 Ok(result)
85}
86
87#[allow(clippy::needless_collect)]
88fn _num_on(steps: Vec<(bool, Cuboid)>) -> i64 {
89 let mut counts: HashMap<Cuboid, i64> = HashMap::new();
90 for (state, new_cuboid) in steps {
91 let changes = counts
92 .iter()
93 .filter_map(|(old_cuboid, old_count)| {
94 old_cuboid
95 .intersection(&new_cuboid)
96 .map(|intersection| (intersection, *old_count))
97 })
98 .collect::<Vec<(Cuboid, i64)>>();
99
100 for (intersection, old_count) in changes.into_iter() {
101 *counts.entry(intersection).or_insert(0) -= old_count;
102 }
103
104 if state {
105 *counts.entry(new_cuboid).or_insert(0) += 1;
106 }
107
108 counts.retain(|_, v| *v != 0);
110 }
111 counts
112 .into_iter()
113 .map(|(cuboid, count)| cuboid.volume() * count)
114 .sum()
115}
116
117pub fn part_1(input: &str) -> Result<String, Box<dyn std::error::Error>> {
118 let bound = Cuboid::new(-50, 51, -50, 51, -50, 51).unwrap();
119 let steps = _steps(input)?
120 .into_iter()
121 .filter(|(_, cuboid)| bound.contains_cuboid(cuboid))
122 .collect();
123 Ok(format!("{}", _num_on(steps)))
124}
125
126pub fn part_2(input: &str) -> Result<String, Box<dyn std::error::Error>> {
127 let steps = _steps(input)?;
128 Ok(format!("{}", _num_on(steps)))
129}
130
131fn _from_file<F, T>(func: F, stem: &str) -> T
132where
133 F: Fn(&str) -> Result<T, Box<dyn std::error::Error>>,
134{
135 func(&fs::read_to_string(format!("inputs/22/{}.txt", stem)).unwrap()).unwrap()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn part_1_works_on_example_s() {
144 assert_eq!(_from_file(part_1, "example_s"), "39");
145 }
146
147 #[test]
148 fn part_1_works_on_example_m() {
149 assert_eq!(_from_file(part_1, "example_m"), "590784");
150 }
151
152 #[test]
153 fn part_1_works_on_example_l() {
154 assert_eq!(_from_file(part_1, "example_l"), "474140");
155 }
156
157 #[test]
158 fn part_1_works_on_input() {
159 assert_eq!(_from_file(part_1, "input"), "527915");
160 }
161
162 #[test]
163 fn part_2_works_on_example_l() {
164 assert_eq!(_from_file(part_2, "example_l"), "2758514936282235");
165 }
166
167 #[test]
168 fn part_2_works_on_input() {
169 assert_eq!(_from_file(part_2, "input"), "1218645427221987");
170 }
171}