Ru/Problem K

From HaskellWiki
< Ru
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

Эта программа решает так называемую "задачу К", используемую в секретных лабораториях КГБ для отбора сверхлюдей ("людей К"), направляемых для работы за границу

Описание задачи

Маленький Эксель
----------------

Необходимо реализовать простую электронную таблицу в виде программы, выполняющейся
из командной строки. Она должна уметь обрабатывать ячейки таблицы как и более
продвинутые аналоги, только с упрощенным синтаксисом выражений. Каждая ячейка
может содержать:
 - Ничего
 - Неотрицательное целое число
 - Текстовые строки, которые начинаются с символа '
 - Строки-выражения, которые начинаются с символа '=' и могут содержать
   неотрицательные целые числа, ссылки на ячейки и простые арифметические
   выражения. Скобки запрещены, у всех операций одинаковый приоритет.
   Ссылки на ячейки состоят из одной латинской буквы и следующей за ней
   цифры.

   Эти ограничения введены для упрощения разбора выражений, поскольку разбор
   выражений не является основной частью проблемы. Вы можете спокойно
   положиться на эти ограничения. Вот грамматика содержимого ячейки:

      expression ::= '=' term {operation term}*
      term ::= cell_reference | nonnegative_number
      cell_reference ::= [A-Za-z][0-9] -- 
      operation ::= '+' | '-' | '*' | '/'
      text ::= '\'' {printable_character}

Процесс обработки:
 - Все выражения должны быть заменены на вычисленный результат.
 - Все вычисления выполняются с помощью целочисленной арифметики со знаком.
 - Ячейки с текстом должны быть вычислены как соответствующий текст без
   префикса '.
 - Операции над строками текста запрещены.
 - В случае любой ошибки вычисления формулы, вычисляемая ячейка должна содержать
   слово-сообщение об ошибке, начинающееся с символа '#'. Используйте короткие,
   ясные сообщения. Не надо предоставлять подробности об ошибках в выводе.

Программа должна использовать только стандартные библиотеки и классы и не должна
вызывать сторонние программы, библиотеки или системные компоненты.


Ввод и вывод
------------

Программа получает описание таблицы с формулами из стандартного ввода,
вычисляет ее и печатает полученный результат в стандартный вывод. Входные
данные представлены таблицей, элементы строк которой разделены табуляциями.
Первая строка содержит пару чисел, разделенных табуляцией - высоту и
ширину таблицы, соответственно. Затем идут строки с ячейками таблицы,
в грамматике, приведенной выше.


Выход должен содержать только ожидаемую информацию, включая сообщения об
ошибках, и никакой другой информации в выводе не должно быть, включая и
welcome text. Выход должен быть отформатирован в соответствии с приведенным
ниже примером.

Пример данных:
3            4
12          =C2       3       'Sample
=A1+B1*C1/5 =A2*B1    =B3-C3  'Spread
'Test       =4-3      5       'Sheet

Ожидаемый вывод:
12      -4      3       Sample
4       -16     -4      Spread
Test    1       5       Sheet


Указания по решению
-------------------
Необходимо промышленное качество кода. Более короткое и читаемое решение
предпочтительней. Решение должно содержать тестовые примеры и код, использованные
в процессе создания решения. Не забудьте откомментировать код в ключевых
местах. Код должен быть устойчив к ошибкам.

Представьте, что это требования к первой фазе проекта. Необходимо реализовать
только то, что нужно на этой фазе. Однако, известно, что планируется вторая
фаза, в которой требования будут расширены следующими:
 - Расширить формулы операциями над строками,
 - Оптимизировать производительность для громадных таблиц.

Вам необходимо будет указать, какие изменения необходимо сделать для
реализации второй фазы проекта.

Вот. С этой задачей в одной конторе не справился ни один из тех, кому ее давали.
Точнее, не справился в полном объеме. После более или менее успешного ее решения
проводилось интервью. Задача использовалась для выявления потенциальных архитекторов.

Для полноценного проектирования решения этой задачи в иерархию классов C++ необходимо
вывести кое-какие условия, о которых умолчано в задании, затем тщательно проанализировать
все условия вместе и принять пять ключевых решений по выбору того или иного пути реализации.


Решение

Наиболее интересной особенностью решения является то, что вся обработка ошибок сосредоточена в комбинаторе reliable, остальная часть программы написана в безнадёжном стиле

