Source code for day19.lib.classes

"""well defined classes for day19."""
from dataclasses import dataclass
from enum import StrEnum
from typing import Optional


[docs] @dataclass class Part: """Well defined part with x,m,a,s values.""" x: int m: int a: int s: int @property def rating(self) -> int: """Returns rating of part (sum of xmas).""" return sum([self.x, self.m, self.a, self.s])
[docs] def clone_modify(self, component: "Component", value: int) -> "Part": """Clones this part and modifies one component. Args: component (Component): component to change value (int): new value of component Returns: Part: clone of this part with one component changed. """ x, m, a, s = self.x, self.m, self.a, self.s if component == Component.X: x = value if component == Component.M: m = value if component == Component.A: a = value if component == Component.S: s = value return Part(x, m, a, s)
[docs] def get_value(self, component: "Component") -> int: """Returns value of a component inside this part.""" if component == Component.X: return self.x if component == Component.M: return self.m if component == Component.A: return self.a if component == Component.S: return self.s raise AssertionError(f"Unsupported component{component}")
[docs] class Component(StrEnum): """A well defined component inside a part.""" X = "x" M = "m" A = "a" S = "s"
[docs] @dataclass class PartRange: """A range of parts (min/max) based on component values.""" min_values: Part # from max_values: Part # to, non-inclusive
[docs] def size(self) -> int: """Returns the size of the partrange.""" return ( (self.max_values.x - self.min_values.x) * (self.max_values.m - self.min_values.m) * (self.max_values.a - self.min_values.a) * (self.max_values.s - self.min_values.s) )
[docs] def split( self, component: Component, split_value: int ) -> tuple[Optional["PartRange"], Optional["PartRange"]]: """Split a partrange in two, using a chosen component and splitvalue. In the case that our range falls on one whole side, we return None. E.g. range = 0-100; split == 200 -> return [(0-100), None] range = 100-200; split == 50 -> return [None, (100-200)] range = 100-200, split == 150 -> return [(100-150), (150-200)] """ min_value = self.min_values.get_value(component) max_value = self.max_values.get_value(component) if split_value >= max_value: return (self, None) if split_value < min_value: return (None, self) mid_high = self.min_values.clone_modify(component, split_value) mid_low = self.max_values.clone_modify(component, split_value) return ( PartRange(self.min_values, mid_low), PartRange(mid_high, self.max_values), )
def __str__(self) -> str: """Compact string representing our range.""" return ", ".join( [ f"{self.min_values.x}<=x<={self.max_values.x-1}", f"{self.min_values.m}<=m<={self.max_values.m-1}", f"{self.min_values.a}<=a<={self.max_values.a-1}", f"{self.min_values.s}<=s<={self.max_values.s-1}", ] )
[docs] @dataclass class PartRangeDest: """Combinatoin of partrange and a destination workflow.""" part_range: PartRange destination: str def __str__(self) -> str: """Compact string representation.""" return self.destination + ":" + str(self.part_range)
[docs] class Comparator(StrEnum): """Well defined comparators ``<`` and ``>``.""" LessThan = "<" GreaterThan = ">"
[docs] @dataclass class Condition: """A condition for a part to succeed/fail.""" component: Component sign: Comparator value: int
[docs] def process_part(self, part: Part) -> bool: """Checks a part to see if it matches our condition. Args: part (Part): part to check Raises: AssertionError: if component/sign are unsupported Returns: bool: True if the part passes our condition. """ part_val: int if self.component == Component.X: part_val = part.x elif self.component == Component.M: part_val = part.m elif self.component == Component.A: part_val = part.a elif self.component == Component.S: part_val = part.s else: raise AssertionError(f"Unsupported component: {self.component}") if self.sign == Comparator.GreaterThan: return part_val > self.value elif self.sign == Comparator.LessThan: return part_val < self.value else: raise AssertionError(f"Unsupported comparator: {self.sign}")
[docs] def process_part_range( self, part_range: PartRange ) -> tuple[Optional[PartRange], Optional[PartRange]]: """Splits a part range based on success/fail. Args: part_range (PartRange): Partrange to check. Raises: AssertionError: If we have an unknown comparator Returns: tuple[Optional[PartRange], Optional[PartRange]]: successful part range, failed partrange """ if self.sign == Comparator.LessThan: success, fail = part_range.split(self.component, self.value) return (success, fail) if self.sign == Comparator.GreaterThan: fail, success = part_range.split(self.component, self.value + 1) return (success, fail) raise AssertionError(f"Unknown comparator: {self.sign}")
[docs] @dataclass class Rule: """A Rule consists of a condition + destination.""" destination: str condition: Condition | None = None
[docs] def process_part(self, part: Part) -> str | None: """Processes a part. Returns next workflow if successful, or None if we failed this rule """ if self.condition is None: # always pass return self.destination if self.condition.process_part(part): return self.destination return None
[docs] def process_part_range( self, part_range: PartRange ) -> tuple[Optional[PartRangeDest], Optional[PartRange]]: """Processes a PartRange. Returns next workflow and partrange for succeeding parts. Returns the remainder partrange that failed. Args: part_range (PartRange): base partrange. Returns: tuple[Optional[PartRangeDest], Optional[PartRange]]: success, fail """ success: Optional[PartRange] fail: Optional[PartRange] if self.condition is None: # pass all success, fail = part_range, None else: # split up range success, fail = self.condition.process_part_range(part_range) if success is not None: return (PartRangeDest(success, self.destination), fail) return None, fail
[docs] @dataclass(eq=True) class Workflow: """The name of the workflow + a bunch of rules for parts to follow.""" name: str rules: list[Rule]
[docs] def process_part(self, part: Part) -> str: """Processes a part, returns the next workflow.""" for rule in self.rules: destination = rule.process_part(part) if destination is not None: return destination raise AssertionError("uh oh, hit the end of workflow!")
[docs] def process_part_range(self, part_range: PartRange) -> list[PartRangeDest]: """Follow rule list, splitting off PartRanges. Each success has to branch off. Each failure continues down the chain. """ results: list[PartRangeDest] = [] remainder: Optional[PartRange] = part_range index = 0 while remainder is not None: rule = self.rules[index] success, remainder = rule.process_part_range(remainder) if success is not None: results.append(success) index += 1 return results