-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathday03.py
More file actions
201 lines (152 loc) · 5.75 KB
/
day03.py
File metadata and controls
201 lines (152 loc) · 5.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
from collections import defaultdict
from dataclasses import dataclass, field
from itertools import product
from pathlib import Path
from typing import Iterable, Iterator, overload
from loguru import logger
from typer import Typer
from utils import timer
main = Typer()
@dataclass
class Number:
"""Represent number with neighboring symbols"""
value: int
neighbors: set[str] = field(default_factory=set)
class Schematic:
"""Represent the schematic"""
def __init__(self, input: str):
self.grid = input.splitlines()
@overload
def __getitem__(self, key: tuple[int, int]) -> str:
"""Access via tuple"""
...
@overload
def __getitem__(self, row: int, col: int) -> str:
"""Access via row and col"""
...
def __getitem__(self, *args):
"""Actual implementation"""
if len(args) == 1:
return self.grid[args[0][0]][args[0][1]]
return self.grid[args[0]][args[1]]
@property
def n_rows(self) -> int:
return len(self.grid)
@property
def n_cols(self) -> int:
return len(self.grid[0])
def is_symbol(
self, row_or_tuple: int | tuple[int, int], col: int | None = None
) -> bool:
"""Check if coordinate is a symbol
Digits and '.' are not symbols. Everything else is.
Args:
row_or_tuple: Either row of the schematic or a tuple with the
schematic coordinates.
col: Column go schematic. Ignored if row_or_tuple is a tuple. Defaults to None.
Returns:
True if symbol, False otherwise
"""
if row_or_tuple is tuple:
char = self[row_or_tuple]
else:
char = self[row_or_tuple, col]
return not char.isdigit() and char != "."
def get_symbol_neighbors(self, row: int, col: int) -> Iterator[tuple[int, int]]:
"""Get all neighbors that are symbols
Takes diagonal neighbors into account. Respects schematic boundaries.
Args:
row: Current row
col: Current column
Yields:
(row, col) of all neighbors that are symbols (not digits or '.'
"""
n_rows = self.n_rows
n_cols = self.n_cols
for row_delta, col_delta in product([-1, 0, 1], [-1, 0, 1]):
if row_delta == 0 and col_delta == 0:
continue
if (
0 <= row + row_delta < n_rows
and 0 <= col + col_delta < n_cols
and self.is_symbol(row + row_delta, col + col_delta)
):
yield row + row_delta, col + col_delta
@timer
def extract_numbers_and_engines(self) -> tuple[list[Number], list[Number]]:
"""Extracts the numbers and engines from the schematic
Numbers also save the neighboring symbols. Engines is just a list of
lists of numbers. Each list of numbers represents numbers neighboring a single engine.
Returns:
(numbers, engines)
"""
numbers: list[Number] = []
engines: dict[tuple[int, int], list[Number]] = defaultdict(list)
for row in range(self.n_rows):
# Reset number, neighbors, and engines
value = 0
neighbors: set[str] = set()
is_number = False
engine_set: set[tuple[int, int]] = set()
# Scan row for numbers
for col in range(self.n_cols):
char = self[row, col]
if char.isdigit():
value = 10 * value + int(char)
is_number = True
# Check neighbors for symbols
# TODO: Could be made more efficient by only checking
# the right 3 neighbors (except for the first column
# where we also need top and bottom)
for t in self.get_symbol_neighbors(row, col):
symbol = self[t]
neighbors.add(symbol)
if symbol == "*":
engine_set.add(t)
continue
# If char is no digit, we have to check if we have a number
# that has to be added to the list
# Also add the number to the engine list, if it borders an engine
if is_number:
numbers.append(Number(value, neighbors))
for t in engine_set:
engines[t].append(numbers[-1])
value = 0
neighbors = set()
is_number = False
engine_set = set()
# Necessary if number is at the end of the row
if is_number:
numbers.append(Number(value, neighbors))
for t in engine_set:
engines[t].append(numbers[-1])
return numbers, list(engines.values())
@timer
def task01(numbers: list[Number]) -> int:
"""Sum all numbers that have neighbors"""
return sum(number.value for number in numbers if len(number.neighbors) != 0)
@timer
def task02(engines: Iterable[list[Number]]) -> int:
"""Sum all gear ratios
Only engines with exactly two numbers have a gear ratio, which
is the product of the numbers.
"""
return sum(
numbers[0].value * numbers[1].value for numbers in engines if len(numbers) == 2
)
@main.command()
def entrypoint(path: Path):
"""Entrypoint
Args:
path: Path to input file
"""
with open(path, "r") as f:
input = f.read().strip()
schematic = Schematic(input)
numbers, engines = schematic.extract_numbers_and_engines()
logger.info("Task 01")
logger.info(task01(numbers))
logger.info("Task 02")
logger.info(task02(engines))
if __name__ == "__main__":
main()