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

153
.gitignore vendored Normal file
View file

@ -0,0 +1,153 @@
# Created by https://www.toptal.com/developers/gitignore/api/python,vscode
# Edit at https://www.toptal.com/developers/gitignore?templates=python,vscode
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
doc/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
pythonenv*
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# profiling data
.prof
### vscode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# End of https://www.toptal.com/developers/gitignore/api/python,vscode

32
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,32 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Advent",
"type": "python",
"request": "launch",
"console": "integratedTerminal",
"program": "advent"
},
{
"name": "Python: Advent with Args",
"type": "python",
"request": "launch",
"console": "integratedTerminal",
"program": "advent",
"args": [
"${input:dayToRun}"
]
}
],
"inputs": [
{
"id": "dayToRun",
"type": "promptString",
"description": "Which day should be run?"
}
]
}

22
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
"python.terminal.activateEnvInCurrentTerminal": true,
"python.analysis.diagnosticMode": "workspace",
"python.linting.flake8Enabled": true,
"python.analysis.typeCheckingMode": "strict",
"files.exclude": {
".venv/": true,
"**/__pycache__/": true,
"**/.pytest_cache/": true,
"**/*.egg-info/": true
},
"python.testing.pytestArgs": [],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"python.formatting.autopep8Args": [
"--max-line-length",
"100",
"--aggressive",
"--ignore",
"W503"
]
}

5
README.md Normal file
View file

@ -0,0 +1,5 @@
# Advent of Code 2022
These are my solutions for [Advent of Code 2022](https://adventofcode.com/2022). Thanks to [Eric Wastl](http://was.tl) for the great puzzles and the great time I had solving them.
All code is published under the [Unlicense](https://unlicense.org/)

24
UNLICENSE Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

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()

9
setup.cfg Normal file
View file

@ -0,0 +1,9 @@
[flake8]
# ignore = E203, E266, E501, W503
ignore=W503
max-line-length = 100
max-complexity = 18
select = B,C,E,F,W,T4
[tool:pytest]
norecursedirs = .* day__

3
setup.py Normal file
View file

@ -0,0 +1,3 @@
from setuptools import find_packages, setup
setup(name='advent', packages=find_packages())