lots of cleanup

This commit is contained in:
Ruediger Ludwig 2022-12-10 19:30:10 +01:00
parent 7d0d3e504e
commit 0385cbd62e
26 changed files with 337 additions and 417 deletions

View file

@ -1,7 +1,7 @@
import sys
from importlib import import_module
from advent.common import utils
from advent.common import input
from advent.days.template import Day, ResultType, is_day
@ -30,7 +30,7 @@ def get_day(day_num: int) -> Day:
def run(day: Day, part: int) -> None:
data = utils.read_data(day.day_num, 'input.txt')
data = input.read_lines(day.day_num, 'input.txt')
match part:
case 1: output(day.day_num, 1, day.part1(data))
case 2: output(day.day_num, 2, day.part2(data))

View file

@ -1,37 +0,0 @@
from typing import Iterable, Iterator
from .provider import EofException, Provider
class CharProvider(Provider[str]):
data: Iterator[str]
def __init__(self, data: Iterator[str] | Iterable[str]) -> None:
if isinstance(data, Iterator):
self.data = data
else:
self.data = iter(data)
self.peeked: list[str] = []
def _ensure_next(self) -> str:
if not self.peeked:
try:
self.peeked = [next(self.data)]
except StopIteration:
raise EofException() from None
return self.peeked[0]
def peek(self) -> str:
return self._ensure_next()
def get(self) -> str:
result = self._ensure_next()
self.peeked = self.peeked[1:]
return result
def finished(self) -> bool:
try:
self._ensure_next()
return False
except EofException:
return True

View file

@ -1,33 +0,0 @@
from advent.common.provider import EofException
from .char_provider import CharProvider
class ReaderException(Exception):
pass
class CharReader:
@staticmethod
def read_word(provider: CharProvider, word: str) -> str:
result = ''
for expected_char in word:
try:
char = provider.get()
if char == expected_char:
result += char
else:
raise ReaderException(f'Expected {word} but received {result}{char}')
except EofException:
raise ReaderException(f'Expected {word} but received {result}[EOF]')
return word
@staticmethod
def read_unsigned_int(provider: CharProvider) -> int:
if not provider.peek().isdigit():
raise ReaderException('Expected unsigned int')
number = 0
while not provider.finished() and provider.peek().isdigit():
number = number * 10 + int(provider.get())
return number

19
advent/common/input.py Normal file
View file

@ -0,0 +1,19 @@
from pathlib import Path, PurePath
from typing import Iterator, TypeVar
T = TypeVar('T')
def read_lines(day: int, file_name: str) -> Iterator[str]:
'''
Returns an iterator over the content of the mentioned file
All lines are striped of an eventual trailing '\n' their
'''
with open(
Path.cwd()
/ PurePath('advent/days/day{0:02}/data'.format(day))
/ PurePath(file_name),
'rt',
) as file:
while line := file.readline():
yield line.rstrip('\n')

View file

@ -1,31 +0,0 @@
from abc import abstractmethod
from typing import Iterable, Iterator, Protocol, TypeVar
T = TypeVar('T', covariant=True)
class EofException(Exception):
pass
class Provider(Iterator[T], Iterable[T], Protocol[T]):
@abstractmethod
def peek(self) -> T:
...
@abstractmethod
def get(self) -> T:
...
@abstractmethod
def finished(self) -> bool:
...
def __next__(self) -> T:
if self.finished():
raise StopIteration()
return self.get()
def __iter__(self):
return self

View file

@ -1,46 +0,0 @@
from pathlib import Path, PurePath
from typing import Callable, Generator, Iterator, ParamSpec, TypeVar
T = TypeVar('T')
def read_data(day: int, file_name: str) -> Iterator[str]:
'''
Returns an iterator over the content of the mentioned file
All lines are striped of an eventual trailing '\n' their
'''
with open(
Path.cwd()
/ PurePath('advent/days/day{0:02}/data'.format(day))
/ PurePath(file_name),
'rt',
) as file:
while True:
line = file.readline()
if line:
yield line if line[-1] != '\n' else line[:-1]
else:
return
def split_set(full_set: set[T], predicate: Callable[[T], bool]) -> tuple[set[T], set[T]]:
''' Splits a set in two sorted by the predicate '''
true_set: set[T] = set()
false_set: set[T] = set()
for item in full_set:
(true_set if predicate(item) else false_set).add(item)
return true_set, false_set
P = ParamSpec('P')
Y = TypeVar('Y')
S = TypeVar('S')
R = TypeVar('R')
def coroutine(func: Callable[P, Generator[Y, S, R]]) -> Callable[P, Generator[Y, S, R]]:
def start(*args: P.args, **kwargs: P.kwargs) -> Generator[Y, S, R]:
cr = func(*args, **kwargs)
next(cr)
return cr
return start

View file

