Осма задача

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

Краен срок:
11.01.2016 17:30
Точки:
6

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

Spreadsheets

Днес съществуват поне няколко софтуерни пакета, които позволяват обработка на таблични документи, тип "spreadsheets" – Microsoft Excel, Google Sheets, Open/LibreOffice Calc, Numbers под OS X и т.н. Принципът им на действие е сравнително прост и интуитивен, като същевременно позволяват голяма гъвкавост и имат забележително широко поле на приложение.

В тази задача ще имате за цел да имплементирате част от функционалността на едно spreadsheet приложение.

Клас Spreadsheet

Класът Spreadsheet ще е основната входна точка на програмния ни интерфейс. Той трябва да предоставя следните методи:

  • Конструктор, който приема един опционален аргумент. Ако се извика без аргументи, създава празна таблица (sheet). Ако се подаде аргумент, той трябва да е текстов низ, представляващ таблични данни в разновидност на TSV (tab-separated values). В този текстов вид, всеки ред от низа представлява ред от табличните данни. Редовете трябва да са разделени със символа "\n". В рамките на един ред, отделните клетки, формиращи колони, могат да са разделени по два начина:

    • Със символа "\t" (tab).
    • С два или повече последователни интервала.

    Празни редове в началото или в края на подадената като низ таблица трябва да бъдат премахвани. Считат се за празни и редовете, които съдържат само whitespace (интервали, табове и нови редове).

    При конструиране на таблица от низ, whitespace-ът в началото и в края на всеки ред от низа трябва да бъде премахнат. Същото се отнася и за whitespace-а около всяка клетка.

    Ето пример за конструиране на таблица с два реда и две колони:

    sheet = Spreadsheet.new("cell1\tcell2\ncell3\tcell4")
    

    Алтернативен запис на същата таблица, използваща HEREDOC низове и интервали за разделител между клетките:

    sheet = Spreadsheet.new <<-TABLE
      cell1  cell2
      cell3  cell4
    TABLE
    

    Следното също трябва да се възприема като таблица, идентична на горните две:

    sheet = Spreadsheet.new '
      cell1  cell2
      cell3  cell4
    '
    

    Възможните стойности на клетките са текст, числа и формули. За тях ще дадем информация по-долу.

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

  • Метод #empty?, връщащ истинна стойност, ако инстанцията на Spreadsheet представлява празна таблица – такива са таблици с нула клетки.

  • Метод #cell_at(cell_index), връщащ "суровата", неизчислена стойност на клетката, идентифицирана от подадения на cell_at аргумент. cell_index е низ, идентифициращ желаната клетка в координати, подобни на тези, използвани и в други spreadsheet програми. Индексът се състои от букви и цифри, като буквите задават номера на колоната, а цифрите – номера на реда. Буквите са само главни букви от английската азбука, "A" до "Z". Буква "A" отговаря на първа колона, "B" – на втора колона и т.н. до "Z", отговаряща за 26-та колона. След "Z" следва "AA", "AB" и т.н., след това "BA", "BB", ..., до "ZZ", след което следва "AAA" и така нататък до безкрай.

    Ето няколко примера за индекси на клетки:

    • "A1" – първата клетка в таблицата, на първия ред и в първата колона.
    • "B1" – втората клетка на първия ред.
    • "C1" – третата клетка на първия ред.
    • "A2" – първата клетка на втория ред.
    • "B4" – втората клетка на четвъртия ред.
    • "AB2" – 28-мата клетка на втория ред.
    • "BA42" – 53-тата клетка на 42-рия ред.

    И пример за употребата на #cell_at:

    sheet = Spreadsheet.new '
      cell1  cell2
      cell3  cell4
    '
    
    sheet.cell_at("A1") # => "cell1"
    sheet.cell_at("B2") # => "cell4"
    

    Ако на cell_at се подаде невалиден индекс, трябва да се предизвика изключение. Изключението трябва да е от тип Spreadsheet::Error и да е с текст "Invalid cell index 'индекс'". Например:

    begin
      Spreadsheet.new("foo").cell_at('FOO')
    rescue Spreadsheet::Error => e
      e.message # => "Invalid cell index 'FOO'"
    end
    

    Ако на cell_at се подаде индекс на несъществуваща клетка, изключението трябва да е с текст "Cell 'ABC123' does not exist", където ABC123 е индексът, подаден на метода.

    Всички изключения, които методите Spreadsheet ще искаме да хвърлят, трябва да са от тип Spreadsheet::Error.

    Ако не хвърли изключение, резултатът от cell_at трябва винаги да е низ.

  • Метод #[](cell_index). Този метод е много подобен на cell_at. Ако подадем невалиден индекс, или индекс на несъществуваща клетка, очакваме да получим същите грешки, както и при cell_at. Разликата между двата метода идва при клетките, които съдържат формули (изрази). В тези случаи, cell_at трябва да връща самия израз, в оригиналния си вид, докато [] трябва да връща изчислената стойност на израза.

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

    • Обикновени числа. Например, =42 трябва да се оцени на 42. Трябва да се поддържат както цели, така и числа с плаваща запетая.
    • Референции към други клетки. Например, =B2 трябва да се оцени на стойността на клетката B2. Това реално трябва да е еквивалент на sheet['B2'].
    • Формули. Формулите имат проста структура – име и нула или повече параметъра. Например, клетка със стойност =ADD(2, 2) трябва да се оцени като "4". Вижте повече за формулите в следващата секция.

    Ако не хвърли изключение, резултатът от [] трябва винаги да е низ, дори да е число, резултат от изчисление на формула.

  • Метод #to_s, който връща "изчислената" таблица като низ, в TSV формат. За разлика от конструктора, методът to_s връща винаги валиден TSV формат, със следните особености:

    • Редовете са разделени със символа "\n". Няма символ за нов ред след последния ред в таблицата.
    • Клетките в рамките на един ред са разделени с таб – "\t".

    Например:

    sheet = Spreadsheet.new <<-TABLE
      cell1  cell2
      cell3    cell4
      cell5\tcell6
    TABLE
    
    sheet.to_s # => "cell1\tcell2\ncell3\tcell4\ncell5\tcell6"
    

    Тук спазваме една важна максима при писането на софтуер, обработващ някаква форма на данни (99% от софтуера на света) – бъдете толерантни към формата на входните данни и стриктни към формата на данните, които продуцирате като резултат. Изходът на to_s винаги следва стриктно един и същ формат.

    Ако в някоя клетка има формула, то тя трябва да е "изчислена" в резултата на to_s. Съответно, to_s трябва да хвърля същите грешки, които хвърлят и методите, описани по-горе, занимаващи се с изчисление на формули.

Формули (функции)

По-долу ще използваме "формула" и "функция" като синоними.

Формулите, поддържани от нашето spreadsheet приложение, имат много проста структура – знак за равенство, последван от име на формулата и нула или повече параметъра, оградени в скоби. Всяка клетка, чието съдържание започва с = се счита за съдържаща формула.

Имената на формулите са винаги на латиница и винаги с главни букви. Формулите не могат да се влагат. Общият им вид е следният:

=FORMULA(param, param, param, param)

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

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

Някои формули имат фиксиран брой аргументи, други формули имат променлив брой аргументи.

Ако клетка съдържа формула, която е непозната, при опит за оценка на формулата, трябва да се предизвика изключение със следния текст:

begin
  Spreadsheet.new('=LARODI(1, 2)')['A1']
rescue Spreadsheet::Error => e
  e.message # => "Unknown function 'LARODI'"
end

Ако клетка съдържа грешка в "синтаксиса" на формулата, следва да се хвърли изключение със следния текст:

begin
  Spreadsheet.new('=ADD(1, 2')['A1']
rescue Spreadsheet::Error => e
  e.message # => "Invalid expression 'ADD(1, 2'"
end

Ако формулата FOO очаква точно N параметъра, но ѝ бъдат подадени M, тогава трябва да се хвърли изключение с текст:

"Wrong number of arguments for 'FOO': expected N, got M"

Ако формулата FOO очаква поне N параметъра, но ѝ бъдат подадени M, където M < N, тогава изключението трябва да е с текст:

"Wrong number of arguments for 'FOO': expected at least N, got M"

Напомняме, че всички изключения, хвърляни от методите на Spreadsheet, трябва да са от тип Spreadsheet::Error.

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

Трябва да имплементирате следните функции:

  • ADD – приема два или повече аргумента и връща сбора им. Аргументите, както е споменато и по-горе, могат да са числа, или референции към други клетки.
  • MULTIPLY – приема два или повече аргумента и връща произведението им.
  • SUBTRACT – приема точно два аргумента и връща разликата на първия минус втория.
  • DIVIDE – приема точно два аргумента, разделя първия на втория и връща резултата.
  • MOD – приема точно два аргумента и връща остатъка от делението на първия аргумент на втория (аналогично на "оператора" % в Ruby).

Работа с числа

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

Ако резултатът е точно число, без дробна част, то дробната част трябва да се пропусне, т.е. 2.0 трябва да се показва като "2".

Ако резултатът от операцията има дробна част, закръгляте до втори знак и винаги показвате с точност до два знака. Например, 4.2 трябва да се показва като "4.20". Припомняме, че крайният резултат на всички изрази, следователно и формули, е винаги низ.

Примерен тест

Примерните тестове се намират в GitHub хранилището с домашните. За информация как да ги изпълните, погледнете README-то на хранилището.

Ограничения

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

  • Най-много 80 символа на ред
  • Най-много 6 реда на метод
  • Най-много 2 нива на влагане
  • Най-много 4 аргумента на метод

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

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