Skip to content

Commit 2a5344c

Browse files
authored
Solution article for Day 01, 2025 by spamegg1 (#851)
* Solution article for Day 01, 2025 by spamegg1 Added detailed explanation of the puzzle, including parsing instructions, logic for rotating the dial, and solutions for both parts of the challenge. * Rename 'next' method to 'rotate' in Dial class * Refactor parse function for input parsing Updated the parse function to return an iterator instead of a sequence.
1 parent c214001 commit 2a5344c

1 file changed

Lines changed: 281 additions & 0 deletions

File tree

docs/2025/puzzles/day01.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,291 @@ import Solver from "../../../../../website/src/components/Solver.js"
22

33
# Day 1: Secret Entrance
44

5+
by [@spamegg1](https://github.com/spamegg1)
6+
57
## Puzzle description
68

79
https://adventofcode.com/2025/day/1
810

11+
## Solution summary
12+
13+
- Iterate over each line of the input to parse the rotation instructions:
14+
- For the direction of rotation, we will use -1 (left) or 1 (right).
15+
- For part 1, rotate the dial, keeping track of how many times it hits zero.
16+
- For part 2, rotate the dial, keeping track of how many times it passes through zero.
17+
- For both parts, we have to be careful about the modular arithmetic and edge cases!
18+
19+
## Parsing
20+
21+
We can parse each line into an instruction.
22+
Each instruction has a direction (left/right) and magnitude, or number of clicks.
23+
24+
Let's use [named tuples](https://www.scala-lang.org/api/3.7.4/docs/docs/reference/other-new-features/named-tuples.html) for a quick-and-dirty type alias:
25+
26+
```scala
27+
type Instr = (dir: Int, clicks: Int) // dir = ±1
28+
```
29+
30+
We can parse one instruction like this:
31+
32+
```scala
33+
def parseLine(line: String): Instr = line match
34+
case s"L$value" => (dir = -1, clicks = value.toInt)
35+
case s"R$value" => (dir = 1, clicks = value.toInt)
36+
```
37+
38+
Then parse all of the input:
39+
40+
```scala
41+
def parse(input: String) = input.linesIterator.map(parseLine)
42+
```
43+
44+
## Part 1
45+
46+
To keep track of things, let's make a case class for the dial:
47+
48+
```scala
49+
case class Dial(pointer: Int, hits: Int)
50+
```
51+
52+
### Rotating the dial
53+
54+
Let's think about how to calculate the next pointer.
55+
If the values stay within the range of 0-99 then we don't have a problem.
56+
For example, if `pointer = 32` and `instr = (dir = 1, clicks = 67)`
57+
then we end up at `32 + 67 = 99`.
58+
Or if `instr = (dir = -1, clicks = 29)` then we end up at `32 - 29 = 3`.
59+
60+
What happens when we go over 100, or we go into negative values?
61+
For example, if `pointer = 32` and `instr = (dir = 1, clicks = 70)`
62+
then we end up at `32 + 70 = 102`, but we should be at 2 instead.
63+
In this case we can reduce it modulo 100:
64+
65+
```scala
66+
scala> (32 + 70) % 100
67+
val res0: Int = 2
68+
```
69+
70+
However this does not quite work for negative values.
71+
For example, if `pointer = 32` and `instr = (dir = -1, clicks = 43)`
72+
then we end up at `32 - 43 = -11`, which should be `(32 - 43) % 100 = 89` instead.
73+
74+
But:
75+
76+
```scala
77+
scala> (32 - 43) % 100
78+
val res1: Int = -11
79+
```
80+
81+
So we need to make sure to always return a nonnegative number in the 0-99 range.
82+
We will have to add 100 in case the result is negative.
83+
Let's make our own mod function to correct this:
84+
85+
```scala
86+
def mod(n: Int, modulo: Int) =
87+
val res = n % modulo
88+
res + (if res < 0 then modulo else 0)
89+
```
90+
91+
If we want to be fancy about it, we can make it `infix` and into an
92+
[extension method](https://docs.scala-lang.org/scala3/reference/contextual/extension-methods.html) for `Int`:
93+
94+
```scala
95+
extension (n: Int)
96+
infix def mod(modulo: Int) =
97+
val res = n % modulo
98+
res + (if res < 0 then modulo else 0)
99+
```
100+
101+
### Following the instructions
102+
103+
Let's write the logic for processing one instruction at a time.
104+
If the next pointer is at 0, we increment the hits:
105+
106+
```scala
107+
case class Dial(pointer: Int, hits: Int):
108+
def rotate(instr: Instr): Dial =
109+
val newPointer = (pointer + instr.dir * instr.clicks) mod 100
110+
val newHits = hits + (if newPointer == 0 then 1 else 0)
111+
Dial(newPointer, newHits)
112+
```
113+
114+
Now process all instructions, in sequence. Initially the pointer is at 50.
115+
After, we just get the hit count of the final dial instance:
116+
117+
```scala
118+
def part1(input: String) =
119+
val instrs = parse(input)
120+
instrs
121+
.foldLeft(Dial(50, 0))((dial, instr) => dial.rotate(instr))
122+
.hits
123+
```
124+
125+
## Part 2
126+
127+
Now we need to count how many times we pass through 0 instead.
128+
Let's refactor our `Dial` class to account for this:
129+
130+
```scala
131+
case class Dial(pointer: Int, hits: Int, passes: Int)
132+
```
133+
134+
### Passing through 0
135+
136+
Let's think about instructions with large click counts.
137+
Say `pointer = 32` and `instr = (dir = 1, clicks = 680)`.
138+
Then we will end up at `32 + 680 = 712`. This passes through 0 exactly 7 times:
139+
140+
- once at 100
141+
- once at 200
142+
- ...
143+
- once at 700
144+
- finish at 712
145+
146+
In this case the simple arithmetic `(32 + 680) / 100 = 7` gives us the correct result.
147+
148+
What about negative instructions?
149+
Say `pointer = 32` and `instr = (dir = -1, clicks = 680)`.
150+
This passes through 0 exactly 7 times:
151+
152+
- once at 0
153+
- once at -100
154+
- once at -200
155+
- ...
156+
- once at -600
157+
- finish at -648
158+
159+
In this case the simple arithmetic `(32 - 680) / 100 = -6` is **not** the correct result.
160+
Neither is its absolute value `-6.abs = 6`. It seems like we are off-by-one!
161+
162+
### Clicks needed to reach zero at least once
163+
164+
Let's think about a correct logic for both examples.
165+
It would be really awesome if we always started at 0 every time, wouldn't it?
166+
So let's simplify the problem, first reach 0, then deal with the rest of it.
167+
168+
In the first example,
169+
170+
- starting from 32 rotating right,
171+
- we first needed 68 points to reach 0 again: `32 + 68 = 100`.
172+
- Those 68 clicks are spent and got us to 0,
173+
- now there are `680 - 68 = 612` clicks to go, and we are at 0.
174+
- This should give us `612 / 100 = 6` round trips, for a total of `6 + 1 = 7` passes.
175+
176+
In the second example,
177+
178+
- starting from 32 rotating left,
179+
- we first needed 32 points to reach 0 again: `32 - 32 = 0`.
180+
- Those 32 clicks are spend and got us to 0,
181+
- now there are `680 - 32 = 648` clicks to go, and we are at 0.
182+
- This should give us `648 / 100 = 6` round trips, for a total of `6 + 1 = 7` passes.
183+
184+
So the number of clicks needed to reach zero is
185+
186+
- `100 - pointer` if we are rotating right (positive),
187+
- `pointer` if we are rotating left (negative).
188+
189+
Then we can divide the remaining clicks by 100 to count the round trips.
190+
So, the number of passes will be 1 + round trips.
191+
192+
### Edge cases
193+
194+
What if the pointer is already at 0? Then, in either direction,
195+
the number of clicks to pass through 0 again is 100, not zero!
196+
Because we have to go all the way round. *Being* at 0 already does not count as a pass.
197+
198+
We should also be careful with off-by-one errors.
199+
If we never reach zero, round trip count will be 0, but passes **won't** be `1 + 0 = 1`.
200+
The passes will still be 0, because we never reached zero.
201+
So we need to check if we can reach zero at least once:
202+
203+
```scala
204+
// val clicksToReachZero = ...
205+
// ...
206+
val passZeroAtLeastOnce = instr.clicks >= clicksToReachZero
207+
```
208+
209+
### Coding the logic
210+
211+
Carefully, we can put these ideas at work.
212+
Now our `rotate` method has logic for both parts:
213+
214+
```scala
215+
case class Dial(pointer: Int, hits: Int, passes: Int):
216+
def rotate(instr: Instr): Dial =
217+
// part 1
218+
val newPointer = (pointer + instr.dir * instr.clicks) mod 100
219+
val newHits = hits + (if newPointer == 0 then 1 else 0)
220+
221+
// part 2
222+
val clicksToReachZero =
223+
if instr.dir == -1 then (if pointer == 0 then 100 else pointer)
224+
else 100 - pointer
225+
val roundTrips = (instr.clicks - clicksToReachZero) / 100
226+
val passZeroAtLeastOnce = instr.clicks >= clicksToReachZero
227+
val newPasses = passes + (if passZeroAtLeastOnce then roundTrips + 1 else 0)
228+
Dial(newPointer, newHits, newPasses)
229+
```
230+
231+
Then we can use the same code from part 1, but get the passes at the end:
232+
233+
```scala
234+
def part2(input: String) =
235+
val instrs = parse(input)
236+
instrs
237+
.foldLeft(Dial(50, 0, 0))((dial, instr) => dial.rotate(instr))
238+
.passes
239+
```
240+
241+
## Final code
242+
243+
I'll iterate over the instructions just once for both parts together,
244+
since `Dial` will contain solutions to both parts.
245+
246+
```scala
247+
type Instr = (dir: Int, clicks: Int) // dir = ±1
248+
249+
extension (n: Int)
250+
infix def mod(modulo: Int) =
251+
val res = n % modulo
252+
res + (if res < 0 then modulo else 0)
253+
254+
def parseLine(line: String): Instr = line match
255+
case s"L$value" => (dir = -1, clicks = value.toInt)
256+
case s"R$value" => (dir = 1, clicks = value.toInt)
257+
258+
def parse(input: String): Seq[Instr] = input
259+
.linesIterator
260+
.map(parseLine)
261+
.toSeq
262+
263+
case class Dial(pointer: Int, hits: Int, passes: Int):
264+
def rotate(instr: Instr): Dial =
265+
// part 1
266+
val newPointer = (pointer + instr.dir * instr.clicks) mod 100
267+
val newHits = hits + (if newPointer == 0 then 1 else 0)
268+
269+
// part 2
270+
val clicksToReachZero =
271+
if instr.dir == -1 then (if pointer == 0 then 100 else pointer)
272+
else 100 - pointer
273+
val roundTrips = (instr.clicks - clicksToReachZero) / 100
274+
val passZeroAtLeastOnce = instr.clicks >= clicksToReachZero
275+
val newPasses = passes + (if passZeroAtLeastOnce then roundTrips + 1 else 0)
276+
Dial(newPointer, newHits, newPasses)
277+
278+
def part1(input: String) =
279+
parse(input)
280+
.foldLeft(Dial(50, 0, 0))((dial, instr) => dial.rotate(instr))
281+
.hits
282+
283+
def part2(input: String) =
284+
parse(input)
285+
.foldLeft(Dial(50, 0, 0))((dial, instr) => dial.rotate(instr))
286+
.passes
287+
```
288+
289+
9290
## Solutions from the community
10291

11292
- [Solution](https://github.com/merlinorg/advent-of-code/blob/main/src/main/scala/year2025/day01.scala) by [merlinorg](https://github.com/merlinorg)

0 commit comments

Comments
 (0)