This commit is contained in:
Ruediger Ludwig 2022-11-30 19:39:52 +01:00
commit a355de5d8b
24 changed files with 1133 additions and 0 deletions

0
advent/__init__.py Normal file
View file

77
advent/__main__.py Normal file
View file

@ -0,0 +1,77 @@
import sys
from importlib import import_module
from advent.common import utils
from advent.days.template import Day, ResultType, is_day
def output(day: int, part: int, result: ResultType | None) -> None:
match result:
case int(value):
print('Day {0:02} Part {1}: {2}'.format(day, part, value))
case str(value):
print('Day {0:02} Part {1}: {2}'.format(day, part, value))
case list(value):
print('Day {0:02} Part {1}: {2}'.format(day, part, value[0]))
for line in value[1:]:
print(f' {line}')
case None:
print('Day {0:02} Part {1}: (No Result)'.format(day, part))
case _:
print('Day {0:02} Part {1}: (Unknown result type)'.format(day, part))
def get_day(day_num: int) -> Day:
day_module = import_module('advent.days.day{0:02}.solution'.format(day_num))
if not is_day(day_module):
raise Exception(f'Not a valid day: {day_num}')
return day_module
def run(day: Day, part: int) -> None:
data = utils.read_data(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))
case _: raise Exception(f'Unknown part {part}')
def run_from_string(day_str: str) -> None:
match day_str.split('/'):
case [d]:
day = get_day(int(d))
run(day, 1)
run(day, 2)
case [d, p]:
day = get_day(int(d))
part = int(p)
run(day, part)
case _:
raise Exception(f'{day_str} is not a valid day description')
def main() -> None:
match sys.argv:
case [_]:
try:
for day_num in range(1, 25):
day = get_day(day_num)
run(day, 1)
run(day, 2)
except ModuleNotFoundError:
pass
case [_, argument]:
run_from_string(argument)
case _:
raise Exception(f'Usage: python {sys.argv[0]} [day[/part]]')
if __name__ == '__main__':
main()

View file

View file

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

View file

@ -0,0 +1,33 @@
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

31
advent/common/provider.py Normal file
View file

@ -0,0 +1,31 @@
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

46
advent/common/utils.py Normal file
View file

@ -0,0 +1,46 @@
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

0
advent/days/__init__.py Normal file
View file

View file

View file

View file

@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Iterator
day_num = 0
def part1(lines: Iterator[str]) -> None:
return None
def part2(lines: Iterator[str]) -> None:
return None

View file

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

23
advent/days/template.py Normal file
View file

@ -0,0 +1,23 @@
import typing
ResultType = int | str | list[str]
class Day(typing.Protocol):
day_num: int
@staticmethod
def part1(lines: typing.Iterator[str]) -> ResultType | None:
...
@staticmethod
def part2(lines: typing.Iterator[str]) -> ResultType | None:
...
def is_day(object: typing.Any) -> typing.TypeGuard[Day]:
try:
return (isinstance(object.day_num, int)
and callable(object.part1) and callable(object.part2))
except AttributeError:
return False

View file

359
advent/parser/parser.py Normal file
View file

