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 import sys
from importlib import import_module from importlib import import_module
from advent.common import utils from advent.common import input
from advent.days.template import Day, ResultType, is_day 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: 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: match part:
case 1: output(day.day_num, 1, day.part1(data)) case 1: output(day.day_num, 1, day.part1(data))
case 2: output(day.day_num, 2, day.part2(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 from .solution import day_num, part1, part2
def test_part1(): def test_part1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 24_000 expected = 24_000
result = part1(data) result = part1(data)
assert result == expected assert result == expected
def test_part2(): def test_part2():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 45_000 expected = 45_000
result = part2(data) result = part2(data)
assert result == expected assert result == expected

View file

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

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator from typing import Iterator, Self
day_num = 4 day_num = 4
@ -19,10 +19,10 @@ class Range:
start: int start: int
end: int end: int
@staticmethod @classmethod
def parse(line: str) -> Range: def parse(cls, line: str) -> Self:
match line.split('-'): 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}") case _: raise Exception(f"Not a valid range: {line}")
def includes(self, other: Range) -> bool: def includes(self, other: Range) -> bool:
@ -39,10 +39,10 @@ class Pair:
first: Range first: Range
second: Range second: Range
@staticmethod @classmethod
def parse(line: str) -> Pair: def parse(cls, line: str) -> Self:
match line.split(','): 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}") case _: raise Exception(f"Not a valid Pair: {line}")
def includes(self) -> bool: 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 from .solution import Pair, Range, day_num, part1, part2
def test_part1(): def test_part1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 2 expected = 2
result = part1(data) result = part1(data)
assert result == expected assert result == expected
def test_part2(): def test_part2():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 4 expected = 4
result = part2(data) result = part2(data)
assert result == expected assert result == expected

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import ClassVar, Iterator from typing import ClassVar, Iterator, Self
from advent.parser.parser import P from advent.parser.parser import P
@ -31,13 +31,13 @@ class Move:
to_parser: ClassVar[P[int]] = P.second(P.string(" to "), P.unsigned()) 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) move_parser: ClassVar[P[tuple[int, int, int]]] = P.seq(amount_parser, from_parser, to_parser)
@staticmethod @classmethod
def parse(line: str) -> Move | None: def parse(cls, line: str) -> Self | None:
parsed = Move.move_parser.parse(line) parsed = cls.move_parser.parse(line)
if parsed.is_fail(): if parsed.is_fail():
return None return None
amount, frm, to = parsed.get() 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]: 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)) P.one_char().in_brackets(), P.string(" ").replace(None))
crate_row_parser: ClassVar[P[list[str | None]]] = crate_parser.sep_by(P.is_char(' ')) crate_row_parser: ClassVar[P[list[str | None]]] = crate_parser.sep_by(P.is_char(' '))
@staticmethod @classmethod
def parse_crate_row(line: str) -> list[None | str] | None: def parse_crate_row(cls, line: str) -> list[None | str] | None:
return Crane.crate_row_parser.parse(line).get_or(None) return Crane.crate_row_parser.parse(line).get_or(None)
@staticmethod @classmethod
def parse_stacks(lines: Iterator[str]) -> list[str]: def parse_stacks(cls, lines: Iterator[str]) -> list[str]:
stacks: list[str] = [] stacks: list[str] = []
for line in lines: for line in lines:
crate_row = Crane.parse_crate_row(line) crate_row = Crane.parse_crate_row(line)
@ -83,14 +83,14 @@ class Crane:
raise Exception("Can never happen") raise Exception("Can never happen")
@staticmethod @classmethod
def parse(lines: Iterator[str], is_9001: bool) -> Crane: def parse(cls, lines: Iterator[str], is_9001: bool) -> Self:
drawing = Crane.parse_stacks(lines) drawing = cls.parse_stacks(lines)
moves = [p for p in (Move.parse(line) for line in lines) if p is not None] 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 @classmethod
def top(crates: list[str]) -> str: def top(cls, crates: list[str]) -> str:
""" Lists the last item in the given stacks. Fails if any stack is empty """ """ Lists the last item in the given stacks. Fails if any stack is empty """
return ''.join(stack[-1] for stack in crates) 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 from .solution import Move, day_num, part1, part2, Crane
def test_part1(): def test_part1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = "CMZ" expected = "CMZ"
result = part1(data) result = part1(data)
assert result == expected assert result == expected
def test_part2(): def test_part2():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = "MCD" expected = "MCD"
result = part2(data) result = part2(data)
assert result == expected assert result == expected
@ -32,7 +32,7 @@ def test_parse_line2():
def test_drawing(): def test_drawing():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = ["ZN", "MCD", "P"] expected = ["ZN", "MCD", "P"]
result = Crane.parse_stacks(data) result = Crane.parse_stacks(data)
assert result == expected assert result == expected
@ -46,7 +46,7 @@ def test_parse_move():
def test_parse_all(): def test_parse_all():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = Crane( expected = Crane(
["ZN", "MCD", "P"], ["ZN", "MCD", "P"],
[Move(1, 1, 0), Move(3, 0, 2), Move(2, 1, 0), Move(1, 0, 1)], True) [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(): 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) crane = Crane.parse(data, False)
expected = ["C", "M", "PDNZ"] expected = ["C", "M", "PDNZ"]
result = crane.perform_all_moves() result = crane.perform_all_moves()
@ -63,7 +63,7 @@ def test_all_moves():
def test_all_moves9001(): 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) crane = Crane.parse(data, True)
expected = ["M", "C", "PZND"] expected = ["M", "C", "PZND"]
result = crane.perform_all_moves() 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 from .solution import day_num, marker, part1, part2
def test_part1(): def test_part1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 7 expected = 7
result = part1(data) result = part1(data)
assert result == expected assert result == expected
def test_part2(): def test_part2():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 19 expected = 19
result = part2(data) result = part2(data)
assert result == expected assert result == expected

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Iterator from typing import Iterator, Self
day_num = 7 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) return directory.get_min_delete_size(70_000_000, 30_000_000)
@dataclass(slots=True) @dataclass(slots=True, eq=False)
class Directory: class Directory:
name: str name: str
parent: Directory | None parent: Directory | None
subdirs: list[Directory] = field(default_factory=list) subdirs: list[Directory] = field(default_factory=list, init=False)
files: list[tuple[str, int]] = field(default_factory=list) files: list[tuple[str, int]] = field(default_factory=list, init=False)
size: int | None = None 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: def cd_into(self, name: str) -> Directory:
""" """
Returns the named sub directory or .. for parent Returns the named sub directory or .. for parent
May fail if unkown subdirectory - or already in root May fail if unkown subdirectory - or already in root
""" """
if name == "..": match name:
if self.parent is None: case '/':
raise Exception('Already at root Directory') current = self
return self.parent while current.parent is not None:
current = current.parent
return current
for sub in self.subdirs: case '..':
if sub.name == name: if self.parent is None:
return sub raise Exception('Already at root Directory')
raise Exception(f"Could not find subdir {name}") 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): def add_directory(self, name: str):
""" Adds the named directory.""" """ 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): def add_file(self, name: str, size: int):
""" Adds the given file and size """ """ 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 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 given as a parameter and the given disk size
#""" """
unused = disk_size - self.get_size() unused = disk_size - self.get_size()
minimum: int | None = None minimum: int | None = None
for dir in self.get_all_directories(): for dir in self.get_all_directories():
@ -82,14 +94,11 @@ class Directory:
return minimum return minimum
@staticmethod @classmethod
def parse(lines: Iterator[str]) -> Directory: def parse(cls, lines: Iterator[str]) -> Self:
line = next(lines) root = cls.create_root()
if line != '$ cd /':
raise Exception(f"Illegal first line: {line}")
root = Directory('/', None)
current = root current = root
for line in lines: for line in lines:
match line.split(): match line.split():
case ['$', 'cd', name]: 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 from .solution import day_num, part1, part2, Directory
def test_part1(): def test_part1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 95437 expected = 95437
result = part1(data) result = part1(data)
assert result == expected assert result == expected
def test_part2(): def test_part2():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 24933642 expected = 24933642
result = part2(data) result = part2(data)
assert result == expected assert result == expected
def test_size(): def test_size():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 48381165 expected = 48381165
directory = Directory.parse(data) directory = Directory.parse(data)
result = directory.get_size() result = directory.get_size()
@ -26,7 +26,7 @@ def test_size():
def test_maxed_size(): def test_maxed_size():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 95437 expected = 95437
directory = Directory.parse(data) directory = Directory.parse(data)
result = directory.get_maxed_size(100_000) result = directory.get_maxed_size(100_000)
@ -34,7 +34,7 @@ def test_maxed_size():
def test_find_to_delete(): def test_find_to_delete():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 24933642 expected = 24933642
directory = Directory.parse(data) directory = Directory.parse(data)
result = directory.get_min_delete_size(70_000_000, 30_000_000) result = directory.get_min_delete_size(70_000_000, 30_000_000)

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator from typing import Iterator, Self
day_num = 8 day_num = 8
@ -20,10 +20,10 @@ class Forest:
width: int width: int
height: int height: int
@staticmethod @classmethod
def parse(lines: Iterator[str]) -> Forest: def parse(cls, lines: Iterator[str]) -> Self:
trees = [[int(tree) for tree in line] for line in lines] 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: def count_visible_trees(self) -> int:
visible: set[tuple[int, int]] = set() 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 from .solution import Forest, day_num, part1, part2
def test_part1(): def test_part1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 21 expected = 21
result = part1(data) result = part1(data)
assert result == expected assert result == expected
def test_part2(): def test_part2():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 8 expected = 8
result = part2(data) result = part2(data)
assert result == expected assert result == expected
def test_visible(): def test_visible():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 21 expected = 21
result = Forest.parse(data).count_visible_trees() result = Forest.parse(data).count_visible_trees()
assert result == expected assert result == expected
def test_distance(): def test_distance():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 4 expected = 4
result = Forest.parse(data).single_scenic_score(2, 1) result = Forest.parse(data).single_scenic_score(2, 1)
assert result == expected assert result == expected
def test_distance2(): def test_distance2():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 8 expected = 8
result = Forest.parse(data).single_scenic_score(2, 3) result = Forest.parse(data).single_scenic_score(2, 3)
assert result == expected assert result == expected
def test_max_distance(): def test_max_distance():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 8 expected = 8
result = Forest.parse(data).max_scenic_score() result = Forest.parse(data).max_scenic_score()
assert result == expected assert result == expected

