def from_file(cls, path: str) -> 'DayDescription': with open(path) as file: # """ # Advent of Code 2020 # Day 20: Jurassic Jigsaw # https://adventofcode.com/2020/day/20 # """ if next(file) != '"""\n': raise ValueError(f"missing description in {path}") try: year, = parse_line(next(file), "Advent of Code $\n") day, title = parse_line(next(file), "Day $: $\n") except AssertionError as exc: raise ValueError( f"description in {path} in unexpected format") from exc aoc_url = next(file).strip() if aoc_url != f'https://adventofcode.com/{year}/day/{day}': raise ValueError(f"unexpected url in {path}") if next(file) != '"""\n': raise ValueError( f"description missing closing quotes in {path}") return cls(year=int(year), day=int(day), title=title, path=path, aoc_url=aoc_url)
def rules() -> Iterable[Rule]: moves = {'left': -1, 'right': +1} while True: try: empty_line = next(lines_it) assert not empty_line except StopIteration: # all rules read return try: current_state, = parse_line(next(lines_it), "In state $:") for current_value in range(2): assert next( lines_it ) == f"If the current value is {current_value}:" write, = parse_line(next(lines_it), "- Write the value $.") move_dir, = parse_line(next(lines_it), "- Move one slot to the $.") next_state, = parse_line(next(lines_it), "- Continue with state $.") action = Action(int(write), moves[move_dir], next_state) yield (current_state, current_value), action except StopIteration as stop: # unexpected end of file/lines raise EOFError() from stop
def rules_from_lines(lines: Iterable[str]) -> dict[int, Rule]: rules: dict[int, Rule] = {} rule_refs: dict[int, list[list[int]]] = {} for line in lines: if '"' in line: # 1: "a" number_str, text = parse_line(line.strip(), '$: "$"') rule = RuleText(int(number_str), text) rules[rule.number] = rule else: # 2: 1 3 # 6: 12 # 8: 14 | 15 # 10: 2 4 | 8 16 number_str, rdef = parse_line(line.strip(), '$: $') rule_refs[int(number_str)] = [[ int(v) for v in group_text.split(' ') ] for group_text in rdef.split(' | ')] while rule_refs: number, groups = next( (number, groups) for number, groups in rule_refs.items() if all(num in rules for group in groups for num in group)) referenced_rules = [[rules[num] for num in group] for group in groups] rules[number] = RuleGroups(number, referenced_rules) del rule_refs[number] return rules
def from_lines(cls, name: str, lines: Iterable[str]) -> 'Character': lines = (line.strip() for line in lines) hit_points, = parse_line(next(lines), "Hit Points: $") damage, = parse_line(next(lines), "Damage: $") armor, = parse_line(next(lines), "Armor: $") return cls(name, hit_points=int(hit_points), damage=int(damage), armor=int(armor))
def from_str(cls, line: str) -> 'Event': # [1518-11-01 00:00] Guard #10 begins shift # [1518-11-01 00:05] falls asleep ts_str, action = parse_line(line, "[$] $") time = Timestamp.from_str(ts_str) if action.startswith("Guard #"): guard_id, what = parse_line(action, "Guard #$ $") return cls(time, int(guard_id), what) else: return cls(time, None, action)
def instructions_from_lines(): for line in lines: line = line.strip() if line.startswith('mask'): (mask_string,) = parse_line(line, 'mask = $') yield 'mask', BitMask(mask_string) elif line.startswith('mem'): address, value = parse_line(line, 'mem[$] = $') yield 'mem', int(address), int(value) else: raise ValueError(line)
def load_walls(fn: str) -> Iterable[Pos]: with open(fn) as file: for line in file: if line.startswith('x='): # x=651, y=334..355 x, y1, y2 = (int(v) for v in parse_line(line, 'x=$, y=$..$\n')) assert y1 <= y2 yield from ((x, y) for y in range(y1, y2 + 1)) elif line.startswith('y='): # y=1708, x=505..523 y, x1, x2 = (int(v) for v in parse_line(line, 'y=$, x=$..$\n')) assert x1 <= x2 yield from ((x, y) for x in range(x1, x2 + 1)) else: raise ValueError(line)
def roads_from_lines(lines: Iterable[str]) -> Iterable[Road]: # "London to Dublin = 464" for line in lines: place_1, place_2, distance_str = parse_line(line.strip(), "$ to $ = $") distance = int(distance_str) yield (place_1, place_2), distance yield (place_2, place_1), distance
def coordinates_from_text(text: str) -> tuple[int, int]: row, col = parse_line( text.strip(), "To continue, please consult the code grid in the manual. " "Enter the code at row $, column $.") return int(row), int(col)
def from_str(cls, line: str) -> 'Rotation': s_x, s_y, s_z = parse_line(line, 'x->$, y->$, z->$') return cls( x_to=cls.axis_from_str(s_x), y_to=cls.axis_from_str(s_y), z_to=cls.axis_from_str(s_z) )
def from_lines(cls, lines: Iterable[str]) -> 'Tower': # name -> weight, names of subtowers protos: dict[str, tuple[int, list[str]]] = {} # name -> parent (tower it stands on) depends_on: dict[str, str] = {} for line in lines: line = line.strip() # enurd (528) -> gpaljor, ncuksjv, ozdrm, qkmsfo name, weight, children_part = parse_line(line, "$ ($)$") children = children_part.removeprefix(' -> ').split(', ') if children_part else [] protos[name] = (int(weight), children) for child in children: depends_on[child] = name def find_root_tower_name(dependences: dict[str, str]) -> str: # take any name ... key = next(iter(dependences.keys())) # ... and traverse to parents until you reach the root (bottom) tower, while key in dependences: key = dependences[key] # ... which is not dependent on any other tower return key return cls._from_protos(find_root_tower_name(depends_on), protos)
def from_str(cls, line: str) -> 'Claim': id_, left, top, width, height = parse_line(line, "#$ @ $,$: $x$") return cls(id_=int(id_), top=int(top), left=int(left), width=int(width), height=int(height))
def data_from_lines( lines: Iterable[str]) -> Iterable[tuple[PasswordRule, str]]: for line in lines: # 1-3 b: cdefg min_count, max_count, character, password = parse_line( line.strip(), "$-$ $: $") yield PasswordRule(int(min_count), int(max_count), character), password
def from_str(cls, line: str) -> 'Reindeer': # 'Vixen can fly 19 km/s for 7 seconds, but then must rest for 124 seconds.' args = parse_line( line, "$ can fly $ km/s for $ seconds, but then must rest for $ seconds." ) # pylint: disable=no-value-for-parameter return cls(*args)
def command(self, cmd: str) -> None: # rect 2x1 if cmd.startswith("rect "): rect_width, rect_height = parse_line(cmd, "rect $x$") self.rect(int(rect_width), int(rect_height)) # rotate row y=0 by 3 elif cmd.startswith("rotate row"): row_y, right = parse_line(cmd, "rotate row y=$ by $") self.rotate_row(int(row_y), int(right)) # rotate column x=8 by 2 elif cmd.startswith("rotate column"): column_x, down = parse_line(cmd, "rotate column x=$ by $") self.rotate_column(int(column_x), int(down)) else: raise ValueError(cmd)
def discs_from_lines(lines: Iterable[str]) -> Iterable[Disc]: # "Disc #1 has 5 positions; at time=0, it is at position 4." for line_no, line in enumerate(lines): disc_no, size, pos0 = parse_line( line=line.strip(), pattern="Disc #$ has $ positions; at time=0, it is at position $." ) assert line_no + 1 == int(disc_no) yield int(pos0), int(size)
def move_from_str(line: str) -> Move: match line[0]: case 's': # s1 length, = parse_line(line, "s$") return Spin(int(length)) case 'x': # x3/4 pos_1, pos_2 = parse_line(line, "x$/$") return Exchange(int(pos_1), int(pos_2)) case 'p': # pe/b dancer_1, dancer_2 = parse_line(line, "p$/$") return Partner(dancer_1, dancer_2) case _: raise ValueError(line) # TODO: remove when mypy realizes this is unreachable assert False
def input_from_lines(lines: Iterable[str]) -> tuple[State, Rules]: lines_it = (line.strip() for line in lines) # initial state initial_state_str, = parse_line(next(lines_it), "initial state: $") initial_state = State.from_line(initial_state_str) # empty line assert not next(lines_it) # rules rules = {rule[0] for line in lines_it if (rule := rule_from_line(line))[1]} return initial_state, rules
def rule_from_str(line: str) -> Rule: # 'Alice would gain 54 happiness units by sitting next to Bob.' # 'Alice would lose 79 happiness units by sitting next to Carol.' name1, verb, amount_str, name2 = parse_line( line, pattern="$ would $ $ happiness units by sitting next to $.") amount = int(amount_str) assert verb in ("lose", "gain") if verb == "lose": amount = -amount return (name1, name2), amount
def from_str(cls, line: str) -> 'Node': # '/dev/grid/node-x0-y2 85T 73T 12T 85%' name, size_str, used_str, avail_str, _ = [p for p in line.strip().split(' ') if p] x, y = parse_line(name, "/dev/grid/node-x$-y$") pos = (int(x), int(y)) size = int(size_str.rstrip('T')) used = int(used_str.rstrip('T')) avail = int(avail_str.rstrip('T')) assert size == used + avail return Node(pos, size, used)
def from_str(cls, line: str) -> 'Command': line = line.strip() if line.startswith("swap position "): # swap position 4 with position 0 pos_1, pos_2 = parse_line(line, "swap position $ with position $") return cls('swap_pos', int(pos_1), int(pos_2)) elif line.startswith("swap letter "): # swap letter d with letter b letter_1, letter_2 = parse_line(line, "swap letter $ with letter $") return cls('swap_letter', letter_1, letter_2) elif line.startswith("rotate left ") or line.startswith("rotate right "): # rotate left 1 step direction, steps, _ = parse_line(line, "rotate $ $ step$") return cls('rot_steps', direction, int(steps)) elif line.startswith("rotate based "): # rotate based on position of letter b letter, = parse_line(line, "rotate based on position of letter $") return cls('rot_letter', letter) elif line.startswith("reverse positions "): # reverse positions 0 through 4 pos_1, pos_2 = parse_line(line, "reverse positions $ through $") return cls('reverse', int(pos_1), int(pos_2)) elif line.startswith("move position "): # move position 1 to position 4 pos_1, pos_2 = parse_line(line, "move position $ to position $") return cls('move', int(pos_1), int(pos_2)) else: raise ValueError(line)
def from_str(cls, line: str) -> 'Cuboid': def parse_edge(edge: str) -> tuple[int, int]: if '..' in edge: left, right = edge.split('..') return int(left), int(right) else: return int(edge), int(edge) e_x, e_y, e_z = parse_line(line.strip(), 'x=$,y=$,z=$') x_0, x_1 = parse_edge(e_x) y_0, y_1 = parse_edge(e_y) z_0, z_1 = parse_edge(e_z) return cls(corner_0=(x_0, y_0, z_0), corner_1=(x_1, y_1, z_1))
def from_lines(cls, lines: Iterable[str]) -> 'Machine': lines_it = (line.strip() for line in lines) init_state, = parse_line(next(lines_it), "Begin in state $.") steps, = parse_line(next(lines_it), "Perform a diagnostic checksum after $ steps.") def rules() -> Iterable[Rule]: moves = {'left': -1, 'right': +1} while True: try: empty_line = next(lines_it) assert not empty_line except StopIteration: # all rules read return try: current_state, = parse_line(next(lines_it), "In state $:") for current_value in range(2): assert next( lines_it ) == f"If the current value is {current_value}:" write, = parse_line(next(lines_it), "- Write the value $.") move_dir, = parse_line(next(lines_it), "- Move one slot to the $.") next_state, = parse_line(next(lines_it), "- Continue with state $.") action = Action(int(write), moves[move_dir], next_state) yield (current_state, current_value), action except StopIteration as stop: # unexpected end of file/lines raise EOFError() from stop return cls(init_state=init_state, steps=int(steps), rules=rules())
def instructions_from_lines(lines: Iterable[str]) -> Instructions: # 'value 5 goes to bot 2' # 'bot 2 gives low to bot 1 and high to bot 0' init: MutableState = defaultdict(list) comparisons: Comparisons = {} for line in lines: line = line.strip() if "goes to" in line: value, target = parse_line(line, "value $ goes to $") init[target].append(int(value)) elif "gives low" in line: bot, low, high = parse_line(line, "$ gives low to $ and high to $") assert bot.startswith("bot ") assert bot not in comparisons comparisons[bot] = (low, high) else: raise ValueError(line) return frozen_state(init), comparisons
def parse_rule(text: str) -> Rule: """ >>> parse_rule("light red bags contain 1 bright white bag, 2 muted yellow bags.") ('light red', [(1, 'bright white'), (2, 'muted yellow')]) >>> parse_rule("bright white bags contain 1 shiny gold bag.") ('bright white', [(1, 'shiny gold')]) >>> parse_rule("faded blue bags contain no other bags.") ('faded blue', []) """ color, inside_text = parse_line(text.strip(), "$ bags contain $.") if inside_text != "no other bags": inside = [parse_count_color(part) for part in inside_text.split(", ")] else: inside = [] return color, inside
def from_line(cls, line: str): # mxmxvkd kfcds sqjhc nhms (contains dairy, fish) ingredients, allergens = parse_line(line, "$ (contains $)") return cls(ingredients.split(" "), allergens.split(", "))
def parse_line(cls, line: str) -> 'Instruction': # fold along x=655 axis, value = parse_line(line, 'fold along $=$') return cls(**{axis: int(value)})
def from_str(cls, line: str) -> 'Instruction': phrases = ('turn on', 'turn off', 'toggle') phrase = next(p for p in phrases if line.startswith(p + ' ')) rect_str = line[len(phrase):] x1, y1, x2, y2 = parse_line(rect_str, '$,$ through $,$') return cls(phrase, Rect((int(x1), int(y1)), (int(x2), int(y2))))
def from_str(cls, line: str) -> 'Rule': # "../.# => ##./#../..." g_from, g_to = parse_line(line.strip(), "$ => $") return cls(Grid.from_line(g_from), Grid.from_line(g_to))
def parse_day_number(fn: str) -> int: num, _ = parse_line(fn, "day$_$.py") return int(num)