Initial
This commit is contained in:
commit
a355de5d8b
24 changed files with 1133 additions and 0 deletions
153
.gitignore
vendored
Normal file
153
.gitignore
vendored
Normal 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
32
.vscode/launch.json
vendored
Normal 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
22
.vscode/settings.json
vendored
Normal 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
5
README.md
Normal 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
24
UNLICENSE
Normal 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
0
advent/__init__.py
Normal file
77
advent/__main__.py
Normal file
77
advent/__main__.py
Normal 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()
|
||||
0
advent/common/__init__.py
Normal file
0
advent/common/__init__.py
Normal file
37
advent/common/char_provider.py
Normal file
37
advent/common/char_provider.py
Normal 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
|
||||
33
advent/common/char_reader.py
Normal file
33
advent/common/char_reader.py
Normal 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
31
advent/common/provider.py
Normal 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
46
advent/common/utils.py
Normal 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
0
advent/days/__init__.py
Normal file
0
advent/days/day__/__init__.py
Normal file
0
advent/days/day__/__init__.py
Normal file
0
advent/days/day__/data/input.txt
Normal file
0
advent/days/day__/data/input.txt
Normal file
13
advent/days/day__/solution.py
Normal file
13
advent/days/day__/solution.py
Normal 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
|
||||
17
advent/days/day__/test_solution.py
Normal file
17
advent/days/day__/test_solution.py
Normal 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
23
advent/days/template.py
Normal 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
|
||||
0
advent/parser/__init__.py
Normal file
0
advent/parser/__init__.py
Normal file
359
advent/parser/parser.py
Normal file
359
advent/parser/parser.py
Normal 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
109
advent/parser/result.py
Normal 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
|
||||
140
advent/parser/test_parser.py
Normal file
140
advent/parser/test_parser.py
Normal 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
9
setup.cfg
Normal 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
3
setup.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
setup(name='advent', packages=find_packages())
|
||||
Loading…
Add table
Add a link
Reference in a new issue