The first puzzle of 2025's AoC (available at https://adventofcode.com/2025/day/1) introduces us to a story where elves have discovered project management. Good for them. Now we need to help open a safe — time to break some law.
The safe has a dial that starts at position 50, and we need to count the number of times the dial points at zero after processing a sequence of rotations. The dial has 100 positions (0-99) and wraps around.
Parsing the Input
The input consists of instructions in the format L68 or R30, where:
Rmeans rotate right (toward higher numbers: 1, 2, 3, 4...)Lmeans rotate left (toward lower numbers: 49, 48... 1, 0, 99, 98...)
As always, I start by defining proper types for the data:
type Instruction = {
direction: "R" | "L";
steps: number;
};
const processInput = (day: number): Instruction[] => {
const lines = readInputLines(day);
const instructions = lines.map(line => {
const direction = line.charAt(0) as "R" | "L";
const steps = parseInt(line.slice(1), 10);
return { direction, steps };
});
return instructions;
};
The toLock Function
Before we dive into the solutions, we need to handle JavaScript's quirky modulo behavior. When dealing with negative numbers:
-1 % 100 // returns -1, not 99
-300 % 100 // returns -0 (yes, negative zero is a thing in JavaScript!)
We need a proper modulo function that always returns a value between 0 and 99:
const toLock = (n: number) => (n % 100 + 100) % 100;
This works by:
- First applying
% 100, which gives us a number between -99 and +99 - Adding 100 to guarantee a positive number (between 1 and 199)
- Applying
% 100again to get back to the 0-99 range
Part One: Counting Zero Landings
For part one, we need to count how many times the dial lands exactly on zero after applying each instruction:
const partOne = (input: Instruction[], debug: boolean) => {
let position = 50;
let result = 0;
for (const instruction of input) {
if (instruction.direction === "R") {
position += instruction.steps;
} else {
position -= instruction.steps;
}
position = toLock(position);
if (position === 0) {
result += 1;
}
}
return result;
};
With the toLock function handling the wraparound correctly, part one returns 141 for my input.
Part Two: Counting Zero Crossings
Part two changes the requirement - now we need to count every time the dial passes through zero during rotation, not just when it lands on zero at the end.
For example, starting at position 50 and rotating left 68 steps means we pass through zero once on our way to position 82.
The mathematically elegant solution would be to calculate how many times we cross zero based on the start position, end position, and direction. But I chose the simpler (and admittedly less efficient) approach: simulate every single click.
const partTwo = (input: Instruction[], debug: boolean) => {
let position = 50;
let result = 0;
for (const instruction of input) {
if (instruction.direction === "R") {
let clicks = instruction.steps;
while (clicks > 0) {
position += 1;
position = toLock(position);
if (position === 0) {
result += 1;
}
clicks -= 1;
}
} else {
let clicks = instruction.steps;
while (clicks > 0) {
position -= 1;
position = toLock(position);
if (position === 0) {
result += 1;
}
clicks -= 1;
}
}
}
return result;
};
Performance Considerations
This simulation approach is wasteful. If we rotate 200 times, we're doing 200 loop iterations when we could just calculate that we pass zero twice (200 / 100 = 2).
However, a quick analysis of the input shows:
- Maximum steps per instruction: 999
- Total instructions: ~4,500
That's a few million iterations at worst - child's play for modern hardware. Part two runs in about 14 milliseconds compared to 1 millisecond for part one.
The answer for part two is 6,634.
Takeaways
Know your language's quirks: JavaScript's modulo behavior with negative numbers catches many developers off guard. The pattern
(n % m + m) % mis a common fix.Simple solutions are often good enough: The brute-force simulation for part two is inefficient but perfectly adequate. Premature optimization is the root of all evil, and this code is readable and obviously correct.
Type your data early: Defining the
Instructiontype immediately makes the rest of the code cleaner and catches errors at compile time rather than runtime.
The full solution is available in the repository.