Skip to content

Commit

Permalink
Python: replace ParseCharMap, CharCoordParser, AsciiBoolMapParser, Ch…
Browse files Browse the repository at this point in the history
…arCoordinatesParser with CoordinatesParser
  • Loading branch information
IsaacG committed Dec 9, 2024
1 parent 58e7179 commit eac7757
Show file tree
Hide file tree
Showing 9 changed files with 43 additions and 111 deletions.
7 changes: 3 additions & 4 deletions 2015/d18.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,14 @@ class Day18(aoc.Challenge):
aoc.TestCase(inputs=SAMPLE, part=1, want=4),
aoc.TestCase(inputs=SAMPLE, part=2, want=17),
]
INPUT_PARSER = aoc.ParseCharMap(lambda x: x == "#")

def simulate(self, bitmap: dict[complex, bool], cycles: int, corners: bool) -> int:
def simulate(self, bitmap: aoc.Map, cycles: int, corners: bool) -> int:
"""Run Conway's Game of Life and return number of lights on at end."""
# The number of neighbors which will turn a light on.
want = {True: (2, 3), False: (3,)}
# Track just the lights which are on.
on = {p for p, v in bitmap.items() if v}
corner_points = aoc.Board(bitmap).corners
on = bitmap["#"]
corner_points = bitmap.corners

for _ in range(cycles):
if corners:
Expand Down
5 changes: 2 additions & 3 deletions 2017/d19.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ class Day19(aoc.Challenge):
aoc.TestCase(part=1, inputs=SAMPLE, want="ABCDEF"),
aoc.TestCase(part=2, inputs=SAMPLE, want=38),
]

INPUT_PARSER = aoc.ParseCharMap(lambda x: None if x == " " else x)
INPUT_PARSER = aoc.CoordinatesParser(ignore=" ")

