From 0385cbd62eeb98bc111144ea3debd64e73b741fa Mon Sep 17 00:00:00 2001 From: Ruediger Ludwig Date: Sat, 10 Dec 2022 19:30:10 +0100 Subject: [PATCH] lots of cleanup --- advent/__main__.py | 4 +- advent/common/char_provider.py | 37 ----- advent/common/char_reader.py | 33 ---- advent/common/input.py | 19 +++ advent/common/provider.py | 31 ---- advent/common/utils.py | 46 ------ advent/days/day01/test_solution.py | 6 +- advent/days/day02/solution.py | 44 +++--- advent/days/day02/test_solution.py | 6 +- advent/days/day03/test_solution.py | 6 +- advent/days/day04/solution.py | 14 +- advent/days/day04/test_solution.py | 6 +- advent/days/day05/solution.py | 30 ++-- advent/days/day05/test_solution.py | 14 +- advent/days/day06/test_solution.py | 6 +- advent/days/day07/solution.py | 53 ++++--- advent/days/day07/test_solution.py | 12 +- advent/days/day08/solution.py | 8 +- advent/days/day08/test_solution.py | 14 +- advent/days/day09/solution.py | 58 +++---- advent/days/day09/test_solution.py | 26 ++-- advent/days/day10/solution.py | 2 +- advent/days/day10/test_solution.py | 16 +- advent/days/day__/test_solution.py | 6 +- advent/parser/parser.py | 241 ++++++++++++++++------------- advent/parser/test_parser.py | 16 +- 26 files changed, 337 insertions(+), 417 deletions(-) delete mode 100644 advent/common/char_provider.py delete mode 100644 advent/common/char_reader.py create mode 100644 advent/common/input.py delete mode 100644 advent/common/provider.py delete mode 100644 advent/common/utils.py diff --git a/advent/__main__.py b/advent/__main__.py index ffd647a..855dac7 100644 --- a/advent/__main__.py +++ b/advent/__main__.py @@ -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)) diff --git a/advent/common/char_provider.py b/advent/common/char_provider.py deleted file mode 100644 index df03a2d..0000000 --- a/advent/common/char_provider.py +++ /dev/null @@ -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 diff --git a/advent/common/char_reader.py b/advent/common/char_reader.py deleted file mode 100644 index 3bfd03d..0000000 --- a/advent/common/char_reader.py +++ /dev/null @@ -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 diff --git a/advent/common/input.py b/advent/common/input.py new file mode 100644 index 0000000..3517704 --- /dev/null +++ b/advent/common/input.py @@ -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') diff --git a/advent/common/provider.py b/advent/common/provider.py deleted file mode 100644 index 6435f2b..0000000 --- a/advent/common/provider.py +++ /dev/null @@ -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 diff --git a/advent/common/utils.py b/advent/common/utils.py deleted file mode 100644 index 583961a..0000000 --- a/advent/common/utils.py +++ /dev/null @@ -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 diff --git a/advent/days/day01/test_solution.py b/advent/days/day01/test_solution.py index 563034b..da2414c 100644 --- a/advent/days/day01/test_solution.py +++ b/advent/days/day01/test_solution.py @@ -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 diff --git a/advent/days/day02/solution.py b/advent/days/day02/solution.py index 1cfabe0..bfe8281 100644 --- a/advent/days/day02/solution.py +++ b/advent/days/day02/solution.py @@ -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: diff --git a/advent/days/day02/test_solution.py b/advent/days/day02/test_solution.py index e8ff014..22bbda5 100644 --- a/advent/days/day02/test_solution.py +++ b/advent/days/day02/test_solution.py @@ -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 diff --git a/advent/days/day03/test_solution.py b/advent/days/day03/test_solution.py index 1b0e5c3..361776f 100644 --- a/advent/days/day03/test_solution.py +++ b/advent/days/day03/test_solution.py @@ -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 diff --git a/advent/days/day04/solution.py b/advent/days/day04/solution.py index e221fe5..88936cc 100644 --- a/advent/days/day04/solution.py +++ b/advent/days/day04/solution.py @@ -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: diff --git a/advent/days/day04/test_solution.py b/advent/days/day04/test_solution.py index 87156e9..ab6a67b 100644 --- a/advent/days/day04/test_solution.py +++ b/advent/days/day04/test_solution.py @@ -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 diff --git a/advent/days/day05/solution.py b/advent/days/day05/solution.py index b2103c6..f0b892c 100644 --- a/advent/days/day05/solution.py +++ b/advent/days/day05/solution.py @@ -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) diff --git a/advent/days/day05/test_solution.py b/advent/days/day05/test_solution.py index 7e4db64..bab29a2 100644 --- a/advent/days/day05/test_solution.py +++ b/advent/days/day05/test_solution.py @@ -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() diff --git a/advent/days/day06/test_solution.py b/advent/days/day06/test_solution.py index 0e6c3f7..33127a8 100644 --- a/advent/days/day06/test_solution.py +++ b/advent/days/day06/test_solution.py @@ -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 diff --git a/advent/days/day07/solution.py b/advent/days/day07/solution.py index 34be7c5..d5fda26 100644 --- a/advent/days/day07/solution.py +++ b/advent/days/day07/solution.py @@ -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]: diff --git a/advent/days/day07/test_solution.py b/advent/days/day07/test_solution.py index 4576dc4..3271cf4 100644 --- a/advent/days/day07/test_solution.py +++ b/advent/days/day07/test_solution.py @@ -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) diff --git a/advent/days/day08/solution.py b/advent/days/day08/solution.py index 4812b8e..340a455 100644 --- a/advent/days/day08/solution.py +++ b/advent/days/day08/solution.py @@ -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() diff --git a/advent/days/day08/test_solution.py b/advent/days/day08/test_solution.py index f1edcdc..982ccee 100644 --- a/advent/days/day08/test_solution.py +++ b/advent/days/day08/test_solution.py @@ -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 diff --git a/advent/days/day09/solution.py b/advent/days/day09/solution.py index 4e73761..86b71fa 100644 --- a/advent/days/day09/solution.py +++ b/advent/days/day09/solution.py @@ -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) diff --git a/advent/days/day09/test_solution.py b/advent/days/day09/test_solution.py index f97b41e..9e1c117 100644 --- a/advent/days/day09/test_solution.py +++ b/advent/days/day09/test_solution.py @@ -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 diff --git a/advent/days/day10/solution.py b/advent/days/day10/solution.py index e29a1d2..2a34a26 100644 --- a/advent/days/day10/solution.py +++ b/advent/days/day10/solution.py @@ -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 += ' ' diff --git a/advent/days/day10/test_solution.py b/advent/days/day10/test_solution.py index 35da1fd..6c6100e 100644 --- a/advent/days/day10/test_solution.py +++ b/advent/days/day10/test_solution.py @@ -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 diff --git a/advent/days/day__/test_solution.py b/advent/days/day__/test_solution.py index 107a5e8..9aea245 100644 --- a/advent/days/day__/test_solution.py +++ b/advent/days/day__/test_solution.py @@ -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 diff --git a/advent/parser/parser.py b/advent/parser/parser.py index 9659483..43b5954 100644 --- a/advent/parser/parser.py +++ b/advent/parser/parser.py @@ -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() diff --git a/advent/parser/test_parser.py b/advent/parser/test_parser.py index 3a78f4c..485fc2d 100644 --- a/advent/parser/test_parser.py +++ b/advent/parser/test_parser.py @@ -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