-- чистое время решения задачи - 8 часов
import Prelude hiding (catch)
import Control.Exception
import Data.Char
import Data.Graph
import Data.List
import Data.Function (on)
import Data.Ord (comparing)

-----------------------------------------------------------------------------------------------
-- Мини-библиотека ----------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------

mapSnds         = map.mapSnd
mapSnd  f (a,b) = (a,f b)
groupOn f       = groupBy ((==) `on` f)
sortOn  f       = sortBy  (comparing f)
a.$b = b a

-- Вычислить (f x), возвращая вместо ошибок при вычислении errValue
reliable :: (x->y) -> y -> x -> IO y
reliable f errValue x  =  (evaluate$ f x) `catch` (\_ -> return errValue)



-----------------------------------------------------------------------------------------------
-- Типы данных --------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------

-- Таблица, содержимое ячеек которой представлено типом a
type Sheet a = [NamedCell a]
-- Отображение содержимого ячеек
mapSheet  f = mapSnds f
mapSheetM f = mapM (\(a,b) -> do fb <- f b; return (a,fb))

-- Поименованная ячейка
type NamedCell a = (CellName,a)

-- Имя ячейки (такое, как "a1")
type CellName = String

-- Содержимое ячейки: пусто/строка/выражение/ошибка
--   a = Expr Int   до вычислений по формулам
--   a = Int        после вычислений
data Cell a  =  EmptyCell | StringCell String | ExprCell a | ErrorCell String

-- AST выражения, имеющего тип a
data Expr a  =  BinOp (BinOp a) (Expr a) (Expr a)
             |  RefCell CellName
             |  Const a

-- Бинарная операция
type BinOp a  =  a->a->a
-- Превращение строковой записи в бинарную операцию
cvt "+" = (+)
cvt "-" = (-)
cvt "*" = (*)
cvt "/" = div

-- Список ячеек, на которые ссылается Expr
refs (BinOp   _ x y)  =  refs x++refs y
refs (RefCell name)   =  [name]
refs (Const   _)      =  []


-----------------------------------------------------------------------------------------------
-- Программа ----------------------------------------------------------------------------------
-----------------------------------------------------------------------------------------------
--main = interact$ fromSheet.mapSheet showCell.calcSheet.removeCycles.mapSheet parseCell.toSheet


main = do
    input <- getContents
    -- Конвертирует входной формат данных в таблицу строк
    let inputSheet      = toSheet input                   :: Sheet String
    -- Проводит парсинг содержимого каждой ячейки
    parsedSheet <- mapSheetM (reliable parseCell (ErrorCell "#PARSING")) inputSheet
                                                          :: IO (Sheet (Cell (Expr Int)))
    -- Заменяет формулы в циклически зависящих ячейках на сообщения об ошибке
    let acyclicSheet    = removeCycles parsedSheet        :: Sheet (Cell (Expr Int))
    -- Вычисляет формулы в ячейках
    let calculatedSheet = calcSheet acyclicSheet          :: Sheet (Cell Int)
    -- Заменяет результат вычисления в каждой ячейке его текстовым представлением
    outputSheet <- mapSheetM (reliable showCell "#EVAL") calculatedSheet  :: IO (Sheet String)
    -- Конвертирует таблицу строк в выходной формат данных
    putStrLn (fromSheet outputSheet)



-- Превращает входные данные в список пар (имя ячейки, её содержимое):
--   [ ("a1", "2"), ("a2", "=a1+1"),  ("b1", "=a2+1"), ("b2", "=b1") ]
toSheet :: String -> Sheet String
toSheet input  =  enumSheet xs  where
   -- xs - тексты ячеек, разбитые построчно
   xs :: [[String]]
   x:xs = map words$ lines input
   -- число строк/столбцов в таблице, записанное в первой строке ввода
   -- [rows,cols] = map read x
   -- Перенумеровать все ячейки таблицы
   enumSheet :: [[a]] -> Sheet a
   enumSheet = concat . zipWith enumRow [1..]
   -- Перенумеровать ячейки строки num
   enumRow :: Int -> [a] -> [NamedCell a]
   enumRow num = zipWith (enumCell num) ['a'..]
   -- Занумеровать ячейку столбца letter строки num
   enumCell :: Int -> Char -> a -> NamedCell a
   enumCell num letter cell = (letter:show num, cell)