def solver(self, puzzle_input: dict[complex, str], part_one: bool) -> int | str:
"""Walk the maze and track steps/letters."""
maze = puzzle_input
maze = puzzle_input.chars
# Initialize
direction = complex(0, 1)
location = next(i for i in maze if i.imag == 0)
Expand Down
6 changes: 3 additions & 3 deletions 2017/d22.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Day22(aoc.Challenge):
INPUT_PARSER = aoc.ParseMultiple(
[
aoc.Transform(lambda x: len(x.splitlines())),
aoc.AsciiBoolMapParser("#", origin_top_left=False),
aoc.CoordinatesParser(origin_top_left=False),
]
)
TESTS = [
Expand All @@ -25,8 +25,8 @@ class Day22(aoc.Challenge):

def solver(self, puzzle_input: tuple[int, set[complex]], part_one: bool) -> int:
"""Simulate a virus and count the infections."""
dimension, initial_infected = puzzle_input
board = {i: INFECTED for i in initial_infected}
dimension, mapper = puzzle_input
board = {i: INFECTED for i in mapper["#"]}
states = STATES_P1 if part_one else STATES_P2
next_state = dict(zip(states[:-1], states[1:]))
direction = complex(0, 1)
Expand Down
10 changes: 4 additions & 6 deletions 2018/d13.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,9 @@ def solver(self, puzzle_input: InputType, part_one: bool) -> str:

def input_parser(self, puzzle_input: str) -> InputType:
"""Parse the input data."""
# tracks = aoc.AsciiBoolMapParser("/\-|+^v<>").parse(puzzle_input)
junctions = aoc.AsciiBoolMapParser("+").parse(puzzle_input)
turns = aoc.ParseCharMap(lambda x: CORNERS.get(x, None)).parse(puzzle_input)
wagons_directions = aoc.ParseCharMap(
lambda x: DIRECTIONS.get(x, None)
).parse(puzzle_input)
parsed = aoc.CoordinatesParser().parse(puzzle_input)
junctions = parsed.coords.get("+", set())
turns = {coord: CORNERS[char] for char in "/\\" for coord in parsed.coords.get(char, [])}
wagons_directions = {coord: direction for char, direction in DIRECTIONS.items() for coord in parsed.coords.get(char, [])}
wagons = {location: (direction, 0) for location, direction in wagons_directions.items()}
return (turns, junctions, wagons)
5 changes: 3 additions & 2 deletions 2018/d15.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,12 @@ def simulate(self, spaces: set[complex], units_by_type: dict[str, dict[complex,

def input_parser(self, puzzle_input: str) -> InputType:
"""Parse the input data."""
spaces = aoc.AsciiBoolMapParser(".EG").parse(puzzle_input)
data = aoc.CoordinatesParser().parse(puzzle_input)
spaces = data - "#"
units = {
unit_type: {
i: 200
for i in aoc.AsciiBoolMapParser(unit_type).parse(puzzle_input)
for i in data[unit_type]
}
for unit_type in "EG"
}
Expand Down
4 changes: 2 additions & 2 deletions 2022/d17.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ class Day17(aoc.Challenge):
def pre_run(self, puzzle_input: InputType) -> None:
"""Parse the rock shapes from ASCII art."""
rocks = []
parser = aoc.AsciiBoolMapParser("#")
parser = aoc.CoordinatesParser()
for block in ROCKS.strip().split("\n\n"):
block = block.replace(" ", "")
# Flip rocks upside down so Y increases from the bottom to the top.
# Most my parsing assumes Y increases as you move down through text.
block = "\n".join(reversed(block.splitlines()))
rocks.append(parser.parse(block))
rocks.append(parser.parse(block)["#"])
self.rocks = rocks
self.rock_heights = [int(max(i.imag for i in r)) for r in rocks]

Expand Down
13 changes: 5 additions & 8 deletions 2022/d23.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/bin/python
"""Advent of Code, Day 23: Unstable Diffusion. Spread elves out across a field to plant trees."""

from typing import Optional
from typing import cast, Optional

from lib import aoc

Expand All @@ -15,8 +15,6 @@
.#..#.."""


InputType = set[complex]

N, S, E, W = -1j, 1j, 1, -1
DIRECTIONS = [
(N, (N + W, N, N + E)),
Expand All @@ -33,7 +31,6 @@ class Day23(aoc.Challenge):
aoc.TestCase(inputs=SAMPLE, part=1, want=110),
aoc.TestCase(inputs=SAMPLE, part=2, want=20),
]
INPUT_PARSER = aoc.AsciiBoolMapParser("#")

def simulate(self, positions: set[complex], step: int) -> set[complex]:
"""Simulate cycles of elves spreading out."""
Expand Down Expand Up @@ -78,9 +75,9 @@ def simulate(self, positions: set[complex], step: int) -> set[complex]:

return new_positions

def part1(self, puzzle_input: InputType) -> int:
def part1(self, puzzle_input: aoc.Map) -> int:
"""Return the number of unoccupied positions after 10 moves."""
positions = puzzle_input
positions = cast(set[complex], puzzle_input["#"])
for step in range(10):
positions = self.simulate(positions, step)
min_x = min(elf.real for elf in positions)
Expand All @@ -91,9 +88,9 @@ def part1(self, puzzle_input: InputType) -> int:
answer = int((max_x - min_x + 1) * (max_y - min_y + 1)) - len(positions)
return answer

def part2(self, puzzle_input: InputType) -> int:
def part2(self, puzzle_input: aoc.Map) -> int:
"""Return the number of moves until the pattern is stable."""
positions = puzzle_input
positions = cast(set[complex], puzzle_input["#"])
for step in range(1500):
new_positions = self.simulate(positions, step)
if positions == new_positions:
Expand Down
101 changes: 21 additions & 80 deletions pylib/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,6 @@ def parse(self, puzzle_input: str) -> Any:
return parsed


@dataclasses.dataclass
class ParseCharMap(BaseParser):
"""Parse a map of values, generating a set[complex]."""

transform: Callable[[str], Any]

def parse(self, puzzle_input: str) -> dict[complex, Any]:
"""Parse a map."""
out: dict[complex, Any] = {}
for y, line in enumerate(puzzle_input.splitlines()):
for x, char in enumerate(line):
if (val := self.transform(char)) is not None:
out[complex(x, y)] = val
return out


@dataclasses.dataclass
class Transform(BaseParser):
"""Apply an arbitrary transform to the data."""
Expand All @@ -253,44 +237,6 @@ def parse(self, puzzle_input: Any) -> Any:
return self.func(puzzle_input)


@dataclasses.dataclass
class CharCoordParser(BaseParser):
"""Parse a map of chars, returning a set of coords for each char."""

origin_top_left: bool = True

def parse(self, puzzle_input: str) -> dict[str, set[complex]]:
"""Parse ASCII to find points in a map which are "on"."""
lines = puzzle_input.splitlines()
if not self.origin_top_left:
lines.reverse()
data = collections.defaultdict(set)
for y, line in enumerate(lines):
for x, char in enumerate(line):
data[char].add(complex(x, y))
return dict(data)


@dataclasses.dataclass
class AsciiBoolMapParser(BaseParser):
"""Parse a map of on/off values to build a set of "on" points."""

on_chars: str
origin_top_left: bool = True

def parse(self, puzzle_input: str) -> set[complex]:
"""Parse ASCII to find points in a map which are "on"."""
lines = puzzle_input.splitlines()
if not self.origin_top_left:
lines.reverse()
return {
complex(x, y)
for y, line in enumerate(lines)
for x, char in enumerate(line)
if char in self.on_chars
}


@dataclasses.dataclass
class Map:
max_x: int
Expand All @@ -305,6 +251,14 @@ def __post_init__(self) -> None:
self.width = self.max_x + 1
self.height = self.max_y + 1

def __sub__(self, key: str) -> set[complex]:
return self.all_coords - self[key]

def __contains__(self, item) -> bool:
if not isinstance(item, complex):
raise TypeError(f"contains not defined for {type(item)}")
return item in self.all_coords

def __getitem__(self, key: str | complex) -> str | int | set[complex] | list[set[complex]]:
if isinstance(key, complex):
return self.chars[key]
Expand All @@ -325,13 +279,23 @@ def size(self) -> int:
def get_coords(self, chars: collections.abc.Iterable[str]) -> list[set[complex]]:
return [self.coords[char] for char in chars]

@property
def corners(self) -> tuple[complex, complex, complex, complex]:
return (
0,
complex(0, self.max_y),
complex(self.max_x, 0),
complex(self.max_x, self.max_y)
)


@dataclasses.dataclass
class CoordinatesParser(BaseParser):
"""Generate coordinate sets for multiple characters."""

chars: collections.abc.Iterable[str] | None = None
origin_top_left: bool = True
ignore: str | None = None

def parse(self, puzzle_input: str) -> Map:
"""Parse a map and return the coordinates of different chars."""
Expand All @@ -344,6 +308,8 @@ def parse(self, puzzle_input: str) -> Map:

all_chars = set(i for line in lines for i in line)
want_chars = set(self.chars) if self.chars else all_chars
if self.ignore:
want_chars -= set(self.ignore)
transform: Callable[[str], str | int]
if all(i.isdigit() for i in all_chars):
transform = int
Expand Down Expand Up @@ -375,28 +341,6 @@ def parse(self, puzzle_input: str) -> Map:
)


@dataclasses.dataclass
class CharCoordinatesParser(BaseParser):
"""Generate coordinate sets for multiple characters."""

chars: collections.abc.Iterable[str]

def parse(self, puzzle_input: str) -> tuple[tuple[int, int], list[set[complex]]]:
"""Parse a map and return the coordinates of different chars."""
lines = puzzle_input.splitlines()
size = (len(lines[0]), len(lines))
maps = [
{
complex(x, y)
for y, line in enumerate(lines)
for x, got in enumerate(line)
if got == want
}
for want in self.chars
]
return size, maps


# Convert the entire input into one str.
parse_one_str = ParseOneWord(str)
# Convert the entire input into one int.
Expand All @@ -415,9 +359,9 @@ def parse(self, puzzle_input: str) -> tuple[tuple[int, int], list[set[complex]]]
# Convert the input into list[list[int]], splitting each line into multiple words.
parse_re_group_int = lambda x: BaseParseReGroups(x, input_to_ints)
parse_re_findall_int = lambda x: BaseParseReFindall(x, input_to_ints)
parse_ints = BaseParseReFindall(RE_INT, input_to_ints)
parse_ints_one_line = BaseParseReFindall(RE_INT, input_to_ints, one_line=True)
parse_ints_per_line = BaseParseReFindall(RE_INT, input_to_ints, one_line=False)
parse_ints = parse_ints_per_line

# Convert the input into list[list[int | str]], splitting each line into multiple words.
parse_multi_mixed_per_line = BaseParseMultiPerLine(input_to_mixed)
Expand All @@ -426,6 +370,3 @@ def parse(self, puzzle_input: str) -> tuple[tuple[int, int], list[set[complex]]]
parse_re_findall_points = BaseParseReFindall(RE_POINT, input_to_complex)

parse_multiple_words_per_line = BaseParseMultiPerLine(input_to_auto)

char_map = ParseCharMap(str)
int_map = ParseCharMap(int)
3 changes: 0 additions & 3 deletions shared/tmpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,8 @@ class Day${day}(aoc.Challenge):
# INPUT_PARSER = aoc.parse_re_findall_mixed(r"\d+|foo|bar")
# INPUT_PARSER = aoc.ParseBlocks([aoc.parse_one_str_per_line, aoc.parse_re_findall_int(r"\d+")])
# INPUT_PARSER = aoc.ParseOneWord(aoc.Board.from_int_block)
# INPUT_PARSER = aoc.AsciiBoolMapParser("#")
# INPUT_PARSER = aoc.char_map
# INPUT_PARSER = aoc.CoordinatesParser(chars=None, origin_top_left=True)
# ---
# INPUT_PARSER = aoc.CharCoordinatesParser("S.#")
# (width, height), start, garden, rocks = puzzle_input
# max_x, max_y = width - 1, height - 1

Expand Down

0 comments on commit eac7757

Please sign in to comment.