Трета задача

Предадени решения

Краен срок:
07.11.2011 17:00
Точки:
6

Срокът за предаване на решения е отминал

Промоции

Едно от задължителните изисквания към всички електронни магазини е някаква форма на промоции. Обикновено това е пипкава работа. В тази задача ще имплементирате част от електронен магазин, в който има няколко вида промоции.

Инвентар

Всеки магазин има продукти, които продава на определени цени. Например, ако искате продавате няколко вида чай, може да конструирате инвентара си така:

inventory = Inventory.new
inventory.register 'Green Tea', '1.99'
inventory.register 'Red Tea',   '2.49'
inventory.register 'Earl Grey', '1.49'

Където първият аргумент е името на продукта, а вторият — цената като текстов низ. Важат следните ограничения:

  • Името на продукта трябва да е най-много 40 символа.
  • Цената трябва да е между 0.01 и 999.99.
  • Едно име на продукт може да бъде регистрирано най-много веднъж.

Ако #register се извика с аргументи, които нарушават това правило, трябва да се вдигне съобщение за грешка.

Вдигане на грешки

За вдигане на грешки и изключения ще си говорим по-подробно на някоя от следващите сбирки. Дотогава, приемете на доверие, че грешки в Ruby се вдигат с raise (документация):

raise "Invalid parameters passed."
raise RuntimeError.new("Invalid parameters passed.")

Горните два реда са еквивалентни. Спокойно може да ползвате първия вариант, за да реагирате в случаите, в които условието изисква вдигане на грешка в определена ситуация. raise в Ruby е сходен с throw в други езици.

Цени

Никога, никога, ама абсолютно никога не пазете пари като float или double. Причината е, че информацията в тези типове се прази с приблизителна точност и ще се опарите много бързо. Дори съвсем простият израз ((10.0 / 3.0) + 1.0) * 3.0 дава грешка от .000000000000002. При по-сложни изрази тази грешка може да се натрупа и да резултира в нещо много неприятно.

float е идеален за графични изчисления, където малките грешки не са проблем. Напълно неприложим е, обаче, за изчисления с пари, където дори малките грешки са недопустими. За това повечето езици имат числов тип с неограничена точност. Операциите с него са по-бавни, но надеждни.

В Ruby той се казва BigDecimal и може да го ползвате така:

require 'bigdecimal'
require 'bigdecimal/util' # добавя String#to_d

almost_pi = BigDecimal('3.1415')
answer    = '0.42'.to_d

За протокола, ако пазите пари в база от данни, обикновено има тип decimal (или сходен), който е далеч по-подходящ от float.

Пазарска количка

След като вече имате създаден инвентар, може да си вземете една пазарска количка и да си изберете какво да си купите:

cart = inventory.new_cart

cart.add 'Green Tea'
cart.add 'Red Tea', 2
cart.add 'Green Tea', 2

cart.total # връща '10.95'.to_d (според примерните цени, дадени по-горе)

#add добавя в количката продукта с даденото име. Има опционален втори параметър, който казва колко пъти да се добави продукта. Ако не е подаден, стойността му по подразбиране е 1. Обърнете внимание, че #add може да се извика два пъти за един продукт. В горния пример в количката има 3 пъти зелен чай.

#total връща цената на всички продукти като BigDecimal.

Следните неща пораждат грешка:

  • Име на продукт, което не съществува в инвентара.
  • Един продукт да присъства в количката 0 или < 0 пъти.
  • Един продукт да присъства в количката повече от 99 пъти.

Касова бележка

От всяка поръчка можете да издадете касова бележка. Текстът ѝ се достъпва с cart.invoice и връща текстов низ, съдържащ таблица с определено форматиране за прегледност.

inventory.register 'Green Tea',    '0.79'
inventory.register 'Earl Grey',    '0.99'
inventory.register 'Black Coffee', '1.99'

cart.add 'Green Tea'
cart.add 'Earl Grey', 3
cart.add 'Black Coffee', 2

puts cart.invoice

Това ще изведе следната касова бележка:

+------------------------------------------------+----------+
| Name                                       qty |    price |
+------------------------------------------------+----------+
| Green Tea                                    1 |     0.79 |
| Earl Grey                                    3 |     2.97 |
| Black Coffee                                 2 |     3.98 |
+------------------------------------------------+----------+
| TOTAL                                          |     7.74 |
+------------------------------------------------+----------+

Името на продукта е подравнено отляво, цената и броя артикули - отдясно. Всички цени са във формата XXXXX.XX. Двете цифри след плаващата запетая да задължителни. Примерни цени са 2.14, 3.20 и 15.00.

Може да съобразите детайлите как се принтира касовата бележка от примерния тест.

Промоции

Всеки продукт може да бъде пуснат на промоция. При регистрация на продукт може да определите дали е пуснат под промоция. Това става като подадете допълнителен параметър на Inventory#register.

Един продукт може да се пусне само под една промоция.

Промоциите са три вида:

Всеки n-ти е безплатен

Например, всеки четвърти чай е безплатен:

inventory.register 'Green Tea', '1.00', get_one_free: 4

cart.add 'Green Tea', 3
cart.total == '3.00'.to_d # true

cart.add 'Green Tea'
cart.total == '3.00'.to_d # true; този продукт ни е безплатен

cart.add 'Green Tea'
cart.total == '4.00'.to_d # true

cart.add 'Green Tea', 3
cart.total == '6.00'.to_d # true; имаме още един продукт на промоция

Обърнете внимание, че ако вземете три чая, не получавате четвърти. Вместо това, ако вземете четири, плащате само за три.

Ако са закупени 22 чая, 5 от тях са безплатни.

