{-# LANGUAGE FlexibleContexts, TypeFamilies, PackageImports #-}
module Hledger.Data.StringFormat (
parseStringFormat
, defaultStringFormatStyle
, StringFormat(..)
, StringFormatComponent(..)
, ReportItemField(..)
, tests_Hledger_Data_StringFormat
) where
import Prelude ()
import "base-compat-batteries" Prelude.Compat
import Numeric
import Data.Char (isPrint)
import Data.Maybe
import Test.HUnit
import Text.Megaparsec
import Text.Megaparsec.Char
import Hledger.Utils.Parse
import Hledger.Utils.String (formatString)
data StringFormat =
OneLine [StringFormatComponent]
| TopAligned [StringFormatComponent]
| BottomAligned [StringFormatComponent]
deriving (Show, Eq)
data StringFormatComponent =
FormatLiteral String
| FormatField Bool
(Maybe Int)
(Maybe Int)
ReportItemField
deriving (Show, Eq)
data ReportItemField =
AccountField
| DefaultDateField
| DescriptionField
| TotalField
| DepthSpacerField
| FieldNo Int
deriving (Show, Eq)
parseStringFormat :: String -> Either String StringFormat
parseStringFormat input = case (runParser (stringformatp <* eof) "(unknown)") input of
Left y -> Left $ show y
Right x -> Right x
defaultStringFormatStyle = BottomAligned
stringformatp :: SimpleStringParser StringFormat
stringformatp = do
alignspec <- optional (try $ char '%' >> oneOf "^_,")
let constructor =
case alignspec of
Just '^' -> TopAligned
Just '_' -> BottomAligned
Just ',' -> OneLine
_ -> defaultStringFormatStyle
constructor <$> many componentp
componentp :: SimpleStringParser StringFormatComponent
componentp = formatliteralp <|> formatfieldp
formatliteralp :: SimpleStringParser StringFormatComponent
formatliteralp = do
s <- some c
return $ FormatLiteral s
where
isPrintableButNotPercentage x = isPrint x && (not $ x == '%')
c = (satisfy isPrintableButNotPercentage <?> "printable character")
<|> try (string "%%" >> return '%')
formatfieldp :: SimpleStringParser StringFormatComponent
formatfieldp = do
char '%'
leftJustified <- optional (char '-')
minWidth <- optional (some $ digitChar)
maxWidth <- optional (do char '.'; some $ digitChar)
char '('
f <- fieldp
char ')'
return $ FormatField (isJust leftJustified) (parseDec minWidth) (parseDec maxWidth) f
where
parseDec s = case s of
Just text -> Just m where ((m,_):_) = readDec text
_ -> Nothing
fieldp :: SimpleStringParser ReportItemField
fieldp = do
try (string "account" >> return AccountField)
<|> try (string "depth_spacer" >> return DepthSpacerField)
<|> try (string "date" >> return DescriptionField)
<|> try (string "description" >> return DescriptionField)
<|> try (string "total" >> return TotalField)
<|> try (some digitChar >>= (\s -> return $ FieldNo $ read s))
testFormat :: StringFormatComponent -> String -> String -> Assertion
testFormat fs value expected = assertEqual name expected actual
where
(name, actual) = case fs of
FormatLiteral l -> ("literal", formatString False Nothing Nothing l)
FormatField leftJustify min max _ -> ("field", formatString leftJustify min max value)
testParser :: String -> StringFormat -> Assertion
testParser s expected = case (parseStringFormat s) of
Left error -> assertFailure $ show error
Right actual -> assertEqual ("Input: " ++ s) expected actual
tests_Hledger_Data_StringFormat = test [ formattingTests ++ parserTests ]
formattingTests = [
testFormat (FormatLiteral " ") "" " "
, testFormat (FormatField False Nothing Nothing DescriptionField) "description" "description"
, testFormat (FormatField False (Just 20) Nothing DescriptionField) "description" " description"
, testFormat (FormatField False Nothing (Just 20) DescriptionField) "description" "description"
, testFormat (FormatField True Nothing (Just 20) DescriptionField) "description" "description"
, testFormat (FormatField True (Just 20) Nothing DescriptionField) "description" "description "
, testFormat (FormatField True (Just 20) (Just 20) DescriptionField) "description" "description "
, testFormat (FormatField True Nothing (Just 3) DescriptionField) "description" "des"
]
parserTests = [
testParser "" (defaultStringFormatStyle [])
, testParser "D" (defaultStringFormatStyle [FormatLiteral "D"])
, testParser "%(date)" (defaultStringFormatStyle [FormatField False Nothing Nothing DescriptionField])
, testParser "%(total)" (defaultStringFormatStyle [FormatField False Nothing Nothing TotalField])
, testParser "^%(total)" (TopAligned [FormatField False Nothing Nothing TotalField])
, testParser "_%(total)" (BottomAligned [FormatField False Nothing Nothing TotalField])
, testParser ",%(total)" (OneLine [FormatField False Nothing Nothing TotalField])
, testParser "Hello %(date)!" (defaultStringFormatStyle [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"])
, testParser "%-(date)" (defaultStringFormatStyle [FormatField True Nothing Nothing DescriptionField])
, testParser "%20(date)" (defaultStringFormatStyle [FormatField False (Just 20) Nothing DescriptionField])
, testParser "%.10(date)" (defaultStringFormatStyle [FormatField False Nothing (Just 10) DescriptionField])
, testParser "%20.10(date)" (defaultStringFormatStyle [FormatField False (Just 20) (Just 10) DescriptionField])
, testParser "%20(account) %.10(total)\n" (defaultStringFormatStyle [FormatField False (Just 20) Nothing AccountField
, FormatLiteral " "
, FormatField False Nothing (Just 10) TotalField
, FormatLiteral "\n"
])
]