Another year, another Advent of Code. This year has moved to an abbreviated format, with 12 puzzles rather than 25. I fully appreciate that producing these puzzles is a huge amount of work, so I understand Eric Wastl's desire to spend something less than every waking hour on this project. Thank you, Eric, for all the previous Advents of Code, and I hope the changes mean the project is sustainable in future!
But on with day 1's puzzle.
Part 1
As the instructions are Left and Right, I decided to misuse the Either datatype and use it to represent instructions. Parsing the input file is as fragile as fragile can be, but works for the very clean input of the puzzle.
type Instruction = Either Int Int
readInstruction :: String -> Instruction
readInstruction (dir:qty) =
case dir of
'L' -> Left (read qty)
'R' -> Right (read qty)
_ -> error "invalid direction"Applying instructions is a foldl along the list of instructions, updating the position of the dial as we go. As the dial has 100 positions and wraps around, I take the new position modulus 100. In this instance, I want the position after each instruction rather than only the final position, so I use scanl rather than foldl.
Once I have all the positions, use filter to find all the zeros and length to count them.
part1 instructions = length $ filter (==0) positions
where positions = scanl' move 50 instructions
move here (Left n) = (here - n) `mod` 100
move here (Right n) = (here + n) `mod` 100Part 2
Part 2 was to count the number of times the dial reaches or crosses the zero position. That means I can't keep track of just the end points of the moves, but I keep a running total of the zero meet-or-crossings. That means the overall control structure reverts to being foldl rather than scanl.
My attempts were a mass of off-by-one errors. position mod 100 gives the dial position, and position div 100 gives (mostly) the number of rotations. If all the rotations were rightwards, that would be all that was needed! It took me far longer to sort out all the errors that I should admit to, even if I ended up with something fairly clear.
If I start in position 30 and apply a move of Left 55, I end in position -25. -25 divided by 100 is -1 remainder 75. The -1 captures the one zero-crossing I need.
However, starting in position 30 and moving Left 30 ends at 0. The division gives 0, but I want to record a zero meet-or-crossing. Similarly, ending at -100, -200, or so on would give an an additional meet-or-crossing to record. Conversely, starting at position 0 and moving Left 30 ends at -30, or -1 remainder 70, and I don't want to record that zero meet-or-crossing.
That gives a fiddly little correction parameter into the definition of move.
part2 instructions = snd $ foldl' move2 (50, 0) instructions
move2 :: (Int, Int) -> Instruction -> (Int, Int)
move2 (here, count) instruction = (there `mod` 100, count + rotations + correction)
where there = case instruction of
Left n -> (here - n)
Right n -> (here + n)
rotations = abs (there `div` 100)
-- count extra when turning left to end at a multiple of 100
correction = if | there <= 0 && (there `mod` 100) == 0 -> 1
-- count less when turning left away from zero
| there < 0 && here == 0 -> -1
| otherwise -> 0Code
You can get the code from Codeberg.