CMU Coding Bootcamp
1from typing import List, Optional
2
3
4class 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
160odometer = Odometer(2467)
161Odometer.print(odometer.miles, odometer.get_minimum())
162Odometer.print(odometer.miles, odometer.get_maximum())
163Odometer.print(odometer.miles, odometer.nth_reading_after(6))
164Odometer.print(odometer.miles, odometer.nth_reading_before(6))
165Odometer.print(odometer.miles, odometer.next_reading())
166Odometer.print(odometer.miles, odometer.previous_reading())
167Odometer.print(odometer.miles, f"1234: {odometer.distance(1234)}")
168
169odo2 = Odometer(123)
170Odometer.print(odo2.miles, odo2.get_minimum())
171Odometer.print(odo2.miles, odo2.get_maximum())
172Odometer.print(odo2.miles, odo2.nth_reading_after(83))
173Odometer.print(odo2.miles, f"123: {odo2.distance(123)}")
174
175print(Odometer.possible_values(8))