Architecture & Resilience: Advent of Code 2025 Day 3 – Maximizing Battery Joltage

By Andy Carlberg | Published on 12/3/2025

Day 3 presented a fascinating sequence manipulation problem. My initial assumptions about data structures led to a flawed implementation, but TDD and persistent debugging eventually pointed the way to a highly optimized solution for both parts. The biggest challenge? Realizing the O(N) algorithm for Part 1, and then correctly applying a different O(N) algorithm (the stack) for Part 2.


Testing the Boundaries (TDD Setup)

As with the previous days, I used Gemini to generate a comprehensive test suite. This was important, as it immediately raised important edge cases:

  • Implicit Constraints: The initial tests assumed the input should be handled robustly, even including non-digit characters and lines with too few batteries. I decided to filter non- standard input, focusing on lines that met the minimal requirements (at least two batteries for Part 1).
  • The BigInt Revelation: For Part 2, Gemini’s generated tests immediately incorporated BigInt for the expected values, alerting me early that the final sum would exceed JavaScript’s safe integer limit. This saved significant debugging time later.
  • Catching Test Issues: Ironically, the generated suite itself had two flaws (misplaced comments being included as input and incorrect expected values). This experience reinforced the key lesson from Day 2: while the AI is a powerful TDD accelerant, it still requires careful human validation of its output, especially concerning specific input formats and core problem logic, to ensure architectural robustness.

Part 1: Finding the Max Two-Digit Joltage

The initial goal was to find the largest two-digit number formed by any pair of digits in the bank sequence, where the tens digit must appear before the ones digit in the sequence. This seemingly simple task became an early test of my ability to resist premature complexity and simplify my state management.

Make it Work: The Flawed Queue and the Logical Breakthrough

My initial approach was driven by a premature assumption about the data structure:

1.  Initial Misstep (The Queue): I believed a custom Queue was the solution, allowing me to maintain the last two digits and check their resulting joltage. This failed because I was only comparing the incoming digit to the front of the queue, completely missing the comprehensive check needed to find the maximum possible pair across the entire sequence.

2.  Debugging & Realization: After several failing tests, I realized the core problem wasn’t queue management; it was determining the best possible result from the entire bank in a single pass. The solution needed a greedy algorithm that tracked the best overall result, not just the last two digits.

3.  The O(N) Breakthrough: The necessary logic was realized: at any point, the maximum joltage is either the currentMax found so far, OR the new joltage formed by pairing the highest preceding digit found so far (bestTensDigit) with the current incoming digit. This single-pass comparison was the key to an efficient O(N) solution.

Make it Right: Refactoring for Clarity and Resilience

Once the logic was functionally correct, the focus shifted to code hygiene and architectural clarity. This phase strongly echoed lessons learned in previous days regarding separation of concerns and state simplification.

1.  Lessons in Abstraction and State: My initial custom Queue class was clumsy. It was doing too much, tracking an underlying array while trying to manage the overall maximum. This highlights the need to start with the simplest, clearest state representation possible.

2.  Refactoring to Minimal State: The most significant clarity improvement came from refactoring the “queue-like” logic to track only two essential pieces of state:

  • currentMax: The best result found globally so far.
  • bestTensDigit: The single most valuable digit seen previously (the best candidate for the tens place).

3.  Encapsulation of Business Logic: By focusing the class only on these two pieces of state and implementing the two-step greedy logic, the solution became transparent and resilient. The complex multi-case comparison was elegantly replaced by two simple, ordered checks, making the logic much easier to understand.

Make it Fast: Optimization and Efficiency

With the correct O(N) algorithm in place, the solution was already as fast as possible in terms of Big O complexity, as there’s no way to avoid iterating over every digit in the input bank.

  • Optimal Time Complexity: The reliance on the single-pass greedy algorithm ensured the complexity remained linear, O(N).
  • Any additional changes would provide minimal benefit and reduce the clarity unless you’re very familiar with JavaScript’s idiosyncrasies.

Part 2: Maximizing the Twelve-Digit Sequence

The constraint changed from finding the largest 2-digit number to finding the largest 12-digit number by dropping any extra digits.

Make it Work: The Stack

My initial assumption was that this was a queue problem, but it turns out that the true required structure is a stack (LIFO: Last-In, First-Out).

1.  Stack Logic: To form the largest number, every digit must be as large as possible, placed   as far to the left as possible.

2.  The Greedy Rule: When a new digit arrives, if it is larger than the digit currently at the   top of our sequence (stack), I pop the smaller digit off the stack, effectively deleting it,   because putting the larger digit to the left yields a bigger number. This process continues until   the stack top is greater than the new digit, or I run out of allowed drops.

3.  No Prefill: I initially tried to prefill the stack but realized that this could add digits   that should have been greedily dropped earlier. The solution must rely on the always-add   strategy: always push the currentDigit after the deletions, ensuring it’s available for   comparison with future digits.

The final structure was a single loop using a simple array as a stack, relying on pop() and push().

// Simplified logic for greedy deletion
while (digitsToDrop > 0 && currentDigit > stack[stack.length - 1]) {
    stack.pop();
    digitsToDrop--;
}
stack.push(currentDigit);

A final debug session was necessary to fix a regex error that excluded ‘0’ from valid banks, which caused one of the new test cases to fail. Once fixed, all tests passed.

Make it Right

The main cleanup focused on clarity:

  • Constants: Defined SEQUENCE_LENGTH = 12 to eliminate magic numbers.
  • Naming: Some variable names could be cleaned up for clarity.

Make it Fast

As in Part 1, no further optimization was possible without sacrificing clarity.


View the Full Codebase

The complete, final, and tested TypeScript solution for Day 3 is available for review on GitHub, demonstrating the implementation of the TDD and architectural principles discussed here.

View the Advent of Code 2025 Repository on GitHub

This process, driven by TDD, proved that even when initial assumptions are wrong, persistent testing and refactoring lead to the most correct and optimized solution.