Cash Register Project: my ~500 line solution

Rather a bit overkill I imagine, but here it is.

How can I make it more efficient?

Solution for Cash Register
JS

console.log(checkCashRegister(19.5, 20, [["PENNY", 0.5], ["NICKEL", 0], ["DIME", 0], ["QUARTER", 0], ["ONE", 0], ["FIVE", 0], ["TEN", 0], ["TWENTY", 0], ["ONE HUNDRED", 0]]))

function checkCashRegister(price, amountPaid, cashOnHand) {

  // define currency types in descending order of value
  const coinTypes = ["QUARTER", "DIME", "NICKEL", "PENNY"]
  const billTypes = ["ONE HUNDRED", "TWENTY", "TEN", "FIVE", "ONE"]

  // stipulate an exchange rate for each currency unit
  const exchangeRate = {
    PENNY: .01,
    NICKEL: .05,
    DIME: .1,
    QUARTER: .25,
    ONE: 1,
    FIVE: 5,
    TEN: 10,
    TWENTY: 20,
    'ONE HUNDRED': 100
  }


  // get num of each currency unit in 2 arrays
  const [coinArr, billArr] = parser(cashOnHand)

  // determine if exact change is available
  return solver(price, amountPaid, coinArr, billArr, cashOnHand)


  function coinCounter([QUARTERS, DIMES, NICKELS, PENNIES]) {

    const coinArr = [QUARTERS, DIMES, NICKELS, PENNIES]
    const exchangeRate = [25, 10, 5, 1]

    const coinSum = coinArr
      .map((numCoins, idx) => numCoins * exchangeRate[idx])
      .reduce((acc, cv) => acc + cv)

    const computeValue = sum => {
      const remainder = sum % 100;
      const quotient = (sum - remainder) / 100;
      return [quotient, remainder]
    }

    const [dollars, cents] = computeValue(coinSum)

    return [dollars, cents]
  }


  function getTotalValue(bills, coins) {

    const coinRate = [25, 10, 5, 1]
    const billRate = [100, 20, 10, 5, 1]

    const numCents = coins.reduce((acc, numOfCoin, idx) => acc + numOfCoin * coinRate[idx]) / 100
    const numDollars = bills.reduce((acc, numOfBill, idx) => acc + numOfBill * billRate[idx])

    // console.log("numCents", numCents)
    // console.log("numDollars", numDollars)

    return numCents + numDollars

  }

  function billCounter([HUNDREDS, TWENTIES, TENS, FIVERS, ONES]) {

    const billArr = [HUNDREDS, TWENTIES, TENS, FIVERS, ONES]
    const exchangeRate = [100, 20, 10, 5, 1]

    // console.log("billArr", billArr)
    const billSum = billArr
      .map((numBills, idx) => numBills * exchangeRate[idx])
      .reduce((acc, cv) => acc + cv)

    // because bills are a whole number, no need to run computeValue
    return billSum

    // console.log("billSum", billSum)
  }


  function getCoinMap(sortedUniques) {

    const dollarSet = [...new Set(sortedUniques
      .filter(x => x[0] >= 0)
      .map(x => x[0])
      .sort((a, b) => a - b)
    )]
    const getCentSet = dollars => [...new Set(sortedUniques
      .filter(x => x[0] === dollars)
      .map(x => x[1])
    )]
    const dollarKeys = dollarSet.map(d => {
      return [
        d,
        getCentSet(d)
      ]
    })

    const coinMap = Object.fromEntries(new Map(dollarKeys))

    return coinMap
  }


  function getIterations(iterNums, iterBase) {

    // Ported (with minor revisions) from the following SO answer:
    // https://stackoverflow.com/a/14001422/14347717

    const nums = iterNums
    // iterBase is optional; create an array of 0's if it isn't provided
    const base = iterBase ? iterBase : Array.from(Array(nums.length), () => 0)
    const iterations = []

    function step() {
      let incomplete = false
      for (let i = 0; i < base.length; i++) {
        incomplete = incomplete || (base[i] < nums[i])
        if (incomplete) break;
      }
      if (!incomplete) return false;

      for (let j = 0; j < base.length; j++) {
        if (base[j] < nums[j]) {
          base[j]++
          return true
        } else {
          base[j] = 0
          continue
        }
      }
    }

    let processing = true
    while (processing) {
      processing = step()
      if (processing) iterations.push([...base])
    }
    return iterations
  }


  function generateOutput(status, dollarChange = [], coinChange = [], cashOnHand) {

    // console.log("status", status)

    switch (status) {
      case "CLOSED":
        return {
          status,
          change: cashOnHand
        }
      case "OPEN":
        return {
          status,
          change: generateChange(dollarChange, coinChange)
        }
      case "INSUFFICIENT_FUNDS":
        return {
          status,
          change: []
        }

    }

    function generateChange(dollarChange, coinChange) {

      // console.log("dollarChange", dollarChange)
      // console.log("coinChange", coinChange)

      const changeArr = []

      billTypes.forEach((bill, idx) => {
        if (dollarChange[idx] > 0) {
          changeArr.push([
            getVariableName({ bill }),
            exchangeRate[bill] * dollarChange[idx]
          ])
        }
      })

      coinTypes.forEach((coin, idx) => {
        if (coinChange[idx] > 0) {
          changeArr.push([
            getVariableName({ coin }),
            exchangeRate[coin] * coinChange[idx]
          ])
        }
      })

      return changeArr

    }

  }


  function parser(cashOnHand) {

    // cashMap: returns object with each unit and total value of unit
    const cashMap = Object.fromEntries(cashOnHand)

    // create two arrays, for coins and bills, with num of units
    // matching the name of each unit in coinTypes/billTypes

    const coinArr = coinTypes.map((coinName) => {
      // wrap coinName in an object to work with getVariableName
      const coinType = getVariableName({ coinName })
      const coinSum = cashMap[coinType]
      const coinValue = exchangeRate[coinType]
      const numCoins = Math.round(coinSum / coinValue)

      return numCoins
    })

    const billArr = billTypes.map((billName) => {
      const billType = getVariableName({ billName })
      const billSum = cashMap[billType]
      const billValue = exchangeRate[billType]
      const numBills = billSum / billValue

      return numBills
    })

    // return `coinArr` and `billArr` once completed
    return [coinArr, billArr]


  }


  function solver(price, amountPaid, coinArr, billArr, cashOnHand) {

    // calculate amount of dollars and cents owed
    const changeOwed = amountPaid - price
    console.log(price)
    console.log(amountPaid)
    console.log(changeOwed)
    const [dollarsOwed, centsOwed] = calcChange(changeOwed)
    const exactAmountPaid = changeOwed === getTotalValue(billArr, coinArr)

    // GENERATE CLOSED OUTPUT IF EXACT PAYMENT PROVIDED
    if (exactAmountPaid) return generateOutput("CLOSED", 0, 0, cashOnHand)

    // GENERATE INSUFFICIENT FUNDS IF AMOUNT PAID IS LESS THAN PRICE
    if (changeOwed < 0) return generateOutput("INSUFFICIENT_FUNDS")

    // initialize solution variables
    let dollarCombos = []
    let coinCombos = []

    // intialized optimized solution variables
    let optimizedDollarChange = []
    let optimizedCoinChange = []

    // initialized solution boolean
    let hasExactChange = false

    // analyze coins. compute all unique coin permutations,
    // and store them in an object with dollar values as key
    const coinIterations = getIterations(coinArr)
    const allCoinTotals = sumCoinTotals(coinIterations)
    const uniqueCoinTotals = getUniqueTotals(allCoinTotals)

    // analyze bills. compute all unique permutations and store in an arr
    const billIterations = getIterations(billArr)
    const allBillTotals = sumBillTotals(billIterations)
    const uniqueBillTotals = getUniqueTotals(allBillTotals)
    // console.log("uniqueBillTotals", uniqueBillTotals)

    // sorting is probably not needed for solution;
    // using in development to make output easier to read
    const sortedUniqueCoinTotals = sortUniqueCoinTotals(uniqueCoinTotals)
    const sortedUniqueBillTotals = sortUniqueBillTotals(uniqueBillTotals)

    // console.log("solver > sortedUniqueBillTotals", sortedUniqueBillTotals)

    // generate a coin map showing cents available per dollar of change
    const coinMap = getCoinMap(sortedUniqueCoinTotals)
    // console.log("coinMap", coinMap)

    // determine whether it is possible to provide number of cents owed
    // per dollar of change available
    const exactChangePerDollarKey = computeCoinsAvailablePerDollarInterval(centsOwed, coinMap)
    const hasCorrectCoinsPotentially = exactChangePerDollarKey.length > 0

    // log whether correct coins are potentially available
    // console.log(
    //   "centsOwed:",
    //   `${centsOwed} -- has exact: `,
    //   hasCorrectCoinsPotentially
    // )

    // determine if exact bill change is available (not including from coins)
    const hasExactBills = sortedUniqueBillTotals.includes(dollarsOwed)
    // console.log(
    //   "dollarsOwed:",
    //   `${dollarsOwed} -- has exact:`,
    //   hasExactBills
    // )


    // if exact matches for both dollars and coins, generate optimized combo
    if (hasCorrectCoinsPotentially && hasExactBills) {

      // console.log("has exact for both!")

      const additionalDollarsNeeded = 0

      dollarCombos = filterBillTotals(
        billIterations,
        dollarsOwed
      )
      coinCombos = filterCoinTotals(
        coinIterations,
        additionalDollarsNeeded,
        centsOwed
      )

      optimizedDollarChange = dollarCombos[0]
      optimizedCoinChange = coinCombos[0]

      hasExactChange = true

      // console.log("optimizedDollarChange", optimizedDollarChange)
      // console.log("optimizedCoinChange", optimizedCoinChange)

    }

    // if no exact bills, check if bill-coin combination allows exact change
    else if (hasCorrectCoinsPotentially && !hasExactBills) {
      // console.log("checking if bill-coins are available...")

      // get max amt of dollars available as change
      const maxDollarChangeAvailable = sortedUniqueBillTotals
        .filter(x => x < dollarsOwed)
        .slice(-1)[0]

      // determine if matching change is available
      const additionalDollarsNeeded = dollarsOwed - maxDollarChangeAvailable
      const dollarCoinMatch = coinMap[additionalDollarsNeeded] || false
      // console.log("dollarCoinMatch", dollarCoinMatch)
      const hasDollarChange = dollarCoinMatch && dollarCoinMatch.includes(centsOwed)

      if (hasDollarChange) {

        // console.log("exact change is available!")

        dollarCombos = filterBillTotals(
          billIterations,
          maxDollarChangeAvailable
        )
        coinCombos = filterCoinTotals(
          coinIterations,
          additionalDollarsNeeded,
          centsOwed
        )

        optimizedDollarChange = dollarCombos[0]
        optimizedCoinChange = coinCombos[0]

        // console.log("optimizedDollarChange", optimizedDollarChange)
        // console.log("optimizedCoinChange", optimizedCoinChange)

        hasExactChange = true

      }

      // console.log("maxDollarChangeAvailable", maxDollarChangeAvailable)
      // console.log("additionalDollarsNeeded", additionalDollarsNeeded)
      // console.log("centsOwed", centsOwed)
      // console.log("dollarCoinMatch", dollarCoinMatch)
      // console.log("hasDollarChange", hasDollarChange)

    }

    // console.log("hasExactChange", hasExactChange)


    // RENDER OUTPUT

    if (hasExactChange) {
      return generateOutput(
        "OPEN",
        optimizedDollarChange,
        optimizedCoinChange
      )
    } else if (!hasExactChange) {
      return generateOutput("INSUFFICIENT_FUNDS")
    }
  }


  function sumCoinTotals(coinIterations) {
    return coinIterations.map(iteration => coinCounter(iteration))
  }


  function sumBillTotals(billIterations) {
    return billIterations.map(iteration => billCounter(iteration))
  }


  function filterBillTotals(billIterations, maxDollarChangeAvailable) {
    return billIterations
      .filter(iter => billCounter(iter) === maxDollarChangeAvailable)
      .sort((a, b) =>
        a.reduce((acc, cv) => acc + cv)
        - b.reduce((acc, cv) => acc + cv)
      )
  }


  function filterCoinTotals(coinIterations, dollarsNeeded, centsNeeded) {
    return coinIterations
      .filter(iter =>
        JSON.stringify(coinCounter(iter))
        === JSON.stringify([dollarsNeeded, centsNeeded])
      )
      .sort((a, b) =>
        a.reduce((acc, cv) => acc + cv)
        - b.reduce((acc, cv) => acc + cv)
      )
  }


  function getUniqueTotals(totalsArr) {
    // stringify arr values to enable equality comparison
    const stringArr = totalsArr.map(JSON.stringify)
    let uniqueStringArray = new Set(stringArr);
    let uniqueArray = Array.from(uniqueStringArray, JSON.parse);

    return [0].concat(uniqueArray)
  }


  function sortUniqueCoinTotals(coinArr) {
    const sortedUniques = coinArr.sort((a, b) => b[0] - a[0] || b[1] - a[1])

    return sortedUniques
  }


  function sortUniqueBillTotals(billArr) {
    const sortedUniques = billArr.sort((a, b) => a - b)

    // console.log("sortedUniques", sortedUniques)
    return sortedUniques
  }


  // filter thru 2D array of all unique dollar/coin combinations 
  // to find exact coin availability per dollar of change available
  function computeCoinsAvailablePerDollarInterval(centsOwed, coinMap) {
    const coinMapOE = Object.entries(coinMap)

    // uncomment to log all unique coin combination values:
    // console.log(coinMapOE)

    // return each dollar interval of coin combinations
    // that includes the number of exact cents owed
    return coinMapOE.filter(x => x[1].includes(centsOwed)).map(x => parseInt(x[0]))
  }


  // calculate the number of dollars and number of cents owed
  // return cents as an integer
  function calcChange(sum) {
    const remainder = sum % 1;
    const centsOwed = Math.round(remainder * 100)
    const dollarsOwed = sum - remainder;
    return [dollarsOwed, centsOwed]
  }


  // output the name of a variable passed into a function
  function getVariableName(v) {
    return Object.values(v)[0]

Nice work on that!

Your code is a pretty good example of what I’d call “overengineered”. I think that doing it this way was probably a really good exercise, but you can definitely go smaller.

One thing that I think stands out to me is that you are copying data around a lot more than you need to.

function checkCashRegister(price, cash, cid) {
  // Use cents to avoid roundoff
  const unitToCents = {
    "PENNY"       : 1,
    "NICKEL"      : 5,
    "DIME"        : 10,
    "QUARTER"     : 25,
    "ONE"         : 100,
    "FIVE"        : 500,
    "TEN"         : 1000,
    "TWENTY"      : 2000,
    "ONE HUNDRED" : 10000,
  };
  // Copy drawer to cents
  let drawer = cid
    .map(denom => [denom[0], Math.round(denom[1] * 100)])
    .reverse();
  // Compute change needed
  let totalChange = Math.round((cash - price) * 100);
  let change = drawer.reduce(
    (change, denom) => {
      const cents = unitToCents[denom[0]];
      let currentChange = 0;
      while (totalChange >= cents && denom[1] >= cents) {
        totalChange -= cents;
        denom[1] -= cents;
        currentChange += cents;
      }
      change.push([denom[0], currentChange / 100]);
      return change;
    },
    []
  )
  // Check for sufficient funds
  if (totalChange > 0.0) {
    return { status: "INSUFFICIENT_FUNDS", change: [] };
  }
  // Check and return change and drawer status
  let empty = drawer.every(denom => denom[1] == 0);
  const status = empty ? "CLOSED" : "OPEN";
  change = empty ? cid : change.filter(denom => denom[1] > 0);
  return { status, change };
}

In my solution, I tried to avoid copying around data. (I could have copied around a bit less if I wasn’t also avoiding mutating my inputs :smiley: )

I think there is a balance between robustness and simplicity. I went with simplicity, but you probably learned more while making your project your way.

Yup, it is a long solution.
No, I will not read it.

Works? Works!
That alone is worthy of congratulations.

Remember - the less code you write the less of your code you will have to debug later.
Also: “Weeks of coding can save you hours of planning.”

Yes, it was perhaps one of the most enriching learning experiences I’ve ever embarked upon. A few things stand out to me as what I learned along the way:

  • Accessing the name of a passed variable from within a function
  • Sorting a 2D array by subsequent values (sort by index 0, then by index 1)
  • The joys of Map() and Set() (worked with these a tiny bit before, but now I really get them)
  • Using Object.fromEntries to turn a 2D array into an object
  • Handling JavaScript’s utterly wonky behavior with multiplying floats

All in all I spent about a week from start to finish building everything out, including 2 or 3 days where I did little else.

It was totally worth it and I really appreciate your kind words and feedback.

Ahaha yes it certainly does work and I’m full glad of it.

That’s a fantastic quote. FWIW, the solution, as complex as it was, did in fact come out of planning. I decided not to read up on other ways to solve the problem since I really wanted the solution to be one that I came up with entirely on my own.

Here’s how I split apart the problem:

  • First, I tackled the coins, to make sure that there is exact coin change available. If you determine that there isn’t coin change available, then you already know that insufficient funds are available without having to worry about dollars.
  • Second, to solve this challenge, I turned the summed value of each coin into an integer representing the number of each coin available. (For example, 1.25 for quarters would mean 5 quarters are available.)
  • Third, I generated a 2D array of every possible permutation of the four coin types in my drawer, and then ran a second operation to render these into unique combinations.
  • Fourth, I calculated the total value of each permutation and compared it to the number of cents owed.
  • Fifth, I generated a “dollar-coin” object to account for the edge case of requiring a certain number of dollars in change beyond those available with the bills on hand. The object has each dollar-amount-in-coins as its key, and then an array with the number of cents available per dollar as its value. (For example, if there are $3.22 in coins, there would only be values up to 22 for the key of 3, but more than this for values 0-2)
  • Sixth, I calculated all dollar permutations based on the bills available
  • Seventh, I ran a few checks to see if there are enough cents, and enough bills, and also if there aren’t enough bills, whether there are enough coins to make up for it while still providing exact change.
  • Eigth, I computed the most efficient way to distribute the change. This one was quite clever: the answer is simply the iteration with the lowest total sum of currency units, repeated uniquely for both the array of available bills and available coins.
  • Ninth, I passed the outcome into the output generator
  • Tenth, I used a simple switch statement to provide the output as requested

Easy as pie :slight_smile:

function checkCashRegister(price, cash, cid) {

  //multiplying by 100 to avoid floating point errors
  var unpaidChange = Math.round(cash * 100) - Math.round(price * 100);
  var value = [1, 5, 10, 25, 100, 500, 1000, 2000, 10000]

  //drawers [name, total, 1 unit]
  var drawers = cid.map((nominal, index) => 
    {return ([nominal[0], Math.round(nominal[1] * 100), value[index]]);});
  
  //function counting total cash in drawers
  function countCashInDrawers(arr){
    return arr.reduce((total, item) =>{return total += item[1]}, 0);
  };

  //final function to return object
  //it creates result based on passed array value
  function result(array){
    let message = '';

    //convert values in array back to floats
    let returnArray = array.map(i => [i[0], i[1]/100]);

    if (array.length == 0 ||
        Math.round(cash - price) != Math.round(countCashInDrawers(returnArray)))
      {message = "INSUFFICIENT_FUNDS";}
    else if (unpaidChange == countCashInDrawers(drawers))
      {message = "CLOSED"; returnArray = cid;}
    else
      {message = "OPEN";}
    return {status: message, change: returnArray}
  };

  //empty array and function for edge case
  //example: when unpaid change is 0.30 and there is many 0.25 and many 0.10 left
  //normal function will deduct 0.25 from 0.30 and correct payment will be impossible
  //this one is adding one 0.25 to few things and continues as usual
  var edgeArray = [];
  function edgeCase(arr){
    let change = arr[0];
    let drawers = arr[1];
    let finalArr = arr[2];

    //first, add 1 QUARTER to change and finalArr, remove quarter section if value == 0
    change += 25;
    finalArr[finalArr.length - 1][1] -= 25;
    if (finalArr[finalArr.length - 1][1] == 0) finalArr.pop()

    //then continue pay function as normal, with currencies less than 0.25
    let res = pay(change, drawers.slice(0, drawers.length), finalArr);
    return result(res);
  };


  //recursive function
  //if highest drawer not empty: deduct highest possible nominal when possible
  //call function again with shorter drawers array
  function pay(unpaidChange, drawers, changeArr = []){

    //creating variables without mutations
    let change = unpaidChange;
    let cashLeft = countCashInDrawers(drawers);
    let currentDrawer = drawers[drawers.length - 1][1];
    let currentNominal = drawers[drawers.length - 1][2];
    let combinedArray = changeArr.slice();
    
    //creating one drawer - it will be appended to final array if value inside > 0
    let sumToPay = [drawers[drawers.length - 1][0], 0];

    while(change >= currentNominal && cashLeft >= change && currentDrawer > 0){
      change -= currentNominal;
      cashLeft -= currentNominal;
      currentDrawer -= currentNominal;
      sumToPay[1] += currentNominal;
    }

    //prepare data for eventual edge case
    if(drawers[drawers.length - 1][0] == "QUARTER")
      edgeArray = [change, drawers.slice(0, drawers.length-1), combinedArray];

    //if something was deducted push ["NOMINAL_NAME", amount] to combinedArray
    if (sumToPay[1] > 0) combinedArray.push(sumToPay);

    //check if finished, if not, make recursive call
    if (change == 0)
      return combinedArray;
    if (drawers.length > 1) 
      return pay(change, drawers.slice(0, drawers.length - 1), combinedArray);
    else 
      return combinedArray;
  };


let payment = pay(unpaidChange, drawers, []);

//if payfunction 
if (payment.length == 0 || unpaidChange == countCashInDrawers(payment))
  {return(result(payment))}
else if (unpaidChange != countCashInDrawers(payment))
  {return(edgeCase(edgeArray))}
else
  {return ('Unexpected behavior in function checkCashRegister()')}

}



checkCashRegister(19.5, 20, [["PENNY", 1.01], ["NICKEL", 2.05], ["DIME", 3.1], ["QUARTER", 4.25], ["ONE", 90], ["FIVE", 55], ["TEN", 20], ["TWENTY", 60], ["ONE HUNDRED", 100]]);

Well, for your convenience and my fun I decided to do that challenge again.
Keep in mind that I have done it few years ago already.

It is far from perfect, I would like to polish it better, but it gets the job done, even with edge cases. Tests are passing even when edge case is not taken into consideration.
Some of my concepts may be useful to you.

Cheers.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.