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