@@ -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
79https://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