diff --git a/.gitignore b/.gitignore index ea8c4bf..5c7a807 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +/real_puzzel_input/* +!/real_puzzel_input/.gitkeep diff --git "a/\\" "b/\\" deleted file mode 100644 index 924ffc4..0000000 --- "a/\\" +++ /dev/null @@ -1,60 +0,0 @@ -use std::{fs, io, path::Path, process::ExitCode}; - -use advent_of_code_2023::{cli::AppCliArguments, solutions}; -use clap::Parser; - -use thiserror::Error; - -fn main() -> ExitCode { - let args = AppCliArguments::parse(); - let solution = solve_given(&args); - match solution { - Ok(found_solution) => { - println!("{}", found_solution); - ExitCode::SUCCESS - } - Err(error) => { - eprintln!("{}", error); - ExitCode::FAILURE - } - } -} - -fn solve_given(args: &AppCliArguments) -> Result { - let all_solutions = solutions::create_solutions(); - - let found_task = { - let day: u32 = args.day().into(); - let task: u32 = args.task().into(); - let found_day = all_solutions - .get(day.saturating_sub(1) as usize) - .ok_or_else(|| CouldNotSolveError::DayNotFound(day))?; - found_day - .get(task.saturating_sub(1) as usize) - .ok_or_else(|| CouldNotSolveError::TaskNotFound { day, task }) - }?; - - let solved = (found_task)(args.input()); - Ok(solved) -} - -fn try_read_from_file_if_demanded(args: &AppCliArguments) -> io::Result { - let content = if args.read_as_file() { - let path = Path::new(args.input()); - let input_as_file = fs::read_to_string(path)?; - input_as_file - } else { - args.input().to_string() - }; - Ok(content) -} - -#[derive(Debug, Clone, Copy, Error)] -enum CouldNotSolveError { - #[error("There is no solution for the day {0}")] - DayNotFound(u32), - #[error("There is not solution for task {task} under the day {day}")] - TaskNotFound { day: u32, task: u32 }, - #[error("Could not read puzzel input from the given file\n {0}")] - CouldNotReadFromFile(#[from] io::Error), -} diff --git a/real_puzzel_input/.gitkeep b/real_puzzel_input/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/solutions.rs b/src/solutions.rs index be2e7a6..120b419 100644 --- a/src/solutions.rs +++ b/src/solutions.rs @@ -2,7 +2,7 @@ pub mod day5; pub fn create_solutions() -> Vec String>> { vec![ - vec![not_implemented_yet], + vec![not_implemented_yet, not_implemented_yet], vec![not_implemented_yet, not_implemented_yet], vec![not_implemented_yet, not_implemented_yet], vec![not_implemented_yet, not_implemented_yet], diff --git a/src/solutions/day5.rs b/src/solutions/day5.rs index a827c29..1e53498 100644 --- a/src/solutions/day5.rs +++ b/src/solutions/day5.rs @@ -1,15 +1,47 @@ use mapping_layer::MappingLayer; +type UnsignedNumber = u64; mod mapping_layer; mod parsing; mod range_mapping; pub fn solve_task_1(input: &str) -> String { - let _parsed = parsing::parse(input); - todo!() + let (seeds, layers) = parsing::parse(input); + let location = get_lowest_locations_from(&seeds, &layers); + location.to_string() } -fn location_of_one_point(seeds: u32, layers: &[MappingLayer]) -> u32 { - todo!() +fn get_lowest_locations_from(seeds: &[UnsignedNumber], layers: &[MappingLayer]) -> UnsignedNumber { + seeds + .into_iter() + .map(|next_seed| location_of_one_point(*next_seed, layers)) + .min() + .unwrap() +} + +fn location_of_one_point(seed: UnsignedNumber, layers: &[MappingLayer]) -> UnsignedNumber { + let mut current_position = seed; + for next_layer in layers { + let ranges = next_layer.ranges(); + let range_to_use = match ranges + .binary_search_by_key(¤t_position, |extract_source| extract_source.source()) + { + Ok(exact) => ranges + .get(exact) + .expect("Exact index can not be out of bounds"), + Err(not_exact) => { + let not_exact = not_exact.clamp(0, ranges.len() - 1); + let source = ranges.get(not_exact).unwrap(); + let source_pos = source.source(); + if current_position > source_pos { + source + } else { + ranges.get(not_exact.saturating_sub(1)).unwrap() + } + } + }; + current_position = range_to_use.map_position(current_position); + } + current_position } #[cfg(test)] @@ -22,8 +54,8 @@ mod testing { // Seed 13, soil 13, fertilizer 52, water 41, light 34, temperature 34, humidity 35, location 35. #[test] fn name() { - fn assert_case(seeds: u32, layer: &[MappingLayer], expected: u32) { - let actual = location_of_one_point(seeds, layer); + fn assert_case(seeds: UnsignedNumber, layer: &[MappingLayer], expected: UnsignedNumber) { + let actual = location_of_one_point(seeds, &layer); assert_eq!(expected, actual, "Given seeds: {}", seeds); } let input = parsing::parse(include_str!("day5/day5_example_input.txt")); diff --git a/src/solutions/day5/mapping_layer.rs b/src/solutions/day5/mapping_layer.rs index a56c20e..1e40c55 100644 --- a/src/solutions/day5/mapping_layer.rs +++ b/src/solutions/day5/mapping_layer.rs @@ -11,9 +11,59 @@ pub struct MappingLayer { impl MappingLayer { pub fn new(label: impl Into, mut ranges: Vec) -> Self { ranges.sort_by_key(|element| element.source()); + let ranges = Self::fill_in_the_gaps(ranges); Self { label: label.into(), ranges, } } + + /// # Expected + /// It is assumed that ranges is sorted by the field `source` + pub fn fill_in_the_gaps(ranges: Vec) -> Vec { + let mut current_source_start = 0; + let mut without_gaps: Vec = Vec::new(); + for next_range in ranges { + let next_source = next_range.source(); + + assert!(current_source_start <= next_source); + if current_source_start == next_source { + current_source_start = next_range.inclusive_end_source().saturating_add(1); + without_gaps.push(next_range); + } else { + let new_range = next_source; + without_gaps.push(RangeMapping::just_with_source( + current_source_start, + new_range, + )); + current_source_start = next_range.inclusive_end_source() + 1; + without_gaps.push(next_range); + } + } + + without_gaps.push(RangeMapping::source_to_max(current_source_start)); + + without_gaps + } + + pub fn ranges(&self) -> &[RangeMapping] { + &self.ranges + } +} + +#[cfg(test)] +mod testing { + use crate::solutions::day5::{mapping_layer::MappingLayer, range_mapping::RangeMapping}; + + #[test] + fn should_fill_in_the_gaps() { + let input = vec![ + RangeMapping::new(45, 81, 19), + RangeMapping::new(64, 68, 13), + RangeMapping::new(77, 45, 23), + RangeMapping::new(110, 55, 20), + ]; + let actual = MappingLayer::fill_in_the_gaps(input); + insta::assert_debug_snapshot!(actual); + } } diff --git a/src/solutions/day5/parsing.rs b/src/solutions/day5/parsing.rs index 9b1db83..730772f 100644 --- a/src/solutions/day5/parsing.rs +++ b/src/solutions/day5/parsing.rs @@ -1,8 +1,8 @@ use crate::{parsing_utils, solutions::day5::range_mapping::RangeMapping}; -use super::mapping_layer::MappingLayer; +use super::{mapping_layer::MappingLayer, UnsignedNumber}; -pub fn parse(input: &str) -> (Vec, Vec) { +pub fn parse(input: &str) -> (Vec, Vec) { fn parse_one_layer(lines: Vec<&str>) -> MappingLayer { const ALREADY_GOT_MAPPING_NAME: usize = 1; let mapping_name = *lines.first().unwrap(); @@ -33,12 +33,13 @@ pub fn parse(input: &str) -> (Vec, Vec) { #[cfg(test)] mod testing { + use crate::solutions::day5::UnsignedNumber; #[test] fn parse_input_day5() { let input = include_str!("day5_example_input.txt"); let actual = super::parse(input); - let expected_seeds: Vec = vec![79, 14, 55, 13]; + let expected_seeds: Vec = vec![79, 14, 55, 13]; assert_eq!(expected_seeds, actual.0); insta::assert_debug_snapshot!(actual.1); } diff --git a/src/solutions/day5/range_mapping.rs b/src/solutions/day5/range_mapping.rs index cd0b505..cefa836 100644 --- a/src/solutions/day5/range_mapping.rs +++ b/src/solutions/day5/range_mapping.rs @@ -1,12 +1,14 @@ -use std::str::FromStr; +use std::{str::FromStr, u32}; use thiserror::Error; -#[derive(Debug, PartialEq, Eq)] +use super::UnsignedNumber; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct RangeMapping { - source: u32, - target: u32, - range: u32, + source: UnsignedNumber, + target: UnsignedNumber, + range: UnsignedNumber, } #[derive(Debug, Error)] @@ -16,13 +18,14 @@ pub enum ParseErrorRangeMapping { #[error("Every item must be a valid unsigned number")] InvalidFormatForNumbers, } + impl FromStr for RangeMapping { type Err = ParseErrorRangeMapping; fn from_str(s: &str) -> Result { let mut numbers_as_3 = s.split(" ").take(3).map(|unparsed| { unparsed - .parse::() + .parse::() .map_err(|_| ParseErrorRangeMapping::InvalidFormatForNumbers) }); let (target, source, range) = ( @@ -46,20 +49,72 @@ impl FromStr for RangeMapping { } impl RangeMapping { - pub fn source(&self) -> u32 { + pub fn new(source: UnsignedNumber, target: UnsignedNumber, range: UnsignedNumber) -> Self { + Self { + source, + target, + range, + } + } + + pub fn just_with_source(source: UnsignedNumber, range: UnsignedNumber) -> Self { + Self { + source, + target: source, + range, + } + } + + pub fn source_to_max(source: UnsignedNumber) -> Self { + const ENSURES_SOURCE_END_IS_MAX: UnsignedNumber = 1; + let range = if source == 0 { + UnsignedNumber::MAX + } else { + UnsignedNumber::MAX - (source - ENSURES_SOURCE_END_IS_MAX) + }; + + Self::just_with_source(source, range) + } + + pub fn source(&self) -> UnsignedNumber { self.source } - pub fn target(&self) -> u32 { + pub fn inclusive_end_source(&self) -> UnsignedNumber { + if self.range() == UnsignedNumber::MAX { + UnsignedNumber::MAX + } else { + self.source() + (self.range() - 1) + } + } + + pub fn map_position(&self, position: UnsignedNumber) -> UnsignedNumber { + let inclusive_end_source = self.inclusive_end_source(); + if self.source() > position || position > inclusive_end_source { + panic!( + "Given position ({}) is outside of this range between ({}) and ({})", + position, + self.source(), + inclusive_end_source + ); + } + let difference = position - self.source(); + self.target() + difference + } + + pub fn target(&self) -> UnsignedNumber { self.target } - pub fn range(&self) -> u32 { + pub fn range(&self) -> UnsignedNumber { self.range } } + #[cfg(test)] mod testing { + use std::u32; + use super::*; #[test] @@ -73,4 +128,29 @@ mod testing { let actual: RangeMapping = INPUT.parse().unwrap(); assert_eq!(expected, actual); } + + #[test] + fn calc_inclusive_end_source() { + fn assert_case(given: RangeMapping, expected: UnsignedNumber) { + let actual = given.inclusive_end_source(); + assert_eq!(expected, actual, "Given: {:#?}", given); + } + assert_case(RangeMapping::new(98, 50, 2), 99); + assert_case(RangeMapping::new(0, 50, 1), 0); + assert_case(RangeMapping::new(10, 50, 5), 14); + } + + #[test] + fn create_from_source_to_max_integer_max_correctly() { + fn assert_case(input: RangeMapping) { + const EXPECTED: UnsignedNumber = UnsignedNumber::MAX; + let actual = input.inclusive_end_source(); + assert_eq!(EXPECTED, actual, "Given: {:#?}", input); + } + assert_case(RangeMapping::source_to_max(UnsignedNumber::MAX)); + assert_case(RangeMapping::source_to_max(UnsignedNumber::MAX - 1)); + assert_case(RangeMapping::source_to_max(100)); + assert_case(RangeMapping::source_to_max(2)); + assert_case(RangeMapping::source_to_max(0)); + } } diff --git a/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__mapping_layer__testing__should_fill_in_the_gaps.snap b/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__mapping_layer__testing__should_fill_in_the_gaps.snap new file mode 100644 index 0000000..cb2a2cf --- /dev/null +++ b/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__mapping_layer__testing__should_fill_in_the_gaps.snap @@ -0,0 +1,41 @@ +--- +source: src/solutions/day5/mapping_layer.rs +expression: actual +--- +[ + RangeMapping { + source: 0, + target: 0, + range: 45, + }, + RangeMapping { + source: 45, + target: 81, + range: 19, + }, + RangeMapping { + source: 64, + target: 68, + range: 13, + }, + RangeMapping { + source: 77, + target: 45, + range: 23, + }, + RangeMapping { + source: 100, + target: 100, + range: 110, + }, + RangeMapping { + source: 110, + target: 55, + range: 20, + }, + RangeMapping { + source: 130, + target: 130, + range: 18446744073709551486, + }, +] diff --git a/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__parsing__testing__parse_input_day5.snap b/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__parsing__testing__parse_input_day5.snap index aefb441..01c324f 100644 --- a/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__parsing__testing__parse_input_day5.snap +++ b/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__parsing__testing__parse_input_day5.snap @@ -6,6 +6,11 @@ expression: actual.1 MappingLayer { label: "seed-to-soil map:", ranges: [ + RangeMapping { + source: 0, + target: 0, + range: 50, + }, RangeMapping { source: 50, target: 52, @@ -16,6 +21,11 @@ expression: actual.1 target: 50, range: 2, }, + RangeMapping { + source: 100, + target: 100, + range: 18446744073709551516, + }, ], }, MappingLayer { @@ -36,6 +46,11 @@ expression: actual.1 target: 37, range: 2, }, + RangeMapping { + source: 54, + target: 54, + range: 18446744073709551562, + }, ], }, MappingLayer { @@ -61,11 +76,21 @@ expression: actual.1 target: 49, range: 8, }, + RangeMapping { + source: 61, + target: 61, + range: 18446744073709551555, + }, ], }, MappingLayer { label: "water-to-light map:", ranges: [ + RangeMapping { + source: 0, + target: 0, + range: 18, + }, RangeMapping { source: 18, target: 88, @@ -76,11 +101,21 @@ expression: actual.1 target: 18, range: 70, }, + RangeMapping { + source: 95, + target: 95, + range: 18446744073709551521, + }, ], }, MappingLayer { label: "light-to-temperature map:", ranges: [ + RangeMapping { + source: 0, + target: 0, + range: 45, + }, RangeMapping { source: 45, target: 81, @@ -96,6 +131,11 @@ expression: actual.1 target: 45, range: 23, }, + RangeMapping { + source: 100, + target: 100, + range: 18446744073709551516, + }, ], }, MappingLayer { @@ -111,11 +151,21 @@ expression: actual.1 target: 0, range: 1, }, + RangeMapping { + source: 70, + target: 70, + range: 18446744073709551546, + }, ], }, MappingLayer { label: "humidity-to-location map:", ranges: [ + RangeMapping { + source: 0, + target: 0, + range: 56, + }, RangeMapping { source: 56, target: 60, @@ -126,6 +176,11 @@ expression: actual.1 target: 56, range: 4, }, + RangeMapping { + source: 97, + target: 97, + range: 18446744073709551519, + }, ], }, ]