Славена обнови решението на 07.11.2011 10:59 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+class Integer
+ def ordinal
+ sufix = %w{ th st nd rd th th th th th th }
+ self.to_s + (self / 10 == 1 ? 'th' : sufix[self%10])
+ end
+end
+
+class Product
+ attr_reader :name, :promotion
+
+ def initialize name, price_to_string, promotion = nil
+ @name = name
+ @price = price_to_string.to_d
+ validate
+ @promotion = Promotion.get_promotion promotion if promotion
+ end
+
+ def discount quantity
+ @promotion ? @promotion.discount(@price, quantity) : 0
+ end
+
+ def price quantity
+ @price * quantity
+ end
+
+ def discounted_price quantity
+ price(quantity) - discount(quantity)
+ end
+
+ def validate
+ raise "Product name is too long" if @name.length > 40
+ raise "Product price is not valid" if not @price.between?(0.01, 999.99)
+ end
+end
+
+class GetOneFree
+ def initialize promotion_params
+ @product_count = promotion_params
+ end
+
+ def discount price, quantity
+ (quantity / @product_count) * price
+ end
+
+ def to_s
+ "buy %d, get 1 free" % (@product_count - 1)
+ end
+end
+
+class Package
+ def initialize promotion_params
+ @product_count, @percents_discount = promotion_params.flatten
+ end
+
+ def discount price, quantity
+ discount_per_package = price * @product_count * @percents_discount / 100
+ (quantity / @product_count) * discount_per_package
+ end
+
+ def to_s
+ "get %d%% off for every %d" % [@percents_discount, @product_count]
+ end
+end
+
+class Threshold
+ def initialize promotion_params
+ @product_count, @percents_discount = promotion_params.flatten
+ end
+
+ def discount price, quantity
+ discounted_quantity = [quantity - @product_count, '0'.to_d].max
+ discounted_quantity * price * @percents_discount / 100
+ end
+
+ def to_s
+ "%d%% off of every after the %s" % [@percents_discount, @product_count.ordinal]
+ end
+end
+
+class Promotion
+ PROMOTION_TYPES = {
+ get_one_free: ->(x) { GetOneFree.new x } ,
+ package: ->(x) { Package.new x } ,
+ threshold: ->(x) { Threshold.new x }
+ }
+
+ def Promotion.get_promotion promotion_map
+ promotion_type, promotion_args = promotion_map.flatten
+ PROMOTION_TYPES[promotion_type].(promotion_args)
+ end
+end
+
+class PercentCoupon
+ def initialize name, percent
+ @name = name
+ @percent = percent
+ end
+
+ def discount total
+ @percent * total / 100
+ end
+
+ def to_s
+ "Coupon %s - %d%% off" % [@name, @percent]
+ end
+end
+
+class AmountCoupon
+ def initialize name, amount
+ @name = name
+ @amount = amount.to_d
+ end
+
+ def discount total
+ [total, @amount].min
+ end
+
+ def to_s
+ "Coupon %s - %.2f off" % [@name, @amount]
+ end
+end
+
+class Coupon
+ COUPON_TYPES = {
+ percent: ->(x, y) { PercentCoupon.new x, y },
+ amount: ->(x, y) { AmountCoupon.new x, y }
+ }
+
+ def Coupon.get_coupon name, coupon_map
+ coupon_type, coupon_param = coupon_map.flatten
+ COUPON_TYPES[coupon_type].(name, coupon_param)
+ end
+end
+
+class Inventory
+ attr_reader :products, :coupons
+
+ def initialize
+ @products = {}
+ @coupons = {}
+ end
+
+ def register name, price, promotion = nil
+ raise "This product already exists in inventory" if @products[name]
+ @products[name] = Product.new name, price, promotion
+ end
+
+ def register_coupon name, props
+ @coupons[name] = Coupon.get_coupon name, props
+ end
+
+ def new_cart
+ Cart.new self
+ end
+end
+
+class Cart
+ attr_reader :products, :quantity, :coupon
+
+ def initialize inventory
+ @inventory = inventory
+ @products = {}
+ @quantity = Hash.new 0
+ end
+
+ def add product_name, quantity = 1
+ @products[product_name] = @inventory.products.fetch(product_name) do
+ raise "No such product in inventory."
+ end
+ unless quantity.between? 1, 99
+ raise "Invalid product count."
+ end
+ @quantity[product_name] += quantity
+ end
+
+ def use coupon_name
+ @coupon = @inventory.coupons.fetch(coupon_name) do
+ raise "This coupon does not exist."
+ end
+ end
+
+ def discount
+ @coupon ? @coupon.discount(clear_total) : 0
+ end
+
+ def clear_total
+ @products.inject(0) do |total, product|
+ total += product[1].discounted_price @quantity[product[0]]
+ end
+ end
+
+ def total
+ total_no_discount = clear_total
+ total_no_discount - discount
+ end
+
+ def invoice
+ BillFormatter.new(self).bill
+ end
+end
+
+class Templates
+ ROW = "| %s | %8.2f |\n"
+ PRODUCT = "%-43s%3d"
+ PROMOTION = " (%s)"
+ DESCRIPTION = "%-46s"
+ TOTAL = DESCRIPTION % "TOTAL"
+ BORDER = "+%s+%s+\n" % ["-" * 48, "-" * 10]
+ TITLE = "| %-43s%3s | %8s |\n" % ["Name", "qty", "price"]
+ HEADER = [BORDER, TITLE, BORDER].join
+ FOOTER = [BORDER, "%s", BORDER].join
+end
+
+
+class BillFormatter
+ def initialize cart
+ @cart = cart
+ end
+
+ def bill
+ footer = Templates::FOOTER % format_total(@cart.total)
+ products_list = []
+ @cart.products.each do |name, product|
+ quantity = @cart.quantity[name]
+ products_list << format_product(product, quantity)
+ products_list << format_promotion(product, quantity) if product.promotion
+ end
+ products_list << format_coupon if @cart.coupon
+ [Templates::HEADER, products_list.join, footer].join
+ end
+
+ def format_product product, quantity
+ descripton = Templates::PRODUCT % [product.name, quantity]
+ Templates::ROW % [descripton, product.price(quantity)]
+ end
+
+ def format_promotion product, quantity
+ promotion_string = Templates::PROMOTION % product.promotion.to_s
+ descripton = Templates::DESCRIPTION % promotion_string
+ Templates::ROW % [descripton, -1 * product.discount(quantity)]
+ end
+
+ def format_coupon
+ descripton = Templates::DESCRIPTION % @cart.coupon.to_s
+ Templates::ROW % [descripton, -1 * @cart.discount]
+ end
+
+ def format_total total
+ Templates::ROW % [Templates::TOTAL, total]
+ end
+end