Генади обнови решението на 03.11.2011 01:28 (преди около 13 години)
+%w{ bigdecimal bigdecimal/util }.each { |p| require p }
+
+class Inventory
+ MAX_PRODUCT_NAME_LENGTH = 40
+ HIGHEST_PRODUCT_PRICE = '999.99'.to_d
+ LOWEST_PRODUCT_PRICE = '0.01'.to_d
+
+ def initialize
+ @coupons = {}
+ @products = {}
+ @promotions = {}
+ end
+
+ attr_reader :products, :coupons, :promotions
+
+ def register(product, price, promo = {})
+ raise "Product name too long" if product.length > MAX_PRODUCT_NAME_LENGTH
+ raise "Already registered product" if @products.include?(product)
+
+ price = price.to_d
+
+ raise "Price too low" if price < LOWEST_PRODUCT_PRICE
+ raise "Price too high" if price > HIGHEST_PRODUCT_PRICE
+
+ @promotions[product] = Promotion.delegate_new(promo) unless promo.empty?
+ @products[product] = price
+ end
+
+ def register_coupon(name, definition)
+ raise "Already registered coupon" if @coupons.include?(name)
+
+ coupon = Coupon.delegate_new(name, definition)
+
+ @coupons[name] = coupon
+ end
+
+ def new_cart
+ Cart.new(self)
+ end
+end
+
+class Cart
+ MIN_PRODUCT_QUANTITY = 1
+ MAX_PRODUCT_QUANTITY = 99
+
+ def initialize(inventory)
+ @inventory = inventory
+ @items = Hash.new(0)
+ @coupon = nil
+ end
+
+ def add(product, quantity = 1)
+ raise "Invalid product" unless @inventory.products.include?(product)
+
+ raise "Invalid quantity" if quantity < MIN_PRODUCT_QUANTITY
+ raise "Too much" if @items[product] + quantity > MAX_PRODUCT_QUANTITY
+
+ @items[product] += quantity
+ end
+
+ def use(coupon)
+ raise "Already using a coupon" unless @coupon.nil?
+ raise "Coupon not found" unless @inventory.coupons.include?(coupon)
+
+ @coupon = @inventory.coupons[coupon]
+ end
+
+ def total
+ return total_before_coupon if @coupon.nil?
+ total_before_coupon - @coupon.discount_for(total_before_coupon)
+ end
+
+ def invoice
+ format_invoice
+ end
+
+ private
+
+ def price_of(product, quantity = 1)
+ @inventory.products[product] * quantity
+ end
+
+ def discount_for(product, quantity = 1)
+ promotion = @inventory.promotions[product]
+ return '0'.to_d if promotion.nil?
+ promotion.apply_for(price_of(product), quantity)
+ end
+
+ def promotion_for(product)
+ @inventory.promotions[product]
+ end
+
+ def total_before_coupon
+ sum = BigDecimal.new('0')
+ @items.each do |product, quantity|
+ sum += price_of(product, quantity) - discount_for(product, quantity)
+ end
+ sum
+ end
+end
+
+module Coupon
+ def self.delegate_new(name, definition)
+ type, value = definition.flatten
+
+ case type
+ when :percent
+ PercentCoupon.new(name, value)
+ when :amount
+ AmountCoupon.new(name, value)
+ else
+ raise "Invalid coupon type"
+ end
+ end
+
+ class Abstract
+ def initialize(name, value)
+ @name = name
+ @value = value
+ end
+
+ def discount_for(sum)
+ raise NotImplementedError, "Subclasses should implement #discount_for"
+ end
+ end
+
+ class PercentCoupon < Abstract
+ def discount_for(sum)
+ (@value / '100.0'.to_d) * sum
+ end
+
+ def to_s
+ "Coupon #@name - #@value% off"
+ end
+ end
+
+ class AmountCoupon < Abstract
+ def discount_for(sum)
+ @value.to_d.tap do |value|
+ return sum if sum < value
+ end
+ end
+
+ def to_s
+ "Coupon #@name - #@value off"
+ end
+ end
+end
+
+module Promotion
+ def self.delegate_new(definition)
+ type, value = definition.flatten
+
+ case type
+ when :get_one_free
+ GetOneFreePromotion.new(value)
+ when :package
+ PackagePromotion.new(value)
+ when :threshold
+ ThresholdPromotion.new(value)
+ else
+ raise "Invalid coupon type"
+ end
+ end
+
+ class Abstract
+ def initialize(value)
+ @value = value
+ end
+
+ def apply_for(product_price, quantity = 1)
+ raise NotImplementedError, "Subclasses should implement #apply_for"
+ end
+ end
+
+ class GetOneFreePromotion < Abstract
+ def apply_for(product_price, quantity = 1)
+ return '0'.to_d if (quantity < @value) or @value.zero?
+ (quantity / @value) * product_price
+ end
+
+ def to_s
+ "(buy #{@value.pred}, get 1 free)"
+ end
+ end
+
+ class PackagePromotion < Abstract
+ def apply_for(product_price, quantity = 1)
+ n, percents = @value.flatten
+ return '0'.to_d if n.zero?
+ (percents / '100.0'.to_d) * product_price * (quantity / n) * n
+ end
+
+ def to_s
+ n, percents = @value.flatten
+ "(get #{percents}% off for every #{n})"
+ end
+ end
+
+ class ThresholdPromotion < Abstract
+ def apply_for(product_price, quantity = 1)
+ nth, percent = @value.flatten
+ (percent / '100.0'.to_d) * [quantity - nth, 0].max * product_price
+ end
+
+ def to_s
+ nth, percents = @value.flatten
+ "(#{percents}% off of every after the #{ordinalize(nth)})"
+ end
+
+ private
+
+ def ordinalize(number)
+ return "#{number}th" if number.remainder(100).abs.between?(11, 13)
+
+ case number.remainder(10).abs
+ when 1 then"#{number}st"
+ when 2 then "#{number}nd"
+ when 3 then "#{number}rd"
+ else "#{number}th"
+ end
+ end
+ end
+end
+
+module InvoiceFormatter
+ BORDER = "+" + ("-" * 48) + "+" + ("-" * 10) + "+"
+ TEXT_FORMAT = "| %- 40s %5s | %8s |"
+ PRODUCT_FORMAT = "| %- 40s %5s | %8.2f |"
+ PROMOTION_FORMAT = "| %- 42s | %8.2f |"
+
+ def format_invoice
+ format = format_header
+ format << format_products_with_promotions
+ format << format_coupon unless @coupon.nil?
+ format << format_footer
+ format.join("\n") + "\n"
+ end
+
+ private
+
+ def format_header
+ [BORDER, TEXT_FORMAT % ["Name", "qty", "price"], BORDER]
+ end
+
+ def format_products_with_promotions
+ @items.map do |item, quantity|
+ format = [PRODUCT_FORMAT % [item, quantity, price_of(item, quantity)]]
+ unless promotion_for(item).nil?
+ format << PROMOTION_FORMAT % [promotion_for(item), -discount_for(item, quantity)]
+ end
+ format
+ end
+ end
+
+ def format_coupon
+ [PRODUCT_FORMAT % [@coupon, nil, -@coupon.discount_for(total_before_coupon)]]
+ end
+
+ def format_footer
+ [BORDER, PRODUCT_FORMAT % ["TOTAL", nil, total], BORDER]
+ end
+end
+
+class Cart
+ include InvoiceFormatter
+end
- Без
active_support
. Няма да го има на машината, на която тестваме. Няма го и в стандартното Ruby. - Monkey-patch-а на
String
ще ти струва поне две точки. -
require 'bigdecimal/util'
, ако искашString#to_d
.
Също, добре е да имаш по един клас за всяка промоция и по един клас за всеки купоп. Пробвай го и виж какво ще стане.
Надявах се active_support
да беше минал :) Ще оправям идните дни.