clerk-0.1.0.0: src/Clerk.hs
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{-# OPTIONS_GHC -Wno-unused-top-binds #-}
module Clerk (
Builder,
Cell (unCell),
CellData,
Coords (Coords),
FCTransform,
FormatCell,
ToCellData,
SheetBuilder (..),
column,
columnWidth,
columnWidth_,
column_,
composeXlsx,
ex,
ex',
horizontalAlignment,
mkColorStyle,
overCol,
overRow,
placeInput,
placeInputs_,
(|+|),
(|-|),
(|*|),
(|/|),
(|:|),
(|^|),
(|$|),
(<|),
Expr(..)
) where
import Codec.Xlsx qualified as X
import Codec.Xlsx.Formatted qualified as X
import Control.Lens (Identity (runIdentity), (%~), (&), (?~))
import Control.Lens.Operators ((.~))
import Control.Monad.State (
MonadState,
StateT (StateT),
evalStateT,
get,
gets,
modify,
void,
)
import Control.Monad.Trans.Writer (execWriter, runWriter)
import Control.Monad.Writer (MonadWriter (..), Writer)
import Data.Char (toUpper)
import Data.Default (Default (..))
import Data.Foldable (Foldable (..))
import Data.List (intercalate)
import Data.Map.Strict qualified as Map (Map, insert)
import Data.Maybe (isJust, maybeToList)
import Data.Text qualified as T
-- Coords
-- TODO Allow sheet addresses
-- | Coords of a cell
data Coords = Coords {row :: Int, col :: Int}
instance Show Coords where
show :: Coords -> String
show (Coords{..}) = toLetters col <> show row
alphabet :: [String]
alphabet = (: "") <$> ['A' .. 'Z']
toLetters :: Int -> String
toLetters x = f "" (x - 1)
where
new :: Int -> String -> String
new cur acc = alphabet !! (cur `mod` 26) <> acc
f :: String -> Int -> String
f acc cur = if cur `div` 26 > 0 then f (new cur acc) (cur `div` 26 - 1) else new cur acc
{-
>>>toLetters <$> [1, 26, 27, 52, 78]
["A","Z","AA","AZ","BZ"]
-}
instance Num Coords where
(+) :: Coords -> Coords -> Coords
(+) (Coords r1 c1) (Coords r2 c2) = Coords (r1 + r2) (c1 + c2)
(*) :: Coords -> Coords -> Coords
(*) (Coords r1 c1) (Coords r2 c2) = Coords (r1 * r2) (c1 * c2)
(-) :: Coords -> Coords -> Coords
(-) (Coords r1 c1) (Coords r2 c2) = Coords (r1 - r2) (c1 - c2)
abs :: Coords -> Coords
abs (Coords r1 c1) = Coords (abs r1) (abs c1)
signum :: Coords -> Coords
signum (Coords r1 c1) = Coords (signum r1) (signum c1)
fromInteger :: Integer -> Coords
fromInteger x = Coords (fromIntegral (abs x)) (fromIntegral (abs x))
-- Cell
-- | Index of an input
type Index = Int
-- | Format a single cell depending on its coordinates, index, and data
type FormatCell = Coords -> Index -> CellData -> X.FormattedCell
-- | Cell with contents, style, column props
data CellTemplate input output = CellTemplate
{ mkOutput :: input -> output
, format :: FormatCell
, columnsProperties :: Maybe X.ColumnsProperties
}
-- Transforms
type FormattedMap = Map.Map (X.RowIndex, X.ColumnIndex) X.FormattedCell
type FMTransform = FormattedMap -> FormattedMap
type WSTransform = X.Worksheet -> X.Worksheet
-- | A transform of the map of formats and a transform of a worksheet
data Transform = Transform {fmTransform :: FMTransform, wsTransform :: WSTransform}
instance Semigroup Transform where
(<>) :: Transform -> Transform -> Transform
(Transform a1 b1) <> (Transform a2 b2) = Transform (a2 . a1) (b2 . b1)
instance Monoid Transform where
mempty :: Transform
mempty = Transform id id
instance Default Transform where
def :: Transform
def = mempty
-- Template
-- | Template for multiple cells
newtype Template input output = Template [CellTemplate input output]
deriving (Semigroup, Monoid)
-- Builder
-- | A builder
newtype Builder input output a = Builder {unBuilder :: StateT Coords (Writer (Template input output)) a}
deriving (Functor, Applicative, Monad, MonadState Coords, MonadWriter (Template input output))
-- | Run builder on given coordinates. Get a result and a template
runBuilder :: Builder input output a -> Coords -> (a, Template input output)
runBuilder builder coord = runWriter (evalStateT (unBuilder builder) coord)
-- | Run builder on given coordinates. Get a template
evalBuilder :: Builder input output a -> Coords -> Template input output
evalBuilder builder coord = snd $ runBuilder builder coord
-- | Run builder on given coordinates. Get a result
execBuilder :: Builder input output a -> Coords -> a
execBuilder builder coord = fst $ runBuilder builder coord
type RenderTemplate m input output = (Monad m, ToCellData output) => Coords -> Index -> input -> Template input output -> m Transform
type RenderBuilderInputs m input output a = (Monad m, ToCellData output) => Builder input output a -> [input] -> m (Transform, a)
type RenderBuilderInput m input output a = (Monad m, ToCellData output) => Builder input output a -> input -> m (Transform, a)
-- Render
-- meaning produce a transform
-- | Render a builder with given coords and inputs. Return the result calculated using the topmost row
renderBuilderInputs :: (Monad m, ToCellData output) => Coords -> RenderTemplate m input output -> RenderBuilderInputs m input output a
renderBuilderInputs offset render builder inputs = ret
where
ts =
[ (coord, template)
| row <- [0 .. length inputs]
, let coord = offset + Coords{row, col = 0}
template = evalBuilder builder coord
]
-- result obtained from the top row
a = execBuilder builder (offset + Coords{row = 0, col = 0})
transform =
fold
<$> sequenceA
( zipWith3
( \input inputIdx (coord, template) ->
render coord inputIdx input template
)
inputs
[0 ..]
ts
)
ret = (,a) <$> transform
-- | Render a template with a given offset, input index and input
renderTemplate :: RenderTemplate m input output
renderTemplate Coords{..} inputIdx input (Template columns) = return $ fold ps
where
ps =
zipWith
( \columnIdx mk ->
let
CellTemplate{..} = mk
cd' = toCellData (mkOutput input)
col' = (col + columnIdx)
coords' = Coords row col'
c = format coords' inputIdx cd'
fmTransform = Map.insert (fromIntegral row, fromIntegral col') c
wsTransform
-- add column width only once
| inputIdx == 0 = X.wsColumnsProperties %~ (\x -> x ++ maybeToList columnsProperties)
| otherwise = id
in
def{fmTransform, wsTransform}
)
[0 ..]
columns
-- Columns
newtype ColumnsProperties = ColumnsProperties {unColumnsProperties :: X.ColumnsProperties}
instance Default ColumnsProperties where
def :: ColumnsProperties
def =
ColumnsProperties
X.ColumnsProperties
{ cpMin = 1
, cpMax = 1
, cpWidth = Nothing
, cpStyle = Nothing
, cpHidden = False
, cpCollapsed = False
, cpBestFit = False
}
-- TODO fix doesn't work for non-first row
-- need to filter the final list
-- | Produce a column with a given style and width and get a cell
columnWidthCell :: forall a input output. Maybe Double -> FormatCell -> (input -> output) -> Builder input output (Cell a)
columnWidthCell width format mkOutput = do
coords <- get
let columnsProperties =
Just $
(unColumnsProperties def)
{ X.cpMin = coords.col
, X.cpMax = coords.col
, X.cpWidth = width
}
tell (Template [CellTemplate{format, mkOutput, columnsProperties}])
cell <- gets Cell
modify (\x -> x{col = (x.col) + 1})
return cell
columnWidth :: ToCellData output => Double -> FormatCell -> (input -> output) -> Builder input CellData (Cell a)
columnWidth width fmtCell mkOutput = columnWidthCell (Just width) fmtCell (toCellData . mkOutput)
columnWidth_ :: ToCellData output => Double -> FormatCell -> (input -> output) -> Builder input CellData ()
columnWidth_ width fmtCell mkOutput = void (columnWidth width fmtCell mkOutput)
-- | Produce a column with a given style and get a cell
column :: ToCellData output => FormatCell -> (input -> output) -> Builder input CellData (Cell a)
column fmtCell mkOutput = columnWidthCell Nothing fmtCell (toCellData . mkOutput)
-- | Produce a column with a given style
column_ :: ToCellData output => FormatCell -> (input -> output) -> Builder input CellData ()
column_ fmtCell mkOutput = void (column fmtCell mkOutput)
-- | Produce a transform and a result from inputs and a builder
composeTransformResult :: forall a input output. ToCellData output => RenderTemplate Identity input output -> Coords -> [input] -> Builder input output a -> (Transform, a)
composeTransformResult renderTemplate' offset input builder = runIdentity $ renderBuilderInputs offset renderTemplate' builder input
-- | Produce a result
defaultTransformResult :: ToCellData output => Coords -> [input] -> Builder input output a -> (Transform, a)
defaultTransformResult = composeTransformResult renderTemplate
-- TODO
-- Store current sheet info for formulas
-- | Top monad to compose the results of Builders
newtype SheetBuilder a = SheetBuilder {unSheetBuilder :: Writer Transform a}
deriving (Functor, Applicative, Monad, MonadWriter Transform)
class Functor a => Discardable a where
discard :: a b -> a ()
placeInputs :: ToCellData output => Coords -> [input] -> Builder input output a -> SheetBuilder a
placeInputs offset inputs b = do
let transformResult = defaultTransformResult offset inputs b
tell (fst transformResult)
return (snd transformResult)
placeInput :: ToCellData output => Coords -> input -> Builder input output a -> SheetBuilder a
placeInput coords input = placeInputs coords [input]
placeInputs_ :: ToCellData output => Coords -> [input] -> Builder input output a -> SheetBuilder ()
placeInputs_ coords inputs b = void (placeInputs coords inputs b)
placeInput_ :: ToCellData output => Coords -> input -> Builder input output a -> SheetBuilder ()
placeInput_ coords input = placeInputs_ coords [input]
composeXlsx :: [(T.Text, SheetBuilder ())] -> X.Xlsx
composeXlsx sheetBuilders = workBook'
where
getTransform x = execWriter $ unSheetBuilder x
workBook = X.formatWorkbook ((\(name, tf') -> (name, (getTransform tf').fmTransform X.def)) <$> sheetBuilders) X.def
filterWidths ws = ws & X.wsColumnsProperties %~ filter (isJust . X.cpWidth)
workBook' =
workBook
& X.xlSheets
%~ \sheets -> zipWith (\x (name, ws) -> (name, (getTransform x).wsTransform ws & filterWidths)) (snd <$> sheetBuilders) sheets
{- Lib. Formulas -}
-- | Formula expressions
data Expr t
= Add (Expr t) (Expr t)
| Sub (Expr t) (Expr t)
| Mul (Expr t) (Expr t)
| Div (Expr t) (Expr t)
| Function String [Expr t]
| Range (Expr t) (Expr t)
| ExprCell (Cell t)
deriving (Functor)
-- | Change phantom type of an Expr
ex' :: forall b a. Expr a -> Expr b
ex' = toExpr
-- | Something that can be turned into an expression
class ToExpr v where
toExpr :: v -> Expr t
instance ToExpr (Cell a) where
toExpr :: Cell a -> Expr t
toExpr (Cell c) = ExprCell (Cell c)
instance ToExpr Coords where
toExpr :: Coords -> Expr t
toExpr c = ExprCell (Cell c)
toExprCell :: Cell a -> Coords
toExprCell (Cell c1) = c1
instance ToExpr (Expr a) where
toExpr :: Expr a -> Expr b
toExpr (Add l r) = Add (toExpr l) (toExpr r)
toExpr (Sub l r) = Sub (toExpr l) (toExpr r)
toExpr (Mul l r) = Mul (toExpr l) (toExpr r)
toExpr (Div l r) = Div (toExpr l) (toExpr r)
toExpr (Function name args) = Function name (toExpr <$> args)
toExpr (Range l r) = Range (toExpr l) (toExpr r)
toExpr (ExprCell (Cell c)) = ExprCell (Cell c)
showOp2 :: (Show a, Show b) => String -> a -> b -> String
showOp2 operator c1 c2 = show c1 <> operator <> show c2
mkOp2 :: (ToExpr a, ToExpr b) => (Expr t -> Expr t -> Expr t) -> a -> b -> Expr t
mkOp2 f c1 c2 = f (toExpr c1) (toExpr c2)
mkNumOp2 :: (Num t, ToExpr a, ToExpr b) => (Expr t -> Expr t -> Expr t) -> a -> b -> Expr t
mkNumOp2 = mkOp2
-- | Assemble a range expression
(|:|) :: Cell a -> Cell b -> Expr c
(|:|) = mkOp2 Range
infixr 5 |:|
-- | Assemble an addition expression
(|+|) :: Num a => Expr a -> Expr a -> Expr a
(|+|) = mkNumOp2 Add
infixl 6 |+|
-- | Assemble a subtraction expression
(|-|) :: Num a => Expr a -> Expr a -> Expr a
(|-|) = mkNumOp2 Sub
infixl 6 |-|
-- | Assemble a division expression
(|/|) :: Num a => Expr a -> Expr a -> Expr a
(|/|) = mkNumOp2 Div
infixl 7 |/|
-- | Assemble a multiplication expression
(|*|) :: Num a => Expr a -> Expr a -> Expr a
(|*|) = mkNumOp2 Mul
infixl 6 |*|
-- | Assemble a multiplication expression
(|^|) :: Num a => Expr a -> Expr a -> Expr a
(|^|) = mkNumOp2 Mul
infixr 8 |^|
-- | Assemble a function expression
(|$|) :: ToExpr a => String -> [a] -> Expr t
(|$|) n as = Function (toUpper <$> n) (toExpr <$> as)
infixr 0 |$|
instance Show (Expr t) where
show :: Expr t -> String
show (Add c1 c2) = showOp2 "+" c1 c2
show (Sub c1 c2) = showOp2 "-" c1 c2
show (Mul c1 c2) = showOp2 "*" c1 c2
show (Div c1 c2) = showOp2 "/" c1 c2
show (Range c1 c2) = showOp2 ":" c1 c2
show (ExprCell (Cell e)) = show e
show (Function n as) = n <> "(" <> intercalate "," (show <$> as) <> ")"
-- | Coordinates of a cell with a given phantom type
newtype Cell a = Cell {unCell :: Coords} deriving (Functor)
cellCol :: Cell a -> Int
cellCol (Cell c) = c.col
cellRow :: Cell a -> Int
cellRow (Cell c) = c.row
overCol :: (Int -> Int) -> Coords -> Coords
overCol f (Coords row col) = Coords row (f col)
overRow :: (Int -> Int) -> Coords -> Coords
overRow f (Coords row col) = Coords (f row) col
instance Num (Cell a) where
(+) :: Cell a -> Cell a -> Cell a
(+) (Cell c1) (Cell c2) = Cell (c1 + c2)
(*) :: Cell a -> Cell a -> Cell a
(*) (Cell c1) (Cell c2) = Cell (c1 * c2)
(-) :: Cell a -> Cell a -> Cell a
(-) (Cell c1) (Cell c2) = Cell (c1 - c2)
abs :: Cell a -> Cell a
abs (Cell c1) = Cell (abs c1)
signum :: Cell a -> Cell a
signum (Cell c1) = Cell (signum c1)
fromInteger :: Integer -> Cell a
fromInteger x = Cell (fromInteger x)
-- | Convert a typed cell to an expression
ex :: Cell a -> Expr a
ex = toExpr
{- Lib.Example.Typechecks
>>>str = ex (Cell (Coords 1 1)) :: Expr String
>>> str |+| str
No instance for (Num String) arising from a use of `|+|'
In the expression: str |+| str
NOW In an equation for `it_a1TViD': it_a1TViD = str |+| str
>>>int = ex (Cell (Coords 1 1)) :: Expr Int
>>>double = ex (Cell (Coords 2 5)) :: Expr Double
>>>ex' int |+| double
A1+E2
-}
mkColorStyle :: T.Text -> FormatCell
mkColorStyle color _ _ cd =
X.def
& X.formattedCell .~ dataCell cd
& X.formattedFormat
.~ ( X.def
& X.formatFill
?~ ( X.def
& X.fillPattern
?~ ( X.def
& ( X.fillPatternFgColor
?~ (X.def & X.colorARGB ?~ color)
)
& ( X.fillPatternType
?~ X.PatternTypeSolid
)
)
)
)
type FCTransform = X.FormattedCell -> X.FormattedCell
infixl 5 <|
(<|) :: FCTransform -> FormatCell -> FormatCell
f <| fc = \coords idx cd -> f $ fc coords idx cd
horizontalAlignment :: X.CellHorizontalAlignment -> FCTransform
horizontalAlignment alignment fc =
fc
& X.formattedFormat
%~ ( \ff ->
ff
& X.formatAlignment
?~ ( X.def & X.alignmentHorizontal ?~ alignment
)
)
-- | A union of some Cell components
data CellData
= CellFormula X.CellFormula
| CellValue X.CellValue
| CellComment X.Comment
-- | Convert some Cell component into a cell
dataCell :: CellData -> X.Cell
dataCell cd =
X.def
& case cd of
CellValue d -> X.cellValue ?~ d
CellFormula d -> X.cellFormula ?~ d
CellComment d -> X.cellComment ?~ d
class ToCellData a where
toCellData :: a -> CellData
instance ToCellData String where
toCellData :: String -> CellData
toCellData = CellValue . X.CellText . T.pack
instance ToCellData Int where
toCellData :: Int -> CellData
toCellData = CellValue . X.CellDouble . fromIntegral
instance ToCellData Double where
toCellData :: Double -> CellData
toCellData = CellValue . X.CellDouble
instance ToCellData Bool where
toCellData :: Bool -> CellData
toCellData = CellValue . X.CellBool
instance ToCellData CellData where
toCellData :: CellData -> CellData
toCellData = id
instance ToCellData (Expr a) where
toCellData :: Expr a -> CellData
toCellData e =
CellFormula
X.CellFormula
{ X._cellfAssignsToName = False
, X._cellfCalculate = True
, X._cellfExpression = X.NormalFormula $ X.Formula $ T.pack $ show e
}