Cleanup day 2 solution
This commit is contained in:
@@ -4,14 +4,13 @@ __author__ = "Nettika <nettika@leaf.ninja>"
|
|||||||
__version__ = "1.0.0"
|
__version__ = "1.0.0"
|
||||||
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from advent_of_code.trebuchet import (
|
|
||||||
recover_calibration_digits_and_words,
|
from advent_of_code import cubes, trebuchet
|
||||||
recover_calibration_digits_only,
|
|
||||||
)
|
|
||||||
|
|
||||||
Solver = Callable[[str], str]
|
Solver = Callable[[str], str]
|
||||||
|
|
||||||
|
|
||||||
solvers: dict[int, tuple[Solver, Solver]] = {
|
solvers: dict[int, tuple[Solver, Solver]] = {
|
||||||
1: (recover_calibration_digits_only, recover_calibration_digits_and_words),
|
1: (trebuchet.solve_part_1, trebuchet.solve_part_2),
|
||||||
|
2: (cubes.solve_part_1, cubes.solve_part_2),
|
||||||
}
|
}
|
||||||
|
92
advent_of_code/cubes.py
Normal file
92
advent_of_code/cubes.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from collections import UserDict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration(UserDict[str, int]):
|
||||||
|
color_yield_pattern: ClassVar[re.Pattern] = re.compile(r"(\d+) (\w+)")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_color_yield(yield_desc: str) -> tuple[str, int]:
|
||||||
|
yield_match = Configuration.color_yield_pattern.match(yield_desc.strip())
|
||||||
|
if not yield_match:
|
||||||
|
raise ValueError(f"Color yield is invalid: {yield_desc}")
|
||||||
|
amount = int(yield_match.group(1))
|
||||||
|
color = yield_match.group(2)
|
||||||
|
return (color, amount)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, desc: str) -> Configuration:
|
||||||
|
return cls(
|
||||||
|
cls._parse_color_yield(yield_desc)
|
||||||
|
for yield_desc in desc.split(",")
|
||||||
|
if len(yield_desc)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_sequence(cls, desc: str) -> list[Configuration]:
|
||||||
|
return [cls.parse(draw_desc) for draw_desc in desc.split(";") if len(draw_desc)]
|
||||||
|
|
||||||
|
def power(self) -> int:
|
||||||
|
return self.get("blue", 0) * self.get("green", 0) * self.get("red", 0)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Game:
|
||||||
|
id: int
|
||||||
|
draws: list[Configuration]
|
||||||
|
|
||||||
|
id_pattern: ClassVar[re.Pattern] = re.compile(r"Game (\d+)$")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_table(cls, table: str) -> list[Game]:
|
||||||
|
return [cls.parse(line) for line in table.split("\n")]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, desc: str) -> Game:
|
||||||
|
id_segment, _, draws_segment = desc.partition(":")
|
||||||
|
return cls(
|
||||||
|
cls._parse_id(id_segment),
|
||||||
|
Configuration.parse_sequence(draws_segment),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_id(desc: str) -> int:
|
||||||
|
id_match = Game.id_pattern.match(desc)
|
||||||
|
if not id_match:
|
||||||
|
raise ValueError("Game ID is invalid.")
|
||||||
|
return int(id_match.group(1))
|
||||||
|
|
||||||
|
def meets_configuration(self, configuration: Configuration) -> bool:
|
||||||
|
for draw in self.draws:
|
||||||
|
for name, quantity in draw.items():
|
||||||
|
if quantity > configuration.get(name, 0):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def minimum_configuration(self) -> Configuration:
|
||||||
|
config = Configuration()
|
||||||
|
for draw in self.draws:
|
||||||
|
for name, quantity in draw.items():
|
||||||
|
config[name] = max(quantity, config.get(name, 0))
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def solve_part_1(input: str) -> str:
|
||||||
|
actual_bag = Configuration(red=12, green=13, blue=14)
|
||||||
|
return str(
|
||||||
|
sum(
|
||||||
|
game.id
|
||||||
|
for game in Game.parse_table(input)
|
||||||
|
if game.meets_configuration(actual_bag)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def solve_part_2(input: str) -> str:
|
||||||
|
return str(
|
||||||
|
sum(game.minimum_configuration().power() for game in Game.parse_table(input))
|
||||||
|
)
|
@@ -1,5 +1,6 @@
|
|||||||
import re
|
"Day 1: Trebuchet?!"
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
number_word_map = {
|
number_word_map = {
|
||||||
"one": "1",
|
"one": "1",
|
||||||
@@ -61,7 +62,7 @@ def _recover_all_calibration_values(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def recover_calibration_digits_only(input: str) -> str:
|
def solve_part_1(input: str) -> str:
|
||||||
return str(
|
return str(
|
||||||
_recover_all_calibration_values(
|
_recover_all_calibration_values(
|
||||||
input,
|
input,
|
||||||
@@ -72,7 +73,7 @@ def recover_calibration_digits_only(input: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def recover_calibration_digits_and_words(input: str) -> str:
|
def solve_part_2(input: str) -> str:
|
||||||
return str(
|
return str(
|
||||||
_recover_all_calibration_values(
|
_recover_all_calibration_values(
|
||||||
input,
|
input,
|
||||||
|
121
tests/cubes_test.py
Normal file
121
tests/cubes_test.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from advent_of_code.cubes import Configuration, Game, solve_part_1, solve_part_2
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration_parse():
|
||||||
|
assert Configuration.parse("1 red") == Configuration(red=1)
|
||||||
|
assert Configuration.parse(" 2 blue, 4 green,") == Configuration(green=4, blue=2)
|
||||||
|
assert Configuration.parse("") == Configuration()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Configuration.parse("invalid")
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration_parse_sequence():
|
||||||
|
assert Configuration.parse_sequence("1 red; 2 blue, 4 green;") == [
|
||||||
|
Configuration(red=1),
|
||||||
|
Configuration(green=4, blue=2),
|
||||||
|
]
|
||||||
|
assert Configuration.parse_sequence("") == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_configuration_power():
|
||||||
|
assert Configuration(red=2, blue=3, green=5).power() == 30
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_parse_table():
|
||||||
|
assert list(Game.parse_table("Game 1: 1 red\nGame 2: 1 blue")) == [
|
||||||
|
Game(1, [{"red": 1}]),
|
||||||
|
Game(2, [{"blue": 1}]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_parse():
|
||||||
|
assert Game.parse("Game 3: 1 green") == Game(3, [Configuration(green=1)])
|
||||||
|
assert Game.parse(
|
||||||
|
"Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green",
|
||||||
|
) == Game(
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
Configuration(blue=3, red=4),
|
||||||
|
Configuration(red=1, green=2, blue=6),
|
||||||
|
Configuration(green=2),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert Game.parse("Game 4:") == Game(4, [])
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
assert Game.parse("invalid")
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
assert Game.parse(":")
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_meets_configuration():
|
||||||
|
bag = Configuration(red=5, green=5, blue=5)
|
||||||
|
|
||||||
|
assert Game(
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
Configuration(red=3, blue=5),
|
||||||
|
Configuration(green=2, blue=3),
|
||||||
|
Configuration(red=4),
|
||||||
|
],
|
||||||
|
).meets_configuration(bag)
|
||||||
|
assert Game(3, []).meets_configuration(bag)
|
||||||
|
assert not Game(
|
||||||
|
2,
|
||||||
|
[
|
||||||
|
Configuration(green=3),
|
||||||
|
Configuration(red=6, blue=2),
|
||||||
|
Configuration(blue=2),
|
||||||
|
],
|
||||||
|
).meets_configuration(bag)
|
||||||
|
assert not Game(3, [Configuration(orange=1)]).meets_configuration(bag)
|
||||||
|
|
||||||
|
|
||||||
|
def test_game_minimum_configuration():
|
||||||
|
assert Game(
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
Configuration(red=3, blue=5),
|
||||||
|
Configuration(green=2, blue=3),
|
||||||
|
Configuration(red=4),
|
||||||
|
],
|
||||||
|
).minimum_configuration() == Configuration(red=4, blue=5, green=2)
|
||||||
|
|
||||||
|
assert Game(
|
||||||
|
2,
|
||||||
|
[
|
||||||
|
Configuration(red=1),
|
||||||
|
Configuration(blue=1),
|
||||||
|
Configuration(green=1),
|
||||||
|
],
|
||||||
|
).minimum_configuration() == Configuration(red=1, blue=1, green=1)
|
||||||
|
|
||||||
|
|
||||||
|
mock_input = "\n".join(
|
||||||
|
[
|
||||||
|
"Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green",
|
||||||
|
"Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue",
|
||||||
|
"Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red",
|
||||||
|
"Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red",
|
||||||
|
"Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_solve_pt_1():
|
||||||
|
assert solve_part_1(mock_input) == "8"
|
||||||
|
|
||||||
|
|
||||||
|
def test_solve_pt_2():
|
||||||
|
assert solve_part_2(mock_input) == "2286"
|
||||||
|
|
||||||
|
|
||||||
|
# def test_configuration_power():
|
||||||
|
# assert [
|
||||||
|
# _configuration_power(Game.parse(game_desc).minimum_configuration())
|
||||||
|
# for game_desc in mock_input
|
||||||
|
# ] == [48, 12, 1560, 630, 36]
|
@@ -1,9 +1,12 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from advent_of_code.trebuchet import (
|
from advent_of_code.trebuchet import (
|
||||||
recover_calibration_digits_and_words,
|
|
||||||
recover_calibration_digits_only,
|
|
||||||
_recover_all_calibration_values,
|
_recover_all_calibration_values,
|
||||||
_recover_calibration_value,
|
_recover_calibration_value,
|
||||||
|
solve_part_1,
|
||||||
|
solve_part_2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -18,6 +21,14 @@ def test_recover_calibration_value():
|
|||||||
== 12
|
== 12
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
_recover_calibration_value(
|
||||||
|
".",
|
||||||
|
re.compile("ab"),
|
||||||
|
re.compile("ba"),
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_recover_all_calibration_values():
|
def test_recover_all_calibration_values():
|
||||||
assert (
|
assert (
|
||||||
@@ -31,9 +42,9 @@ def test_recover_all_calibration_values():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_recover_calibration_digits_only():
|
def test_solve_part_1():
|
||||||
assert (
|
assert (
|
||||||
recover_calibration_digits_only(
|
solve_part_1(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
[
|
[
|
||||||
"1abc2",
|
"1abc2",
|
||||||
@@ -47,9 +58,9 @@ def test_recover_calibration_digits_only():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_recover_calibration_digits_and_words():
|
def test_solve_part_2():
|
||||||
assert (
|
assert (
|
||||||
recover_calibration_digits_and_words(
|
solve_part_2(
|
||||||
"\n".join(
|
"\n".join(
|
||||||
[
|
[
|
||||||
"two1nine",
|
"two1nine",
|
||||||
|
Reference in New Issue
Block a user