Question

I am working on understanding a Rubymonk coding exercise solution, and am having trouble understanding what is going on in the cost method.

For reference, the menu is {:rice => 3, :noodles => 2} and the purpose of this is to calculate the total cost of orders from the menu.

An order example would be:

{:rice => 1, :noodles => 1} )

The solution I came up with was simpler, at least in my head, but returned a "cannot convert Symbol to an Integer" error which I was unable to rectify through to_i.

    class Restaurant
  def initialize(menu)
    @menu = menu
  end

  def cost(*orders)
    orders.inject(0) do |total_cost, order|
      total_cost + order.keys.inject(0) {|cost, key| cost + @menu[key]*order[key] }
    end
  end
end

Can someone please explain each step in the cost method simply?

Was it helpful?

Solution

Considering that a total cost is being calculated, it appears that @menu contains unit prices (as one generally finds, except perhaps at the very best restaurants) and each order contains the number of each menu item that is ordered. Suppose:

@menu = {rice: 0.69, noodles: 0.89}

where the values are unit prices and an element of orders looks something like this:

{rice: 3, noodles: 2}

where the values are quantities ordered. The cost to supply the quantities given by this order would be:

(3)(0.69) + (2)(0.89) = 3.95

You are to sum this cost over all orders.

First, let's write the method like this,

def cost( *orders )
   orders.inject(0) do |total_cost, order|
     total_cost + order.keys.inject(0) do |cost, key|
       cost + order[key] * @menu[key] 
     end
   end
 end

to clarify its structure. inject (aka reduce) is iterating over orders and accumulating a value in the variable total_cost. You can assign total_costan initial value by passing an argument to inject (as you have done). If you don't give inject an argument initial value, total_cost is set equal to the first evaluated value in the block that follows inject. In this case, you would get the same results if you dropped the arguments to inject.

For the first value of orders (the block variable order), the following number is added to the accumulator total_cost:

order.keys.inject(0) do |cost, key|
  cost + @menu[key] * order[key]
end

To obtain this value, Ruby must perform a side calculation. Suppose @menu and order have the values I gave above.

inject(0) iterates over order.keys (which is [:rice, :noodles]), using cost as its accumulator. The block is executed for :rice and then for noodles:

  cost + order[:rice]    * @menu[:rice]    => 0    + 3 * 0.69  # => 2.07 => cost
  cost + order[:noodles] * @menu[:noodles] => 2.07 + 2 * 0.89  # => 3.95 => cost

This completes the side calculation, so 3.95 is added to the outer accumulator total_cost (which previously equaled zero). The next element of orders is then processed by the outer inject, and so on.

OTHER TIPS

First, understand how ruby's Enumerable inject works. "ruby inject and the Mandelbrot set" is an introductory article on it.

Based on that understanding, we see that this code:

order.keys.inject(0) {|cost, key| cost + @menu[key]*order[key] }

is simply returning the sum of all values @menu[key]*order[key] as key iterates over order.keys to give the total cost of each order.

Finally, the outer loop orders.inject(0) do |total_cost, order| ... loops over the the cost of each order in the orders list, to return the total cost of all orders.

The key to the cost definition in your post is obviously the inject method. The inject method is also callable as reduce, which is a more sensible name to many English speakers, because it takes a list and reduces it to a single value. (Just to confuse things further, in the literature of functional programming, this function is almost always called "fold").

There are lots of examples; consider finding the sum of a list of integers:

[1,2,3,4,5].inject(0) {|sum, num| return sum + num}  #=> 15

So what's going on here? The first argument to the block is the running result - the partial sum in this case. It starts out as whatever you pass as the argument to inject, which in the above example is 0.

The block is called once per item in the list, and the current item becomes the second argument. The value returned by the block becomes the running value (first argument) to the next iteration of the block.

So if we expand the above injection into more explicit imperative code, we get something like this:

def block(sum, num) 
   return sum + num
end

result = 0
for i in [1,2,3,4,5]
  result = block(result, i)
end

Armed with this knowledge, let's tackle cost:

 def cost(*orders)
   orders.inject(0) do |total_cost, order|
     total_cost + order.keys.inject(0) {|cost, key| cost + @menu[key]*order[key] }
   end
 end

First, it's taking advantage of the fact that you can leave off the return in Ruby; the value of the last expression in a block is the return value of the block.

Both inject calls look a lot like my example above - they're just simple summation loops. The outer inject builds the grand total across all the individual orders, but since those orders are maps rather than numbers, it has to do more work to get the cost of each order before adding them together. That "more work" is the inner inject call.

order.keys.inject(0) {|cost, key| cost + @menu[key]*order[key] }

Using my expansion above, you can see how this works - it just adds up the result of multiplying each value in the order (the item quantity) times the price of that item (the key) according to the menu.

Incidentally, you could avoid having to look up the keys in the order map inside the block by reducing over the key/value pairs instead of just the values. You can also take advantage of the fact that if you don't pass in an initial value to inject/reduce, it defaults to zero:

orders.inject { |grand_total, order|
  grand_total + order.inject { |subtotal, line_item|
    item, quantity = line_item
    subtotal + quantity * @menu[item]
  }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top