From cb7e8465a2342048f0d10c73834dfa3bb6a7e03f Mon Sep 17 00:00:00 2001 From: BoolPurist Date: Fri, 16 Aug 2024 16:36:56 +0200 Subject: [PATCH] Implemented input as direct value or as file input Implemented parsing for day5 --- Cargo.lock | 55 ++++++++ Cargo.toml | 3 + "\\" | 60 ++++++++ file_input/.gitkeep | 0 file_input/day5_example_input.txt | 33 +++++ src/cli.rs | 4 + src/lib.rs | 1 + src/main.rs | 28 +++- src/parsing_utils.rs | 56 ++++++++ src/solutions/day5.rs | 31 ++++- src/solutions/day5/day5_example_input.txt | 33 +++++ src/solutions/day5/mapping_layer.rs | 19 +++ src/solutions/day5/parsing.rs | 45 ++++++ src/solutions/day5/range_mapping.rs | 76 ++++++++++ ...5__parsing__testing__parse_input_day5.snap | 131 ++++++++++++++++++ 15 files changed, 567 insertions(+), 8 deletions(-) create mode 100644 "\\" create mode 100644 file_input/.gitkeep create mode 100644 file_input/day5_example_input.txt create mode 100644 src/parsing_utils.rs create mode 100644 src/solutions/day5/day5_example_input.txt create mode 100644 src/solutions/day5/mapping_layer.rs create mode 100644 src/solutions/day5/parsing.rs create mode 100644 src/solutions/day5/range_mapping.rs create mode 100644 src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__parsing__testing__parse_input_day5.snap diff --git a/Cargo.lock b/Cargo.lock index 6824097..2843f2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "clap", "derive_more", "env_logger", + "insta", "log", "regex", "thiserror", @@ -118,6 +119,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "windows-sys", +] + [[package]] name = "convert_case" version = "0.6.0" @@ -149,6 +162,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "env_filter" version = "0.1.2" @@ -184,12 +203,42 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "insta" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "810ae6042d48e2c9e9215043563a58a80b877bc863228a74cf10c49d4620a6f5" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.156" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "log" version = "0.4.22" @@ -249,6 +298,12 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 2e3b037..8c75ded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,6 @@ clap = { version = "4.5.15", features = ["derive"] } env_logger = "0.11.5" log = "0.4.22" thiserror = "1.0.63" + +[dev-dependencies] +insta = "1.39.0" diff --git "a/\\" "b/\\" new file mode 100644 index 0000000..924ffc4 --- /dev/null +++ "b/\\" @@ -0,0 +1,60 @@ +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/file_input/.gitkeep b/file_input/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/file_input/day5_example_input.txt b/file_input/day5_example_input.txt new file mode 100644 index 0000000..f756727 --- /dev/null +++ b/file_input/day5_example_input.txt @@ -0,0 +1,33 @@ +seeds: 79 14 55 13 + +seed-to-soil map: +50 98 2 +52 50 48 + +soil-to-fertilizer map: +0 15 37 +37 52 2 +39 0 15 + +fertilizer-to-water map: +49 53 8 +0 11 42 +42 0 7 +57 7 4 + +water-to-light map: +88 18 7 +18 25 70 + +light-to-temperature map: +45 77 23 +81 45 19 +68 64 13 + +temperature-to-humidity map: +0 69 1 +1 0 69 + +humidity-to-location map: +60 56 37 +56 93 4 diff --git a/src/cli.rs b/src/cli.rs index af8e1bb..0d067d5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -28,4 +28,8 @@ impl AppCliArguments { pub fn input(&self) -> &str { &self.input } + + pub fn read_as_file(&self) -> bool { + self.read_as_file + } } diff --git a/src/lib.rs b/src/lib.rs index 5551262..b531276 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ pub mod cli; pub mod constants; +pub mod parsing_utils; pub mod solutions; diff --git a/src/main.rs b/src/main.rs index 9f9f381..4276aa2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::process::ExitCode; +use std::{fs, io, path::Path, process::ExitCode}; use advent_of_code_2023::{cli::AppCliArguments, solutions}; use clap::Parser; @@ -20,7 +20,7 @@ fn main() -> ExitCode { } } -fn solve_given(args: &AppCliArguments) -> Result { +fn solve_given(args: &AppCliArguments) -> Result { let all_solutions = solutions::create_solutions(); let found_task = { @@ -28,20 +28,34 @@ fn solve_given(args: &AppCliArguments) -> Result { let task: u32 = args.task().into(); let found_day = all_solutions .get(day.saturating_sub(1) as usize) - .ok_or_else(|| NoSolutionFound::DayNotFound(day))?; + .ok_or_else(|| CouldNotSolveError::DayNotFound(day))?; found_day .get(task.saturating_sub(1) as usize) - .ok_or_else(|| NoSolutionFound::TaskNotFound { day, task }) + .ok_or_else(|| CouldNotSolveError::TaskNotFound { day, task }) }?; - let solved = (found_task)(args.input()); + let puzzel_input = try_read_from_file_if_demanded(args)?; + let solved = (found_task)(&puzzel_input); Ok(solved) } -#[derive(Debug, Clone, Copy, Error)] -enum NoSolutionFound { +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, 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/src/parsing_utils.rs b/src/parsing_utils.rs new file mode 100644 index 0000000..dccef24 --- /dev/null +++ b/src/parsing_utils.rs @@ -0,0 +1,56 @@ +pub fn blocks_of_lines_seperated_by<'a>( + input: &'a str, + on_skip: impl Fn(&'a str) -> bool, +) -> impl Iterator> { + let mut current_block = Vec::new(); + let mut lines = input.lines(); + std::iter::from_fn(move || { + while let Some(next_line) = lines.next() { + if on_skip(next_line) { + if !current_block.is_empty() { + let to_return = std::mem::take(&mut current_block); + return Some(to_return); + } + } else { + current_block.push(next_line); + } + } + + if !current_block.is_empty() { + let to_return = std::mem::take(&mut current_block); + return Some(to_return); + } + None + }) +} +pub fn blocks_of_lines_seperated_by_empty_lines<'a>( + input: &'a str, +) -> impl Iterator> { + blocks_of_lines_seperated_by(input, |line| line.trim().is_empty()) +} + +#[cfg(test)] +mod testing { + use std::vec; + + use super::*; + + #[test] + fn split_blocks_by_empty_lines() { + const INPUT: &str = " +1st +2st + + +3st + +4st + +5st +"; + let expected = vec![vec!["1st", "2st"], vec!["3st"], vec!["4st"], vec!["5st"]]; + let actual: Vec> = + blocks_of_lines_seperated_by(INPUT, |line| line.trim().is_empty()).collect(); + assert_eq!(expected, actual); + } +} diff --git a/src/solutions/day5.rs b/src/solutions/day5.rs index d6cbcfc..a827c29 100644 --- a/src/solutions/day5.rs +++ b/src/solutions/day5.rs @@ -1,7 +1,36 @@ +use mapping_layer::MappingLayer; + +mod mapping_layer; +mod parsing; +mod range_mapping; pub fn solve_task_1(input: &str) -> String { + let _parsed = parsing::parse(input); todo!() } -fn parse(input: &str) -> ! { +fn location_of_one_point(seeds: u32, layers: &[MappingLayer]) -> u32 { todo!() } + +#[cfg(test)] +mod testing { + use super::*; + + // Seed 79, soil 81, fertilizer 81, water 81, light 74, temperature 78, humidity 78, location 82. + // Seed 14, soil 14, fertilizer 53, water 49, light 42, temperature 42, humidity 43, location 43. + // Seed 55, soil 57, fertilizer 57, water 53, light 46, temperature 82, humidity 82, location 86. + // 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); + assert_eq!(expected, actual, "Given seeds: {}", seeds); + } + let input = parsing::parse(include_str!("day5/day5_example_input.txt")); + let layers = &input.1; + assert_case(79, layers, 82); + assert_case(14, layers, 43); + assert_case(55, layers, 86); + assert_case(13, layers, 35); + } +} diff --git a/src/solutions/day5/day5_example_input.txt b/src/solutions/day5/day5_example_input.txt new file mode 100644 index 0000000..f756727 --- /dev/null +++ b/src/solutions/day5/day5_example_input.txt @@ -0,0 +1,33 @@ +seeds: 79 14 55 13 + +seed-to-soil map: +50 98 2 +52 50 48 + +soil-to-fertilizer map: +0 15 37 +37 52 2 +39 0 15 + +fertilizer-to-water map: +49 53 8 +0 11 42 +42 0 7 +57 7 4 + +water-to-light map: +88 18 7 +18 25 70 + +light-to-temperature map: +45 77 23 +81 45 19 +68 64 13 + +temperature-to-humidity map: +0 69 1 +1 0 69 + +humidity-to-location map: +60 56 37 +56 93 4 diff --git a/src/solutions/day5/mapping_layer.rs b/src/solutions/day5/mapping_layer.rs new file mode 100644 index 0000000..a56c20e --- /dev/null +++ b/src/solutions/day5/mapping_layer.rs @@ -0,0 +1,19 @@ +use derive_more::derive; + +use super::range_mapping::RangeMapping; + +#[derive(Debug)] +pub struct MappingLayer { + label: String, + ranges: Vec, +} + +impl MappingLayer { + pub fn new(label: impl Into, mut ranges: Vec) -> Self { + ranges.sort_by_key(|element| element.source()); + Self { + label: label.into(), + ranges, + } + } +} diff --git a/src/solutions/day5/parsing.rs b/src/solutions/day5/parsing.rs new file mode 100644 index 0000000..9b1db83 --- /dev/null +++ b/src/solutions/day5/parsing.rs @@ -0,0 +1,45 @@ +use crate::{parsing_utils, solutions::day5::range_mapping::RangeMapping}; + +use super::mapping_layer::MappingLayer; + +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(); + let mut mappings = Vec::new(); + + for next_line in lines.into_iter().skip(ALREADY_GOT_MAPPING_NAME) { + let next_mapping: RangeMapping = next_line.parse().unwrap(); + mappings.push(next_mapping); + } + + MappingLayer::new(mapping_name.to_string(), mappings) + } + let mut blocks = parsing_utils::blocks_of_lines_seperated_by_empty_lines(input); + let first_block = blocks.next().unwrap(); + + assert!(first_block.len() == 1); + let seeds = first_block + .first() + .unwrap() + .trim_start_matches("seeds: ") + .split(" ") + .map(|to_number| to_number.parse().unwrap()) + .collect(); + let mappings = blocks.into_iter().map(parse_one_layer).collect(); + + (seeds, mappings) +} + +#[cfg(test)] +mod testing { + + #[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]; + 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 new file mode 100644 index 0000000..cd0b505 --- /dev/null +++ b/src/solutions/day5/range_mapping.rs @@ -0,0 +1,76 @@ +use std::str::FromStr; + +use thiserror::Error; + +#[derive(Debug, PartialEq, Eq)] +pub struct RangeMapping { + source: u32, + target: u32, + range: u32, +} + +#[derive(Debug, Error)] +pub enum ParseErrorRangeMapping { + #[error("Needs to have at least 3 numbers")] + LessThan3Numbers, + #[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::() + .map_err(|_| ParseErrorRangeMapping::InvalidFormatForNumbers) + }); + let (target, source, range) = ( + numbers_as_3 + .next() + .ok_or(ParseErrorRangeMapping::LessThan3Numbers)??, + numbers_as_3 + .next() + .ok_or(ParseErrorRangeMapping::LessThan3Numbers)??, + numbers_as_3 + .next() + .ok_or(ParseErrorRangeMapping::LessThan3Numbers)??, + ); + + Ok(Self { + target, + source, + range, + }) + } +} + +impl RangeMapping { + pub fn source(&self) -> u32 { + self.source + } + + pub fn target(&self) -> u32 { + self.target + } + + pub fn range(&self) -> u32 { + self.range + } +} +#[cfg(test)] +mod testing { + use super::*; + + #[test] + fn parse_from_line() { + const INPUT: &str = "50 98 2"; + let expected = RangeMapping { + source: 98, + target: 50, + range: 2, + }; + let actual: RangeMapping = INPUT.parse().unwrap(); + assert_eq!(expected, actual); + } +} 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 new file mode 100644 index 0000000..aefb441 --- /dev/null +++ b/src/solutions/day5/snapshots/advent_of_code_2023__solutions__day5__parsing__testing__parse_input_day5.snap @@ -0,0 +1,131 @@ +--- +source: src/solutions/day5/parsing.rs +expression: actual.1 +--- +[ + MappingLayer { + label: "seed-to-soil map:", + ranges: [ + RangeMapping { + source: 50, + target: 52, + range: 48, + }, + RangeMapping { + source: 98, + target: 50, + range: 2, + }, + ], + }, + MappingLayer { + label: "soil-to-fertilizer map:", + ranges: [ + RangeMapping { + source: 0, + target: 39, + range: 15, + }, + RangeMapping { + source: 15, + target: 0, + range: 37, + }, + RangeMapping { + source: 52, + target: 37, + range: 2, + }, + ], + }, + MappingLayer { + label: "fertilizer-to-water map:", + ranges: [ + RangeMapping { + source: 0, + target: 42, + range: 7, + }, + RangeMapping { + source: 7, + target: 57, + range: 4, + }, + RangeMapping { + source: 11, + target: 0, + range: 42, + }, + RangeMapping { + source: 53, + target: 49, + range: 8, + }, + ], + }, + MappingLayer { + label: "water-to-light map:", + ranges: [ + RangeMapping { + source: 18, + target: 88, + range: 7, + }, + RangeMapping { + source: 25, + target: 18, + range: 70, + }, + ], + }, + MappingLayer { + label: "light-to-temperature map:", + ranges: [ + RangeMapping { + source: 45, + target: 81, + range: 19, + }, + RangeMapping { + source: 64, + target: 68, + range: 13, + }, + RangeMapping { + source: 77, + target: 45, + range: 23, + }, + ], + }, + MappingLayer { + label: "temperature-to-humidity map:", + ranges: [ + RangeMapping { + source: 0, + target: 1, + range: 69, + }, + RangeMapping { + source: 69, + target: 0, + range: 1, + }, + ], + }, + MappingLayer { + label: "humidity-to-location map:", + ranges: [ + RangeMapping { + source: 56, + target: 60, + range: 37, + }, + RangeMapping { + source: 93, + target: 56, + range: 4, + }, + ], + }, +]