#!/usr/bin/env python3 import importlib import os import re import pathlib import traceback from argparse import ArgumentParser PATH = pathlib.Path(__file__).parent INPUT_PATH = PATH / 'input' class Tests(list): """ Test cases class. A test case is a tuple of the form (puzzle_input, partI_result, partII_result). """ def add(self, test_input, partI=None, partII=None): """Add a test case""" self.append((str(test_input), partI, partII)) def testf(tests, f, part): """ Tests a function f on test cases tests and compares the result to test_case[part]. The test is not run if test_case[part] == None. """ tests = [(t[0], t[part]) for t in tests if t[part] != None] if tests == []: return "No tests" for i, (case, sol) in enumerate(tests): if sol == None: continue r = f(case) if r == sol: continue return "Failed on test {}. Expected {}. Got {}".format(i+1, sol, r) return "OK" class Solver: """ Solver class. A solver is a module with functions partI and partII, an optional preprocessing function preproc, and an optional variable tests of type Tests. """ def __init__(self, mod): """Initilizes the solver instance of module mod.""" def unpack(name, default): """Returns mod.name if it exists and default otherwise.""" return getattr(mod, name) if name in dir(mod) else default dn_pattern = re.compile("\d\d?") self.preproc = unpack('preproc', NotImplemented) self.tests = unpack('tests', []) self.partI = getattr(mod, 'partI') self.partII = getattr(mod, 'partII') def solve(self, puzzle_input): """Solves puzzle input and returns pair of solutions""" puzzle_input = puzzle_input.rstrip() if self.preproc != NotImplemented: puzzle_input = self.preproc(puzzle_input) ptI = self.partI(puzzle_input) ptII = self.partII(puzzle_input) return ptI, ptII def test(self): """Tests the solver and returns pair of test results.""" tests = self.tests if self.preproc != NotImplemented: tests = [(self.preproc(str(t)), s1, s2) for (t, s1, s2) in tests] ptI = testf(tests, self.partI, 1) ptII = testf(tests, self.partII, 2) return ptI, ptII return r parser = ArgumentParser() parser.description = \ """Solves Advent of Code 2019 puzzle of day {}. The puzzle is available at https://adventofcode.com/2019/day/{}""" parser.add_argument('-d', '--day', action='store', type=str, metavar='DAY[-DAY]', help='solve puzzle of day DAY or of range DAY-DAY') parser.add_argument('-i', '--input', action='store', type=str, metavar='INPUT', default=INPUT_PATH, help='use INPUT as puzzle input') parser.add_argument('-t', '--test', action='store_true', help="run tests") def parse_day_arg(s, mind, maxd): """Parses day argument. Returns a list of days.""" split = s.split('-') if len(split) == 1: return [int(split[0])] elif len(split) == 2: d, D = map(int, split) return list(range(max(1, d), min(D+1, 26))) raise ValueError def main(): solver_pattern = re.compile("day(\d\d?).py") matches = map(solver_pattern.fullmatch, os.listdir(PATH)) solvers = {int(s.group(1)) : s.group(0) for s in matches if s != None} mind, maxd = min(solvers), max(solvers) days = sorted(solvers.keys()) args = parser.parse_args() #Handle day argument if args.day != None: try: days = parse_day_arg(args.day, mind, maxd) except ValueError: return 'invalid format of argument -d/--day' #Handle input argument inputs = {} input_path = pathlib.Path('.') num_pattern = re.compile(r'\d\d?') try: fs = os.listdir(args.input) input_path /= args.input for fn in fs: ns = num_pattern.findall(fn) if ns == []: print('warning : ignoring input file {}'.format(fn)) continue n = int(ns[0]) inputs[n] = fn except FileNotFoundError: return "input directory or file doesn't exist" except NotADirectoryError: if len(days) > 1: return "input is file" day = days[0] inputs[day] = args.input #Solve days for d in days: print("=" * 6 + " Day {} ".format(d) + "=" * 6) if d not in solvers: return "no solver for day {}".format(d) mod_name = solvers[d][:-3] try: mod = importlib.import_module(mod_name) solver = Solver(mod) except: return "bad solver\n\n{}".format(traceback.format_exc()) try: if args.test: ptI, ptII = solver.test() else: if d not in inputs: return 'no input file' with open(input_path / inputs[d]) as f: puzzle_input = f.read() ptI, ptII = solver.solve(puzzle_input) print("Part One : {}".format(ptI)) print("Part Two : {}".format(ptII)) except: return "error in solver\n\n{}".format(traceback.format_exc()) if __name__ == '__main__': error = main() if error != None: print('error : {}'.format(error))