From 64e3eb063cf9486598c878d5cf811c99f7631853 Mon Sep 17 00:00:00 2001 From: Ruediger Ludwig Date: Thu, 8 Dec 2022 07:42:26 +0100 Subject: [PATCH] Minor tweaks on Parser --- advent/days/day05/solution.py | 2 +- advent/parser/parser.py | 131 +++++++++++++++++----------------- advent/parser/result.py | 2 +- advent/parser/test_parser.py | 20 +++++- 4 files changed, 86 insertions(+), 69 deletions(-) diff --git a/advent/days/day05/solution.py b/advent/days/day05/solution.py index 6bb62dd..1267ebe 100644 --- a/advent/days/day05/solution.py +++ b/advent/days/day05/solution.py @@ -59,7 +59,7 @@ class Crane: is_9001: bool crate_parser: ClassVar[P[str | None]] = P.either( - P.any_char().in_brackets(), P.string(" ").replace(None)) + P.one_char().in_brackets(), P.string(" ").replace(None)) crate_row_parser: ClassVar[P[list[str | None]]] = crate_parser.sep_by(P.is_char(' ')) @staticmethod diff --git a/advent/parser/parser.py b/advent/parser/parser.py index f2eea10..1b5edba 100644 --- a/advent/parser/parser.py +++ b/advent/parser/parser.py @@ -121,7 +121,7 @@ class P(Generic[T]): def many(self) -> P[list[T]]: return P.either(self.some(), P.pure([])) - def satisfies(self, pred: Callable[[T], bool], failtext: str) -> P[T]: + 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]: @@ -150,18 +150,18 @@ class P(Generic[T]): return P(inner) - @ staticmethod + @staticmethod def map2(p1: P[T1], p2: P[T2], func: Callable[[T1, T2], TR]) -> P[TR]: return p1.bind(lambda v1: p2.fmap(lambda v2: func(v1, v2))) - @ staticmethod + @staticmethod def map3(p1: P[T1], p2: P[T2], p3: P[T3], func: Callable[[T1, T2, T3], TR]) -> P[TR]: return p1.bind( lambda v1: p2.bind( lambda v2: p3.fmap( lambda v3: func(v1, v2, v3)))) - @ staticmethod + @staticmethod def map4(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], func: Callable[[T1, T2, T3, T4], TR]) -> P[TR]: return p1.bind( @@ -170,7 +170,7 @@ class P(Generic[T]): lambda v3: p4.fmap( lambda v4: func(v1, v2, v3, v4))))) - @ staticmethod + @staticmethod def map5(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5], func: Callable[[T1, T2, T3, T4, T5], TR]) -> P[TR]: return p1.bind( @@ -180,56 +180,56 @@ class P(Generic[T]): lambda v4: p5.fmap( lambda v5: func(v1, v2, v3, v4, v5)))))) - @ staticmethod - @ overload + @staticmethod + @overload def seq(p1: P[T1], p2: P[T2], /) -> P[tuple[T1, T2]]: ... - @ staticmethod - @ overload + @staticmethod + @overload def seq(p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[tuple[T1, T2, T3]]: ... - @ staticmethod - @ overload + @staticmethod + @overload def seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[tuple[T1, T2, T3, T4]]: ... - @ staticmethod - @ overload + @staticmethod + @overload def seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5], /) -> P[tuple[T1, T2, T3, T4, T5]]: ... - @ staticmethod + @staticmethod def seq(*ps: P[Any]) -> P[tuple[Any, ...]]: return reduce(lambda p, x: x.bind( lambda a: p.fmap(lambda b: chain([a], b))), list(ps)[::-1], P.pure(iter([]))).fmap(tuple) - @ staticmethod - @ overload + @staticmethod + @overload def sep_seq(p1: P[T1], p2: P[T2], /, *, sep: P[Any]) -> P[tuple[T1, T2]]: ... - @ staticmethod - @ overload + @staticmethod + @overload def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3]]: ... - @ staticmethod - @ overload + @staticmethod + @overload def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3, T4]]: ... - @ staticmethod - @ overload + @staticmethod + @overload def sep_seq(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5], /, *, sep: P[Any]) -> P[tuple[T1, T2, T3, T4, T5]]: ... - @ staticmethod + @staticmethod def sep_seq(*ps: P[Any], sep: P[Any]) -> P[tuple[Any, ...]]: first, *rest = list(ps) return P.map2(first, @@ -238,7 +238,7 @@ class P(Generic[T]): rest[::-1], P.pure(iter([]))), lambda f, r: (f,) + tuple(r)) - @ staticmethod + @staticmethod def either(p1: P[T1], p2: P[T2], /) -> P[T1 | T2]: def inner(parserPos: ParserInput): yield from p1.func(parserPos) @@ -246,102 +246,103 @@ class P(Generic[T]): return P(inner) - @ staticmethod - @ overload + @staticmethod + @overload def choice(p1: P[T1], p2: P[T2], p3: P[T3], /) -> P[T1 | T2 | T3]: ... - @ staticmethod - @ overload + @staticmethod + @overload def choice(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], /) -> P[T1 | T2 | T3 | T4]: ... - @ staticmethod - @ overload + @staticmethod + @overload def choice(p1: P[T1], p2: P[T2], p3: P[T3], p4: P[T4], p5: P[T5], /) -> P[T1 | T2 | T3 | T4 | T5]: ... - @ staticmethod + @staticmethod def choice(*ps: P[Any]) -> P[Any]: - return reduce(P.either, ps, P.fail()) + def inner(parserPos: ParserInput) -> Iterator[Any]: + for p in ps: + yield from p.func(parserPos) + return P(inner) - @ staticmethod - def choice_same(*ps: P[T]) -> P[T]: + @staticmethod + def choice2(*ps: P[T]) -> P[T]: return P.choice(*ps) - @ staticmethod - def any_char() -> P[str]: + @staticmethod + def one_char() -> P[str]: def inner(parserPos: ParserInput) -> ParserResult[str]: if not parserPos.is_eof(): yield parserPos.step() return P(inner) - @ staticmethod + @staticmethod def eof() -> P[tuple[()]]: def inner(parserPos: ParserInput) -> ParserResult[tuple[()]]: if parserPos.is_eof(): yield parserPos, () return P(inner) - @ staticmethod + @staticmethod + def char_func(cmp: Callable[[str], bool]) -> P[str]: + return P.one_char().satisfies(cmp) + + @staticmethod def is_char(cmp: str) -> P[str]: - return P.any_char().satisfies(lambda c: c == cmp, f'match {cmp}') + return P.char_func(lambda c: c == cmp) @staticmethod def is_not_char(s: str) -> P[tuple[()]]: return P.no_match(P.is_char(s)) - @ staticmethod - def char_by_func(cmp: Callable[[str], bool], failtext: str) -> P[str]: - return P.any_char().satisfies(cmp, failtext) - - @ staticmethod + @staticmethod def string(s: str) -> P[str]: return P.seq(*map(P.is_char, s)).replace(s) - @ staticmethod + @staticmethod def one_of(s: str) -> P[str]: - return P.char_by_func(lambda c: c in s, f'one of {s}') + return P.char_func(lambda c: c in s) - @ staticmethod + @staticmethod def any_decimal() -> P[str]: - return P.char_by_func(lambda c: c.isdecimal(), 'decimal') + return P.char_func(lambda c: c.isdecimal()) - @ staticmethod + @staticmethod def is_decimal(num: int) -> P[str]: - return P.any_decimal().bind( - lambda c: P.pure(c) if unicodedata.decimal(c) == num else P.fail()) + return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) == num) - @ staticmethod + @staticmethod def is_not_decimal(num: int) -> P[str]: - return P.any_decimal().bind( - lambda c: P.pure(c) if unicodedata.decimal(c) != num else P.fail()) + return P.any_decimal().satisfies(lambda c: unicodedata.decimal(c) != num) - @ staticmethod + @staticmethod def lower() -> P[str]: - return P.char_by_func(lambda c: c.islower(), 'lower') + return P.char_func(lambda c: c.islower()) - @ staticmethod + @staticmethod def upper() -> P[str]: - return P.char_by_func(lambda c: c.isupper(), 'upper') + return P.char_func(lambda c: c.isupper()) - @ staticmethod + @staticmethod + def space() -> P[str]: + return P.char_func(lambda c: c.isspace()) + + @staticmethod def joined(p1: P[str]) -> P[str]: return p1.many().fmap(lambda cs: ''.join(cs)) - @ staticmethod - def space() -> P[str]: - return P.char_by_func(lambda c: c.isspace(), 'space') - - @ staticmethod + @staticmethod def unsigned() -> P[int]: return P.either(P.fst(P.is_decimal(0), P.no_match(P.any_decimal())), P.map2(P.is_not_decimal(0), P.any_decimal().many(), lambda f, s: f + ''.join(s)) ).fmap(int) - @ staticmethod + @staticmethod def signed() -> P[int]: return P.map2(P.one_of('+-').optional(), P.unsigned(), lambda sign, num: num if sign != '-' else -num) diff --git a/advent/parser/result.py b/advent/parser/result.py index b7ba9f8..cba4d41 100644 --- a/advent/parser/result.py +++ b/advent/parser/result.py @@ -62,7 +62,7 @@ class Success(Result[S]): return False def fmap(self, func: Callable[[S], S2]) -> Result[S2]: - return Result.of(func(self.value)) # type: ignore + return Result.of(func(self.value)) def bind(self, func: Callable[[S], Result[S2]]) -> Result[S2]: return func(self.value) diff --git a/advent/parser/test_parser.py b/advent/parser/test_parser.py index 779b45a..d119444 100644 --- a/advent/parser/test_parser.py +++ b/advent/parser/test_parser.py @@ -112,7 +112,7 @@ def test_choice2(): def test_seq(): input = '1234' - parser = P.seq(P.any_char(), P.any_char(), P.any_char(), P.any_char()) + parser = P.seq(P.one_char(), P.one_char(), P.one_char(), P.one_char()) expected = ('1', '2', '3', '4') result = parser.parse(input).get() assert result == expected @@ -120,7 +120,7 @@ def test_seq(): def test_seq_seq(): input = '1,2,3,4' - digit = P.char_by_func(lambda c: c.isdigit(), "") + digit = P.char_func(lambda c: c.isdigit(), ) parser = P.sep_seq(digit, digit, digit, digit, sep=P.is_char(',')) expected = ('1', '2', '3', '4') @@ -165,3 +165,19 @@ def test_seq_eof(): expected = [(['a', 'a'], ())] result = list(parser.parse_multi(input)) assert result == expected + + +def test_optional(): + input = '12' + parser = P.seq(P.is_char('1').optional(), P.unsigned()) + expected = [('1', 2), (None, 12), (None, 1)] + result = list(parser.parse_multi(input)) + assert result == expected + + +def test_choice(): + input = '1' + parser = P.choice(P.is_char('1'), P.is_char('b'), P.unsigned()) + expected = ['1', 1] + result = list(parser.parse_multi(input)) + assert result == expected