@ -1,17 +1,17 @@
from advent.common import utils
from advent.common import input
from .solution import day_num, part1, part2
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 24_000
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 45_000
result = part2(data)
assert result == expected

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from typing import Iterator
from typing import Iterator, Self
from enum import Enum
day_num = 2
@ -26,8 +26,8 @@ class Shape(Enum):
Paper = 2
Scissors = 3
@staticmethod
def parse(line: str) -> tuple[Shape, Shape]:
@classmethod
def parse(cls, line: str) -> tuple[Self, Self]:
"""
Parses a line into a game of RPC
Parameters
@ -47,11 +47,11 @@ class Shape(Enum):
or either of the shapes is unknown
"""
match line.strip().split():
case [o, p]: return Shape.parse_opponent(o), Shape.parse_player(p)
case [o, p]: return cls.parse_opponent(o), cls.parse_player(p)
case _: raise Exception(f"Unknown line: {line}")
@staticmethod
def parse_opponent(char: str) -> Shape:
@classmethod
def parse_opponent(cls, char: str) -> Self:
"""
Parses a shape for RPC
A -> Rock
@ -73,13 +73,13 @@ class Shape(Enum):
If the character does not describe a valid shape
"""
match char.strip().upper():
case 'A': return Shape.Rock
case 'B': return Shape.Paper
case 'C': return Shape.Scissors
case 'A': return cls.Rock
case 'B': return cls.Paper
case 'C': return cls.Scissors
case _: raise Exception(f"Unknown char : {char}")
@staticmethod
def parse_player(char: str) -> Shape:
@classmethod
def parse_player(cls, char: str) -> Self:
"""
Parses a shape for RPC using rules for player shapes
X -> Rock
@ -101,9 +101,9 @@ class Shape(Enum):
If the character does not describe a valid shape
"""
match char.strip().upper():
case 'X': return Shape.Rock
case 'Y': return Shape.Paper
case 'Z': return Shape.Scissors
case 'X': return cls.Rock
case 'Y': return cls.Paper
case 'Z': return cls.Scissors
case _: raise Exception(f"Unknown char : {char}")
def prev(self) -> Shape:
@ -135,8 +135,8 @@ class Result(Enum):
Draw = 2
Win = 3
@staticmethod
def parse(line: str) -> tuple[Shape, Result]:
@classmethod
def parse(cls, line: str) -> tuple[Shape, Self]:
"""
Parses a line into a game of RPC with anm expected outcome
Parameters
@ -156,11 +156,11 @@ class Result(Enum):
or either the shape or result is unknown
"""
match line.strip().split():
case [o, r]: return Shape.parse_opponent(o), Result.parse_result(r)
case [o, r]: return Shape.parse_opponent(o), cls.parse_result(r)
case _: raise Exception(f"Unknown line: {line}")
@staticmethod
def parse_result(char: str) -> Result:
@classmethod
def parse_result(cls, char: str) -> Self:
"""
Parses an expected result for RPC
X -> Lose
@ -182,9 +182,9 @@ class Result(Enum):
If the character does not describe a valid result
"""
match char.strip().upper():
case 'X': return Result.Lose
case 'Y': return Result.Draw
case 'Z': return Result.Win
case 'X': return cls.Lose
case 'Y': return cls.Draw
case 'Z': return cls.Win
case _: raise Exception(f"Unknown char : {char}")
def player_shape(self, other: Shape) -> Shape:

View file

@ -1,17 +1,17 @@
from advent.common import utils
from advent.common import input
from .solution import day_num, part1, part2, Shape, Result
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 15
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 12
result = part2(data)
assert result == expected

View file

@ -1,17 +1,17 @@
from advent.common import utils
from advent.common import input
from .solution import day_num, part1, part2
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 157
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 70
result = part2(data)
assert result == expected

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterator
from typing import Iterator, Self
day_num = 4
@ -19,10 +19,10 @@ class Range:
start: int
end: int
@staticmethod
def parse(line: str) -> Range:
@classmethod
def parse(cls, line: str) -> Self:
match line.split('-'):
case [s, e]: return Range(int(s), int(e))
case [s, e]: return cls(int(s), int(e))
case _: raise Exception(f"Not a valid range: {line}")
def includes(self, other: Range) -> bool:
@ -39,10 +39,10 @@ class Pair:
first: Range
second: Range
@staticmethod
def parse(line: str) -> Pair:
@classmethod
def parse(cls, line: str) -> Self:
match line.split(','):
case [f, s]: return Pair(Range.parse(f), Range.parse(s))
case [f, s]: return cls(Range.parse(f), Range.parse(s))
case _: raise Exception(f"Not a valid Pair: {line}")
def includes(self) -> bool:

View file

