Source code for day03.lib.classes

"""Day3 classes."""
import re
from dataclasses import dataclass

NON_PART = "123456789."
NUMBER_REGEX = r"\d+"


[docs] @dataclass class PartNumber: """Class respresenting a potential part number, and its position.""" col: int row: int length: int value: int
[docs] def touching(self, col: int, row: int, row_size: int) -> bool: """Returns if a given coordinate is touching this PartNumber.""" start_x = max(0, self.col - 1) end_x = min(self.end_index, row_size) if not start_x <= col <= end_x: return False if not self.row - 1 <= row <= self.row + 1: return False return True
@property def end_index(self) -> int: """Returns the end column index of the number.""" return self.col + self.length
[docs] @dataclass class Gear: """Class representing a potential gear (``*`` icon).""" col: int row: int part_numbers: list[PartNumber] | None = None @property def gear_ratio(self) -> int: """If we have exactly two parts, returns the gear ratio.""" if self.part_numbers is None: raise ValueError("self.part_numbers not initialized") if len(self.part_numbers) == 2: return self.part_numbers[0].value * self.part_numbers[1].value return 0 # or None..
[docs] @dataclass class Matrix: """Represents the entire 2d array.""" data: list[str] @property def row_size(self) -> int: """How long each row is.""" return len(self.data[0]) @property def row_count(self) -> int: """How many rows there are.""" return len(self.data)
[docs] def get_part_numbers(self) -> list[PartNumber]: """Retrieve numbered words like 456 from the matrix.""" results = [] for row, line in enumerate(self.data): matches = re.finditer(NUMBER_REGEX, line) for match in matches: start, end = match.start(), match.end() value = int(line[start:end]) part_number = PartNumber( row=row, col=start, length=end - start, value=value ) results.append(part_number) return results
[docs] @staticmethod def is_engine_part_row(row: str) -> bool: """Returns if there is an engine part in this row.""" return any(char not in NON_PART for char in row)
[docs] def is_engine_part(self, part_number: PartNumber) -> bool: """Return whether a part_number is an engine part by looking at its surroundings.""" start_x = max(0, part_number.col - 1) end_x = min(part_number.end_index + 1, self.row_size) if ( part_number.row >= 1 and self.is_engine_part_row( # row above self.data[part_number.row - 1][start_x:end_x] ) ): return True if ( # row below part_number.row < self.row_count - 1 and self.is_engine_part_row(self.data[part_number.row + 1][start_x:end_x]) ): return True # left one if self.data[part_number.row][start_x] not in NON_PART: return True # right one if self.data[part_number.row][end_x - 1] not in NON_PART: return True return False
[docs] def get_gears(self, part_numbers: list[PartNumber]) -> list[Gear]: """Retrieve gears from the matrix.""" results = [] for row, line in enumerate(self.data): for col, char in enumerate(line): if char == "*": gear = Gear(col=col, row=row) gear.part_numbers = self.find_gear_parts(gear, part_numbers) results.append(gear) return results
[docs] def find_gear_parts( self, gear: Gear, part_numbers: list[PartNumber] ) -> list[PartNumber]: """Returns a list of part_numbers that are touching a given gear.""" result = [] for part_number in part_numbers: if part_number.touching(gear.col, gear.row, self.row_size): result.append(part_number) return result
[docs] def filter_engine_parts(self, part_numbers: list[PartNumber]) -> list[PartNumber]: """Return the legit part numbers.""" return list(filter(self.is_engine_part, part_numbers))