Здравко обнови решението на 07.11.2011 14:02 (преди около 13 години)
+require 'bigdecimal'
+require 'bigdecimal/util'
+
+class NonePromotion
+ def discount_for(price, count)
+ 0
+ end
+end
+
+class GetOneFreePromotion
+ attr_reader :promotion_count
+
+ def initialize(promotion_count)
+ @promotion_count = promotion_count
+ end
+
+ def discount_for(price, count)
+ price * (count / promotion_count)
+ end
+end
+
+class PackagePromotion
+ attr_reader :promotion_count, :percent
+
+ def initialize(promotion_count_to_percent)
+ @promotion_count, @percent = promotion_count_to_percent.to_a.first
+ end
+
+ def discount_for(price, count)
+ price * promotion_count * (count / promotion_count) * percent / '100'.to_d
+ end
+end
+
+class ThresholdPromotion
+ attr_reader :threshold, :percent
+
+ def initialize(threshold_to_percent)
+ @threshold, @percent = threshold_to_percent.to_a.first
+ end
+
+ def discount_for(price, count)
+ price * [(count - threshold), 0].max * percent / '100'.to_d
+ end
+end
+
+class ProductInfo
+ PROMOTION_TYPE_TO_CLASS = {
+ none: NonePromotion,
+ get_one_free: GetOneFreePromotion,
+ package: PackagePromotion,
+ threshold: ThresholdPromotion,
+ }
+
+ attr_reader :price, :promotion
+
+ def initialize(price, promotion)
+ if price.to_d < "0.01".to_d or price.to_d > "999.99".to_d
+ raise "Price must be between 0.01 and 999.99."
+ end
+
+ @price = price.to_d
+ promotion_type, promotion_value = promotion.to_a.first
+ @promotion = PROMOTION_TYPE_TO_CLASS[promotion_type].new(promotion_value)
+ end
+
+ def price_for(count)
+ price * count
+ end
+
+ def price_with_discount_for(count)
+ price_for(count) - discount_for(count)
+ end
+
+ def discount_for(count)
+ promotion.discount_for(price, count)
+ end
+end
+
+class NoneCoupon
+ def apply(price)
+ price
+ end
+end
+
+class PercentCoupon
+ attr_reader :percent
+
+ def initialize(percent)
+ @percent = percent
+ end
+
+ def apply(price)
+ price * (100 - percent) / '100'.to_d
+ end
+end
+
+class AmountCoupon
+ attr_reader :amount
+
+ def initialize(amount)
+ @amount = amount.to_d
+ end
+
+ def apply(price)
+ [price - amount, 0].max
+ end
+end
+
+class Inventory
+ COUPON_TYPE_TO_CLASS = {
+ percent: PercentCoupon,
+ amount: AmountCoupon,
+ }
+
+ def initialize
+ @product_to_product_info = {}
+ @coupon_name_to_coupon = {"NONE" => NoneCoupon.new}
+ end
+
+ def register(product, price, promotion = {none: nil})
+ raise "Product already present." if has_product?(product)
+ raise "Product's name must be at most 40 symbols." if product.size > 40
+
+ @product_to_product_info[product] = ProductInfo.new(price, promotion)
+ end
+
+ def register_coupon(coupon_name, coupon)
+ raise "Coupon already present." if has_coupon?(coupon_name)
+
+ type, value = coupon.to_a.first
+ @coupon_name_to_coupon[coupon_name] = COUPON_TYPE_TO_CLASS[type].new(value)
+ end
+
+ def new_cart
+ Cart.new(self)
+ end
+
+ def has_product?(product)
+ @product_to_product_info.has_key?(product)
+ end
+
+ def product_info_of(product)
+ @product_to_product_info[product]
+ end
+
+ def has_coupon?(coupon_name)
+ @coupon_name_to_coupon.has_key?(coupon_name)
+ end
+
+ def get_coupon(coupon_name)
+ @coupon_name_to_coupon[coupon_name]
+ end
+end
+
+class Cart
+ attr_reader :inventory, :used_coupon_name
+
+ def initialize(inventory)
+ @inventory = inventory
+ @product_to_count = Hash.new(0)
+ @used_coupon_name = "NONE"
+ end
+
+ def products
+ @product_to_count.keys
+ end
+
+ def count_of(product)
+ @product_to_count[product]
+ end
+
+ def used_coupon
+ inventory.get_coupon used_coupon_name
+ end
+
+ def add(product, count = 1)
+ raise "Product not in inventory." unless inventory.has_product?(product)
+ if count <= 0 or @product_to_count[product] + count > 99
+ raise "A product's count must be between 0 and 99."
+ end
+
+ @product_to_count[product] += count
+ end
+
+ def use(coupon_name)
+ raise "Unknown coupon." unless inventory.has_coupon?(coupon_name)
+
+ @used_coupon_name = coupon_name
+ end
+
+ def total_of_products
+ products.map { |product| price_of product }.inject(&:+)
+ end
+
+ def total
+ used_coupon.apply(total_of_products)
+ end
+
+ def invoice(invoice_creator = DefaultInvoice.new)
+ invoice_creator.make_invoice_of(self)
+ end
+
+ private
+
+ def price_of(product)
+ inventory.product_info_of(product).price_with_discount_for count_of(product)
+ end
+end
+
+class ElementColumn
+ attr_reader :width, :align
+
+ def initialize(width, align)
+ @width, @align = width, align
+ end
+
+ def to_sprintf_sign
+ align == :left ? "-" : ""
+ end
+end
+
+class SimpleTextTable
+ def initialize(columns)
+ @columns = add_border(columns)
+ @rows = []
+ end
+
+ def add_row(elements)
+ @rows << elements
+ end
+
+ def add_horizontal_separator
+ @rows << :separator
+ end
+
+ def to_text
+ rows_as_text = add_border(@rows).map do |row|
+ case row
+ when :separator then make_separator_row
+ else make_elements_row row
+ end
+ end
+
+ rows_as_text.join("\n")
+ end
+
+ private
+
+ def add_border(table_part)
+ [:separator] + table_part + [:separator]
+ end
+
+ def make_separator_row
+ row_chunks = @columns.map do |column|
+ case column
+ when :separator then "+"
+ else "-" * (column.width + 2)
+ end
+ end
+
+ row_chunks.join ""
+ end
+
+ def make_elements_row(elements)
+ row_chunks = @columns.map do |column|
+ case column
+ when :separator then "|"
+ else
+ next_element, *elements = elements
+ " %#{column.to_sprintf_sign}#{column.width}s " % next_element
+ end
+ end
+
+ row_chunks.join ""
+ end
+end
+
+class DefaultInvoice
+ def make_invoice_of(cart)
+ table = create_table
+ table.add_row ["Name", "qty", "price"]
+ table.add_horizontal_separator
+
+ add_products table, cart
+ add_coupon_info table, cart
+
+ table.add_horizontal_separator
+ table.add_row ["TOTAL", "", format_decimal(cart.total)]
+
+ table.to_text + "\n"
+ end
+
+ private
+
+ def create_table
+ columns = [
+ ElementColumn.new(40, :left),
+ ElementColumn.new(4, :right),
+ :separator,
+ ElementColumn.new(8, :right),
+ ]
+
+ SimpleTextTable.new(columns)
+ end
+
+ def ordinal_suffix_of(number)
+ return "th" if (11..19).include? number
+
+ case number % 10
+ when 1 then "st"
+ when 2 then "nd"
+ when 3 then "rd"
+ else "th"
+ end
+ end
+
+ def as_ordinal(number)
+ "#{number}#{ordinal_suffix_of number}"
+ end
+
+ def get_promotion_description(promotion)
+ case promotion
+ when GetOneFreePromotion
+ "buy #{promotion.promotion_count - 1}, get 1 free"
+ when PackagePromotion
+ "get #{promotion.percent}% off for every #{promotion.promotion_count}"
+ when ThresholdPromotion
+ "#{promotion.percent}% off of every after the #{as_ordinal promotion.threshold}"
+ else
+ "unknown promotion"
+ end
+ end
+
+ def add_promotion_row(table, product_info, count)
+ promotion_description = get_promotion_description product_info.promotion
+ discount = format_decimal -product_info.discount_for(count)
+ table.add_row [" (#{promotion_description})", "", discount]
+ end
+
+ def add_products(table, cart)
+ cart.products.each do |product|
+ product_info = cart.inventory.product_info_of product
+
+ count = cart.count_of product
+ price = format_decimal product_info.price_for(count)
+ table.add_row [product, count, price]
+
+ if (product_info.discount_for(count) > 0)
+ add_promotion_row(table, product_info, count)
+ end
+ end
+ end
+
+ def get_coupon_description(coupon)
+ case coupon
+ when PercentCoupon
+ "#{coupon.percent}% off"
+ when AmountCoupon
+ "#{format_decimal coupon.amount} off"
+ else
+ "unknown coupon"
+ end
+ end
+
+ def add_coupon_info(table, cart)
+ difference = cart.total_of_products - cart.total
+
+ if difference > 0
+ coupon_description = get_coupon_description cart.used_coupon
+ table.add_row [
+ "Coupon #{cart.used_coupon_name} - #{coupon_description}",
+ "",
+ format_decimal(-difference)
+ ]
+ end
+ end
+
+ def format_decimal(number)
+ "%.2f" % number
+ end
+end