module Hledger.Data.FormatStrings ( parseFormatString , formatStrings , formatValue , FormatString(..) , HledgerFormatField(..) , tests ) where import Numeric import Data.Char (isPrint) import Data.Maybe import Test.HUnit import Text.ParserCombinators.Parsec import Text.Printf import Hledger.Data.Types formatValue :: Bool -> Maybe Int -> Maybe Int -> String -> String formatValue leftJustified min max value = printf formatS value where l = if leftJustified then "-" else "" min' = maybe "" show min max' = maybe "" (\i -> "." ++ (show i)) max formatS = "%" ++ l ++ min' ++ max' ++ "s" parseFormatString :: String -> Either String [FormatString] parseFormatString input = case (runParser formatStrings () "(unknown)") input of Left y -> Left $ show y Right x -> Right x {- Parsers -} field :: GenParser Char st HledgerFormatField field = 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 (many1 digit >>= (\s -> return $ FieldNo $ read s)) formatField :: GenParser Char st FormatString formatField = do char '%' leftJustified <- optionMaybe (char '-') minWidth <- optionMaybe (many1 $ digit) maxWidth <- optionMaybe (do char '.'; many1 $ digit) -- TODO: Can this be (char '1') *> (many1 digit) char '(' f <- field 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 formatLiteral :: GenParser Char st FormatString formatLiteral = do s <- many1 c return $ FormatLiteral s where isPrintableButNotPercentage x = isPrint x && (not $ x == '%') c = (satisfy isPrintableButNotPercentage "printable character") <|> try (string "%%" >> return '%') formatStr :: GenParser Char st FormatString formatStr = formatField <|> formatLiteral formatStrings :: GenParser Char st [FormatString] formatStrings = many formatStr testFormat :: FormatString -> String -> String -> Assertion testFormat fs value expected = assertEqual name expected actual where (name, actual) = case fs of FormatLiteral l -> ("literal", formatValue False Nothing Nothing l) FormatField leftJustify min max _ -> ("field", formatValue leftJustify min max value) testParser :: String -> [FormatString] -> Assertion testParser s expected = case (parseFormatString s) of Left error -> assertFailure $ show error Right actual -> assertEqual ("Input: " ++ s) expected actual tests = 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 "" [] , testParser "D" [FormatLiteral "D"] , testParser "%(date)" [FormatField False Nothing Nothing DescriptionField] , testParser "%(total)" [FormatField False Nothing Nothing TotalField] , testParser "Hello %(date)!" [FormatLiteral "Hello ", FormatField False Nothing Nothing DescriptionField, FormatLiteral "!"] , testParser "%-(date)" [FormatField True Nothing Nothing DescriptionField] , testParser "%20(date)" [FormatField False (Just 20) Nothing DescriptionField] , testParser "%.10(date)" [FormatField False Nothing (Just 10) DescriptionField] , testParser "%20.10(date)" [FormatField False (Just 20) (Just 10) DescriptionField] , testParser "%20(account) %.10(total)\n" [ FormatField False (Just 20) Nothing AccountField , FormatLiteral " " , FormatField False Nothing (Just 10) TotalField , FormatLiteral "\n" ] ]