111 lines
2.8 KiB
Python
111 lines
2.8 KiB
Python
|
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)
|