packages feed

classify-frog-0.2.3: src/Spreadsheet/Formula.hs

module Spreadsheet.Formula (
   Formula,
   CellId(CellId),
   Agg(..),
   constant,
   pi,
   fromInt,
   formatCSV,
   formatXML2003,
   aggregate,
   sum,

   Tracked(..),
   untracked,
   CellTracked,
   FormulaTracked,
   trackFraction,
   trackedVar,
   ) where

import qualified Data.Bifunctor as Bifunc
import Control.Applicative (liftA2)
import Data.Bitraversable (Bitraversable, bitraverse, bifoldMapDefault, )
import Data.Bifoldable (Bifoldable, bifoldMap, )
import Data.Biapplicative (Biapplicative, bipure, biliftA2, (<<*>>), )
import Data.Bifunctor (Bifunctor, bimap, )

import qualified Data.NonEmpty as NonEmpty
import qualified Data.List as List
import Data.Tuple.HT (swap)
import Data.Maybe.HT (toMaybe)
import Text.Printf (printf)
import Data.Char (ord, chr)
import Data.Ord.HT (comparing)
import Data.Eq.HT (equating)

import qualified Algebra.ToRational as ToRational
import qualified Algebra.RealRing as Real
import qualified Algebra.Transcendental as Trans
import qualified Algebra.Field as Field
import qualified Algebra.Ring as Ring
import qualified Algebra.Additive as Additive
import Algebra.ToRational (realToField)
import NumericPrelude.Numeric hiding (sum, pi)
import NumericPrelude.Base
import Prelude ()


data Formula =
     Const Integer
   | Any String
   | Var CellId
   | Infix Infix Formula Formula
   | Agg Agg [Either Formula (CellId, CellId)]


data CellId = CellId Int Int
   deriving (Eq, Ord, Show)

data Infix = Times | Divide | Plus | Minus
   deriving (Show)

data Precedence = PrecSum | PrecProduct | PrecLiteral
   deriving (Eq, Ord, Enum)

data Agg = Sum | Minimum | Maximum | Median | Quartile Int
   deriving (Show)

format :: (CellId -> String) -> Formula -> String
format fmtCellId =
   let go paren form =
         case form of
            Const a -> show a
            Any str -> str
            Var cell -> fmtCellId cell
            Agg agg cells ->
               printf (formatAgg agg) $ List.intercalate ";" $
               map (either (go (PrecSum>)) (cellRange fmtCellId)) cells
            Infix Times x y ->
               parentheses (paren PrecProduct) $
               go (PrecProduct>) x ++ "*" ++ go (PrecProduct>) y
            Infix Divide x y ->
               parentheses (paren PrecProduct) $
               go (PrecProduct>) x ++ "/" ++ go (PrecProduct>=) y
            Infix Plus x y ->
               parentheses (paren PrecSum) $
               go (PrecSum>) x ++ "+" ++ go (PrecSum>) y
            Infix Minus x y ->
               parentheses (paren PrecSum) $
               go (PrecSum>) x ++ "-" ++ go (PrecSum>=) y
   in  go (const False)

formatCSV :: Formula -> String
formatCSV = format formatCellId

formatCellId :: CellId -> String
formatCellId (CellId row column) = formatColumnId column ++ show (row+1)

formatColumnId :: Int -> String
formatColumnId =
   let range = ord 'Z' - ord 'A' + 1
   in  map chr . map (ord 'A' +) . reverse .
       List.unfoldr (\k -> toMaybe (k>0) $ swap $ divMod (k-1) range) . (1+)

formatXML2003 :: Formula -> String
formatXML2003 = format formatCellIdXML2003

formatCellIdXML2003 :: CellId -> String
formatCellIdXML2003 (CellId row column) =
   printf "R%dC%d" (row+1) (column+1)

