Day 7 was a nice little puzzle, and thankfully much simpler than it could have been! Things would have been much tricker if the possible hands had includes straights and the like. My solution was a little verbose, but that's mainly because there are a lot of cards and types of hand.

Representation and parsing

My first decision was to be explicit about how I represented the data. I defined my own data type for Card values and hand classifications (HandClass), both in order from lowest to highest. That meant I could more easily do comparisons and sorting. I also defined data types for a Hand (cards and the bid) and a ClassifiedHand (including the HandClass). Again, these are arranged so that the default sort does the right thing.

data Card = Two | Three | Four | Five | Six | Seven | Eight | Nine | 
            Ten | Jack | Queen | King | Ace deriving (Eq, Ord, Show)

data HandClass = HighCard | OnePair | TwoPair | ThreeOfAKind | FullHouse | 
                 FourOfAKind | FiveOfAKind deriving (Eq, Ord, Show)

data Hand = Hand [Card] Int deriving (Eq, Ord, Show)
data ClassifiedHand = CHand HandClass [Card] Int deriving (Eq, Ord, Show)

Parsing the input involved matching the text strings against the Card constructors.

handsP = handP `sepBy` endOfLine
handP = Hand <$> ((many1 cardP) <* space) <*> decimal

cardP = (Two <$ "2") <|> (Three <$ "3") <|> (Four <$ "4") <|> 
        (Five <$ "5") <|> (Six <$ "6") <|> (Seven <$ "7") <|> 
        (Eight <$ "8") <|> (Nine <$ "9") <|> (Ten <$ "T") <|> 
        (Jack <$ "J") <|> (Queen <$ "Q") <|> (King <$ "K") <|> 
        (Ace <$ "A")

Part 1

The classification of hands in "Camel Cards" is based purely on the size of the largest group (or two) of identical cards, with ties broken in lexicographical order of the cards-as-dealt. I called this "groups in order" representation of a hand its signature.

type SignatureElement = (Int, [Card])
type Signature = [SignatureElement]

That gave me a pipeline operation to sign a set of cards: sort then group them, decorate each group with its size, then sort the decorated groups. Ensure the largest group is first.

sign :: [Card] -> Signature
sign = reverse . sort . fmap (\g -> (length g, g)) . group . sort

That means the sample hands from the problem text are signed like this:

[ [(2,[Three,Three]),(1,[King]),(1,[Ten]),(1,[Two])]
, [(3,[Five,Five,Five]),(1,[Jack]),(1,[Ten])]
, [(2,[King,King]),(2,[Seven,Seven]),(1,[Six])]
, [(2,[Jack,Jack]),(2,[Ten,Ten]),(1,[King])]
, [(3,[Queen,Queen,Queen]),(1,[Ace]),(1,[Jack])]
]

I then defined a bunch of functions to recognise each of the hand classifications, and a classify function to classify a Hand. I could have done this with a big case statement in classify, but the separate functions are easier to test should it be needed.

classify :: Hand -> ClassifiedHand
classify (Hand cards bid) 
  | isFiveOfAKind signature = CHand FiveOfAKind cards bid
  | isFourOfAKind signature = CHand FourOfAKind cards bid
  | isFullHouse signature = CHand FullHouse cards bid
  | isThreeOfAKind signature = CHand ThreeOfAKind cards bid
  | isTwoPair signature = CHand TwoPair cards bid
  | isOnePair signature = CHand OnePair cards bid
  | otherwise = CHand HighCard cards bid
  where signature = sign cards
  
isFiveOfAKind, isFourOfAKind, isFullHouse, isThreeOfAKind, isTwoPair, 
isOnePair :: Signature -> Bool
isFiveOfAKind ((5, _):_) = True
isFiveOfAKind _ = False

isFourOfAKind ((4, _):_) = True
isFourOfAKind _ = False

isFullHouse ((3, _):(2, _):_) = True
isFullHouse _ = False

isThreeOfAKind ((3, _):_) = True
isThreeOfAKind _ = False

isTwoPair ((2, _):(2, _):_) = True
isTwoPair _ = False

isOnePair ((2, _):_) = True
isOnePair _ = False

-- isHighCard :: Signature -> Bool
-- isHighCard _ = True

From that, finding the score isn't too hard. Classify the hands, sort them, combine with their rank (so the weakest hand has rank 1, counting up), then sum the scores.

part1, part2 :: [Hand] -> Int
part1 hands = sum $ fmap score rankedHands
  where sortedHands = sort $ fmap classify hands
        rankedHands = zip [1..] sortedHands
        score (r, CHand _ _ bid) = r * bid

Part 2

The "J" cards aren't "Jacks", they're "Jokers".

First of all, it's a new type of card that's smaller than the others. I added it to the Card type...

data Card = Joker | Two | Three ...

and made sure I could replace all the Jacks with Jokers.

enJoker :: Hand -> Hand
enJoker (Hand cards bid) = Hand jCards bid
  where jCards = replace Jack Joker cards

replace :: Eq a => a -> a -> [a] -> [a]
replace f t = fmap (\x -> if x == f then t else x)

The tricky part comes from signing a hand. I remove the Jokers, sign the rest of the cards as before, then add the Jokers to the largest group.

sign :: [Card] -> Signature
sign cards = addJokers nonJokerSigned (length jokers, jokers)
  where (jokers, nonJokers) = partition (== Joker) cards
        nonJokerSigned = reverse $ sort $ fmap (\g -> (length g, g)) $ 
                           group $ sort nonJokers

addJokers :: Signature -> SignatureElement -> Signature
addJokers [] js = [js]
addJokers ((n, cs):xs) (jn, js) = (n + jn, cs ++ js):xs

That means the sample hands, with Jokers, have these signatures:

[ [(2,[Three,Three]),(1,[King]),(1,[Ten]),(1,[Two])]
, [(4,[Five,Five,Five,Joker]),(1,[Ten])]
, [(2,[King,King]),(2,[Seven,Seven]),(1,[Six])]
, [(4,[Ten,Ten,Joker,Joker]),(1,[King])]
, [(4,[Queen,Queen,Queen,Joker]),(1,[Ace])]
]

Everything else is the same.

part2 = part1 . fmap enJoker

Code

You can get the code from my locally-hosted Git repo, or from Gitlab.