Пакети от по n броя

Получавате 20% отстъпка за всеки три чая, които купите:

inventory.register 'Red Tea', '1.00', package: {3 => 20}

cart.add 'Red Tea', 2
cart.total == '2.00'.to_d # true

cart.add 'Red Tea'
cart.total == '2.40'.to_d # true

cart.add 'Red Tea'
cart.total == '3.40'.to_d # true

:package има стойност хеш с точно една двойка. Ключът е размера на пакета, а стойноста е отстъпката за пакета (в проценти, цяло число).

Отстъпка за количество

Всеки чай след десетия е на половин цена:

inventory.register 'Earl Grey', '1.00', threshold: {10 => 50}

cart.add 'Earl Grey', 10
cart.total == '10.00'.to_d # true

cart.add 'Earl Grey', 5
cart.total == '12.50'.to_d # true

cart.add 'Earl Grey', 5
cart.total == '15.00'.to_d # true

:threshold има стойност хеш с точно една двойка. Стойността е отстъпка (проценти, цяло число). Ключът е брой закупени продукти, след който има отспъка.

Купони

Промоциите са приложими за един продукт. Купоните са приложими за цялата покупка.

Купони се декларират в инвентара:

inventory.register_coupon 'TEATIME', percent: 20
inventory.register_coupon 'FRIENDS', amount: '10.00'

Първият параметър на #register_coupon е име на купона, докато вторият описва каква отстъпка се получава с този купон.

Във всяка количка може да ползвате най-много един купон. Това става с cart.use и точното име на купона:

cart.use 'TEATIME'

Има два вида купони:

Процент от цялата цена

inventory.register 'Red Tea', '1.00'
inventory.register_coupon 'TEATIME', percent: 20

cart = inventory.new_cart
cart.add 'Red Tea', 10
cart.use 'TEATIME'

cart.total == '8.00'.to_d # true

Отстъпката от купона се прилага след отстъпката от промоции по продуктите.

Фиксирана сума

inventory.register 'Red Tea', '1.00'
inventory.register_coupon 'FRIENDS', amount: '7.00'

cart = inventory.new_cart
cart.add 'Red Tea', 10
cart.use 'FRIENDS'

cart.total == '3.00'.to_d # true

Отстъпката от купона се прилага след отстъпката от промоции по продуктите.

Ако отстъпката от купона е по-голяма от общата цена на продуктите, цената на цялата поръчка става 0.00.

Отразяване в касовата бележка

Всяка група продукти се отразява в касовата бележка точно веднъж. Цената на съответния ред в бележката е винаги <количество> * <единична цена>, независимо дали за въпросната група продукти е в сила някоя промоция или не.

В случай, че за даден продукт или група продукти е в сила някоя от промоциите описани по-горе, то това трябва да бъде отразено в касовата бележка под формата на допълнителен ред, описващ приложената промоция в колона Name, без стойност в колона qty и с отрицателна сума, съответстваща на паричното изражение на отстъпката, в колона price.

Всички суми в колона price трябва да са логични за проследяване от човек. Ако се сумират всички числа в колона price, то трябва да се получи точно резултатът, изписан в TOTAL (с точност 2 знака след десетичния разделител).

Вижте примера в следващата секция, където представянето в касовата бележка на всеки вид отстъпка е онагледено. Форматирането, white-space-ът (интревалите и новите редове) и суфиксите към числата (напр. 3rd, 2nd и т.н.) са важни.

Цялостен пример

Ето една поръчка, която ползва трите вида промоции и един от видовете купони:

inventory.register 'Green Tea',    '2.79', get_one_free: 2
inventory.register 'Black Coffee', '2.99', package: {2 => 20}
inventory.register 'Milk',         '1.79', threshold: {3 => 30}
inventory.register 'Cereal',       '2.49'
inventory.register_coupon 'BREAKFAST', percent: 10

cart = inventory.new_cart
cart.add 'Green Tea', 8
cart.add 'Black Coffee', 5
cart.add 'Milk', 5
cart.add 'Cereal', 3
cart.use 'BREAKFAST'

puts cart.invoice

Касовата бележка изглежда ето така:

+------------------------------------------------+----------+
| Name                                       qty |    price |
+------------------------------------------------+----------+
| Green Tea                                    8 |    22.32 |
|   (buy 1, get 1 free)                          |   -11.16 |
| Black Coffee                                 5 |    14.95 |
|   (get 20% off for every 2)                    |    -2.39 |
| Milk                                         5 |     8.95 |
|   (30% off of every after the 3rd)             |    -1.07 |
| Cereal                                       3 |     7.47 |
| Coupon BREAKFAST - 10% off                     |    -3.91 |
+------------------------------------------------+----------+
| TOTAL                                          |    35.16 |
+------------------------------------------------+----------+

Дизайн

Обърнете внимание, че единствените задължителни неща са:

  • Клас Inventory с методи #register, #register_coupon и #new_cart
  • Количката да има методи #add, #use, #total и #invoice с описаната по-горе семантика

Ограничения

Тази задача има следните ограничения:

  • Най-много 90 символа на ред
  • Най-много 8 реда на метод
  • Най-много 2 нива на влагане

Ако искате да проверите дали задачата ви спазва ограниченията, следвайте инструкциите в описанието на хранилището за домашните.

Няма да приемаме решения, които не спазват ограниченията. Изпълнявайте rubocop редовно, докато пишете кода. Ако смятате, че rubocop греши по някакъв начин, пишете ни на fmi@ruby.bg, заедно с прикачен код или линк към такъв като private gist. Ако пуснете кода си публично (например във форумите), ще смятаме това за преписване.