Задача №4

Створити клас TBankomat, який моделює роботу банкомата. Клас повинен містити поля для зберігання кількості купюр кожного із номіналів від 5 до 200 гривень. Реалізувати методи знаходження максимальної та мінімальної сум, які може видати банкомат, та метод зняття деякої суми.


Рішення:

'use strict'

class Bill {
  #worth
  #amount

  /**
   * @param {number} worth
   * @param {number} amount
   */
  constructor(worth, amount) {
    this.worth = worth
    this.amount = amount
  }

  get worth() {
    return this.#worth
  }

  set worth(newWorth) {
    if (newWorth <= 0) throw new Error("A bill can't be worthless.")

    this.#worth = newWorth
  }

  get amount() {
    return this.#amount
  }

  set amount(newAmount) {
    if (newAmount < 0) throw new Error("Amount of bills can't be negative.")

    this.#amount = newAmount
  }

  toString() {
    return `${this.worth}: ${this.amount}`
  }

  valueOf() {
    return this.worth * this.amount
  }
}

class TBankomat {
  /** @type {Bill[]} newBills */
  #bills

  /** @param {Bill[]} bills */
  constructor(bills) {
    // store an array of bills by worth in descending order
    this.bills = bills.sort((billA, billB) => billB.worth - billA.worth)
  }

  get bills() {
    return this.#bills
  }

  /** @param {Bill[]} newBills */
  set bills(newBills) {
    if (!newBills.every((bill) => bill instanceof Bill))
      throw new Error('A bill must be an instance of the Bill class.')

    this.#bills = newBills
  }

  get minimalSum() {
    const minBill = this.bills.findLast(({amount}) => amount > 0)

    return minBill ? minBill.worth : 0
  }

  get maximalSum() {
    return this.bills.reduce((sum, bill) => sum + Number(bill), 0)
  }

  /**
   * @param {number} toWithdraw
   */
  getRequiredBills(toWithdraw) {
    const bills = []

    for (const bill of this.bills) {
      if (toWithdraw === 0) break

      const {worth, amount} = bill
      const possibleAmount = Math.min(Math.floor(toWithdraw / worth), amount)

      if (possibleAmount) {
        bills.push({bill, possibleAmount})
        toWithdraw -= worth * possibleAmount
      }
    }

    return toWithdraw === 0 ? bills : []
  }

  /**
   * @param {number} toWithdraw
   */
  withdrawal(toWithdraw) {
    if (toWithdraw <= 0)
      throw new Error('You can only withdraw positive amounts.')
    if (toWithdraw < this.minimalSum)
      throw new Error(`Minimum withdrawal amount is: $${this.minimalSum}.`)
    if (toWithdraw > this.maximalSum)
      throw new Error(
        `Insufficient funds. Maximum available: $${this.maximalSum}.`,
      )

    const billsToWithdraw = this.getRequiredBills(toWithdraw)

    if (billsToWithdraw.length === 0)
      throw new Error(`Cannot dispense ${toWithdraw}. Try a different amount.`)

    let log = ''

    for (const {bill, possibleAmount} of billsToWithdraw) {
      bill.amount -= possibleAmount
      log += `Withdrawn ${possibleAmount} x 💲${bill.worth}\n`
    }

    return log
  }

  toString() {
    return this.bills.reduce((group, bill) => group + String(bill) + '; ', '')
  }

  valueOf() {
    return this.maximalSum
  }
}

const billsList = [
  new Bill(5, 100),
  new Bill(10, 100),
  new Bill(20, 100),
  new Bill(50, 1000),
  new Bill(100, 1000),
  new Bill(200, 10000),
]

const personalBankATM = new TBankomat(billsList)

if (confirm('Почати тестування?')) {
  while (personalBankATM.minimalSum) {
    const userWants = prompt(
      `🏧 Personal Bank ATM 🏧\nAvailable sum: 💲${personalBankATM.maximalSum}\nEnter money amount:`,
      '0',
    )

    if (userWants === null) break

    const userWantAmount = parseInt(userWants)

    if (!isFinite(userWantAmount)) {
      alert('⚠️ Incorrect input. Please, enter a valid number.')
      continue
    }

    try {
      const log = personalBankATM.withdrawal(userWantAmount)
      alert(log)
    } catch (error) {
      alert('⚠️ ' + error)
    }
  }
}