refactor: parser cleanup
This commit is contained in:
parent
27510b0106
commit
8fd94ef6f5
@ -3,151 +3,7 @@
|
|||||||
|
|
||||||
Parsers for standard ledger and timelog files.
|
Parsers for standard ledger and timelog files.
|
||||||
|
|
||||||
-}
|
Here is the ledger grammar from the ledger 2.5 manual:
|
||||||
|
|
||||||
module Ledger.Parse
|
|
||||||
where
|
|
||||||
import Control.Monad.Error (ErrorT(..), MonadIO, liftIO, throwError, catchError)
|
|
||||||
import Text.ParserCombinators.Parsec
|
|
||||||
import Text.ParserCombinators.Parsec.Char
|
|
||||||
import Text.ParserCombinators.Parsec.Combinator
|
|
||||||
import System.Directory
|
|
||||||
#if __GLASGOW_HASKELL__ <= 610
|
|
||||||
import Prelude hiding (readFile, putStr, putStrLn, print, getContents)
|
|
||||||
import System.IO.UTF8
|
|
||||||
#endif
|
|
||||||
import Ledger.Utils
|
|
||||||
import Ledger.Types
|
|
||||||
import Ledger.Dates
|
|
||||||
import Ledger.AccountName (accountNameFromComponents,accountNameComponents)
|
|
||||||
import Ledger.Amount
|
|
||||||
import Ledger.Transaction
|
|
||||||
import Ledger.Posting
|
|
||||||
import Ledger.Journal
|
|
||||||
import Ledger.Commodity (dollars,dollar,unknown)
|
|
||||||
import System.FilePath(takeDirectory,combine)
|
|
||||||
|
|
||||||
|
|
||||||
-- utils
|
|
||||||
|
|
||||||
-- | Some context kept during parsing.
|
|
||||||
data LedgerFileCtx = Ctx {
|
|
||||||
ctxYear :: !(Maybe Integer) -- ^ the default year most recently specified with Y
|
|
||||||
, ctxCommod :: !(Maybe String) -- ^ I don't know
|
|
||||||
, ctxAccount :: ![String] -- ^ the current stack of "container" accounts specified by !account
|
|
||||||
} deriving (Read, Show)
|
|
||||||
|
|
||||||
emptyCtx :: LedgerFileCtx
|
|
||||||
emptyCtx = Ctx { ctxYear = Nothing, ctxCommod = Nothing, ctxAccount = [] }
|
|
||||||
|
|
||||||
-- containing accounts "nest" hierarchically
|
|
||||||
|
|
||||||
pushParentAccount :: String -> GenParser tok LedgerFileCtx ()
|
|
||||||
pushParentAccount parent = updateState addParentAccount
|
|
||||||
where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 }
|
|
||||||
normalize = (++ ":")
|
|
||||||
|
|
||||||
popParentAccount :: GenParser tok LedgerFileCtx ()
|
|
||||||
popParentAccount = do ctx0 <- getState
|
|
||||||
case ctxAccount ctx0 of
|
|
||||||
[] -> unexpected "End of account block with no beginning"
|
|
||||||
(_:rest) -> setState $ ctx0 { ctxAccount = rest }
|
|
||||||
|
|
||||||
getParentAccount :: GenParser tok LedgerFileCtx String
|
|
||||||
getParentAccount = liftM (concat . reverse . ctxAccount) getState
|
|
||||||
|
|
||||||
setYear :: Integer -> GenParser tok LedgerFileCtx ()
|
|
||||||
setYear y = updateState (\ctx -> ctx{ctxYear=Just y})
|
|
||||||
|
|
||||||
getYear :: GenParser tok LedgerFileCtx (Maybe Integer)
|
|
||||||
getYear = liftM ctxYear getState
|
|
||||||
|
|
||||||
-- let's get to it
|
|
||||||
|
|
||||||
parseLedgerFile :: LocalTime -> FilePath -> ErrorT String IO Journal
|
|
||||||
parseLedgerFile t "-" = liftIO getContents >>= parseLedger t "-"
|
|
||||||
parseLedgerFile t f = liftIO (readFile f) >>= parseLedger t f
|
|
||||||
|
|
||||||
-- | Parses the contents of a ledger file, or gives an error. Requires
|
|
||||||
-- the current (local) time to calculate any unfinished timelog sessions,
|
|
||||||
-- we pass it in for repeatability.
|
|
||||||
parseLedger :: LocalTime -> FilePath -> String -> ErrorT String IO Journal
|
|
||||||
parseLedger reftime inname intxt =
|
|
||||||
case runParser ledgerFile emptyCtx inname intxt of
|
|
||||||
Right m -> liftM (journalConvertTimeLog reftime) $ m `ap` return nulljournal
|
|
||||||
Left err -> throwError $ show err
|
|
||||||
|
|
||||||
|
|
||||||
ledgerFile :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
|
||||||
ledgerFile = do items <- many ledgerItem
|
|
||||||
eof
|
|
||||||
return $ liftM (foldr (.) id) $ sequence items
|
|
||||||
where
|
|
||||||
-- As all ledger line types can be distinguished by the first
|
|
||||||
-- character, excepting transactions versus empty (blank or
|
|
||||||
-- comment-only) lines, can use choice w/o try
|
|
||||||
ledgerItem = choice [ ledgerDirective
|
|
||||||
, liftM (return . addTransaction) ledgerTransaction
|
|
||||||
, liftM (return . addModifierTransaction) ledgerModifierTransaction
|
|
||||||
, liftM (return . addPeriodicTransaction) ledgerPeriodicTransaction
|
|
||||||
, liftM (return . addHistoricalPrice) ledgerHistoricalPrice
|
|
||||||
, ledgerDefaultYear
|
|
||||||
, ledgerIgnoredPrice
|
|
||||||
, ledgerTagDirective
|
|
||||||
, ledgerEndTagDirective
|
|
||||||
, emptyLine >> return (return id)
|
|
||||||
, liftM (return . addTimeLogEntry) timelogentry
|
|
||||||
]
|
|
||||||
|
|
||||||
ledgerDirective :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
|
||||||
ledgerDirective = do char '!' <?> "directive"
|
|
||||||
directive <- many nonspace
|
|
||||||
case directive of
|
|
||||||
"include" -> ledgerInclude
|
|
||||||
"account" -> ledgerAccountBegin
|
|
||||||
"end" -> ledgerAccountEnd
|
|
||||||
_ -> mzero
|
|
||||||
|
|
||||||
ledgerInclude :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
|
||||||
ledgerInclude = do many1 spacenonewline
|
|
||||||
filename <- restofline
|
|
||||||
outerState <- getState
|
|
||||||
outerPos <- getPosition
|
|
||||||
let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"
|
|
||||||
return $ do contents <- expandPath outerPos filename >>= readFileE outerPos
|
|
||||||
case runParser ledgerFile outerState filename contents of
|
|
||||||
Right l -> l `catchError` (throwError . (inIncluded ++))
|
|
||||||
Left perr -> throwError $ inIncluded ++ show perr
|
|
||||||
where readFileE outerPos filename = ErrorT $ liftM Right (readFile filename) `catch` leftError
|
|
||||||
where leftError err = return $ Left $ currentPos ++ whileReading ++ show err
|
|
||||||
currentPos = show outerPos
|
|
||||||
whileReading = " reading " ++ show filename ++ ":\n"
|
|
||||||
|
|
||||||
expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath
|
|
||||||
expandPath pos fp = liftM mkRelative (expandHome fp)
|
|
||||||
where
|
|
||||||
mkRelative = combine (takeDirectory (sourceName pos))
|
|
||||||
expandHome inname | "~/" `isPrefixOf` inname = do homedir <- liftIO getHomeDirectory
|
|
||||||
return $ homedir ++ drop 1 inname
|
|
||||||
| otherwise = return inname
|
|
||||||
|
|
||||||
ledgerAccountBegin :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
|
||||||
ledgerAccountBegin = do many1 spacenonewline
|
|
||||||
parent <- ledgeraccountname
|
|
||||||
newline
|
|
||||||
pushParentAccount parent
|
|
||||||
return $ return id
|
|
||||||
|
|
||||||
ledgerAccountEnd :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
|
||||||
ledgerAccountEnd = popParentAccount >> return (return id)
|
|
||||||
|
|
||||||
-- parsers
|
|
||||||
|
|
||||||
-- | Parse a Journal from either a ledger file or a timelog file.
|
|
||||||
-- It tries first the timelog parser then the ledger parser; this means
|
|
||||||
-- parse errors for ledgers are useful while those for timelogs are not.
|
|
||||||
|
|
||||||
{-| Parse a ledger file. Here is the ledger grammar from the ledger 2.5 manual:
|
|
||||||
|
|
||||||
@
|
@
|
||||||
The ledger file format is quite simple, but also very flexible. It supports
|
The ledger file format is quite simple, but also very flexible. It supports
|
||||||
@ -245,9 +101,147 @@ i, o, b, h
|
|||||||
timelog files.
|
timelog files.
|
||||||
@
|
@
|
||||||
|
|
||||||
See "Tests" for sample data.
|
Here is the timelog grammar from timeclock.el 2.6:
|
||||||
|
|
||||||
|
@
|
||||||
|
A timelog contains data in the form of a single entry per line.
|
||||||
|
Each entry has the form:
|
||||||
|
|
||||||
|
CODE YYYY/MM/DD HH:MM:SS [COMMENT]
|
||||||
|
|
||||||
|
CODE is one of: b, h, i, o or O. COMMENT is optional when the code is
|
||||||
|
i, o or O. The meanings of the codes are:
|
||||||
|
|
||||||
|
b Set the current time balance, or \"time debt\". Useful when
|
||||||
|
archiving old log data, when a debt must be carried forward.
|
||||||
|
The COMMENT here is the number of seconds of debt.
|
||||||
|
|
||||||
|
h Set the required working time for the given day. This must
|
||||||
|
be the first entry for that day. The COMMENT in this case is
|
||||||
|
the number of hours in this workday. Floating point amounts
|
||||||
|
are allowed.
|
||||||
|
|
||||||
|
i Clock in. The COMMENT in this case should be the name of the
|
||||||
|
project worked on.
|
||||||
|
|
||||||
|
o Clock out. COMMENT is unnecessary, but can be used to provide
|
||||||
|
a description of how the period went, for example.
|
||||||
|
|
||||||
|
O Final clock out. Whatever project was being worked on, it is
|
||||||
|
now finished. Useful for creating summary reports.
|
||||||
|
@
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
i 2007/03/10 12:26:00 hledger
|
||||||
|
o 2007/03/10 17:26:02
|
||||||
|
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
module Ledger.Parse
|
||||||
|
where
|
||||||
|
import Control.Monad.Error (ErrorT(..), MonadIO, liftIO, throwError, catchError)
|
||||||
|
import Text.ParserCombinators.Parsec
|
||||||
|
import Text.ParserCombinators.Parsec.Char
|
||||||
|
import Text.ParserCombinators.Parsec.Combinator
|
||||||
|
import System.Directory
|
||||||
|
#if __GLASGOW_HASKELL__ <= 610
|
||||||
|
import Prelude hiding (readFile, putStr, putStrLn, print, getContents)
|
||||||
|
import System.IO.UTF8
|
||||||
|
#endif
|
||||||
|
import Ledger.Utils
|
||||||
|
import Ledger.Types
|
||||||
|
import Ledger.Dates
|
||||||
|
import Ledger.AccountName (accountNameFromComponents,accountNameComponents)
|
||||||
|
import Ledger.Amount
|
||||||
|
import Ledger.Transaction
|
||||||
|
import Ledger.Posting
|
||||||
|
import Ledger.Journal
|
||||||
|
import Ledger.Commodity (dollars,dollar,unknown)
|
||||||
|
import System.FilePath(takeDirectory,combine)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Some context kept during parsing.
|
||||||
|
data LedgerFileCtx = Ctx {
|
||||||
|
ctxYear :: !(Maybe Integer) -- ^ the default year most recently specified with Y
|
||||||
|
, ctxCommod :: !(Maybe String) -- ^ I don't know
|
||||||
|
, ctxAccount :: ![String] -- ^ the current stack of parent accounts specified by !account
|
||||||
|
} deriving (Read, Show)
|
||||||
|
|
||||||
|
emptyCtx :: LedgerFileCtx
|
||||||
|
emptyCtx = Ctx { ctxYear = Nothing, ctxCommod = Nothing, ctxAccount = [] }
|
||||||
|
|
||||||
|
setYear :: Integer -> GenParser tok LedgerFileCtx ()
|
||||||
|
setYear y = updateState (\ctx -> ctx{ctxYear=Just y})
|
||||||
|
|
||||||
|
getYear :: GenParser tok LedgerFileCtx (Maybe Integer)
|
||||||
|
getYear = liftM ctxYear getState
|
||||||
|
|
||||||
|
pushParentAccount :: String -> GenParser tok LedgerFileCtx ()
|
||||||
|
pushParentAccount parent = updateState addParentAccount
|
||||||
|
where addParentAccount ctx0 = ctx0 { ctxAccount = normalize parent : ctxAccount ctx0 }
|
||||||
|
normalize = (++ ":")
|
||||||
|
|
||||||
|
popParentAccount :: GenParser tok LedgerFileCtx ()
|
||||||
|
popParentAccount = do ctx0 <- getState
|
||||||
|
case ctxAccount ctx0 of
|
||||||
|
[] -> unexpected "End of account block with no beginning"
|
||||||
|
(_:rest) -> setState $ ctx0 { ctxAccount = rest }
|
||||||
|
|
||||||
|
getParentAccount :: GenParser tok LedgerFileCtx String
|
||||||
|
getParentAccount = liftM (concat . reverse . ctxAccount) getState
|
||||||
|
|
||||||
|
expandPath :: (MonadIO m) => SourcePos -> FilePath -> m FilePath
|
||||||
|
expandPath pos fp = liftM mkRelative (expandHome fp)
|
||||||
|
where
|
||||||
|
mkRelative = combine (takeDirectory (sourceName pos))
|
||||||
|
expandHome inname | "~/" `isPrefixOf` inname = do homedir <- liftIO getHomeDirectory
|
||||||
|
return $ homedir ++ drop 1 inname
|
||||||
|
| otherwise = return inname
|
||||||
|
|
||||||
|
-- let's get to it
|
||||||
|
|
||||||
|
-- | Parses a ledger file or timelog file to a "Journal", or gives an
|
||||||
|
-- error. Requires the current (local) time to calculate any unfinished
|
||||||
|
-- timelog sessions, we pass it in for repeatability.
|
||||||
|
parseLedgerFile :: LocalTime -> FilePath -> ErrorT String IO Journal
|
||||||
|
parseLedgerFile t "-" = liftIO getContents >>= parseLedger t "-"
|
||||||
|
parseLedgerFile t f = liftIO (readFile f) >>= parseLedger t f
|
||||||
|
|
||||||
|
-- | Like parseLedgerFile, but parses a string. A file path is still
|
||||||
|
-- provided to save in the resulting journal.
|
||||||
|
parseLedger :: LocalTime -> FilePath -> String -> ErrorT String IO Journal
|
||||||
|
parseLedger reftime inname intxt =
|
||||||
|
case runParser ledgerFile emptyCtx inname intxt of
|
||||||
|
Right m -> liftM (journalConvertTimeLog reftime) $ m `ap` return nulljournal
|
||||||
|
Left err -> throwError $ show err -- XXX raises an uncaught exception if we have a parsec user error, eg from many ?
|
||||||
|
|
||||||
|
-- parsers
|
||||||
|
|
||||||
|
-- | Top-level journal parser. Returns a mighty composite, I/O performing,
|
||||||
|
-- error-raising journal transformation, which should be applied to a
|
||||||
|
-- journal to get the final result.
|
||||||
|
ledgerFile :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
||||||
|
ledgerFile = do items <- many ledgerItem
|
||||||
|
eof
|
||||||
|
return $ liftM (foldr (.) id) $ sequence items
|
||||||
|
where
|
||||||
|
-- As all ledger line types can be distinguished by the first
|
||||||
|
-- character, excepting transactions versus empty (blank or
|
||||||
|
-- comment-only) lines, can use choice w/o try
|
||||||
|
ledgerItem = choice [ ledgerExclamationDirective
|
||||||
|
, liftM (return . addTransaction) ledgerTransaction
|
||||||
|
, liftM (return . addModifierTransaction) ledgerModifierTransaction
|
||||||
|
, liftM (return . addPeriodicTransaction) ledgerPeriodicTransaction
|
||||||
|
, liftM (return . addHistoricalPrice) ledgerHistoricalPrice
|
||||||
|
, ledgerDefaultYear
|
||||||
|
, ledgerIgnoredPriceCommodity
|
||||||
|
, ledgerTagDirective
|
||||||
|
, ledgerEndTagDirective
|
||||||
|
, emptyLine >> return (return id)
|
||||||
|
, liftM (return . addTimeLogEntry) timelogentry
|
||||||
|
]
|
||||||
|
|
||||||
emptyLine :: GenParser Char st ()
|
emptyLine :: GenParser Char st ()
|
||||||
emptyLine = do many spacenonewline
|
emptyLine = do many spacenonewline
|
||||||
optional $ (char ';' <?> "comment") >> many (noneOf "\n")
|
optional $ (char ';' <?> "comment") >> many (noneOf "\n")
|
||||||
@ -270,6 +264,41 @@ ledgercommentline = do
|
|||||||
return s
|
return s
|
||||||
<?> "comment"
|
<?> "comment"
|
||||||
|
|
||||||
|
ledgerExclamationDirective :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
||||||
|
ledgerExclamationDirective = do
|
||||||
|
char '!' <?> "directive"
|
||||||
|
directive <- many nonspace
|
||||||
|
case directive of
|
||||||
|
"include" -> ledgerInclude
|
||||||
|
"account" -> ledgerAccountBegin
|
||||||
|
"end" -> ledgerAccountEnd
|
||||||
|
_ -> mzero
|
||||||
|
|
||||||
|
ledgerInclude :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
||||||
|
ledgerInclude = do many1 spacenonewline
|
||||||
|
filename <- restofline
|
||||||
|
outerState <- getState
|
||||||
|
outerPos <- getPosition
|
||||||
|
let inIncluded = show outerPos ++ " in included file " ++ show filename ++ ":\n"
|
||||||
|
return $ do contents <- expandPath outerPos filename >>= readFileE outerPos
|
||||||
|
case runParser ledgerFile outerState filename contents of
|
||||||
|
Right l -> l `catchError` (throwError . (inIncluded ++))
|
||||||
|
Left perr -> throwError $ inIncluded ++ show perr
|
||||||
|
where readFileE outerPos filename = ErrorT $ liftM Right (readFile filename) `catch` leftError
|
||||||
|
where leftError err = return $ Left $ currentPos ++ whileReading ++ show err
|
||||||
|
currentPos = show outerPos
|
||||||
|
whileReading = " reading " ++ show filename ++ ":\n"
|
||||||
|
|
||||||
|
ledgerAccountBegin :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
||||||
|
ledgerAccountBegin = do many1 spacenonewline
|
||||||
|
parent <- ledgeraccountname
|
||||||
|
newline
|
||||||
|
pushParentAccount parent
|
||||||
|
return $ return id
|
||||||
|
|
||||||
|
ledgerAccountEnd :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
||||||
|
ledgerAccountEnd = popParentAccount >> return (return id)
|
||||||
|
|
||||||
ledgerModifierTransaction :: GenParser Char LedgerFileCtx ModifierTransaction
|
ledgerModifierTransaction :: GenParser Char LedgerFileCtx ModifierTransaction
|
||||||
ledgerModifierTransaction = do
|
ledgerModifierTransaction = do
|
||||||
char '=' <?> "modifier transaction"
|
char '=' <?> "modifier transaction"
|
||||||
@ -298,8 +327,8 @@ ledgerHistoricalPrice = do
|
|||||||
restofline
|
restofline
|
||||||
return $ HistoricalPrice date symbol price
|
return $ HistoricalPrice date symbol price
|
||||||
|
|
||||||
ledgerIgnoredPrice :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
ledgerIgnoredPriceCommodity :: GenParser Char LedgerFileCtx (ErrorT String IO (Journal -> Journal))
|
||||||
ledgerIgnoredPrice = do
|
ledgerIgnoredPriceCommodity = do
|
||||||
char 'N' <?> "ignored-price commodity"
|
char 'N' <?> "ignored-price commodity"
|
||||||
many1 spacenonewline
|
many1 spacenonewline
|
||||||
commoditysymbol
|
commoditysymbol
|
||||||
@ -331,8 +360,8 @@ ledgerDefaultYear = do
|
|||||||
setYear y'
|
setYear y'
|
||||||
return $ return id
|
return $ return id
|
||||||
|
|
||||||
-- | Try to parse a ledger entry. If we successfully parse an entry, ensure it is balanced,
|
-- | Try to parse a ledger entry. If we successfully parse an entry,
|
||||||
-- and if we cannot, raise an error.
|
-- check it can be balanced, and fail if not.
|
||||||
ledgerTransaction :: GenParser Char LedgerFileCtx Transaction
|
ledgerTransaction :: GenParser Char LedgerFileCtx Transaction
|
||||||
ledgerTransaction = do
|
ledgerTransaction = do
|
||||||
date <- ledgerdate <?> "transaction"
|
date <- ledgerdate <?> "transaction"
|
||||||
@ -356,8 +385,8 @@ ledgerfulldate = do
|
|||||||
(y,m,d) <- ymd
|
(y,m,d) <- ymd
|
||||||
return $ fromGregorian (read y) (read m) (read d)
|
return $ fromGregorian (read y) (read m) (read d)
|
||||||
|
|
||||||
-- | Match a partial M/D date in a ledger. Warning, this terminates the
|
-- | Match a partial M/D date in a ledger, and also require that a default
|
||||||
-- program if it finds a match when there is no default year specified.
|
-- year directive was previously encountered.
|
||||||
ledgerpartialdate :: GenParser Char LedgerFileCtx Day
|
ledgerpartialdate :: GenParser Char LedgerFileCtx Day
|
||||||
ledgerpartialdate = do
|
ledgerpartialdate = do
|
||||||
(_,m,d) <- md
|
(_,m,d) <- md
|
||||||
@ -381,7 +410,7 @@ ledgerdatetime = do
|
|||||||
ledgereffectivedate :: Day -> GenParser Char LedgerFileCtx (Maybe Day)
|
ledgereffectivedate :: Day -> GenParser Char LedgerFileCtx (Maybe Day)
|
||||||
ledgereffectivedate actualdate = do
|
ledgereffectivedate actualdate = do
|
||||||
char '='
|
char '='
|
||||||
-- kludgily use actual date for default year
|
-- kludgy way to use actual date for default year
|
||||||
let withDefaultYear d p = do
|
let withDefaultYear d p = do
|
||||||
y <- getYear
|
y <- getYear
|
||||||
let (y',_,_) = toGregorian d in setYear y'
|
let (y',_,_) = toGregorian d in setYear y'
|
||||||
@ -397,9 +426,9 @@ ledgerstatus = try (do { many1 spacenonewline; char '*' <?> "status"; return Tru
|
|||||||
ledgercode :: GenParser Char st String
|
ledgercode :: GenParser Char st String
|
||||||
ledgercode = try (do { many1 spacenonewline; char '(' <?> "code"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""
|
ledgercode = try (do { many1 spacenonewline; char '(' <?> "code"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""
|
||||||
|
|
||||||
-- Complicated to handle intermixed comment lines.. please make me better.
|
|
||||||
ledgerpostings :: GenParser Char LedgerFileCtx [Posting]
|
ledgerpostings :: GenParser Char LedgerFileCtx [Posting]
|
||||||
ledgerpostings = do
|
ledgerpostings = do
|
||||||
|
-- complicated to handle intermixed comment lines.. please make me better.
|
||||||
ctx <- getState
|
ctx <- getState
|
||||||
let parses p = isRight . parseWithCtx ctx p
|
let parses p = isRight . parseWithCtx ctx p
|
||||||
ls <- many1 $ try linebeginningwithspaces
|
ls <- many1 $ try linebeginningwithspaces
|
||||||
@ -427,15 +456,15 @@ ledgerposting = do
|
|||||||
newline
|
newline
|
||||||
return (Posting status account' amount comment ptype Nothing)
|
return (Posting status account' amount comment ptype Nothing)
|
||||||
|
|
||||||
-- Qualify with the parent account from parsing context
|
-- qualify with the parent account from parsing context
|
||||||
transactionaccountname :: GenParser Char LedgerFileCtx AccountName
|
transactionaccountname :: GenParser Char LedgerFileCtx AccountName
|
||||||
transactionaccountname = liftM2 (++) getParentAccount ledgeraccountname
|
transactionaccountname = liftM2 (++) getParentAccount ledgeraccountname
|
||||||
|
|
||||||
-- | Account names may have single spaces inside them, and are terminated
|
-- | Parse an account name. Account names may have single spaces inside
|
||||||
-- by two or more spaces. They should have one or more components of at
|
-- them, and are terminated by two or more spaces. They should have one or
|
||||||
-- least one character, separated by the account separator char.
|
-- more components of at least one character, separated by the account
|
||||||
|
-- separator char.
|
||||||
ledgeraccountname :: GenParser Char st String
|
ledgeraccountname :: GenParser Char st AccountName
|
||||||
ledgeraccountname = do
|
ledgeraccountname = do
|
||||||
a <- many1 (nonspace <|> singlespace)
|
a <- many1 (nonspace <|> singlespace)
|
||||||
let a' = striptrailingspace a
|
let a' = striptrailingspace a
|
||||||
@ -450,6 +479,8 @@ ledgeraccountname = do
|
|||||||
-- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace
|
-- accountnamechar = notFollowedBy (oneOf "()[]") >> nonspace
|
||||||
-- <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"
|
-- <?> "account name character (non-bracket, non-parenthesis, non-whitespace)"
|
||||||
|
|
||||||
|
-- | Parse an amount, with an optional left or right currency symbol and
|
||||||
|
-- optional price.
|
||||||
postingamount :: GenParser Char st MixedAmount
|
postingamount :: GenParser Char st MixedAmount
|
||||||
postingamount =
|
postingamount =
|
||||||
try (do
|
try (do
|
||||||
@ -512,7 +543,7 @@ priceamount =
|
|||||||
|
|
||||||
-- gawd.. trying to parse a ledger number without error:
|
-- gawd.. trying to parse a ledger number without error:
|
||||||
|
|
||||||
-- | parse a ledger-style numeric quantity and also return the number of
|
-- | Parse a ledger-style numeric quantity and also return the number of
|
||||||
-- digits to the right of the decimal point and whether thousands are
|
-- digits to the right of the decimal point and whether thousands are
|
||||||
-- separated by comma.
|
-- separated by comma.
|
||||||
amountquantity :: GenParser Char st (Double, Int, Bool)
|
amountquantity :: GenParser Char st (Double, Int, Bool)
|
||||||
@ -551,42 +582,7 @@ numberpartsstartingwithpoint = do
|
|||||||
return ("",frac)
|
return ("",frac)
|
||||||
|
|
||||||
|
|
||||||
{-| Parse a timelog entry. Here is the timelog grammar from timeclock.el 2.6:
|
-- | Parse a timelog entry.
|
||||||
|
|
||||||
@
|
|
||||||
A timelog contains data in the form of a single entry per line.
|
|
||||||
Each entry has the form:
|
|
||||||
|
|
||||||
CODE YYYY/MM/DD HH:MM:SS [COMMENT]
|
|
||||||
|
|
||||||
CODE is one of: b, h, i, o or O. COMMENT is optional when the code is
|
|
||||||
i, o or O. The meanings of the codes are:
|
|
||||||
|
|
||||||
b Set the current time balance, or \"time debt\". Useful when
|
|
||||||
archiving old log data, when a debt must be carried forward.
|
|
||||||
The COMMENT here is the number of seconds of debt.
|
|
||||||
|
|
||||||
h Set the required working time for the given day. This must
|
|
||||||
be the first entry for that day. The COMMENT in this case is
|
|
||||||
the number of hours in this workday. Floating point amounts
|
|
||||||
are allowed.
|
|
||||||
|
|
||||||
i Clock in. The COMMENT in this case should be the name of the
|
|
||||||
project worked on.
|
|
||||||
|
|
||||||
o Clock out. COMMENT is unnecessary, but can be used to provide
|
|
||||||
a description of how the period went, for example.
|
|
||||||
|
|
||||||
O Final clock out. Whatever project was being worked on, it is
|
|
||||||
now finished. Useful for creating summary reports.
|
|
||||||
@
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
i 2007/03/10 12:26:00 hledger
|
|
||||||
o 2007/03/10 17:26:02
|
|
||||||
|
|
||||||
-}
|
|
||||||
timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry
|
timelogentry :: GenParser Char LedgerFileCtx TimeLogEntry
|
||||||
timelogentry = do
|
timelogentry = do
|
||||||
code <- oneOf "bhioO"
|
code <- oneOf "bhioO"
|
||||||
@ -596,10 +592,8 @@ timelogentry = do
|
|||||||
return $ TimeLogEntry (read [code]) datetime (fromMaybe "" comment)
|
return $ TimeLogEntry (read [code]) datetime (fromMaybe "" comment)
|
||||||
|
|
||||||
|
|
||||||
-- misc parsing
|
-- | Parse a hledger display expression, which is a simple date test like
|
||||||
|
-- "d>[DATE]" or "d<=[DATE]", and return a "Posting"-matching predicate.
|
||||||
-- | Parse a --display expression which is a simple date predicate, like
|
|
||||||
-- "d>[DATE]" or "d<=[DATE]", and return a posting-matching predicate.
|
|
||||||
datedisplayexpr :: GenParser Char st (Posting -> Bool)
|
datedisplayexpr :: GenParser Char st (Posting -> Bool)
|
||||||
datedisplayexpr = do
|
datedisplayexpr = do
|
||||||
char 'd'
|
char 'd'
|
||||||
@ -660,8 +654,8 @@ tests_Parse = TestList [
|
|||||||
assertParseEqual (parseWithCtx emptyCtx postingamount " $1.")
|
assertParseEqual (parseWithCtx emptyCtx postingamount " $1.")
|
||||||
(Mixed [Amount Commodity {symbol="$",side=L,spaced=False,comma=False,precision=0} 1 Nothing])
|
(Mixed [Amount Commodity {symbol="$",side=L,spaced=False,comma=False,precision=0} 1 Nothing])
|
||||||
|
|
||||||
,"ledgerIgnoredPrice" ~: do
|
,"ledgerIgnoredPriceCommodity" ~: do
|
||||||
assertParse (parseWithCtx emptyCtx ledgerIgnoredPrice "N $\n")
|
assertParse (parseWithCtx emptyCtx ledgerIgnoredPriceCommodity "N $\n")
|
||||||
|
|
||||||
,"ledgerTagDirective" ~: do
|
,"ledgerTagDirective" ~: do
|
||||||
assertParse (parseWithCtx emptyCtx ledgerTagDirective "tag foo\n")
|
assertParse (parseWithCtx emptyCtx ledgerTagDirective "tag foo\n")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user