Restructure solver modules with classes and unittest suites
This commit is contained in:
73
puzzles/1.py
73
puzzles/1.py
@@ -1,33 +1,56 @@
|
|||||||
from collections import Counter
|
from collections import Counter
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import override
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
test_input = """
|
from puzzles._solver import Solver
|
||||||
3 4
|
|
||||||
4 3
|
|
||||||
2 5
|
|
||||||
1 3
|
|
||||||
3 9
|
|
||||||
3 3
|
|
||||||
"""
|
|
||||||
|
|
||||||
test_solution_p1 = 11
|
|
||||||
test_solution_p2 = 31
|
|
||||||
|
|
||||||
|
|
||||||
def solve_p1(puzzle_input: str) -> int:
|
class DayOneSolver(Solver):
|
||||||
list1, list2 = _parse_lists(puzzle_input)
|
list1: list[int]
|
||||||
pairs = zip(sorted(list1), sorted(list2))
|
list2: list[int]
|
||||||
distances = (abs(pair[0] - pair[1]) for pair in pairs)
|
|
||||||
return sum(distances)
|
@override
|
||||||
|
def __init__(self, puzzle_input: str):
|
||||||
|
lines = (line.partition(" ") for line in puzzle_input.strip().split("\n"))
|
||||||
|
list1, list2 = zip(*((int(line[0]), int(line[2])) for line in lines))
|
||||||
|
self.list1 = list(list1)
|
||||||
|
self.list2 = list(list2)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def solve_p1(self) -> int:
|
||||||
|
minimum_pairs = zip(sorted(self.list1), sorted(self.list2))
|
||||||
|
distances = (abs(pair[0] - pair[1]) for pair in minimum_pairs)
|
||||||
|
return sum(distances)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def solve_p2(self) -> int:
|
||||||
|
occurrences = Counter(self.list2)
|
||||||
|
similarities = (value * occurrences.get(value, 0) for value in self.list1)
|
||||||
|
return sum(similarities)
|
||||||
|
|
||||||
|
|
||||||
def solve_p2(puzzle_input: str) -> int:
|
class TestDayOneSolver(TestCase):
|
||||||
list1, list2 = _parse_lists(puzzle_input)
|
test_input = dedent(
|
||||||
occurrences = Counter(list2)
|
"""
|
||||||
similarities = (value * occurrences.get(value, 0) for value in list1)
|
3 4
|
||||||
return sum(similarities)
|
4 3
|
||||||
|
2 5
|
||||||
|
1 3
|
||||||
|
3 9
|
||||||
|
3 3
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_parse(self):
|
||||||
|
solver = DayOneSolver(self.test_input)
|
||||||
|
self.assertListEqual(solver.list1, [3, 4, 2, 1, 3, 3])
|
||||||
|
self.assertListEqual(solver.list2, [4, 3, 5, 3, 9, 3])
|
||||||
|
|
||||||
def _parse_lists(puzzle_input: str) -> tuple[list[int], list[int]]:
|
def test_solve_p1(self):
|
||||||
lines = (line.partition(" ") for line in puzzle_input.strip().split("\n"))
|
solver = DayOneSolver(self.test_input)
|
||||||
list1, list2 = zip(*((int(line[0]), int(line[2])) for line in lines))
|
self.assertEqual(solver.solve_p1(), 11)
|
||||||
return list1, list2
|
|
||||||
|
def test_solve_p2(self):
|
||||||
|
solver = DayOneSolver(self.test_input)
|
||||||
|
self.assertEqual(solver.solve_p2(), 31)
|
||||||
|
132
puzzles/2.py
132
puzzles/2.py
@@ -1,63 +1,93 @@
|
|||||||
from itertools import islice
|
from dataclasses import dataclass
|
||||||
from typing import Iterator
|
from functools import cached_property
|
||||||
|
from itertools import pairwise
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import Iterator, override
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
from more_itertools import ilen
|
from more_itertools import ilen
|
||||||
|
|
||||||
test_input = """
|
from puzzles._solver import Solver
|
||||||
7 6 4 2 1
|
|
||||||
1 2 7 8 9
|
|
||||||
9 7 6 2 1
|
|
||||||
1 3 2 4 5
|
|
||||||
8 6 4 4 1
|
|
||||||
1 3 6 7 9
|
|
||||||
"""
|
|
||||||
|
|
||||||
test_solution_p1 = 2
|
|
||||||
test_solution_p2 = 4
|
|
||||||
|
|
||||||
|
|
||||||
def solve_p1(puzzle_input: str) -> int:
|
@dataclass
|
||||||
reports = _parse_reports(puzzle_input)
|
class Report:
|
||||||
delta_reports = (_deltas(report) for report in reports)
|
levels: tuple[int, ...]
|
||||||
safe_delta_reports = filter(_is_gradual_monotonic, delta_reports)
|
|
||||||
return ilen(safe_delta_reports)
|
@cached_property
|
||||||
|
def deltas(self) -> tuple[int, ...]:
|
||||||
|
return tuple(b - a for a, b in pairwise(self.levels))
|
||||||
|
|
||||||
|
def is_gradual_monotonic(self) -> bool:
|
||||||
|
sign = 1 if self.deltas[0] > 0 else -1
|
||||||
|
return all(
|
||||||
|
delta != 0 and delta / abs(delta) == sign and abs(delta) < 4
|
||||||
|
for delta in self.deltas
|
||||||
|
)
|
||||||
|
|
||||||
|
def permute_dampened_variations(self) -> Iterator["Report"]:
|
||||||
|
yield self
|
||||||
|
for i in range(0, len(self.levels)):
|
||||||
|
yield Report(self.levels[:i] + self.levels[i + 1 :])
|
||||||
|
|
||||||
|
|
||||||
def solve_p2(puzzle_input: str) -> int:
|
class DayTwoSolver(Solver):
|
||||||
reports = _parse_reports(puzzle_input)
|
reports: list[Report]
|
||||||
dampened_report_collections = (_dampen_permutations(report) for report in reports)
|
|
||||||
delta_report_collections = (
|
@override
|
||||||
(_deltas(report) for report in report_collection)
|
def __init__(self, puzzle_input: str):
|
||||||
for report_collection in dampened_report_collections
|
self.reports = list(
|
||||||
)
|
Report(tuple(int(level) for level in line.split(" ")))
|
||||||
safe_report_collections = filter(
|
for line in puzzle_input.strip().splitlines()
|
||||||
lambda delta_report_collection: any(
|
)
|
||||||
_is_gradual_monotonic(delta_report)
|
|
||||||
for delta_report in delta_report_collection
|
@override
|
||||||
),
|
def solve_p1(self) -> int:
|
||||||
delta_report_collections,
|
safe_reports = filter(Report.is_gradual_monotonic, self.reports)
|
||||||
)
|
return ilen(safe_reports)
|
||||||
return ilen(safe_report_collections)
|
|
||||||
|
@override
|
||||||
|
def solve_p2(self) -> int:
|
||||||
|
safe_reports = filter(
|
||||||
|
lambda report: any(
|
||||||
|
report_variation.is_gradual_monotonic()
|
||||||
|
for report_variation in report.permute_dampened_variations()
|
||||||
|
),
|
||||||
|
self.reports,
|
||||||
|
)
|
||||||
|
return ilen(safe_reports)
|
||||||
|
|
||||||
|
|
||||||
def _parse_reports(puzzle_input: str) -> Iterator[tuple[int, ...]]:
|
class TestDayNSolver(TestCase):
|
||||||
lines = puzzle_input.strip().splitlines()
|
test_input = dedent(
|
||||||
return (tuple(int(level) for level in line.split(" ")) for line in lines)
|
"""
|
||||||
|
7 6 4 2 1
|
||||||
|
1 2 7 8 9
|
||||||
def _deltas(report: tuple[int, ...]) -> tuple[int, ...]:
|
9 7 6 2 1
|
||||||
pairs = zip(report, islice(report, 1, None))
|
1 3 2 4 5
|
||||||
return tuple(b - a for a, b in pairs)
|
8 6 4 4 1
|
||||||
|
1 3 6 7 9
|
||||||
|
"""
|
||||||
def _is_gradual_monotonic(delta_report: tuple[int]) -> bool:
|
|
||||||
sign = 1 if delta_report[0] > 0 else -1
|
|
||||||
return all(
|
|
||||||
delta != 0 and delta / abs(delta) == sign and abs(delta) < 4
|
|
||||||
for delta in delta_report
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_parse(self):
|
||||||
|
solver = DayTwoSolver(self.test_input)
|
||||||
|
self.assertListEqual(
|
||||||
|
solver.reports,
|
||||||
|
[
|
||||||
|
Report((7, 6, 4, 2, 1)),
|
||||||
|
Report((1, 2, 7, 8, 9)),
|
||||||
|
Report((9, 7, 6, 2, 1)),
|
||||||
|
Report((1, 3, 2, 4, 5)),
|
||||||
|
Report((8, 6, 4, 4, 1)),
|
||||||
|
Report((1, 3, 6, 7, 9)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def _dampen_permutations(report: tuple[int, ...]) -> Iterator[tuple[int, ...]]:
|
def test_solve_p1(self):
|
||||||
yield report
|
solver = DayTwoSolver(self.test_input)
|
||||||
yield from (report[:i] + report[i + 1 :] for i in range(0, len(report)))
|
self.assertEqual(solver.solve_p1(), 2)
|
||||||
|
|
||||||
|
def test_solve_p2(self):
|
||||||
|
solver = DayTwoSolver(self.test_input)
|
||||||
|
self.assertEqual(solver.solve_p2(), 4)
|
||||||
|
82
puzzles/3.py
82
puzzles/3.py
@@ -1,40 +1,58 @@
|
|||||||
import re
|
import re
|
||||||
from functools import reduce
|
from typing import override
|
||||||
|
from unittest import TestCase
|
||||||
|
|
||||||
test_input_p1 = """
|
from puzzles._solver import Solver
|
||||||
xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))
|
|
||||||
"""
|
|
||||||
|
|
||||||
test_input_p2 = """
|
|
||||||
xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))
|
|
||||||
"""
|
|
||||||
|
|
||||||
test_solution_p1 = 161
|
|
||||||
test_solution_p2 = 48
|
|
||||||
|
|
||||||
|
simple_instruction_pattern = re.compile(r"mul\((\d+),(\d+)\)")
|
||||||
instruction_pattern = re.compile(r"mul\((\d+),(\d+)\)|(do(?:n't)?)\(\)")
|
instruction_pattern = re.compile(r"mul\((\d+),(\d+)\)|(do(?:n't)?)\(\)")
|
||||||
|
|
||||||
|
|
||||||
def solve_p1(puzzle_input: str) -> int:
|
class DayNSolver(Solver):
|
||||||
instructions = (
|
memory: str
|
||||||
tuple(map(int, match[:2]))
|
|
||||||
for match in instruction_pattern.findall(puzzle_input)
|
@override
|
||||||
if match[0]
|
def __init__(self, puzzle_input: str):
|
||||||
)
|
self.memory = puzzle_input
|
||||||
return sum(a * b for a, b in instructions)
|
|
||||||
|
@override
|
||||||
|
def solve_p1(self) -> int:
|
||||||
|
total = 0
|
||||||
|
|
||||||
|
for match in simple_instruction_pattern.findall(self.memory):
|
||||||
|
a, b = match
|
||||||
|
total += int(a) * int(b)
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
@override
|
||||||
|
def solve_p2(self) -> int:
|
||||||
|
total = 0
|
||||||
|
enabled = True
|
||||||
|
|
||||||
|
for match in instruction_pattern.findall(self.memory):
|
||||||
|
a, b, enable_flag = match
|
||||||
|
match enable_flag:
|
||||||
|
case "do":
|
||||||
|
enabled = True
|
||||||
|
case "don't":
|
||||||
|
enabled = False
|
||||||
|
case "":
|
||||||
|
if enabled:
|
||||||
|
total += int(a) * int(b)
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
def solve_p2(puzzle_input: str) -> int:
|
class TestDayNSolver(TestCase):
|
||||||
instructions = (
|
def test_solve_p1(self):
|
||||||
match[2] or tuple(map(int, match[:2]))
|
solver = DayNSolver(
|
||||||
for match in instruction_pattern.findall(puzzle_input)
|
"xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))\n"
|
||||||
)
|
)
|
||||||
return reduce(
|
self.assertEqual(solver.solve_p1(), 161)
|
||||||
lambda a, i: (
|
|
||||||
(1 if i == "do" else 0, a[1])
|
def test_solve_p2(self):
|
||||||
if isinstance(i, str)
|
solver = DayNSolver(
|
||||||
else (a[0], a[1] + a[0] * i[0] * i[1])
|
"xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))\n"
|
||||||
),
|
)
|
||||||
instructions,
|
self.assertEqual(solver.solve_p2(), 48)
|
||||||
(1, 0),
|
|
||||||
)[1]
|
|
||||||
|
12
puzzles/_solver.py
Normal file
12
puzzles/_solver.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Solver(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def __init__(self, puzzle_input: str): ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def solve_p1(self) -> int: ...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def solve_p2(self) -> int: ...
|
82
solve.py
82
solve.py
@@ -1,80 +1,60 @@
|
|||||||
import sys
|
import sys
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from inspect import getmembers, isclass
|
||||||
from time import time
|
from time import time
|
||||||
|
from unittest import TextTestRunner, defaultTestLoader
|
||||||
|
|
||||||
from advent.errors import InvalidSessionIDError, PuzzleNotFoundError
|
|
||||||
from advent.functions import get_puzzle_input
|
from advent.functions import get_puzzle_input
|
||||||
|
|
||||||
|
from puzzles._solver import Solver
|
||||||
def usage():
|
|
||||||
print("Usage: python solve.py <day>")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
if len(sys.argv) != 2:
|
if len(sys.argv) != 2:
|
||||||
usage()
|
raise RuntimeError("Usage: python solve.py <day>")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
day = int(sys.argv[1])
|
day = int(sys.argv[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Day must be an integer")
|
raise RuntimeError("Day must be an integer")
|
||||||
usage()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
print(f"Loading 2024, day {day} solver...")
|
||||||
try:
|
try:
|
||||||
print(f"Loading 2024, day {day} solver")
|
|
||||||
mod = import_module(f"puzzles.{day}")
|
mod = import_module(f"puzzles.{day}")
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
print(f"Solver for day {day} not found")
|
print(f"Solver for day {day} not found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
try:
|
solvers = getmembers(mod, lambda m: isclass(m) and issubclass(m, Solver))
|
||||||
print("Testing part 1 solution...")
|
if len(solvers) == 0:
|
||||||
test_input_p1 = getattr(mod, "test_input_p1", getattr(mod, "test_input", None))
|
raise RuntimeError("No solver found")
|
||||||
start = time()
|
solver_class: type[Solver] = solvers[0][1]
|
||||||
assert mod.solve_p1(test_input_p1) == mod.test_solution_p1
|
|
||||||
print(f"Test passed in {time() - start:.3f} seconds")
|
|
||||||
|
|
||||||
print("Testing part 2 solution...")
|
test_suite = defaultTestLoader.loadTestsFromModule(mod)
|
||||||
test_input_p2 = getattr(mod, "test_input_p2", getattr(mod, "test_input", None))
|
test_runner = TextTestRunner(verbosity=0)
|
||||||
start = time()
|
test_result = test_runner.run(test_suite)
|
||||||
assert mod.solve_p2(test_input_p2) == mod.test_solution_p2
|
if not test_result.wasSuccessful():
|
||||||
print(f"Test passed in {time() - start:.3f} seconds")
|
raise RuntimeError("Tests failed")
|
||||||
|
|
||||||
except AssertionError:
|
print("Fetching puzzle input...")
|
||||||
print("Test failed")
|
puzzle_input = get_puzzle_input(2024, int(day))
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
except Exception as exc:
|
solver = solver_class(puzzle_input)
|
||||||
print("Error:", exc)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
print("Solving part 1...")
|
||||||
print("Fetching puzzle input...")
|
start = time()
|
||||||
puzzle_input = get_puzzle_input(2024, int(day))
|
solution_p1 = solver.solve_p1()
|
||||||
except (PuzzleNotFoundError, InvalidSessionIDError) as exc:
|
print(f"Part 1 solution: {solution_p1} ({time() - start:.3f} seconds)")
|
||||||
print(exc)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
print("Solving part 2...")
|
||||||
print("Solving part 1...")
|
start = time()
|
||||||
start = time()
|
solution_p2 = solver.solve_p2()
|
||||||
solution_p1 = mod.solve_p1(puzzle_input)
|
print(f"Part 2 solution: {solution_p2} ({time() - start:.3f} seconds)")
|
||||||
print(f"Part 1 solution: {solution_p1} ({time() - start:.3f} seconds)")
|
|
||||||
|
|
||||||
print("Solving part 2...")
|
|
||||||
solution_p2 = mod.solve_p2(puzzle_input)
|
|
||||||
print(f"Part 2 solution: {solution_p2} ({time() - start:.3f} seconds)")
|
|
||||||
|
|
||||||
except NotImplementedError:
|
|
||||||
print("Puzzle solver is incomplete. Exiting")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
except Exception as exc:
|
|
||||||
print("Error:", exc)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as exc:
|
||||||
|
print(exc)
|
||||||
|
sys.exit(1)
|
||||||
|
Reference in New Issue
Block a user