removed unnecessary parser
This commit is contained in:
parent
f59089d5eb
commit
38efdd1d1c
11 changed files with 105 additions and 1043 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue