Веселин обнови решението на 04.11.2011 00:47 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+class Inventory
+ def initialize
+ @articles = {}
+ @coupons = {}
+ end
+
+ def register(name, price, promotion = {})
+ if (name.length > 40 || 0.01 > price.to_f || price.to_f > 999.99 ||
+ @articles.has_key?(name))
+ raise "Invalid parameters passed."
+ end
+ @articles[name] = Article.new(name, price.to_d, promotion)
+ end
+
+ def register_coupon(name, coupon_data)
+ if (@coupons.has_key? name)
+ raise "A coupon with the same name already exists."
+ end
+ @coupons[name] = Coupon.create(name, coupon_data)
+ end
+
+ def new_cart
+ Cart.new(@articles, @coupons)
+ end
+end
+
+class Cart
+ def initialize(articles, coupons)
+ @articles, @coupons = articles, coupons
+ @articles_in_cart, @coupon_in_use = Hash.new(0), nil
+ end
+
+ def add(item_name, amount = 1)
+ if (!@articles.has_key?(item_name) || amount < 0 || amount > 99)
+ raise "Invalid parameters passed."
+ end
+ @articles_in_cart[item_name] += amount
+ end
+
+ def use(coupon_name)
+ if (@coupons.has_key? coupon_name)
+ @coupon_in_use = @coupons[coupon_name]
+ else
+ raise "A coupon with the specified name doesn't exist."
+ end
+ end
+
+ def total
+ total = @articles_in_cart.inject('0'.to_d) do |sum, (k, v)|
+ sum + @articles[k].calculate_total(v)
+ end
+
+ @coupon_in_use ? @coupon_in_use.calculate_total(total) : total
+ end
+
+ def invoice
+ InvoiceCreator.create_invoice(@articles, @articles_in_cart, total, @coupon_in_use)
+ end
+end
+
+class Article
+ attr_reader :name
+
+ def initialize(name, price, promotion)
+ @name, @price, @quantity = name, price
+ if !promotion.empty?
+ @promotion = Promotion.create(promotion)
+ else
+ @promotion = nil
+ end
+ end
+
+ def calculate_total(number_of_items)
+ if @promotion.nil?
+ number_of_items * @price
+ else
+ @promotion.calculate_total(number_of_items, @price)
+ end
+ end
+
+ def get_invoice_info(number_of_items)
+ invoice_info = [{:name => "#{@name}", :quantity => number_of_items,
+ :price => (@price * number_of_items)}]
+ if @promotion
+ invoice_info.push({:name => @promotion.to_s_representation,
+ :price => -@promotion.calculate_discount(number_of_items, @price)})
+ end
+
+ invoice_info
+ end
+end
+
+class InvoiceCreator
+ DelimiterLine = "+#{"-" * 48}+#{"-" * 10}+\n"
+
+ def InvoiceCreator.create_invoice(items, items_in_cart, total, coupon = nil)
+ invoice = ""
+ invoice += InvoiceCreator.create_header
+
+ items_in_cart.each do |item_name, quantity|
+ items[item_name].get_invoice_info(quantity).each do |invoice_info|
+ invoice += print_product_line(invoice_info)
+ end
+ end
+ if (coupon)
+ invoice += InvoiceCreator.print_product_line :name => coupon.to_s_representation,
+ :price => -coupon.calculate_discount(total)
+ end
+
+ invoice += InvoiceCreator.create_footer(total)
+ end
+
+ private
+
+ def InvoiceCreator.create_header
+ DelimiterLine + (InvoiceCreator.print_title_line :name => "Name",
+ :quantity => "qty", :price => "price") + DelimiterLine
+ end
+
+ def InvoiceCreator.create_footer(total)
+ DelimiterLine + format("| %s | %8.2f |\n", "TOTAL".ljust(46), total.to_f) +
+ DelimiterLine
+ end
+
+ def InvoiceCreator.print_title_line(line_info)
+ format("| %s %3s | %8s |\n", line_info[:name].ljust(42), line_info[:quantity],
+ line_info[:price])
+ end
+
+ def InvoiceCreator.print_product_line(line_info)
+ if (line_info[:quantity])
+ format("| %s %3d | %8.2f |\n", line_info[:name].ljust(42), line_info[:quantity],
+ line_info[:price].to_f)
+ else
+ format("| %s | %8.2f |\n", line_info[:name].ljust(46), line_info[:price].to_f)
+ end
+ end
+end
+
+class Promotion
+ def Promotion.create(promotion_data)
+ promotion_type, promotion_attrs = promotion_data.flatten
+ {:get_one_free => GetOneFreePromotion, :package => PackagePromotion,
+ :threshold => ThresholdPromotion}[promotion_type].new(promotion_attrs)
+ end
+end
+
+class GetOneFreePromotion
+ def initialize(free_item_frequency)
+ @free_item_frequency = free_item_frequency
+ end
+
+ def calculate_total(number_of_items, item_price)
+ (number_of_items - number_of_items / @free_item_frequency) * item_price
+ end
+
+ def calculate_discount(number_of_items, item_price)
+ (number_of_items / @free_item_frequency) * item_price
+ end
+
+ def to_s_representation
+ " (buy #{@free_item_frequency - 1}, get 1 free)"
+ end
+end
+
+class PackagePromotion
+ def initialize(promotion_attrs)
+ @package_size, discount_percent = promotion_attrs.flatten
+ @discount_percent = discount_percent / '100'.to_d
+ end
+
+ def calculate_total(number_of_items, item_price)
+ items_at_regular_price = number_of_items % @package_size
+
+ items_at_regular_price * item_price +
+ (number_of_items - items_at_regular_price) * item_price * (1 - @discount_percent)
+ end
+
+ def calculate_discount(number_of_items, item_price)
+ (number_of_items - number_of_items % @package_size) * @discount_percent * item_price
+ end
+
+ def to_s_representation
+ " (get #{(@discount_percent * 100).to_i}% off for every #{@package_size})"
+ end
+end
+
+class ThresholdPromotion
+ def initialize(promotion_attrs)
+ @threshold, discount_percent = promotion_attrs.flatten
+ @discount_percent = discount_percent / '100'.to_d
+ end
+
+ def calculate_total(number_of_items, item_price)
+ if number_of_items > @threshold
+ discounted_items = number_of_items - @threshold
+ (discounted_items * (1 - @discount_percent) + @threshold) * item_price
+ else
+ number_of_items * item_price
+ end
+ end
+
+ def calculate_discount(number_of_items, item_price)
+ if number_of_items > @threshold
+ (number_of_items - @threshold) * item_price * @discount_percent
+ else
+ '0'.to_d
+ end
+ end
+
+ def to_s_representation
+ ordinalized_threshold = Utils::Conversions.ordinalize(@threshold)
+ discount_percent = (@discount_percent * 100).to_i
+ " (#{discount_percent}% off of every after the #{ordinalized_threshold})"
+ end
+end
+
+#I wish I could have used the active_support's ordinalize.
+module Utils
+ class Conversions
+ def Conversions.ordinalize(number)
+ if (11..13).cover?(number % 100)
+ return "#{number}th"
+ else
+ case number % 10
+ when 1 then return "#{number}st"
+ when 2 then return "#{number}nd"
+ when 3 then return "#{number}rd"
+ else return "#{number}th"
+ end
+ end
+ end
+ end
+end
+
+class Coupon
+ def Coupon.create(name, coupon_data)
+ coupon_type, coupon_attrs = coupon_data.flatten
+ {:percent => PercentCoupon,
+ :amount => FlatAmountCoupon}[coupon_type].new(name, coupon_attrs)
+ end
+
+ def initialize(name)
+ @name = name
+ end
+end
+
+class PercentCoupon < Coupon
+ def initialize(name, discount_percent)
+ super(name)
+ @discount_percent = discount_percent / '100'.to_d
+ end
+
+ def calculate_total(total)
+ @total = total
+
+ total * (1 - @discount_percent)
+ end
+
+ def calculate_discount(total_after_discount)
+ @total * @discount_percent
+ end
+
+ def to_s_representation
+ "Coupon #{@name} - #{(@discount_percent * 100).to_i}% off"
+ end
+end
+
+class FlatAmountCoupon < Coupon
+ def initialize(name, discount_amount)
+ super(name)
+ @discount_amount = BigDecimal(discount_amount.to_s)
+ end
+
+ def calculate_total(total)
+ @total = total
+ total_after_discount = total - @discount_amount
+
+ total_after_discount < 0 ? 0 : total_after_discount
+ end
+
+ def calculate_discount(total_after_discount)
+ [@total, @discount_amount].min
+ end
+
+ def to_s_representation
+ format("Coupon %s - %.2f off", @name, @discount_amount.to_f)
+ end
+end