Advent of Code 2024 day 3
Day 3 required a data structure, some parsing, and a fold
to handle when things should and shouldn't be included.
Data structure and parsing
I had a suspicion that the "junk" in the input might be useful, so I decided to keep it around. The input is either a mul
term or a string of junk
, so that was my initial data structure for terms.
data Term = Mul Int Int
| Junk String
deriving (Eq, Show)
I can parse that with Attoparsec and the parsers:
junkP = Junk <$> many1 anyChar
mulP = Mul <$> ("mul(" *> decimal <* ",") <*> decimal <* ")"
termsP = (++) <$> (many' junkP) <*> (mulP `sepBy` junkP)
Unfortunately, that doesn't work! The junkP
parser will read any number of characters, including any following mul
terms. To read the input, I needed to slow down junkP
to read just one character at a time.
That gave the revised data structure of:
data Term = Mul Int Int
| Junk Char
deriving (Eq, Show)
and this parser:
junkP = Junk <$> anyChar
mulP = Mul <$> ("mul(" *> decimal <* ",") <*> decimal <* ")"
termsP = many' (mulP <|> junkP)
This parser tries to read a mul
term; if it can't, it consumes just one character and tries again. That reads the input properly, returning a list of Mul
or Junk
.
Evaluating
Evaluating the parse input is simple: Mul
terms get calculated, Junk
terms become zero, and the whole lot is added together.
part1 :: [Term] -> In
part1 = sum . fmap evalTerm
evalTerm :: Term -> Int
evalTerm (Mul a b) = a * b
evalTerm _ = 0
Dos and Don'ts
Part 2 includes do
and don't
terms. Those need to be added to the data structure and parser.
data Term = Mul Int Int
| Junk Char
| DoTerm
| DontTerm
deriving (Eq, Show)
junkP = Junk <$> anyChar
doP = DoTerm <$ "do()"
dontP = DontTerm <$ "don't()"
mulP = Mul <$> ("mul(" *> decimal <* ",") <*> decimal <* ")"
termsP = many' (choice [mulP, doP, dontP, junkP])
Note that choice
tries parsers in order, so the real terms are tried before we resort to calling a character junk
.
Given all the Mul
, Do
, and Dont
terms, I need to switch off the correct Mul
terms. That's a foldl'
over the list of terms, keeping track of both whether to include Mul
and the set of Mul
terms included so far. It has to be a left fold, as the order (left to right) of the terms matters.
There are four cases:
- Including
Mul
terms and I've found one: add it to the list of foundMul
s. - Reached a
DontTerm
: turn off including futureMul
s. - Reached a
DoTerm
: turn on including futureMul
s. - Anything else: leave the state unchanged.
enabledMuls :: [Term] -> [Term]
enabledMuls terms = snd $ foldl' go (True, []) terms
where go (True, ts) (Mul a b) = (True, Mul a b : ts)
go (_, ts) DontTerm = (False, ts)
go (_, ts) DoTerm = (True, ts)
go (c, ts) _ = (c, ts)
This returns the enabled Mul
terms in reverse order, but as I'm only summing them it doesn't matter.
part2 = sum . fmap evalTerm . enabledMuls
Code
You can get the code from my locally-hosted Git repo, or from Codeberg.