@ -0,0 +1,359 @@
from __future__ import annotations
from dataclasses import dataclass
from functools import reduce
from itertools import chain
from typing import Any, Callable, Generic, Self, TypeVar, overload
import unicodedata
from .result import Result
T = TypeVar('T')
T1 = TypeVar('T1')
T2 = TypeVar('T2')
T3 = TypeVar('T3')
T4 = TypeVar('T4')
T5 = TypeVar('T5')
TR = TypeVar('TR')
@dataclass(slots=True, frozen=True)
class ParserInput:
input: str
start: int
def step(self, count: int = 1) -> tuple[Self, str]:
assert count > 0
if self.start + count > len(self.input):
raise Exception("Not enough chars left in string")
return ParserInput(self.input, self.start
+ count), self.input[self.start:self.start + count]
def is_eof(self) -> bool:
return self.start >= len(self.input)
def __repr__(self) -> str:
if self.start == 0:
return f'->[{self.input}]'
if self.start >= len(self.input):
return f'{self.input}'
if self.start < 3:
return f'{self.input[0:self.start-1]}->[{self.input[self.start:]}]'
return f'{self.input[self.start-3:self.start-1]}->[{self.input[self.start:]}]'
ParserResult = Result[tuple[ParserInput, T]]
ParserFunc = Callable[[ParserInput], ParserResult[T]]
class P(Generic[T]):
def __init__(self, func: ParserFunc[T]):
self.func = func
def parse(self, s: str, i: int = 0) -> Result[T]:
result = self.func(ParserInput(s, i))
return result.fmap(lambda pr: pr[1])
@staticmethod
def pure(value: T) -> P[T]:
return P(lambda parserPos: Result.of((parserPos, value)))
@staticmethod
def fail(text: str) -> P[Any]:
return P(lambda pp: Result.fail(f'@: {pp}\ntext: {text}'))
@staticmethod
def _fix(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
def _forward(self, func: ParserFunc[T]) -> Self:
self.func = func
return self
def bind(self, bind_func: Callable[[T], P[TR]]) -> P[TR]:
def inner(x: tuple[ParserInput, T]):
parserPos, v = x
return bind_func(v).func(parserPos)
return P(lambda parserPos: self.func(parserPos).bind(inner))
def fmap(self, map_func: Callable[[T], TR]) -> P[TR]:
def inner(x: tuple[ParserInput, T]):
parserPos, v = x
return (parserPos, map_func(v))
return P(lambda parserPos: self.func(parserPos).fmap(inner))
def safe_fmap(self, map_func: Callable[[T], TR]) -> P[TR]:
def inner(value: T) -> P[TR]:
try:
return P.pure(map_func(value))
except Exception as e:
return P.fail(f'Failed with {e}')
return self.bind(inner)
def replace(self, value: TR) -> P[TR]:
return self.fmap(lambda _: value)
def 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))))
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(self) -> P[list[T]]:
return P._fix(lambda p: self.bind(
lambda x: P.either(p, P.pure([])).fmap(lambda ys: [x] + ys)))
def many(self) -> P[list[T]]:
return P.either(self.some(), P.pure([]))
def satisfies(self, pred: Callable[[T], bool], failtext: str) -> P[T]:
return self.bind(lambda v: P.pure(v) if pred(
v) else P.fail(f'Does not satisfy: {failtext}'))
def optional(self) -> P[T | None]:
return P.either(self, P.pure(None))
def sep_by(self, sep: P[Any]) -> P[list[T]]:
return P.map2(self, P.snd(sep, self).many(), lambda f, r: [f] + r)
@staticmethod
def snd(p1: P[Any], p2: P[T2]) -> P[T2]:
return p1.bind(lambda _: p2)
@staticmethod
def fst(p1: P[T1], p2: P[Any]) -> P[T1]:
return P.map2(p1, p2, lambda v1, _: v1)
@staticmethod
def no_match(p: P[Any], failtext: str) -> P[tuple[()]]:
def inner(pp: ParserInput) -> ParserResult[tuple[()]]:
result = p.func(pp)
if result.is_fail():
return Result.of((pp, ()))
else:
return Result.fail(failtext)
return P(inner)
@ staticmethod
def map2(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]:
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],
func: Callable[[T1, T2, T3, T4], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
lambda v2: p3.bind(
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],
func: Callable[[T1, T2, T3, T4, T5], TR]) -> P[TR]:
return p1.bind(
lambda v1: p2.bind(
lambda v2: p3.bind(
lambda v3: p4.bind(
lambda v4: p5.fmap(
lambda v5: func(v1, v2, v3, v4, v5))))))
@ staticmethod
@ overload
def seq(p1: P[T1], p2: P[T2], /) -> P[tuple[T1, T2]]:
...
@ staticmethod
@ overload
def seq(p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[tuple[T1, T2, T3]]:
...
@ staticmethod
@ overload
def seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[tuple[T1, T2, T3, T4]]:
...
@ staticmethod
@ overload
def seq(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, ...]]:
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
@ overload
def sep_seq(p1: P[T1], p2: P[T2], /, *, sep: P[Any]) -> P[tuple[T1, T2]]:
...
@ staticmethod
@ overload
def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3]]:
...
@ staticmethod
@ overload
def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /,
*, sep: P[Any]) -> P[tuple[T1, T2, T3, T4]]:
...
@ staticmethod
@ overload
def sep_seq(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, ...]]:
first, *rest = list(ps)
return P.map2(first,
reduce(lambda p, x: P.snd(sep, x.bind(
lambda a: p.fmap(lambda b: chain([a], b)))),
rest[::-1], P.pure(iter([]))),
lambda f, r: (f,) + tuple(r))
@ staticmethod
def either(p1: P[T1], p2: P[T2], /) -> P[T1 | T2]:
def inner(parserPos: ParserInput):
result = p1.func(parserPos)
return result if result.is_ok() else p2.func(parserPos)
return P(inner)
@ staticmethod
@ overload
def choice(p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[T1 | T2 | T3]:
...
@ staticmethod
@ overload
def choice(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[T1 | T2 | T3 | T4]:
...
@ staticmethod
@ overload
def choice(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]:
return reduce(P.either, ps, P.fail('No Choice matched'))
@ staticmethod
def choice_same(*ps: P[T]) -> P[T]:
return P.choice(*ps)
@ staticmethod
def any_char() -> P[str]:
return P(lambda pp: Result.of(pp.step()) if not pp.is_eof()
else Result.fail('At EOF'))
@ staticmethod
def eof() -> P[tuple[()]]:
return P.no_match(P.any_char(), 'Not at eof')
@ staticmethod
def is_char(cmp: str) -> P[str]:
return P.any_char().satisfies(lambda c: c == cmp, f'match {cmp}')
@staticmethod
def is_not_char(s: str) -> P[tuple[()]]:
return P.no_match(P.is_char(s), f'Did match {s}')
@ staticmethod
def char_by_func(cmp: Callable[[str], bool], failtext: str) -> P[str]:
return P.any_char().satisfies(cmp, failtext)
@ staticmethod
def string(s: str) -> P[str]:
return P.seq(*map(P.is_char, s)).replace(s)
@ staticmethod
def one_of(s: str) -> P[str]:
return P.char_by_func(lambda c: c in s, f'one of {s}')
@ staticmethod
def any_decimal() -> P[str]:
return P.char_by_func(lambda c: c.isdecimal(), 'decimal')
@ staticmethod
def is_decimal(num: int) -> P[str]:
return P.any_decimal().bind(
lambda c: P.pure(c) if unicodedata.decimal(c) == num else P.fail(f'Not {num}'))
@ staticmethod
def is_not_decimal(num: int) -> P[str]:
return P.any_decimal().bind(
lambda c: P.pure(c) if unicodedata.decimal(c) != num else P.fail(f'Is {num}'))
@ staticmethod
def lower() -> P[str]:
return P.char_by_func(lambda c: c.islower(), 'lower')
@ staticmethod
def upper() -> P[str]:
return P.char_by_func(lambda c: c.isupper(), 'upper')
@ staticmethod
def joined(p1: P[str]) -> P[str]:
return p1.many().fmap(lambda cs: ''.join(cs))
@ staticmethod
def space() -> P[str]:
return P.char_by_func(lambda c: c.isspace(), 'space')
@ staticmethod
def unsigned() -> P[int]:
return P.either(P.fst(P.is_decimal(0), P.no_match(P.any_decimal(), 'starting Zero')),
P.map2(P.is_not_decimal(0), P.any_decimal().many(),
lambda f, s: f + ''.join(s))
).fmap(int)
@ staticmethod
def signed() -> P[int]:
return P.map2(P.one_of('+-').optional(), P.unsigned(),
lambda sign, num: num if sign != '-' else -num)
def in_parens(self) -> P[T]:
return self.between(P.is_char('('), P.is_char(')'))
def in_angles(self) -> P[T]:
return self.between(P.is_char('<'), P.is_char('>'))
def in_brackets(self) -> P[T]:
return self.between(P.is_char('['), P.is_char(']'))
def in_curleys(self) -> P[T]:
return self.between(P.is_char('{'), P.is_char('}'))
def trim_left(self) -> P[T]:
return P.snd(WHITE_SPACE, self)
def trim_right(self) -> P[T]:
return P.fst(self, WHITE_SPACE)
def trim(self) -> P[T]:
return self.surround(WHITE_SPACE)
WHITE_SPACE: P[tuple[()]] = P.space().many().unit()
SEP_SPACE: P[tuple[()]] = P.space().some().unit()

109
advent/parser/result.py Normal file
View file

@ -0,0 +1,109 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Callable, Generic, Never, TypeVar
S = TypeVar("S", covariant=True)
S1 = TypeVar("S1")
S2 = TypeVar("S2")
S3 = TypeVar("S3")
class Result(ABC, Generic[S]):
@staticmethod
def of(value: S1) -> Result[S1]:
return Success(value)
@staticmethod
def fail(failure: str) -> Result[Any]:
return Failure(failure)
@abstractmethod
def is_ok(self) -> bool:
pass
@abstractmethod
def is_fail(self) -> bool:
pass
@abstractmethod
def fmap(self, func: Callable[[S], S2]) -> Result[S2]:
pass
@abstractmethod
def bind(self, func: Callable[[S], Result[S2]]) -> Result[S2]:
pass
@abstractmethod
def get(self) -> S:
pass
@abstractmethod
def get_or(self, default: S1) -> S | S1:
pass
@abstractmethod
def get_or_else(self, default: Callable[[], S]) -> S:
pass
@abstractmethod
def get_error(self) -> str:
pass
class Success(Result[S]):
def __init__(self, value: S):
self.value = value
def is_ok(self) -> bool:
return True
def is_fail(self) -> bool:
return False
def fmap(self, func: Callable[[S], S2]) -> Result[S2]:
return Result.of(func(self.value)) # type: ignore
def bind(self, func: Callable[[S], Result[S2]]) -> Result[S2]:
return func(self.value)
def get(self) -> S:
return self.value
def get_or(self, default: S1) -> S | S1:
return self.value
def get_or_else(self, default: Callable[[], S]) -> S:
return self.value
def get_error(self) -> Never:
raise Exception("No Error in Success Value")
class Failure(Result[Any]):
def __init__(self, value: str):
self.value = value
def is_ok(self) -> bool:
return False
def is_fail(self) -> bool:
return True
def fmap(self, func: Callable[[Any], S2]) -> Result[S2]:
return self
def bind(self, func: Callable[[Any], Result[S2]]) -> Result[S2]:
return self
def get(self) -> Never:
raise Exception("No value in Fail")
def get_or(self, default: S1) -> S1:
return default
def get_or_else(self, default: Callable[[], S]) -> S:
return default()
def get_error(self) -> str:
return self.value

View file

@ -0,0 +1,140 @@
from .parser import P
import pytest
def test_one_letter():
parser = P.is_char('!')
input = '!'
expected = '!'
result = parser.parse(input).get()
assert result == expected
def test_one_letter_longer():
parser = P.is_char('!')
input = '!!'
expected = '!'
result = parser.parse(input).get()
assert result == expected
def test_one_string():
parser = P.string('123')
input = '12345'
expected = '123'
result = parser.parse(input).get()
assert result == expected
def test_eof():
parser = P.eof()
input = ''
result = parser.parse(input).get()
assert result == ()
input = '!'
with pytest.raises(Exception):
parser.parse(input).get()
def test_integer():
parser = P.signed()
input = '123456'
expected = 123456
result = parser.parse(input).get()
assert result == expected
def test_signed_integer():
parser = P.signed()
input = '-123456'
expected = -123456
result = parser.parse(input).get()
assert result == expected
def test_starting_zero():
parser = P.unsigned()
input = '0a'
expected = 0
result = parser.parse(input).get()
assert result == expected
input2 = '01'
result2 = parser.parse(input2)
assert result2.is_fail()
def test_between():
parser = P.signed().between(P.is_char('<'), P.is_char('>'))
input = '<-123456>'
expected = -123456
result = parser.parse(input).get()
assert result == expected
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()
assert result == expected
def test_trim():
parser = P.signed().trim()
input = '1'
expected = 1
result = parser.parse(input).get()
assert result == expected
def test_sep_by_trim():
parser = P.signed().sep_by(P.is_char(',').trim()).trim()
input = ' 1 , 1 , 2 , 3 , 5 , 8 , 13!'
expected = [1, 1, 2, 3, 5, 8, 13]
result = parser.parse(input).get()
assert result == expected
def test_choice2():
parser = P.choice(P.is_char('a'), P.unsigned(), P.string('hallo'))
input = '1'
expected = 1
result = parser.parse(input).get()
assert result == expected
input = 'hallo'
expected = 'hallo'
result = parser.parse(input).get()
assert result == expected
def test_seq():
input = '1234'
parser = P.seq(P.any_char(), P.any_char(), P.any_char(), P.any_char())
expected = ('1', '2', '3', '4')
result = parser.parse(input).get()
assert result == expected
def test_seq_seq():
input = '1,2,3,4'
digit = P.char_by_func(lambda c: c.isdigit(), "")
parser = P.sep_seq(digit, digit, digit, digit, sep=P.is_char(','))
expected = ('1', '2', '3', '4')
result = parser.parse(input).get()
assert result == expected
def test_not():
input = 'a'
parser = P.snd(P.no_match(P.is_char('!'), 'found !'), P.is_char('a'))
expected = 'a'
result = parser.parse(input).get()
assert result == expected
input2 = '!'
result2 = parser.parse(input2)
assert result2.is_fail()