+175
python/oct10/level0/odometer.py
+175
python/oct10/level0/odometer.py
···
1
+
from typing import List, Optional
2
+
3
+
4
+
class Odometer:
5
+
"""An odometer where all digits must be in ascending order"""
6
+
7
+
miles: int
8
+
9
+
@staticmethod
10
+
def _is_error(miles: int) -> bool:
11
+
"""Return True if the given miles are not in ascending order or contain a zero."""
12
+
try:
13
+
Odometer(miles)
14
+
except ValueError:
15
+
return True
16
+
return False
17
+
18
+
@staticmethod
19
+
def possible_values(odo_len: int) -> List[int]:
20
+
"""Return a list of possible values for the given miles."""
21
+
min = Odometer._get_minimum(odo_len)
22
+
max = Odometer._get_maximum(odo_len)
23
+
return [i for i in range(min, max + 1) if not Odometer._is_error(i)]
24
+
25
+
@staticmethod
26
+
def _ascending(digits: List[int]) -> bool:
27
+
return all(digits[i] < digits[i + 1] for i in range(len(digits) - 1))
28
+
29
+
@staticmethod
30
+
def _get_digits(miles: int) -> List[int]:
31
+
"""Return a list of digits from the given miles."""
32
+
return list(map(int, str(miles)))
33
+
34
+
@staticmethod
35
+
def _verify_miles(miles: int) -> None:
36
+
"""Verify that the given miles are in ascending order and do not contain a zero. Throws ValueError if not."""
37
+
digits = Odometer._get_digits(miles)
38
+
if len(digits) > 9:
39
+
raise ValueError("Miles cannot be greater than 9 digits")
40
+
if not Odometer._ascending(digits):
41
+
raise ValueError("Miles must be in ascending order")
42
+
if any(d == 0 for d in digits):
43
+
raise ValueError("Miles cannot contain a zero")
44
+
45
+
@staticmethod
46
+
def _get_maximum(odo_len: int) -> int:
47
+
"""Return the maximum possible miles given the current odometer length."""
48
+
return int("".join([str(i) for i in range(10 - odo_len, 10)]))
49
+
50
+
@staticmethod
51
+
def _get_minimum(odo_len: int) -> int:
52
+
"""Return the minimum possible miles given the current odometer length."""
53
+
return int("".join([str(i) for i in range(1, odo_len + 1)]))
54
+
55
+
def __init__(self, starting_miles: int = 1) -> None:
56
+
"""Initialize an odometer with the given starting miles."""
57
+
if starting_miles < 0:
58
+
raise ValueError("Starting miles cannot be negative")
59
+
Odometer._verify_miles(starting_miles)
60
+
self.miles = starting_miles
61
+
62
+
63
+
def verify_miles(self) -> bool:
64
+
"""Verify that the odometer's miles are in ascending order and do not contain a zero."""
65
+
try:
66
+
Odometer._verify_miles(self.miles)
67
+
except ValueError:
68
+
return False
69
+
return True
70
+
71
+
def get_digits(self) -> List[int]:
72
+
"""Return the odometer's miles as a list of digits."""
73
+
return list(map(int, str(self.miles)))
74
+
75
+
def get_minimum(self) -> int:
76
+
"""Return the minimum valid odometer reading."""
77
+
return Odometer._get_minimum(len(self.get_digits()))
78
+
79
+
def get_maximum(self) -> int:
80
+
"""Return the maximum valid odometer reading."""
81
+
return Odometer._get_maximum(len(self.get_digits()))
82
+
83
+
def next_reading(self) -> Optional[int]:
84
+
"""Return the next valid odometer reading."""
85
+
first_run = True
86
+
if self.miles == self.get_maximum():
87
+
return None
88
+
while not self.verify_miles() or first_run:
89
+
first_run = False
90
+
self.miles += 1
91
+
return self.miles
92
+
93
+
def previous_reading(self) -> Optional[int]:
94
+
"""Return the previous valid odometer reading."""
95
+
first_run = True
96
+
if self.miles == self.get_minimum():
97
+
return None
98
+
while not self.verify_miles() or first_run:
99
+
first_run = False
100
+
self.miles -= 1
101
+
return self.miles
102
+
103
+
def nth_reading_after(self, n: int) -> Optional[int]:
104
+
"""Return the nth valid odometer reading."""
105
+
first_run = True
106
+
i = 0
107
+
while not self.verify_miles() or first_run or i < n:
108
+
if self.miles == self.get_maximum():
109
+
return None
110
+
first_run = False
111
+
self.miles += 1
112
+
if self.verify_miles():
113
+
i += 1
114
+
return self.miles
115
+
116
+
def nth_reading_before(self, n: int) -> Optional[int]:
117
+
"""Return the nth valid odometer reading before the current one."""
118
+
first_run = True
119
+
i = 0
120
+
while not self.verify_miles() or first_run or i < n:
121
+
if self.miles == self.get_minimum():
122
+
return None
123
+
first_run = False
124
+
self.miles -= 1
125
+
if self.verify_miles():
126
+
i += 1
127
+
return self.miles
128
+
129
+
@staticmethod
130
+
def print(start: Optional[int], end) -> None:
131
+
"""Print the start of the odometer with some other value."""
132
+
print(f"{start} -> {end}")
133
+
134
+
@staticmethod
135
+
def _distance(start: int, end: int) -> int:
136
+
"""Calculate the distance between two odometer readings."""
137
+
odo_len = len(Odometer._get_digits(start))
138
+
if odo_len != len(Odometer._get_digits(end)):
139
+
raise ValueError("Odometer readings must have the same number of digits")
140
+
n = 0
141
+
while start != end:
142
+
try:
143
+
Odometer._verify_miles(start)
144
+
n += 1
145
+
if start == Odometer._get_maximum(odo_len):
146
+
start = Odometer._get_minimum(odo_len)
147
+
else:
148
+
start += 1
149
+
except:
150
+
start += 1
151
+
return n
152
+
153
+
def distance(self, end: int) -> Optional[int]:
154
+
"""Calculate the distance between the current odometer reading and another."""
155
+
try:
156
+
return Odometer._distance(self.miles, end)
157
+
except:
158
+
return None
159
+
160
+
odometer = Odometer(2467)
161
+
Odometer.print(odometer.miles, odometer.get_minimum())
162
+
Odometer.print(odometer.miles, odometer.get_maximum())
163
+
Odometer.print(odometer.miles, odometer.nth_reading_after(6))
164
+
Odometer.print(odometer.miles, odometer.nth_reading_before(6))
165
+
Odometer.print(odometer.miles, odometer.next_reading())
166
+
Odometer.print(odometer.miles, odometer.previous_reading())
167
+
Odometer.print(odometer.miles, f"1234: {odometer.distance(1234)}")
168
+
169
+
odo2 = Odometer(123)
170
+
Odometer.print(odo2.miles, odo2.get_minimum())
171
+
Odometer.print(odo2.miles, odo2.get_maximum())
172
+
Odometer.print(odo2.miles, odo2.nth_reading_after(83))
173
+
Odometer.print(odo2.miles, f"123: {odo2.distance(123)}")
174
+
175
+
print(Odometer.possible_values(8))