View file

@ -1,19 +1,19 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Iterator from typing import Iterator, Self
day_num = 9 day_num = 9
def part1(lines: Iterator[str]) -> int: def part1(lines: Iterator[str]) -> int:
lst = [Command.parse(line) for line in lines] commands = (Command.parse(line) for line in lines)
return Command.walk(lst, 2) return simulate(commands, 2)
def part2(lines: Iterator[str]) -> int: def part2(lines: Iterator[str]) -> int:
lst = [Command.parse(line) for line in lines] commands = (Command.parse(line) for line in lines)
return Command.walk(lst, 10) return simulate(commands, 10)
@dataclass(frozen=True, slots=True) @dataclass(frozen=True, slots=True)
@ -21,18 +21,18 @@ class Point:
x: int x: int
y: int y: int
@staticmethod @classmethod
def parse_direction(char: str) -> Point: def parse_direction(cls, char: str) -> Self:
""" Parses the given direction to a Point. May raise if invalid """ """ Parses the given direction to a Point. May raise if invalid """
match char: match char:
case 'R': case 'R':
return Point(1, 0) return cls(1, 0)
case 'U': case 'U':
return Point(0, 1) return cls(0, 1)
case 'L': case 'L':
return Point(-1, 0) return cls(-1, 0)
case 'D': case 'D':
return Point(0, -1) return cls(0, -1)
case _: case _:
raise Exception(f"Unkown Direction: {char}") raise Exception(f"Unkown Direction: {char}")
@ -65,8 +65,8 @@ class Command:
dir: Point dir: Point
steps: int steps: int
@staticmethod @classmethod
def parse(line: str) -> Command: def parse(cls, line: str) -> Self:
""" Parse a command line. My raise exception if the was an illegal line""" """ Parse a command line. My raise exception if the was an illegal line"""
match line.split(): match line.split():
case [dir, steps]: case [dir, steps]:
@ -74,20 +74,20 @@ class Command:
case _: case _:
raise Exception(f"Illegal line: {line}") 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(): def test_part1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 13 expected = 13
result = part1(data) result = part1(data)
assert result == expected assert result == expected
def test_part2(): def test_part2():
data = utils.read_data(day_num, 'test02.txt') data = input.read_lines(day_num, 'test02.txt')
expected = 36 expected = 36
result = part2(data) result = part2(data)
assert result == expected assert result == expected
def test_short(): def test_short():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 13 expected = 13
lst = [Command.parse(line) for line in data] lst = (Command.parse(line) for line in data)
result = Command.walk(lst, 2) result = simulate(lst, 2)
assert result == expected assert result == expected
def test_long1(): def test_long1():
data = utils.read_data(day_num, 'test01.txt') data = input.read_lines(day_num, 'test01.txt')
expected = 1 expected = 1
lst = [Command.parse(line) for line in data] lst = (Command.parse(line) for line in data)
result = Command.walk(lst, 10) result = simulate(lst, 10)
assert result == expected assert result == expected
def test_long2(): def test_long2():
data = utils.read_data(day_num, 'test02.txt') data = input.read_lines(day_num, 'test02.txt')
expected = 36 expected = 36
lst = [Command.parse(line) for line in data] lst = (Command.parse(line) for line in data)
result = Command.walk(lst, 10) result = simulate(lst, 10)
assert result == expected assert result == expected

