How to give discounts in simple Ruby shopping cart? - ruby

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

Related

How can you use a symbol to reference an object in Ruby?

I'm trying to convert a project I've written in Ruby to use Classes.
Block I'm currently using:
elements.each do |element, value|
value /= 100
total.push(value * price[element])
end
Full code: https://gist.github.com/gbourdon/53d3d125b04a9394164daca01b657987
Hash structure:
price = {o: 0.30, c: 2.40, h: 12.00, etc.}
I need the symbol stored in the hash (For instance, :o) to be able to reference an object with the same name (For instance, the object o).
How can I get this to work?
Here's a reworked, Ruby-ified version of your code that avoids the need to have those variables entirely. If you look at the operations here you don't care, particularly, what the element is, you only care about it's value and relative abundance per unit of weight.
A retooled Element class looks like this:
class Element
attr_reader :symbol
attr_reader :price
attr_reader :amount
def initialize(symbol, price, amount)
# Cocerce both inputs into floats
#symbol = symbol
#price = price.to_f
#amount = amount.to_f
end
end
Now that contains information important to the element itself, like its symbol. Keeping the symbol some place like the variable name is actually quite annoying as variable names shouldn't have significant meaning like that, they should only be for readability.
Now you can define all of your elements in one shot, inside one container object:
ELEMENTS = [
Element.new('O', 0.30, 0.65),
Element.new('C', 2.40, 0.18),
Element.new('H', 12, 0.10),
Element.new('N', 0.40, 0.03),
Element.new('Ca', 11, 0.015),
Element.new('P', 4, 0.01),
Element.new('K', 85, 0.0035),
Element.new('S', 0.25, 0.0025),
Element.new('Cl', 0.15, 0.0015),
Element.new('Na', 7, 0.0015)
]
The resulting executable can be streamlined a bunch more, too, especially on the input conversion:
# Take input from the command-line to make re-running this easier
pounds = ARGV[0].to_i
# Quick conversion in one shot. Try and keep variables all lower_case
kg = pounds * 0.4536 * 1000
Now all you need to do is convert each element in that table into a net price based on the weight:
# Convert each element into its equivalent value by weight
total = ELEMENTS.map do |element|
element.price * element.amount * kg
end.reduce(:+) # Added together
Where reduce here is a replacement for the unnecessary Array method. It does wha you need. Rails actually has a sum method which is even easier.
Then to present:
puts "You are worth: $#{(total / 100).round(2)}"
That's it.
With this new structure you could expand on the functionality to give a detailed breakdown of price by element if you wanted, all the information necessary is contained within that element object. That's why a more self-contained object design is better.

Calculating totals from two different hashes

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

Ruby sort_by of Objects/Classes undefined method 'sort_by' for

I have a class Store, each Store have a Hash with differents Products and each Product have a Hash with differents kinds of sell.
class Store
def initialize
#store_products = Hash.new
##store_products['product_id'] = Product
end
end
class Product < Store
def initialize(model)
#model = model
#sell_option = Hash.new
##sell_option['sell_name'] = SellOption
end
end
class SellOption < Product
def initialize(size, price, stock)
#size = size
#price = price
#stock = stock
end
end
With this data:
product1 = Product.new("shirt1")
product1.add_sell_option(SellOption.new("S", 12, 10))
product1.add_sell_option(SellOption.new("M", 15, 10))
product1.add_sell_option(SellOption.new("L", 14, 10))
product2 = Product.new("shirt2")
product2.add_sell_option(SellOption.new("M", 6, 10))
product2.add_sell_option(SellOption.new("L", 7, 10))
product3 = Product.new("shirt2")
product3.add_sell_option(SellOption.new("M", 10, 10))
product3.add_sell_option(SellOption.new("L", 11, 10))
store = Store.new
store.add(product1)
store.add(product2)
store.add(product3)
In class Store I want to do a method that sort my Products by price.
If use this expression:
puts store.products_sorted_by_price
def products_sorted_by_price
#store_products.each_value do |product|
product.sort_by{|k, v| v.value.price}
end
end
Why it returns "undefined method 'sort_by' for Product..."!!
There are lots of things I don't understand about your question:
- Why does Product inherit from Store?
- Why does SellOption inherit from Product?
- Are you trying to sort products by price?
- Which price do you use if the sell options have different prices?
- Why do different sizes have different prices?
- What is the value that you're extracting from the v, which is supposed to be a Product?
Anyway ... sort_by is typically run on an Enumerable, and Product isn't one, as far as I can see. So the precise answer is that Ruby says undefined method 'sort_by' for Product because you haven't defined sort_by on Product, or inherited from a class that does define it.
I am guessing you meant to do something more like this:
#store_products.sort_by {|k,v| v.value.price }?
Except even then, I don't know what value is, and you'll have to figure out how you want to deal with multiple prices in a product before you can really finish sort_by, but hopefully this points you in the right direction.
Clarify your question, and I can try to improve the answer.

Ruby coding exercise solution (rubymonk)

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?
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.
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]
}
}

Checkout system giving strange result

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))

Resources