Related
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
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 no complex navigation of complex nested structures, because a) there are no complex nested structures and b) all the objects know 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.
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] }
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)
2.2.0 :011 > basket.inject(0){ |sum, (item, q)| sum + dishes[item].to_f * q }
=> 28.990000000000002
I'm making artist recommendation program that will match a hash showing artists that a user has seen live and how many times, against a hash showing artists that a given artist has shared a bill with and how many times. The match score is calculated based on these numbers. If a user has seen some artist x amount of times and a given artist has played with this artist at least once, like this:
user = {"artist7" => 3, "artist8" => 1}
artist1 = {"artist6" => 7, "artist7" => 7}
match = 0
user.each do |k, v|
if artist1[k]
match += (1 - ((user[k] - artist1[k])/(user[k] + artist1[k])).abs)
end
end
I have tried this out in irb and the value of match does not change.
All your inputs are integers, so ruby uses integer division. It looks like that's likely to produce 1, and 1 - 1 is zero. Add some to_f to your equation to use float division instead, e.g.:
match += (1 - ((user[k] - artist1[k]).to_f/(user[k] + artist1[k])).abs)
I'm trying to make a very simple Ruby shopping cart, and I need to be able to give discounts if a user buys certain combinations of goods. These are indicated in the #costs - if bulk is true, a user gets a discount (of :bulk_price) for buying :bulk_num of goods. I've got it making basic charges, but now I need to subtract discounts in certain cases. Here's what I have so far:
class Cart
attr_accessor :total, :costs, :item_array, :subtotal
def initialize
#item_array=[]
#subtotal=0
#costs= [{"A"=>{:price=>2, :bulk=>true, :bulk_num=>4, :bulk_price=>7}}, {"B"=>{:price=>12, :bulk=> false}},{"C"=>{:price=>1.25,:bulk=>true, :bulk_num=>6, :bulk_price=>6}}, {"D"=>{:price=>0.15, :bulk=>false}}]
end
def scan(*items)
items.each do |item|
#item_array<<item
#costs.each do |cost|
if cost.has_key?(item)
#subtotal+=cost[item][:price]
end
end
#subtotal
end
end
def total
end
end
Now, I've created an array to keep track of which items are purchased, and I'd ideally like to have the total function check the array and subtract from the subtotal if needed. Maybe I've just been staring at this too long, but I am having trouble figuring that out. Could anyone help?
A few things:
Indent your code properly, it will make it much easier for you in the long run.
Remove :total from attr_accessor, it isn't needed and the generated total method will be overridden by the one you define later on.
Consider making each item an object which knows its own cost, rather than looking up the cost of each item in #costs. Conceptually, it doesn't make sense for a "shopping cart" to keep track of all the prices of all the items in your store.
Make your total method functional. Don't bother subtracting from #subtotal -- it will cause problems if total is called more than once.
Actually, subtotal would also be better if you recalculate whenever needed:
def subtotal
#item_array.reduce(0) { |sum,item| sum + (#costs[item][:price] || 0) }
end
It may not be obvious to you now, but writing your code "functionally", like this, makes it easier to avoid bugs. You can cache values if they are really expensive to calculate, and will be needed more than once, but in this case there's no need to.
For total, you can do something like:
def total
result = self.subtotal
# check which discounts apply and subtract from 'result'
result
end
Since your question involves an exercise, I decided to change it around a bit to make some points that you might find helpful. A few notes:
I renamed scan to checkout, lest the former be confused with String#scan
An order quantity is given for each item ordered, in the form of a hash that is passed to the checkout method;
I changed :bulk_price to a unit price that applies if :bulk is true and the quantity ordered is at least :bulk_num.
I changed #costs to a hash, because you need to access item names, which are now keys.
I moved #costs outside the class, for two reasons. Firstly, that data is likely to change, so it really shouldn't be hardwired in the class definiation. Secondly, doing that provides flexibility should you want different class instances to use different #costs. You'll see I chose to pass that hash as an argument when creating a new class instance.
I eliminated all your accessors.
An exception is now raised if you enter an item name that is not a key in #costs.
This is the approach I took:
class Cart
def initialize(costs)
#costs= costs
end
def checkout(items)
purchases = {}
items.each do |(item, qty)|
cost = #costs[item]
raise ArgumentError, "Item '#{item}' not in #costs array" \
if cost == nil
if cost[:bulk] && qty >= cost[:bulk_num]
tot_cost = qty.to_f * cost[:bulk_price]
discount = qty.to_f * (cost[:price] - cost[:bulk_price])
else
tot_cost = qty.to_f * cost[:price]
discount = 0.0
end
purchases[item] = {qty: qty, tot_cost: tot_cost, discount: discount}
end
purchases
end
def tot_cost(purchases)
purchases.values.reduce(0) {|tot, h| tot + h[:tot_cost]}
end
def tot_discount(purchases)
purchases.values.reduce(0) {|tot, h| tot + h[:discount]}
end
end
costs = {"A"=>{price: 2, bulk: true, bulk_num: 4, bulk_price: 1.75},
"B"=>{price: 12, bulk: false },
"C"=>{price: 1.25, bulk: true, bulk_num: 6, bulk_price: 1.00},
"D"=>{price: 0.15, bulk: false }}
cart = Cart.new(costs)
purchases = cart.checkout({"A"=>6, "B"=>7, "C"=>4}) # item => quantity purchased
p purchases # => {"A"=>{:qty=>6, :tot_cost=>10.5, :discount=>1.5},
# => "B"=>{:qty=>7, :tot_cost=>84.0, :discount=>0.0},
# => "C"=>{:qty=>4, :tot_cost=>5.0, :discount=>0.0}}
p cart.tot_cost(purchases) # => 99.5
p cart.tot_discount(purchases) # => 1.5
So i need some support with my Ruby assignment, I'm not from US so you have to excuse my English.
We are building a hotel and this is the second assignment. It's a console based application and I have a class called main.rb that handles the runtime and a guest-class.
In this second assignment we are to preload the app with five guest-objects, I guess I have to use an array but don't really know how. Below are my guest class and my main class is simply a while-loop with a case statement.
I need help with:
adding 5 guests (not to a db or textfile only to a array or so) when the program starts
the hotel has 20 rooms and i need to randomize the room number and exclude already rented rooms
Hope you can help! Thanks!
class Guest
#Instance variables.
attr_accessor :firstName,
:lastName,
:address,
:phone,
:arrival,
:plot,
:gauge
#Constructor sets the guest details.
def initialize(first, last, adress, phone, arrival)
#firstName = first
#lastName = last
#address = address
#phone = phone
#arrival = arrival
#plot = range_rand(1,32)
#gauge = range_rand(2000,4000)
end
#Using rand()-method to randomize a value between min and max parameters.
def range_rand(min,max)
min + rand(max-min)
end
def to_string
"Name = #{#firstName} , Plot = #{#plot}"
end
end
Creating an array:
number_array = [1, 2, 3, 4, 5]
Accessing the elements of an array:
number_array[2]
# this would return the integer 3
Adding a new element to an array:
number_array << 6
# this would return [1, 2, 3, 4, 5, 6]
You can create a new guest by doing something like this:
Guest.new("John", "Doe", "1500 main street", "123-456-7890", "1/1/2010")
Since this is a homework assignment, I'll leave it to you to combine everything into a working solution ;)
Other people have already answered the first part of your question, so I'll help you with the second one (I'll provide the minimum, so that you still have some work to do :) )
You could create an array containing the 20 room numbers :
empty_rooms = (1..20).to_array
Then for each guest :
1) Take a random number in this array ( hint : randomize the index )
2) Remove this number from the array
3) And assign the room number to a Guest
4) Add the guest to the array of guests
I think what you mean is that you want 5 guest objects. You could put them in an array by creating an array literal and then adding guests to it
#guests = []
#guests << Guest.new()
#guests << Guest.new()
now your #guests array has two guests, etc.
So I'm making a checkout system similar to this. However, I have 3 items (A, B and C priced at $3.11, $5.00 and $11.23, respectively).
The conditions are such that item A has a buy-one-get-one-free offer and item B reduces to $4.50 if 3 or more units are bought. The problem comes when testing the code.
I've written the following code to solve the exercise:
class PriceDiscount # Applies price discounts for a specific number of items
# Initial method with item price and item quantity parameters
def initialize(itemprice, quantity)
#itemprice = itemprice
#quantity = quantity
end
# "calculate_for" method which calculates the discount for the specific number of items
def calculate_for(quantity)
(quantity / #quantity).floor * #itemprice
end
end
class PricePolicy # Calculates the price for a certain quantity of items after discounts
# Initial method with the original price and discounts as the paremeters
def initialize(orgprice, *discounts)
#orgprice = orgprice
#discounts = discounts
end
# Calculates the discounted price of a number of items
def price_for(quantity)
quantity * #orgprice - discount_for(quantity)
end
# Calculates the discount which is given for a number of items
def discount_for(quantity)
#discounts.inject(0) do |mem, discount|
mem + discount.calculate_for(quantity)
end
end
end
# Rule list set up for great flexibility as each rule is specified in one line
RULES = {
'A' => PricePolicy.new(3.11, PriceDiscount.new(3.11, 2)),
'B' => PricePolicy.new(5.00, PriceDiscount.new(4.50, 2)),
'C' => PricePolicy.new(11.23),
}
class Checkout # Checkout class which applies the rules to each item that is scanned
# Initial method which has the rules and items as its parameters
def initialize(rules)
#rules = rules
#items = Hash.new
end
# Method to set up the array in which scanned items are stored
def scan(item)
#items[item] ||= 0
#items[item] += 1
end
# Method which totals the price of the scanned items
def total
#items.inject(0) do |mem, (item, quantity)|
mem + price_for(item, quantity)
end
end
private
def price_for(item, quantity)
if rule_for(item)
rule_for(item).price_for(quantity)
else
raise "Invalid item '#{item}'"
end
end
def rule_for(item)
#rules[item]
end
end
And this is the tester:
require 'test/unit'
require_relative './CheckoutSystem.rb'
class TestPrice < Test::Unit::TestCase
def price(goods)
co = Checkout.new(RULES)
goods.split(//).each { |item| co.scan(item) }
co.total
end
def test_totals
# Scenario 1 with basket: A, B, A, A, C
assert_equal(22.45, price("ABAAC").round(2))
# Scenario 2 with basket: A, A
assert_equal(3.11, price("AA").round(2))
# Scenario 3 with basket: B, B, A, B
assert_equal(16.61, price("BBAB").round(2))
end
end
Scenario 1 and 2 give the correct values. However, scenario 3 gives the value of $13.11 when it should in fact be $16.61.
Does anyone have any idea where I've made a mistake? I've been going over it for ages and can't fix it. Any help would be greatly appreciated!
In order to change your program to pass the test cases, you have to change:
'B' => PricePolicy.new(5.00, PriceDiscount.new(4.50, 2)),
To
'B' => PricePolicy.new(5.00, PriceDiscount.new(1.50, 3)),
This way, when the user purchases 3 'B' items, they will receive a discount of 1.5, meaning that the total price of the 3 items will change from 15 to 13.5 (4.50 each.)
Note that in this case, if the user buys 4 'B' items, they will receive a discount on the first 3, but NO discount on the fourth item.
To solve the problem to work properly when '3 or more items' are purchased, the discount.calculate_for method will have to be changed. This is because 'A' and 'B' items will calculate their discounts differently:
A straightforward implementation would be to define a 'type' for each of the situations. In the example below, an empty type simply represents the 'x or more items' situation, and a bogo type represents a 'buy x and get a flat discount' situation.
class PriceDiscount
def initialize(itemprice, quantity, type = nil)
#itemprice = itemprice
#quantity = quantity
#type = type
raise "Invalid type '#{type}'" if #type != 'bogo' && #type != nil
end
def calculate_for(quantity)
if (#type == 'bogo')
(quantity / #quantity) * #itemprice
elsif quantity >= #quantity
(quantity.to_f / #quantity) * #itemprice
else
0
end
end
end
And the rules would be updated as follows:
RULES = {
'A' => PricePolicy.new(3.11, PriceDiscount.new(3.11, 2, 'bogo')),
'B' => PricePolicy.new(5.00, PriceDiscount.new(1.50, 3)),
'C' => PricePolicy.new(11.23),
}
The bogo items would use the same calculation as you had before, meaning that AA has 1 discount, AAA has 1 discount, and AAAA has 2 discounts.
The 'x or more items' will first check if the number of items meets the threshold, and if so, it will apply the discounted rate to every single one of those items. (In this case, BBB will apply the specified discount of 1.50. BBBB will apply a modified version of the discount --> 2.00, to match the 4 items.)
As well, you can drop the '.floor' method for ints --> it will automatically floor when converting. To get non-integer results, you can see that I've converted one of the values in the second calculation to a float.
Try running these changes with these change-specific tests:
assert_equal(13.11, price("BBA").round(2))
assert_equal(21.11, price("BBBBA").round(2))
assert_equal(22.45, price("AAAABC").round(2))
assert_equal(25.56, price("AAAAABC").round(2))