Calculating change based on input

Posted on

Problem

I decided to make a program that calculates change, based on an input. My code is extremely long, and I would like to know how I can fix this.

def make_change(amount)
  values = []
  coins = []
  hash = {}
  if amount >= 50
    coins.push(:H)
    values.push(amount / 50)
    amount = amount % 50
    if amount % 50 >= 25
      coins.push(:Q)
      values.push(amount / 25)
      amount = amount % 25
      if amount % 25 >= 10
        coins.push(:D)
        values.push(amount / 10)
        amount = amount % 10
        if amount % 10 >= 5
          coins.push(:N)
          values.push(amount / 5)
          amount = amount % 5
          if amount % 5 >= 1
            coins.push(:P)
            values.push(amount / 1)
          end
        end
      end
    end
  elsif amount >= 25
    coins.push(:Q)
    values.push(amount / 25)
    amount = amount % 25
    if amount % 25 >= 10
      coins.push(:D)
      values.push(amount/10)
      amount = amount % 10
      if amount % 10 >= 5
        coins.push(:N)
        values.push(amount/5)
        amount = amount % 5
        if amount % 5 >= 1
          coins.push(:P)
          values.push(amount/1)
        end
      end
    end
  elsif amount >= 10  
    coins.push(:D)
    values.push(amount / 10)
    amount = amount % 10
    if amount % 10 >= 5
      coins.push(:N)
      values.push(amount / 5)
      amount = amount % 5
      if amount % 5 >= 1
        coins.push(:P)
        values.push(amount / 1)
      end
    end
  elsif amount >= 5
    coins.push(:N)
    values.push(amount / 5)
    amount = amount % 5
    if amount % 5 >= 1
      coins.push(:P)
      values.push(amount / 1)
    end
  elsif amount >= 1
    coins.push(:P)
    values.push(amount / 1)
  else
    coins = []
    values = []
  end
  Hash[coins.zip(values)]
end

Examples

make_change(42) should result in
=> {:Q => 1, 😀 => 1, :N => 1, 😛 => 2

make_change(91) should result in
=> {:H => 1, :Q => 1, 😀 => 1, :N => 1, 😛 => 1

Solution

Here’s one way, that uses the method Fixnum#divmod to advantage:

COINS = [["H", 50], ["Q", 25], ["D", 10], ["N", 5], ["P", 1]]

def change(amount)
  raise ArgumentError, 'Only non-negative integers permitted' unless
    amount.is_a?(Fixnum) && amount >= 0
  return [] if amount.zero?
  COINS.map do |label, value|
    nbr, amount = amount.divmod(value)
    [nbr, label]
  end
end

change(260) #=> [[5, "H"], [0, "Q"], [1, "D"], [0, "N"], [0, "P"]]
change(95)  #=> [[1, "H"], [1, "Q"], [2, "D"], [0, "N"], [0, "P"]]
change(100) #=> [[2, "H"], [0, "Q"], [0, "D"], [0, "N"], [0, "P"]]
change(80)  #=> [[1, "H"], [1, "Q"], [0, "D"], [1, "N"], [0, "P"]]
change(45)  #=> [[0, "H"], [1, "Q"], [2, "D"], [0, "N"], [0, "P"]]
change(16)  #=> [[0, "H"], [0, "Q"], [1, "D"], [1, "N"], [1, "P"]]
change(1)   #=> [[0, "H"], [0, "Q"], [0, "D"], [0, "N"], [1, "P"]]
change(0)   #=> []
change(-3)  #=> ArgumentError: Only non-negative integers permitted...
change(1.2) #=> ArgumentError: Only non-negative integers permitted...
change('c') #=> ArgumentError: Only non-negative integers permitted...

I would start by searching for ways to eliminate the deep nesting of the if statements and pulling the triad of similar commands you are repeating into a reusable function.

coins.push(:X)
values.push(amount/y)
return # some mutation imposed on amount

is an obvious template for a reusable function. Any time you copy-paste, you should start asking yourself how to avoid any more copy-paste behavior.

I’m not sure if I’m right about this, but it looks like this might be some sort of refining algorthm. You may want to replace all of those nested ifs with some sort of iterative calculation instead. You may be able to get your variable values for the contitional in the ifs by some function, but if not, you could put them in a list of truples and then map your calculation over that list. Something like

[(1,50,:H),(50,25,:Q),(25,10,:D),(10,5,:N),(5,1,:P)].each { |tuple| func(tuple.fst, tuple.snd, tuple.thrd) } 
# where func(fst, snd, thrd) is a generalization of 
if amount % fst >= snd
  coins.push(thrd)
  values.push(amount/snd)
  amount = amount % snd
end

#notice that this calculation mutates state.  You could avoid such mutation by using reduce instead of map to keep the state encapsulated in your iterative calculation over the list of parameters you want to pass in.  Refer to the Ruby docs for a starting place on how to do this.

Please treat this as an approach/idea presented in psuedo-code. I haven’t tested this solution, and I also know it’s not a comprehensive solution but only addresses the first part of the outer-most if block, but it’s meant to point you in one possible direction for drying up your code.

My solution:

#Define the coins with value
#(Remark: Ruby Hashs are ordered, so this can be used).
VALUES = {
  #'$' => 100,
  :H => 50,
  :Q => 25,
  :D => 10,
  :N =>  5,
  :P =>  1,
}
# In: Get money in cents
# Out: Change the minimum quantity of coins
def make_change(amount, values = VALUES)
  change = {}  
  values.each{|coin, value|
    num, amount = amount.divmod(value)
    change[coin] = num if num > 0
  }
  change
end

Test:

[23,100,42,91].each{|i|
 puts "%-4i: %-10s" % [ i, make_change(i)]
 }

result:

23  : {:D=>2, :P=>3}
100 : {:H=>2}   
42  : {:Q=>1, :D=>1, :N=>1, :P=>2}
91  : {:H=>1, :Q=>1, :D=>1, :N=>1, :P=>1}

One advantage of my solution: You can add new coins very easy. Just add '$' => 100, to support a one dollar coin. (The name of the coin must be uniq!).

Or just give your own coin definition as parameter, e.g. for Euros:

EUR_VALUES = {
  :'2EUR' => 200,
  :EUR => 100,
  50 => 50,
  20 => 20,
  10 => 10,
    5 =>  5,
    2 =>  2,
    1 =>  1,
}
[23,42,91,100,101].each{|i|
 puts "%-4iEUR-Cents: %-10s" % [ i, make_change(i, EUR_VALUES)]
 }

Result:

23  EUR-Cents: {20=>1, 2=>1, 1=>1}
42  EUR-Cents: {20=>2, 2=>1}
91  EUR-Cents: {50=>1, 20=>2, 1=>1}
100 EUR-Cents: {:EUR=>1} 
101 EUR-Cents: {:EUR=>1, 1=>1}

Leave a Reply

Your email address will not be published. Required fields are marked *