packages feed

clerk-0.1.0.1: src/Example.lhs

 ## Example

This is a demo program that uses `clerk` to produce an `xlsx` file that looks as follows:

<img src = "https://raw.githubusercontent.com/deemp/clerk/master/README/demoValues.png" width = "80%">

Alternatively, with formulas enabled:

<img src = "https://raw.githubusercontent.com/deemp/clerk/master/README/demoFormulas.png" width = "80%">

This file has a sheet with several tables. These are tables for
constants' header, a table per a constant's value (three of them), volume & pressure header, volume & pressure values.
Let's see how we can construct such a sheet.

 ### Imports

First, we import the necessary stuff.

> module Example (main) where
> import Clerk
> import Codec.Xlsx qualified as X
> import Codec.Xlsx.Formatted qualified as X
> import Control.Lens ((%~), (&), (?~))
> import Data.ByteString.Lazy qualified as L
> import Data.Text qualified as T
> import Data.Time.Clock.POSIX (getPOSIXTime)
> import Control.Monad (void)

 ### Inputs

Following that, we declare a number of data types that we'll use to store the input values.

A type for constants' headers.

> data ConstantsHeader = ConstantsHeader
>     { hConstant :: String
>     , hSymbol :: String
>     , hValue :: String
>     , hUnits :: String
>     }
> 
> constantsHeader :: ConstantsHeader
> constantsHeader =
>     ConstantsHeader
>         { hConstant = "constant"
>         , hSymbol = "symbol"
>         , hValue = "value"
>         , hUnits = "units"
>         }

A type for constants' data.

> data ConstantsData a = ConstantsData
>     { name :: String
>     , symbol :: String
>     , value :: a
>     , units :: String
>     }

Additionally, we declare a helper type that will store all constants together.

> data ConstantsInput = ConstantsInput
>     { gas :: ConstantsData Double
>     , nMoles :: ConstantsData Double
>     , temperature :: ConstantsData Double
>     }
> 
> constants :: ConstantsInput
> constants =
>     ConstantsInput
>         { gas = ConstantsData "GAS CONSTANT" "R" 0.08206 "L.atm/mol.K"
>         , nMoles = ConstantsData "NUMBER OF MOLES" "n" 1 "moles"
>         , temperature = ConstantsData "TEMPERATURE(K)" "T" 273.2 "K"
>         }

A type for the Volume & Pressure header.

> data ValuesHeader = ValuesHeader
>     { hVolume :: String
>     , hPressure :: String
>     }
> 
> valuesHeader :: ValuesHeader
> valuesHeader =
>     ValuesHeader
>         { hVolume = "VOLUME (L)"
>         , hPressure = "PRESSURE (atm)"
>         }

The last type is for volume inputs. We just generate them

> newtype Volume = Volume
>     { volume :: Double
>     }
> 
> volumeData :: [Volume]
> volumeData = take 10 $ Volume <$> [1 ..]

 ### Styles

Following the headers and data types, we define the styles. Let's start with colors.
We select several color codes and store them into `colors`

> data Colors = Colors
>     { lightBlue :: T.Text
>     , lightGreen :: T.Text
>     , blue :: T.Text
>     , green :: T.Text
>     }
> 
> colors :: Colors
> colors =
>     Colors
>         { lightGreen = "90CCFFCC"
>         , lightBlue = "90CCFFFF"
>         , blue = "FF99CCFF"
>         , green = "FF00FF00"
>         }

Next, we convert them to `FormatCell` function

> colorBlue :: FormatCell
> colorBlue = mkColorStyle colors.blue
> 
> colorLightBlue :: FormatCell
> colorLightBlue = mkColorStyle colors.lightBlue
> 
> colorGreen :: FormatCell
> colorGreen = mkColorStyle colors.green
> 
> colorMixed :: FormatCell
> colorMixed coords idx = mkColorStyle (if even idx then colors.lightGreen else colors.lightBlue) coords idx

Additionally, we compose a transform for the number format

> -- | allow 2 decimal digits
> nf2decimal :: FCTransform
> nf2decimal fc = fc & X.formattedFormat %~ (\ff -> ff & X.formatNumberFormat ?~ X.StdNumberFormat X.Nf2Decimal)

And a transform for centering the cell contents

