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

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

Към профила на Здравко Стойчев

Резултати

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

Код

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

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

...................

Finished in 0.58081 seconds
19 examples, 0 failures

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

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