use itertools::Itertools; use std::collections::LinkedList; use std::fs::File; use std::io::{BufRead, BufReader, Lines}; use std::time::Instant; // BOILERPLATE type InputIter = Lines>; fn get_input() -> InputIter { let f = File::open("input").unwrap(); let br = BufReader::new(f); br.lines() } fn main() { let start = Instant::now(); let ans1 = problem1(get_input()); let duration = start.elapsed(); println!("Problem 1 solution: {} [{}s]", ans1, duration.as_secs_f64()); let start = Instant::now(); let ans2 = problem2(get_input()); let duration = start.elapsed(); println!("Problem 2 solution: {} [{}s]", ans2, duration.as_secs_f64()); } // DATA #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] enum Direction { Left, Right, Up, Down, } #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] enum Turn { LeftNinety, RightNinety, OneEighty, None, } impl Direction { const fn all() -> &'static [Self; 4] { &[Direction::Left, Direction::Right, Direction::Up, Direction::Down] } const fn opposite(&self) -> Self { match self { Direction::Left => Direction::Right, Direction::Right => Direction::Left, Direction::Up => Direction::Down, Direction::Down => Direction::Up, } } const fn offset(&self) -> (isize, isize) { match self { Direction::Left => (-1, 0), Direction::Right => (1, 0), Direction::Up => (0, -1), Direction::Down => (0, 1), } } fn turn_kind(&self, next_dir: Direction) -> Turn { if *self == next_dir { Turn::None } else if self.opposite() == next_dir { Turn::OneEighty } else { match self { Direction::Left if next_dir == Direction::Up => Turn::RightNinety, Direction::Left if next_dir == Direction::Down => Turn::LeftNinety, Direction::Right if next_dir == Direction::Up => Turn::LeftNinety, Direction::Right if next_dir == Direction::Down => Turn::RightNinety, Direction::Up if next_dir == Direction::Left => Turn::LeftNinety, Direction::Up if next_dir == Direction::Right => Turn::RightNinety, Direction::Down if next_dir == Direction::Right => Turn::LeftNinety, Direction::Down if next_dir == Direction::Left => Turn::RightNinety, _ => unreachable!(), } } } } impl From<&str> for Direction { fn from(s: &str) -> Self { match s { "L" => Direction::Left, "R" => Direction::Right, "U" => Direction::Up, "D" => Direction::Down, s => panic!("{} is not a valid direction", s), } } } impl From<&Direction> for char { fn from(dir: &Direction) -> Self { match dir { Direction::Left => '←', Direction::Right => '→', Direction::Up => '↑', Direction::Down => '↓', } } } #[derive(Debug, Clone)] struct DigInstruction { dir: Direction, count: usize, color: String, } impl From<&str> for DigInstruction { fn from(s: &str) -> Self { let mut parts = s.split_ascii_whitespace(); let (dir, count, color) = ( parts.next().unwrap(), parts.next().unwrap(), parts.next().unwrap().chars().skip(2).take(6).collect::(), ); Self { dir: dir.into(), count: count.parse().unwrap(), color: color.into(), } } } impl DigInstruction { fn part2_transform(&mut self) { let (distance_s, direction_s) = self.color.split_at(5); self.count = usize::from_str_radix(distance_s, 16).unwrap(); self.dir = match direction_s { "0" => Direction::Right, "1" => Direction::Down, "2" => Direction::Left, "3" => Direction::Up, s => panic!("`{}` is not a valid direction code", s), }; } } #[derive(Debug)] struct DigPlan { instructions: Vec, } impl DigPlan { fn part2_transform(&mut self) { for i in &mut self.instructions { i.part2_transform(); } } } #[derive(Debug, Clone)] struct DigTile { position: Position, } impl Default for DigTile { fn default() -> Self { Self { position: (0, 0) } } } type Position = (isize, isize); #[derive(Debug)] struct DigHole { tiles_loop: LinkedList, area: u64, } // determinant of positions p1 and p2 fn det(p1: Position, p2: Position) -> i64 { ((p1.0 * p2.1) - (p1.1 * p2.0)) as i64 } impl DigHole { fn new() -> Self { DigHole { tiles_loop: LinkedList::new(), area: 0, } } fn pos_offset_n(&self, pos: Position, offset: (isize, isize), n: usize) -> Position { (pos.0 + offset.0 * n as isize, pos.1 + offset.1 * n as isize) } fn run_plan(&mut self, plan: &DigPlan) { let mut cur_pos = (0, 0); self.tiles_loop.push_back(DigTile { position: cur_pos }); let mut move_offset; for (idx, i) in plan.instructions.iter().enumerate() { let prev_instruction = if idx > 0 { &plan.instructions[idx - 1] } else { &plan.instructions[plan.instructions.len() - 1] }; let Some(next_instruction) = plan.instructions.get(idx + 1).or(Some(&plan.instructions[0])) else { panic!() }; let cur_turn = prev_instruction.dir.turn_kind(i.dir); let next_turn = i.dir.turn_kind(next_instruction.dir); // Point needs to live on the 'outside' corner of the character. to achieve this we need to offset the move // by the following. Found this empirically but there's probably some mathematical principle behind it... move_offset = match (cur_turn, next_turn) { (Turn::RightNinety, Turn::RightNinety) => 1, (Turn::RightNinety, Turn::LeftNinety) => 0, (Turn::LeftNinety, Turn::LeftNinety) => -1, (Turn::LeftNinety, Turn::RightNinety) => 0, t => panic!("turn {:?} not allowed here", t), }; cur_pos = self.pos_offset_n(cur_pos, i.dir.offset(), (i.count as isize + move_offset) as usize); self.tiles_loop.push_back(DigTile { position: cur_pos }); } // Shoelace formula // https://en.wikipedia.org/wiki/Shoelace_formula let double_area: i64 = self .tiles_loop .iter() .tuple_windows() .map(|(a, b)| det(a.position, b.position)) .sum(); self.area = (double_area / 2).abs() as u64; } } impl From> for DigPlan { fn from(lines: Lines) -> Self { Self { instructions: lines.map(|line| DigInstruction::from(line.unwrap().as_str())).collect(), } } } // PROBLEM 1 solution fn problem1(input: Lines) -> u64 { let plan = DigPlan::from(input); let mut dig = DigHole::new(); dig.run_plan(&plan); dig.area } // PROBLEM 2 solution fn problem2(input: Lines) -> u64 { let mut plan = DigPlan::from(input); let mut dig = DigHole::new(); plan.part2_transform(); dig.run_plan(&plan); dig.area } #[cfg(test)] mod tests { use crate::*; use std::io::Cursor; const EXAMPLE: &str = &"R 6 (#70c710) D 5 (#0dc571) L 2 (#5713f0) D 2 (#d2c081) R 2 (#59c680) D 2 (#411b91) L 5 (#8ceee2) U 2 (#caa173) L 1 (#1b58a2) U 2 (#caa171) R 2 (#7807d2) U 3 (#a77fa3) L 2 (#015232) U 2 (#7a21e3)"; const AREA16_SQUARE: &str = &"R 3 (#000000) D 3 (#000000) L 3 (#000000) U 4 (#000000"; #[test] fn area16_square() { let c = Cursor::new(AREA16_SQUARE); assert_eq!(problem1(c.lines()), 16); } #[test] fn problem1_example() { let c = Cursor::new(EXAMPLE); assert_eq!(problem1(c.lines()), 62); } #[test] fn problem2_example() { let c = Cursor::new(EXAMPLE); assert_eq!(problem2(c.lines()), 952408144115); } }