> alignCenter :: FCTransform
> alignCenter = horizontalAlignment X.CellHorizontalAlignmentCenter

 ### `Builder`s

Now, we are able to compose the `Builder`s for tables.

A builder for the constants header.

<img src = "https://raw.githubusercontent.com/deemp/clerk/master/README/constantsHeader.png" width = "50%">

> constantsHeaderBuilder :: Builder ConstantsHeader CellData (Coords, Coords)
> constantsHeaderBuilder = do
>     tl <- columnWidth 20 (alignCenter <| colorBlue) hConstant
>     columnWidth_ 8 (alignCenter <| colorBlue) hSymbol
>     column_ (alignCenter <| colorBlue) hValue
>     tr <- columnWidth 13 (alignCenter <| colorBlue) hUnits
>     return (unCell tl, unCell tr)

A builder for a constant. We'll use this builder for each constant separately
as each constant produces cells of a specific type.

<img src = "https://raw.githubusercontent.com/deemp/clerk/master/README/constants.png" width = "50%">

> constantBuilder :: forall a. ToCellData a => Builder (ConstantsData a) CellData (Coords, Cell a)
> constantBuilder = do
>     topLeft <- column colorLightBlue name
>     column_ colorLightBlue symbol
>     value <- column (nf2decimal <| colorLightBlue) value
>     column_ colorLightBlue units
>     return (unCell topLeft, value)

A builder for values' header.

<img src = "https://raw.githubusercontent.com/deemp/clerk/master/README/valuesHeader.png" width = "50%">

> valuesHeaderBuilder :: Builder ValuesHeader CellData Coords
> valuesHeaderBuilder = do
>     tl <- columnWidth 12 colorGreen hVolume
>     columnWidth_ 16 colorGreen hPressure
>     return (unCell tl)

A builder for volume & pressure (formulas enabled)

<img src = "https://raw.githubusercontent.com/deemp/clerk/master/README/valuesFormulas.png" width = "50%">

To pass values in a structured way, we make a helper type.

> data ConstantsValues = ConstantsValues
>     { gas :: Cell Double
>     , nMoles :: Cell Double
>     , temperature :: Cell Double
>     }

Next, we define a function to produce a builder.

> valuesBuilder :: ConstantsValues -> Builder Volume CellData ()
> valuesBuilder cv = do
>     volume' <- column colorMixed volume
>     let pressure' = ex cv.gas |*| ex cv.nMoles |*| ex cv.temperature |/| ex volume'
>     column_ (nf2decimal <| colorMixed) (const pressure')

 ### `SheetBuilder`

The `SheetBuilder` is used to place builders onto a sheet and glue them together

> full :: SheetBuilder ()
> full = do
>     (constantsHeaderTL, constantsHeaderTR) <- placeInput (Coords 2 2) constantsHeader constantsHeaderBuilder
>     (gasTL, gas) <- placeInput (overRow (+ 2) constantsHeaderTL) constants.gas constantBuilder
>     (nMolesTL, nMoles) <- placeInput (overRow (+ 1) gasTL) constants.nMoles constantBuilder
>     temperature <- snd <$> placeInput (overRow (+ 1) nMolesTL) constants.temperature constantBuilder
>     valuesHeaderTL <- placeInput (overCol (+ 2) constantsHeaderTR) valuesHeader valuesHeaderBuilder
>     placeInputs_ (overRow (+ 2) valuesHeaderTL) volumeData (valuesBuilder $ ConstantsValues{..})

 ### Result

Now, we can write the result and get the spreadsheet images that you've seen at the top of this tutorial.

> writeWorksheet :: SheetBuilder a -> String -> IO ()
> writeWorksheet tb name = do
>     ct <- getPOSIXTime
>     let
>         xlsx = composeXlsx [("List 1", void tb)]
>     L.writeFile ("example-" <> name <> ".xlsx") $ X.fromXlsx ct xlsx
> 
> writeEx :: IO ()
> writeEx = writeWorksheet full "1"
> 
> main :: IO ()
> main = writeEx

Run

< stack run

to get `example-1.xlsx`.

With formulas enabled, `example-1.xlsx` looks like this:

<img src = "https://raw.githubusercontent.com/deemp/clerk/master/README/demoFormulas.png" width = "80%">