diff --git a/2022/day12-hill_climbing_algorithm/src/lib.rs b/2022/day12-hill_climbing_algorithm/src/lib.rs index 2e25420..ec3d295 100644 --- a/2022/day12-hill_climbing_algorithm/src/lib.rs +++ b/2022/day12-hill_climbing_algorithm/src/lib.rs @@ -1,3 +1,18 @@ +use core::fmt::Display; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + LineMalformed(String), +} + +impl Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + } + } +} + use std::u8; #[derive(PartialEq, Eq, Copy, Clone, Debug)] @@ -219,15 +234,19 @@ pub fn get_network_to(destination: Position, grid: &[Vec]) -> Vec]) -> Vec (Vec>, Coordinate, Coordinate, Coordinate) { +pub fn try_parse(map: &str) -> Result<(Vec>, Coordinate, Coordinate, Coordinate), ParseError> { let mut grid = Vec::new(); let mut start = Coordinate { x: 0, y: 0, }; let mut end = Coordinate { x: 0, y: 0, }; @@ -261,11 +283,51 @@ pub fn parse(map: &str) -> (Vec>, Coordinate, Coordinate, Coordinate) { y: grid.len() as u8 - 1, }; - grid.iter().enumerate().for_each(|(idx, row)| { + for (idx, row) in grid.iter().enumerate() { if row.len() != max.x as usize + 1 { - panic!("Tried to parse a non-rectangular map. Row {idx} has {} characters, but row 0 has {}.", row.len(), max.x + 1); + return Err(ParseError::LineMalformed(format!("Tried to parse a non-rectangular map. Row {idx} has {} characters, but row 0 has {}.", row.len(), max.x + 1))); } - }); + } - (grid, start, end, max) + Ok((grid, start, end, max)) +} + +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + let (grid, start, end, max) = try_parse(input)?; + let dest_position = Position::from(25, end, max); + let dest_network = get_network_to(dest_position, &grid); + let first = get_length(&dest_network, |position| position.coordinate() == start); + let second = get_length(&dest_network, |position| position.height() == 0); + Ok((first, second)) +} + +fn get_length(dest_network: &[Vec], start_condition: F) -> usize where + F: Fn(&Position) -> bool + Copy { + dest_network.iter() + .enumerate() + .find(|(_length, positions)| positions.iter().any(start_condition)) + .unwrap() + .0 +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; + + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((31, 29))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((425, 418))); + } } diff --git a/2022/day12-hill_climbing_algorithm/src/main.rs b/2022/day12-hill_climbing_algorithm/src/main.rs index bd1c243..f32ad14 100644 --- a/2022/day12-hill_climbing_algorithm/src/main.rs +++ b/2022/day12-hill_climbing_algorithm/src/main.rs @@ -20,7 +20,7 @@ fn main() { //let map = read_file("sample_input"); let map = read_file("input"); - let (grid, start, end, max) = parse(&map); + let (grid, start, end, max) = try_parse(&map).unwrap(); let end_position = Position::from(25, end, max); let dest_network = get_network_to(end_position, &grid); @@ -32,36 +32,3 @@ fn main() { println!("The shortest scenic route is {} steps long.", shortest_scenic); } -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn sample_input_matches_challenge() { - let map = read_file("tests/sample_input"); - let (grid, start, end, max) = parse(&map); - - let end_position = Position::from(25, end, max); - let dest_network = get_network_to(end_position, &grid); - - let start_finish_length = get_length(&dest_network, |position| position.coordinate() == start); - assert_eq!(start_finish_length, 31); - - let shortest_scenic = get_length(&dest_network, |position| position.height() == 0); - assert_eq!(shortest_scenic, 29) - } - - #[test] - fn challenge_input_matches_solution() { - let map = read_file("tests/input"); - let (grid, start, end, max) = parse(&map); - - let end_position = Position::from(25, end, max); - let dest_network = get_network_to(end_position, &grid); - - let start_finish_length = get_length(&dest_network, |position| position.coordinate() == start); - assert_eq!(start_finish_length, 425); - - let shortest_scenic = get_length(&dest_network, |position| position.height() == 0); - assert_eq!(shortest_scenic, 418) - } -} diff --git a/2022/day12-hill_climbing_algorithm/tests/input b/2022/day12-hill_climbing_algorithm/tests/challenge_input similarity index 100% rename from 2022/day12-hill_climbing_algorithm/tests/input rename to 2022/day12-hill_climbing_algorithm/tests/challenge_input diff --git a/2022/day13-distress_signal/src/main.rs b/2022/day13-distress_signal/src/lib.rs similarity index 65% rename from 2022/day13-distress_signal/src/main.rs rename to 2022/day13-distress_signal/src/lib.rs index 759125b..941665b 100644 --- a/2022/day13-distress_signal/src/main.rs +++ b/2022/day13-distress_signal/src/lib.rs @@ -1,4 +1,18 @@ -use std::{fs, cmp::Ordering}; +use core::fmt::Display; +use std::cmp::Ordering; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError<'a> { + LineMalformed(&'a str), +} + +impl Display for ParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + } + } +} #[derive(Debug, Clone, PartialEq)] enum PacketItem { @@ -13,19 +27,23 @@ struct Pair { right: PacketItem, } -impl Pair { - fn from(read_string: &str) -> Self { - let (first, second) = read_string.split_once('\n').expect("Failed to split input pair."); +impl <'a> TryFrom<&'a str> for Pair { + type Error = ParseError<'a>; - let left = parse_packet_item(first); - let right = parse_packet_item(second); + fn try_from(value: &'a str) -> Result { + if let Some((first, second)) = value.split_once('\n') { - Self { - left, - right, + let left = parse_packet_item(first); + let right = parse_packet_item(second); + + Ok(Self { + left, + right, + }) + } else { + Err(Self::Error::LineMalformed(value)) } } - } fn are_correctly_ordered(left: &PacketItem, right: &PacketItem) -> Option { @@ -118,17 +136,6 @@ fn parse_packet_item(string_representation: &str) -> PacketItem { PacketItem::List(sub_items) } -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not Found") -} - -fn get_pairs(received: &str) -> Vec { - received.split("\n\n") - .map(Pair::from) - .collect() -} - fn get_pair_sum(pairs: &[Pair]) -> usize { pairs.iter() .enumerate() @@ -157,33 +164,60 @@ fn decode(pairs: &[Pair]) -> usize { .enumerate() .filter(|(_, packet)| *packet == divider1 || *packet == divider2) .map(|(index, _)| index + 1) - .reduce(|a, b| a*b) - .unwrap() + .product::() } -fn main() { - let received = read_file("input"); - - let pairs = get_pairs(&received); - - println!("The sum of the indexes of correctly ordered pairs is {}", get_pair_sum(&pairs)); - println!("The decoder key is {}", decode(&pairs)); +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + let pairs = input.split("\n\n").map(Pair::try_from).collect::, _>>()?; + let first = get_pair_sum(&pairs); + let second = decode(&pairs); + Ok((first, second)) } -#[test] -fn sample_input() { - let received = read_file("tests/sample_input"); - let pairs = get_pairs(&received); +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; - assert_eq!(get_pair_sum(&pairs), 13); - assert_eq!(decode(&pairs), 140); -} - -#[test] -fn challenge_input() { - let received = read_file("tests/input"); - let pairs = get_pairs(&received); - - assert_eq!(get_pair_sum(&pairs), 5659); - assert_eq!(decode(&pairs), 22110); + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((13, 140))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((5659, 22110))); + } } +// fn main() { +// let received = read_file("input"); +// +// let pairs = get_pairs(&received); +// +// println!("The sum of the indexes of correctly ordered pairs is {}", get_pair_sum(&pairs)); +// println!("The decoder key is {}", decode(&pairs)); +// } +// +// #[test] +// fn sample_input() { +// let received = read_file("tests/sample_input"); +// let pairs = get_pairs(&received); +// +// assert_eq!(get_pair_sum(&pairs), 13); +// assert_eq!(decode(&pairs), 140); +// } +// +// #[test] +// fn challenge_input() { +// let received = read_file("tests/input"); +// let pairs = get_pairs(&received); +// +// assert_eq!(get_pair_sum(&pairs), 5659); +// assert_eq!(decode(&pairs), 22110); +// } diff --git a/2022/day13-distress_signal/tests/input b/2022/day13-distress_signal/tests/challenge_input similarity index 100% rename from 2022/day13-distress_signal/tests/input rename to 2022/day13-distress_signal/tests/challenge_input diff --git a/2022/day14-regolith_reservoir/src/lib.rs b/2022/day14-regolith_reservoir/src/lib.rs new file mode 100644 index 0000000..a4a5acf --- /dev/null +++ b/2022/day14-regolith_reservoir/src/lib.rs @@ -0,0 +1,208 @@ +use core::fmt::Display; +use std::num::ParseIntError; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError<'a> { + LineMalformed(&'a str), + ParseIntError(std::num::ParseIntError), +} + +impl From for ParseError<'_> { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl Display for ParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"), + } + } +} + +use std::collections::HashSet; + +#[derive(PartialEq)] +enum Status { Resting, Falling, Blocked } + +#[derive(PartialEq)] +enum Mode { EndlessVoid, WithFloor } + +#[derive(PartialEq, Eq, Hash, Clone)] +struct Position { + x: usize, + y: usize, +} + +impl <'a> TryFrom<&'a str> for Position { + type Error = ParseError<'a>; + + fn try_from(value: &'a str) -> Result { + let components = value.split(',').collect::>().iter().map(|i| i.parse()).collect::, _>>()?; + if !components.len() == 2 { + return Err(Self::Error::LineMalformed(value)); + } + + Ok(Self { + x: components[0], + y: components[1], + }) + } +} + +struct Sand { + position: Position, + ymax: usize, +} + +const ORIGIN: Position = Position { + x: 500, + y: 0, +}; + +impl Sand { + fn fall(&mut self, cave: &HashSet, other_sand: &mut HashSet, mode: &Mode) -> Status { + // return if we fall below all structures + if *mode == Mode::EndlessVoid && self.position.y >= self.ymax { + return Status::Falling; + } + // or we reached the floor. + if *mode == Mode::WithFloor && self.position.y > self.ymax { + other_sand.insert(self.position.clone()); + return Status::Resting; + } + // Fall down if possible + if !cave.contains(&Position{ x: self.position.x, y: self.position.y+1 }) && !other_sand.contains(&Position { x: self.position.x, y: self.position.y+1 }) { + self.position.y += 1; + return self.fall(cave, other_sand, mode); + } + // Next try falling left + if !cave.contains(&Position{ x: self.position.x-1, y: self.position.y+1 }) && !other_sand.contains(&Position { x: self.position.x-1, y: self.position.y+1 }) { + self.position.x -= 1; + self.position.y += 1; + return self.fall(cave, other_sand, mode); + } + // Next try falling right + if !cave.contains(&Position{ x: self.position.x+1, y: self.position.y+1 }) && !other_sand.contains(&Position { x: self.position.x+1, y: self.position.y+1 }) { + self.position.x += 1; + self.position.y += 1; + return self.fall(cave, other_sand, mode); + } + // Else we can't fall any more. + other_sand.insert(self.position.clone()); + if self.position == ORIGIN { + Status::Blocked + } else { + Status::Resting + } + } + + fn spawn(cave: &HashSet, ymax: usize, mode: &Mode) -> HashSet { + let mut other_sand = HashSet::new(); + loop { + let mut new_unit = Sand { + position: ORIGIN, + ymax, + }; + let new_status = new_unit.fall(cave, &mut other_sand, mode); + if new_status != Status::Resting { + break; + } + } + other_sand + } +} + +fn positions_of_formation(formation: &str) -> Result, ParseError> { + let mut blocked = Vec::new(); + let corners = formation.split(" -> ") + .map(Position::try_from) + .collect::, _>>()?; + if corners.len() == 1 { + return Ok(corners); + } + for pair in corners.windows(2).collect::>() { + let minx = pair[0].x.min(pair[1].x); + let maxx = pair[0].x.max(pair[1].x); + let miny = pair[0].y.min(pair[1].y); + let maxy = pair[0].y.max(pair[1].y); + + for x in minx..=maxx { + for y in miny..=maxy { + blocked.push(Position{ x, y }); + } + } + } + Ok(blocked) +} + +fn get_cave(scan: &str) -> Result<(HashSet, usize), ParseError> { + let mut cave = HashSet::new(); + for line in scan.lines() { + cave.extend(positions_of_formation(line)?.into_iter()); + } + let ymax = cave.iter() + .map(|pos| pos.y) + .max() + .unwrap_or_default(); + Ok((cave, ymax)) +} + +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + // let items: Vec<_> = input.lines().map(::try_from).collect::, _>>()?; + let (cave, ymax) = get_cave(input)?; + let first = Sand::spawn(&cave, ymax, &Mode::EndlessVoid).len(); + let second = Sand::spawn(&cave, ymax, &Mode::WithFloor).len(); + Ok((first, second)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; + + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((24, 93))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((979, 29044))); + } +} +// fn main() { +// let scan = read_file("input"); +// +// let (cave, ymax) = get_cave(&scan); +// +// let endless_sand = Sand::spawn(&cave, ymax, &Mode::EndlessVoid); +// println!("In Case of an endless void, {} units of sand will come to a rest", endless_sand.len()); +// +// let sand_with_floor = Sand::spawn(&cave, ymax, &Mode::WithFloor); +// println!("In Case of a floor, {} units of sand will be spawned", sand_with_floor.len()); +// } +// +// #[test] +// fn sample_input() { +// let scan = read_file("tests/sample_input"); +// let (cave, ymax) = get_cave(&scan); +// assert_eq!(Sand::spawn(&cave, ymax, &Mode::EndlessVoid).len(), 24); +// assert_eq!(Sand::spawn(&cave, ymax, &Mode::WithFloor).len(), 93); +// } +// +// #[test] +// fn challenge_input() { +// let scan = read_file("tests/input"); +// let (cave, ymax) = get_cave(&scan); +// assert_eq!(Sand::spawn(&cave, ymax, &Mode::EndlessVoid).len(), 979); +// assert_eq!(Sand::spawn(&cave, ymax, &Mode::WithFloor).len(), 29044); +// } diff --git a/2022/day14-regolith_reservoir/src/main.rs b/2022/day14-regolith_reservoir/src/main.rs deleted file mode 100644 index 0f8afd3..0000000 --- a/2022/day14-regolith_reservoir/src/main.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::{fs, collections::HashSet}; - -#[derive(PartialEq)] -enum Status { Resting, Falling, Blocked } - -#[derive(PartialEq)] -enum Mode { EndlessVoid, WithFloor } - -#[derive(PartialEq, Eq, Hash, Clone)] -struct Position { - x: usize, - y: usize, -} - -impl Position { - fn from(string: &str) -> Self { - let components = string.split(',').collect::>().iter().map(|i| i.parse().unwrap()).collect::>(); - if !components.len() == 2 { - panic!("unable to parse {string} into Position"); - } - - Self { - x: components[0], - y: components[1], - } - } -} - -struct Sand { - position: Position, - ymax: usize, -} - -const ORIGIN: Position = Position { - x: 500, - y: 0, -}; - -impl Sand { - //fn fall(&mut self, cave: &Vec, other_sand: &mut Vec, mode: &Mode) -> Status { - fn fall(&mut self, cave: &HashSet, other_sand: &mut HashSet, mode: &Mode) -> Status { - // return if we fall below all structures - if *mode == Mode::EndlessVoid && self.position.y >= self.ymax { - return Status::Falling; - } - // or we reached the floor. - if *mode == Mode::WithFloor && self.position.y > self.ymax { - other_sand.insert(self.position.clone()); - return Status::Resting; - } - // Fall down if possible - if !cave.contains(&Position{ x: self.position.x, y: self.position.y+1 }) && !other_sand.contains(&Position { x: self.position.x, y: self.position.y+1 }) { - self.position.y += 1; - return self.fall(cave, other_sand, mode); - } - // Next try falling left - if !cave.contains(&Position{ x: self.position.x-1, y: self.position.y+1 }) && !other_sand.contains(&Position { x: self.position.x-1, y: self.position.y+1 }) { - self.position.x -= 1; - self.position.y += 1; - return self.fall(cave, other_sand, mode); - } - // Next try falling right - if !cave.contains(&Position{ x: self.position.x+1, y: self.position.y+1 }) && !other_sand.contains(&Position { x: self.position.x+1, y: self.position.y+1 }) { - self.position.x += 1; - self.position.y += 1; - return self.fall(cave, other_sand, mode); - } - // Else we can't fall any more. - other_sand.insert(self.position.clone()); - if self.position == ORIGIN { - Status::Blocked - } else { - Status::Resting - } - } - - fn spawn(cave: &HashSet, ymax: usize, mode: &Mode) -> HashSet { - let mut other_sand = HashSet::new(); - loop { - let mut new_unit = Sand { - position: ORIGIN, - ymax, - }; - let new_status = new_unit.fall(cave, &mut other_sand, mode); - if new_status != Status::Resting { - break; - } - } - other_sand - } -} - -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not Found") -} - -fn positions_of_formation(formation: &str) -> Vec { - let mut blocked = Vec::new(); - let corners = formation.split(" -> ") - .map(Position::from) - .collect::>(); - if corners.len() == 1 { - return corners; - } - for pair in corners.windows(2).collect::>() { - let minx = pair[0].x.min(pair[1].x); - let maxx = pair[0].x.max(pair[1].x); - let miny = pair[0].y.min(pair[1].y); - let maxy = pair[0].y.max(pair[1].y); - - for x in minx..=maxx { - for y in miny..=maxy { - blocked.push(Position{ x, y }); - } - } - } - blocked -} - -fn get_cave(scan: &str) -> (HashSet, usize){ - let cave = scan.lines() - .flat_map(|formation| positions_of_formation(formation).iter() - .cloned() - .collect::>()) - .collect::>(); - let ymax = cave.iter() - .map(|pos| pos.y) - .max() - .unwrap(); - (cave, ymax) -} - -fn main() { - let scan = read_file("input"); - - let (cave, ymax) = get_cave(&scan); - - let endless_sand = Sand::spawn(&cave, ymax, &Mode::EndlessVoid); - println!("In Case of an endless void, {} units of sand will come to a rest", endless_sand.len()); - - let sand_with_floor = Sand::spawn(&cave, ymax, &Mode::WithFloor); - println!("In Case of a floor, {} units of sand will be spawned", sand_with_floor.len()); -} - -#[test] -fn sample_input() { - let scan = read_file("tests/sample_input"); - let (cave, ymax) = get_cave(&scan); - assert_eq!(Sand::spawn(&cave, ymax, &Mode::EndlessVoid).len(), 24); - assert_eq!(Sand::spawn(&cave, ymax, &Mode::WithFloor).len(), 93); -} - -#[test] -fn challenge_input() { - let scan = read_file("tests/input"); - let (cave, ymax) = get_cave(&scan); - assert_eq!(Sand::spawn(&cave, ymax, &Mode::EndlessVoid).len(), 979); - assert_eq!(Sand::spawn(&cave, ymax, &Mode::WithFloor).len(), 29044); -} diff --git a/2022/day14-regolith_reservoir/tests/input b/2022/day14-regolith_reservoir/tests/challenge_input similarity index 100% rename from 2022/day14-regolith_reservoir/tests/input rename to 2022/day14-regolith_reservoir/tests/challenge_input diff --git a/2022/day15-beacon_exclusive_zone/src/main.rs b/2022/day15-beacon_exclusive_zone/src/lib.rs similarity index 56% rename from 2022/day15-beacon_exclusive_zone/src/main.rs rename to 2022/day15-beacon_exclusive_zone/src/lib.rs index ff41c95..d9f4e97 100644 --- a/2022/day15-beacon_exclusive_zone/src/main.rs +++ b/2022/day15-beacon_exclusive_zone/src/lib.rs @@ -1,6 +1,29 @@ -use std::{fs, collections::{HashSet, HashMap}}; +use core::fmt::Display; +use std::num::ParseIntError; +use std::collections::{BTreeSet, BTreeMap}; -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError<'a> { + ParseIntError(std::num::ParseIntError), + LineMalformed(&'a str), +} + +impl From for ParseError<'_> { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl Display for ParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct Position { x: isize, y: isize, @@ -18,11 +41,13 @@ struct Sensor { beacon_distance: isize, } -impl Sensor { - fn from(reading: &str) -> Self { - let components = reading.split(' ').collect::>(); +impl <'a> TryFrom<&'a str> for Sensor { + type Error = ParseError<'a>; + + fn try_from(value: &'a str) -> Result { + let components = value.split(' ').collect::>(); if components.len() != 10 { - panic!("{components:?} does not have 10 items."); + return Err(Self::Error::LineMalformed(value)); } let sensor_x_str = &components[2][2..]; @@ -30,9 +55,9 @@ impl Sensor { let beacon_x_str = &components[8][2..]; let beacon_y_str = &components[9][2..]; - let sensor_x = sensor_x_str[0..sensor_x_str.len()-1].parse::().unwrap(); - let sensor_y = sensor_y_str[0..sensor_y_str.len()-1].parse::().unwrap(); - let beacon_x = beacon_x_str[0..beacon_x_str.len()-1].parse::().unwrap(); + let sensor_x = sensor_x_str[0..sensor_x_str.len()-1].parse::()?; + let sensor_y = sensor_y_str[0..sensor_y_str.len()-1].parse::()?; + let beacon_x = beacon_x_str[0..beacon_x_str.len()-1].parse::()?; let beacon_y = beacon_y_str[0..].parse::().unwrap(); let position = Position { @@ -44,40 +69,42 @@ impl Sensor { y: beacon_y, }; - Self { - position: position.clone(), + Ok(Self { + position, beacon_distance: position.distance_to(&beacon_position), - } + }) } +} - fn beacon_from(reading: &str) -> Position { +impl Sensor { + fn beacon_from(reading: &str) -> Result { let components = reading.split(' ').collect::>(); if components.len() != 10 { - panic!("{components:?} does not have 10 items."); + return Err(ParseError::LineMalformed(reading)); } let beacon_x_str = &components[8][2..]; let beacon_y_str = &components[9][2..]; - let beacon_x = beacon_x_str[0..beacon_x_str.len()-1].parse::().unwrap(); - let beacon_y = beacon_y_str[0..].parse::().unwrap(); + let beacon_x = beacon_x_str[0..beacon_x_str.len()-1].parse::()?; + let beacon_y = beacon_y_str[0..].parse::()?; - Position { + Ok(Position { x: beacon_x, y: beacon_y, - } + }) } - fn at_row(&self, row: isize) -> HashSet { + fn at_row(&self, row: isize) -> BTreeSet { let slice_depth = self.beacon_distance - (row-self.position.y).abs(); match slice_depth { - nope if nope <= 0 => HashSet::new(), + nope if nope <= 0 => BTreeSet::new(), _ => (self.position.x-slice_depth..=self.position.x+slice_depth).collect(), } } - fn first_non_reachables(&self, unreachables: &mut HashMap, min: isize, max: isize) { + fn first_non_reachables(&self, unreachables: &mut BTreeMap, min: isize, max: isize) { // top right for i in 0..=self.beacon_distance { let x = self.position.x+i; @@ -117,12 +144,7 @@ impl Sensor { } -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not Found") -} - -fn beacon_free_positions(row: isize, sensors: &[Sensor], beacons: &HashSet) -> usize { +fn beacon_free_positions(row: isize, sensors: &[Sensor], beacons: &BTreeSet) -> usize { sensors.iter() .map(|s| s.at_row(row)) .reduce(|a, b| a.union(&b).cloned().collect()) @@ -139,45 +161,51 @@ fn is_reachable_by(position: &Position, sensors: &[Sensor]) -> bool { false } -fn get_non_reachable(sensors: &[Sensor], beacons: &HashSet, max: isize) -> isize { - let mut first_non_reachables = HashMap::new(); +fn get_non_reachable(sensors: &[Sensor], beacons: &BTreeSet, max: isize) -> isize { + let mut first_non_reachables = BTreeMap::new(); sensors.iter() .for_each(|s| s.first_non_reachables(&mut first_non_reachables, 0, max)); if let Some((&pos, _)) = &first_non_reachables.iter() .find(|(pos, &repeat)| repeat && !beacons.contains(pos) && !is_reachable_by(pos, sensors)) { pos.x * 4_000_000 + pos.y - } else { - 0 + } else { + 0 + } +} + +pub fn run(input: &str) -> Result<(usize, isize), ParseError> { + let sensors = input.lines().map(Sensor::try_from).collect::, _>>()?; + let beacons = input.lines().map(Sensor::beacon_from).collect::, _>>()?; + + let first = beacon_free_positions(2_000_000, &sensors, &beacons); + let second = get_non_reachable(&sensors, &beacons, 4_000_000); + Ok((first, second)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; + + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn sample_input() { + let readings = read_file("tests/sample_input"); + let sensors = readings.lines().map(Sensor::try_from).collect::, _>>().unwrap(); + let beacons = readings.lines().map(Sensor::beacon_from).collect::, _>>().unwrap(); + + assert_eq!(beacon_free_positions(10, &sensors, &beacons), 26); + assert_eq!(get_non_reachable(&sensors, &beacons, 20), 56000011); + } + + #[test] + fn challenge_input() { + let readings = read_file("tests/challenge_input"); + + assert_eq!(run(&readings), Ok((5367037, 11914583249288))); } } - -fn main() { - let readings = read_file("input"); - - let sensors = readings.lines().map(Sensor::from).collect::>(); - let beacons = readings.lines().map(Sensor::beacon_from).collect::>(); - - println!("Not in Line 2_000_000: {}", beacon_free_positions(2_000_000, &sensors, &beacons)); - println!("Non-Reachable Position found with frequency {}.", get_non_reachable(&sensors, &beacons, 4_000_000)); -} - -#[test] -fn sample_input() { - let readings = read_file("tests/sample_input"); - let sensors = readings.lines().map(Sensor::from).collect::>(); - let beacons = readings.lines().map(Sensor::beacon_from).collect::>(); - - assert_eq!(beacon_free_positions(10, &sensors, &beacons), 26); - assert_eq!(get_non_reachable(&sensors, &beacons, 20), 56000011); -} - -#[test] -fn challenge_input() { - let readings = read_file("tests/input"); - let sensors = readings.lines().map(Sensor::from).collect::>(); - let beacons = readings.lines().map(Sensor::beacon_from).collect::>(); - - assert_eq!(beacon_free_positions(2_000_000, &sensors, &beacons), 5367037); - assert_eq!(get_non_reachable(&sensors, &beacons, 4_000_000), 11914583249288); -} diff --git a/2022/day15-beacon_exclusive_zone/tests/input b/2022/day15-beacon_exclusive_zone/tests/challenge_input similarity index 100% rename from 2022/day15-beacon_exclusive_zone/tests/input rename to 2022/day15-beacon_exclusive_zone/tests/challenge_input diff --git a/2022/day16-proboscidae_volcanium/src/main.rs b/2022/day16-proboscidae_volcanium/src/lib.rs similarity index 77% rename from 2022/day16-proboscidae_volcanium/src/main.rs rename to 2022/day16-proboscidae_volcanium/src/lib.rs index a8b99b2..4e006aa 100644 --- a/2022/day16-proboscidae_volcanium/src/main.rs +++ b/2022/day16-proboscidae_volcanium/src/lib.rs @@ -1,4 +1,28 @@ -use std::{fs, usize, collections::HashMap}; +use core::fmt::Display; +use std::num::ParseIntError; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError<'a> { + ParseIntError(std::num::ParseIntError), + LineMalformed(&'a str), +} + +impl From for ParseError<'_> { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl Display for ParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"), + } + } +} + #[derive(Clone)] struct Valve { @@ -40,11 +64,6 @@ fn lookup_or_insert<'a>(array: &mut Vec<&'a str>, value: &'a str) -> u8 { } } -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not Found") -} - fn try_permutations(valves: &[Valve], distances: &HashMap<(u8,u8),u8>, starting_index: u8, time: u8) -> usize { let closed_valves: Vec = valves.iter().filter(|v| !v.open).cloned().collect(); let mut permutations_map: Vec>> = vec![(0..closed_valves.len()).map(|i| vec![closed_valves[i].id as usize]).collect()]; @@ -154,71 +173,68 @@ fn try_permutation(valves: &[Valve], distances: &HashMap<(u8, u8), u8>, permutat let mut released = 0; for valve_id in permutation { time_remaining -= *distances.get(&(last_position as u8, *valve_id as u8)).unwrap() as usize + 1; - released += time_remaining * valves[*valve_id].flow_rate as usize; + released += time_remaining * valves[*valve_id].flow_rate; last_position = *valve_id; } released } -fn init(scan: &str) -> (Vec, HashMap<(u8, u8), u8>, u8) { +fn init(scan: &str) -> Result<(Vec, HashMap<(u8, u8), u8>, u8), ParseError> { let mut ids = Vec::new(); let mut all_valves: Vec = scan.lines() .map(|valve_line| { let components = valve_line.split(' ').collect::>(); - if components.len() < 10 { panic!("{valve_line} has fewer than 10 components."); } + if components.len() < 10 { return Err(ParseError::LineMalformed(valve_line)); } let id = lookup_or_insert(&mut ids, components[1]); let flow_rate_with_semicolon = &components[4][5..]; - let flow_rate = flow_rate_with_semicolon[..flow_rate_with_semicolon.len()-1].parse::().unwrap(); + let flow_rate = flow_rate_with_semicolon[..flow_rate_with_semicolon.len()-1].parse::()?; let mut connected_valves = Vec::new(); for other_valve_with_comma in components.iter().skip(9).take(components.len()-10) { connected_valves.push(lookup_or_insert(&mut ids, &other_valve_with_comma[..other_valve_with_comma.len()-1])); } connected_valves.push(lookup_or_insert(&mut ids, components[components.len()-1])); - Valve { + Ok(Valve { id, flow_rate, connected_valves, open: flow_rate == 0, - } + }) }) - .collect(); + .collect::, _>>()?; all_valves.sort_by_key(|v| v.id); let all_distances = get_all_distances(&all_valves); - (all_valves, all_distances, lookup_or_insert(&mut ids, "AA")) + Ok((all_valves, all_distances, lookup_or_insert(&mut ids, "AA"))) } -fn main() { - //let scan = read_file("sample_input"); - let scan = read_file("input"); - - - let (all_valves, all_distances, starting_index) = init(&scan); - //let all_distances = get_all_distances(&all_valves); - //let starting_index = lookup_or_insert(&mut ids, "AA"); - - println!("Working alone, we release {} units.", try_permutations(&all_valves, &all_distances, starting_index, 30)); - - let with_elephants = try_permutations_with_elephants(&all_valves, &all_distances, starting_index, 26); - println!("Using elephants, we release {with_elephants} units."); +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + // let items: Vec<_> = input.lines().map(::try_from).collect::, _>>()?; + let (all_valves, all_distances, starting_index) = init(input)?; + let first = try_permutations(&all_valves, &all_distances, starting_index, 30); + let second = try_permutations_with_elephants(&all_valves, &all_distances, starting_index, 26); + Ok((first, second)) } -#[test] -fn sample_input() { - let scan = read_file("tests/sample_input"); - let (all_valves, all_distances, starting_index) = init(&scan); +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; - assert_eq!(try_permutations(&all_valves, &all_distances, starting_index, 30), 1651); - assert_eq!(try_permutations_with_elephants(&all_valves, &all_distances, starting_index, 26), 1707); -} - -#[test] -fn challenge_input() { - let scan = read_file("tests/input"); - let (all_valves, all_distances, starting_index) = init(&scan); - - assert_eq!(try_permutations(&all_valves, &all_distances, starting_index, 30), 2056); - assert_eq!(try_permutations_with_elephants(&all_valves, &all_distances, starting_index, 26), 2513); + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((1651, 1707))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((2056, 2513))); + } } diff --git a/2022/day16-proboscidae_volcanium/tests/input b/2022/day16-proboscidae_volcanium/tests/challenge_input similarity index 100% rename from 2022/day16-proboscidae_volcanium/tests/input rename to 2022/day16-proboscidae_volcanium/tests/challenge_input diff --git a/2022/day17-pyroclastic_flow/src/main.rs b/2022/day17-pyroclastic_flow/src/lib.rs similarity index 88% rename from 2022/day17-pyroclastic_flow/src/main.rs rename to 2022/day17-pyroclastic_flow/src/lib.rs index 9c6a16c..8cebac2 100644 --- a/2022/day17-pyroclastic_flow/src/main.rs +++ b/2022/day17-pyroclastic_flow/src/lib.rs @@ -1,4 +1,17 @@ -use std::{fs, usize}; +use core::fmt::Display; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError { + InvalidDirection(char), +} + +impl Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidDirection(c) => write!(f, "Trying to parse invalid character {c} into Direction"), + } + } +} #[derive(PartialEq)] enum Shape { @@ -9,15 +22,15 @@ enum Shape { Square, } -impl Shape { - fn from(number: usize) -> Self { - match number%5 { +impl > From for Shape { + fn from(number: T) -> Self { + match number.into() % 5 { 0 => Shape::Minus, 1 => Shape::Plus, 2 => Shape::L, 3 => Shape::Pipe, 4 => Shape::Square, - _ => panic!("WTF? This can never happen."), + _ => unreachable!("number%5 can only ever be one of the values above"), } } } @@ -25,6 +38,18 @@ impl Shape { #[derive(PartialEq, Debug)] enum Direction { Left, Right, Down } +impl TryFrom for Direction { + type Error = ParseError; + + fn try_from(value: char) -> Result { + match value { + '<' => Ok(Direction::Left), + '>' => Ok(Direction::Right), + c => Err(Self::Error::InvalidDirection(c)), + } + } +} + #[derive(PartialEq)] enum State { Falling, Resting } @@ -292,7 +317,7 @@ impl Block { self.bl_position.x = match direction { Direction::Left => self.bl_position.x - 1, Direction::Right => self.bl_position.x + 1, - _ => panic!("unexpected direction"), + Direction::Down => panic!("Didn't expect to be pushed down"), }; for position in &old_positions { arena.free(position); @@ -317,14 +342,10 @@ impl PlayArea { fn occupy(&mut self, coordinates: &Position) { self.blocked_tiles[coordinates.y][coordinates.x as usize] = true; - //self.max_y = self.max_y.max(coordinates.y as isize); } fn free(&mut self, coordinates: &Position) { self.blocked_tiles[coordinates.y][coordinates.x as usize] = false; - // if coordinates.y as isize == self.max_y && !self.blocked_tiles[coordinates.y].iter().any(|&b| b) { - // self.max_y -= 1; // self.max_y.saturating_sub(1); - // } } fn new() -> Self { @@ -336,20 +357,6 @@ impl PlayArea { } } -fn parse_directions(winds: &str) -> Vec { - winds.chars() - .filter(|c| ['<', '>'].contains(c)) - .map(|c| match c { - '<' => Direction::Left, - '>' => Direction::Right, - _ => panic!("unexpected wind direction: {}", c), - }).collect() -} - -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not Found") -} fn solve_with_pattern(target: usize, directions: &[Direction]) -> usize { let mut results: Vec<(usize, usize, usize)> = Vec::new(); @@ -369,9 +376,9 @@ fn solve_with_pattern(target: usize, directions: &[Direction]) -> usize { let state = (i, direction_index, arena.max_y as usize); let old_results: Vec<(usize, usize, usize)> = results.iter().filter(|(old_i, old_direction, _)| old_i % 5 == i % 5 && *old_direction == direction_index).cloned().collect(); if old_results.len() > 1 { - let period = (i - old_results[1].0) as usize; + let period = i - old_results[1].0; let period_growth = arena.max_y as usize - old_results[1].2; - let offset = results[target % period].2 as usize; + let offset = results[target % period].2; return (target/period) * period_growth + offset; } else { results.push(state); @@ -383,30 +390,31 @@ fn solve_with_pattern(target: usize, directions: &[Direction]) -> usize { 0 } -fn main() { - let winds = read_file("input"); - - let directions = parse_directions(&winds); - - [2022, 1_000_000_000_000].iter().for_each(|target| { - println!("After {} rocks have fallen, the tower is {} units high.", target, solve_with_pattern(*target, &directions)); - }); +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + let directions = input.chars().map(Direction::try_from).collect::, _>>()?; + let first = solve_with_pattern(2022, &directions); + let second = solve_with_pattern(1_000_000_000_000, &directions); + Ok((first, second)) } -#[test] -fn sample_input() { - let winds = read_file("tests/sample_input"); - let directions = parse_directions(&winds); +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; - assert_eq!(solve_with_pattern(2022, &directions), 3069); - assert_eq!(solve_with_pattern(1_000_000_000_000, &directions), 1514285714288); -} - -#[test] -fn challenge_input() { - let winds = read_file("tests/input"); - let directions = parse_directions(&winds); - - assert_eq!(solve_with_pattern(2022, &directions), 3219); - assert_eq!(solve_with_pattern(1_000_000_000_000, &directions), 1582758620701); + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((3069, 1514285714288))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((3219, 1582758620701))); + } } diff --git a/2022/day17-pyroclastic_flow/tests/input b/2022/day17-pyroclastic_flow/tests/challenge_input similarity index 100% rename from 2022/day17-pyroclastic_flow/tests/input rename to 2022/day17-pyroclastic_flow/tests/challenge_input diff --git a/2022/day18-boiling_boulders/src/lib.rs b/2022/day18-boiling_boulders/src/lib.rs new file mode 100644 index 0000000..7acc8cb --- /dev/null +++ b/2022/day18-boiling_boulders/src/lib.rs @@ -0,0 +1,123 @@ +use core::fmt::Display; +use std::{num::ParseIntError, collections::BTreeSet}; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError<'a> { + ParseIntError(std::num::ParseIntError), + LineMalformed(&'a str), +} + +impl From for ParseError<'_> { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl Display for ParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct Voxel { + x: i8, + y: i8, + z: i8, +} + +impl<'a> TryFrom<&'a str> for Voxel { + type Error = ParseError<'a>; + + fn try_from(value: &'a str) -> Result { + let components = value.split(',').collect::>(); + if components.len() != 3 { + Err(Self::Error::LineMalformed(value)) + } else { + Ok(Self { + x: components[0].parse()?, + y: components[1].parse()?, + z: components[2].parse()?, + }) + } + } +} + +impl Voxel { + fn neighbours(&self) -> [Self; 6] { + [ + Self { x: self.x-1, y: self.y, z: self.z }, + Self { x: self.x+1, y: self.y, z: self.z }, + Self { x: self.x, y: self.y-1, z: self.z }, + Self { x: self.x, y: self.y+1, z: self.z }, + Self { x: self.x, y: self.y, z: self.z-1 }, + Self { x: self.x, y: self.y, z: self.z+1 }, + ] + } +} + +fn find_total_surface_area(voxels: &BTreeSet) -> usize { + voxels.iter() + .map(|v| 6 - v.neighbours().iter().filter(|n| voxels.contains(n)).count()) + .sum() +} + +fn find_area_reachable_from_origin(voxels: &BTreeSet) -> usize { + let max_x = voxels.last().unwrap().x + 1; + let max_y = voxels.iter().map(|v| v.y).max().unwrap() + 1; + let max_z = voxels.iter().map(|v| v.z).max().unwrap() + 1; + + let mut water = BTreeSet::from([Voxel { x: 0, y: 0, z: 0 }]); + let mut water_last_step = water.clone(); + loop { + let mut water_this_step = BTreeSet::new(); + for droplet in &water_last_step { + for neighbour in droplet.neighbours() { + if !water.contains(&neighbour) && !voxels.contains(&neighbour) && (-1..=max_x).contains(&neighbour.x) && (-1..=max_y).contains(&neighbour.y) && (-1..=max_z).contains(&neighbour.z) { + water_this_step.insert(neighbour); + water.insert(neighbour); + } + } + } + if water_this_step.is_empty() { + break; + } + std::mem::swap(&mut water_this_step, &mut water_last_step); + } + + voxels.iter() + .map(|v| v.neighbours().iter().filter(|n| water.contains(n)).count()) + .sum() +} + +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + let voxels: BTreeSet<_> = input.lines().map(Voxel::try_from).collect::, _>>()?; + let first = find_total_surface_area(&voxels); + let second = find_area_reachable_from_origin(&voxels); + Ok((first, second)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; + + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((64, 58))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((4320, 2456))); + } +} diff --git a/2022/day18-boiling_boulders/src/main.rs b/2022/day18-boiling_boulders/src/main.rs deleted file mode 100644 index b650df3..0000000 --- a/2022/day18-boiling_boulders/src/main.rs +++ /dev/null @@ -1,139 +0,0 @@ -use std::fs; - -#[derive(Clone, PartialEq)] -struct Voxel { - x: i8, - y: i8, - z: i8, -} - -impl Voxel { - fn from(line: &str) -> Self { - let components = line.split(',').collect::>(); - if components.len() != 3 { - panic!("Unable to parse line {} into a Voxel.", line); - } - Self { - x: components[0].parse().unwrap(), - y: components[1].parse().unwrap(), - z: components[2].parse().unwrap(), - } - } -} - -fn find_total_surface_area(voxels: &Vec) -> usize { - let mut total_surface = 0; - for voxel in voxels { - total_surface += 6 - voxels.iter() - .filter(|v| - v.x == voxel.x && v.y == voxel.y && (v.z - voxel.z).abs() == 1 || - v.x == voxel.x && (v.y - voxel.y).abs() == 1 && v.z == voxel.z || - (v.x - voxel.x).abs() == 1 && v.y == voxel.y && v.z == voxel.z - ) - .count() - - } - total_surface -} - -fn find_area_reachable_from_origin(voxels: &Vec) -> usize { - let max_x = voxels.iter().map(|v| v.x).max().unwrap() + 1; - let max_y = voxels.iter().map(|v| v.y).max().unwrap() + 1; - let max_z = voxels.iter().map(|v| v.z).max().unwrap() + 1; - - let mut water = vec![vec![Voxel { x: 0, y: 0, z: 0 }]]; - loop { - let mut water_this_step = Vec::new(); - for water_from_last_step in &water[water.len()-1] { - let existing_water = water.iter().flatten().cloned().collect::>(); - for x_shift in [-1,1] { - let this_dropplet = Voxel { - x: water_from_last_step.x + x_shift, - y: water_from_last_step.y, - z: water_from_last_step.z, - }; - if !existing_water.contains(&this_dropplet) && - !water_this_step.contains(&this_dropplet) && - !voxels.contains(&this_dropplet) && - this_dropplet.x <= max_x && this_dropplet.x >= -1 { - water_this_step.push(this_dropplet); - } - } - for y_shift in [-1,1] { - let this_dropplet = Voxel { - x: water_from_last_step.x, - y: water_from_last_step.y + y_shift, - z: water_from_last_step.z, - }; - if !existing_water.contains(&this_dropplet) && - !water_this_step.contains(&this_dropplet) && - !voxels.contains(&this_dropplet) && - this_dropplet.y <= max_y && this_dropplet.y >= -1 { - water_this_step.push(this_dropplet); - } - } - for z_shift in [-1,1] { - let this_dropplet = Voxel { - x: water_from_last_step.x, - y: water_from_last_step.y, - z: water_from_last_step.z + z_shift, - }; - if !existing_water.contains(&this_dropplet) && - !water_this_step.contains(&this_dropplet) && - !voxels.contains(&this_dropplet) && - this_dropplet.z <= max_z && this_dropplet.z >= -1 { - water_this_step.push(this_dropplet); - } - } - } - if water_this_step.is_empty() { - break; - } - water.push(water_this_step); - } - - let mut total_surface = 0; - for voxel in voxels { - total_surface += water.iter() - .flatten() - .filter(|v| - v.x == voxel.x && v.y == voxel.y && (v.z - voxel.z).abs() == 1 || - v.x == voxel.x && (v.y - voxel.y).abs() == 1 && v.z == voxel.z || - (v.x - voxel.x).abs() == 1 && v.y == voxel.y && v.z == voxel.z - ) - .count() - - } - total_surface -} - -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not found") -} - -fn main() { - let scan = read_file("input"); - let voxels = scan.lines().map(Voxel::from).collect::>(); - - println!("The total surface Area including air pockets is {}.", find_total_surface_area(&voxels)); - println!("The outside surface Area is {}.", find_area_reachable_from_origin(&voxels)); -} - -#[test] -fn sample_input() { - let scan = read_file("tests/sample_input"); - let voxels = scan.lines().map(Voxel::from).collect::>(); - - assert_eq!(find_total_surface_area(&voxels), 64); - assert_eq!(find_area_reachable_from_origin(&voxels), 58); -} - -#[test] -fn challenge_input() { - let scan = read_file("tests/input"); - let voxels = scan.lines().map(Voxel::from).collect::>(); - - assert_eq!(find_total_surface_area(&voxels), 4320); - assert_eq!(find_area_reachable_from_origin(&voxels), 2456); -} diff --git a/2022/day18-boiling_boulders/tests/input b/2022/day18-boiling_boulders/tests/challenge_input similarity index 100% rename from 2022/day18-boiling_boulders/tests/input rename to 2022/day18-boiling_boulders/tests/challenge_input diff --git a/2022/day19-not_enough_minerals/src/lib.rs b/2022/day19-not_enough_minerals/src/lib.rs new file mode 100644 index 0000000..ff6b606 --- /dev/null +++ b/2022/day19-not_enough_minerals/src/lib.rs @@ -0,0 +1,232 @@ +use core::fmt::Display; +use std::num::ParseIntError; +use std::collections::{HashMap, VecDeque}; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError<'a> { + ParseIntError(std::num::ParseIntError), + LineMalformed(&'a str), +} + +impl From for ParseError<'_> { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl Display for ParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"), + } + } +} + +#[derive(Debug)] +struct Blueprint { + id: usize, + ore_robot_cost: u8, + clay_robot_cost: u8, + obsidian_robot_ore_cost: u8, + obsidian_robot_clay_cost: u8, + geode_robot_ore_cost: u8, + geode_robot_obsidian_cost: u8, +} + +impl<'a> TryFrom<&'a str> for Blueprint { + type Error = ParseError<'a>; + + fn try_from(value: &'a str) -> Result { + let components: Vec<&str> = value.split(' ').collect(); + if components.len() != 32 { + return Err(Self::Error::LineMalformed(value)); + } + Ok(Self { + id: components[1][..components[1].len()-1].parse()?, + ore_robot_cost: components[6].parse()?, + clay_robot_cost: components[12].parse()?, + obsidian_robot_ore_cost: components[18].parse()?, + obsidian_robot_clay_cost: components[21].parse()?, + geode_robot_ore_cost: components[27].parse()?, + geode_robot_obsidian_cost: components[30].parse()?, + }) + } +} + +fn more_robots_required(robot_count: u8, stock: u8, max_demand: u8, time_remaining: u8) -> bool { + (robot_count as usize * time_remaining as usize + stock as usize) < (max_demand as usize * time_remaining as usize) +} + +impl Blueprint { + fn collect_geodes(&self, time: u8) -> u8 { + self.try_bfs(time) + } + + fn try_bfs(&self, time: u8) -> u8 { + let mut open_set = VecDeque::from([(Inventory::new(), time)]); + let mut mem = HashMap::new(); + let mut best = 0; + + while let Some((inventory, time_remaining)) = open_set.pop_front() { + if time_remaining == 0 { + best = best.max(inventory.geodes); + } else if let Some(best_time) = mem.get(&inventory.as_arr()) { + if *best_time >= time_remaining { + continue; + } + } else { + mem.insert(inventory.as_arr(), time_remaining); + // let inventory = inventory.collect(); + + // Always buy a Geode Robot if we can afford it and there is at least 1 unit of time remaining + // (so it will produce at least once) + if time_remaining > 1 && inventory.ore >= self.geode_robot_ore_cost && inventory.obsidian >= self.geode_robot_obsidian_cost { + let mut new_inventory = inventory; + new_inventory.collect(); + new_inventory.ore -= self.geode_robot_ore_cost; + new_inventory.obsidian -= self.geode_robot_obsidian_cost; + new_inventory.geode_robots += 1; + open_set.push_back((new_inventory, time_remaining-1)); + } else { + // Save Ressources only if there is any robot we can't afford, but we already produce the + // required ressource, or we are close to the end + if time_remaining < 4 || inventory.ore < *[self.ore_robot_cost, self.clay_robot_cost, self.obsidian_robot_ore_cost, self.geode_robot_ore_cost].iter().max().unwrap() || + inventory.clay < self.obsidian_robot_clay_cost && inventory.clay_robots > 0 || + inventory.obsidian < self.geode_robot_obsidian_cost && inventory.obsidian_robots > 0 + { + let mut new_inventory = inventory; + new_inventory.collect(); + open_set.push_back((new_inventory, time_remaining-1)); + } + // Buy an Ore Robot if + // - we can afford it, and + // - we don't already produce enough Ore for any other Robot each round, and + // - there are at least 2 rounds left (1 to produce and buy a Geode Robot, 1 for that + // to produce). + // if time_remaining > 2 && inventory.ore >= self.ore_robot_cost && inventory.ore_robots < *[self.ore_robot_cost, self.clay_robot_cost, self.obsidian_robot_ore_cost, self.geode_robot_ore_cost].iter().max().unwrap() { + if time_remaining > 2 && inventory.ore >= self.ore_robot_cost && more_robots_required(inventory.ore_robots, inventory.ore, *[self.ore_robot_cost, self.clay_robot_cost, self.obsidian_robot_ore_cost, self.geode_robot_ore_cost].iter().max().unwrap(), time_remaining-1) { + let mut new_inventory = inventory; + new_inventory.collect(); + new_inventory.ore -= self.ore_robot_cost; + new_inventory.ore_robots += 1; + open_set.push_back((new_inventory, time_remaining-1)); + } + // Buy a Clay Robot if + // - we can afford it, and + // - we don't already produce enough Clay for an Obsidian Robot each round, and + // - there are at least 3 rounds left (1 to produce and buy an Obsidian Robot, 1 for that + // to produce and buy a Geode Robot, and 1 for that to produce). + // if time_remaining > 3 && inventory.ore >= self.clay_robot_cost && inventory.clay_robots < self.obsidian_robot_clay_cost { + if time_remaining > 3 && inventory.ore >= self.clay_robot_cost && more_robots_required(inventory.clay_robots, inventory.clay, self.obsidian_robot_clay_cost, time_remaining-1) { + let mut new_inventory = inventory; + new_inventory.collect(); + new_inventory.ore -= self.clay_robot_cost; + new_inventory.clay_robots += 1; + open_set.push_back((new_inventory, time_remaining-1)); + } + // Buy an Obsidian Robot if + // - we can afford it, and + // - we don't already produce enough Obsidian for a Geode Robot each round, and + // - there are at least 2 rounds left (1 to produce and buy a Geode Robot, and 1 for that + // to produce). + // if time_remaining > 2 && inventory.ore >= self.obsidian_robot_ore_cost && inventory.clay >= self.obsidian_robot_clay_cost && inventory.obsidian_robots < self.geode_robot_obsidian_cost { + if time_remaining > 2 && inventory.ore >= self.obsidian_robot_ore_cost && inventory.clay >= self.obsidian_robot_clay_cost && more_robots_required(inventory.obsidian_robots, inventory.obsidian, self.geode_robot_obsidian_cost, time_remaining-1) { + let mut new_inventory = inventory; + new_inventory.collect(); + new_inventory.ore -= self.obsidian_robot_ore_cost; + new_inventory.clay -= self.obsidian_robot_clay_cost; + new_inventory.obsidian_robots += 1; + open_set.push_back((new_inventory, time_remaining-1)); + } + } + } + } + best + } +} + +#[derive(Copy, Clone, Debug)] +struct Inventory { + ore: u8, + clay: u8, + obsidian: u8, + geodes: u8, + ore_robots: u8, + clay_robots: u8, + obsidian_robots: u8, + geode_robots: u8, +} + +impl Inventory { + fn new() -> Self { + Self { + ore: 0, + clay: 0, + obsidian: 0, + geodes: 0, + ore_robots: 1, + clay_robots: 0, + obsidian_robots: 0, + geode_robots: 0, + } + } + + fn collect(&mut self) { + self.ore += self.ore_robots; + self.clay += self.clay_robots; + self.obsidian += self.obsidian_robots; + self.geodes += self.geode_robots; + } + + fn as_arr(&self) -> [u8;8] { + [ + self.ore, + self.clay, + self.obsidian, + self.geodes, + self.ore_robots, + self.clay_robots, + self.obsidian_robots, + self.geode_robots, + ] + } +} + +pub fn run(input: &str) -> Result<(usize, usize), ParseError> { + let blueprints: Vec = input.lines().map(Blueprint::try_from).collect::, _>>()?; + + let first = blueprints.iter() + .map(|blueprint| blueprint.id * blueprint.collect_geodes(24) as usize) + .sum(); + + // let second = 0; + let second = blueprints.iter() + .take(3) + .map(|blueprint| blueprint.collect_geodes(32) as usize) + .product(); + + Ok((first, second)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; + + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((33, 3472))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((978, 15939))); + } +} diff --git a/2022/day19-not_enough_minerals/src/main.rs b/2022/day19-not_enough_minerals/src/main.rs deleted file mode 100644 index d2203c9..0000000 --- a/2022/day19-not_enough_minerals/src/main.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::{fs, collections::HashMap}; - -#[derive(Debug)] -struct Blueprint { - id: usize, - ore_robot_cost: u8, - clay_robot_cost: u8, - obsidian_robot_ore_cost: u8, - obsidian_robot_clay_cost: u8, - geode_robot_ore_cost: u8, - geode_robot_obsidian_cost: u8, -} - -impl Blueprint { - fn parse(line: &str) -> Self { - let components: Vec<&str> = line.split(' ').collect(); - if components.len() != 32 { - panic!("{line} does not have 32 components."); - } - Self { - id: components[1][..components[1].len()-1].parse().unwrap(), - ore_robot_cost: components[6].parse().unwrap(), - clay_robot_cost: components[12].parse().unwrap(), - obsidian_robot_ore_cost: components[18].parse().unwrap(), - obsidian_robot_clay_cost: components[21].parse().unwrap(), - geode_robot_ore_cost: components[27].parse().unwrap(), - geode_robot_obsidian_cost: components[30].parse().unwrap(), - } - } - - fn collect_geodes(&self, time: u8) -> u8 { - let best = self.try_all(&Inventory::new(), time, &mut HashMap::new()); - println!("Best Result for Blueprint {} is {}", self.id, best); - best - } - - fn try_all(&self, inventory: &Inventory, time_remaining: u8, mem: &mut HashMap<[u8;8], u8>) -> u8 { - if let Some(best_time) = mem.get(&inventory.as_arr()) { - if *best_time >= time_remaining && time_remaining > 1 { - return 0; - } - } - mem.insert(inventory.as_arr(), time_remaining); - if time_remaining == 0 { - return inventory.geodes; - } - let mut scores = Vec::new(); - // branch - if time_remaining < 4 || inventory.ore < *[self.ore_robot_cost, self.clay_robot_cost, self.obsidian_robot_ore_cost, self.geode_robot_ore_cost].iter().max().unwrap() || - inventory.clay < self.obsidian_robot_clay_cost && inventory.clay_robots > 0 || - inventory.obsidian < self.geode_robot_obsidian_cost && inventory.obsidian_robots > 0 - { - let mut new_inventory = *inventory; - new_inventory.collect(); - scores.push(self.try_all(&new_inventory, time_remaining-1, mem)); - } - if time_remaining > 2 && inventory.ore >= self.ore_robot_cost && inventory.ore_robots < *[self.ore_robot_cost, self.clay_robot_cost, self.obsidian_robot_ore_cost, self.geode_robot_ore_cost].iter().max().unwrap() { - let mut new_inventory = *inventory; - new_inventory.collect(); - new_inventory.ore -= self.ore_robot_cost; - new_inventory.ore_robots += 1; - scores.push(self.try_all(&new_inventory, time_remaining-1, mem)); - } - if time_remaining > 3 && inventory.ore >= self.clay_robot_cost && inventory.clay_robots < self.obsidian_robot_clay_cost { - let mut new_inventory = *inventory; - new_inventory.collect(); - new_inventory.ore -= self.clay_robot_cost; - new_inventory.clay_robots += 1; - scores.push(self.try_all(&new_inventory, time_remaining-1, mem)); - } - if time_remaining > 2 && inventory.ore >= self.obsidian_robot_ore_cost && inventory.clay >= self.obsidian_robot_clay_cost && inventory.obsidian_robots < self.geode_robot_obsidian_cost { - let mut new_inventory = *inventory; - new_inventory.collect(); - new_inventory.ore -= self.obsidian_robot_ore_cost; - new_inventory.clay -= self.obsidian_robot_clay_cost; - new_inventory.obsidian_robots += 1; - scores.push(self.try_all(&new_inventory, time_remaining-1, mem)); - } - if time_remaining > 1 && inventory.ore >= self.geode_robot_ore_cost && inventory.obsidian >= self.geode_robot_obsidian_cost { - let mut new_inventory = *inventory; - new_inventory.collect(); - new_inventory.ore -= self.geode_robot_ore_cost; - new_inventory.obsidian -= self.geode_robot_obsidian_cost; - new_inventory.geode_robots += 1; - scores.push(self.try_all(&new_inventory, time_remaining-1, mem)); - } - scores.iter().cloned().max().unwrap() - } -} - -#[derive(Copy, Clone, Debug)] -struct Inventory { - ore: u8, - clay: u8, - obsidian: u8, - geodes: u8, - ore_robots: u8, - clay_robots: u8, - obsidian_robots: u8, - geode_robots: u8, -} - -impl Inventory { - fn new() -> Self { - Self { - ore: 0, - clay: 0, - obsidian: 0, - geodes: 0, - ore_robots: 1, - clay_robots: 0, - obsidian_robots: 0, - geode_robots: 0, - } - } - - fn collect(&mut self) { - self.ore += self.ore_robots; - self.clay += self.clay_robots; - self.obsidian += self.obsidian_robots; - self.geodes += self.geode_robots; - } - - fn as_arr(&self) -> [u8;8] { - [ - self.ore, - self.clay, - self.obsidian, - self.geodes, - self.ore_robots, - self.clay_robots, - self.obsidian_robots, - self.geode_robots, - ] - } -} - -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not Found") -} - -fn main() { - let blueprints: Vec = read_file("input").lines().map(Blueprint::parse).collect(); - - let quality_level: usize = blueprints.iter() - .map(|blueprint| blueprint.id * blueprint.collect_geodes(24) as usize) - .sum(); - - println!("The sum of all of our quality levels is {quality_level}"); // should be 33 for the sample_input - - let max_score: usize = blueprints.iter() - .take(3) - .map(|blueprint| blueprint.collect_geodes(32) as usize) - .product(); - - println!("With added time, the remaining blueprints multiply to {max_score}."); -} - -#[test] -fn sample_input() { - let blueprints: Vec<_>= read_file("tests/sample_input").lines().map(Blueprint::parse).collect(); - - let quality_level: usize = blueprints.iter().map(|b| b.id * b.collect_geodes(24) as usize).sum(); - let max_score: usize = blueprints.iter().take(3).map(|b| b.collect_geodes(32) as usize).product(); - assert_eq!(quality_level, 33); - assert_eq!(max_score, 62); -} -#[test] -fn challenge_input() { - let blueprints: Vec<_>= read_file("tests/input").lines().map(Blueprint::parse).collect(); - - let quality_level: usize = blueprints.iter().map(|b| b.id * b.collect_geodes(24) as usize).sum(); - let max_score: usize = blueprints.iter().take(3).map(|b| b.collect_geodes(32) as usize).product(); - assert_eq!(quality_level, 978); - assert_eq!(max_score, 15939); -} diff --git a/2022/day19-not_enough_minerals/tests/input b/2022/day19-not_enough_minerals/tests/challenge_input similarity index 100% rename from 2022/day19-not_enough_minerals/tests/input rename to 2022/day19-not_enough_minerals/tests/challenge_input diff --git a/2022/day20-grove_positioning_system/src/main.rs b/2022/day20-grove_positioning_system/src/lib.rs similarity index 54% rename from 2022/day20-grove_positioning_system/src/main.rs rename to 2022/day20-grove_positioning_system/src/lib.rs index dd03d15..5e1f560 100644 --- a/2022/day20-grove_positioning_system/src/main.rs +++ b/2022/day20-grove_positioning_system/src/lib.rs @@ -1,14 +1,9 @@ -use std::fs; - -fn read_file(path: &str) -> String { - fs::read_to_string(path) - .expect("File not Found") -} +use std::num::ParseIntError; fn get_coordinates(encrypted: &[isize], key: isize, rounds: u8) -> (isize, isize, isize) { let decrypted = shuffle_with_key(encrypted, key, rounds); - let offset = decrypted.iter().position(|&x| x == 0).unwrap() as usize; + let offset = decrypted.iter().position(|&x| x == 0).unwrap(); let c1 = decrypted[(1000 + offset) % decrypted.len()]; let c2 = decrypted[(2000 + offset) % decrypted.len()]; let c3 = decrypted[(3000 + offset) % decrypted.len()]; @@ -43,38 +38,33 @@ fn shuffle_with_key(old: &[isize], key: isize, rounds: u8) -> Vec { .collect() } -fn main() { - let contents = read_file("input"); - - let encrypted: Vec = contents.lines().map(|i| i.parse().unwrap()).collect(); - - let (c1, c2, c3) = get_coordinates(&encrypted, 1, 1); - println!("The relevant numbers are {}, {} and {}, totalling {}.", c1, c2, c3, c1+c2+c3); - - let (d1, d2, d3) = get_coordinates(&encrypted, 811589153, 10); - println!("With Key, the relevant numbers are {}, {} and {}, totalling {}.", d1, d2, d3, d1+d2+d3); -} - -#[test] -fn sample_input() { - let contents = read_file("tests/sample_input"); - let encrypted: Vec = contents.lines().map(|i| i.parse().unwrap()).collect(); - +pub fn run(input: &str) -> Result<(isize, isize), ParseIntError> { + let encrypted: Vec = input.lines().map(|i| i.parse()).collect::, _>>()?; let (c1, c2, c3) = get_coordinates(&encrypted, 1, 1); let (d1, d2, d3) = get_coordinates(&encrypted, 811589153, 10); - - assert_eq!((c1, c2, c3), (4, -3, 2)); - assert_eq!((d1, d2, d3), (811589153, 2434767459, -1623178306)); + let first = c1+c2+c3; + let second = d1+d2+d3; + Ok((first, second)) } -#[test] -fn challenge_input() { - let contents = read_file("tests/input"); - let encrypted: Vec = contents.lines().map(|i| i.parse().unwrap()).collect(); +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; - let (c1, c2, c3) = get_coordinates(&encrypted, 1, 1); - let (d1, d2, d3) = get_coordinates(&encrypted, 811589153, 10); + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } - assert_eq!((c1, c2, c3), (6790, 9749, -8511)); - assert_eq!((d1, d2, d3), (6447264231432, 3708150840057, -1356977063816)); + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((3, 1623178306))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((8028, 8798438007673))); + } } diff --git a/2022/day20-grove_positioning_system/tests/input b/2022/day20-grove_positioning_system/tests/challenge_input similarity index 100% rename from 2022/day20-grove_positioning_system/tests/input rename to 2022/day20-grove_positioning_system/tests/challenge_input diff --git a/2022/day21-monkey_math/day_monkey_math-4ba.core b/2022/day21-monkey_math/day_monkey_math-4ba.core new file mode 100644 index 0000000..e53ba83 Binary files /dev/null and b/2022/day21-monkey_math/day_monkey_math-4ba.core differ diff --git a/2022/day21-monkey_math/src/lib.rs b/2022/day21-monkey_math/src/lib.rs new file mode 100644 index 0000000..8f80930 --- /dev/null +++ b/2022/day21-monkey_math/src/lib.rs @@ -0,0 +1,153 @@ +use core::fmt::Display; +use std::num::ParseIntError; +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Eq)] +pub enum ParseError<'a> { + LineMalformed(&'a str), + MissingRoot, + ParseIntError(std::num::ParseIntError), +} + +impl From for ParseError<'_> { + fn from(value: ParseIntError) -> Self { + Self::ParseIntError(value) + } +} + +impl Display for ParseError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"), + Self::MissingRoot => write!(f, "Input didn't contain a root monkey"), + Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"), + } + } +} + + +#[derive(Clone)] +enum Operator { Add, Sub, Mul, Div } + +#[derive(Clone)] +struct Operation { + left: String, + operator: Operator, + right: String, +} + +impl Operation { + fn from(left: String, operator: String, right: String) -> Self { + Self { + left, + operator: match &operator[..] { + "+" => Operator::Add, + "-" => Operator::Sub, + "*" => Operator::Mul, + "/" => Operator::Div, + _ => panic!("Unknown Operator: {operator}"), + }, + right, + } + } + + fn perform(&self, monkeys: &HashMap) -> f64 { + let left = monkeys.get(&self.left).unwrap().get_number(monkeys); + let right = monkeys.get(&self.right).unwrap().get_number(monkeys); + match self.operator { + Operator::Add => left + right, + Operator::Sub => left - right, + Operator::Mul => left * right, + Operator::Div => left / right, + } + } +} + +#[derive(Clone)] +struct Monkey { + number: Option, + operation: Operation, +} + +impl Monkey { + fn get_number(&self, monkeys: &HashMap) -> f64 { + if let Some(number) = self.number { + number + } else { + self.operation.perform(monkeys) + } + } +} + +fn try_parse(input: &str) -> Result, ParseError> { + let mut monkeys = HashMap::new(); + for line in input.lines() { + let components = line.split(' ').collect::>(); + let name = components[0][..components[0].len()-1].to_string(); + let (number, operation) = match components.len() { + 2 => (Some(components[1].parse().unwrap()), Operation { left: name.to_string(), operator: Operator::Add, right: "none".to_string() }), + 4 => (None, Operation::from(components[1].to_string(), components[2].to_string(), components[3].to_string())), + _ => return Err(ParseError::LineMalformed(line)), + }; + monkeys.insert(name, Monkey { number, operation }); + } + Ok(monkeys) +} + +fn guess_number(left: Monkey, rigth: Monkey, last_guess: f64, monkeys: &mut HashMap) -> isize { + monkeys.insert("humn".to_string(), Monkey { number: Some(last_guess), operation: Operation { left: "none".to_string(), operator: Operator::Add, right: "none".to_string() } }); + let diff0 = rigth.get_number(monkeys) - left.get_number(monkeys); + + if diff0 == 0.0 { + return last_guess as isize; + } + monkeys.insert("humn".to_string(), Monkey { number: Some(last_guess+1.0), operation: Operation { left: "none".to_string(), operator: Operator::Add, right: "none".to_string() } }); + let diff1 = rigth.get_number(monkeys) - left.get_number(monkeys); + + if diff1 == 0.0 { + return last_guess as isize + 1; + } + + if diff0 == diff1 { + return guess_number(left, rigth, last_guess-1.0, monkeys); + } + + let mut next_guess = (last_guess + diff0/(diff0-diff1)).round(); + if next_guess == last_guess { + next_guess -= 1.0; + } + guess_number(left, rigth, next_guess, monkeys) +} + +pub fn run(input: &str) -> Result<(isize, isize), ParseError> { + let mut monkeys = try_parse(input)?; + let root = monkeys.get(&"root".to_string()).ok_or(ParseError::MissingRoot)?; + let rl = monkeys.get(&root.operation.left).unwrap().clone(); + let rr = monkeys.get(&root.operation.right).unwrap().clone(); + + let first = root.get_number(&monkeys) as isize; + let second = guess_number(rl, rr, 0.0, &mut monkeys); + Ok((first, second)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::read_to_string; + + fn read_file(name: &str) -> String { + read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string() + } + + #[test] + fn test_sample() { + let sample_input = read_file("tests/sample_input"); + assert_eq!(run(&sample_input), Ok((152, 301))); + } + + #[test] + fn test_challenge() { + let challenge_input = read_file("tests/challenge_input"); + assert_eq!(run(&challenge_input), Ok((158661812617812, 3352886133831))); + } +} diff --git a/2022/day21-monkey_math/src/main.rs b/2022/day21-monkey_math/src/main.rs deleted file mode 100644 index afab53e..0000000 --- a/2022/day21-monkey_math/src/main.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::{fs, collections::HashMap}; - -#[derive(Clone)] -enum Operator { Add, Sub, Mul, Div } - -#[derive(Clone)] -struct Operation { - left: String, - operator: Operator, - right: String, -} - -impl Operation { - fn from(left: String, operator: String, right: String) -> Self { - Self { - left, - operator: match &operator[..] { - "+" => Operator::Add, - "-" => Operator::Sub, - "*" => Operator::Mul, - "/" => Operator::Div, - _ => panic!("Unknown Operator: {operator}"), - }, - right, - } - } - - fn perform(&self, monkeys: &HashMap) -> isize { - let left = monkeys.get(&self.left).unwrap().get_number(monkeys); - let right = monkeys.get(&self.right).unwrap().get_number(monkeys); - match self.operator { - Operator::Add => left + right, - Operator::Sub => left - right, - Operator::Mul => left * right, - Operator::Div => left / right, - } - } -} - -#[derive(Clone)] -struct Monkey { - number: Option, - operation: Operation, -} - -impl Monkey { - fn get_number(&self, monkeys: &HashMap) -> isize { - if let Some(number) = self.number { - number - } else { - self.operation.perform(monkeys) - } - } -} - -fn read_file(path: &str) -> HashMap { - let mut monkeys = HashMap::new(); - fs::read_to_string(path) - .expect("File not Found") - .lines() - .for_each(|line| { - let components = line.split(' ').collect::>(); - let name = components[0][..components[0].len()-1].to_string(); - let (number, operation) = match components.len() { - 2 => (Some(components[1].parse().unwrap()), Operation { left: name.to_string(), operator: Operator::Add, right: "none".to_string() }), - 4 => (None, Operation::from(components[1].to_string(), components[2].to_string(), components[3].to_string())), - _ => panic!("Unexpected number of components in {line}"), - }; - monkeys.insert(name, Monkey { number, operation }); - }); - monkeys -} - -fn guess_number(left: Monkey, rigth: Monkey, last_guess: isize, monkeys: &mut HashMap) -> isize { - monkeys.insert("humn".to_string(), Monkey { number: Some(last_guess), operation: Operation { left: "none".to_string(), operator: Operator::Add, right: "none".to_string() } }); - let diff0 = rigth.get_number(monkeys) - left.get_number(monkeys); - - if diff0 == 0 { - return last_guess; - } - monkeys.insert("humn".to_string(), Monkey { number: Some(last_guess+1), operation: Operation { left: "none".to_string(), operator: Operator::Add, right: "none".to_string() } }); - let diff1 = rigth.get_number(monkeys) - left.get_number(monkeys); - - if diff1 == 0 { - return last_guess+1; - } - - if diff0 == diff1 { - return guess_number(left, rigth, last_guess-1, monkeys); - } - - let mut next_guess = last_guess + diff0/(diff0-diff1); - if next_guess == last_guess { - next_guess -= 1; - } - guess_number(left, rigth, next_guess, monkeys) -} - -fn main() { - let mut monkeys = read_file("input"); - - let root = monkeys.get(&"root".to_string()).unwrap(); - println!("The root number is {}", root.get_number(&monkeys)); - - let rl = monkeys.get(&root.operation.left).unwrap().clone() ; - let rr = monkeys.get(&root.operation.right).unwrap().clone(); - - println!("You should yell {}", guess_number(rl, rr, 0, &mut monkeys)); -} - -#[test] -fn sample_input() { - let mut monkeys = read_file("tests/sample_input"); - let root = monkeys.get(&"root".to_string()).unwrap(); - - assert_eq!(root.get_number(&monkeys), 152); - assert_eq!(guess_number(monkeys.get(&root.operation.left).unwrap().clone(), monkeys.get(&root.operation.right).unwrap().clone(), 0, &mut monkeys), 301); -} - -#[test] -fn challenge_input() { - let mut monkeys = read_file("tests/input"); - let root = monkeys.get(&"root".to_string()).unwrap(); - - assert_eq!(root.get_number(&monkeys), 158661812617812); - // There are actually multiple solutions for my input. I orginally found 3352886133831 (the smallest of - // them). This algorithm finds 3352886133834 (the largest) if seeded with 0. 3352886133832 is also valid - assert!((3352886133831..=3352886133834).contains(&guess_number(monkeys.get(&root.operation.left).unwrap().clone(), monkeys.get(&root.operation.right).unwrap().clone(), 0, &mut monkeys))); -} diff --git a/2022/day21-monkey_math/tests/input b/2022/day21-monkey_math/tests/challenge_input similarity index 100% rename from 2022/day21-monkey_math/tests/input rename to 2022/day21-monkey_math/tests/challenge_input