diff --git a/day8/Cargo.lock b/day8/Cargo.lock index b62c9ab..d4591a5 100644 --- a/day8/Cargo.lock +++ b/day8/Cargo.lock @@ -8,6 +8,7 @@ version = "0.1.0" dependencies = [ "itertools", "nom", + "thiserror", ] [[package]] @@ -48,6 +49,61 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + [[package]] name = "version_check" version = "0.9.3" diff --git a/day8/Cargo.toml b/day8/Cargo.toml index 71ea2b3..69b6d82 100644 --- a/day8/Cargo.toml +++ b/day8/Cargo.toml @@ -8,3 +8,4 @@ edition = "2021" [dependencies] nom = "7.1" itertools = "0.10" +thiserror = "1.0" diff --git a/day8/src/main.rs b/day8/src/main.rs index 97a9359..f1d6ba9 100644 --- a/day8/src/main.rs +++ b/day8/src/main.rs @@ -1,4 +1,5 @@ -use std::collections::HashMap; +#![warn(clippy::all, clippy::pedantic)] +use std::collections::{HashMap, HashSet}; use std::env; use std::fs::File; use std::io::{BufRead, BufReader}; @@ -13,15 +14,140 @@ use nom::{ }; use itertools::Itertools; +use thiserror::Error; -#[derive(Debug)] +const SEGMENT_CHARS: &[char] = &['a', 'b', 'c', 'd', 'e', 'f', 'g']; + +#[derive(Debug, Clone)] struct SignalInfo { signal_patterns: Vec, output_values: Vec, } +struct SevenSegmentSignals { + top: char, + top_right: char, + bottom_right: char, + bottom: char, + bottom_left: char, + top_left: char, + middle: char, +} + +#[derive(Error, Debug)] +enum DecodeError { + #[error("Invalid configuraton string given: {0}")] + InavlidConfiguration(String), +} + +impl SevenSegmentSignals { + fn decode_str(&self, s: &str) -> Result { + // Could these be members? yes. Are they? no. + let zero = Self::make_segment_str(&[ + self.top, + self.top_left, + self.bottom_left, + self.bottom, + self.bottom_right, + self.top_right, + ]); + + let one = Self::make_segment_str(&[self.top_right, self.bottom_right]); + + let two = Self::make_segment_str(&[ + self.top, + self.top_right, + self.middle, + self.bottom_left, + self.bottom, + ]); + + let three = Self::make_segment_str(&[ + self.top, + self.top_right, + self.middle, + self.bottom_right, + self.bottom, + ]); + + let four = Self::make_segment_str(&[ + self.top_left, + self.middle, + self.top_right, + self.bottom_right, + ]); + + let five = Self::make_segment_str(&[ + self.top, + self.top_left, + self.middle, + self.bottom_right, + self.bottom, + ]); + + let six = Self::make_segment_str(&[ + self.top, + self.top_left, + self.middle, + self.bottom_left, + self.bottom, + self.bottom_right, + ]); + + let seven = Self::make_segment_str(&[self.top, self.top_right, self.bottom_right]); + + let eight = Self::make_segment_str(&[ + self.top, + self.top_left, + self.bottom_left, + self.middle, + self.bottom, + self.bottom_right, + self.top_right, + ]); + + let nine = Self::make_segment_str(&[ + self.top, + self.top_left, + self.middle, + self.bottom, + self.bottom_right, + self.top_right, + ]); + + let sorted_str = s.chars().sorted().collect::(); + if sorted_str == one { + Ok(1) + } else if sorted_str == two { + Ok(2) + } else if sorted_str == three { + Ok(3) + } else if sorted_str == four { + Ok(4) + } else if sorted_str == five { + Ok(5) + } else if sorted_str == six { + Ok(6) + } else if sorted_str == seven { + Ok(7) + } else if sorted_str == eight { + Ok(8) + } else if sorted_str == nine { + Ok(9) + } else if sorted_str == zero { + Ok(0) + } else { + Err(DecodeError::InavlidConfiguration(s.to_string())) + } + } + + fn make_segment_str(segments: &[char]) -> String { + segments.iter().sorted().collect::() + } +} + fn is_segment_char(c: char) -> bool { - ('a'..='g').contains(&c) + SEGMENT_CHARS.contains(&c) } fn parse_signal_block(chunk: &str) -> IResult<&str, &str> { @@ -39,20 +165,15 @@ fn parse_line(line: &str) -> IResult<&str, SignalInfo> { )(line)?; let parsed_info = SignalInfo { - signal_patterns: raw_signal_patterns - .into_iter() - .map(|s| s.to_owned()) - .collect(), - output_values: raw_output_values - .into_iter() - .map(|s| s.to_owned()) - .collect(), + signal_patterns: raw_signal_patterns.into_iter().map(str::to_owned).collect(), + output_values: raw_output_values.into_iter().map(str::to_owned).collect(), }; Ok(("", parsed_info)) } -fn determine_signal_mapping(signal_patterns: &[String]) -> HashMap { +/// Find the signal mappings that can be easily known by their number of segments +fn determine_simple_signal_mappings(signal_patterns: &[String]) -> HashMap { signal_patterns .iter() .filter_map(|signal_pattern| { @@ -68,11 +189,18 @@ fn determine_signal_mapping(signal_patterns: &[String]) -> HashMap { .collect() } +fn transpose_signal_map(original: &HashMap) -> HashMap { + original + .iter() + .map(|(s, &count)| (count, s.clone())) + .collect() +} + fn part1(signal_infos: &[SignalInfo]) -> usize { signal_infos .iter() .map(|signal_info| { - let signal_mapping = determine_signal_mapping(&signal_info.signal_patterns); + let signal_mapping = determine_simple_signal_mappings(&signal_info.signal_patterns); signal_info .output_values .iter() @@ -85,6 +213,182 @@ fn part1(signal_infos: &[SignalInfo]) -> usize { .sum() } +fn make_char_set(s: &str) -> HashSet { + s.chars().collect() +} + +// Infer all of the segements from a signal info +// This is long and makes many assertions. I don't have the patience to clean it up at this moment. +// If I was feeling less lazy, I'd turn these assertions into Results on the error type. Maybe later. +// +// This is just advent of code after all :) +#[allow(clippy::too_many_lines)] +fn infer_segments(signal_info: &SignalInfo) -> SevenSegmentSignals { + let signal_mapping = determine_simple_signal_mappings(&signal_info.signal_patterns); + let num_to_signal_map = transpose_signal_map(&signal_mapping); + + let one_signals = make_char_set( + num_to_signal_map + .get(&1) + .expect("no mapping for 1 was determined"), + ); + let seven_signals = make_char_set( + num_to_signal_map + .get(&7) + .expect("no mapping for 7 was determined"), + ); + let seven_one_difference = seven_signals + .difference(&one_signals) + .collect::>(); + assert_eq!(seven_one_difference.len(), 1); + + let top_segment = **seven_one_difference.iter().next().unwrap(); + println!("top => {}", top_segment); + + let four_signals = make_char_set( + num_to_signal_map + .get(&4) + .expect("no mapping for 4 was determined"), + ); + + // This will have the two segments that don't have the right "stick" of the four. + let four_one_difference = four_signals + .difference(&one_signals) + .copied() + .collect::>(); + assert_eq!(four_one_difference.len(), 2); + + // There are three items that use six segments: 0 and 6, 9. Only zero matches only the lefthand "prong" of the four, + // so the one with one intersection will disambiguate that one + let six_element_char_sets = signal_info + .signal_patterns + .iter() + .filter(|s| s.len() == 6) + .map(|s| make_char_set(s)) + .collect::>(); + assert_eq!(six_element_char_sets.len(), 3); + + let one_element_left_from_six_set = six_element_char_sets + .iter() + .map(|set| { + four_one_difference + .difference(set) + .copied() + .collect::>() + }) + .filter(|set| set.len() == 1) + .collect::>(); + assert_eq!(one_element_left_from_six_set.len(), 1); + let middle_segment_set = &one_element_left_from_six_set[0]; + assert_eq!(middle_segment_set.len(), 1); + let middle_segment = *middle_segment_set.iter().next().unwrap(); + println!("middle => {}", middle_segment); + + // Now that we know the middle, the only element left in the original "four one difference" set will be the top left + let top_left_segment_set = four_one_difference + .iter() + .filter(|&&c| c != middle_segment) + .collect::>(); + assert_eq!(top_left_segment_set.len(), 1); + let top_left_segment = *top_left_segment_set[0]; + println!("top left => {}", top_left_segment); + + // Finding the top right segment is pretty easy. If we consider the segments of the "one", there is only one + // six-element segment which these sets is not a super-set of the "one": the top right. + let five_signals_set = six_element_char_sets + .iter() + .filter(|set| !set.is_superset(&one_signals)) + .collect::>(); + assert_eq!(five_signals_set.len(), 1); + + let five_signals = five_signals_set[0]; + let top_right_difference = one_signals.difference(five_signals).collect::>(); + assert_eq!(top_right_difference.len(), 1); + let top_right_segment = **top_right_difference.iter().next().unwrap(); + println!("top right => {}", top_right_segment); + + // And of course, knowing the top right, we know the bottom right, given there's only one other element in the one. + let bottom_right_set = one_signals + .iter() + .filter(|&&c| c != top_right_segment) + .collect::>(); + let bottom_right_segment = **bottom_right_set.iter().next().unwrap(); + println!("bottom right => {}", bottom_right_segment); + + // Now that we know the bottom and top left, of the six signal elements, we can uniquely identify the nine. + // Of our possible input signals, the only one it _doesn't_ have must be the bottom left. + let nine_char_set_set = six_element_char_sets + .iter() + .filter(|set| set.contains(&top_right_segment) && set.contains(&middle_segment)) + .collect::>(); + assert_eq!(nine_char_set_set.len(), 1); + + let nine_char_set = nine_char_set_set[0]; + let segment_chars_set = SEGMENT_CHARS.iter().copied().collect::>(); + + let bottom_left_set = segment_chars_set + .difference(nine_char_set) + .collect::>(); + assert_eq!(bottom_left_set.len(), 1); + let bottom_left_segment = **bottom_left_set.iter().next().unwrap(); + println!("bottom_left => {}", bottom_left_segment); + + // aaaand all that's left is the bottom + let all_but_bottom = vec![ + top_segment, + top_right_segment, + bottom_right_segment, + bottom_left_segment, + top_left_segment, + middle_segment, + ] + .into_iter() + .collect::>(); + + let bottom_set = segment_chars_set + .difference(&all_but_bottom) + .collect::>(); + assert_eq!(bottom_set.len(), 1); + + let bottom_segment = **bottom_set.iter().next().unwrap(); + + println!("bottom => {}", bottom_segment); + + SevenSegmentSignals { + top: top_segment, + top_right: top_right_segment, + bottom_right: bottom_right_segment, + bottom: bottom_segment, + bottom_left: bottom_left_segment, + top_left: top_left_segment, + middle: middle_segment, + } +} + +fn part2(signal_infos: &[SignalInfo]) -> u32 { + signal_infos + .iter() + .enumerate() + .map(|(i, signal_info)| { + println!("Item {}", i + 1); + let segments = infer_segments(signal_info); + let res = signal_info + .output_values + .iter() + .map(|output| { + segments + .decode_str(output) + .unwrap_or_else(|err| panic!("Failed to decode {}: {:?}", output, err)) + .into() + }) + .fold(0_u32, |total, n: u32| (total * 10) + n); + + println!("{}\n", res); + res + }) + .sum() +} + fn main() { let input_file_name = env::args().nth(1).expect("No input filename specified"); let input_file = File::open(input_file_name).expect("Could not open input file"); @@ -98,6 +402,6 @@ fn main() { }) .collect::>(); - // println!("{:?}", input_lines); println!("Part 1: {}", part1(&input_lines)); + println!("Part 2: {}", part2(&input_lines)); }