diff --git a/file_input/day7_example.txt b/file_input/day7_example.txt new file mode 100644 index 0000000..e3500c3 --- /dev/null +++ b/file_input/day7_example.txt @@ -0,0 +1,5 @@ +32T3K 765 +T55J5 684 +KK677 28 +KTJJT 220 +QQQJA 483 diff --git a/src/solutions.rs b/src/solutions.rs index 43cf98c..6677dd3 100644 --- a/src/solutions.rs +++ b/src/solutions.rs @@ -10,7 +10,7 @@ pub fn create_solutions() -> Vec String>> { vec![not_implemented_yet, not_implemented_yet], vec![day5::solve_task_1, day5::solve_task_2], vec![day6::solve_task_1, day6::solve_task_2], - vec![day7::solve_task_1, not_implemented_yet], + vec![day7::solve_task_1, day7::solve_task_2], 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/day7.rs b/src/solutions/day7.rs index 529c05e..fc48af6 100644 --- a/src/solutions/day7.rs +++ b/src/solutions/day7.rs @@ -1,4 +1,4 @@ -use categorized_hand::CategorizedHand; +use categorized_hand::{CategorizedHand, JokerOrdered}; mod categorized_hand; mod dealt_hand; @@ -17,6 +17,26 @@ pub fn solve_task_1(input: &str) -> String { total_winning.to_string() } +pub fn solve_task_2(input: &str) -> String { + const JOKER: char = 'J'; + let parsed = parsing::parse_input(input); + let sorted_and_categorized: Vec = { + let mut categorized: Vec = parsed + .into_iter() + .map(|not_categorized| (not_categorized, JOKER).into()) + .map(JokerOrdered::new) + .collect(); + categorized.sort(); + + categorized + .into_iter() + .map(|to_unwrap| to_unwrap.into()) + .collect() + }; + let total_winning = calc_total_winning(&sorted_and_categorized); + total_winning.to_string() +} + fn calc_total_winning(sorted: &[CategorizedHand]) -> usize { sorted .into_iter() @@ -52,4 +72,11 @@ mod testing { let expected = "6440"; assert_eq!(expected, actual); } + + #[test] + fn solve_example_according_to_task_2() { + let actual = super::solve_task_2(TEST_INPUT); + let expected = "5905"; + assert_eq!(expected, actual); + } } diff --git a/src/solutions/day7/categorized_hand.rs b/src/solutions/day7/categorized_hand.rs index 262d194..f68dfe8 100644 --- a/src/solutions/day7/categorized_hand.rs +++ b/src/solutions/day7/categorized_hand.rs @@ -1,10 +1,9 @@ -use std::{cmp::Reverse, collections::HashMap, usize}; - -use log::warn; +use derive_more::derive::Into; use crate::solutions::day7::second_ordering; +use std::usize; -use super::{dealt_hand::DealtHand, hand_kind::HandKind}; +use super::{dealt_hand::DealtHand, hand_kind::HandKind, second_ordering::StrengthOfSymbols}; #[derive(Debug, PartialEq, Eq)] pub struct CategorizedHand { @@ -12,7 +11,36 @@ pub struct CategorizedHand { rest: DealtHand, } +#[derive(Debug, PartialEq, Eq, Into)] +pub struct JokerOrdered(pub CategorizedHand); + +impl JokerOrdered { + pub fn new(value: CategorizedHand) -> Self { + Self(value) + } +} + +impl Ord for JokerOrdered { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + hand_cmp( + &self.0, + &other.0, + &second_ordering::LABEL_ORDERING_WITH_JOKER, + ) + } +} +impl PartialOrd for JokerOrdered { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl CategorizedHand { + #[cfg(test)] + pub fn new(kind: HandKind, rest: DealtHand) -> Self { + Self { kind, rest } + } + pub fn rest(&self) -> &DealtHand { &self.rest } @@ -23,27 +51,39 @@ impl CategorizedHand { impl Ord for CategorizedHand { fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match self.kind.cmp(&other.kind) { - std::cmp::Ordering::Equal => { - assert!( - self.kind == other.kind, - "If ordering is equal then the hand kind must be the same too\n\ - Self kind: ({:?}) and other kind: ({:?})", - self.kind, - other.kind - ); - second_ordering::compare_along_str(self.rest().hand(), other.rest().hand()) - } - not_equal_ordering => { - assert!( - self.kind != other.kind, - "If ordering is not equal then the hand kind must not be the same either\n\ - Self kind: ({:?}) and other kind: ({:?})", - self.kind, - other.kind - ); - not_equal_ordering - } + hand_cmp(self, other, &second_ordering::LABEL_ORDERING) + } +} + +fn hand_cmp( + left: &CategorizedHand, + right: &CategorizedHand, + label_ordering: &StrengthOfSymbols, +) -> std::cmp::Ordering { + match left.kind.cmp(&right.kind) { + std::cmp::Ordering::Equal => { + assert!( + left.kind == right.kind, + "If ordering is equal then the hand kind must be the same too\n\ + left kind: ({:?}) and right kind: ({:?})", + left.kind, + right.kind + ); + second_ordering::compare_along_str( + left.rest().hand(), + right.rest().hand(), + label_ordering, + ) + } + not_equal_ordering => { + assert!( + left.kind != right.kind, + "If ordering is not equal then the hand kind must not be the same either\n\ + left kind: ({:?}) and right kind: ({:?})", + left.kind, + right.kind + ); + not_equal_ordering } } } @@ -56,59 +96,13 @@ impl PartialOrd for CategorizedHand { impl From for CategorizedHand { fn from(value: DealtHand) -> Self { - fn calc_kind(values: &[usize]) -> Option { - let max = values.first()?; - - let kind = match max { - 5 => HandKind::Five, - 4 => HandKind::Four, - under_five_or_four => { - let second_after_max = values.get(1)?; - match (under_five_or_four, second_after_max) { - (3, 2) => HandKind::FullHouse, - (3, _) => HandKind::Three, - (2, 2) => HandKind::TwoPair, - (2, 1) => HandKind::OnePair, - _no_duplicates => HandKind::HighCard, - } - } - }; - - Some(kind) - } - - fn calc_duplicate_counted(value: &DealtHand) -> Vec { - const FIRST_ENCOUNTERED: usize = 1; - - let mut count_of_same: HashMap = HashMap::with_capacity(10); - - for next_char in value.hand().chars() { - count_of_same - .entry(next_char) - .and_modify(|count| *count = *count + 1) - .or_insert(FIRST_ENCOUNTERED); - } - - { - let mut to_sort: Vec = - count_of_same.into_iter().map(|(_, count)| count).collect(); - to_sort.sort_by_key(|&count| Reverse(count)); - to_sort - } - } - - let duplicated_counted = calc_duplicate_counted(&value); - - let kind = calc_kind(&duplicated_counted).unwrap_or_else(|| { - warn!( - "Got hand ({:?}) with less than 2 cards", - &duplicated_counted - ); - let default_value = HandKind::default(); - warn!("Default kind ({:?}) is assumed", default_value); - default_value - }); - + let kind = value.hand().into(); + CategorizedHand { kind, rest: value } + } +} +impl From<(DealtHand, char)> for CategorizedHand { + fn from((value, joker): (DealtHand, char)) -> Self { + let kind = (value.hand(), joker).into(); CategorizedHand { kind, rest: value } } } @@ -117,29 +111,7 @@ impl From for CategorizedHand { mod testing { use std::cmp::Ordering; - use crate::solutions::day7::{ - categorized_hand::CategorizedHand, dealt_hand::DealtHand, hand_kind::HandKind, - }; - - #[test] - fn from_dealt_hand_to_categorized_hand() { - fn assert_case(given: DealtHand, expected: HandKind) { - let expected_hand = CategorizedHand { - kind: expected, - rest: given.clone(), - }; - let actual = given.into(); - assert_eq!(expected_hand, actual); - } - - assert_case(DealtHand::new("AAAAA", 20), HandKind::Five); - assert_case(DealtHand::new("AA8AA", 20), HandKind::Four); - assert_case(DealtHand::new("23332", 20), HandKind::FullHouse); - assert_case(DealtHand::new("TTT98", 20), HandKind::Three); - assert_case(DealtHand::new("23456", 20), HandKind::HighCard); - assert_case(DealtHand::new("23432", 20), HandKind::TwoPair); - assert_case(DealtHand::new("A23A4", 20), HandKind::OnePair); - } + use crate::solutions::day7::{categorized_hand::CategorizedHand, dealt_hand::DealtHand}; #[test] fn compare_correctly_by_kind_and_second_ordering() { diff --git a/src/solutions/day7/hand_kind.rs b/src/solutions/day7/hand_kind.rs index 74bcd26..4f4cbd0 100644 --- a/src/solutions/day7/hand_kind.rs +++ b/src/solutions/day7/hand_kind.rs @@ -1,3 +1,9 @@ +use std::{cmp::Reverse, collections::HashMap}; + +use log::warn; +use non_zero_count::NonZeroCount; +mod non_zero_count; + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default)] pub enum HandKind { /// High card, where all cards' labels are distinct: 23456 @@ -16,3 +22,168 @@ pub enum HandKind { /// Five of a kind, where all five cards have the same label: AAAAA Five, } + +impl From<(&'_ str, char)> for HandKind { + fn from((characters, joker_char): (&'_ str, char)) -> Self { + fn get_joker_count(joker: char, symbols: &str) -> Option { + let count = symbols.chars().filter(|&next| next == joker).count(); + NonZeroCount::new(count) + } + fn use_jokers( + kind: HandKind, + how_many_jokers: NonZeroCount, + ) -> (HandKind, Option) { + match kind { + HandKind::HighCard => (HandKind::OnePair, how_many_jokers.decrement()), + HandKind::OnePair => (HandKind::Three, how_many_jokers.decrement()), + HandKind::TwoPair => (HandKind::FullHouse, how_many_jokers.decrement()), + HandKind::Three => (HandKind::Four, how_many_jokers.decrement()), + HandKind::FullHouse => (HandKind::Four, how_many_jokers.decrement()), + HandKind::Four | HandKind::Five => (HandKind::Five, Some(how_many_jokers)), + } + } + + let duplicated_counted = calc_duplicate_counted(&characters, |next| next != joker_char); + + let mut current_kind = choose_kind_from(&duplicated_counted); + let mut joker_count = get_joker_count(joker_char, characters); + + while joker_count.is_some() && current_kind != HandKind::Five { + let (new_kind, new_joker_count) = use_jokers( + current_kind, + joker_count.expect("While loop only iterates if there is some joke count left"), + ); + current_kind = new_kind; + joker_count = new_joker_count; + } + current_kind + } +} + +impl From<&'_ str> for HandKind { + fn from(value: &'_ str) -> Self { + let duplicated_counted = calc_duplicate_counted(&value, |_| true); + choose_kind_from(&duplicated_counted) + } +} + +fn calc_duplicate_counted( + characters: &str, + on_consider_next_char: impl Fn(char) -> bool, +) -> Vec { + const FIRST_ENCOUNTERED: usize = 1; + + let mut count_of_same: HashMap = HashMap::with_capacity(10); + + for next_char in characters.chars() { + if on_consider_next_char(next_char) { + count_of_same + .entry(next_char) + .and_modify(|count| *count = *count + 1) + .or_insert(FIRST_ENCOUNTERED); + } + } + + { + let mut to_sort: Vec = count_of_same.into_iter().map(|(_, count)| count).collect(); + to_sort.sort_by_key(|&count| Reverse(count)); + to_sort + } +} + +fn choose_kind_from(to_choose_from: &[usize]) -> HandKind { + fn calc_kind(values: &[usize]) -> Option { + let max = values.first()?; + + let kind = match max { + 5 => HandKind::Five, + 4 => HandKind::Four, + under_five_or_four => { + let opt_second_after_max = values.get(1); + match opt_second_after_max { + Some(second_after_max) => match (under_five_or_four, second_after_max) { + (3, 2) => HandKind::FullHouse, + (3, _) => HandKind::Three, + (2, 2) => HandKind::TwoPair, + (2, 1) => HandKind::OnePair, + _no_duplicates => HandKind::HighCard, + }, + None => match under_five_or_four { + 3 => HandKind::Three, + 2 => HandKind::OnePair, + _no_duplicates => HandKind::HighCard, + }, + } + } + }; + + Some(kind) + } + + calc_kind(&to_choose_from).unwrap_or_else(|| { + warn!("Got hand ({:?}) with less than 2 cards", &to_choose_from); + let default_value = HandKind::default(); + warn!("Default kind ({:?}) is assumed", default_value); + default_value + }) +} + +#[cfg(test)] +mod testing { + use crate::solutions::day7::{ + categorized_hand::CategorizedHand, dealt_hand::DealtHand, hand_kind::HandKind, + }; + + #[test] + fn from_dealt_hand_to_categorized_hand() { + fn assert_case(given: DealtHand, expected: HandKind) { + let expected_hand = CategorizedHand::new(expected, given.clone()); + let actual = given.into(); + assert_eq!(expected_hand, actual); + } + assert_case(DealtHand::new("AAAAA", 20), HandKind::Five); + assert_case(DealtHand::new("AA8AA", 20), HandKind::Four); + assert_case(DealtHand::new("23332", 20), HandKind::FullHouse); + assert_case(DealtHand::new("TTT98", 20), HandKind::Three); + assert_case(DealtHand::new("23456", 20), HandKind::HighCard); + assert_case(DealtHand::new("23432", 20), HandKind::TwoPair); + assert_case(DealtHand::new("A23A4", 20), HandKind::OnePair); + } + + #[test] + fn from_cards_to_kind_with_joker() { + const JOKER: char = 'J'; + fn assert_case(given: &str, expected: HandKind) { + let actual: HandKind = (given, JOKER).into(); + assert_eq!(expected, actual, "Given: {:?}", given); + } + + //32T3K is still the only one pair; + //it doesn't contain any jokers, so its strength doesn't increase. + assert_case("32T3K", HandKind::OnePair); + //KK677 is now the only two pair, making it the second-weakest hand. + assert_case("KK677", HandKind::TwoPair); + //T55J5, KTJJT, and QQQJA are now all four of a kind! T55J5 gets rank 3, + //QQQJA gets rank 4, and KTJJT gets rank 5. + assert_case("T55J5", HandKind::Four); + assert_case("KTJJT", HandKind::Four); + assert_case("QQQJA", HandKind::Four); + + // from highcard + assert_case("1234J", HandKind::OnePair); + assert_case("123JJ", HandKind::Three); + assert_case("12JJJ", HandKind::Four); + assert_case("1JJJJ", HandKind::Five); + assert_case("JJJJJ", HandKind::Five); + + // from four + assert_case("QQQQJ", HandKind::Five); + assert_case("QQQQQ", HandKind::Five); + + // from three + assert_case("QQQJJ", HandKind::Five); + + // From two pairs + assert_case("QQTTJ", HandKind::FullHouse); + } +} diff --git a/src/solutions/day7/hand_kind/non_zero_count.rs b/src/solutions/day7/hand_kind/non_zero_count.rs new file mode 100644 index 0000000..c01e921 --- /dev/null +++ b/src/solutions/day7/hand_kind/non_zero_count.rs @@ -0,0 +1,32 @@ +use derive_more::derive::Into; + +#[derive(Debug, PartialEq, Eq, Into, Clone, Copy)] +pub struct NonZeroCount(usize); + +impl std::ops::Sub for NonZeroCount { + type Output = Option; + + fn sub(self, rhs: Self) -> Self::Output { + assert!(self.0 >= rhs.0); + + Self::new(self.0 - rhs.0) + } +} + +impl NonZeroCount { + pub fn one() -> Self { + Self(1) + } + + pub fn decrement(&self) -> Option { + *self - Self::one() + } + + pub fn new(value: usize) -> Option { + if value > 0 { + Some(Self(value)) + } else { + None + } + } +} diff --git a/src/solutions/day7/second_ordering.rs b/src/solutions/day7/second_ordering.rs index 7b3cebb..a4c6609 100644 --- a/src/solutions/day7/second_ordering.rs +++ b/src/solutions/day7/second_ordering.rs @@ -2,29 +2,50 @@ use std::{cmp::Ordering, collections::HashMap, ops::RangeInclusive, sync::LazyLo use log::warn; +const JOKER: char = 'J'; +static LETTERS_WITHOUT_JOKER: &[char] = &['T', 'J', 'Q', 'K', 'A']; static LETTERS: &[char] = &['T', 'J', 'Q', 'K', 'A']; const NUMBERS: RangeInclusive = 0..=9; -static LABEL_ORDERING: LazyLock> = LazyLock::new(|| { - let numbers = NUMBERS +pub type StrengthOfSymbols = HashMap; + +fn numbers_in_chars() -> impl Iterator { + NUMBERS .into_iter() - .map(|number| std::char::from_digit(number, 10).unwrap()); - numbers + .map(|number| std::char::from_digit(number, 10).unwrap()) +} + +//A hand consists of five cards labeled one of A, K, Q, J, T, 9, 8, 7, 6, 5, 4, 3, or 2. +//The relative strength of each card follows this order, where A is the highest and 2 is the lowest. +pub static LABEL_ORDERING: LazyLock = LazyLock::new(|| { + numbers_in_chars() .chain(LETTERS.into_iter().copied()) .into_iter() .enumerate() .map(|(index, character)| (character, index)) .collect() }); -// -//A hand consists of five cards labeled one of A, K, Q, J, T, 9, 8, 7, 6, 5, 4, 3, or 2. -//The relative strength of each card follows this order, where A is the highest and 2 is the lowest. -pub fn compare_along_str(left: &str, right: &str) -> Ordering { + +pub static LABEL_ORDERING_WITH_JOKER: LazyLock = LazyLock::new(|| { + std::iter::once(JOKER) + .chain(numbers_in_chars()) + .chain(LETTERS_WITHOUT_JOKER.into_iter().copied()) + .into_iter() + .enumerate() + .map(|(index, character)| (character, index)) + .collect() +}); + +pub fn compare_along_str( + left: &str, + right: &str, + symbols_to_strength: &StrengthOfSymbols, +) -> Ordering { for (left_char, right_char) in left.chars().zip(right.chars()) { - let left_rank_value = LABEL_ORDERING + let left_rank_value = symbols_to_strength .get(&left_char) .unwrap_or_else(|| panic!("left char ({}) is not valid", left_char)); - let right_rank_value = LABEL_ORDERING + let right_rank_value = symbols_to_strength .get(&right_char) .unwrap_or_else(|| panic!("right char ({}) is not valid", right_char));