Advent of Code 2022 - Day 5: Supply Stacks
It's Day 5, and it's time to move some crates. We're given 9 initial stacks of crates, and instructions for how a crane moves crates from one stack to another. Here is our sample input:
[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
Initial thought: There's no way I'm parsing that stack diagram!
Parsing Moves
Based on parsing tricks I've learned recently, parsing the move instructions is straightforward:
data Move = Move Int Int Int
parseMove :: GenParser Char u Move
parseMove =
liftA3
Move
(string "move " >> parseInt)
(string " from " >> parseInt)
(string " to " >> parseInt)
-- parseInt now lives in my reusable Input module:
parseInt :: GenParser Char u Int
parseInt = read <$> many1 digit
Modeling the Stacks
Lists are a great way to represent stacks in just about any functional language like Haskell, since you can push and pop the top of the list while the tail remains unmodified. But how will we represent our set of 9 stacks? For that, I opted for an array. Compared to Lists, Arrays in Haskell are fixed size and provide faster access and updates to indexed elements. We will be looking up individual stacks and modifying them for each move instruction, so this is perfect for our needs.
Like I said, I wasn't going to parse that initial structure. Here is my input, hardcoded as an array. The start of each string is the top of each stack.
stacks :: Array Int String
stacks =
listArray
(1, 9)
[ "BVWTQNHD",
"BWD",
"CJWQST",
"PTZNRJF",
"TSMJVPG",
"NTFWB",
"NVHFQDLB",
"RFPH",
"HPNLBMSZ"
]
Haskell sidesteps the debate about 0-based and 1-based indexing, as you need to specify the index range explicitly! Here, our stacks start at 1, so we use 1-9 instead of 0-8.
Applying Moves to Our Stack
Now all we have to do is fold our list of Moves onto our Array. Part 1 is actually a little trickier than Part 2 here, as we can only move one item at a time. If we're moving more than one item, let's just move one and then recurse.
move :: Array Int String -> Move -> Array Int String
move a (Move 0 _ _) = a
move a (Move n x y) =
let (h, rest) = splitAt 1 (a ! x)
in move (a // [(x, rest), (y, h ++ (a ! y))]) (Move (n - 1) x y)
(a ! x)
accesses the array element at index `x`. This code splits that element into the top item and the rest. It uses (a // assocs)
to update the elements at the old and new index. Then it continues on, moving the next crate until the move is complete.
Part 2 is simpler, as we can move them all in one go:
moveAll :: Array Int String -> Move -> Array Int String
moveAll a (Move n x y) =
let (pickedUp, rest) = splitAt n (a ! x)
in a // [(x, rest), (y, pickedUp ++ (a ! y))]
Finally, we need to take the top item from each stack and concatenate them into a string:
peekStacks :: Array Int String -> String
peekStacks = fmap head . elems
part1 :: [Move] -> String
part1 = peekStacks . foldl move stacks
part2 :: [Move] -> String
part2 = peekStacks . foldl moveAll stacks
Is Parsing Really So Bad?
I solved this last night. When I woke up, I was refreshed and really didn't want to let this input get the best of me. How might we parse these stacks? Here's the plan:
- Parse each row one by one.
- Rows are either
"[A]"
or"\s\s\s"
, separated by spaces. In either case, grab the middle character. - After we parse all rows, transpose them to get our stacks, filtering out any blank spaces.
Here is how that ended up looking:
parseStacks :: GenParser Char u [String]
parseStacks = fmap catMaybes . transpose <$> parseLines
where
parseCrate = find isLetter . Just <$> (oneOf "[ " *> anyChar <* oneOf " ]")
parseLine = parseCrate `sepBy1` char ' '
parseLines = parseLine `endBy1` newline
I stopped here. It should be trivial to parse this from the input file, and then parse the rest of the moves afterwards. We'll also need to convert our list of Strings into an Array by examining the length.
Full Code
module AOC2022.Day05 (spec) where
import Control.Applicative
import Data.Array
import Data.Char
import Data.List
import Data.Maybe
import Input
import Test.Hspec
import Text.ParserCombinators.Parsec
stacks :: Array Int String
stacks =
listArray
(1, 9)
[ "BVWTQNHD",
"BWD",
"CJWQST",
"PTZNRJF",
"TSMJVPG",
"NTFWB",
"NVHFQDLB",
"RFPH",
"HPNLBMSZ"
]
data Move = Move Int Int Int
parseMove :: GenParser Char u Move
parseMove =
liftA3
Move
(string "move " >> parseInt)
(string " from " >> parseInt)
(string " to " >> parseInt)
input :: IO (Either ParseError [Move])
input = traverse (parse parseMove "") . lines <$> readDay 2022 5
spec :: IO ()
spec = hspec $ do
describe "Part 1" $ do
it "parses the stacks" $ do
part1 [] `shouldBe` "BBCPTNNRH"
it "runs on custom input" $ do
myInput <- input
part1 <$> myInput `shouldBe` Right "_________" -- redacted
describe "Part 2" $ do
it "runs on custom input" $ do
myInput <- input
part2 <$> myInput `shouldBe` Right "_________" -- redacted
describe "Parsing" $ do
it "parses the raw stack string" $ do
parse parseStacks "" rawStacks
`shouldBe` Right
[ "BVWTQNHD",
"BWD",
"CJWQST",
"PTZNRJF",
"TSMJVPG",
"NTFWB",
"NVHFQDLB",
"RFPH",
"HPNLBMSZ"
]
move :: Array Int String -> Move -> Array Int String
move a (Move 0 _ _) = a
move a (Move n x y) =
let (h, rest) = splitAt 1 (a ! x)
in move (a // [(x, rest), (y, h ++ (a ! y))]) (Move (n - 1) x y)
peekStacks :: Array Int String -> String
peekStacks = fmap head . elems
part1 :: [Move] -> String
part1 = peekStacks . foldl move stacks
moveAll :: Array Int String -> Move -> Array Int String
moveAll a (Move n x y) =
let (pickedUp, rest) = splitAt n (a ! x)
in a // [(x, rest), (y, pickedUp ++ (a ! y))]
part2 :: [Move] -> String
part2 = peekStacks . foldl moveAll stacks
-- Parsing
rawStacks :: String
rawStacks =
unlines
[ "[B] [N] [H]",
"[V] [P] [T] [V] [P]",
"[W] [C] [T] [S] [H] [N]",
"[T] [J] [Z] [M] [N] [F] [L]",
"[Q] [W] [N] [J] [T] [Q] [R] [B]",
"[N] [B] [Q] [R] [V] [F] [D] [F] [M]",
"[H] [W] [S] [J] [P] [W] [L] [P] [S]",
"[D] [D] [T] [F] [G] [B] [B] [H] [Z]",
" 1 2 3 4 5 6 7 8 9 "
]
parseStacks :: GenParser Char u [String]
parseStacks = fmap catMaybes . transpose <$> parseLines
where
parseCrate = find isLetter . Just <$> (oneOf "[ " *> anyChar <* oneOf " ]")
parseLine = parseCrate `sepBy1` char ' '
parseLines = parseLine `endBy1` newline
Advent of Code 2022 Series
This post is part of a series describing my Haskell solutions to Advent of Code 2022.
Next: Day 6: Tuning Trouble Previous: Day 4: Camp Cleanup
Cheers!