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:

  1. Including Mul terms and I've found one: add it to the list of found Muls.
  2. Reached a DontTerm: turn off including future Muls.
  3. Reached a DoTerm: turn on including future Muls.
  4. 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.