The problem
Yep, I’m sure part 1 was easy—just to lure me into a false sense of security. 😅
The new requirement is to handle do() and don't() instructions:
do(): Enables processing of subsequent instructions.don't(): Disables processing of subsequent instructions.
Disabled instructions are skipped entirely. Here’s an example:
xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))
In this string, only two mul instructions should be processed instead of four.
Important Note: The input starts in the “enabled” state by default, so there’s no do() instruction at the beginning.
The plan
The approach seemed simple enough:
- Use a regex to identify all substrings starting with
don't()and ending at the nextdo(). - Remove those substrings.
- Process the remaining instructions.
I used Rubular to visually test the regex and came up with this:
/(don't\(\)).*?(?=do\(\))/
The implementation (and a failure)
With the regex in place, I deleted the matching substrings and ran the original Part 1 code on the modified input.
Result: The program produced a number, but it was wrong.
Debugging Steps:
- I visually checked the regex against the input, and it matched correctly for smaller test strings.
- However, when I tested the full input, Rubular couldn’t handle it (the input was too long).
I thought the problem might be related to line terminators (\n). To fix this:
- I replaced all
\ncharacters with a dot (.), which wouldn’t interfere with the regex. - Ran the code again, and… still wrong.
Then it hit me: The regex assumed there would always be a do() after a don't(). But what if the input ended before the next do()?
Fixing the regex
I updated the regex to also match the end of the string ($) as a possible terminator:
/(don't\(\)).*?(?=do\(\)|$)/
Finally, the program returned the correct number!
The final implementation
Here’s the complete solution:
input = File.read("puzzle_input.txt").gsub("\n", '.')
# input = File.read "./example.txt"
# input = File.read "./example2.txt"
sum = 0
sum_pt_2 = 0
r = /(don\'t\(\)).*?(?=do\(\)|$)/
def instructions(s)
s.scan(/mul\((\d+),(\d+)\)/)
end
part1 = instructions(input)
part2 = instructions(input.gsub(r, ''))
part1.each do |m|
sum += m.map(&:to_i).inject(:*)
end
part2.each do |line|
sum_pt_2 += line.map(&:to_i).inject(:*)
end
puts sum
puts sum_pt_2
Possible improvements
- Refactor the loops:
The
part{1,2}.eachloops are identical. They could be extracted into a reusable method that takes the instruction list and returns the sum. Example:def calculate_sum(instructions) instructions.sum { |line| line.map(&:to_i).inject(:*) } end -
Cleaner Input Handling: Instead of modifying the input to replace
\nwith., process each line separately or use multiline regex flags (/m) to handle line terminators correctly. - Optimized Regex Usage: For larger inputs probably a custom parser would be a better idea.
Final thoughts
This problem was trickier than it seemed at first glance. Debugging the regex required understanding edge cases (like missing do() instructions) and adapting accordingly.
Regex is powerful, but it can be fragile—especially with large and complex inputs. For more complex instruction handling, a dedicated parser might be worth considering.