@ -1,17 +1,17 @@
from advent.common import utils
from advent.common import input
from .solution import Pair, Range, day_num, part1, part2
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 2
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 4
result = part2(data)
assert result == expected

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import ClassVar, Iterator
from typing import ClassVar, Iterator, Self
from advent.parser.parser import P
@ -31,13 +31,13 @@ class Move:
to_parser: ClassVar[P[int]] = P.second(P.string(" to "), P.unsigned())
move_parser: ClassVar[P[tuple[int, int, int]]] = P.seq(amount_parser, from_parser, to_parser)
@staticmethod
def parse(line: str) -> Move | None:
parsed = Move.move_parser.parse(line)
@classmethod
def parse(cls, line: str) -> Self | None:
parsed = cls.move_parser.parse(line)
if parsed.is_fail():
return None
amount, frm, to = parsed.get()
return Move(amount, frm - 1, to - 1)
return cls(amount, frm - 1, to - 1)
def do_move(self, crates: list[str], as_9001: bool) -> list[str]:
"""
@ -62,12 +62,12 @@ class Crane:
P.one_char().in_brackets(), P.string(" ").replace(None))
crate_row_parser: ClassVar[P[list[str | None]]] = crate_parser.sep_by(P.is_char(' '))
@staticmethod
def parse_crate_row(line: str) -> list[None | str] | None:
@classmethod
def parse_crate_row(cls, line: str) -> list[None | str] | None:
return Crane.crate_row_parser.parse(line).get_or(None)
@staticmethod
def parse_stacks(lines: Iterator[str]) -> list[str]:
@classmethod
def parse_stacks(cls, lines: Iterator[str]) -> list[str]:
stacks: list[str] = []
for line in lines:
crate_row = Crane.parse_crate_row(line)
@ -83,14 +83,14 @@ class Crane:
raise Exception("Can never happen")
@staticmethod
def parse(lines: Iterator[str], is_9001: bool) -> Crane:
drawing = Crane.parse_stacks(lines)
@classmethod
def parse(cls, lines: Iterator[str], is_9001: bool) -> Self:
drawing = cls.parse_stacks(lines)
moves = [p for p in (Move.parse(line) for line in lines) if p is not None]
return Crane(drawing, moves, is_9001)
return cls(drawing, moves, is_9001)
@staticmethod
def top(crates: list[str]) -> str:
@classmethod
def top(cls, crates: list[str]) -> str:
""" Lists the last item in the given stacks. Fails if any stack is empty """
return ''.join(stack[-1] for stack in crates)

View file

@ -1,17 +1,17 @@
from advent.common import utils
from advent.common import input
from .solution import Move, day_num, part1, part2, Crane
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = "CMZ"
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = "MCD"
result = part2(data)
assert result == expected
@ -32,7 +32,7 @@ def test_parse_line2():
def test_drawing():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = ["ZN", "MCD", "P"]
result = Crane.parse_stacks(data)
assert result == expected
@ -46,7 +46,7 @@ def test_parse_move():
def test_parse_all():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = Crane(
["ZN", "MCD", "P"],
[Move(1, 1, 0), Move(3, 0, 2), Move(2, 1, 0), Move(1, 0, 1)], True)
@ -55,7 +55,7 @@ def test_parse_all():
def test_all_moves():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
crane = Crane.parse(data, False)
expected = ["C", "M", "PDNZ"]
result = crane.perform_all_moves()
@ -63,7 +63,7 @@ def test_all_moves():
def test_all_moves9001():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
crane = Crane.parse(data, True)
expected = ["M", "C", "PZND"]
result = crane.perform_all_moves()

View file

@ -1,17 +1,17 @@
from advent.common import utils
from advent.common import input
from .solution import day_num, marker, part1, part2
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 7
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 19
result = part2(data)
assert result == expected

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterator
from typing import Iterator, Self
day_num = 7
@ -15,32 +15,44 @@ def part2(lines: Iterator[str]) -> int:
return directory.get_min_delete_size(70_000_000, 30_000_000)
@dataclass(slots=True)
@dataclass(slots=True, eq=False)
class Directory:
name: str
parent: Directory | None
subdirs: list[Directory] = field(default_factory=list)
files: list[tuple[str, int]] = field(default_factory=list)
size: int | None = None
subdirs: list[Directory] = field(default_factory=list, init=False)
files: list[tuple[str, int]] = field(default_factory=list, init=False)
size: int | None = field(default=None, init=False, repr=False)
@classmethod
def create_root(cls) -> Self:
return cls('/', None)
def cd_into(self, name: str) -> Directory:
"""
Returns the named sub directory or .. for parent
May fail if unkown subdirectory - or already in root
"""
if name == "..":
if self.parent is None:
raise Exception('Already at root Directory')
return self.parent
match name:
case '/':
current = self
while current.parent is not None:
current = current.parent
return current
for sub in self.subdirs:
if sub.name == name:
return sub
raise Exception(f"Could not find subdir {name}")
case '..':
if self.parent is None:
raise Exception('Already at root Directory')
return self.parent
case _:
for sub in self.subdirs:
if sub.name == name:
return sub
raise Exception(f"Could not find subdir {name}")
def add_directory(self, name: str):
""" Adds the named directory."""
self.subdirs.append(Directory(name, self))
self.subdirs.append(Directory(name, parent=self))
def add_file(self, name: str, size: int):
""" Adds the given file and size """
@ -69,7 +81,7 @@ class Directory:
"""
Returns the size of the smallest directory that must be removed to created the free space
given as a parameter and the given disk size
#"""
"""
unused = disk_size - self.get_size()
minimum: int | None = None
for dir in self.get_all_directories():
@ -82,14 +94,11 @@ class Directory:
return minimum
@staticmethod
def parse(lines: Iterator[str]) -> Directory:
line = next(lines)
if line != '$ cd /':
raise Exception(f"Illegal first line: {line}")
root = Directory('/', None)
@classmethod
def parse(cls, lines: Iterator[str]) -> Self:
root = cls.create_root()
current = root
for line in lines:
match line.split():
case ['$', 'cd', name]:

View file

@ -1,24 +1,24 @@
from advent.common import utils
from advent.common import input
from .solution import day_num, part1, part2, Directory
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 95437
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 24933642
result = part2(data)
assert result == expected
def test_size():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 48381165
directory = Directory.parse(data)
result = directory.get_size()
@ -26,7 +26,7 @@ def test_size():
def test_maxed_size():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 95437
directory = Directory.parse(data)
result = directory.get_maxed_size(100_000)
@ -34,7 +34,7 @@ def test_maxed_size():
def test_find_to_delete():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 24933642
directory = Directory.parse(data)
result = directory.get_min_delete_size(70_000_000, 30_000_000)

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterator
from typing import Iterator, Self
day_num = 8
@ -20,10 +20,10 @@ class Forest:
width: int
height: int
@staticmethod
def parse(lines: Iterator[str]) -> Forest:
@classmethod
def parse(cls, lines: Iterator[str]) -> Self:
trees = [[int(tree) for tree in line] for line in lines]
return Forest(trees, len(trees[0]), len(trees))
return cls(trees, len(trees[0]), len(trees))
def count_visible_trees(self) -> int:
visible: set[tuple[int, int]] = set()

View file

@ -1,45 +1,45 @@
from advent.common import utils
from advent.common import input
from .solution import Forest, day_num, part1, part2
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 21
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 8
result = part2(data)
assert result == expected
def test_visible():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 21
result = Forest.parse(data).count_visible_trees()
assert result == expected
def test_distance():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 4
result = Forest.parse(data).single_scenic_score(2, 1)
assert result == expected
def test_distance2():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 8
result = Forest.parse(data).single_scenic_score(2, 3)
assert result == expected
def test_max_distance():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 8
result = Forest.parse(data).max_scenic_score()
assert result == expected

View file

@ -1,19 +1,19 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterator
from typing import Iterator, Self
day_num = 9
def part1(lines: Iterator[str]) -> int:
lst = [Command.parse(line) for line in lines]
return Command.walk(lst, 2)
commands = (Command.parse(line) for line in lines)
return simulate(commands, 2)
def part2(lines: Iterator[str]) -> int:
lst = [Command.parse(line) for line in lines]
return Command.walk(lst, 10)
commands = (Command.parse(line) for line in lines)
return simulate(commands, 10)
@dataclass(frozen=True, slots=True)
@ -21,18 +21,18 @@ class Point:
x: int
y: int
@staticmethod
def parse_direction(char: str) -> Point:
@classmethod
def parse_direction(cls, char: str) -> Self:
""" Parses the given direction to a Point. May raise if invalid """
match char:
case 'R':
return Point(1, 0)
return cls(1, 0)
case 'U':
return Point(0, 1)
return cls(0, 1)
case 'L':
return Point(-1, 0)
return cls(-1, 0)
case 'D':
return Point(0, -1)
return cls(0, -1)
case _:
raise Exception(f"Unkown Direction: {char}")
@ -65,8 +65,8 @@ class Command:
dir: Point
steps: int
@staticmethod
def parse(line: str) -> Command:
@classmethod
def parse(cls, line: str) -> Self:
""" Parse a command line. My raise exception if the was an illegal line"""
match line.split():
case [dir, steps]:
@ -74,20 +74,20 @@ class Command:
case _:
raise Exception(f"Illegal line: {line}")
@staticmethod
def walk(lst: list[Command], rope_length: int):
""" Walks the whole rope in Planck length steps according to commands """
rope = [Point(0, 0)] * rope_length
visited = {rope[-1]}
for command in lst:
for _ in range(command.steps):
rope[0] = rope[0].add(command.dir)
for n in range(1, rope_length):
moved_piece = rope[n].step_to(rope[n - 1])
if not moved_piece:
break
rope[n] = moved_piece
if n == rope_length - 1:
visited.add(rope[n])
return len(visited)
def simulate(lst: Iterator[Command], rope_length: int) -> int:
""" Walks the whole rope in Planck length steps according to commands """
rope = [Point(0, 0)] * rope_length
visited = {rope[-1]}
for command in lst:
for _ in range(command.steps):
rope[0] = rope[0].add(command.dir)
for n in range(1, rope_length):
moved_piece = rope[n].step_to(rope[n - 1])
if not moved_piece:
break
rope[n] = moved_piece
if n == rope_length - 1:
visited.add(rope[n])
return len(visited)

View file

@ -1,41 +1,41 @@
from advent.common import utils
from advent.common import input
from .solution import Command, day_num, part1, part2
from .solution import Command, day_num, part1, part2, simulate
def test_part1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 13
result = part1(data)
assert result == expected
def test_part2():
data = utils.read_data(day_num, 'test02.txt')
data = input.read_lines(day_num, 'test02.txt')
expected = 36
result = part2(data)
assert result == expected
def test_short():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 13
lst = [Command.parse(line) for line in data]
result = Command.walk(lst, 2)
lst = (Command.parse(line) for line in data)
result = simulate(lst, 2)
assert result == expected
def test_long1():
data = utils.read_data(day_num, 'test01.txt')
data = input.read_lines(day_num, 'test01.txt')
expected = 1
lst = [Command.parse(line) for line in data]
result = Command.walk(lst, 10)
lst = (Command.parse(line) for line in data)
result = simulate(lst, 10)
assert result == expected
def test_long2():
data = utils.read_data(day_num, 'test02.txt')
data = input.read_lines(day_num, 'test02.txt')
expected = 36
lst = [Command.parse(line) for line in data]
result = Command.walk(lst, 10)
lst = (Command.parse(line) for line in data)
result = simulate(lst, 10)
assert result == expected

View file

@ -53,7 +53,7 @@ def draw(lines: Iterator[str], width: int, height: int) -> list[str]:
picture = ""
for cycle, sprite in enumerate(cycles(lines)):
crt_pos = cycle % width
if sprite - 1 <= crt_pos and crt_pos <= sprite + 1:
if sprite - 1 <= crt_pos <= sprite + 1:
picture += '#'
else:
picture += ' '

View file

@ -1,38 +1,38 @@
from advent.common import utils
from advent.common import input
from .solution import cycles, day_num, draw, grab_values, part1, part2
def test_part1():
lines = utils.read_data(day_num, 'test01.txt')
lines = input.read_lines(day_num, 'test01.txt')
expected = 13140
result = part1(lines)
assert result == expected
def test_part2():
lines = utils.read_data(day_num, 'test01.txt')
expected = list(utils.read_data(day_num, 'expected.txt'))
lines = input.read_lines(day_num, 'test01.txt')
expected = list(input.read_lines(day_num, 'expected.txt'))
result = part2(lines)
assert result == expected
def test_small():
lines = utils.read_data(day_num, 'test02.txt')
lines = input.read_lines(day_num, 'test02.txt')
expected = [1, 1, 1, 4, 4, -1]
result = list(cycles(lines))
assert result == expected
def test_grab_values():
lines = utils.read_data(day_num, 'test01.txt')
lines = input.read_lines(day_num, 'test01.txt')
expected = [420, 1140, 1800, 2940, 2880, 3960]
result = list(grab_values(lines))
assert result == expected
def test_draw():
lines = utils.read_data(day_num, 'test01.txt')
expected = list(utils.read_data(day_num, 'expected.txt'))
lines = input.read_lines(day_num, 'test01.txt')
expected = list(input.read_lines(day_num, 'expected.txt'))
result = draw(lines, 40, 6)
assert result == expected

View file

@ -1,17 +1,17 @@
from advent.common import utils
from advent.common import input
from .solution import day_num, part1, part2
def test_part1():
lines = utils.read_data(day_num, 'test01.txt')
lines = input.read_lines(day_num, 'test01.txt')
expected = None
result = part1(lines)
assert result == expected
def test_part2():
lines = utils.read_data(day_num, 'test01.txt')
lines = input.read_lines(day_num, 'test01.txt')
expected = None
result = part2(lines)
assert result == expected

View file

@ -60,16 +60,16 @@ class P(Generic[T]):
def parse_multi(self, s: str, i: int = 0) -> Iterator[T]:
return (v for _, v in self.func(ParserInput(s, i)))
@staticmethod
def pure(value: T) -> P[T]:
@classmethod
def pure(cls, value: T) -> P[T]:
return P(lambda pp: iter([(pp, value)]))
@staticmethod
def fail() -> P[Any]:
@classmethod
def fail(cls) -> P[Any]:
return P(lambda _: iter([]))
@staticmethod
def _fix(p1: Callable[[P[Any]], P[T]]) -> P[T]:
@classmethod
def _fix(cls, p1: Callable[[P[Any]], P[T]]) -> P[T]:
""" Not really nice helper function, but it works"""
return [p._forward(q.func) for p in [P(None)] for q in [p1(p)]][0] # type: ignore
@ -101,26 +101,31 @@ class P(Generic[T]):
def replace(self, value: TR) -> P[TR]:
return self.fmap(lambda _: value)
def unit(self) -> P[tuple[()]]:
def as_unit(self) -> P[tuple[()]]:
return self.fmap(lambda _: ())
def apply(self, p2: P[Callable[[T], TR]]) -> P[TR]:
return self.bind(lambda x: p2.bind(lambda y: P.pure(y(x))))
@classmethod
def first(cls, p1: P[T1], p2: P[Any]) -> P[T1]:
return p1.bind(lambda v1: p2.fmap(lambda _: v1))
@classmethod
def second(cls, p1: P[Any], p2: P[T2]) -> P[T2]:
return p1.bind(lambda _: p2)
def between(self, pre: P[Any], post: P[Any]) -> P[T]:
return P.map3(pre, self, post, lambda _1, v, _2: v)
def surround(self, other: P[Any]) -> P[T]:
return P.map3(other, self, other, lambda _1, v, _2: v)
def some_lazy(self) -> P[list[T]]:
return P._fix(lambda p: self.bind(
lambda x: P.either(P.pure([]), p).fmap(lambda ys: [x] + ys)))
def some(self) -> P[list[T]]:
return P._fix(lambda p: self.bind(
lambda x: P.either(p, P.pure([])).fmap(lambda ys: [x] + ys)))
def some_lazy(self) -> P[list[T]]:
return P._fix(lambda p: self.bind(
lambda x: P.either(P.pure([]), p).fmap(lambda ys: [x] + ys)))
def many(self) -> P[list[T]]:
return P.either(self.some(), P.pure([]))
@ -136,6 +141,22 @@ class P(Generic[T]):
def optional_lazy(self) -> P[T | None]:
return P.either(P.pure(None), self)
@overload
def times(self, *, exact: int) -> P[list[T]]:
...
@overload
def times(self, *, min: int) -> P[list[T]]:
...
@overload
def times(self, *, max: int) -> P[list[T]]:
...
@overload
def times(self, *, min: int, max: int) -> P[list[T]]:
...
def times(self, *, max: int | None = None, min: int | None = None,
exact: int | None = None) -> P[list[T]]:
match (exact, min, max):
@ -145,8 +166,26 @@ class P(Generic[T]):
return self.many().satisfies(lambda lst: len(lst) >= mn)
case (None, None, int(mx)):
return self.many().satisfies(lambda lst: len(lst) <= mx)
case (None, int(mn), int(mx)):
return self.many().satisfies(lambda lst: mn <= len(lst) <= mx)
case _:
raise Exception("Choose exactly one of exact, min or max")
raise Exception("Illegal combination of parameters")
@overload
def times_lazy(self, *, exact: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, min: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, max: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, min: int, max: int) -> P[list[T]]:
...
def times_lazy(self, *, max: int | None = None, min: int | None = None,
exact: int | None = None) -> P[list[T]]:
@ -157,24 +196,20 @@ class P(Generic[T]):
return self.many_lazy().satisfies(lambda lst: len(lst) >= mn)
case (None, None, int(mx)):
return self.many_lazy().satisfies(lambda lst: len(lst) <= mx)
case (None, int(mn), int(mx)):
return self.many_lazy().satisfies(lambda lst: mn <= len(lst) <= mx)
case _:
raise Exception("Choose exactly one of exact, min or max")
raise Exception("Illegal combination of parameters")
def sep_by(self, sep: P[Any]) -> P[list[T]]:
return P.map2(self, P.second(sep, self).many(), lambda f, r: [f] + r)
@staticmethod
def first(p1: P[T1], p2: P[Any]) -> P[T1]:
return P.map2(p1, p2, lambda v1, _: v1)
def sep_by_lazy(self, sep: P[Any]) -> P[list[T]]:
return P.map2(self, P.second(sep, self).many_lazy(), lambda f, r: [f] + r)
@staticmethod
def second(p1: P[Any], p2: P[T2]) -> P[T2]:
return p1.bind(lambda _: p2)
@staticmethod
def no_match(p: P[Any]) -> P[tuple[()]]:
def no(self) -> P[tuple[()]]:
def inner(parserPos: ParserInput) -> ParserResult[tuple[()]]:
result = p.func(parserPos)
result = self.func(parserPos)
try:
next(result)
# Silently yields nothing so is an empty Generator
@ -183,19 +218,19 @@ class P(Generic[T]):
return P(inner)
@staticmethod
def map2(p1: P[T1], p2: P[T2], func: Callable[[T1, T2], TR]) -> P[TR]:
@classmethod
def map2(cls, p1: P[T1], p2: P[T2], func: Callable[[T1, T2], TR]) -> P[TR]:
return p1.bind(lambda v1: p2.fmap(lambda v2: func(v1, v2)))
@staticmethod
def map3(p1: P[T1], p2: P[T2], p3: P[T3], func: Callable[[T1, T2, T3], TR]) -> P[TR]:
@classmethod
def map3(cls, p1: P[T1], p2: P[T2], p3: P[T3], func: Callable[[T1, T2, T3], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
lambda v2: p3.fmap(
lambda v3: func(v1, v2, v3))))
@staticmethod
def map4(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
@classmethod
def map4(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
func: Callable[[T1, T2, T3, T4], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
@ -203,8 +238,8 @@ class P(Generic[T]):
lambda v3: p4.fmap(
lambda v4: func(v1, v2, v3, v4)))))
@staticmethod
def map5(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5],
@classmethod
def map5(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5],
func: Callable[[T1, T2, T3, T4, T5], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
@ -213,57 +248,57 @@ class P(Generic[T]):
lambda v4: p5.fmap(
lambda v5: func(v1, v2, v3, v4, v5))))))
@staticmethod
@classmethod
@overload
def seq(p1: P[T1], p2: P[T2], /) -> P[tuple[T1, T2]]:
def seq(cls, p1: P[T1], p2: P[T2], /) -> P[tuple[T1, T2]]:
...
@staticmethod
@classmethod
@overload
def seq(p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[tuple[T1, T2, T3]]:
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[tuple[T1, T2, T3]]:
...
@staticmethod
@classmethod
@overload
def seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[tuple[T1, T2, T3, T4]]:
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[tuple[T1, T2, T3, T4]]:
...
@staticmethod
@classmethod
@overload
def seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /) -> P[tuple[T1, T2, T3, T4, T5]]:
...
@staticmethod
def seq(*ps: P[Any]) -> P[tuple[Any, ...]]:
@classmethod
def seq(cls, *ps: P[Any]) -> P[tuple[Any, ...]]:
return reduce(lambda p, x: x.bind(
lambda a: p.fmap(lambda b: chain([a], b))),
list(ps)[::-1], P.pure(iter([]))).fmap(tuple)
@staticmethod
@classmethod
@overload
def sep_seq(p1: P[T1], p2: P[T2], /, *, sep: P[Any]) -> P[tuple[T1, T2]]:
def sep_seq(cls, p1: P[T1], p2: P[T2], /, *, sep: P[Any]) -> P[tuple[T1, T2]]:
...
@staticmethod
@classmethod
@overload
def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3]]:
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3]]:
...
@staticmethod
@classmethod
@overload
def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /,
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /,
*, sep: P[Any]) -> P[tuple[T1, T2, T3, T4]]:
...
@staticmethod
@classmethod
@overload
def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3, T4, T5]]:
...
@staticmethod
def sep_seq(*ps: P[Any], sep: P[Any]) -> P[tuple[Any, ...]]:
@classmethod
def sep_seq(cls, *ps: P[Any], sep: P[Any]) -> P[tuple[Any, ...]]:
first, *rest = list(ps)
return P.map2(first,
reduce(lambda p, x: P.second(sep, x.bind(
@ -271,112 +306,108 @@ class P(Generic[T]):
rest[::-1], P.pure(iter([]))),
lambda f, r: (f,) + tuple(r))
@staticmethod
def either(p1: P[T1], p2: P[T2], /) -> P[T1 | T2]:
@classmethod
def either(cls, p1: P[T1], p2: P[T2], /) -> P[T1 | T2]:
def inner(parserPos: ParserInput):
yield from p1.func(parserPos)
yield from p2.func(parserPos)
return P(inner)
@staticmethod
@classmethod
@overload
def choice(p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[T1 | T2 | T3]:
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[T1 | T2 | T3]:
...
@staticmethod
@classmethod
@overload
def choice(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[T1 | T2 | T3 | T4]:
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[T1 | T2 | T3 | T4]:
...
@staticmethod
@classmethod
@overload
def choice(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /) -> P[T1 | T2 | T3 | T4 | T5]:
...
@staticmethod
def choice(*ps: P[Any]) -> P[Any]:
@classmethod
def choice(cls, *ps: P[Any]) -> P[Any]:
def inner(parserPos: ParserInput) -> Iterator[Any]:
for p in ps:
yield from p.func(parserPos)
return P(inner)
@staticmethod
def choice2(*ps: P[T]) -> P[T]:
@classmethod
def choice2(cls, *ps: P[T]) -> P[T]:
return P.choice(*ps)
@staticmethod
def one_char() -> P[str]:
@classmethod
def one_char(cls) -> P[str]:
def inner(parserPos: ParserInput) -> ParserResult[str]:
if parserPos.has_data():
yield parserPos.step()
return P(inner)
@staticmethod
def eof() -> P[tuple[()]]:
@classmethod
def eof(cls) -> P[tuple[()]]:
def inner(parserPos: ParserInput) -> ParserResult[tuple[()]]:
if not parserPos.has_data():
yield parserPos, ()
return P(inner)
@staticmethod
def char_func(cmp: Callable[[str], bool]) -> P[str]:
@classmethod
def char_func(cls, cmp: Callable[[str], bool]) -> P[str]:
return P.one_char().satisfies(cmp)
@staticmethod
def is_char(cmp: str) -> P[str]:
@classmethod
def is_char(cls, cmp: str) -> P[str]:
return P.char_func(lambda c: c == cmp)
@staticmethod
def is_not_char(s: str) -> P[tuple[()]]:
return P.no_match(P.is_char(s))
@staticmethod
def string(s: str) -> P[str]:
@classmethod
def string(cls, s: str) -> P[str]:
return P.seq(*map(P.is_char, s)).replace(s)
@staticmethod
def one_of(s: str) -> P[str]:
@classmethod
def one_of(cls, s: str) -> P[str]:
return P.char_func(lambda c: c in s)
@staticmethod
def any_decimal() -> P[str]:
@classmethod
def any_decimal(cls) -> P[str]:
return P.char_func(lambda c: c.isdecimal())
@staticmethod
def is_decimal(num: int) -> P[str]:
@classmethod
def is_decimal(cls, num: int) -> P[str]:
return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) == num)
@staticmethod
def is_not_decimal(num: int) -> P[str]:
@classmethod
def is_not_decimal(cls, num: int) -> P[str]:
return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) != num)
@staticmethod
def lower() -> P[str]:
@classmethod
def lower(cls) -> P[str]:
return P.char_func(lambda c: c.islower())
@staticmethod
def upper() -> P[str]:
@classmethod
def upper(cls) -> P[str]:
return P.char_func(lambda c: c.isupper())
@staticmethod
def space() -> P[str]:
@classmethod
def space(cls) -> P[str]:
return P.char_func(lambda c: c.isspace())
@staticmethod
def word(p1: P[str]) -> P[str]:
return P.first(p1.many().fmap(lambda cs: ''.join(cs)), P.no_match(p1))
@classmethod
def word(cls, p1: P[str]) -> P[str]:
return P.first(p1.many().fmap(lambda cs: ''.join(cs)), p1.no())
@staticmethod
def unsigned() -> P[int]:
return P.either(P.first(P.is_decimal(0), P.no_match(P.any_decimal())),
@classmethod
def unsigned(cls) -> P[int]:
return P.either(P.first(P.is_decimal(0), P.any_decimal().no()),
P.map2(P.is_not_decimal(0), P.word(P.any_decimal()),
lambda f, s: f + s)
).fmap(int)
@staticmethod
def signed() -> P[int]:
@classmethod
def signed(cls) -> P[int]:
return P.map2(P.one_of('+-').optional(), P.unsigned(),
lambda sign, num: num if sign != '-' else -num)
@ -399,8 +430,8 @@ class P(Generic[T]):
return P.first(self, WHITE_SPACE)
def trim(self) -> P[T]:
return self.surround(WHITE_SPACE)
return self.between(WHITE_SPACE, WHITE_SPACE)
WHITE_SPACE: P[tuple[()]] = P.space().many().unit()
SEP_SPACE: P[tuple[()]] = P.space().some().unit()
WHITE_SPACE: P[tuple[()]] = P.space().many().as_unit()
SEP_SPACE: P[tuple[()]] = P.space().some().as_unit()

View file

@ -75,9 +75,17 @@ def test_between():
def test_sep_by():
parser = P.signed().sep_by(P.is_char(','))
input = '1,1,2,3,5,8,13'
expected = [1, 1, 2, 3, 5, 8, 13]
result = parser.parse(input).get()
input = '2,3,5'
expected = [[2, 3, 5], [2, 3], [2]]
result = list(parser.parse_multi(input))
assert result == expected
def test_sep_by_lazy():
parser = P.signed().sep_by_lazy(P.is_char(','))
input = '2,3,5'
expected = [[2], [2, 3], [2, 3, 5]]
result = list(parser.parse_multi(input))
assert result == expected
@ -130,7 +138,7 @@ def test_seq_seq():
def test_not():
input = 'a'
parser = P.second(P.no_match(P.is_char('!')), P.is_char('a'))
parser = P.second(P.is_char('!').no(), P.is_char('a'))
expected = 'a'
result = parser.parse(input).get()
assert result == expected