diff --git a/day5/Cargo.lock b/day5/Cargo.lock new file mode 100644 index 0000000..36c965e --- /dev/null +++ b/day5/Cargo.lock @@ -0,0 +1,95 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day5" +version = "0.1.0" +dependencies = [ + "nom", + "thiserror", +] + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109" +dependencies = [ + "memchr", + "minimal-lexical", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "version_check" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" diff --git a/day5/Cargo.toml b/day5/Cargo.toml new file mode 100644 index 0000000..6292fc9 --- /dev/null +++ b/day5/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "day5" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nom = "7.1" +thiserror = "1.0" diff --git a/day5/src/main.rs b/day5/src/main.rs new file mode 100644 index 0000000..06d8a52 --- /dev/null +++ b/day5/src/main.rs @@ -0,0 +1,168 @@ +#![warn(clippy::all, clippy::pedantic)] +use std::cmp; +use std::collections::HashMap; +use std::env; +use std::fs::File; +use std::io::{BufRead, BufReader}; + +use nom::bytes::complete::tag; +use nom::combinator::eof; +use nom::sequence::terminated; +use nom::{ + bytes::complete::take_while1, character::complete::char, combinator::map_res, + sequence::separated_pair, IResult, +}; +use thiserror::Error; + +#[derive(Debug, Error)] +enum Error { + #[error("The coordinates {0:?} and {0:?} are not in line with the given strategy {:0?}")] + InvalidDirection(Coordinate, Coordinate, Strategy), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct Coordinate(u32, u32); + +#[derive(Debug, Clone, Copy)] +enum Strategy { + OrthogonalOnly, + OrthogonalAnd45Degrees, +} + +impl Strategy { + fn points_follow_strategy(self, a: Coordinate, b: Coordinate) -> bool { + let x_range = order_pair(a.0, b.0); + let y_range = order_pair(a.1, b.1); + + let rise = y_range.1 - y_range.0; + let run = x_range.1 - x_range.0; + + match self { + Self::OrthogonalOnly => run == 0 || rise == 0, + Self::OrthogonalAnd45Degrees => run == 0 || rise == 0 || run == rise, + } + } +} + +impl Coordinate { + /// Create an iterator that will move between this point and a given ending point. + /// + /// # Errors + /// If the direction between this point and the other are not in a direction + /// that matches the given strategy, [`Error::InvalidDirection`] is returned + fn iter_between( + self, + other: Coordinate, + strategy: Strategy, + ) -> Result, Error> { + if !strategy.points_follow_strategy(self, other) { + return Err(Error::InvalidDirection(self, other, strategy)); + } + + let x_range = order_pair(self.0, other.0); + let y_range = order_pair(self.1, other.1); + // Calculate the distance between points using the "max norm" + // this is like the manhattan distance, but diagonals are 1 + // (I'm effectively using it as an integral euclidian distance given the context) + let travel_distance = cmp::max(x_range.1 - x_range.0, y_range.1 - y_range.0); + + let iter = (0..=travel_distance).map(move |n| { + let end_x = add_to_component_directionally(self.0, other.0, n); + let end_y = add_to_component_directionally(self.1, other.1, n); + + Coordinate(end_x, end_y) + }); + Ok(iter) + } +} + +fn order_pair(a: T, b: T) -> (T, T) { + if a > b { + (b, a) + } else { + (a, b) + } +} + +/// Add some number, n, to the start component of a vector, in the direction of its ending point. +fn add_to_component_directionally(start: u32, end: u32, n: u32) -> u32 { + match start.cmp(&end) { + cmp::Ordering::Equal => start, + cmp::Ordering::Greater => start - n, + cmp::Ordering::Less => start + n, + } +} + +/// Build a map of the number of intersections between lines bounded (inclusively) by each element +/// the `coordinate_pairs` slice. The retruend map will indicate the number of (non-zero) interactions +/// at each point +fn build_intersection_count_map( + coordinate_pairs: &[(Coordinate, Coordinate)], + strategy: Strategy, +) -> Result, Error> { + let mut counts = HashMap::new(); + for &pair in coordinate_pairs { + let (start, end) = pair; + + let iter_res = start.iter_between(end, strategy); + if let Err(Error::InvalidDirection(_, _, _)) = iter_res { + continue; + } + + for coord in iter_res? { + let count = counts.get(&coord).unwrap_or(&0); + let updated_count = count + 1; + counts.insert(coord, updated_count); + } + } + + Ok(counts) +} + +fn part1(coordinate_pairs: &[(Coordinate, Coordinate)]) -> usize { + let map = build_intersection_count_map(coordinate_pairs, Strategy::OrthogonalOnly) + .expect("Failed to build coordinate map from input"); + + map.values().filter(|&&n| n >= 2).count() +} + +fn part2(coordinate_pairs: &[(Coordinate, Coordinate)]) -> usize { + let map = build_intersection_count_map(coordinate_pairs, Strategy::OrthogonalAnd45Degrees) + .expect("Failed to build coordinate map from input"); + + map.values().filter(|&&n| n >= 2).count() +} + +fn parse_number(s: &str) -> IResult<&str, u32> { + map_res(take_while1(|c: char| c.is_ascii_digit()), str::parse)(s) +} + +fn parse_coordinate(s: &str) -> IResult<&str, Coordinate> { + let (remaining, parsed_numbers) = separated_pair(parse_number, char(','), parse_number)(s)?; + + Ok((remaining, Coordinate(parsed_numbers.0, parsed_numbers.1))) +} + +fn parse_line(line: &str) -> IResult<&str, (Coordinate, Coordinate)> { + terminated( + separated_pair(parse_coordinate, tag(" -> "), parse_coordinate), + eof, + )(line) +} + +fn main() { + let input_file_name = env::args().nth(1).expect("No input filename specified"); + let input_file = File::open(input_file_name).expect("Could not open input file"); + + let input_coordinates = BufReader::new(input_file) + .lines() + .map(|res| res.expect("Failed to read line")) + .map(|s| { + let (_, coords) = parse_line(&s).expect("Failed to read line"); + coords + }) + .collect::>(); + + println!("Part 1: {}", part1(&input_coordinates)); + println!("Part 2: {}", part2(&input_coordinates)); +}