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

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

Условие задачи:
Реализовать класс ““Банкомат”” Есть валюты: 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 = amount

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

            if available_count == 0:
                continue

            needed_count = remaining_amount // nominal
            count_to_take = min(available_count, needed_count)

            if count_to_take > 0:
                result.append((nominal, count_to_take))
                remaining_amount -= nominal * count_to_take

        if remaining_amount != 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"])

Например, после выдачи 2600 рублей в банкомате останется меньше купюр номиналом 1000, 500 и 100.

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

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. Уменьшаем остаток суммы.
6. Если остаток стал 0 — выдача возможна.
7. Если после прохода остаток не 0 — бросаем UnavailableError.
8. Только после успешной проверки списываем купюры из хранилища.

Важно, что списание происходит только после того, как сумма успешно собрана. Если сумму собрать нельзя, состояние банкомата не меняется.