use itertools::Itertools; use std::{collections::HashSet, num::ParseIntError}; use thiserror::Error; use crate::common::file::split_lines; use super::template::{DayTrait, ResultType}; const DAY_NUMBER: usize = 18; pub struct Day; impl DayTrait for Day { fn get_day_number(&self) -> usize { DAY_NUMBER } fn part1(&self, lines: &str) -> anyhow::Result { let blob = Blob::try_from(lines)?; Ok(ResultType::Integer(blob.count_sides())) } fn part2(&self, lines: &str) -> anyhow::Result { let blob = Blob::try_from(lines)?; Ok(ResultType::Integer(blob.count_outside())) } } #[derive(Debug, Error)] enum DropletError { #[error("Illegal Droplet: {0}")] IllegalDroplet(String), #[error("no Integer")] NoInteger(#[from] ParseIntError), } #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] struct Droplet(i64, i64, i64); impl Droplet { fn parse(line: &str) -> Result { let split = line.split(",").collect_vec(); if split.len() != 3 { return Err(DropletError::IllegalDroplet(line.to_owned())); } let (x, y, z) = (split[0].parse()?, split[1].parse()?, split[2].parse()?); Ok(Droplet(x, y, z)) } pub fn neighbors(&self) -> Neighbors<'_> { Neighbors::new(self) } } struct Neighbors<'a>(&'a Droplet, usize); impl<'a> Neighbors<'a> { pub fn new(droplet: &'a Droplet) -> Neighbors<'a> { Neighbors(droplet, 0) } } impl<'a> Iterator for Neighbors<'a> { type Item = Droplet; fn next(&mut self) -> Option { self.1 += 1; match self.1 { 1 => Some(Droplet(self.0 .0 - 1, self.0 .1, self.0 .2)), 2 => Some(Droplet(self.0 .0 + 1, self.0 .1, self.0 .2)), 3 => Some(Droplet(self.0 .0, self.0 .1 - 1, self.0 .2)), 4 => Some(Droplet(self.0 .0, self.0 .1 + 1, self.0 .2)), 5 => Some(Droplet(self.0 .0, self.0 .1, self.0 .2 - 1)), 6 => Some(Droplet(self.0 .0, self.0 .1, self.0 .2 + 1)), _ => None, } } } struct Ranges { x: (i64, i64), y: (i64, i64), z: (i64, i64), } impl Ranges { pub fn new(droplet: &Droplet) -> Self { Ranges { x: (droplet.0, droplet.0), y: (droplet.1, droplet.1), z: (droplet.2, droplet.2), } } pub fn contains(&self, droplet: &Droplet) -> bool { self.x.0 <= droplet.0 && droplet.0 <= self.x.1 && self.y.0 <= droplet.1 && droplet.1 <= self.y.1 && self.z.0 <= droplet.2 && droplet.2 <= self.z.1 } pub fn grow(&mut self) { self.x.0 -= 1; self.x.1 += 1; self.y.0 -= 1; self.y.1 += 1; self.z.0 -= 1; self.z.1 += 1; } pub fn first(&self) -> Droplet { Droplet(self.x.0, self.y.0, self.z.0) } pub fn extend(&mut self, droplet: &Droplet) { self.x.0 = self.x.0.min(droplet.0); self.x.1 = self.x.1.max(droplet.0); self.y.0 = self.y.0.min(droplet.1); self.y.1 = self.y.1.max(droplet.1); self.z.0 = self.z.0.min(droplet.2); self.z.1 = self.z.1.max(droplet.2); } #[allow(dead_code)] pub fn volume(&self) -> i64 { (self.x.1 - self.x.0 + 1) * (self.y.1 - self.y.0 + 1) * (self.z.1 - self.z.0 + 1) } } struct Blob { droplets: HashSet, } impl TryFrom<&str> for Blob { type Error = DropletError; fn try_from(lines: &str) -> Result { let droplets = split_lines(lines) .map(|line| Droplet::parse(line)) .try_collect()?; Ok(Blob { droplets }) } } impl Blob { fn extent(&self) -> Option { if self.droplets.is_empty() { return None; } let mut range = Ranges::new(&self.droplets.iter().next().unwrap()); for droplet in self.droplets.iter().skip(1) { range.extend(droplet); } Some(range) } pub fn count_sides(&self) -> i64 { let mut sides = 0; let mut known = HashSet::new(); for droplet in &self.droplets { sides += 6; for neighbor in droplet.neighbors() { if known.contains(&neighbor) { sides -= 2; } } known.insert(droplet); } sides } pub fn count_outside(&self) -> i64 { let Some(mut extent)= self.extent() else { return 0; }; let mut sides = 0; extent.grow(); let mut outer = HashSet::new(); let mut queue = Vec::new(); queue.push(extent.first()); while let Some(next) = queue.pop() { for neighbor in next.neighbors() { if extent.contains(&neighbor) && !outer.contains(&neighbor) { if self.droplets.contains(&neighbor) { sides += 1; } else if !queue.contains(&neighbor) { queue.push(neighbor); } } } outer.insert(next); } sides } } #[cfg(test)] mod test { use super::*; use crate::common::file::read_string; use anyhow::Result; #[test] fn test_part1() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; let expected = ResultType::Integer(64); let result = day.part1(&lines)?; assert_eq!(result, expected); Ok(()) } #[test] fn test_part2() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; let expected = ResultType::Integer(58); let result = day.part2(&lines)?; assert_eq!(result, expected); Ok(()) } #[test] fn parse() -> Result<()> { let input = "1,2,3"; let expected = Droplet(1, 2, 3); let result = Droplet::parse(input)?; assert_eq!(result, expected); Ok(()) } #[test] fn neighbors() { let droplet = Droplet(1, 1, 1); let expected = vec![ Droplet(0, 1, 1), Droplet(2, 1, 1), Droplet(1, 0, 1), Droplet(1, 2, 1), Droplet(1, 1, 0), Droplet(1, 1, 2), ]; assert_eq!(droplet.neighbors().collect_vec(), expected); } #[test] fn simple() { let blob = Blob { droplets: HashSet::from_iter(vec![Droplet(1, 1, 1), Droplet(1, 1, 2)]), }; assert_eq!(blob.count_sides(), 10); } }