removed unnecessary parser

This commit is contained in:
Ruediger Ludwig 2023-01-20 07:10:24 +01:00
parent f59089d5eb
commit 38efdd1d1c
11 changed files with 105 additions and 1043 deletions

View file

@ -31,13 +31,17 @@ def get_day(day_num: int) -> Day:
def run(day: Day, part: int) -> float:
data = input.read_lines(day.day_num, 'input.txt')
t0 = time.time()
start_time = time.time()
match part:
case 1: result = day.part1(data)
case 2: result = day.part2(data)
case _: raise Exception(f'Unknown part {part}')
t1 = time.time()
delta = t1 - t0
if result is None:
return 0.0
end_time = time.time()
delta = end_time - start_time
output(day.day_num, part, result, delta)
return delta
@ -75,7 +79,7 @@ def main() -> None:
match sys.argv:
case [_]:
try:
for day_num in range(1, 25):
for day_num in range(1, 26):
day = get_day(day_num)
if day_num == day.day_num:
time += run(day, 1)

View file

@ -1,9 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import ClassVar, Iterator, Self
from advent.parser.parser import P
from typing import Iterator, Self
day_num = 5
@ -26,18 +24,13 @@ class Move:
frm: int
to: int
amount_parser: ClassVar[P[int]] = P.second(P.string("move "), P.unsigned())
from_parser: ClassVar[P[int]] = P.second(P.string(" from "), P.unsigned())
to_parser: ClassVar[P[int]] = P.second(P.string(" to "), P.unsigned())
move_parser: ClassVar[P[tuple[int, int, int]]] = P.seq(amount_parser, from_parser, to_parser)
@classmethod
def parse(cls, line: str) -> Self | None:
parsed = cls.move_parser.parse(line)
if parsed.is_fail():
return None
amount, frm, to = parsed.get()
return cls(amount, frm - 1, to - 1)
def parse(cls, line: str) -> Self:
match line.split():
case ['move', amount, 'from', frm, 'to', to]:
return cls(int(amount), int(frm) - 1, int(to) - 1)
case _:
raise Exception("Not a valid move")
def do_move(self, crates: list[str], as_9001: bool) -> list[str]:
"""
@ -58,21 +51,23 @@ class Crane:
moves: list[Move]
is_9001: bool
crate_parser: ClassVar[P[str | None]] = P.either(
P.any_char().in_brackets(), P.string(" ").replace(None))
crate_row_parser: ClassVar[P[list[str | None]]] = crate_parser.sep_by(P.char(' '))
@classmethod
def parse_crate_row(cls, line: str) -> list[None | str] | None:
return Crane.crate_row_parser.parse(line).get_or(None)
def parse_crate_row(cls, line: str) -> list[None | str]:
result: list[str | None] = []
for c in line[1::4]:
if c.isalnum():
result.append(c)
else:
result.append(None)
return result
@classmethod
def parse_stacks(cls, lines: Iterator[str]) -> list[str]:
stacks: list[str] = []
for line in lines:
crate_row = Crane.parse_crate_row(line)
if crate_row is None:
if not line:
return stacks
crate_row = Crane.parse_crate_row(line)
if len(stacks) < len(crate_row):
stacks += [""] * (len(crate_row) - len(stacks))
@ -86,7 +81,7 @@ class Crane:
@classmethod
def parse(cls, lines: Iterator[str], is_9001: bool) -> Self:
drawing = cls.parse_stacks(lines)
moves = [p for p in (Move.parse(line) for line in lines) if p is not None]
moves = [Move.parse(line) for line in lines]
return cls(drawing, moves, is_9001)
@classmethod

View file

@ -48,7 +48,7 @@ def test_parse_move():
def test_parse_all():
data = input.read_lines(day_num, 'example01.txt')
expected = Crane(
["ZN", "MCD", "P"],
["1ZN", "2MCD", "3P"],
[Move(1, 1, 0), Move(3, 0, 2), Move(2, 1, 0), Move(1, 0, 1)], True)
result = Crane.parse(data, True)
assert result == expected

View file

@ -2,9 +2,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from math import prod
import re
from typing import Callable, Iterator, Self
from advent.parser.parser import P
day_num = 11
@ -21,44 +21,16 @@ def part2(lines: Iterator[str]) -> int:
return horde.inspected_result()
def worry_increaser(op: str, value: int | str) -> WorryIncreaser:
match (op, value):
case '*', int(v): return lambda old: old * v
case '*', 'old': return lambda old: old ** 2
case '+', int(v): return lambda old: old + v
case '+', 'old': return lambda old: 2 * old
case _: raise Exception(f"Illegal line: {op} {value}")
class Parser:
""" All the parsers needed for this solution """
worry_inc: P[WorryIncreaser] = P.second(
P.tstring("Operation: new = old"),
P.map2(P.one_of('+*'), P.either(P.tstring('old'), P.tsigned()),
worry_increaser)).tline()
monkey_number: P[int] = P.unsigned().between(P.tstring('Monkey'), P.tchar(':')).tline()
items: P[list[int]] = P.second(
P.tstring('Starting items:'), P.unsigned().sep_by(sep=P.tchar(','))).tline()
modulo: P[int] = P.second(
P.tstring("Test: divisible by"), P.unsigned()).tline()
throw_parser: P[int] = P.second(
P.seq(
P.tstring("If"),
P.either(P.tstring("true"), P.tstring("false")),
P.tstring(": throw to monkey")),
P.unsigned()).tline()
test: P[tuple[int, int, int]] = P.seq(
modulo, throw_parser, throw_parser)
monkey: P[Monkey] = P.map4(monkey_number, items,
worry_inc, test,
lambda number, items, worry_inc, test:
Monkey(number, items, worry_inc, *test))
monkey_list: P[list[Monkey]] = P.second(P.eol().optional(), monkey).many()
WorryIncreaser = Callable[[int], int]
def match_raise(pattern: str, string: str) -> re.Match[str]:
result = re.match(pattern, string)
if result is None:
raise Exception("Pattern did not match")
return result
@dataclass(slots=True)
class Monkey:
number: int
@ -69,6 +41,31 @@ class Monkey:
catcher_if_not_divides: int
inspected: int = field(default=0, compare=False)
@classmethod
def parse(cls, lines: Iterator[str]) -> Monkey:
re_number = match_raise(r"Monkey (?P<number>\d+):", next(lines))
number = int(re_number.group('number'))
starting = next(lines).split(":")
items = list(int(item.strip()) for item in starting[1].split(","))
s_operation = next(lines).split('=')
match s_operation[1].split():
case ['old', '*', 'old']:
operation: WorryIncreaser = lambda old: old ** 2
case ['old', '*', num]:
number = int(num)
operation: WorryIncreaser = lambda old: old * number
case ['old', '+', num]:
number = int(num)
operation: WorryIncreaser = lambda old: old + number
case _: raise Exception("Illegal operation")
s_modulo = next(lines).split("by")
modulo = int(s_modulo[1].strip())
s_if_true = next(lines).split("monkey")
if_true = int(s_if_true[1])
s_if_false = next(lines).split("monkey")
if_false = int(s_if_false[1])
return Monkey(number, items, operation, modulo, if_true, if_false)
def inspect_items(self, worry_decrease: int | None) -> Iterator[tuple[int, int]]:
for item in self.items:
self.inspected += 1
@ -93,6 +90,15 @@ class Monkey:
class Troop(ABC):
monkeys: list[Monkey]
@classmethod
def parse_monkeys(cls, lines: Iterator[str]) -> Iterator[Monkey]:
while True:
try:
yield Monkey.parse(lines)
next(lines)
except StopIteration:
return
@abstractmethod
def single_round(self):
...
@ -108,11 +114,9 @@ class Troop(ABC):
@dataclass(slots=True)
class Troop_While_Worried(Troop):
""" The """
@classmethod
def parse(cls, lines: Iterator[str]) -> Self:
monkeys = Parser.monkey_list.parse(lines).get()
return Troop_While_Worried(monkeys)
return Troop_While_Worried(list(Troop.parse_monkeys(lines)))
def single_round(self):
for currentMonkey in self.monkeys:
@ -126,7 +130,7 @@ class Troop_While_Kinda_Relieved(Troop):
@classmethod
def parse(cls, lines: Iterator[str]) -> Self:
monkeys = Parser.monkey_list.parse(lines).get()
monkeys = list(Troop.parse_monkeys(lines))
return Troop_While_Kinda_Relieved(monkeys, prod(monkey.modulator for monkey in monkeys))
def single_round(self):

View file

@ -3,9 +3,9 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from itertools import product
from queue import PriorityQueue
import re
from typing import Iterator, Literal, NamedTuple, Self
from advent.parser.parser import P
day_num = 16
@ -20,30 +20,29 @@ def part2(lines: Iterator[str]) -> int:
return system.under_pressure(26, 2)
valve_parser = P.map3(
P.second(P.string("Valve "), P.upper().word()),
P.second(P.string(" has flow rate="), P.unsigned()),
P.second(P.either(P.string("; tunnels lead to valves "), P.string("; tunnel leads to valve ")),
P.upper().word().sep_by(P.string(", "))),
lambda name, flow_rate, following: RawValve(name, flow_rate, following)
)
pattern = re.compile(r"Valve (?P<name>[a-zA-Z]+)[^=]+=(?P<flow_rate>\d+).+valves? (?P<exits>.*)")
class RawValve(NamedTuple):
name: str
flow_rate: int
following: list[str]
exits: list[str]
@classmethod
def parse(cls, line: str) -> Self:
return valve_parser.parse(line).get()
result = pattern.match(line)
if not result:
raise Exception("Not a valid valve")
return RawValve(result.group('name'),
int(result.group('flow_rate')),
result.group('exits').split(', '))
@dataclass(slots=True)
class Valve:
name: str
flow_rate: int
following: list[Valve]
exits: list[Valve]
paths: dict[str, int] = field(default_factory=dict, init=False)
def __eq__(self, other: object) -> bool:
@ -58,7 +57,7 @@ class Valve:
return self.name < other.name
def __repr__(self) -> str:
return f"{self.name}:{self.flow_rate}->{','.join(v.name for v in self.following)}"
return f"{self.name}:{self.flow_rate}->{','.join(v.name for v in self.exits)}"
def travel_time(self, to: str) -> int:
return self.paths[to]
@ -71,7 +70,7 @@ class Valve:
to_check = to_check[1:]
paths[current.name] = steps, (current.flow_rate > 0)
for next in current.following:
for next in current.exits:
known_path, _ = paths.get(next.name, (steps + 2, False))
if known_path > steps + 1:
to_check.append((next, steps + 1))
@ -281,8 +280,8 @@ class Network:
valves = {valve.name: Valve(valve.name, valve.flow_rate, []) for valve in raw_system}
for raw in raw_system:
current = valves[raw.name]
for follow in raw.following:
current.following.append(valves[follow])
for follow in raw.exits:
current.exits.append(valves[follow])
return Network(valves)

View file

@ -6,9 +6,9 @@ from itertools import islice
from math import prod
from multiprocessing import Pool
from queue import PriorityQueue
import re
from typing import Iterable, Iterator, Self
from advent.parser.parser import P
day_num = 19
@ -35,17 +35,6 @@ class Processor:
return blueprint, blueprint.run(self.rounds)
number_parser = P.second(P.string("Blueprint "), P.unsigned())
ore_parser = P.second(P.string(": Each ore robot costs "), P.unsigned())
clay_parser = P.second(P.string(" ore. Each clay robot costs "), P.unsigned())
tuple_parser = P.seq(P.unsigned(), P.second(P.string(" ore and "), P.unsigned()))
obsidian_parser = P.second(P.string(" ore. Each obsidian robot costs "), tuple_parser)
geode_parser = P.second(P.string(" clay. Each geode robot costs "), tuple_parser)
blueprint_parser = P.map5(number_parser, ore_parser, clay_parser, obsidian_parser, geode_parser,
lambda number, ore, clay, obsidian, geode:
Blueprint.create(number, ore, clay, obsidian, geode))
class Element(IntEnum):
Geode = 0
Obsidian = 1
@ -114,6 +103,15 @@ class State:
return self.material > other.material
r_number = r"Blueprint (?P<number>\d+):"
r_ore = r".*(?P<ore_ore>\d+) ore."
r_clay = r".*(?P<clay_ore>\d+) ore."
r_obsidian = r".*(?P<obsidian_ore>\d+) ore and (?P<obsidian_clay>\d+) clay."
r_geode = r".*(?P<geode_ore>\d+) ore and (?P<geode_obsidian>\d+) obsidian."
pattern = re.compile(r_number + r_ore + r_clay + r_obsidian + r_geode)
@dataclass(slots=True)
class Blueprint:
number: int
@ -134,7 +132,16 @@ class Blueprint:
@classmethod
def parse(cls, line: str) -> Self:
return blueprint_parser.parse(line).get()
result = pattern.match(line)
if result is None:
raise Exception("Not a valid Blueprint")
return Blueprint.create(
number=int(result.group('number')),
ore=int(result.group('ore_ore')),
clay=int(result.group('clay_ore')),
obsidian=(int(result.group('obsidian_ore')), int(result.group('obsidian_clay'))),
geode=(int(result.group('geode_ore')), int(result.group('geode_obsidian'))),
)
def run(self, rounds: int) -> int:
queue: PriorityQueue[State] = PriorityQueue()

View file

@ -1,442 +0,0 @@
from __future__ import annotations
from functools import reduce
from itertools import chain
from typing import Any, Callable, Generic, Iterator, Self, TypeVar, overload
import unicodedata
from advent.parser.parser_input import AllowedParserInput, ParserInput, create_parser_input
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')
ParserResult = Iterator[tuple[ParserInput, T]]
ParserFunc = Callable[[ParserInput], ParserResult[T]]
class P(Generic[T]):
def __init__(self, func: ParserFunc[T]):
self.func = func
def parse(self, input: AllowedParserInput) -> Result[T]:
parser_input = create_parser_input(input)
all_results = self.func(parser_input)
try:
_, result = next(all_results)
return Result.of(result)
except StopIteration:
return Result.fail("No result")
def parse_multi(self, input: AllowedParserInput) -> Iterator[T]:
parser_input = create_parser_input(input)
all_results = self.func(parser_input)
return (v for _, v in all_results)
@classmethod
def pure(cls, value: T) -> P[T]:
return P(lambda pp: iter([(pp, value)]))
@classmethod
def fail(cls) -> P[Any]:
return P(lambda _: iter([]))
@classmethod
def _fix(cls, 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(parserPos: ParserInput) -> ParserResult[TR]:
return (r for rs in (bind_func(v).func(pp)
for pp, v in self.func(parserPos)) for r in rs)
return P(inner)
def fmap(self, map_func: Callable[[T], TR]) -> P[TR]:
def inner(parserPos: ParserInput) -> ParserResult[TR]:
return ((pp, map_func(v)) for pp, v in self.func(parserPos))
return P(inner)
def safe_fmap(self, map_func: Callable[[T], TR]) -> P[TR]:
def inner(parserPos: ParserInput) -> ParserResult[TR]:
for pp, v in self.func(parserPos):
try:
yield pp, map_func(v)
except Exception:
pass
return P(inner)
def replace(self, value: TR) -> P[TR]:
return self.fmap(lambda _: value)
def ignore(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))))
@classmethod
def first(cls, p1: P[T1], p2: P[Any]) -> P[T1]:
return p1.bind(lambda v1: p2.fmap(lambda _: v1))
@classmethod
def second(cls, p1: P[Any], p2: P[T2]) -> P[T2]:
return p1.bind(lambda _: p2)
def between(self, pre: P[Any], post: P[Any]) -> P[T]:
return P.map3(pre, self, post, 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 some_lazy(self) -> P[list[T]]:
return P._fix(lambda p: self.bind(
lambda x: P.either(P.pure([]), p).fmap(lambda ys: [x] + ys)))
def many(self) -> P[list[T]]:
return P.either(self.some(), P.pure([]))
def many_lazy(self) -> P[list[T]]:
return P.either(P.pure([]), self.some_lazy())
def satisfies(self, pred: Callable[[T], bool]) -> P[T]:
return self.bind(lambda v: P.pure(v) if pred(v) else P.fail())
def optional(self) -> P[T | None]:
return P.either(self, P.pure(None))
def optional_lazy(self) -> P[T | None]:
return P.either(P.pure(None), self)
@overload
def times(self, *, exact: int) -> P[list[T]]:
...
@overload
def times(self, *, min: int) -> P[list[T]]:
...
@overload
def times(self, *, max: int) -> P[list[T]]:
...
@overload
def times(self, *, min: int, max: int) -> P[list[T]]:
...
def times(self, *, max: int | None = None, min: int | None = None,
exact: int | None = None) -> P[list[T]]:
match (exact, min, max):
case (int(e), None, None):
return self.many().satisfies(lambda lst: len(lst) == e)
case (None, int(mn), None):
return self.many().satisfies(lambda lst: len(lst) >= mn)
case (None, None, int(mx)):
return self.many().satisfies(lambda lst: len(lst) <= mx)
case (None, int(mn), int(mx)):
return self.many().satisfies(lambda lst: mn <= len(lst) <= mx)
case _:
raise Exception("Illegal combination of parameters")
@overload
def times_lazy(self, *, exact: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, min: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, max: int) -> P[list[T]]:
...
@overload
def times_lazy(self, *, min: int, max: int) -> P[list[T]]:
...
def times_lazy(self, *, max: int | None = None, min: int | None = None,
exact: int | None = None) -> P[list[T]]:
match (exact, min, max):
case (int(e), None, None):
return self.many_lazy().satisfies(lambda lst: len(lst) == e)
case (None, int(mn), None):
return self.many_lazy().satisfies(lambda lst: len(lst) >= mn)
case (None, None, int(mx)):
return self.many_lazy().satisfies(lambda lst: len(lst) <= mx)
case (None, int(mn), int(mx)):
return self.many_lazy().satisfies(lambda lst: mn <= len(lst) <= mx)
case _:
raise Exception("Illegal combination of parameters")
def sep_by(self, sep: P[Any]) -> P[list[T]]:
return P.map2(self, P.second(sep, self).many(), lambda f, r: [f] + r)
def sep_by_lazy(self, sep: P[Any]) -> P[list[T]]:
return P.map2(self, P.second(sep, self).many_lazy(), lambda f, r: [f] + r)
def no_match(self) -> P[tuple[()]]:
def inner(parserPos: ParserInput) -> ParserResult[tuple[()]]:
result = self.func(parserPos)
try:
next(result)
# Silently yields nothing so is an empty Generator
except StopIteration:
yield (parserPos, ())
return P(inner)
@classmethod
def map2(cls, p1: P[T1], p2: P[T2], func: Callable[[T1, T2], TR]) -> P[TR]:
return p1.bind(lambda v1: p2.fmap(lambda v2: func(v1, v2)))
@classmethod
def map3(cls, 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))))
@classmethod
def map4(cls, 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)))))
@classmethod
def map5(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5],
func: Callable[[T1, T2, T3, T4, T5], TR]) -> P[TR]:
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))))))
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], /) -> P[tuple[T1, T2]]:
...
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[tuple[T1, T2, T3]]:
...
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[tuple[T1, T2, T3, T4]]:
...
@classmethod
@overload
def seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /) -> P[tuple[T1, T2, T3, T4, T5]]:
...
@classmethod
def seq(cls, *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)
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], /, *, sep: P[Any]) -> P[tuple[T1, T2]]:
...
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3]]:
...
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /,
*, sep: P[Any]) -> P[tuple[T1, T2, T3, T4]]:
...
@classmethod
@overload
def sep_seq(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3, T4, T5]]:
...
@classmethod
def sep_seq(cls, *ps: P[Any], sep: P[Any]) -> P[tuple[Any, ...]]:
first, *rest = list(ps)
return P.map2(first,
reduce(lambda p, x: P.second(sep, x.bind(
lambda a: p.fmap(lambda b: chain([a], b)))),
rest[::-1], P.pure(iter([]))),
lambda f, r: (f,) + tuple(r))
@classmethod
def either(cls, p1: P[T1], p2: P[T2], /) -> P[T1 | T2]:
def inner(parserPos: ParserInput):
yield from p1.func(parserPos)
yield from p2.func(parserPos)
return P(inner)
@classmethod
@overload
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[T1 | T2 | T3]:
...
@classmethod
@overload
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[T1 | T2 | T3 | T4]:
...
@classmethod
@overload
def choice(cls, p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4],
p5: P[T5], /) -> P[T1 | T2 | T3 | T4 | T5]:
...
@classmethod
def choice(cls, *ps: P[Any]) -> P[Any]:
def inner(parserPos: ParserInput) -> Iterator[Any]:
for p in ps:
yield from p.func(parserPos)
return P(inner)
@classmethod
def choice2(cls, *ps: P[T]) -> P[T]:
return P.choice(*ps)
# Start of String functions
@classmethod
def any_char(cls) -> P[str]:
def inner(parserPos: ParserInput) -> ParserResult[str]:
if parserPos.has_data():
yield parserPos.step()
return P(inner)
@classmethod
def eof(cls) -> P[tuple[()]]:
return P.any_char().no_match()
@classmethod
def char_func(cls, cmp: Callable[[str], bool]) -> P[str]:
return P.any_char().satisfies(cmp)
@classmethod
def char(cls, cmp: str) -> P[str]:
return P.char_func(lambda c: c == cmp)
@classmethod
def string(cls, cmp: str) -> P[str]:
return P.seq(*map(P.char, cmp)).replace(cmp)
@classmethod
def tchar(cls, cmp: str) -> P[str]:
return P.char(cmp).trim()
@classmethod
def tstring(cls, cmp: str) -> P[str]:
return P.string(cmp).trim()
@classmethod
def one_of(cls, s: str) -> P[str]:
return P.char_func(lambda c: c in s)
@classmethod
def any_decimal(cls) -> P[str]:
return P.char_func(lambda c: c.isdecimal())
@classmethod
def is_decimal(cls, num: int) -> P[str]:
return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) == num)
@classmethod
def is_not_decimal(cls, num: int) -> P[str]:
return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) != num)
@classmethod
def lower(cls) -> P[str]:
return P.char_func(lambda c: c.islower())
@classmethod
def upper(cls) -> P[str]:
return P.char_func(lambda c: c.isupper())
@classmethod
def eol(cls) -> P[tuple[()]]:
return P.char('\n').ignore()
@classmethod
def space(cls) -> P[str]:
return P.char_func(lambda c: c.isspace())
def word(self) -> P[str]:
return P.first(self.many().fmap(lambda cs: ''.join(str(c) for c in cs)), self.no_match())
@classmethod
def unsigned(cls) -> P[int]:
return P.either(P.first(P.is_decimal(0), P.any_decimal().no_match()).replace(0),
P.map2(P.is_not_decimal(0), P.any_decimal().word(),
lambda f, s: int(f + s)))
@classmethod
def signed(cls) -> P[int]:
return P.map2(P.one_of('+-').optional(), P.unsigned(),
lambda sign, num: num if sign != '-' else -num)
@classmethod
def tunsigned(cls) -> P[int]:
return P.unsigned().trim()
@classmethod
def tsigned(cls) -> P[int]:
return P.signed().trim()
def line(self) -> P[T]:
return P.first(self, P.eol())
def tline(self) -> P[T]:
return P.first(self.trim(), P.eol())
def in_parens(self) -> P[T]:
return self.between(P.char('('), P.char(')'))
def in_angles(self) -> P[T]:
return self.between(P.char('<'), P.char('>'))
def in_brackets(self) -> P[T]:
return self.between(P.char('['), P.char(']'))
def in_curleys(self) -> P[T]:
return self.between(P.char('{'), P.char('}'))
def trim_left(self) -> P[T]:
return P.second(WHITE_SPACE, self)
def trim_right(self) -> P[T]:
return P.first(self, WHITE_SPACE)
def trim(self) -> P[T]:
return self.between(WHITE_SPACE, WHITE_SPACE)
WHITE_SPACE: P[tuple[()]] = P.space().many().ignore()
SEP_SPACE: P[tuple[()]] = P.space().some().ignore()

View file

@ -1,101 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterator, Protocol, Self
AllowedParserInput = str | Iterator[str]
def create_parser_input(input: AllowedParserInput) -> ParserInput:
if isinstance(input, str):
return SimpleParserInput(input, 0)
else:
return IteratorParserInput(StringDispenser(input), 0)
class ParserInput(Protocol):
def step(self) -> tuple[Self, str]:
...
def has_data(self) -> bool:
...
@dataclass(slots=True, frozen=True)
class SimpleParserInput:
input: str
start: int
def step(self) -> tuple[Self, str]:
if self.start >= len(self.input):
raise Exception("Already at End of Input")
return SimpleParserInput(self.input, self.start + 1), self.input[self.start]
def has_data(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:]}]'
@dataclass(slots=True)
class StringDispenser:
lines: Iterator[str]
input: str = field(default="", init=False)
length: int = field(default=0, init=False)
def read_more(self):
try:
part = next(self.lines)
if self.input:
self.input = f"{self.input}\n{part}"
else:
self.input = part
self.length = len(self.input)
return True
except StopIteration:
return False
def get_str(self, pos: int) -> str | None:
assert pos >= 0
if pos < self.length:
return self.input[pos]
elif pos == self.length and pos != 0:
return "\n"
elif self.read_more():
return self.get_str(pos)
else:
return None
def has_more(self, pos: int) -> bool:
assert pos >= 0
if pos <= self.length:
return True
elif self.read_more():
return self.has_more(pos)
else:
return False
@dataclass(slots=True, frozen=True)
class IteratorParserInput:
dispenser: StringDispenser
start: int
def step(self) -> tuple[Self, str]:
char = self.dispenser.get_str(self.start)
if char is None:
raise Exception("Already at End of Input")
return IteratorParserInput(self.dispenser, self.start + 1), char
def has_data(self) -> bool:
return self.dispenser.has_more(self.start)

View file

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

@ -1,295 +0,0 @@
from advent.parser.parser import P
import pytest
def test_one_letter():
parser = P.char('!')
input = '!'
expected = '!'
result = parser.parse(input).get()
assert result == expected
def test_one_letter_longer():
parser = P.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.char('<'), P.char('>'))
input = '<-123456>'
expected = -123456
result = parser.parse(input).get()
assert result == expected
def test_sep_by_single():
parser = P.signed().sep_by(P.char(','))
input = '2'
expected = [[2]]
result = list(parser.parse_multi(input))
assert result == expected
def test_sep_by():
parser = P.signed().sep_by(P.char(','))
input = '2,3,5'
expected = [[2, 3, 5], [2, 3], [2]]
result = list(parser.parse_multi(input))
assert result == expected
def test_sep_by_lazy():
parser = P.signed().sep_by_lazy(P.char(','))
input = '2,3,5'
expected = [[2], [2, 3], [2, 3, 5]]
result = list(parser.parse_multi(input))
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.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.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_func(lambda c: c.isdigit(), )
parser = P.sep_seq(digit, digit, digit, digit, sep=P.char(','))
expected = ('1', '2', '3', '4')
result = parser.parse(input).get()
assert result == expected
def test_not():
input = 'a'
parser = P.second(P.char('!').no_match(), P.char('a'))
expected = 'a'
result = parser.parse(input).get()
assert result == expected
input2 = '!'
result2 = parser.parse(input2)
assert result2.is_fail()
def test_multi():
input = 'aa'
parser = P.char('a').many()
expected = [['a', 'a'], ['a'], []]
result = list(parser.parse_multi(input))
assert result == expected
def test_either():
input = 'aab'
parser = P.either(
P.seq(
P.char('a').many(), P.string('b')), P.seq(
P.string('a'), P.string('ab')))
expected = [(['a', 'a'], 'b'), ('a', 'ab')]
result = list(parser.parse_multi(input))
assert result == expected
def test_seq_eof():
input = 'aa'
parser = P.seq(P.char('a').many(), P.eof())
expected = [(['a', 'a'], ())]
result = list(parser.parse_multi(input))
assert result == expected
def test_optional():
input = '12'
parser = P.seq(P.char('1').optional(), P.unsigned())
expected = [('1', 2), (None, 12)]
result = list(parser.parse_multi(input))
assert result == expected
def test_choice():
input = '1'
parser = P.choice(P.char('1'), P.char('b'), P.unsigned())
expected = ['1', 1]
result = list(parser.parse_multi(input))
assert result == expected
def test_times_exact():
input = 'aaa'
parser = P.char('a').times(exact=2)
expected = [['a', 'a']]
result = list(parser.parse_multi(input))
assert result == expected
def test_times_min():
input = 'aaa'
parser = P.char('a').times(min=2)
expected = [['a', 'a', 'a'], ['a', 'a']]
result = list(parser.parse_multi(input))
assert result == expected
def test_times_max():
input = 'aaa'
parser = P.char('a').times(max=2)
expected = [['a', 'a'], ['a'], []]
result = list(parser.parse_multi(input))
assert result == expected
def test_some_lazy():
input = 'aa'
parser = P.char('a').some_lazy()
expected = [['a'], ['a', 'a']]
result = list(parser.parse_multi(input))
assert result == expected
def test_many_lazy():
input = 'aa'
parser = P.char('a').many_lazy()
expected = [[], ['a'], ['a', 'a']]
result = list(parser.parse_multi(input))
assert result == expected
def test_times_lazy_exact():
input = 'aaa'
parser = P.char('a').times_lazy(exact=2)
expected = [['a', 'a']]
result = list(parser.parse_multi(input))
assert result == expected
def test_times_lazy_min():
input = 'aaa'
parser = P.char('a').times_lazy(min=2)
expected = [['a', 'a'], ['a', 'a', 'a']]
result = list(parser.parse_multi(input))
assert result == expected
def test_times_lazy_max():
input = 'aaa'
parser = P.char('a').times_lazy(max=2)
expected = [[], ['a'], ['a', 'a']]
result = list(parser.parse_multi(input))
assert result == expected
def test_word():
input = '123'
parser = P.any_decimal().word()
expected = ['123']
result = list(parser.parse_multi(input))
assert result == expected
def test_word2():
input = '123a'
parser = P.seq(P.any_decimal().word(), P.char('a'))
expected = [('123', 'a')]
result = list(parser.parse_multi(input))
assert result == expected
def test_iterator_input():
input = iter(['1', '2'])
parser = P.unsigned().line().many()
expected = [1, 2]
result = parser.parse(input).get()
assert result == expected
def test_iterator_trim_input():
input = iter(['1 ', '2 '])
parser = P.unsigned().trim().line().many()
expected = [1, 2]
result = parser.parse(input).get()
assert result == expected