formatAgg :: Agg -> String
formatAgg agg =
   case agg of
      -- ToDo: SUMME in German locale
      Sum -> "SUM(%s)"
      Minimum -> "MIN(%s)"
      Maximum -> "MAX(%s)"
      Median -> "MEDIAN(%s)"
      Quartile n -> "QUARTILE(%s;" ++ show n ++ ")"

parentheses :: Bool -> String -> String
parentheses b xs =
   if b
     then '(' : xs ++ ")"
     else xs


constant :: Integer -> Formula
constant = Const


instance Additive.C Formula where
   zero = Const zero
   x + y  =  Infix Plus x y
   x - y  =  Infix Minus x y

instance Ring.C Formula where
   fromInteger = Const . Ring.fromInteger
   one = Const one
   x * y  =  Infix Times x y

instance Field.C Formula where
   x / y  =  Infix Divide x y


{- |
This type tracks the operations applied to the @a@-typed value.
Cf. EFA.Equation.Pair
-}
data Tracked formula a = Tracked {trackedFormula :: formula, trackedNumber :: a}
   deriving (Show)

instance Bifunctor Tracked where
   bimap f g (Tracked formula number) = Tracked (f formula) (g number)

instance Biapplicative Tracked where
   bipure = Tracked
   Tracked ff f <<*>> Tracked fx x  =  Tracked (ff fx) (f x)

instance Bifoldable Tracked where
   bifoldMap = bifoldMapDefault

instance Bitraversable Tracked where
   bitraverse f g (Tracked formula number) =
      liftA2 Tracked (f formula) (g number)

type FormulaTracked = Tracked Formula
type CellTracked = Tracked CellId

untracked :: String -> Tracked (Maybe Formula) String
untracked = Tracked Nothing


instance (Eq a) => Eq (Tracked formula a) where
   (==)  =  equating trackedNumber

instance (Ord a) => Ord (Tracked formula a) where
   compare  =  comparing trackedNumber


instance
   (Additive.C formula, Additive.C a) =>
      Additive.C (Tracked formula a) where
   zero = bipure zero zero
   (+) = biliftA2 (+) (+)
   (-) = biliftA2 (-) (-)

instance (Ring.C formula, Ring.C a) => Ring.C (Tracked formula a) where
   fromInteger n = bipure (Ring.fromInteger n) (Ring.fromInteger n)
   one = bipure one one
   (*) = biliftA2 (*) (*)

instance (Field.C formula, Field.C a) => Field.C (Tracked formula a) where
   (/) = biliftA2 (/) (/)


trackFraction :: (Real.C a, ToRational.C a) => a -> FormulaTracked a
trackFraction x =
   case splitFraction x of
      (i, frac) ->
         Tracked (if isZero frac then constant i else realToField x) x

trackedVar :: CellTracked a -> FormulaTracked a
trackedVar = Bifunc.first Var

fromInt :: (Ring.C a) => Tracked formula Int -> Tracked formula a
fromInt = Bifunc.second fromIntegral


pi :: (Trans.C a) => FormulaTracked a
pi = Tracked (Any "PI()") Trans.pi



{- |
Simplified implementation that works only for a rectangle of cells.
-}
cellRange :: (CellId -> String) -> (CellId, CellId) -> String
cellRange fmtCellId (from, to) =
   printf "%s:%s" (fmtCellId from) (fmtCellId to)

{- |
The set of cells must form a rectangle.
It is an unchecked error if that does not apply.
-}
aggregate ::
   Agg -> (NonEmpty.T [] a -> a) ->
   NonEmpty.T [] (CellTracked a) -> FormulaTracked a
aggregate op agg xs =
   Tracked
      (Agg op
         (let cells = fmap trackedFormula xs
          in  [Right (NonEmpty.minimum cells, NonEmpty.maximum cells)]))
      (agg $ fmap trackedNumber xs)

sum :: (Additive.C a) => [CellTracked a] -> FormulaTracked a
sum =
   maybe zero (aggregate Sum (Additive.sum . NonEmpty.flatten)) .
   NonEmpty.fetch