Day 2 part 1 works out nicely in a functional paradigm because it can be seen as just building a couple of frequency tables.
I often use this function to generate a frequency table of values in a list:
import qualified Data.Map as M
freqs :: [a] -> Map a Int
freqs = M.fromListWith (+) . map (,1)
Day 2 part 1 is then to:
- Build a frequency map for chars for each line
- Aggregate all of the seen frequencies in each line
- Build a frequency map of the seen frequencies
- Look up how often freq 2 and freq 3 occurred, and then multiply
So we have:
day02a :: [String] -> Maybe Int
day02a = mulTwoThree
. freqs
. concatMap (nubOrd . M.elems . freqs)
mulTwoThree :: Map Int Int -> Maybe Int
mulTwoThree mp = (*) <$> M.lookup 2 mp <*> M.lookup 3 mp
Part 2 for this day is pretty much the same as Part 2 for day 1, only instead of finding the first item that has already been seen, we find the first item who has any neighbors who had already been seen.
import Control.Lens
import qualified Data.Set as S
firstNeighbor :: [String] -> Maybe (String, String)
firstNeighbor = go S.empty
where
go seen (x:xs) = case find (`S.member` seen) (neighbors x) of
Just n -> Just (x, n)
Nothing -> go (x `S.insert` seen) xs
go _ [] = Nothing
neighbors :: String -> [String]
neighbors xs = [ xs & ix i .~ newChar
| i <- [0 .. length xs - 1]
| newChar <- ['a'..'z']
]
firstNeighbor
will return the first item who has a neighbor that has already
been seen, along with that neighbor.
The answer we need to return is the common letters between the two strings, so we can write a function to only keep common letters between two strings:
onlySame :: String -> String -> String
onlySame xs = catMaybes . zipWith (\x y -> x <$ guard (x == y)) xs
-- > onlySame "abcd" "abed" == "abd"
And that's pretty much the entire pipeline:
day02a :: [String] -> Maybe String
day02a = fmap (uncurry onlySame) . firstNeighbor
Parsing is just lines :: String -> [String]
, which splits a string on lines.