diff --git a/advent/__main__.py b/advent/__main__.py index 7a722d4..1fe4780 100644 --- a/advent/__main__.py +++ b/advent/__main__.py @@ -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) diff --git a/advent/days/day05/solution.py b/advent/days/day05/solution.py index c354029..87099b2 100644 --- a/advent/days/day05/solution.py +++ b/advent/days/day05/solution.py @@ -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 diff --git a/advent/days/day05/test_solution.py b/advent/days/day05/test_solution.py index 70d4fec..7c5872c 100644 --- a/advent/days/day05/test_solution.py +++ b/advent/days/day05/test_solution.py @@ -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 diff --git a/advent/days/day11/solution.py b/advent/days/day11/solution.py index 4bb2379..92fb197 100644 --- a/advent/days/day11/solution.py +++ b/advent/days/day11/solution.py @@ -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\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): diff --git a/advent/days/day16/solution.py b/advent/days/day16/solution.py index 3b85b26..8ecb2b8 100644 --- a/advent/days/day16/solution.py +++ b/advent/days/day16/solution.py @@ -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[a-zA-Z]+)[^=]+=(?P\d+).+valves? (?P.*)") 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) diff --git a/advent/days/day19/solution.py b/advent/days/day19/solution.py index 4874a97..027be32 100644 --- a/advent/days/day19/solution.py +++ b/advent/days/day19/solution.py @@ -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\d+):" +r_ore = r".*(?P\d+) ore." +r_clay = r".*(?P\d+) ore." +r_obsidian = r".*(?P\d+) ore and (?P\d+) clay." +r_geode = r".*(?P\d+) ore and (?P\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() diff --git a/advent/parser/__init__.py b/advent/parser/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/advent/parser/parser.py b/advent/parser/parser.py deleted file mode 100644 index dc88d2f..0000000 --- a/advent/parser/parser.py +++ /dev/null @@ -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() diff --git a/advent/parser/parser_input.py b/advent/parser/parser_input.py deleted file mode 100644 index 6a53869..0000000 --- a/advent/parser/parser_input.py +++ /dev/null @@ -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) diff --git a/advent/parser/result.py b/advent/parser/result.py deleted file mode 100644 index cba4d41..0000000 --- a/advent/parser/result.py +++ /dev/null @@ -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 diff --git a/advent/parser/test_parser.py b/advent/parser/test_parser.py deleted file mode 100644 index 3ca9057..0000000 --- a/advent/parser/test_parser.py +++ /dev/null @@ -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