I was expecting a bit of a difficulty curve up for today and while we did get one, it happened to hit my skills very well so it didn’t feel like such. As soon as I read the puzzle description, I knew I’d be reaching for regular expressions. The link is to a blog post of mine where I go through how regular expressions work in a bit more detail.

Reading input, part 1

This time I’ve split the input reading to two parts because they require different treatment. It’s also important to note that this time, the example input is different in the two parts. I did not expect that so I spent a good chunk of time debugging my regular expression in the second part.

For the first part, our example input looks like this:

xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))

It’s a bunch of multiplication clauses, some of which are corrupted. We need to find all clauses that match mul([int], [int]), for example mul(2,4) near the beginning.

p1_pattern = r"mul\((\d+),(\d+)\)"
 
def map_fn(line):
    return re.findall(p1_pattern, line)

I created a regular expression that matches a string of the given format and I capture both of the numbers inside. The (\d+) that’s there twice means: 1. find all numbers (\d+) and 2. capture them (the parenthesis around it).

Once that’s run against the line, it will create a list of tuples where the first item in the tuple is the first number and the second item is the second number. When run against the example input, the result is

[('2', '4'), ('5', '5'), ('11', '8'), ('8', '5')]

Part 1: Multiplications

Regular expression in the data parsing does most of the heavy lifting in this solution.

def part_1():
    lines = read_input(3, map_fn)
    result = 0
    for instructions in lines:
        for a, b in instructions:
            result += int(a) * int(b)
 
    print(f"Part 1: {result}")

Once we’ve read in all the lines, we loop over them, then loop over all the instructions (pairs of numbers), cast them to integers, multiply together and add to our result.

Reading input, part 2

In the second part, we also care about do() and don't() instructions that either enable or disable multiplication readings.

Now the example input has changed to:

xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))

I adjusted the regular expression pattern:

p2_pattern = r"(mul\((\d+),(\d+)\)|do\(\)|don't\(\))"
 
def map_fn_2(line):
    return re.findall(p2_pattern, line)

Here, we capture either mul(a,b), do() or don't(). The | character between them in the pattern means OR.

When we use re.findall with this one, it will capture the entire set (everything is wrapped inside parentheses) into the first tuple item and then the two numbers into the next two items.

With the new example input, it results in:

[('mul(2,4)', '2', '4'), ("don't()", '', ''), ('mul(5,5)', '5', '5'), ('mul(11,8)', '11', '8'), ('do()', '', ''), ('mul(8,5)', '8', '5')]

Part 2: Conditional multiplications

def part_2():
    lines = read_input(3, map_fn_2)
    result = 0
    enabled = True
    for instructions in lines:
        for instruction in instructions:
            match instruction:
                case ("do()", _, _):
                    enabled = True
                case ("don't()", _, _):
                    enabled = False
                case (operator, a, b) if enabled and "mul" in operator:
                    result += int(a) * int(b)
    print(f"Part 2: {result}")

Part 2’s code is a bit more complex.

In this part, we need to keep track of whether our multiplication is enabled or not and we do it with a boolean variable enabled.

Our main logic happens inside pattern matching flow. In the pattern matching, we start with a match statement declaring which variable are we matching against. In our case, it’s the entire instruction (which is in form (operator, a, b)).

In each case statement, we match against a specific “blueprint”:

case ("do()", _, _) matches any tuple where the first item is literal "do()" and the other two can be anything. Similarly with the second case with "don't()".

The third case matches anything else and it maps the first item into variable operator, second into a and third into b. We then check if the enabled flag is set and if the operator includes the word “mul”.

Today’s puzzle was fun. I made couple of silly mistakes (like writing 2 instead of 3 into my read input arguments and wondering for a long time why my regular expression didn’t work) and then not noticing the example input changed between the parts.

I got to use two of the functionalities I really like: regular expressions and pattern matching. I’m sure we get to see a whole lot more of them in the future as well. In terms of this digital garden experiment, if I were new to either of them, I would have written separate notes about them in my Knowledge folder.