Sean McLemon | Advent of Code

Home | Czech | Blog | GitHub | Advent Of Code | Notes


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