Георги обнови решението на 06.11.2011 16:20 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+class Inventory
+ def initialize
+ @articles, @coupons = {}, {}
+ is_new = ->(available) { ->(name) { !self.send available, name } }
+ @article_constraints = Constraints.new custom: is_new.(:available?)
+ @coupon_constraints = Constraints.new custom: is_new.(:available_coupon?)
+ @article_length_constraints = Constraints.new in: 0..40
+ @price_constraints = Constraints.new in: (0.01..999.99)
+ only_one_discount = ->(discount) { discount.keys.size <= 1 }
+ @discount_constraints = Constraints.new custom: only_one_discount
+ end
+
+
+ def register article_name, price = '0.00', discount = {}
+ enforce_constraints article_name, price, discount
+ @articles[article_name] = Article.new article_name, price.to_d, discount
+ end
+
+ def register_coupon coupon_name, parameter = {}
+ @coupon_constraints.enforce coupon_name
+ @coupons[coupon_name] = Coupon.new coupon_name, parameter
+ end
+
+ def article article_name
+ @articles[article_name]
+ end
+
+ def coupon coupon_name
+ @coupons[coupon_name]
+ end
+
+ def available? article_name
+ @articles.has_key? article_name
+ end
+
+ def available_coupon? coupon_name
+ @coupons.has_key? coupon_name
+ end
+
+ def new_cart
+ Cart.new self
+ end
+
+ private
+
+ def enforce_constraints article_name, price, discount
+ @article_constraints.enforce article_name
+ @article_length_constraints.enforce article_name.length
+ @price_constraints.enforce price.to_d
+ @discount_constraints.enforce discount
+ end
+end
+
+class Article
+ attr_reader :name, :price, :discount
+
+ def initialize name, price, discount = {}
+ @name, @price, @discount = name, price, discount
+ end
+
+ def has_discount? quantity
+ @discount.size > 0 && calculate_discount(quantity) > 0
+ end
+
+ def discount_type
+ @discount.keys.first
+ end
+
+ def discount_name
+ contract = @discount[discount_type]
+ case discount_type
+ when :get_one_free then "(buy #{contract - 1}, get 1 free)"
+ when :package then "(get #{percent}% off for every #{count})"
+ when :threshold then "(#{percent}% off of every after the #{ordinal count})"
+ end
+ end
+
+ def calculate_price quantity
+ price * quantity
+ end
+
+ def calculate_discount quantity
+ [case discount_type
+ when :get_one_free then (quantity / discount[:get_one_free]) * price
+ when :package then (quantity / count) * (percent / 100.0) * calculate_price(count)
+ when :threshold then (quantity - count) * (percent / 100.0) * price
+ else 0
+ end, 0].max
+ end
+
+ private
+
+ def count
+ @discount[discount_type].keys.first
+ end
+
+ def percent
+ @discount[discount_type].values.first
+ end
+
+ def ordinal num
+ case num
+ when 1 then "1st"
+ when 2 then "2nd"
+ when 3 then "3rd"
+ else "#{num}th"
+ end
+ end
+
+end
+
+class Coupon
+ attr_reader :name
+
+ def initialize name, parameter
+ @name, @parameter = name, parameter
+ end
+
+ def calculate_discount total
+ discount = [case coupon_type
+ when :amount then @parameter[:amount].to_d
+ when :percent then total * (@parameter[:percent] / 100.0)
+ else total
+ end, 0].max
+ [total, discount].min
+ end
+
+ def to_s
+ discount = case coupon_type
+ when :amount then @parameter[:amount]
+ when :percent then "#{@parameter[:percent]}%"
+ else 0
+ end
+ "Coupon #{name} - #{discount} off"
+ end
+
+ private
+
+ def coupon_type
+ @parameter.keys.first
+ end
+end
+
+class Cart
+ attr_reader :coupon
+
+ def initialize inventory
+ @inventory = inventory
+ @cart = Hash.new(0)
+ exists = ->(available) { ->(name) { @inventory.send available, name } }
+ @article_constraints = Constraints.new custom: exists.(:available?)
+ @coupon_constraints = Constraints.new custom: exists.(:available_coupon?)
+ @quantity_constraints = Constraints.new in: 0..99
+ end
+
+ def add article_name, quantity = 1
+ enforce_constraints article_name, quantity
+ @cart[article_name] += quantity
+ end
+
+ def use coupon_name
+ @coupon_constraints.enforce coupon_name
+ @coupon = @inventory.coupon coupon_name
+ end
+
+ def total_no_coupon
+ total = @cart.inject(0) do |result, (article_name, quantity)|
+ article = @inventory.article article_name
+ result + article.calculate_price(quantity) - article.calculate_discount(quantity)
+ end
+ end
+
+ def total
+ discount = (@coupon.calculate_discount(total_no_coupon) if @coupon) || 0
+ [total_no_coupon - discount, 0].max
+ end
+
+ def invoice
+ Invoice.new(self).to_s
+ end
+
+ def quantity article
+ @cart[article.name]
+ end
+
+ def articles
+ @cart.keys.map { |article_name| @inventory.article article_name }
+ end
+
+ private
+
+ def enforce_constraints article_name, quantity
+ @article_constraints.enforce article_name
+ @quantity_constraints.enforce quantity
+ article = @inventory.article article_name
+ @quantity_constraints.enforce quantity(article) + quantity
+ end
+end
+
+class Invoice
+ NameColumnSize = 48
+ PriceColumnSize = 10
+
+ def initialize cart
+ @cart = cart
+ end
+
+ def to_s
+ table = Array(header)
+ table += @cart.articles.map { |article| article_row article }
+ table += coupon_row if @cart.coupon
+ table += footer
+ table.join "\n"
+ end
+
+ private
+
+ def article_row article
+ quantity = @cart.quantity(article)
+ price = format_price article.calculate_price(quantity)
+ row = [table_row(article.name, quantity, price)]
+ discount_row = discount_row article, quantity if article.has_discount? quantity
+ row += Array(discount_row)
+ end
+
+ def discount_row article, quantity
+ discount = article.calculate_discount(quantity) * -1
+ discount_row = table_row article.discount_name, "", format_price(discount), 3
+ end
+
+ def coupon_row
+ discount = @cart.coupon.calculate_discount(@cart.total_no_coupon) * -1 || 0
+ coupon_row = table_row @cart.coupon.to_s, "", format_price(discount)
+ Array(coupon_row)
+ end
+
+ def table_row name, quantity, price, indent = 1
+ name_width = NameColumnSize - quantity.to_s.length - indent - 1
+ name_format = " " * indent + "%-#{name_width}s#{quantity} "
+ name_column = format name_format, name, quantity
+ price_format = "%#{PriceColumnSize - 1}s "
+ price_column = format price_format, price
+ ["", name_column, price_column, ""].join "|"
+ end
+
+ def header
+ [separator, table_row("Name", "qty", "price"), separator]
+ end
+
+ def footer
+ [separator, table_row("TOTAL", "", format_price(@cart.total)), separator, ""]
+ end
+
+ def separator
+ ["", '-' * NameColumnSize, '-' * PriceColumnSize, ""].join '+'
+ end
+
+ def format_price price
+ format "%.2f", price
+ end
+end
+
+class Constraints
+ def initialize constraints = {}
+ @constraints = constraints
+ end
+
+ def enforce arg
+ validity_vector = @constraints.keys.map { |type| satisfies? type, arg }
+ raise build_error_message(validity_vector, arg) unless validity_vector.all?
+ end
+
+ private
+
+ def satisfies? constraint_type, arg
+ limits = @constraints[constraint_type]
+ case constraint_type
+ when :in then limits.include? arg
+ when :<, :<=, :==, :>, :>= then arg.send constraint_type, limits
+ when :custom then limits.(arg)
+ else true
+ end
+ end
+
+ def build_error_message validity_vector, arg
+ zipped_results = validity_vector.zip @constraints.keys
+ failed = zipped_results.select { |passed, constraint| constraint unless passed }
+ failed = failed.map do |passed, constraint|
+ "#{arg} #{constraint} #{@constraints[constraint]}"
+ end
+ "The following constraints were not satisfied: #{failed.to_s}"
+ end
+end