Евгени обнови решението на 06.11.2011 22:46 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+require 'active_support/core_ext/integer/inflections'
+
+class Inventory
+ attr_reader :products, :coupons
+ def initialize
+ @products = {}
+ @coupons = {}
+ end
+
+ def register(name, price, promotion={})
+ price = price.to_d
+ raise "Duplicate #{name}" if @products.include? name
+ raise "Negative price for #{name}" if price <= 0
+ raise "#{name} - to long name" if name.length > 40
+ raise "#{name} is too expensive" if price > 999.99
+ raise "#{name} has more than one promotion" if promotion.length > 1
+ @products[name] = Product.new(name, price, promotion)
+ end
+
+ def register_coupon(name, coupon_data)
+ raise "Coupon #{name} already registered" if @coupons.include? name
+ raise "Bad data for coupon #{name}" if coupon_data.length != 1
+ @coupons[name] = Coupon.new(*coupon_data.to_a.flatten)
+ end
+
+ def new_cart
+ ShoppingCart.new self
+ end
+
+ def cost_of(count, product)
+ @products[product].cost_for count
+ end
+
+ def discount_from_coupon(coupon, price)
+ @coupons[coupon].discount_from_coupon(price)
+ end
+end
+
+
+class ShoppingCart
+ attr_reader :products, :coupon, :inventory
+
+ def initialize(inventory)
+ @inventory = inventory
+ @products = {}
+ @products.default = 0
+ end
+
+ def add(name, count=1)
+ raise "No #{name} in inventory" if not @inventory.products.include? name
+ raise "Negative count for #{name}" if @products[name] + count <= 0
+ raise "Too much #{name}" if @products[name] + count > 99
+ @products[name] += count
+ end
+
+ def use(coupon)
+ raise "Missing coupon #{coupon} " if not @inventory.coupons.include? coupon
+ @coupon = coupon
+ end
+
+ def total
+ @coupon ? price - @inventory.discount_from_coupon(@coupon, price) : price
+ end
+
+ def price
+ @products.map{|name, count| @inventory.cost_of count, name }.inject(&:+)
+ end
+
+ def invoice
+ InvoicePrinter.new(self).get_invoice
+ end
+end
+
+
+class Coupon
+ attr_reader :type, :count
+ def initialize(type, count)
+ @type = type
+ @count = count.to_s.to_d
+ end
+
+ def total_after_discount(total)
+ total - discount_from_coupon(total)
+ end
+
+ def discount_from_coupon(price)
+ case @type
+ when :percent
+ price*@count/100
+ when :amount
+ @count < price ? @count : price
+ end
+ end
+end
+class Product
+ attr_reader :name, :price, :promotion_type,
+ :promotion_attr, :group_size,
+ :discount_percent, :threshold
+ alias eq? ==
+
+ def initialize(name, price, promotion)
+ @name, @price = name, price
+ @promotion_type, @promotion_attr = *promotion.to_a.flatten
+ extract_promotion_params
+ end
+
+ def extract_promotion_params
+ case @promotion_type
+ when :package
+ @group_size, @discount_percent = *@promotion_attr.to_a.flatten
+ when :threshold
+ @threshold, @discount_percent = *@promotion_attr.to_a.flatten
+ end
+ end
+
+ def cost_for(count)
+ base_price = count*@price
+ discount = discount_for count
+ @price*count - discount
+ end
+
+ def discount_for(count)
+ case @promotion_type
+ when :get_one_free
+ get_one_free_discount count
+ when :package
+ package_discount count
+ when :threshold
+ threshold_discount count
+ else
+ 0
+ end
+ end
+
+ def get_one_free_discount(count)
+ @price*(count/@promotion_attr)
+ end
+
+ def package_discount(count)
+ groups = count/@group_size
+ groups*@group_size*@price*@discount_percent/100
+ end
+
+ def threshold_discount(count)
+ return 0 if count <= @threshold
+ (count - @threshold)*@price*@discount_percent/100
+ end
+end
+
+
+class InvoicePrinter
+ HEADER = <<HEADER
++#{'-'*48}+#{'-'*10}+\n| Name#{' '*39}qty | price |\n+#{'-'*48}+#{'-'*10}+
+HEADER
+ TOTAL = <<TOTAL
++#{'-'*48}+#{'-'*10}+\n| TOTAL#{' '*41} | %5.2f |\n+#{'-'*48}+#{'-'*10}+
+TOTAL
+
+ def initialize(cart)
+ @cart = cart
+ end
+
+ def get_invoice
+ all_prods = @cart.inventory.products
+ strings = @cart.products.map { |name, count| product_invoice(all_prods[name], count) }
+ strings << (coupon_invoice(@cart.coupon) or '')
+ strings << TOTAL % [@cart.total]
+ strings.inject(HEADER) { |str1, str2| str1 + str2 }
+ end
+
+ def product_invoice(product, count)
+ product_invoice_line(product, count) +
+ (promotion_invoice_line(product, count) or '')
+ end
+
+ private
+ def product_invoice_line(product, count)
+ "| %-44s%2s |%9.2f |\n" % [product.name, count, count*product.price]
+ end
+
+ def promotion_invoice_line(product, count)
+ if product.promotion_type == :get_one_free
+ one_free_promotion_line product, count
+ elsif product.promotion_type == :package
+ package_promotion_line product, count
+ elsif product.promotion_type == :threshold
+ threshold_promotion_line product, count
+ end
+ end
+
+ def one_free_promotion_line(product, count)
+ "| %-45s" %
+ ("(buy %d, get 1 free)" % [product.promotion_attr-1]) +
+ "|%9.2f |\n" % [-product.discount_for(count)]
+ end
+
+ def package_promotion_line(product, count)
+ "| %-45s" % ("(get %s%% off for every %s)" %
+ [product.discount_percent, product.group_size]) +
+ "|%9.2f |\n" % [-product.discount_for(count)]
+ end
+
+ def threshold_promotion_line(product, count)
+ "| %-45s" % "(%s%% off of every after the %s)" %
+ [product.discount_percent, product.threshold.ordinalize] +
+ "|%9.2f |\n" % [-product.discount_for(count)]
+ end
+
+ def coupon_invoice(coupon)
+ return if not coupon
+ discount = @cart.inventory.coupons[coupon].discount_from_coupon(@cart.price)
+ "| Coupon #{coupon} - #{coupon_description coupon} off".ljust(49) +
+ "| %8.2f |\n" % -discount
+ end
+
+ def coupon_description(coupon_name)
+ coupon = @cart.inventory.coupons[coupon_name]
+ case coupon.type
+ when :percent
+ "%d%%" % coupon.count
+ when :amount
+ "%.2f" % coupon.count
+ end
+ end
+end