View file

@ -53,7 +53,7 @@ def draw(lines: Iterator[str], width: int, height: int) -> list[str]:
picture = "" picture = ""
for cycle, sprite in enumerate(cycles(lines)): for cycle, sprite in enumerate(cycles(lines)):
crt_pos = cycle % width crt_pos = cycle % width
if sprite - 1 <= crt_pos and crt_pos <= sprite + 1: if sprite - 1 <= crt_pos <= sprite + 1:
picture += '#' picture += '#'
else: else:
picture += ' ' 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 from .solution import cycles, day_num, draw, grab_values, part1, part2
def test_part1(): def test_part1():
lines = utils.read_data(day_num, 'test01.txt') lines = input.read_lines(day_num, 'test01.txt')
expected = 13140 expected = 13140
result = part1(lines) result = part1(lines)
assert result == expected assert result == expected
def test_part2(): def test_part2():
lines = utils.read_data(day_num, 'test01.txt') lines = input.read_lines(day_num, 'test01.txt')
expected = list(utils.read_data(day_num, 'expected.txt')) expected = list(input.read_lines(day_num, 'expected.txt'))
result = part2(lines) result = part2(lines)
assert result == expected assert result == expected
def test_small(): 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] expected = [1, 1, 1, 4, 4, -1]
result = list(cycles(lines)) result = list(cycles(lines))
assert result == expected assert result == expected
def test_grab_values(): 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] expected = [420, 1140, 1800, 2940, 2880, 3960]
result = list(grab_values(lines)) result = list(grab_values(lines))
assert result == expected assert result == expected
def test_draw(): def test_draw():
lines = utils.read_data(day_num, 'test01.txt') lines = input.read_lines(day_num, 'test01.txt')
expected = list(utils.read_data(day_num, 'expected.txt')) expected = list(input.read_lines(day_num, 'expected.txt'))
result = draw(lines, 40, 6) result = draw(lines, 40, 6)
assert result == expected 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 from .solution import day_num, part1, part2
def test_part1(): def test_part1():
lines = utils.read_data(day_num, 'test01.txt') lines = input.read_lines(day_num, 'test01.txt')
expected = None expected = None
result = part1(lines) result = part1(lines)
assert result == expected assert result == expected
def test_part2(): def test_part2():
lines = utils.read_data(day_num, 'test01.txt') lines = input.read_lines(day_num, 'test01.txt')
expected = None expected = None
result = part2(lines) result = part2(lines)
assert result == expected assert result == expected

View file

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

View file

@ -75,9 +75,17 @@ def test_between():
def test_sep_by(): def test_sep_by():
parser = P.signed().sep_by(P.is_char(',')) parser = P.signed().sep_by(P.is_char(','))
input = '1,1,2,3,5,8,13' input = '2,3,5'
expected = [1, 1, 2, 3, 5, 8, 13] expected = [[2, 3, 5], [2, 3], [2]]
result = parser.parse(input).get() 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 assert result == expected
@ -130,7 +138,7 @@ def test_seq_seq():
def test_not(): def test_not():
input = 'a' 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' expected = 'a'
result = parser.parse(input).get() result = parser.parse(input).get()
assert result == expected assert result == expected