<jupyter_start><jupyter_text>Day 10 - Syntax Scoringhttps://adventofcode.com/2021/day/10<jupyter_code>from pathlib import Path

INPUTS = Path("input.txt").read_text().strip().split("\n")

OPENCLOSE = {
    "(": ")",
    "[": "]",
    "{": "}",
    "<": ">",
}<jupyter_output><empty_output><jupyter_text>Part 1Finding the first illegal character is relatively simple if we just use a stack:- Go through the line one character at a time.- For each opening char, append its expected closing char to the stack.- For each closing char, pop the stack to get the one we expected to find. If they don't match, the line is illegal, and we simply return the unexpected char.- If we hit the end without finding any problems, explicitly return `None` to indicate there are no issues.<jupyter_code>def first_illegal(line: str) -> str | None:
    """If the given line is corrupted, returns the first illegal character encountered.

    If the line is legal, returns None, instead.
    """
    close_stack = []
    for char in line:
        if char in OPENCLOSE.keys():
            close_stack.append(OPENCLOSE[char])
        else:
            expected = close_stack.pop()
            if expected != char:
                return char
    return None


def test_first_illegal():
    # Example corrupted lines as given by the AoC site.
    assert first_illegal("{([(<{}[<>[]}>{[]{[(<()>") == "}"
    assert first_illegal("[[<[([]))<([[{}[[()]]]") == ")"
    assert first_illegal("[{[{({}]{}}([{[{{{}}([]") == "]"
    assert first_illegal("[<(<(<(<{}))><([]([]()") == ")"
    assert first_illegal("<{([([[(<>()){}]>(<<{{") == ">"
    
    # Following is an incomplete line, which should not show as corrupted
    assert first_illegal("[({(<(())[]>[[{[]{<()<>>") is None


test_first_illegal()<jupyter_output><empty_output><jupyter_text>Easy enough. Now to the scoring element to find our solution<jupyter_code>ILLEGAL_POINTS = {
    ")": 3,
    "]": 57,
    "}": 1197,
    ">": 25137,
}

point_value = 0
for line in INPUTS:
    char = first_illegal(line)
    if char is not None:
        point_value += ILLEGAL_POINTS[char]

print(f"Syntax error score: {point_value}")<jupyter_output>Syntax error score: 341823<jupyter_text>Part 2Going to take the same logical approach here, but invert it slightly. I just need to exit fast when there are syntax errors, and return the remaining closing stack in all other cases.<jupyter_code>def autocomplete(line: str) -> str | None:
    close_stack = []
    for char in line:
        if char in OPENCLOSE.keys():
            close_stack.append(OPENCLOSE[char])
        else:
            expected = close_stack.pop()
            if expected != char:
                return None
    # The closing stack we operate on above is inverted: the innermost closing char
    # comes last. So, we need to reverse it before returning, which is easily done by
    # reverse slice: [::-1]
    return "".join(close_stack[::-1])


def test_autocomplete():
    assert autocomplete("[({(<(())[]>[[{[]{<()<>>") == "}}]])})]"
    assert autocomplete("[(()[<>])]({[<{<<[]>>(") == ")}>]})"
    assert autocomplete("(((({<>}<{<{<>}{[]{[]{}") == "}}>}>))))"
    assert autocomplete("{<[[]]>}<{[{[{[]{()[[[]") == "]]}}]}]}>"
    assert autocomplete("<{([{{}}[<[[[<>{}]]]>[]]") == "])}>"


test_autocomplete()<jupyter_output><empty_output><jupyter_text>Easy peasy. Now to the scoring algorithm.<jupyter_code>AUTOCOMPLETE_POINTS = {
    ")": 1,
    "]": 2,
    "}": 3,
    ">": 4,
}

def score_for_autocompletion(section: str) -> int:
    score = 0
    for char in section:
        score *= 5
        score += AUTOCOMPLETE_POINTS[char]
    return score

scores = []
for line in INPUTS:
    completion = autocomplete(line)
    if completion is not None:
        scores.append(score_for_autocompletion(completion))

# As stated, the final score is the median one:
scores = sorted(scores)
median = scores[len(scores) // 2]

print(f"Median score: {median}")<jupyter_output>Median score: 2801302861
{'fraction_non_alphanumeric': 0.18017565872020075, 'fraction_numerical': 0.01204516938519448, 'mean_word_length': 3.640279394644936, 'pattern_counts': {'":': 12, '<': 53, '<?xml version=': 0, '>': 51, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '28267', 'license': 'MIT', 'n_tokens_mistral': 1370, 'n_tokens_neox': 1285, 'n_words': 370, 'path': '2021/day10/main.ipynb', 'repo_name': 'GriceTurrble/advent-of-code-2020-answers', 'size': '3985'}