Реализация банкомата с внесением и выдачей купюр-2

45. Реализация банкомата с внесением и выдачей купюр-2

Условие задачи:
Реализовать класс ““Банкомат”” Есть валюты: currencies = [RUB, USD, EUR] Монеты/купюры каждой валюты бывают ТОЛьКО такого номинала: nominal = [1, 5, 10, 50, 100, 500, 1000, 5000] Нужно разработать класс ““Банкомат”” со следующими возможностями: Принять в банкомат несколько монет/купюр: put(currency, nominal, count) -> None Выдать из банкомата определенную сумму денег в определенной валюте: get(currency, amount) -> [(nominal, count), …] | raise Unavailable Error Ответом должен быть список, каким образом банкомат выдаст нужную сумму, какими монетами/купюрами, в каком количестве. Банкомат обычный, сам деньги не печатает, может выдавать только то, что в него положили. Если выдать требуемую сумму нет возможности - бросаем исключение Unavailable Error.

Спойлеры к решению
Подсказки
  • Деньги нужно хранить отдельно по каждой валюте.
  • Для хранения удобно использовать вложенный словарь: currency -> nominal -> count.
  • Метод put() должен проверять валюту, номинал и количество.
  • Метод get() должен собрать нужную сумму только из доступных купюр/монет.
  • Выдачу можно собирать от большего номинала к меньшему.
  • Состояние банкомата нужно менять только после того, как сумма точно собрана.
  • Если сумму собрать нельзя, нужно выбросить UnavailableError.
Решение
class UnavailableError(Exception):
    pass


class ATM:
    currencies = {"RUB", "USD", "EUR"}
    nominals = [1, 5, 10, 50, 100, 500, 1000, 5000]

    def __init__(self):
        self.storage = {
            currency: {
                nominal: 0
                for nominal in self.nominals
            }
            for currency in self.currencies
        }

    def put(self, currency: str, nominal: int, count: int) -> None:
        self._validate_currency(currency)
        self._validate_nominal(nominal)

        if count <= 0:
            raise ValueError("count must be positive")

        self.storage[currency][nominal] += count

    def get(self, currency: str, amount: int) -> list[tuple[int, int]]:
        self._validate_currency(currency)

        if amount <= 0:
            raise ValueError("amount must be positive")

        result = []
        remaining = amount

        for nominal in sorted(self.nominals, reverse=True):
            available_count = self.storage[currency][nominal]

            if available_count == 0:
                continue

            need_count = remaining // nominal
            take_count = min(need_count, available_count)

            if take_count > 0:
                result.append((nominal, take_count))
                remaining -= nominal * take_count

        if remaining != 0:
            raise UnavailableError("requested amount is unavailable")

        for nominal, count in result:
            self.storage[currency][nominal] -= count

        return result

    def _validate_currency(self, currency: str) -> None:
        if currency not in self.currencies:
            raise ValueError("unsupported currency")

    def _validate_nominal(self, nominal: int) -> None:
        if nominal not in self.nominals:
            raise ValueError("unsupported nominal")

Пример использования:

atm = ATM()

atm.put("RUB", 1000, 2)
atm.put("RUB", 500, 3)
atm.put("RUB", 100, 10)

print(atm.get("RUB", 2600))

Результат:

[(1000, 2), (500, 1), (100, 1)]

После успешной выдачи купюры списываются из банкомата:

print(atm.storage["RUB"])

Если нужную сумму выдать невозможно:

atm = ATM()

atm.put("USD", 100, 1)

print(atm.get("USD", 150))

будет выброшено исключение:

UnavailableError: requested amount is unavailable

Потому что в банкомате есть только одна купюра 100 USD, а сумму 150 USD из доступных купюр собрать нельзя.

Главная логика метода get():

1. Проверяем валюту и сумму.
2. Идём по номиналам от большего к меньшему.
3. Берём столько купюр текущего номинала, сколько нужно и сколько доступно.
4. Уменьшаем остаток суммы.
5. Если остаток не равен 0, выдача невозможна.
6. Если сумма собрана, списываем купюры из хранилища.
7. Возвращаем список пар: (номинал, количество).

Важно: списание из storage происходит только после успешной сборки всей суммы. Поэтому если возникает UnavailableError, состояние банкомата не меняется.