<jupyter_start><jupyter_text>Day 16 - Data validationWe are asked to find all numbers that don't fit the validation rules. I've simply parsed all the rules into `range()` objects, then used the [`any()` function](https://docs.python.org/3/library/functions.htmlany) to check a given value against validation rules (a value is valid if there is at least one validation rule that matches it).<jupyter_code>from dataclasses import dataclass

@dataclass(frozen=True)
class ValidationRule:
    name: str
    range1: range
    range2: range

    def __contains__(self, value: int) -> bool:
        return value in self.range1 or value in self.range2

    @classmethod
    def from_line(cls, line: str) -> "ValidationRule":
        name, _, ranges = line.partition(":")
        return cls(
            name,
            *(
                range(int(start), int(stop) + 1)
                for spec in ranges.split("or")
                for start, stop in (spec.split("-"),)
            )
        )

class TicketInfo:
    rules: list[ValidationRule]
    ticket: list[int]
    nearby: list[list[int]]

    def __init__(self, data: str) -> None:
        sections = [section.splitlines() for section in data.split("\n\n")]
        self.rules = [ValidationRule.from_line(l) for l in sections[0]]
        self.ticket = [int(v) for v in sections[1][1].split(",")]
        self.nearby = [[int(v) for v in line.split(",")] for line in sections[2][1:]]
    
    def error_rate(self):
        return sum(
            v
            for ticket in self.nearby
            for v in ticket
            if not any(v in rule for rule in self.rules)
        )

test_tickets = TicketInfo("""\
class: 1-3 or 5-7
row: 6-11 or 33-44
seat: 13-40 or 45-50

your ticket:
7,1,14

nearby tickets:
7,3,47
40,4,50
55,2,20
38,6,12
""")

assert test_tickets.error_rate() == 71
import aocd
data = aocd.get_data(day=16, year=2020)
print("Part 1:", TicketInfo(data).error_rate())<jupyter_output>Part 1: 21996<jupyter_text>Part 2To find what rule matches what column, I collected all rules that fit a given column of values together into a set. Then, for any set of size 1, you know that that single rule must belong to that specific column and you can remove the rule from the other sets. Repeat until there are no columns without rules left.<jupyter_code>class MatchedFieldsTicketInfo(TicketInfo):
    @property
    def valid_nearby_tickets(self):
        return [
            ticket
            for ticket in self.nearby
            if all(
                any(v in rule for rule in self.rules)
                for v in ticket
            )
        ]

    @property
    def rule_order(self):
        columns = list(zip(*self.valid_nearby_tickets))
        column_rules = [
            (i, {rule for rule in self.rules if all(v in rule for v in col)})
            for i, col in enumerate(columns)]
        order = [None] * len(column_rules)
        while column_rules:
            idx, colrules = min(column_rules, key=lambda i_r: len(i_r[1]))
            (rule,) = colrules  # if there isn't just one, we can't solve this
            order[idx] = rule
            column_rules = [(i, rules - {rule}) for i, rules in column_rules if i != idx]
        
        return order
    
    @property
    def ticket_data(self):
        return {
            rule.name: v
            for v, rule in zip(self.ticket, self.rule_order)
        }


test2_data = MatchedFieldsTicketInfo("""\
class: 0-1 or 4-19
row: 0-5 or 8-19
seat: 0-13 or 16-19

your ticket:
11,12,13

nearby tickets:
3,9,18
15,1,5
5,14,9
""")

assert test2_data.ticket_data == {"class": 12, "row": 11, "seat": 13}
from functools import reduce
from operator import mul

ticket = MatchedFieldsTicketInfo(data).ticket_data
print("Part 2:", reduce(mul, (v for name, v in ticket.items() if name.startswith("departure"))))
print(*(f"{field:>20}: {value}" for field, value in ticket.items()), sep="\n")<jupyter_output>train: 173
    arrival location: 191
     departure track: 61
                seat: 199
               class: 101
       arrival track: 179
      departure date: 257
     arrival station: 79
                 row: 193
                type: 223
            duration: 139
    arrival platform: 97
               wagon: 83
               route: 197
                zone: 251
  departure platform: 53
      departure time: 89
   departure station: 149
               price: 181
  departure location: 59
{'fraction_non_alphanumeric': 0.10427505089346302, 'fraction_numerical': 0.03890522506220312, 'mean_word_length': 2.355083459787557, 'pattern_counts': {'":': 5, '<': 7, '<?xml version=': 0, '>': 11, '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': '2', 'dir': 'jupyter-structured-clean-dedup', 'id': '261900', 'license': 'MIT', 'n_tokens_mistral': 1494, 'n_tokens_neox': 1356, 'n_words': 467, 'path': '2020/Day 16.ipynb', 'repo_name': 'AwesomeGitHubRepos/adventofcode', 'size': '4421'}