-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbase.py
More file actions
323 lines (257 loc) · 9.74 KB
/
base.py
File metadata and controls
323 lines (257 loc) · 9.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
"""
This program contains:
**in this file**
- a base class LeetCodeProblem with utilities for testing
**in utils**
- a basic linked list implementation and conversion utility
- a function call caching decorator (obviously pure functions only)
- a python implementation of min heap
"""
import time
##
# Result tester decorator
def make_tester(store: dict, key: str = None):
"""this function saves the decoratee under key to dict store"""
def _decorator(decoratee):
if key is None:
name = decoratee.__name__
else:
name = key
store[name] = decoratee
return decoratee
return _decorator
class LeetCodeProblem:
_testers = {}
def __init__(self):
self.error_cases = []
self.error_count = 0
self.total_count = 0
self.time_elapsed = None
def get_tests(self) -> (()):
"""
# returns a tuple of test cases
# each test case has the following structure
# ( parameter , expected output ),
# # OR # #
# ( (param1, param2), expected output ),
"""
raise NotImplementedError
def solution(self, *args):
"""
step-by-step user guide:
>>see example.py for code example<<
1. inherent from this LeetCodeProblem class
2. override get_tests to return a tuple of test cases
3. override solution function with the body of your solution
4. make a new instance of your class and call instance.run()
5. (optional) instance.print_result can be called repeatedly, this can be useful in ipython (maybe)
"""
raise NotImplementedError
def print_result(self):
print(f"did {self.total_count} tests, {self.error_count} fails, time spent {self.time_elapsed :.2f} ms")
if self.error_count:
print("Input | Expected | Output")
print(*[f"{each_case} \n" for each_case in self.error_cases])
def run(self, verbose=False):
start_time = time.time()
for each_case in self.get_tests():
self.total_count += 1
params, expected = each_case
if verbose:
print("______________")
print(f"input : {params}")
# call the solution and obtain results
try:
if isinstance(params, tuple):
result = self.solution(*params)
else:
result = self.solution(params)
except RuntimeError:
self.error_count += 1
self.error_cases.append((params, expected, None))
continue
# run tester function and obtain stats
try:
try:
# resolve tester
__tester = self._testers[self._get_tester()]
__tester(self, expected, result)
except KeyError:
print(f"Invalid mode <{self.tester}> specified, printing results for manual inspection \n ______________")
print(expected, result)
self.error_count += 1
except AssertionError:
self.error_count += 1
self.error_cases.append((params, expected, result))
self.time_elapsed = (time.time() - start_time) * 1000
# final printout
self.print_result()
##
# Testers
# To alter the default behaviour,
# set self.tester to the key of a tester function decorated with @bind_to
def _get_tester(self):
try:
return self.tester
except AttributeError:
return "exact"
@make_tester(_testers, key="exact")
def exact_match(self, truth, result):
assert truth == result
@make_tester(_testers, key="any")
def one_to_many(self, truth, result):
if not isinstance(result, (list, tuple)):
raise TypeError("expected output must be a list or tuple")
assert result in truth
@make_tester(_testers, key="all")
def many_to_many(self, truth, result):
if not isinstance(result, (list, tuple)):
raise TypeError("expected output must be a list or tuple")
assert sorted(truth) == sorted(result)
@make_tester(_testers, key="linked_lists")
def linked_lists_equal(self, truth_head, result_head):
if not isinstance(truth_head, ListNode) or not isinstance(result_head, ListNode):
raise TypeError("linked list comparison can only take ListNode as arguments")
# iterate through both at the same time and compare
left = truth_head
right = result_head
while left is not None or right is not None:
if left is None or right is None:
raise AssertionError
else:
assert left.val == right.val
if left:
left = left.next
if right:
right = right.next
@make_tester(_testers, key="object")
def method_calls(self, expected: list, instance):
"""
in order to use this tester the solution must
1. take arguments for yourclass.init
2. return instance of your class
this tester requires specific test case structure:
`( (init_args, ) , [(method_name, *args,)], [results] ) `
note that expected must be a list, not a tuple
its sort of a hacky mess i know (:P)
"""
calls, expected_results = expected
# call each method specified and collect results in a list
working_results = [getattr(instance, each_call[0])(*each_call[1:]) for each_call in calls]
# this is a hideous hack !
# modifying expected to pass error data back
if working_results != expected_results:
expected[1] = working_results
expected[0] = expected_results
raise AssertionError
##
# linked list utility
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def __repr__(self):
return f"ListNode:{self.val}"
def to_linked_list(plist):
"""takes python list and return head of linked list"""
if len(plist) == 0:
return None
head = ListNode(plist[0])
tail = head
for val in plist[1:]:
current = ListNode(val)
tail.next = current
tail = current
return head
def to_plist(head):
"""scans from head onwards, returns python list"""
plist = [head.val]
current = head
while current.next:
plist.append(current.next.val)
current = current.next
return plist
##
# Caching utility
def cached(__cache: dict):
"""
cache returned values in __cache,
note that decoratee function can *ONLY* take positional args
"""
def _decorator(decoratee):
def _inner(*args):
try:
return __cache[args]
except KeyError:
result = decoratee(*args)
__cache[args] = result
return result
return _inner
return _decorator
##
# heap utility
class MinHeap:
def __init__(self, initial_array=None, known_heap=False):
if initial_array is None:
self.__array = []
else:
self.__array = initial_array.copy()
if not known_heap:
# build heap property from leaves up
for i in range(len(initial_array)):
self._min_heapify(len(initial_array) - 1 - i, down_heap=True)
def __len__(self):
return len(self.__array)
def min(self):
return self.__array[0]
def pop_min(self):
value = self.__array[0]
# pop end element and replace root, then restore heap property from root down
self.__array[0] = self.__array.pop()
self._min_heapify(0, down_heap=True)
return value
def push(self, value):
self.__array.append(value)
self._min_heapify(len(self.__array) - 1)
def _min_heapify(self, __index, down_heap=False):
parent = self.__array[__index]
left_i = __index * 2 + 1
left_child = self.__array[left_i] if left_i < self.__len__() else None
right_child = self.__array[left_i + 1] if left_i + 1 < self.__len__() else None
swap_index = None
if (left_child is not None and parent > left_child) or (right_child is not None and parent > right_child):
# heap property violated
# swap parent with smallest child
# the switch below is so long and repetitive
# TODO : find better way to resolve swap
if left_child is None:
swap_index = left_i + 1
swap_value = right_child
elif right_child is None:
swap_index = left_i
swap_value = left_child
# both children present, swap biggest
elif left_child < right_child:
swap_index = left_i
swap_value = left_child
else:
swap_index = left_i + 1
swap_value = right_child
# do the swap
self.__array[swap_index] = parent
self.__array[__index] = swap_value
if down_heap and swap_index:
self._min_heapify(swap_index, down_heap=True)
elif not down_heap and __index != 0:
self._min_heapify((__index - 1) // 2)
if __name__ == "__main__":
print("""
step-by-step user guide:
>>see example.py for code example<<
1. inherent from this LeetCodeProblem class
2. override get_tests to return a tuple of test cases
3. override solution function with the body of your solution
4. make a new instance of your class and call instance.run()
5. (optional) instance.print_result can be called repeatedly, this can be useful in ipython (maybe)
"""
)