| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190 |
- # encoding: utf-8
-
- ###########################
- # Advent of Code Day 1-2 #
- ###########################
- # 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 ever sees 0.
- # 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 self.current_position == self.first_position:
- logger.debug(f"Saw a zero.")
- self.num_zeroes += 1
- # 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)
|