diff --git a/aoc_2023/day_15/__init__.py b/aoc_2023/day_15/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aoc_2023/day_15/a.py b/aoc_2023/day_15/a.py new file mode 100644 index 0000000..6f2583e --- /dev/null +++ b/aoc_2023/day_15/a.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from aoc_2023.day_15.common import hash_it +from aoc_2023.day_15.parser import Parser + + +@dataclass +class Day15PartASolver: + lines: list[str] + + @property + def solution(self) -> int: + return sum(hash_it(line) for line in self.lines) + + +def solve(input: str) -> int: + data = Parser.parse(input) + solver = Day15PartASolver(data) + + return solver.solution + + +def get_solution() -> int: + with open("aoc_2023/day_15/input.txt", "r") as f: + input = f.read() + return solve(input) + + +if __name__ == "__main__": + print(get_solution()) diff --git a/aoc_2023/day_15/b.py b/aoc_2023/day_15/b.py new file mode 100644 index 0000000..8588c56 --- /dev/null +++ b/aoc_2023/day_15/b.py @@ -0,0 +1,108 @@ +from dataclasses import dataclass, field +from functools import cached_property +from typing import Optional +from aoc_2023.day_15.common import hash_it +from aoc_2023.day_15.parser import Parser + + +@dataclass +class Minus: + label: str + + +@dataclass +class Equal: + label: str + val: int + + +@dataclass +class Lens: + label: str + focus_length: int + deleted: bool = False + + +@dataclass(frozen=True) +class Box: + id: int + lenses: list[Optional[Lens]] = field(hash=False, default_factory=list) + lens_index_lookup: dict[str, Lens] = field(hash=False, default_factory=dict) + + @property + def focusing_power(self) -> int: + output = 0 + box_num = self.id + 1 + lenses = [lens for lens in self.lenses if lens and not lens.deleted] + for i, lens in enumerate(lenses): + lens_num = i + 1 + output += box_num * lens_num * lens.focus_length + + return output + + def remove_lens(self, label: str) -> None: + if label in self.lens_index_lookup: + self.lens_index_lookup[label].deleted = True + del self.lens_index_lookup[label] + + def set_lens(self, label: str, value) -> None: + if label in self.lens_index_lookup: + lens = self.lens_index_lookup[label] + lens.focus_length = value + else: + lens = Lens(label, value) + self.lenses.append(lens) + self.lens_index_lookup[label] = lens + + +@dataclass +class Day15PartBSolver: + lines: list[str] + + @property + def solution(self) -> int: + for op in self.operations: + self.do_operation(op) + return sum(box.focusing_power for box in self.boxes) + + @cached_property + def boxes(self) -> list[Box]: + return [Box(i, []) for i in range(256)] + + @cached_property + def operations(self) -> list[Minus | Equal]: + output = list[Minus | Equal]() + for line in self.lines: + if line[-1] == "-": + output.append(Minus(line[:-1])) + else: + label, val = line.split("=") + output.append(Equal(label, int(val))) + return output + + def do_operation(self, op: Minus | Equal) -> None: + box_id = hash_it(op.label) + box = self.boxes[box_id] + + match op: + case Minus(label): + box.remove_lens(label) + case Equal(label, val): + box.set_lens(label, val) + + +def solve(input: str) -> int: + data = Parser.parse(input) + solver = Day15PartBSolver(data) + + return solver.solution + + +def get_solution() -> int: + with open("aoc_2023/day_15/input.txt", "r") as f: + input = f.read() + return solve(input) + + +if __name__ == "__main__": + print(get_solution()) diff --git a/aoc_2023/day_15/common.py b/aoc_2023/day_15/common.py new file mode 100644 index 0000000..72532a3 --- /dev/null +++ b/aoc_2023/day_15/common.py @@ -0,0 +1,7 @@ +def hash_it(line: str) -> int: + output = 0 + for char in line: + output += ord(char) + output *= 17 + output %= 256 + return output diff --git a/aoc_2023/day_15/from_prompt.py b/aoc_2023/day_15/from_prompt.py new file mode 100644 index 0000000..46b6de2 Binary files /dev/null and b/aoc_2023/day_15/from_prompt.py differ diff --git a/aoc_2023/day_15/input.txt b/aoc_2023/day_15/input.txt new file mode 100644 index 0000000..9c98e1c Binary files /dev/null and b/aoc_2023/day_15/input.txt differ diff --git a/aoc_2023/day_15/parser.py b/aoc_2023/day_15/parser.py new file mode 100644 index 0000000..ca6d714 --- /dev/null +++ b/aoc_2023/day_15/parser.py @@ -0,0 +1,4 @@ +class Parser: + @staticmethod + def parse(input: str) -> list[str]: + return input.strip().split(",") diff --git a/tests/test_day_15/__init__.py b/tests/test_day_15/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_day_15/test_a.py b/tests/test_day_15/test_a.py new file mode 100644 index 0000000..25ef46c --- /dev/null +++ b/tests/test_day_15/test_a.py @@ -0,0 +1,13 @@ +from aoc_2023.day_15.a import get_solution, solve +from aoc_2023.day_15.from_prompt import ( + SAMPLE_DATA, + SAMPLE_SOLUTION_A, +) + + +def test_solve(): + assert solve(SAMPLE_DATA) == SAMPLE_SOLUTION_A + + +def test_my_solution(): + assert get_solution() == 517015 diff --git a/tests/test_day_15/test_b.py b/tests/test_day_15/test_b.py new file mode 100644 index 0000000..873a7ff --- /dev/null +++ b/tests/test_day_15/test_b.py @@ -0,0 +1,10 @@ +from aoc_2023.day_15.b import get_solution, solve +from aoc_2023.day_15.from_prompt import SAMPLE_DATA, SAMPLE_SOLUTION_B + + +def test_solve(): + assert solve(SAMPLE_DATA) == SAMPLE_SOLUTION_B + + +def test_my_solution(): + assert get_solution() == 286104 diff --git a/tests/test_day_15/test_common.py b/tests/test_day_15/test_common.py new file mode 100644 index 0000000..e39cd67 --- /dev/null +++ b/tests/test_day_15/test_common.py @@ -0,0 +1,6 @@ +from aoc_2023.day_15.common import hash_it +from aoc_2023.day_15.from_prompt import HASH, HASH_HASHED + + +def test_hash(): + assert hash_it(HASH) == HASH_HASHED