Йоан обнови решението на 07.11.2011 16:23 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+class InventoryError < Exception
+end
+
+
+class Promotion
+ def self.create type, arg
+ return GetOneFreePromotion.new(arg) if type == :get_one_free
+ return PackagePromotion.new(arg) if type == :package
+ return ThresholdPromotion.new(arg) if type == :threshold
+ return NullPromotion.new if type == nil
+ end
+
+ def append_to_invoice(invoice, amount)
+ invoice.row(@name, '', amount)
+ end
+end
+
+class GetOneFreePromotion < Promotion
+ def initialize(every_n_th)
+ @name = " (buy #{every_n_th-1}, get 1 free)"
+ @every_n_th = every_n_th
+ end
+
+ def apply(price, count)
+ free_items_count = count / @every_n_th
+ price * free_items_count
+ end
+end
+
+class PackagePromotion < Promotion
+ def initialize(arg)
+ @package_size, @percentage = arg.to_a[0]
+ @name = " (get #{@percentage}% off for every #{@package_size})"
+ end
+
+ def apply(price, count)
+ packages_with_discount = count / @package_size * @package_size
+ price * packages_with_discount * @percentage.to_s.to_d / '100'.to_d
+ end
+end
+
+class ThresholdPromotion < Promotion
+ def initialize(arg)
+ @after_n_th, @percentage = arg.to_a[0]
+
+ #TODO 1st / 2nd/ 3rd suffixes
+ @name = " (#{@percentage}% off of every after the #{@after_n_th}th)"
+ end
+
+ def apply(price, count)
+ items_with_discount = count - @after_n_th
+ items_with_discount = 0 if items_with_discount < 0
+ price * items_with_discount * @percentage.to_s.to_d / '100'.to_d
+ end
+end
+
+class NullPromotion < Promotion
+ def append_to_invoice(invoice, amount)
+ end
+
+ def apply(price, count)
+ '0'.to_d
+ end
+end
+
+class Coupon
+
+ def self.create name, type, arg
+ return PercentDiscountCoupon.new(name, arg) if type == :percent
+ return AmountDiscountCoupon.new(name, arg) if type == :amount
+ return NullCoupon.new if type == nil
+ end
+
+ def append_to_invoice(invoice, total)
+ invoice.row @name, '', apply(total)
+ end
+end
+
+class PercentDiscountCoupon < Coupon
+ def initialize(name, percentage)
+ @factor = ('100'.to_d - percentage.to_s.to_d) / '100'.to_d
+ @name = "Coupon #{name} - #{percentage}% off"
+ end
+
+ def apply(total)
+ total * @factor
+ end
+end
+
+class AmountDiscountCoupon < Coupon
+ def initialize(name, discount)
+ @discount = discount.to_d
+ @name = "Coupon #{name} - #{discount} off"
+ end
+
+ def apply(total)
+ discounted_price = total - @discount
+ if discounted_price < 0
+ return '0'.to_d
+ else
+ return discounted_price
+ end
+ end
+end
+
+class NullCoupon < Coupon
+
+ def apply(total)
+ total
+ end
+
+ def append_to_invoice(invoice, total)
+ end
+end
+
+class InvoiceBuilder
+
+ # TODO: move special symbols to constants
+
+ def initialize
+ @result = ''
+ end
+
+ def separator
+ row('', '', '', '+', '-')
+ self
+ end
+
+ def row(name, qantity, price, separator = '|', blank = ' ')
+ @result << separator + blank
+ @result << name.ljust(40, blank) + blank
+ @result << qantity.rjust(5, blank) + blank
+ @result << separator + blank
+ @result << price.rjust(8, blank) + blank
+ @result << separator + "\n"
+ self
+ end
+
+ def append(item, arg)
+ item.append_to_invoice(self, arg)
+ self
+ end
+
+ def to_s
+ @result
+ end
+
+end
+
+class Cart
+ def initialize(inventory)
+ @inventory = inventory
+ @items_count = Hash.new 0
+ @coupon = NullCoupon.new
+ end
+
+ def add(item, count = 1)
+ raise InventoryError unless count_allowed?(@items_count[item] + count)
+ raise InventoryError unless @inventory.items.include? item
+ @items_count[item] += count
+ end
+
+ def use(coupon_name)
+ @coupon = @inventory.coupons[coupon_name]
+ end
+
+ def total
+ total_price = @items_count.keys.map { |item| price(item) + discount(item) }.inject(:+)
+ total_price = @coupon.apply total_price
+ total_price
+ end
+
+ def invoice
+ invoice = InvoiceBuilder.new.separator.row('Name', 'qty', 'price').separator
+ @items_count.each do |item, count|
+ invoice.row(item, count.to_s, format_dec(price(item)))
+ invoice.append(@inventory.promotions[item], format_dec(discount(item)))
+ end
+ total_amount = format_dec(total)
+ invoice.append(@coupon, total_amount).separator
+ invoice.row('TOTAL', '', total_amount).separator.to_s
+ end
+
+ private
+
+ MIN_ITEM_COUNT = 1
+ MAX_ITEM_COUNT = 99
+
+ def format_dec(decimal)
+ '%.2f' % decimal
+ end
+
+ def count_allowed?(count)
+ count >= MIN_ITEM_COUNT and count <= MAX_ITEM_COUNT
+ end
+
+ def price(item)
+ @inventory.items[item] * @items_count[item]
+ end
+
+ def discount(item)
+ - @inventory.promotions[item].apply(@inventory.items[item], @items_count[item])
+ end
+end
+
+class Inventory
+ def initialize
+ @items = {}
+ @promotions = {}
+ @coupons = {}
+ end
+
+ def register(item, price, promotion = {})
+ decimal_price = price.to_d
+ raise InventoryError unless price_allowed? decimal_price
+ raise InventoryError if @items.include? item
+ raise InventoryError if item.length > MAX_ITEM_NAME_LENGTH
+ @items[item] = decimal_price
+ type, arg = get_single_argument promotion
+ @promotions[item] = Promotion.create type, arg
+ end
+
+ def register_coupon(name, coupon_type)
+ type, arg = get_single_argument coupon_type
+ @coupons[name] = Coupon.create name, type, arg
+ end
+
+ def new_cart
+ Cart.new self
+ end
+
+ attr_reader :items, :coupons, :promotions
+
+ private
+
+ MAX_ALLOWED_PRICE = '999.99'.to_d
+ MIN_ALLOWED_PRICE = '0.01'.to_d
+ MAX_ITEM_NAME_LENGTH = 40
+
+ def price_allowed?(price)
+ price >= MIN_ALLOWED_PRICE and price <= MAX_ALLOWED_PRICE
+ end
+
+ def get_single_argument(args)
+ if args.length == 0
+ return nil, nil
+ elsif args.length > 1
+ raise InventoryError # TODO - only one named arguemnt can be applied
+ else
+ return args.to_a[0]
+ end
+ end
+
+end