Advent of Code 2022 day 5

    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:

    1. convert rows to columns
    2. combine all the Just values in each stack
    3. 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.

    Neil Smith

    Read more posts by this author.