Calculating totals from two different hashes

2019-09-07 02:34发布

问题:

I have two hashes:

For example, one contains a list of dishes and their prices

dishes = {"Chicken"=>12.5, "Pizza"=>10, "Pasta"=>8.99}

The other is a basket hash i.e. I've selected one pasta and two pizzas:

basket = {"Pasta"=>1, "Pizza"=>2}

Now I am trying to calculate the total cost of the basket but can't seem to get my references right.

Have tried

basket.inject { |item, q| dishes[item] * q }

But keep getting the following error

NoMethodError: undefined method `*' for nil:NilClass

回答1:

basket.inject { |item, q| dishes[item] * q }

Let's look at the documentation for Enumerable#inject to see what is going on. inject "folds" the collection into a single object, by taking a "starting object" and then repeatedly applying the binary operation to the starting object and the first element, then to the result of that and the second element, then to the result of that and the third element, and so forth.

So, the block receives two arguments: the current value of the accumulator and the current element, and the block returns the new value of the accumulator for the next invocation of the block. If you don't supply a starting value for the accumulator, then the first element of the collection is used.

So, during the first iteration here, since you didn't supply a starting value for the accumulator, the value is going to be the first element; and iteration is going to start from the second element. This means that during the first iteration, item is going to be ['Pasta', 1] and q is going to be ['Pizza', 2]. Let's just run through the example in our heads:

dishes[item] * q                    # item is ['Pasta', 1]
dishes[['Pasta', 1]] * q            # q is ['Pizza', 2]
dishes[['Pasta', 1]] * ['Pizza', 2] # there is no key ['Pasta', 1] in dishes
nil * ['Pizza', 2]                  # nil doesn't have * method

Ergo, you get a NoMethodError.

Now, I believe, what you actually wanted to do was something like this:

basket.inject(0.0) {|sum, (item, q)| sum + dishes[item] * q }
#             ↑↑↑    ↑↑↑             ↑↑↑↑↑
  • You don't want to accumulate orders, you want to accumulate numbers, so you need to supply a number as the starting value; if you don't, the starting value will be the first element, which is an order, not a number
  • You were mixing up the meaning of the block parameters
  • You weren't actually summing anything

Now, while inject is capable of summing (in fact, inject is capable of anything, it is a general iteration operation, i.e. anything you could do with a loop, you can also do with inject), it is usually better to use more specialized operations if they exist. In this case, a more specialized operation for summing does exist, and it is called Enumerable#sum:

basket.sum {|item, q| dishes[item] * q }

But there is a deeper underlying problem with your code: Ruby is an object-oriented language. It is not an array-of-hash-of-strings-and-floats-oriented language. You should build objects that represent your domain abstractions:

class Dish < Struct.new(:name, :price)
  def to_s; "#{name}: $#{price}" end
  def *(num) num * price end
  def coerce(other) [other, price] end
end

require 'bigdecimal'
require 'bigdecimal/util'

dishes = {
  chicken: Dish.new('Chicken', '12.5'.to_d), 
  pizza: Dish.new('Pizza', '10'.to_d),
  pasta: Dish.new('Pasta', '8.99'.to_d)
}

class Order < Struct.new(:dish, :quantity)
  def to_s; "#{quantity} * #{dish}" end
  def total; quantity * dish end
end

class Basket
  def initialize(*orders)
    self.orders = orders
  end

  def <<(order)
    orders << order
  end

  def to_s; orders.join("\n") end

  def total; orders.sum(&:total) end

  private

  attr_accessor :orders
end

basket = Basket.new(
  Order.new(dishes[:pasta], 1), 
  Order.new(dishes[:pizza], 2)
)

basket.total
#=> 0.2899e2

Now, of course, for such a simple example, this is overkill. But I hope that you can see that despite this being more code, it is also much much simpler. There is complex navigation of complex nested structures, because a) there are no complex nested structures and b) all the objects know for how to take care of themselves, there is never a need to "take apart" an object to examine its parts and run complex calculations on them, because the objects themselves know their own parts and how to run calculations on them.

Note: personally, I do not think that allowing arithmetic operations on Dishes is a good idea. It is more of a "neat hack" that I wanted to show off in this code snippet.



回答2:

With Ruby 2.4, you could use Hash(Enumerable)#sum with a block :

basket = {"Pasta"=>1, "Pizza"=>2}
prices = {"Chicken"=>12.5, "Pizza"=>10, "Pasta"=>8.99}

basket.sum{ |dish, quantity| quantity * prices[dish] }
# 28.99

Data structure

dishes

dishes (what I called prices to avoid writing dishes[dish]) is the correct data structure :

  • Hash lookup is fast
  • If you want to update the price of a dish, you only have to do it in one place
  • It's basically a mini database.

basket

basket is also fine as a Hash, but only if you don't oder any dish more than once. If you want to order 2 pizzas, 1 pasta and then 3 pizzas again :

{"Pizza"=>2, "Pasta" => 1, "Pizza" =>3}
=> {"Pizza"=>3, "Pasta"=>1}

you'll lose the first order.

In that case, you might want to use an array of pairs (a 2-element array with dish and quantity) :

basket = [["Pizza", 2], ["Pasta", 1], ["Pizza", 3]]

With this structure, you could use the exact same syntax to get the total as with a Hash :

basket.sum{ |dish, quantity| quantity * prices[dish] }


回答3:

Try this one

basket.inject(0) do |acc, item|
  dish, q = item
  acc + (dishes[dish] * q)
end
 => 28.990000000000002 

one line

basket.inject(0) { |acc, item| acc + (dishes[item.first] * item.last) }

Your variables for the block are wrong. You have the accumulator and an item (that it's an hash)



回答4:

2.2.0 :011 > basket.inject(0){ |sum, (item, q)| sum + dishes[item].to_f * q }
=> 28.990000000000002 


标签: ruby hash lookup