Checkout system giving strange result - ruby

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

Related

Ruby - How to calculate subtotal of items in a grocery list

I want to create a program where a user basically "creates" a grocery list where the user inputs item and price until they want to quit. If the user enters ’q’ or ’Q’, then the program should stop prompting the user and instead should calculate the subtotal, add in a nominal 6% sales tax, and display the total result.
I got the first part down where user inputs their item and price but I don't know how to make it tell me the subtotal and give them a receipt. I have been trying for 7 hours!! When I run it, it's supposed to say:
Enter an item and its price, or ’Q/q’ to quit: eggs 2.13
Enter an item and its price, or ’Q/q’ to quit: milk 1.26
Enter an item and its price, or ’Q/q’ to quit: batteries 3.14
Enter an item and its price, or ’Q/q’ to quit: q
Receipt:
--------
eggs => $2.13
milk => $1.26
batteries => $3.14
---------
subtotal: $6.53
tax: $0.39
total: $6.92
Here is the code I made: (Can anyone please help me???)
def create_list
puts 'Please enter item and its price or type "quit" to exit'
items = gets.chomp.split(' ')
grocery_list = {}
index = 0
until index == items.length
grocery_list[items[index]] = 1
index += 1
end
grocery_list
end
def add_item (list)
items = ''
until items == 'quit'
puts "Enter a new item & amount, or type 'quit'."
items = gets.chomp
if items != 'quit'
new_item = items.split(' ')
if new_item.length > 2
#store int, delete, combine array, set to list w/ stored int
qty = new_item[-1]
new_item.delete_at(-1)
new_item.join(' ')
p new_item
end
list[new_item[0]] = new_item[-1]
else
break
end
end
list
end
add_item(create_list)
puts "Receipt: "
puts "------------"
Not sure you need hashes for this as they are used to store key value pairs.
Also you should organize your code where you define your variables, then your methods, then run your code last. Keep the methods simple.
#define instance variabes so they can be called inside methods
#grocery_list = [] # use array for simple grouping. hash not needed here
#quit = false # use boolean values to trigger when to stop things.
#done_shopping = false
#line = "------------" # defined to not repeat ourselves (DRY)
#define methods using single responsibility principle.
def add_item
puts 'Please enter item and its price or type "quit" to exit'
item = gets.chomp
if item == 'quit'
#done_shopping = true
else
#grocery_list << item.split(' ')
end
end
# to always use 2 decimal places
def format_number(float)
'%.2f' % float.round(2)
end
#just loop until we're done shopping.
until #done_shopping
add_item
end
puts "\n"
#print receipt header
puts "Receipt: "
puts #line
#now loop over the list to output the items in arrray.
#grocery_list.each do |item|
puts "#{item[0]} => $#{item[1]}"
end
puts #line
# do the math
#subtotal = #grocery_list.map{|i| i[1].to_f}.inject(&:+) #.to_f converts to float
#tax = #subtotal * 0.825
#total = #subtotal + #tax
#print the totals
puts "subtotal: $#{format_number #subtotal}"
puts "tax: $#{format_number #tax}"
puts "total: $#{format_number #total}"
#close receipt
puts #line

How to tackle this Ruby candy store homework?

I have a class in university that asks students to learn three languages in
one semester. Like one is from really old languages such as Haskell, the other one should be from interpreter languages.
So, now I have to learn Ruby, and I need help. Let's say there is class that has
class Help
##array = Array.new
##count = 0
#store
#chocolate
#candy
#store_code
store is string (name of store)
chocolate, candy, store_code are integer (price, and code number)
Lets consider that I have an add function and call it twice
def add (s, i, i, i)
array = [s, i, i, i]
count += 1
end
store_a = Help.new
store_a.add (A, 20, 1, 100)
store_b = Help.new
store_b.add (B, 50, 1, 100)
Anyway, store_a chocolate price is 20
store_b chocolate price is 50 now
How do I make a function inside of class that calculates average of chocolate price? (I make the count variable for this, but I don't know if I need it or not).
This can be refactored and made shorter, also you can make use of class variables like you mentioned in the question using "##", but my goal here is to keep it basic so you can start grasping it and slowly moving to more advanced techniques and designs:
class Warehouse
attr_accessor :products_stores
def initialize
#products_stores = []
end
def add_product(args)
#products_stores << args
end
def product_price_avg
avg = 0
#products_stores.each do |o|
avg += o[:product].price
end
avg / #products_stores.count
end
end
class Store
attr_accessor :code
def initialize(code)
#code = code
end
end
class Chocolate
attr_accessor :price
def initialize(price)
#price = price
end
end
store_a = Store.new(100)
store_b = Store.new(200)
cheap_chocolate = Chocolate.new(20)
expensive_chocolate = Chocolate.new(50)
warehouse = Warehouse.new
warehouse.add_product({store: store_a, product: cheap_chocolate})
warehouse.add_product({store: store_b, product: expensive_chocolate})
puts warehouse.product_price_avg

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

Why doesnt this ruby code work as expected?

I am working on learning some Hash operations in ruby. The code is about increasing each item's price by 10%. Why doesn't this code work?
restaurant_menu = { "Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2 }
restaurant_menu.each do |item, price|
price = price + (price * 0.1)
end
while this one does:
restaurant_menu = { "Ramen" => 3, "Dal Makhani" => 4, "Coffee" => 2 }
restaurant_menu.each do |item, price|
restaurant_menu[item] = price + (price * 0.1)
end
And any reasons as to why the latter is a better way to do it than the former as explained by #Mike Manfrin?
In the first one, you're setting the local variable of price to your new price, and then it gets discarded. item and price are scoped only to that one line they're used on.
In your second example, you are setting a variable, restaurant_menu, that exists outside the each block, so those changes will persist after the each block has finished running.

How to give discounts in simple Ruby shopping cart?

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

Resources