use super::template::{DayTrait, ResultType}; use crate::common::{ parser::{eol_terminated, extract_result, ignore, trim0}, pos::Pos2D, }; use itertools::Itertools; use nom::{ bytes::complete::tag, character::complete::{char, i64}, error::Error, multi::many0, sequence::{separated_pair, tuple}, Err, IResult, }; use std::collections::HashSet; use thiserror::Error; const DAY_NUMBER: usize = 15; pub struct Day; fn parse_row(input: &str) -> IResult<&str, i64> { eol_terminated(i64)(input) } impl DayTrait for Day { fn get_day_number(&self) -> usize { DAY_NUMBER } fn part1(&self, lines: &str) -> anyhow::Result { let (lines, row) = parse_row(lines).map_err(|_| SensorError::RowLineNotFound)?; let (sensors, beacons) = Day::parse_all(lines)?; let result = Day::count_coverage_at(&sensors, &beacons, row); Ok(ResultType::Integer(result)) } fn part2(&self, lines: &str) -> anyhow::Result { let (lines, _) = parse_row(lines).map_err(|_| SensorError::RowLineNotFound)?; let (sensors, _) = Day::parse_all(lines)?; let mid_lines = Day::dividing_lines(&sensors); let points = mid_lines .iter() .tuple_combinations() .filter_map(|(first, second)| first.cross(second)) .filter(|pos| sensors.iter().all(|sensor| !sensor.contains(*pos))) .dedup() .collect::>(); if points.len() != 1 { Err(SensorError::NotExactlyOneResult(points.len()))?; } let point = points.first().unwrap(); Ok(ResultType::Integer(point.x() * 4000000 + point.y())) } } impl Day { fn parse_all(lines: &str) -> Result<(HashSet, HashSet>), SensorError> { let data = extract_result(many0(eol_terminated(Sensor::parse)))(lines)?; let mut sensors = HashSet::new(); let mut beacons = HashSet::new(); for (sensor, beacon) in data { sensors.insert(sensor); beacons.insert(beacon); } Ok((sensors, beacons)) } fn count_coverage_at( sensors: &HashSet, beacons: &HashSet>, row: i64, ) -> i64 { let ranges = sensors .iter() .filter_map(|sensor| sensor.range_at(row)) .collect::>(); let merged: i64 = Range::merge_all(&ranges) .iter() .map(|range| range.width()) .sum(); let sensors = sensors .iter() .filter(|sensor| sensor.pos.y() == row) .count() as i64; let beacons = beacons.iter().filter(|beacons| beacons.y() == row).count() as i64; merged - (sensors + beacons) } fn dividing_lines(sensors: &HashSet) -> Vec { sensors .iter() .tuple_combinations() .filter_map(|(first, second)| Line::create(first, second)) .collect() } } #[derive(Debug, Error)] enum SensorError { #[error("Error Parsing Input: {0}")] ParsingError(String), #[error("Did not find exactly one point for the sensor: {0}")] NotExactlyOneResult(usize), #[error("Row line not found at start of Input")] RowLineNotFound, } impl From>> for SensorError { fn from(error: Err>) -> Self { SensorError::ParsingError(error.to_string()) } } #[derive(Debug)] struct Line { start: Pos2D, is_up: bool, steps: i64, } impl Line { pub fn create(first: &Sensor, second: &Sensor) -> Option { if first.border_distance(second) != 2 { return None; } let (one, two) = if first.pos.x() < second.pos.x() { (first, second) } else { (second, first) }; let start; let is_up; if one.pos.y() < two.pos.y() { is_up = true; if one.pos.y() + one.radius <= two.pos.y() { start = Pos2D::new(one.pos.x(), one.pos.y() + one.radius + 1); } else { start = Pos2D::new(two.pos.x() - two.radius - 1, two.pos.y()); } } else { is_up = false; if one.pos.y() - one.radius >= two.pos.y() { start = Pos2D::new(one.pos.x(), one.pos.y() - one.radius - 1); } else { start = Pos2D::new(two.pos.x() - two.radius - 1, two.pos.y()); } } let steps = two.pos.x().min(one.pos.x() + one.radius) - start.x(); Some(Line { start, is_up, steps, }) } fn cross(&self, other: &Line) -> Option> { if self.is_up == other.is_up { return None; } let (bottom_up, top_down) = if self.is_up { (self, other) } else { (other, self) }; let r2 = bottom_up.start.x() + bottom_up.start.y() - (top_down.start.x() + top_down.start.y()); if r2 % 2 != 0 { return None; } let r = r2 / 2; if r < 0 || r > top_down.steps { return None; } let pos = top_down.start + Pos2D::splat(r); Some(pos) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct Range(i64, i64); impl Range { fn overlaps(&self, other: &Range) -> bool { self.0 <= other.1 && other.0 <= self.1 } pub fn width(&self) -> i64 { self.1 - self.0 + 1 } pub fn merge_all(ranges: &[Range]) -> Vec { let mut ranges: Vec = ranges.iter().sorted().copied().collect(); assert!(!ranges.is_empty()); loop { let mut next = vec![]; let mut merged = false; let mut current = ranges[0]; for range in &ranges[1..] { if current.overlaps(range) { current = Range(current.0, current.1.max(range.1)); merged = true; } else { next.push(current); current = *range; } } next.push(current); if merged { ranges = next; } else { return next; } } } } #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] struct Sensor { pos: Pos2D, radius: i64, } impl Sensor { fn component<'a>(name: &'a str) -> impl FnMut(&'a str) -> IResult<&'a str, i64> { move |input: &str| { let input = ignore(tuple((tag(name), char('='))))(input)?; trim0(i64)(input) } } fn parse_pos(input: &str) -> IResult<&str, Pos2D> { let (input, (x, y)) = separated_pair( Sensor::component("x"), trim0(char(',')), Sensor::component("y"), )(input)?; Ok((input, Pos2D::new(x, y))) } pub fn parse(input: &str) -> IResult<&str, (Sensor, Pos2D)> { let input = ignore(tag("Sensor at"))(input)?; let (input, pos) = trim0(Sensor::parse_pos)(input)?; let input = ignore(tag(": closest beacon is at"))(input)?; let (input, beacon) = trim0(Sensor::parse_pos)(input)?; let radius = pos.taxicab_between(beacon); Ok((input, (Sensor { pos, radius }, beacon))) } pub fn range_at(&self, y: i64) -> Option { let distance = (self.pos.y() - y).abs(); if distance > self.radius { None } else { let extent = self.radius - distance; Some(Range(self.pos.x() - extent, self.pos.x() + extent)) } } pub fn border_distance(&self, other: &Sensor) -> i64 { let distance = self.pos.taxicab_between(other.pos); distance - self.radius - other.radius } pub fn contains(&self, pos: Pos2D) -> bool { self.pos.taxicab_between(pos) <= self.radius } } #[cfg(test)] mod test { use super::*; use crate::common::file::read_string; use anyhow::Result; #[test] fn test_parse() -> Result<()> { let input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15"; let expected = ( Sensor { pos: Pos2D::new(2, 18), radius: 7, }, Pos2D::new(-2, 15), ); let result = extract_result(Sensor::parse)(input)?; assert_eq!(result, expected); Ok(()) } #[test] fn test_width() { let sensor = Sensor { pos: Pos2D::new(8, 7), radius: 9, }; assert_eq!(sensor.range_at(17), None); assert_eq!(sensor.range_at(7), Some(Range(-1, 17))); assert_eq!(sensor.range_at(10), Some(Range(2, 14))); } #[test] fn test_merge() { let input = vec![Range(4, 10), Range(1, 3), Range(5, 9), Range(8, 12)]; let expected = vec![Range(1, 3), Range(4, 12)]; assert_eq!(Range::merge_all(&input), expected); } #[test] fn test_part1() -> Result<()> { let day = Day {}; let lines = read_string(day.get_day_number(), "example01.txt")?; let expected = ResultType::Integer(26); 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(56000011); let result = day.part2(&lines)?; assert_eq!(result, expected); Ok(()) } }