diff --git a/src/solutions.rs b/src/solutions.rs index c4459d9..43cf98c 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![not_implemented_yet, not_implemented_yet], + vec![day7::solve_task_1, 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/day7.rs b/src/solutions/day7.rs index 3b082df..4c16345 100644 --- a/src/solutions/day7.rs +++ b/src/solutions/day7.rs @@ -1 +1,49 @@ +use categorized_hand::CategorizedHand; + +mod categorized_hand; mod dealt_hand; +mod hand_kind; +mod parsing; + +/// TODO: wrong answer for 253639210 is too low for my puzzle input +pub fn solve_task_1(input: &str) -> String { + let parsed = parsing::parse_input(input); + let sorted_and_categorized = { + let mut categorized: Vec<_> = parsed.into_iter().map(CategorizedHand::from).collect(); + categorized.sort(); + categorized + }; + let total_score = calc_bid_and_ranks(&sorted_and_categorized); + total_score.to_string() +} + +fn calc_bid_and_ranks(sorted: &[CategorizedHand]) -> usize { + sorted + .into_iter() + .enumerate() + .map(|(index, hand)| { + let rank = index + 1; + hand.score() * rank + }) + .sum() +} + +#[cfg(test)] +const TEST_INPUT: &str = "32T3K 765 +T55J5 684 +KK677 28 +KTJJT 220 +QQQJA 483 +"; + +#[cfg(test)] +mod testing { + use super::TEST_INPUT; + + #[test] + fn solve_example_according_to_task_1() { + let actual = super::solve_task_1(TEST_INPUT); + let expected = "6440"; + assert_eq!(expected, actual); + } +} diff --git a/src/solutions/day7/categorized_hand.rs b/src/solutions/day7/categorized_hand.rs new file mode 100644 index 0000000..2881450 --- /dev/null +++ b/src/solutions/day7/categorized_hand.rs @@ -0,0 +1,194 @@ +use std::{ + cmp::{Ordering, Reverse}, + collections::HashMap, + sync::LazyLock, + usize, +}; + +use super::{dealt_hand::DealtHand, hand_kind::HandKind}; + +#[derive(Debug, PartialEq, Eq)] +pub struct CategorizedHand { + kind: HandKind, + rest: DealtHand, +} + +impl CategorizedHand { + pub fn rest(&self) -> &DealtHand { + &self.rest + } + pub fn score(&self) -> usize { + self.rest().score() + } +} + +impl Ord for CategorizedHand { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + match self.kind.cmp(&other.kind) { + std::cmp::Ordering::Equal => compare_along_str(self.rest().hand(), other.rest().hand()), + not_equal_ordering => not_equal_ordering, + } + } +} + +impl PartialOrd for CategorizedHand { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +static LABEL_ORDERING: LazyLock> = LazyLock::new(|| { + let numbers = (0..9) + .into_iter() + .map(|number| std::char::from_digit(number, 10).unwrap()); + let letters = ['T', 'J', 'Q', 'K', 'A']; + numbers + .chain(letters.into_iter()) + .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. +fn compare_along_str(left: &str, right: &str) -> Ordering { + for (left_char, right_char) in left.chars().zip(right.chars()) { + let left_rank_value = LABEL_ORDERING.get(&left_char); + let right_rank_value = LABEL_ORDERING.get(&right_char); + let current_ordering = left_rank_value.cmp(&right_rank_value); + if current_ordering != Ordering::Equal { + return current_ordering; + } + } + left.len().cmp(&right.len()) +} + +impl From for CategorizedHand { + fn from(value: DealtHand) -> Self { + 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(1); + } + let values = { + 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 max = values.first().unwrap(); + let kind = match max { + 5 => HandKind::Five, + 4 => HandKind::Four, + _ => { + let second = values.get(1).unwrap(); + match (max, second) { + (3, 2) => HandKind::FullHouse, + (3, _) => HandKind::Three, + (2, 2) => HandKind::TwoPair, + (2, 1) => HandKind::OnePair, + _ => HandKind::HighCard, + } + } + }; + + CategorizedHand { kind, rest: value } + } +} + +#[cfg(test)] +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); + } + + #[test] + fn correct_cmp() { + fn assert_case(left: DealtHand, right: DealtHand, expected: Ordering) { + let (left, right): (CategorizedHand, CategorizedHand) = (left.into(), right.into()); + let actual = left.cmp(&right); + assert_eq!(expected, actual, "Left: {:?}\nRight: {:?}", left, right); + } + + assert_case( + DealtHand::new("AAAAA", 20), + DealtHand::new("BBBBA", 30), + Ordering::Greater, + ); + assert_case( + DealtHand::new("12345", 20), + DealtHand::new("BBBBA", 30), + Ordering::Less, + ); + assert_case( + DealtHand::new("12345", 20), + DealtHand::new("28495", 30), + Ordering::Less, + ); + assert_case( + DealtHand::new("QQQJA", 20), + DealtHand::new("T55J5", 30), + Ordering::Greater, + ); + //KK677 and KTJJT are both two pair. + //Their first cards both have the same label, + //but the second card of KK677 is stronger (K vs T), so KTJJT gets rank 2 and KK677 gets rank 3. + assert_case( + DealtHand::new("KK677", 20), + DealtHand::new("KTJJT", 30), + Ordering::Greater, + ); + } + + #[test] + fn sort_example_correctly() { + //32T3K is the only one pair and the other hands are all a stronger type, so it gets rank 1. + // + //KK677 and KTJJT are both two pair. + //Their first cards both have the same label, + //but the second card of KK677 is stronger (K vs T), so KTJJT gets rank 2 and KK677 gets rank 3. + // + //T55J5 and QQQJA are both three of a kind. + //QQQJA has a stronger first card, so it gets rank 5 and T55J5 gets rank 4. + + let mut input: Vec<_> = vec![ + DealtHand::new("32T3K", 765), + DealtHand::new("T55J5", 684), + DealtHand::new("KK677", 28), + DealtHand::new("KTJJT", 220), + DealtHand::new("QQQJA", 483), + ] + .into_iter() + .map(CategorizedHand::from) + .collect(); + + input.sort(); + + insta::assert_debug_snapshot!(input); + } +} diff --git a/src/solutions/day7/dealt_hand.rs b/src/solutions/day7/dealt_hand.rs index 86b340c..85e1b9e 100644 --- a/src/solutions/day7/dealt_hand.rs +++ b/src/solutions/day7/dealt_hand.rs @@ -3,12 +3,28 @@ use std::{str::FromStr, usize}; use derive_more::derive::Debug; use thiserror::Error; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct DealtHand { hand: String, score: usize, } +impl DealtHand { + #[cfg(test)] + pub fn new(hand: impl Into, score: usize) -> Self { + let hand = hand.into(); + Self { hand, score } + } + + pub fn hand(&self) -> &str { + &self.hand + } + + pub fn score(&self) -> usize { + self.score + } +} + #[derive(Debug, Error, PartialEq, Eq)] pub enum InvalidStrDealtHand { #[error("Dealt hand must consist of 2 elements")] diff --git a/src/solutions/day7/hand_kind.rs b/src/solutions/day7/hand_kind.rs new file mode 100644 index 0000000..29ac974 --- /dev/null +++ b/src/solutions/day7/hand_kind.rs @@ -0,0 +1,17 @@ +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum HandKind { + /// High card, where all cards' labels are distinct: 23456 + HighCard, + /// One pair, where two cards share one label, and the other three cards have a different label from the pair and each other: A23A4 + OnePair, + /// Two pair, where two cards share one label, two other cards share a second label, and the remaining card has a third label: 23432 + TwoPair, + /// Three of a kind, where three cards have the same label, and the remaining two cards are each different from any other card in the hand: TTT98 + Three, + /// Full house, where three cards have the same label, and the remaining two cards share a different label: 23332 + FullHouse, + /// Four of a kind, where four cards have the same label and one card has a different label: AA8AA + Four, + /// Five of a kind, where all five cards have the same label: AAAAA + Five, +} diff --git a/src/solutions/day7/parsing.rs b/src/solutions/day7/parsing.rs index e69de29..5adad52 100644 --- a/src/solutions/day7/parsing.rs +++ b/src/solutions/day7/parsing.rs @@ -0,0 +1,23 @@ +use super::dealt_hand::DealtHand; + +pub fn parse_input(input: &str) -> Vec { + input.lines().map(|line| line.parse().unwrap()).collect() +} + +#[cfg(test)] +mod testing { + use crate::solutions::day7::{dealt_hand::DealtHand, TEST_INPUT}; + + #[test] + fn parse() { + let actual = super::parse_input(TEST_INPUT); + let expected = vec![ + DealtHand::new("32T3K", 765), + DealtHand::new("T55J5", 684), + DealtHand::new("KK677", 28), + DealtHand::new("KTJJT", 220), + DealtHand::new("QQQJA", 483), + ]; + assert_eq!(expected, actual); + } +} diff --git a/src/solutions/day7/snapshots/advent_of_code_2023__solutions__day7__categorized_hand__testing__sort_example_correctly.snap b/src/solutions/day7/snapshots/advent_of_code_2023__solutions__day7__categorized_hand__testing__sort_example_correctly.snap new file mode 100644 index 0000000..5ad1575 --- /dev/null +++ b/src/solutions/day7/snapshots/advent_of_code_2023__solutions__day7__categorized_hand__testing__sort_example_correctly.snap @@ -0,0 +1,41 @@ +--- +source: src/solutions/day7/categorized_hand.rs +expression: input +--- +[ + CategorizedHand { + kind: OnePair, + rest: DealtHand { + hand: "32T3K", + score: 765, + }, + }, + CategorizedHand { + kind: TwoPair, + rest: DealtHand { + hand: "KTJJT", + score: 220, + }, + }, + CategorizedHand { + kind: TwoPair, + rest: DealtHand { + hand: "KK677", + score: 28, + }, + }, + CategorizedHand { + kind: Three, + rest: DealtHand { + hand: "T55J5", + score: 684, + }, + }, + CategorizedHand { + kind: Three, + rest: DealtHand { + hand: "QQQJA", + score: 483, + }, + }, +]