My Advent of Code solutions in Python.
kevinyap.ca/2019/12/going-fast-in-advent-of-code/
advent-of-code
python
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4import os
5import re
6import sys
7import glob
8import argparse
9import resource
10import subprocess
11
12try:
13 from halo import Halo
14except ImportError:
15 # Use a noop context manager if Halo isn't installed
16 from contextlib import contextmanager
17
18 @contextmanager
19 def Halo(text):
20 yield
21
22
23class bcolors:
24 HEADER = '\033[95m'
25 OKBLUE = '\033[94m'
26 OKGREEN = '\033[92m'
27 WARNING = '\033[93m'
28 FAIL = '\033[91m'
29 ENDC = '\033[0m'
30
31
32def clock():
33 return resource.getrusage(resource.RUSAGE_CHILDREN)[0]
34
35
36def color_time_str(str, timespan):
37 if abs(timespan) >= 10:
38 color = bcolors.FAIL
39 elif abs(timespan) >= 1:
40 color = bcolors.WARNING
41 else:
42 color = ''
43
44 return '{}{}{}'.format(color, str, bcolors.ENDC)
45
46
47def format_time(timespan, padding=None):
48 """Formats the timespan in a human readable format"""
49 if timespan >= 1.0:
50 time_str = '{:.3g} s'.format(timespan)
51 else:
52 time_str = '{:.3g} ms'.format(timespan * 1e3)
53
54 if padding is not None:
55 time_str = time_str.rjust(padding)
56
57 return color_time_str(time_str, timespan)
58
59
60def check_solution(program, day, input_file, output_file, pypy=False):
61 with Halo(text='Day {:02}'.format(day)):
62 cmd = ['pypy' if pypy else 'python', program, input_file]
63 start = clock()
64 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
65 stdout = proc.communicate()[0]
66 end = clock()
67 cpu_usr = end - start
68
69 valid = True
70
71 with open(output_file) as f:
72 for line in f:
73 if line.strip() not in stdout:
74 valid = False
75 break
76
77 return valid, stdout, cpu_usr
78
79
80if __name__ == '__main__':
81 parser = argparse.ArgumentParser(description="Advent of Code puzzle runner.")
82 parser.add_argument('year', type=int)
83 parser.add_argument('puzzles', type=int, metavar='puzzle', nargs='*')
84 parser.add_argument('--pypy', const=True, action='store_const',
85 help="use PyPy instead of CPython")
86 parser.add_argument('--benchmark', const=True, action='store_const',
87 help="compare PyPy against CPython")
88
89 args = parser.parse_args()
90
91 year = args.year
92 puzzles = args.puzzles
93 pypy = args.pypy
94
95 if puzzles:
96 programs = []
97 for p in puzzles:
98 programs.extend(glob.glob('%s/day%02i.py' % (year, p)))
99 else:
100 programs = glob.glob('%s/day*.py' % year)
101
102 to_run = []
103
104 for program in sorted(programs):
105 try:
106 day = int(re.findall(r'(\d+).py', program)[0])
107 except IndexError:
108 continue
109 input_file = '%s/inputs/%02i.txt' % (year, day)
110 output_file = '%s/outputs/%02i.txt' % (year, day)
111
112 if os.path.exists(output_file):
113 to_run.append((program, day, input_file, output_file))
114
115 if args.benchmark:
116 print "Day CPython PyPy Delta Speedup"
117 print '-' * 45
118
119 for program, day, input_file, output_file in to_run:
120 cpy_time = check_solution(program, day, input_file, output_file, pypy=False)[2]
121 pypy_time = check_solution(program, day, input_file, output_file, pypy=True)[2]
122
123 print "{:02} {} {} {} {:0.1f}".format(
124 day,
125 format_time(cpy_time, padding=7),
126 format_time(pypy_time, padding=7),
127 format_time(cpy_time - pypy_time, padding=9),
128 cpy_time / pypy_time,
129 )
130
131 sys.exit(0)
132
133 exit_code = 0
134 runtimes = []
135
136 for program, day, input_file, output_file in to_run:
137 valid, stdout, cpu_usr = check_solution(program, day, input_file, output_file, pypy)
138 runtimes.append(cpu_usr)
139
140 print '{}{}{} Day {:02} ({})'.format(
141 bcolors.OKGREEN if valid else bcolors.FAIL,
142 '✓' if valid else '✗',
143 bcolors.ENDC,
144 day,
145 format_time(cpu_usr),
146 )
147 print stdout
148
149 if not valid:
150 exit_code = 1
151
152 if len(puzzles) != 1:
153 print "Total runtime:", format_time(sum(runtimes))
154
155 cutoffs = [
156 0.025, 0.050, 0.075, 0.100, 0.125, 0.150,
157 0.200, 0.250, 0.300, 0.400, 0.500, 0.750,
158 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5,
159 ]
160
161 cutoffs.extend(range(5, 10))
162 cutoffs.extend(range(10, 20, 2))
163 cutoffs.extend(range(20, 120, 3))
164
165 for day, runtime in enumerate(runtimes, start=1):
166 out = "Day {:02}: {}".format(day, format_time(runtime, padding=7))
167 bar_len = next(i + 1 for i, cutoff in enumerate(cutoffs) if runtime < cutoff)
168 print out, color_time_str('■' * bar_len, runtime)
169
170 sys.exit(exit_code)