Day 1: Secret Entrance
by @spamegg1
Puzzle description
https://adventofcode.com/2025/day/1
Solution summary
- Iterate over each line of the input to parse the rotation instructions:
- For the direction of rotation, we will use -1 (left) or 1 (right).
- For part 1, rotate the dial, keeping track of how many times it hits zero.
- For part 2, rotate the dial, keeping track of how many times it passes through zero.
- For both parts, we have to be careful about the modular arithmetic and edge cases!
Parsing
We can parse each line into an instruction. Each instruction has a direction (left/right) and magnitude, or number of clicks.
Let's use named tuples for a quick-and-dirty type alias:
type Instr = (dir: Int, clicks: Int) // dir = ±1
We can parse one instruction like this:
def parseLine(line: String): Instr = line match
case s"L$value" => (dir = -1, clicks = value.toInt)
case s"R$value" => (dir = 1, clicks = value.toInt)
Then parse all of the input:
def parse(input: String) = input.linesIterator.map(parseLine)
Part 1
To keep track of things, let's make a case class for the dial:
case class Dial(pointer: Int, hits: Int)
Rotating the dial
Let's think about how to calculate the next pointer.
If the values stay within the range of 0-99 then we don't have a problem.
For example, if pointer = 32 and instr = (dir = 1, clicks = 67)
then we end up at 32 + 67 = 99.
Or if instr = (dir = -1, clicks = 29) then we end up at 32 - 29 = 3.
What happens when we go over 100, or we go into negative values?
For example, if pointer = 32 and instr = (dir = 1, clicks = 70)
then we end up at 32 + 70 = 102, but we should be at 2 instead.
In this case we can reduce it modulo 100:
scala> (32 + 70) % 100
val res0: Int = 2
However this does not quite work for negative values.
For example, if pointer = 32 and instr = (dir = -1, clicks = 43)
then we end up at 32 - 43 = -11, which should be (32 - 43) % 100 = 89 instead.
But:
scala> (32 - 43) % 100
val res1: Int = -11
So we need to make sure to always return a nonnegative number in the 0-99 range. We will have to add 100 in case the result is negative. Let's make our own mod function to correct this:
def mod(n: Int, modulo: Int) =
val res = n % modulo
res + (if res < 0 then modulo else 0)
If we want to be fancy about it, we can make it infix and into an
extension method for Int:
extension (n: Int)
infix def mod(modulo: Int) =
val res = n % modulo
res + (if res < 0 then modulo else 0)
Following the instructions
Let's write the logic for processing one instruction at a time. If the next pointer is at 0, we increment the hits:
case class Dial(pointer: Int, hits: Int):
def rotate(instr: Instr): Dial =
val newPointer = (pointer + instr.dir * instr.clicks) mod 100
val newHits = hits + (if newPointer == 0 then 1 else 0)
Dial(newPointer, newHits)
Now process all instructions, in sequence. Initially the pointer is at 50. After, we just get the hit count of the final dial instance:
def part1(input: String) =
val instrs = parse(input)
instrs
.foldLeft(Dial(50, 0))((dial, instr) => dial.rotate(instr))
.hits
Part 2
Now we need to count how many times we pass through 0 instead.
Let's refactor our Dial class to account for this:
case class Dial(pointer: Int, hits: Int, passes: Int)
Passing through 0
Let's think about instructions with large click counts.
Say pointer = 32 and instr = (dir = 1, clicks = 680).
Then we will end up at 32 + 680 = 712. This passes through 0 exactly 7 times:
- once at 100
- once at 200
- ...
- once at 700
- finish at 712
In this case the simple arithmetic (32 + 680) / 100 = 7 gives us the correct result.
What about negative instructions?
Say pointer = 32 and instr = (dir = -1, clicks = 680).
This passes through 0 exactly 7 times:
- once at 0
- once at -100
- once at -200
- ...
- once at -600
- finish at -648
In this case the simple arithmetic (32 - 680) / 100 = -6 is not the correct result.
Neither is its absolute value -6.abs = 6. It seems like we are off-by-one!
Clicks needed to reach zero at least once
Let's think about a correct logic for both examples. It would be really awesome if we always started at 0 every time, wouldn't it? So let's simplify the problem, first reach 0, then deal with the rest of it.
In the first example,
- starting from 32 rotating right,
- we first needed 68 points to reach 0 again:
32 + 68 = 100. - Those 68 clicks are spent and got us to 0,
- now there are
680 - 68 = 612clicks to go, and we are at 0. - This should give us
612 / 100 = 6round trips, for a total of6 + 1 = 7passes.
In the second example,
- starting from 32 rotating left,
- we first needed 32 points to reach 0 again:
32 - 32 = 0. - Those 32 clicks are spend and got us to 0,
- now there are
680 - 32 = 648clicks to go, and we are at 0. - This should give us
648 / 100 = 6round trips, for a total of6 + 1 = 7passes.
So the number of clicks needed to reach zero is
100 - pointerif we are rotating right (positive),pointerif we are rotating left (negative).
Then we can divide the remaining clicks by 100 to count the round trips. So, the number of passes will be 1 + round trips.
Edge cases
What if the pointer is already at 0? Then, in either direction, the number of clicks to pass through 0 again is 100, not zero! Because we have to go all the way round. Being at 0 already does not count as a pass.
We should also be careful with off-by-one errors.
If we never reach zero, round trip count will be 0, but passes won't be 1 + 0 = 1.
The passes will still be 0, because we never reached zero.
So we need to check if we can reach zero at least once:
// val clicksToReachZero = ...
// ...
val passZeroAtLeastOnce = instr.clicks >= clicksToReachZero
Coding the logic
Carefully, we can put these ideas at work.
Now our rotate method has logic for both parts:
case class Dial(pointer: Int, hits: Int, passes: Int):
def rotate(instr: Instr): Dial =
// part 1
val newPointer = (pointer + instr.dir * instr.clicks) mod 100
val newHits = hits + (if newPointer == 0 then 1 else 0)
// part 2
val clicksToReachZero =
if instr.dir == -1 then (if pointer == 0 then 100 else pointer)
else 100 - pointer
val roundTrips = (instr.clicks - clicksToReachZero) / 100
val passZeroAtLeastOnce = instr.clicks >= clicksToReachZero
val newPasses = passes + (if passZeroAtLeastOnce then roundTrips + 1 else 0)
Dial(newPointer, newHits, newPasses)
Then we can use the same code from part 1, but get the passes at the end:
def part2(input: String) =
val instrs = parse(input)
instrs
.foldLeft(Dial(50, 0, 0))((dial, instr) => dial.rotate(instr))
.passes
Final code
I'll iterate over the instructions just once for both parts together,
since Dial will contain solutions to both parts.
type Instr = (dir: Int, clicks: Int) // dir = ±1
extension (n: Int)
infix def mod(modulo: Int) =
val res = n % modulo
res + (if res < 0 then modulo else 0)
def parseLine(line: String): Instr = line match
case s"L$value" => (dir = -1, clicks = value.toInt)
case s"R$value" => (dir = 1, clicks = value.toInt)
def parse(input: String): Seq[Instr] = input
.linesIterator
.map(parseLine)
.toSeq
case class Dial(pointer: Int, hits: Int, passes: Int):
def rotate(instr: Instr): Dial =
// part 1
val newPointer = (pointer + instr.dir * instr.clicks) mod 100
val newHits = hits + (if newPointer == 0 then 1 else 0)
// part 2
val clicksToReachZero =
if instr.dir == -1 then (if pointer == 0 then 100 else pointer)
else 100 - pointer
val roundTrips = (instr.clicks - clicksToReachZero) / 100
val passZeroAtLeastOnce = instr.clicks >= clicksToReachZero
val newPasses = passes + (if passZeroAtLeastOnce then roundTrips + 1 else 0)
Dial(newPointer, newHits, newPasses)
def part1(input: String) =
parse(input)
.foldLeft(Dial(50, 0, 0))((dial, instr) => dial.rotate(instr))
.hits
def part2(input: String) =
parse(input)
.foldLeft(Dial(50, 0, 0))((dial, instr) => dial.rotate(instr))
.passes
Solutions from the community
Share your solution to the Scala community by editing this page. You can even write the whole article! Go here to volunteer