Решение на Трета задача от Генади Самоковаров

Обратно към всички решения

Към профила на Генади Самоковаров

Резултати

  • 6 точки от тестове
  • 0 бонус точки
  • 6 точки общо
  • 18 успешни тест(а)
  • 1 неуспешни тест(а)

Код

%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

Лог от изпълнението

.................F.

Failures:

  1) Inventory with an 'amount off' coupon shows the discount in the invoice
     Failure/Error: cart.invoice.should eq <<INVOICE
       
       expected: "+------------------------------------------------+----------+\n| Name                                       qty |    price |\n+------------------------------------------------+----------+\n| Green Tea                                    5 |     5.00 |\n| Coupon TEA-TIME - 10.00 off                    |    -5.00 |\n+------------------------------------------------+----------+\n| TOTAL                                          |     0.00 |\n+------------------------------------------------+----------+\n"
            got: "+------------------------------------------------+----------+\n| Name                                       qty |    price |\n+------------------------------------------------+----------+\n| Green Tea                                    5 |     5.00 |\n| Coupon TEA-TIME - 0.1E2 off                    |    -5.00 |\n+------------------------------------------------+----------+\n| TOTAL                                          |     0.00 |\n+------------------------------------------------+----------+\n"
       
       (compared using ==)
       
       Diff:
       @@ -2,7 +2,7 @@
        | Name                                       qty |    price |
        +------------------------------------------------+----------+
        | Green Tea                                    5 |     5.00 |
       -| Coupon TEA-TIME - 10.00 off                    |    -5.00 |
       +| Coupon TEA-TIME - 0.1E2 off                    |    -5.00 |
        +------------------------------------------------+----------+
        | TOTAL                                          |     0.00 |
        +------------------------------------------------+----------+
     # /tmp/d20111115-5847-16jim3z/spec.rb:281:in `block (3 levels) in <top (required)>'
     # ./lib/homework/run_with_timeout.rb:5:in `block (3 levels) in <top (required)>'
     # ./lib/homework/run_with_timeout.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.5718 seconds
19 examples, 1 failure

Failed examples:

rspec /tmp/d20111115-5847-16jim3z/spec.rb:274 # Inventory with an 'amount off' coupon shows the discount in the invoice

История (1 версия и 2 коментара)

Генади обнови решението на 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.

Също, добре е да имаш по един клас за всяка промоция и по един клас за всеки купоп. Пробвай го и виж какво ще стане.