Христо обнови решението на 07.11.2011 16:55 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+class Product
+ attr_accessor :name, :price, :promotion
+
+ def initialize(name, price, promotion)
+ @name = name
+ @price = price.to_d
+ @promotion = promotion
+ end
+end
+
+class Inventory
+ attr_accessor :products, :coupons
+
+ def initialize
+ @products = {}
+ @coupons = {}
+ end
+
+ def register(name, price, promotion = nil)
+ raise "The name must have less than 40 characters" if name.size > 40
+ raise "There is another product with that name" if @products.key? name
+ raise "Invalid price value" if not price.to_d.between? 0.001, 999.99
+ raise "Only one promotion per product" if not promotion.nil? and promotion.size > 1
+
+ @products[name] = Product.new(name,price,promotion)
+ end
+
+ def register_coupon(name, discount)
+ @coupons[name] = discount
+ end
+
+ def new_cart
+ Cart.new(self)
+ end
+
+ def get_product (name)
+ @products[name]
+ end
+end
+
+class Cart
+ attr_accessor :products, :coupon_name, :calculator, :inventory
+
+ def initialize(inventory)
+ @inventory = inventory
+ @products = {}
+ @coupon_name = nil
+ @calculator = ProductCalculator.new(@inventory, self)
+ end
+
+ def add (name, quantity = 1)
+ raise "The given product does not exist!" if @inventory.products[name].nil?
+
+ new_quantity = (@products.key?(name) ? @products[name] : 0) + quantity
+
+ raise "Invalid quantity" if not new_quantity.between? 1, 99
+
+ @products[name] = new_quantity
+ end
+
+ def total
+ @calculator.total
+ end
+
+ def invoice
+ Invoice.new(self).text
+ end
+
+ def use (coupon_name)
+ raise "Only one coupon is allowed" if not @coupon_name.nil?
+ raise "The coupon is not registered" if not @inventory.coupons.key? coupon_name
+ @coupon_name = coupon_name
+ end
+
+ def coupon
+ @inventory.coupons[@coupon_name]
+ end
+end
+
+class ProductCalculator
+ def initialize(inventory, cart)
+ @inventory = inventory
+ @cart = cart
+ end
+
+ def total
+ return "0.00".to_d if @cart.products.size == 0
+
+ sum = sum_for_coupon
+ sum = apply_coupon(sum)
+ sum
+ end
+
+ def product_total(name, quantity)
+ @inventory.products[name].price * quantity
+ end
+
+ def sum_for_coupon
+ decimals = @cart.products.map do |name, quantity|
+ product_total(name, quantity) + apply_promotion(name, quantity)
+ end
+
+ decimals.inject &:+
+ end
+
+ def apply_promotion(name, quantity)
+ product = @inventory.products[name]
+ promotion = product.promotion.to_a.first
+ return 0 if promotion.nil?
+
+ case promotion[0]
+ when :get_one_free then get_one_free(product, quantity, promotion[1])
+ when :package then package(product, quantity, promotion[1].to_a.first)
+ when :threshold then threshold(product, quantity, promotion[1].to_a.first)
+ else 0
+ end
+ end
+
+ def apply_coupon(total)
+ return total if @cart.coupon_name.nil?
+
+ coupon = @inventory.coupons[@cart.coupon_name].to_a.first
+
+ case coupon[0]
+ when :percent then total * (100 - coupon[1]) / "100.00".to_d
+ when :amount then total > coupon[1].to_d ? total - coupon[1].to_d : 0
+ end
+ end
+
+ private
+
+ def get_one_free(product, quantity, every)
+ 0 - quantity / every * product.price
+ end
+
+ def package(product, quantity, promotion_body)
+ count = promotion_body[0]
+ cents = promotion_body[1]
+ 0 - (quantity - quantity % count) * cents / "100.00".to_d * product.price
+ end
+
+ def threshold(product, quantity, promotion_body)
+ count = quantity - promotion_body[0]
+ cents = promotion_body[1]
+ count <= 0 ? 0 : 0 - count * cents / "100.00".to_d * product.price
+ end
+end
+
+class Invoice
+ attr_accessor :text
+ LineSeparator = "+------------------------------------------------+----------+\n"
+ Header = "| Name qty | price |\n"
+ Template = "| %-43s%3s | %8s |\n"
+
+
+ def initialize(cart)
+ @text = LineSeparator + Header + LineSeparator
+ @calc = cart.calculator
+ cart.products.each do |name, quantity|
+ product = cart.inventory.products[name]
+ add_line(name, quantity, @calc.product_total(name, quantity))
+ add_promotion(product.promotion, product.name, quantity)
+ end
+ add_coupon cart.coupon_name, cart.coupon
+ add_footer
+ end
+
+ private
+
+ def add_line(name, quantity = "", price)
+ @text = @text + Template % [name, quantity, "%0.2f" % price.to_s('F')]
+ end
+
+ def add_footer
+ @text += LineSeparator
+ add_line("TOTAL", @calc.total)
+ @text += LineSeparator
+ end
+
+ def add_promotion(promo, product_name, quantity)
+ return if promo.nil?
+
+ promo_text = case promo.keys.first
+ when :get_one_free then " (buy %d, get 1 free)" % [promo.values.first-1]
+ when :package then package_text(promo.values.last.to_a.first)
+ when :threshold then threshold_text(promo.values.last.to_a.first)
+ end
+ add_line(promo_text, @calc.apply_promotion(product_name, quantity))
+ end
+
+ def package_text(values)
+ " (get %s off for every %d)" % [values.last.to_s + "%", values.first]
+ end
+
+ def threshold_text(values)
+ " (%s off of every after the %dth)" % [values.last.to_s + "%", values.first]
+ end
+
+ def add_coupon(coupon_name, coupon)
+ return if coupon.nil?
+ coupon_text = case coupon.keys.first
+ when :percent then percent_text(coupon_name, coupon.values.first)
+ when :amount then amount_text(coupon_name, coupon.values.first)
+ end
+ add_line(coupon_text, @calc.apply_coupon(@calc.sum_for_coupon) - @calc.total)
+ end
+
+ def percent_text(name, percent)
+ "Coupon %s - %s off" % [name, percent.to_s + "%"]
+ end
+
+ def amount_text(name, amount)
+ "Coupon %s - %s off" % [name, amount]
+ end
+end