Day 5 was an adventure in parsing! Luckily, my old faithful attoparsec
was up to the job.
Representation
The first thing was to sort out the representation. The Move
was a new agebraic data type, essentially a triple of <quantity, from, to>. A Crate
held its name. Each stack of crates was held as a list, with the topmost crate being the head of the list. The Wharf
was an IntMap [Crate]
from integer position to the stack of crates at that position.
data Crate = Crate Char deriving (Show, Eq)
type Wharf = M.IntMap [Crate]
data Move = Move Int Int Int -- quantity, from, to
deriving (Show, Eq)
extractName :: Crate -> Char
extractName (Crate c) = c
Parsing
Parsing the input was the hardest part of the puzzle, but it was mainly a case of follow the structure.
The input file comprised two parts: a picture of the wharf (the stacks of crates) and a list of moves. The picture of the wharf itself comprised two parts: a picture of the crates and a list of stack names see the example below).
[D]
[N] [C]
[Z] [M] [P]
1 2 3
move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2
The picture of the crates was the hard bit. I had to parse it in the order it appears in the file: row by row. But I wanted to end up with the crates column-by-column to I could store each stack in the Wharf
. Luckily, switching between rows and columns is handled by transpose
. But how to read the rows? And how to keep track of the blank spaces between the tallest few stacks?
If we imagine the picture is a grid of positions, each place in the grid is either occupied by a Crate
or is empty. In other words, each row is a list of Maybe Crate
.
That gave me the parser.
Working from the bottom up, a Crate
is a letter between square brackets. A blank is three spaces. Each cell in the picture is either a crate or blank. Each line of the picture is a bunch of cells separated by spaces (and at least one cell).
blankP = Nothing <$ (count 3 space)
crateP = (Just . Crate) <$> ("[" *> letter) <* "]"
wharfCellP = crateP <|> blankP
wharfLineP = wharfCellP `sepBy1` (char ' ')
Meanwhile, the stack label row is some whitespace, then some numbers separated by spaces, then some more whitespace. (Note that this consumes the newlines before and after this line.)
The wharf as a whole is a bunch of picture lines along with the label row.
stackLabelsP = (many1 space)
*> (decimal `sepBy` (many1 space))
<* (many1 space)
wharfP = (,) <$> (wharfLineP `sepBy` endOfLine) <*> stackLabelsP
Parsing the moves is easy in comparison. The moves are separated by newlines, and each move is some text delimiting the three numbers we want. Finally, the problem description is the wharf followed by the moves.
movesP = moveP `sepBy` endOfLine
moveP = Move <$> ("move " *> decimal)
<*> (" from " *> decimal)
<*> (" to " *> decimal)
problemP = (,) <$> wharfP <*> movesP
Then, to convert the output of the parser into the Wharf
. I do this in three stages:
- convert rows to columns
- combine all the
Just
values in each stack - combine the stacks with the labels to build the
Wharf
makeWharf :: [[Maybe Crate]] -> [Int] -> Wharf
makeWharf wharfLines colNames = M.fromList $ zip colNames wharfCols
where wharfCols = fmap catMaybes $ transpose wharfLines
Solving the tasks
There are two ways of moving crates around, so there are two sets of functions to perform them. In each case, applying a bunch of moves is a foldl
, folding each move into a wharf to create a new wharf. (It has to be done from the left, to preserve the order of operations.)
Part 2 is easier to explain.
A single move, moving a substack of crates from place to place, is done by applyMove2
. It finds the crates at the origin, finds the substack to move, finds the existing stack at the destination, then updates the origin and destination stacks. Applying the sequence of moves is the fold.
applyMoves2 :: Wharf -> [Move] -> Wharf
applyMoves2 wharf moves = foldl' applyMove2 wharf moves
applyMove2 :: Wharf -> Move -> Wharf
applyMove2 wharf (Move n from to) = M.insert from origin'
$ M.insert to destination wharf
where origin = wharf!from
moving = take n origin
origin' = drop n origin
destination = moving ++ (fromMaybe [] $ wharf!?to)
The moves in part 1 is the same pattern, but applying a single move instruction is itself the repeated application of a move 1 from here to there
, so that requires an additional level of fold. I use replicate
to generate the list of single-move steps from the one move instruction.
applyMoves1 :: Wharf -> [Move] -> Wharf
applyMoves1 wharf moves = foldl' applyMove1 wharf moves
applyMove1 :: Wharf -> Move -> Wharf
applyMove1 wharf m@(Move n _ _) = foldl' makeMove1 wharf (replicate n m)
makeMove1 :: Wharf -> Move -> Wharf
makeMove1 wharf (Move _ from to) = M.insert from origin
$ M.insert to destination wharf
where (c:origin) = wharf!from
destination = c:(fromMaybe [] $ wharf!?to)
All that's left is a convenience function to extract the names of the top crates on the wharf.
part1 :: Wharf -> [Move] -> String
part1 wharf moves = showTops $ applyMoves1 wharf moves
part2 :: Wharf -> [Move] -> String
part2 wharf moves = showTops $ applyMoves2 wharf moves
showTops :: Wharf -> String
showTops wharf = fmap extractName $ fmap (head . snd) $ M.toAscList wharf
Code
You can get the code from my locally-hosted Git repo, or from Gitlab.