-- Парсит содержимое ячейки, превращая входную строку во внутреннее представление
parseCell :: String -> Cell (Expr Int)
parseCell "\"\""                      =  EmptyCell
parseCell ('\'':xs)                   =  StringCell xs
parseCell ('=':xs)                    =  ExprCell$ parseExpr xs
parseCell xs        | all isDigit xs  =  ExprCell$ Const (read xs)

-- Парсит выражение (начинающееся с '=')
parseExpr :: (Integral a, Read a) =>  String -> Expr a
parseExpr = build.reverse.split where
  -- разбивает строку на список строк [терм,операция,терм,операция...терм]
  split xs | null rest  =  [xs]
           | otherwise  =  x:op:split rest1
    where (x,rest)   = span isAlphaNum xs
          (op,rest1) = break isAlphaNum rest
  -- строит Expr из списка термов/операций (заменить на вызов fold*!)
  build (x:op:xs)  =  BinOp (cvt op) (build xs) (term x)
  build [x]        =  term x
  -- парсит терм = число | имя ячейки
  term xs | all isDigit xs  =  Const (read xs)
          | otherwise       =  RefCell (map toLower xs)



-- Заменяет циклически зависимые ячейки на сообщения об ошибке
removeCycles :: Sheet (Cell (Expr Int)) -> Sheet (Cell (Expr Int))
removeCycles  =  concatMap replaceCycles . topSort  where
  -- Выделить группы ячеек с циклическими зависимостями
  topSort :: Sheet (Cell (Expr Int)) -> [SCC (CellName, Cell (Expr Int))]
  topSort = stronglyConnComp . map f
                where -- f вычисляет, на какие ячейки ссылается эта
                      f x = (x, fst x, refs1 (snd x))
                      refs1 (ExprCell e) = refs e
                      refs1 _            = []
  -- Заменить содержимое циклически зависимых ячеек на сообщения об ошибке
  replaceCycles :: SCC (CellName, Cell (Expr Int)) -> [(CellName, Cell (Expr Int))]
  replaceCycles (AcyclicSCC cell)   =  [cell]
  replaceCycles (CyclicSCC  cells)  =  mapSnds (const$ ErrorCell "#CYCLE") cells



-- Заменяет формулы в ячейках на результаты их вычисления
calcSheet :: Sheet (Cell (Expr Int)) -> Sheet (Cell Int)
calcSheet sheet = result  where
  -- Таблица, в которой формулы заменены результатами их вычислений
  result :: Sheet (Cell Int)
  result = mapSnds calcCell sheet
  -- Замена формулы в ячейке на результат её вычисления
  calcCell :: Cell (Expr Int) -> Cell Int
  calcCell EmptyCell       =  EmptyCell
  calcCell (StringCell s)  =  StringCell s
  calcCell (ExprCell   e)  =  ExprCell   (calc e)
  calcCell (ErrorCell  s)  =  ErrorCell  s
  -- Вычисление значения выражения
  calc :: Expr Int -> Int
  calc (Const x)       =  x
  calc (BinOp op x y)  =  op (calc x) (calc y)
  calc (RefCell x)     =  answer
                            where Just (ExprCell answer) = lookup x result



-- Выводит текстовое представление вычисленной ячейки
showCell :: Cell Int -> String
showCell EmptyCell       =  "\"\""
showCell (StringCell s)  =  s
showCell (ExprCell x)    =  show x
showCell (ErrorCell s)   =  s


-- Конвертирует таблицу строк в выходной формат данных
fromSheet :: Sheet String -> String
fromSheet xs = xs.$ sortOn  (tail.fst)          -- Разбить ячейки на rows
                 .$ groupOn (tail.fst)          -- ..
                 .$ map (unwords.map snd.sort)  -- Текстовое представление каждого row
                 .$ unlines                     -- Слить текстовые представления rows

Тесты

Я расширил тестовый пример несколькими некорректными выражениями:

7 4
12 =C2 3 'Sample
=A1+B1*C1/5 =A2*B1 =B3-C3 'Spread
'Test =4-3 5 'Sheet
"" =A9 =1/0 =A5
=B5 =1+C6+1 =5A =A1++A1
=1+ x =A5 =A6+B6
=a1 =a3 =a4 =a5 

Вывод программы должен быть:

12 -4 3 Sample
4 -16 -4 Spread
Test 1 5 Sheet
"" #EVAL #EVAL #EVAL
#CYCLE #CYCLE #EVAL #EVAL
#EVAL #PARSING #CYCLE #EVAL
12 #EVAL #EVAL #EVAL

Ссылки