lots of cleanup
This commit is contained in:
parent
7d0d3e504e
commit
0385cbd62e
26 changed files with 337 additions and 417 deletions
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
19
advent/common/input.py
Normal 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')
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 += ' '
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue