Шеста задача

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

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

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

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

BigDecimal

Както знаем, аритметиката с числа с плаваща запетая (floats) е приблизителна. Ако искаме да работим с пари, или ако по друга причина ни трябва точна аритметика, то тогава типът "float" е забранена дума. Това е валидно във всички езици за програмиране, имащи тип "float", отговарящ на C-типа float.

За точна аритметика с десетични числа, в Ruby се ползва класът от стандартната библиотека BigDecimal. Може да го включите във вашето приложение с require 'bigdecimal'. С BigDecimal работите като с всеки друг Numeric. Можете да създавате обекти от този клас от низове, съдържащи числото, например така:

BigDecimal.new '12.45'
BigDecimal.new('0.0').zero? # => true

Допълнително, ако включите и require 'bigdecimal/util', ще може да ползвате метод #to_d върху низове и други обекти, по следния начин:

'12.45'.to_d
1.to_d
3.14.to_d
BigDecimal('0').to_d

Вижте документацията му за повече детайли.

Валути и обменни курсове

За да моделираме цени и пари нашия електронен магазин, ще въведем концепцията за валута и обменен курс. Валута ще представвяме като символ от три букви, според съответния ISO стандарт, например :BGN, :USD', :EUR и прочее.

Ще считаме, че навсякъде, където се очкава да бъде подадена валута, ще се подава валидна такава и няма нужда да ги проверявате за валидност.

Целта е да съставим клас ExchangeRate, който моделира нашата идея за обмен между суми в различни валути. Ще приемем, че ако имаме курс за обмяна между валутите А и Б, то ще можем да обменяме и между Б и А, по съответния реципрочен курс.

Този клас трябва да има следните инстанционни методи и характеристики:

  • set(from_currency, to_currency, rate), който задава курс на обмен между двете подадени валути. Валутите се подават като символи, а обменният курс — като BigDecimal. Извикване на този метод задава колко от to_currency ще получим за една единица от from_currency, както и обратния реципрочен курс за обмен в обратна посока между двете валути. След извикване на метода, можем да обръщаме между двете валути в произволен ред, по съответния курс. Методът може да бъде извикван многократно.
  • get(from_currency, to_currency) трябва да върне обменния курс между двете подадени валути. Ако няма дефиниран курс между двете валути, трябва да връща nil. Ако има такъв курс, то той трябва да работи и в двете посоки, тоест:

      rate = ExchangeRate.new
      rate.set :USD, :BGN, '1.6'.to_d
      rate.get(:USD, :EUR) # => nil
      rate.get(:USD, :BGN) # => BigDecimal.new('1.6')
      rate.get(:BGN, :USD) # => BigDecimal.new('.625')
    
  • convert(from_currency, to_currency, amount) се ползва да обърне amount, подаден като BigDecimal, от валутата from_currency в to_currency. Резултатът от този метод трябва да е също BigDecimal. Ако обменният курс между двете подадени валути е неизвестен, трябва да бъде хвърлена грешката ExchangeRate::Unknown, която трябва да наследява от RuntimeError.

  • Обменният курс между две еднакви валути е винаги единица (очевидно) и това не трявба да може да се променя.

Пари

Когато говорим за валути и обменни курсове, не може да става дума за пари, без редом със сумата да върви и валута.

Ще моделираме идеята за "пари", създавайки клас Money. Конструираме дадена сума пари по следния начин:

Money.new '49.98'.to_d, :USD

Така създаденият обект трябва да може да връща сумата през инстанционен метод amount, а валутата — през currency. Допълнително, всеки Money обект трябва да отговаря на следните инстанционни методи:

  • to_s дава текстово представяне на обекта пари от следния вид, като винаги показва точно два знака след десетичната запетая:

      bucks = Money.new 100.to_d, :USD
      bucks.to_s # => '100.00 USD'
    
      cents = Money.new '0.443'.to_d, :USD
      cents.to_s # => '0.44 USD'
    
  • in(currency, exchange_rate) ще върне нов обект от тип Money, който е с подадената валута и със сума, конвертирана в подадената валута според подадените обменни курсове в подадения обект от тип ExchangeRate. Ако обменът не може да се осъществи, трябва да бъде хвърлено изключение ExchangeRate::Unknown. Тук се ползва същата семантика, както при обменните курсове от по-горе.

  • Аритметични операции *, / могат да се ползват с Numeric обекти, докато + и - трябва да работят с други Money обекти. Когато операндите са два обекта от тип Money, то валутата им трябва да е еднаква, иначе трябва да се предизвика Money::IncompatibleCurrencies. Опит за изпълнение на въпросните операции с обекти от друг тип трябва да предизвиква ArgumentError.
  • Следните операции за сравнение трябва да работят спрямо други обекти от тип Money, когато валутата им е еднаква: <=>, ==, <, <=, >, >=. При сравнение на различни валути трябва да се предизвика Money::IncompatibleCurrencies, а при сравнение на Money с друг тип обект — ArgumentError.

Ограничения

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

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

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

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