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

View file

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

View file

@ -48,7 +48,7 @@ def test_parse_move():
def test_parse_all(): def test_parse_all():
data = input.read_lines(day_num, 'example01.txt') data = input.read_lines(day_num, 'example01.txt')
expected = Crane( 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) [Move(1, 1, 0), Move(3, 0, 2), Move(2, 1, 0), Move(1, 0, 1)], True)
result = Crane.parse(data, True) result = Crane.parse(data, True)
assert result == expected assert result == expected

View file

@ -2,9 +2,9 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from math import prod from math import prod
import re
from typing import Callable, Iterator, Self from typing import Callable, Iterator, Self
from advent.parser.parser import P
day_num = 11 day_num = 11
@ -21,44 +21,16 @@ def part2(lines: Iterator[str]) -> int:
return horde.inspected_result() 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] 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) @dataclass(slots=True)
class Monkey: class Monkey:
number: int number: int
@ -69,6 +41,31 @@ class Monkey:
catcher_if_not_divides: int catcher_if_not_divides: int
inspected: int = field(default=0, compare=False) 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]]: def inspect_items(self, worry_decrease: int | None) -> Iterator[tuple[int, int]]:
for item in self.items: for item in self.items:
self.inspected += 1 self.inspected += 1
@ -93,6 +90,15 @@ class Monkey:
class Troop(ABC): class Troop(ABC):
monkeys: list[Monkey] 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 @abstractmethod
def single_round(self): def single_round(self):
... ...
@ -108,11 +114,9 @@ class Troop(ABC):
@dataclass(slots=True) @dataclass(slots=True)
class Troop_While_Worried(Troop): class Troop_While_Worried(Troop):
""" The """
@classmethod @classmethod
def parse(cls, lines: Iterator[str]) -> Self: def parse(cls, lines: Iterator[str]) -> Self:
monkeys = Parser.monkey_list.parse(lines).get() return Troop_While_Worried(list(Troop.parse_monkeys(lines)))
return Troop_While_Worried(monkeys)
def single_round(self): def single_round(self):
for currentMonkey in self.monkeys: for currentMonkey in self.monkeys:
@ -126,7 +130,7 @@ class Troop_While_Kinda_Relieved(Troop):
@classmethod @classmethod
def parse(cls, lines: Iterator[str]) -> Self: 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)) return Troop_While_Kinda_Relieved(monkeys, prod(monkey.modulator for monkey in monkeys))
def single_round(self): def single_round(self):

View file

@ -3,9 +3,9 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from itertools import product from itertools import product
from queue import PriorityQueue from queue import PriorityQueue
import re
from typing import Iterator, Literal, NamedTuple, Self from typing import Iterator, Literal, NamedTuple, Self
from advent.parser.parser import P
day_num = 16 day_num = 16
@ -20,30 +20,29 @@ def part2(lines: Iterator[str]) -> int:
return system.under_pressure(26, 2) return system.under_pressure(26, 2)
valve_parser = P.map3( pattern = re.compile(r"Valve (?P<name>[a-zA-Z]+)[^=]+=(?P<flow_rate>\d+).+valves? (?P<exits>.*)")
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)
)
class RawValve(NamedTuple): class RawValve(NamedTuple):
name: str name: str
flow_rate: int flow_rate: int
following: list[str] exits: list[str]
@classmethod @classmethod
def parse(cls, line: str) -> Self: 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) @dataclass(slots=True)
class Valve: class Valve:
name: str name: str
flow_rate: int flow_rate: int
following: list[Valve] exits: list[Valve]
paths: dict[str, int] = field(default_factory=dict, init=False) paths: dict[str, int] = field(default_factory=dict, init=False)
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
@ -58,7 +57,7 @@ class Valve:
return self.name < other.name return self.name < other.name
def __repr__(self) -> str: 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: def travel_time(self, to: str) -> int:
return self.paths[to] return self.paths[to]
@ -71,7 +70,7 @@ class Valve:
to_check = to_check[1:] to_check = to_check[1:]
paths[current.name] = steps, (current.flow_rate > 0) 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)) known_path, _ = paths.get(next.name, (steps + 2, False))
if known_path > steps + 1: if known_path > steps + 1:
to_check.append((next, 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} valves = {valve.name: Valve(valve.name, valve.flow_rate, []) for valve in raw_system}
for raw in raw_system: for raw in raw_system:
current = valves[raw.name] current = valves[raw.name]
for follow in raw.following: for follow in raw.exits:
current.following.append(valves[follow]) current.exits.append(valves[follow])
return Network(valves) return Network(valves)

View file

@ -6,9 +6,9 @@ from itertools import islice
from math import prod from math import prod
from multiprocessing import Pool from multiprocessing import Pool
from queue import PriorityQueue from queue import PriorityQueue
import re
from typing import Iterable, Iterator, Self from typing import Iterable, Iterator, Self
from advent.parser.parser import P
day_num = 19 day_num = 19
@ -35,17 +35,6 @@ class Processor:
return blueprint, blueprint.run(self.rounds) 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): class Element(IntEnum):
Geode = 0 Geode = 0
Obsidian = 1 Obsidian = 1
@ -114,6 +103,15 @@ class State:
return self.material > other.material 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) @dataclass(slots=True)
class Blueprint: class Blueprint:
number: int number: int
@ -134,7 +132,16 @@ class Blueprint:
@classmethod @classmethod
def parse(cls, line: str) -> Self: 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: def run(self, rounds: int) -> int:
queue: PriorityQueue[State] = PriorityQueue() 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