From 1e17292b18160a7c2a91ba947c86a4fa0040b500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Thu, 7 Dec 2023 10:14:10 +0100 Subject: [PATCH] Add day 7 --- 2023/day7/.gitignore | 1 + 2023/day7/Cargo.lock | 107 +++++++++ 2023/day7/Cargo.toml | 12 + 2023/day7/src/main.rs | 502 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 622 insertions(+) create mode 100644 2023/day7/.gitignore create mode 100644 2023/day7/Cargo.lock create mode 100644 2023/day7/Cargo.toml create mode 100644 2023/day7/src/main.rs diff --git a/2023/day7/.gitignore b/2023/day7/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/2023/day7/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/2023/day7/Cargo.lock b/2023/day7/Cargo.lock new file mode 100644 index 0000000..cbcf6e1 --- /dev/null +++ b/2023/day7/Cargo.lock @@ -0,0 +1,107 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "day7" +version = "0.1.0" +dependencies = [ + "indoc", + "nom", + "strum", + "strum_macros", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "indoc" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "proc-macro2" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" diff --git a/2023/day7/Cargo.toml b/2023/day7/Cargo.toml new file mode 100644 index 0000000..086edaa --- /dev/null +++ b/2023/day7/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "day7" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +indoc = "2.0.4" +nom = "7.1.3" +strum = "0.25.0" +strum_macros = "0.25.3" diff --git a/2023/day7/src/main.rs b/2023/day7/src/main.rs new file mode 100644 index 0000000..66c6a8b --- /dev/null +++ b/2023/day7/src/main.rs @@ -0,0 +1,502 @@ +use std::fmt::Debug; +use std::hash::Hash; +use std::{cmp::Ordering, collections::HashMap}; + +use nom::{ + character::complete::{anychar, char, digit1, multispace0, multispace1}, + combinator::map, + multi::{many1, many_m_n, separated_list1}, + sequence::{delimited, separated_pair}, + IResult, +}; + +use strum_macros::EnumIter; + +fn number(i: &str) -> IResult<&str, usize> { + map(digit1, |f: &str| f.parse::().unwrap())(i) +} + +fn spaces(i: &str) -> IResult<&str, Vec> { + many1(char(' '))(i) +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +enum HandType { + HighCard, + OnePair, + TwoPair, + ThreeOfAKind, + FullHouse, + FourOfAKind, + FiveOfAKind, +} + +trait Parse { + type Out; + fn parse(s: &str) -> IResult<&str, Box>; +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +enum CardWithoutJoker { + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, + Jack, + Queen, + King, + Ace, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)] +enum CardWithJoker { + Joker, + Two, + Three, + Four, + Five, + Six, + Seven, + Eight, + Nine, + Ten, + Queen, + King, + Ace, +} + +impl Parse for CardWithoutJoker { + type Out = Self; + fn parse(s: &str) -> IResult<&str, Box> { + let (rest, c) = anychar(s)?; + Ok(( + rest, + Box::new(match c { + '2' => Self::Two, + '3' => Self::Three, + '4' => Self::Four, + '5' => Self::Five, + '6' => Self::Six, + '7' => Self::Seven, + '8' => Self::Eight, + '9' => Self::Nine, + 'T' => Self::Ten, + 'J' => Self::Jack, + 'Q' => Self::Queen, + 'K' => Self::King, + 'A' => Self::Ace, + _ => { + return Err(nom::Err::Error(nom::error::Error::new( + "unknown card", + nom::error::ErrorKind::Char, + ))) + } + }), + )) + } +} + +impl Parse for CardWithJoker { + type Out = Self; + fn parse(s: &str) -> IResult<&str, Box> { + let (rest, c) = anychar(s)?; + Ok(( + rest, + Box::new(match c { + '2' => Self::Two, + '3' => Self::Three, + '4' => Self::Four, + '5' => Self::Five, + '6' => Self::Six, + '7' => Self::Seven, + '8' => Self::Eight, + '9' => Self::Nine, + 'T' => Self::Ten, + 'J' => Self::Joker, + 'Q' => Self::Queen, + 'K' => Self::King, + 'A' => Self::Ace, + _ => { + return Err(nom::Err::Error(nom::error::Error::new( + "unknown card", + nom::error::ErrorKind::Char, + ))) + } + }), + )) + } +} + +trait Hand: PartialEq + Eq + PartialOrd + Ord + ParseHand + Debug { + type CardType: PartialOrd + PartialEq + Eq + Ord + Parse; + + fn hand_type(&self) -> HandType; + + fn cards(&self) -> &Vec; + + fn from_vec(v: Vec) -> Self; + + fn eq(&self, other: &Self) -> bool { + for (i, card) in self.cards().iter().enumerate() { + if *card != other.cards()[i] { + return false; + } + } + true + } + + fn partial_cmp(&self, other: &Self) -> Option { + let (our_hand_type, their_hand_type) = (self.hand_type(), other.hand_type()); + + if our_hand_type < their_hand_type { + return Some(Ordering::Less); + } + + if our_hand_type > their_hand_type { + return Some(Ordering::Greater); + } + + for (i, card) in self.cards().iter().enumerate() { + if *card < other.cards()[i] { + return Some(Ordering::Less); + } + if *card > other.cards()[i] { + return Some(Ordering::Greater); + } + } + + Some(Ordering::Equal) + } + + fn cmp(&self, other: &Self) -> Ordering { + ::partial_cmp(self, other).unwrap() + } +} + +#[derive(Debug)] +struct HandWithoutJoker(Vec); + +trait ParseHand { + type Out: Hand; + fn parse(s: &str) -> IResult<&str, Box> { + let (rest, cards) = many_m_n(5, 5, <::Out as Hand>::CardType::parse)(s)?; + + Ok(( + rest, + Box::new(Self::Out::from_vec( + cards + .into_iter() + .map(|card| *card) + .collect::::CardType>>(), + )), + )) + } +} + +impl ParseHand for HandWithoutJoker { + type Out = Self; +} + +#[derive(Debug)] +struct HandWithJoker(Vec); + +impl ParseHand for HandWithJoker { + type Out = Self; +} + +impl Hand for HandWithoutJoker { + type CardType = CardWithoutJoker; + + fn cards(&self) -> &Vec { + &self.0 + } + + fn from_vec(v: Vec) -> Self { + Self(v) + } + + fn hand_type(&self) -> HandType { + let mut types: HashMap = HashMap::new(); + self.0.iter().for_each(|card| { + types + .entry(*card) + .and_modify(|count| *count += 1) + .or_insert(1); + }); + match types.len() { + 5 => HandType::HighCard, + 4 => HandType::OnePair, + 3 => { + let max_count = types.values().max().unwrap(); + if *max_count == 3 { + HandType::ThreeOfAKind + } else { + HandType::TwoPair + } + } + 2 => { + let max_count = types.values().max().unwrap(); + if *max_count == 4 { + HandType::FourOfAKind + } else { + HandType::FullHouse + } + } + 1 => HandType::FiveOfAKind, + _ => unreachable!(), + } + } +} + +impl PartialEq for HandWithoutJoker { + fn eq(&self, other: &Self) -> bool { + Hand::eq(self, other) + } +} + +#[allow(clippy::non_canonical_partial_ord_impl)] +impl PartialOrd for HandWithoutJoker { + fn partial_cmp(&self, other: &Self) -> Option { + Hand::partial_cmp(self, other) + } +} + +impl Eq for HandWithoutJoker {} + +impl Ord for HandWithoutJoker { + fn cmp(&self, other: &Self) -> Ordering { + Hand::partial_cmp(self, other).unwrap() + } +} + +impl Hand for HandWithJoker { + type CardType = CardWithJoker; + + fn cards(&self) -> &Vec { + &self.0 + } + + fn from_vec(v: Vec) -> Self { + Self(v) + } + + fn hand_type(&self) -> HandType { + let mut types: HashMap = { + let mut types = HashMap::new(); + + self.cards().iter().for_each(|card| { + types + .entry(*card) + .and_modify(|count| *count += 1) + .or_insert(1); + }); + + types + }; + + // The joker logic is simple: The best way to improve our hand it to use the jokers as the + // same card that we have the *most* of. As jokers substitutions are only used for type + // calcuation and not for the value, it does not matter *which* card we pick if there are + // multiple with the same number + if let Some(joker_count) = types.get(&CardWithJoker::Joker).copied() { + if let Some(highest_non_joker_card) = types + .iter() + .filter(|(card, _count)| **card != CardWithJoker::Joker) + .max_by_key(|(_card, count)| *count) + .map(|(card, _number)| *card) + { + *types.get_mut(&highest_non_joker_card).unwrap() += joker_count; + types.remove(&CardWithJoker::Joker); + } else { + // all cards are jokers + return HandType::FiveOfAKind; + } + } + + match types.len() { + 5 => HandType::HighCard, + 4 => HandType::OnePair, + 3 => { + let max_count = types.values().max().unwrap(); + if *max_count == 3 { + HandType::ThreeOfAKind + } else { + HandType::TwoPair + } + } + 2 => { + let max_count = types.values().max().unwrap(); + if *max_count == 4 { + HandType::FourOfAKind + } else { + HandType::FullHouse + } + } + 1 => HandType::FiveOfAKind, + _ => unreachable!(), + } + } +} + +impl PartialEq for HandWithJoker { + fn eq(&self, other: &Self) -> bool { + Hand::eq(self, other) + } +} + +#[allow(clippy::non_canonical_partial_ord_impl)] +impl PartialOrd for HandWithJoker { + fn partial_cmp(&self, other: &Self) -> Option { + Hand::partial_cmp(self, other) + } +} + +impl Eq for HandWithJoker {} + +impl Ord for HandWithJoker { + fn cmp(&self, other: &Self) -> Ordering { + Hand::partial_cmp(self, other).unwrap() + } +} + +#[derive(Debug)] +struct HandWithBid> { + hand: Box, + bid: usize, +} + +impl> HandWithBid { + fn parse(s: &str) -> IResult<&str, Self> { + let (rest, (hand, bid)) = separated_pair(T::parse, spaces, number)(s)?; + Ok(( + rest, + Self { + hand: Box::new(*hand), + bid, + }, + )) + } +} + +impl> PartialEq for HandWithBid { + fn eq(&self, other: &Self) -> bool { + self.hand == other.hand + } +} + +impl> Eq for HandWithBid {} + +impl> PartialOrd for HandWithBid { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.hand.cmp(&other.hand)) + } +} + +impl> Ord for HandWithBid { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(other).unwrap() + } +} + +#[derive(Debug)] +struct Hands>(Vec>); + +impl> Hands { + fn parse(s: &str) -> IResult<&str, Self> { + let (rest, hands) = delimited( + multispace0, + separated_list1(multispace1, HandWithBid::parse), + multispace0, + )(s)?; + + Ok((rest, Self(hands))) + } + + fn sort_by_cards(&mut self) { + self.0.sort(); + } +} + +fn part1(input: &str) -> Result { + let (rest, mut hands) = Hands::::parse(input).map_err(|e| e.to_string())?; + assert!(rest.is_empty()); + + hands.sort_by_cards(); + + let out = hands + .0 + .iter() + .enumerate() + .map(|(i, hand)| (i + 1) * hand.bid) + .sum(); + + Ok(out) +} + +fn part2(input: &str) -> Result { + let (rest, mut hands) = Hands::::parse(input).map_err(|e| e.to_string())?; + assert!(rest.is_empty()); + + hands.sort_by_cards(); + + let out = hands + .0 + .iter() + .enumerate() + .map(|(i, hand)| (i + 1) * hand.bid) + .sum(); + + Ok(out) +} + +fn main() -> Result<(), String> { + let input = include_str!("../input"); + + let args = std::env::args().skip(1).collect::>(); + let part = args[0].parse::().unwrap(); + + if part == 1 { + println!("Part 1 : {}", part1(input)?); + } else if part == 2 { + println!("Part 2 : {}", part2(input)?); + } else { + return Err("unknown part".into()); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + + #[test] + fn example_01() { + let input = indoc! {" + 32T3K 765 + T55J5 684 + KK677 28 + KTJJT 220 + QQQJA 483 + "}; + assert_eq!(part1(input).unwrap(), 6440); + } + + #[test] + fn example_02() { + let input = indoc! {" + 32T3K 765 + T55J5 684 + KK677 28 + KTJJT 220 + QQQJA 483 + "}; + assert_eq!(part2(input).unwrap(), 5905); + } +}