# encoding: utf-8 ########################### # Advent of Code Day 1-1 # ########################### # Combination lock - rotary dial # Instructions come in the form `DN+`, # where D is a direction L or R, and N+ is a series of integers of # arbitrary length. (e.g. L32, R947) # The dial is 0-indexed and has 100 positions (0-99). # Rotating left decrements the index; rotating right increments it. # Rotating left from 0 goes to 99, and rotating right from 99 goes to 0 # (i.e. it wraps around). # Count the number of times the dial lands on 0 at the end of a move. # Instructions are in day1_input.txt. # I'm gonna overengineer this. import argparse import logging import os logger = logging.getLogger(__name__) sh = logging.StreamHandler() logger.addHandler(sh) logger.setLevel(logging.INFO) DEFAULTS = { "starting_position": 50, "num_positions": 100, "filename": "day1_input.txt" } class RotaryLock: def __init__(self, starting_position:int|None=None, num_positions:int|None=None): try: num_positions = int(num_positions) if num_positions is not None else DEFAULTS["num_positions"] except ValueError: logger.warning(f"Number of positions {num_positions} is not an integer. Setting to default ({DEFAULTS['num_positions']}).") num_positions = DEFAULTS["num_positions"] if num_positions <= 0: logger.warning(f"Number of positions {num_positions} is not positive. Setting to default ({DEFAULTS['num_positions']}).") num_positions = DEFAULTS["num_positions"] self.last_position = num_positions - 1 self.first_position = 0 try: starting_position = int(starting_position) if starting_position is not None else DEFAULTS["starting_position"] except ValueError: logger.warning(f"Starting position {starting_position} is not an integer. Setting to default ({DEFAULTS['starting_position']}).") starting_position = DEFAULTS["starting_position"] if self.first_position <= int(starting_position) <= self.last_position: self.current_position = (starting_position) else: logger.warning( f"Starting position {starting_position} falls outside of possible positions {self.first_position}-{self.last_position}. Setting to 0." ) self.current_position = 0 self.position_history = [] self.position_history.append(self.current_position) self.instructions = [] self.num_zeroes = 0 logger.debug(f"RotaryLock created!") logger.debug(self.__dict__) def read_instructions(self, filename:str|None=None) -> None: filename = filename if filename is not None else DEFAULTS["filename"] if not filename.startswith("/"): full_path = os.path.join(os.curdir, filename) else: full_path = filename if os.path.exists(full_path) and os.path.isfile(full_path): with open(full_path, "r") as f: lines = f.readlines() elif not os.path.exists(full_path): logger.error(f"File {full_path} does not exist.") return elif not os.path.isfile(full_path): logger.error(f"File {full_path} is not a file.") return self.instructions = [l.strip() for l in lines] def rotate_right(self) -> None: if self.current_position == self.last_position: self.current_position = self.first_position else: self.current_position += 1 def rotate_left(self) -> None: if self.current_position == self.first_position: self.current_position = self.last_position else: self.current_position -= 1 def rotate_lock(self, direction:str, magnitude:int) -> None: try: d = direction.lower() except AttributeError: logger.error(f"Supplied direction {direction} is not usable (must be a string).") return if d not in ("l","r"): logger.error(f"Supplied direction {direction} is not usable (must be L or R).") return try: n = int(magnitude) except ValueError: logger.error(f"Supplied magnitude {magnitude} is not an integer.") return for i in range(n): match d: case "r": self.rotate_right() case "l": self.rotate_left() case default: logger.error(f"Direction {d} was not recognized. (How did we get here?)") # if d == "r": # new_pos = self.current_position + n # if new_pos > self.last_position: # overflow = new_pos - (self.last_position + 1) # new_pos = overflow # else: # new_pos = self.current_position - n # if new_pos < 0: # overflow = n - (self.current_position + 1) # new_pos = self.last_position - overflow # self.current_position = new_pos logger.debug(f"After rotating {d.upper()} {n} places, current position is {self.current_position}.") def follow_instruction(self, instruction:str) -> None: try: d, n = instruction[0].lower(), int(instruction[1:]) except (AttributeError, ValueError, IndexError): logger.error(f"Instruction {instruction} is unreadable (format must be DN+).") return logger.debug(f"Following instruction to rotate {d} by {n} positions.") self.rotate_lock(direction=d, magnitude=n) if self.current_position == 0: logger.debug(f"Landed on 0.") self.num_zeroes += 1 def follow_instructions(self, instruction_list:list[str]|None=None) -> None: if instruction_list is None: self.read_instructions() instruction_list = self.instructions else: if len(self.instructions) == 0: logger.error(f"Instructions were not passed and have not been initialized.") return instruction_list = self.instructions try: instructions = [x for x in instruction_list] except TypeError: logger.error(f"Instruction list {instruction_list} is not an iterable.") tenths = len(instructions) // 10 logger.debug(f"Processing {len(instructions)} instructions.") logger.debug(f"Reporting every {tenths} instructions.") for i, instr in enumerate(instructions): if tenths > 0 and i % tenths == 0: logger.debug(f"Processing instruction {i}.") self.follow_instruction(instr) logger.info(f"Completed all instructions.") logger.info(f"Final position is {self.current_position}.") logger.info(f"Number of zeroes is {self.num_zeroes}.") def process_instructions(filename:str, start:int, num_pos:int): rlock = RotaryLock(starting_position=start, num_positions=num_pos) rlock.read_instructions(filename) rlock.follow_instructions() if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--filename", type=str, default=DEFAULTS["filename"], help="Full or local path to instruction file") parser.add_argument("--start", type=int, default=DEFAULTS["starting_position"], help="Starting position on the dial") parser.add_argument("--num_pos", type=int, default=DEFAULTS["num_positions"], help="Number of positions on the dial") parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging") args = parser.parse_args() if args.verbose: logger.setLevel(logging.DEBUG) process_instructions(args.filename, args.start, args.num_pos)