344 lines
9.5 KiB
Rust
344 lines
9.5 KiB
Rust
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<ResultType> {
|
|
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<ResultType> {
|
|
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::<Vec<_>>();
|
|
|
|
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<Sensor>, HashSet<Pos2D<i64>>), 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<Sensor>,
|
|
beacons: &HashSet<Pos2D<i64>>,
|
|
row: i64,
|
|
) -> i64 {
|
|
let ranges = sensors
|
|
.iter()
|
|
.filter_map(|sensor| sensor.range_at(row))
|
|
.collect::<Vec<_>>();
|
|
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<Sensor>) -> Vec<Line> {
|
|
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<Err<Error<&str>>> for SensorError {
|
|
fn from(error: Err<Error<&str>>) -> Self {
|
|
SensorError::ParsingError(error.to_string())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Line {
|
|
start: Pos2D<i64>,
|
|
is_up: bool,
|
|
steps: i64,
|
|
}
|
|
|
|
impl Line {
|
|
pub fn create(first: &Sensor, second: &Sensor) -> Option<Line> {
|
|
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<Pos2D<i64>> {
|
|
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<Range> {
|
|
let mut ranges: Vec<Range> = 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<i64>,
|
|
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<i64>> {
|
|
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<i64>)> {
|
|
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<Range> {
|
|
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<i64>) -> 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(())
|
|
}
|
|
}
|