from typing import List, Optional class Odometer: """An odometer where all digits must be in ascending order""" miles: int @staticmethod def _is_error(miles: int) -> bool: """Return True if the given miles are not in ascending order or contain a zero.""" try: Odometer(miles) except ValueError: return True return False @staticmethod def possible_values(odo_len: int) -> List[int]: """Return a list of possible values for the given miles.""" min = Odometer._get_minimum(odo_len) max = Odometer._get_maximum(odo_len) return [i for i in range(min, max + 1) if not Odometer._is_error(i)] @staticmethod def _ascending(digits: List[int]) -> bool: return all(digits[i] < digits[i + 1] for i in range(len(digits) - 1)) @staticmethod def _get_digits(miles: int) -> List[int]: """Return a list of digits from the given miles.""" return list(map(int, str(miles))) @staticmethod def _verify_miles(miles: int) -> None: """Verify that the given miles are in ascending order and do not contain a zero. Throws ValueError if not.""" digits = Odometer._get_digits(miles) if len(digits) > 9: raise ValueError("Miles cannot be greater than 9 digits") if not Odometer._ascending(digits): raise ValueError("Miles must be in ascending order") if any(d == 0 for d in digits): raise ValueError("Miles cannot contain a zero") @staticmethod def _get_maximum(odo_len: int) -> int: """Return the maximum possible miles given the current odometer length.""" return int("".join([str(i) for i in range(10 - odo_len, 10)])) @staticmethod def _get_minimum(odo_len: int) -> int: """Return the minimum possible miles given the current odometer length.""" return int("".join([str(i) for i in range(1, odo_len + 1)])) def __init__(self, starting_miles: int = 1) -> None: """Initialize an odometer with the given starting miles.""" if starting_miles < 0: raise ValueError("Starting miles cannot be negative") Odometer._verify_miles(starting_miles) self.miles = starting_miles def verify_miles(self) -> bool: """Verify that the odometer's miles are in ascending order and do not contain a zero.""" try: Odometer._verify_miles(self.miles) except ValueError: return False return True def get_digits(self) -> List[int]: """Return the odometer's miles as a list of digits.""" return list(map(int, str(self.miles))) def get_minimum(self) -> int: """Return the minimum valid odometer reading.""" return Odometer._get_minimum(len(self.get_digits())) def get_maximum(self) -> int: """Return the maximum valid odometer reading.""" return Odometer._get_maximum(len(self.get_digits())) def next_reading(self) -> Optional[int]: """Return the next valid odometer reading.""" first_run = True if self.miles == self.get_maximum(): return None while not self.verify_miles() or first_run: first_run = False self.miles += 1 return self.miles def previous_reading(self) -> Optional[int]: """Return the previous valid odometer reading.""" first_run = True if self.miles == self.get_minimum(): return None while not self.verify_miles() or first_run: first_run = False self.miles -= 1 return self.miles def nth_reading_after(self, n: int) -> Optional[int]: """Return the nth valid odometer reading.""" first_run = True i = 0 while not self.verify_miles() or first_run or i < n: if self.miles == self.get_maximum(): return None first_run = False self.miles += 1 if self.verify_miles(): i += 1 return self.miles def nth_reading_before(self, n: int) -> Optional[int]: """Return the nth valid odometer reading before the current one.""" first_run = True i = 0 while not self.verify_miles() or first_run or i < n: if self.miles == self.get_minimum(): return None first_run = False self.miles -= 1 if self.verify_miles(): i += 1 return self.miles @staticmethod def print(start: Optional[int], end) -> None: """Print the start of the odometer with some other value.""" print(f"{start} -> {end}") @staticmethod def _distance(start: int, end: int) -> int: """Calculate the distance between two odometer readings.""" odo_len = len(Odometer._get_digits(start)) if odo_len != len(Odometer._get_digits(end)): raise ValueError("Odometer readings must have the same number of digits") n = 0 while start != end: try: Odometer._verify_miles(start) n += 1 if start == Odometer._get_maximum(odo_len): start = Odometer._get_minimum(odo_len) else: start += 1 except: start += 1 return n def distance(self, end: int) -> Optional[int]: """Calculate the distance between the current odometer reading and another.""" try: return Odometer._distance(self.miles, end) except: return None odometer = Odometer(2467) Odometer.print(odometer.miles, odometer.get_minimum()) Odometer.print(odometer.miles, odometer.get_maximum()) Odometer.print(odometer.miles, odometer.nth_reading_after(6)) Odometer.print(odometer.miles, odometer.nth_reading_before(6)) Odometer.print(odometer.miles, odometer.next_reading()) Odometer.print(odometer.miles, odometer.previous_reading()) Odometer.print(odometer.miles, f"1234: {odometer.distance(1234)}") odo2 = Odometer(123) Odometer.print(odo2.miles, odo2.get_minimum()) Odometer.print(odo2.miles, odo2.get_maximum()) Odometer.print(odo2.miles, odo2.nth_reading_after(83)) Odometer.print(odo2.miles, f"123: {odo2.distance(123)}") print(Odometer.possible_values(8))