{- | Export table data as OpenDocument Spreadsheet . This format supports character encodings, fixed header rows and columns, number formatting, text styles, merged cells, formulas, hyperlinks. Currently we support Flat ODS, a plain uncompressed XML format. This is derived from -} module Hledger.Write.Ods ( printFods, ) where import qualified Hledger.Write.Spreadsheet as Spr import Hledger.Write.Spreadsheet (Type(..), Style(..), Emphasis(..), Cell(..)) import Hledger.Data.Types (CommoditySymbol, AmountPrecision(..)) import Hledger.Data.Types (acommodity, aquantity, astyle, asprecision) import qualified Data.Text.Lazy as TL import qualified Data.Text as T import Data.Text (Text) import qualified Data.Foldable as Fold import qualified Data.List as List import qualified Data.Map as Map import qualified Data.Set as Set import Data.Foldable (fold) import Data.Map (Map) import Data.Set (Set) import Data.Maybe (catMaybes) import qualified System.IO as IO import Text.Printf (printf) printFods :: IO.TextEncoding -> Map Text ((Maybe Int, Maybe Int), [[Cell Spr.NumLines Text]]) -> TL.Text printFods encoding tables = let fileOpen customStyles = map (map (\c -> case c of '\'' -> '"'; _ -> c)) $ printf "" (show encoding) : "" : "" : " " : " " : " -" : " " : " -" : " " : " " : customStyles ++ "" : [] fileClose = "" : [] tableConfig tableNames = " " : " " : " " : " " : " " : (fold $ flip Map.mapWithKey tableNames $ \tableName (mTopRow,mLeftColumn) -> printf " " tableName : (flip foldMap mLeftColumn $ \leftColumn -> " 2" : printf " %d" leftColumn : printf " %d" leftColumn : []) ++ (flip foldMap mTopRow $ \topRow -> " 2" : printf " %d" topRow : printf " %d" topRow : []) ++ " " : []) ++ " " : " " : " " : " " : " " : [] tableOpen name = "" : "" : printf "" name : [] tableClose = "" : "" : "" : [] in TL.unlines $ map (TL.fromStrict . T.pack) $ fileOpen (let styles = cellStyles (foldMap (concat.snd) tables) in (numberConfig =<< Set.toList (foldMap (numberParams.snd) styles)) ++ (cellConfig =<< Set.toList styles)) ++ tableConfig (fmap fst tables) ++ (Map.toAscList tables >>= \(name,(_,table)) -> tableOpen name ++ (table >>= \row -> "" : (row >>= formatCell) ++ "" : []) ++ tableClose) ++ fileClose dataStyleFromType :: Type -> DataStyle dataStyleFromType typ = case typ of TypeString -> DataString TypeDate -> DataDate TypeAmount amt -> DataAmount (acommodity amt) (asprecision $ astyle amt) TypeMixedAmount -> DataMixedAmount cellStyles :: (Ord border) => [Cell border Text] -> Set ((Spr.Border border, Style), DataStyle) cellStyles = Set.fromList . map (\cell -> ((cellBorder cell, cellStyle cell), dataStyleFromType $ cellType cell)) numberStyleName :: (CommoditySymbol, AmountPrecision) -> String numberStyleName (comm, prec) = printf "%s-%s" comm $ case prec of NaturalPrecision -> "natural" Precision k -> show k numberParams :: DataStyle -> Set (CommoditySymbol, AmountPrecision) numberParams (DataAmount comm prec) = Set.singleton (comm, prec) numberParams _ = Set.empty numberConfig :: (CommoditySymbol, AmountPrecision) -> [String] numberConfig (comm, prec) = let precStr = case prec of NaturalPrecision -> "" Precision k -> printf " number:decimal-places='%d'" k name = numberStyleName (comm, prec) in printf " " name : printf " " precStr : printf " %s%s" (if T.null comm then "" else " ") comm : " " : [] emphasisName :: Emphasis -> String emphasisName emph = case emph of Item -> "item" Total -> "total" cellStyleName :: Style -> String cellStyleName style = case style of Head -> "head" Body emph -> emphasisName emph linesName :: Spr.NumLines -> Maybe String linesName prop = case prop of Spr.NoLine -> Nothing Spr.SingleLine -> Just "single" Spr.DoubleLine -> Just "double" linesStyle :: Spr.NumLines -> String linesStyle prop = case prop of Spr.NoLine -> "none" Spr.SingleLine -> "1.5pt solid #000000" Spr.DoubleLine -> "1.5pt double-thin #000000" borderLabels :: Spr.Border String borderLabels = Spr.Border "left" "right" "top" "bottom" borderName :: Spr.Border Spr.NumLines -> String borderName border = (\bs -> case bs of [] -> "noborder" _ -> ("border="++) $ List.intercalate "," $ map (\(name,num) -> name ++ ':' : num) bs) $ catMaybes $ Fold.toList $ liftA2 (\name numLines -> (,) name <$> linesName numLines) borderLabels border borderStyle :: Spr.Border Spr.NumLines -> [String] borderStyle border = if border == Spr.noBorder then [] else (:[]) $ printf " " $ (id :: String -> String) $ fold $ liftA2 (printf " fo:border-%s='%s'") borderLabels $ fmap linesStyle border data DataStyle = DataString | DataDate | DataAmount CommoditySymbol AmountPrecision | DataMixedAmount deriving (Eq, Ord, Show) cellConfig :: ((Spr.Border Spr.NumLines, Style), DataStyle) -> [String] cellConfig ((border, cstyle), dataStyle) = let moreStyles = borderStyle border ++ ( case cstyle of Body Item -> [] Body Total -> [" "] Head -> " " : " " : [] ) ++ ( case dataStyle of DataMixedAmount -> [" "] _ -> [] ) cstyleName = cellStyleName cstyle bordName = borderName border style :: String style = case dataStyle of DataDate -> printf "style:name='%s-%s-date' style:data-style-name='iso-date'" cstyleName bordName DataAmount comm prec -> let name = numberStyleName (comm, prec) in printf "style:name='%s-%s-%s' style:data-style-name='number-%s'" cstyleName bordName name name _ -> printf "style:name='%s-%s'" cstyleName bordName in case moreStyles of [] -> printf " " style : [] _ -> printf " " style : moreStyles ++ " " : [] formatCell :: Cell Spr.NumLines Text -> [String] formatCell cell = let style, valueType :: String style = tableStyle styleName cstyleName = cellStyleName $ cellStyle cell bordName = borderName $ cellBorder cell styleName :: String styleName = case dataStyleFromType $ cellType cell of DataDate -> printf "%s-%s-date" cstyleName bordName DataAmount comm prec -> let name = numberStyleName (comm, prec) in printf "%s-%s-%s" cstyleName bordName name _ -> printf "%s-%s" cstyleName bordName tableStyle = printf " table:style-name='%s'" valueType = case cellType cell of TypeAmount amt -> printf "office:value-type='float' office:value='%s'" (show $ aquantity amt) TypeDate -> printf "office:value-type='date' office:date-value='%s'" (cellContent cell) _ -> "office:value-type='string'" in printf "" style valueType : printf "%s" (escape $ T.unpack $ cellContent cell) : "" : [] escape :: String -> String escape = concatMap $ \c -> case c of '\n' -> " " '&' -> "&" '<' -> "<" '>' -> ">" '"' -> """ '\'' -> "'" _ -> [c]