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