2024-12-03 - Mull It Over
(original .ipynb)
Day 3 puzzle input is a bunch of text with something that looks like function calls embedded (mul(x,y)
, don't()
, do()
) scattered throughout, mine is here). Part 1 involves evaluating each mul
expression and summing the results. Part 2 involves doing the same but in-order, but ignoring all mul
expressions after a dont
call until we hit a do
call.
I misunderstood the task - I thought maybe the parser had to be a little more advanced and that we'd maybe have expressions with nested calls like mul(10, mul(20,30), 40)
, but apparently not. Many people easily solved this by just tearing through it with a regex like mul(\d+,\d+)|do()|don't()
and iterating over the matches.
puzzle_input_str = open("./puzzle_input/day3.txt").read() test_input_str = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))" def parse_number(input_str, pos): if not input_str[pos].isdigit(): return pos + 1, None digits = [] while input_str[pos].isdigit(): digits.append(input_str[pos]) pos += 1 parsed_number = None if len(digits) == 0 else int("".join(digits)) return pos, parsed_number def parse_number_pair(input_str, pos): pos, param1 = parse_number(input_str, pos) if param1 is None: return pos, None if input_str[pos] != ",": return pos, None pos, param2 = parse_number(input_str, pos + 1) if param2 is None: return pos, None return pos, (param1, param2) def parse_params(input_str, pos): if input_str[pos] != "(": return pos+1, None pos, params = parse_number_pair(input_str, pos+1) if params is None: return pos, None if input_str[pos] != ")": return pos, None return pos+1, params def parse_mul(input_str, pos): if pos + len("mul(x,y)") > len(input_str): return pos, None if input_str[pos:pos+3] == "mul": pos, params = parse_params(input_str, pos+3) if params is None: return pos, None return pos, ("mul",) + params return pos, None def parse_do(input_str, pos): do_call = "do()" do_call_len = len(do_call) if input_str[pos:pos+do_call_len] == do_call: return pos + do_call_len, ("do", 0, 0) return pos, None def parse_dont(input_str, pos): dont_call = "don't()" dont_call_len = len(dont_call) if input_str[pos:pos+dont_call_len] == dont_call: return pos + dont_call_len, ("dont", 0, 0) return pos, None def parse_conditional(input_str, pos): pos, op = parse_do(input_str, pos) if op is not None: return pos, op pos, op = parse_dont(input_str, pos) if op is not None: return pos, op return pos+1, None def parse(input_str): pos = 0 while pos < len(input_str): new_pos, res = parse_conditional(input_str, pos) if res is not None: pos = new_pos yield res continue new_pos, res = parse_mul(input_str, pos) if res is not None: pos = new_pos yield res continue pos += 1 def evaluate(expression): if type(expression) == tuple: op, param1, param2 = expression if op == "mul": return param1 * param2 else: return 0 if type(expression) == int: return expression raise Exception(f"unknown expression:{str(op)}") def part_one(input_str): expressions = parse(input_str) return sum(evaluate(expression) for expression in expressions) assert 161 == part_one(test_input_str) print("part one:", part_one(puzzle_input_str))
part one: 188192787
test_input_str = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))" def evaluate(expression, do): if type(expression) == tuple: op, param1, param2 = expression if op == "mul": return param1 * param2, do if op == "dont": return 0, False if op == "do": return 0, True raise Exception(f"unknown op:{op}") if type(expression) == int: return expression raise Exception(f"unknown expression:{str(op)}") def part_two(input_str): expressions = list(parse(input_str)) do = True results = [] for expression in expressions: value, do = evaluate(expression, do) if do: results.append(value) return sum(results) assert 48 == part_two(test_input_str) print("part two:", part_two(puzzle_input_str))
part two: 113965544