from collections.abc import Iterable from functools import reduce from itertools import product from pathlib import Path from typing import NamedTuple class Point(NamedTuple): x: int y: int def iter_adjacent(self, wrap=False) -> Iterable["Point"]: for offset in product((1, 0, -1), (1, 0, -1)): adj = Point(self.x + offset[0], self.y + offset[1]) if adj == self: continue if not wrap and (adj.x < 0 or adj.y < 0): continue yield adj def __str__(self) -> str: return f"Point({self.x}, {self.y})" def is_symbol(v: str) -> bool: return not v.isnumeric() and v != "." def read_part(grid: list[str], point: Point) -> tuple[Point, int]: points = [point] num = grid[point.y][point.x] if not num.isnumeric(): raise ValueError(f"Can't read a part because {num}@{point} is not a number") row = grid[point.y] x = point.x - 1 while x >= 0 and row[x].isnumeric(): num = row[x] + num points.append(Point(x, point.y)) x -= 1 left_point = Point(x, point.y) x = point.x + 1 while x < len(row) and row[x].isnumeric(): num = num + row[x] points.append(Point(x, point.y)) x += 1 print(f"Part {num}@{left_point} for {point}") return left_point, int(num) def find_parts(grid: list[str], point: Point) -> dict[Point, int]: parts: dict[Point, int] = {} for adjacent in point.iter_adjacent(): if adjacent.y >= len(grid) or adjacent.x >= len(grid[adjacent.y]): continue if grid[adjacent.y][adjacent.x].isnumeric(): point, part_num = read_part(grid, adjacent) parts[point] = part_num return parts def part1(input: Path) -> int: with input.open() as f: grid = [line.strip() for line in f] all_parts: dict[Point, int] = {} for y, row in enumerate(grid): for x, cell in enumerate(row): p = Point(x, y) if is_symbol(cell): parts = find_parts(grid, p) all_parts.update(parts) return sum(all_parts.values()) def part2(input: Path) -> int: with input.open() as f: grid = [line.strip() for line in f] total = 0 for y, row in enumerate(grid): for x, cell in enumerate(row): p = Point(x, y) if cell != "*": continue parts = find_parts(grid, p) if len(parts) != 2: continue gear_ratio = reduce(lambda x, y: x*y, parts.values(), 1) total += gear_ratio return total if __name__ == "__main__": result1 = part1(Path("./input.txt")) print("part 1", result1) result2 = part2(Path("./input.txt")) print("part 2", result2)