Ивайло обнови решението на 03.11.2011 16:28 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+class Inventory
+ def initialize
+ @products, @coupons = {}, {}
+ end
+
+ def new_cart
+ Cart.new self
+ end
+
+ def register_coupon name, type
+ raise "Invalid parameters passed." if type.size > 1 or @coupons[name]
+
+ key, coupon_args = type.flatten
+ coupon_class = {percent: PercentCoupon, amount: AmountCoupon}[key]
+
+ @coupons[name] = coupon_class.new name, coupon_args
+ end
+
+ def register name, price, promotion = {}
+ price = price.to_d
+ if @products[name] or name.size > 40 or price < '0.01'.to_d or
+ price > '999.99'.to_d
+ raise "Invalid parameters passed."
+ end
+ @products[name] = Product.new name, price, promotion
+ end
+
+ def get_product name
+ raise "Invalid parameters passed" unless @products[name]
+ @products[name]
+ end
+
+ def get_coupon name
+ raise "Invalid parameters passed" unless @coupons[name]
+ @coupons[name]
+ end
+end
+
+class Cart
+ def initialize inventory
+ @inventory, @products = inventory, Hash.new('0'.to_d)
+ end
+
+ def orders
+ @products.map { |product, amount| Order.new product, amount }
+ end
+
+ def add name, amount = 1
+ raise "Invalid parameters passed" unless amount > 0 and amount < 100
+ @products[@inventory.get_product(name)] += amount
+ end
+
+ def use coupon_name
+ raise "Invalid parameters passed" unless @inventory.get_coupon coupon_name
+ @coupon_name = coupon_name
+ end
+
+ def total
+ products_total - coupon_discount(products_total)
+ end
+
+ def invoice
+ Printer.new.print orders, coupon, -coupon_discount(products_total), total
+ end
+
+ def coupon_discount value
+ @coupon_name ? @inventory.get_coupon(@coupon_name).get_discount(value) : 0.0
+ end
+
+ def products_total
+ @products.inject(0) do |accumulated, product|
+ accumulated + product[0].total_price_for_amount(product[1])
+ end
+ end
+
+ def coupon
+ @inventory.get_coupon @coupon_name if @coupon_name
+ end
+end
+
+class Order
+ attr_reader :product, :amount
+
+ def initialize product, amount
+ @product, @amount = product, amount
+ end
+
+ def to_s
+ @product.invoice_s @amount
+ end
+
+ def invoice_s amount_s, discount_s
+ amount_s.call(@product.name, "%.0f" % @amount, @product.price_for_amount(@amount)) +
+ discount_s.call(@product.promotion, -@product.discount_for_amount(@amount))
+ end
+end
+
+class Product
+ attr_reader :name, :promotion
+
+ def initialize name, price, promotion
+ @name, @price= name, price
+ promotions = {
+ get_one_free: GetOneFreePromotion,
+ package: PackagePromotion,
+ threshold: ThresholdPromotion
+ }
+
+ key, args = promotion.flatten
+ unless promotion.empty?
+ @promotion = promotions[key].new self, *[*args].flatten
+ end
+ end
+
+ def price_for_amount amount
+ amount * @price
+ end
+
+ def discount_for_amount amount
+ @promotion ? @promotion.calculate_discount(amount.to_i) : '0'.to_d
+ end
+
+ def total_price_for_amount amount
+ price_for_amount(amount) - discount_for_amount(amount)
+ end
+
+ def eql? other
+ @name.eql? other.name
+ end
+
+ def hash
+ @name.hash
+ end
+end
+
+class GetOneFreePromotion
+ def initialize product, count
+ raise "Invalid parameters passed" unless count > 0
+ @product, @count = product, count
+ end
+
+ def calculate_discount amount
+ @product.price_for_amount(amount / @count)
+ end
+
+ def to_s
+ "(buy #{@count-1}, get 1 free)"
+ end
+end
+
+class PackagePromotion
+ def initialize product, count, discount_percent
+ raise "Invalid parameters passed" unless count > 0 && discount_percent > 0
+ @product, @count, @discount_percent = product, count, discount_percent
+ end
+
+ def calculate_discount amount
+ @product.price_for_amount((amount / @count ) * @count) * @discount_percent / 100.0
+ end
+
+ def to_s
+ "(get #{@discount_percent.to_i}% off for every #{@count})"
+ end
+end
+
+class ThresholdPromotion
+ def initialize product, amount, percent
+ raise "Invalid parameters passed" unless amount > 0 and percent > 0 and percent < 100
+ @product, @amount, @percent = product, amount, percent
+ end
+
+ def calculate_discount amount
+ if amount > @amount
+ @product.price_for_amount(amount - @amount) * @percent / 100.0
+ else
+ '0.00'.to_d
+ end
+ end
+
+ def to_s
+ suffixes = Hash.new('th').merge({1 => 'st', 2 => 'nd', 3 => 'rd'})
+ "(#{@percent.to_i}% off of every after the #{@amount}#{suffixes[@amount]})"
+ end
+end
+
+class PercentCoupon
+ def initialize name, percent
+ raise "Invalid parameters passed" unless percent > 0 and percent < 100
+ @name, @percent = name, percent
+ end
+
+ def get_discount sum
+ @percent * sum / 100
+ end
+
+ def to_s
+ "Coupon #{@name} - #{@percent.to_i}% off"
+ end
+end
+
+class AmountCoupon
+ def initialize name, amount
+ @name, @amount = name, amount
+ end
+
+ def get_discount sum
+ @amount > sum ? sum : @amount
+ end
+
+ def to_s
+ "Coupon #{@name} - " + "%.2f off" % @amount
+ end
+end
+
+class Printer
+ VerticalSeparator = "+------------------------------------------------+----------+\n"
+
+ ProductToS = ->(name, amount, price, format = '.2f') do
+ "| #{name.ljust(43)}#{amount.rjust(3)} |" + " %8#{format} |\n" % price
+ end
+
+ TotalToS = ->(str, price) do
+ str and str != '' ? "| #{str.ljust(46)} |" + " %8.2f |\n" % price : ''
+ end
+
+ DiscountToS = ->(promotion, price) do
+ promotion ? TotalToS.call(' ' + promotion.to_s, price) : ''
+ end
+
+ def print orders, coupon, coupon_discount, total
+ orders_s = orders.map { |order| order.invoice_s(ProductToS, DiscountToS)}
+ coupon_s = TotalToS.call(coupon.to_s, coupon_discount)
+ orders_s.inject(make_header, &:+) + coupon_s + total_s(total)
+ end
+
+ def make_header
+ VerticalSeparator + ProductToS.call('Name', 'qty', 'price', 's') + VerticalSeparator
+ end
+
+ def total_s total
+ total = " %8.2f " % total
+ VerticalSeparator + TotalToS.call('TOTAL', total) + VerticalSeparator
+ end
+end