lib: fix bracketed posting dates, parser cleanup (#304)

Bracketed posting dates were fragile; they worked only if you wrote full
10-character dates. Also some semantics were a bit unclear. Now they
should be robust, and have been documented more clearly. This is a
legacy undocumented Ledger syntax, but it improves compatibility and
might be preferable to the more verbose "date:" tags if you write
posting dates often (as I do).

Internally, bracketed posting dates are no longer considered to be tags.
Journal comment, tag, and posting date parsers have been reworked, all
with doctests. Also the journal parser types generally have been
tightened up and clarified, making it much easier to know how to combine
and run them. There's now

-- | A parser of strings with generic user state, monad and return type.
type StringParser u m a = ParsecT String u m a

-- | A string parser with journal-parsing state.
type JournalParser m a = StringParser JournalContext m a

-- | A journal parser that runs in IO and can throw an error mid-parse.
type ErroringJournalParser a = JournalParser (ExceptT String IO) a

and corresponding convenience functions (and short aliases) for running them.
This commit is contained in:
Simon Michael 2016-04-28 13:23:20 -07:00
parent 259e7bfbe3
commit 856c0b3042
6 changed files with 442 additions and 201 deletions

View File

@ -129,7 +129,9 @@ data PostingType = RegularPosting | VirtualPosting | BalancedVirtualPosting
instance NFData PostingType
type Tag = (String, String) -- ^ A tag name and (possibly empty) value.
type TagName = String
type TagValue = String
type Tag = (TagName, TagValue) -- ^ A tag name and (possibly empty) value.
data ClearedStatus = Uncleared | Pending | Cleared
deriving (Eq,Ord,Typeable,Data,Generic)

View File

@ -1,8 +1,9 @@
--- * doc
-- lines beginning "--- *" are collapsible orgstruct nodes. Emacs users:
-- Lines beginning "--- *" are collapsible orgstruct nodes. Emacs users,
-- (add-hook 'haskell-mode-hook
-- (lambda () (set-variable 'orgstruct-heading-prefix-regexp "--- " t))
-- 'orgstruct-mode)
-- and press TAB on nodes to expand/collapse.
{-|
@ -24,10 +25,12 @@ reader should handle many ledger files as well. Example:
-- {-# OPTIONS_GHC -F -pgmF htfpp #-}
{-# LANGUAGE CPP, RecordWildCards, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts #-}
{-# LANGUAGE CPP, RecordWildCards, NoMonoLocalBinds, ScopedTypeVariables, FlexibleContexts, TupleSections #-}
module Hledger.Read.JournalReader (
--- * exports
-- * Reader
reader,
@ -71,12 +74,13 @@ import qualified Control.Exception as C
import Control.Monad.Compat
import Control.Monad.Except (ExceptT(..), liftIO, runExceptT, throwError, catchError)
import Data.Char (isNumber)
import Data.Functor.Identity
import Data.List.Compat
import Data.List.Split (wordsBy)
import Data.Maybe
import Data.Time.Calendar
import Data.Time.LocalTime
import Safe (headDef, lastDef)
import Safe
import Test.HUnit
#ifdef TESTS
import Test.Framework
@ -112,6 +116,30 @@ parse _ = parseAndFinaliseJournal journalp
--- * parsing utils
-- | A parser of strings with generic user state, monad and return type.
type StringParser u m a = ParsecT String u m a
-- | A string parser with journal-parsing state.
type JournalParser m a = StringParser JournalContext m a
-- | A journal parser that runs in IO and can throw an error mid-parse.
type ErroringJournalParser a = JournalParser (ExceptT String IO) a
-- | Run a string parser with no state in the identity monad.
runStringParser, rsp :: StringParser () Identity a -> String -> Either ParseError a
runStringParser p s = runIdentity $ runParserT p () "" s
rsp = runStringParser
-- | Run a journal parser with a null journal-parsing state.
runJournalParser, rjp :: Monad m => JournalParser m a -> String -> m (Either ParseError a)
runJournalParser p s = runParserT p nullctx "" s
rjp = runJournalParser
-- | Run an error-raising journal parser with a null journal-parsing state.
runErroringJournalParser, rejp :: ErroringJournalParser a -> String -> IO (Either String a)
runErroringJournalParser p s = runExceptT $ runJournalParser p s >>= either (throwError.show) return
rejp = runErroringJournalParser
genericSourcePos :: SourcePos -> GenericSourcePos
genericSourcePos p = GenericSourcePos (sourceName p) (sourceLine p) (sourceColumn p)
@ -177,7 +205,7 @@ combineJournalUpdates us = foldl' (flip (.)) id <$> sequence us
-- | Given a JournalUpdate-generating parsec parser, file path and data string,
-- parse and post-process a Journal so that it's ready to use, or give an error.
parseAndFinaliseJournal ::
(ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate,JournalContext))
(ErroringJournalParser (JournalUpdate,JournalContext))
-> Bool -> FilePath -> String -> ExceptT String IO Journal
parseAndFinaliseJournal parser assrt f s = do
tc <- liftIO getClockTime
@ -192,48 +220,48 @@ parseAndFinaliseJournal parser assrt f s = do
Left estr -> throwError estr
Left e -> throwError $ show e
setYear :: Stream [Char] m Char => Integer -> ParsecT [Char] JournalContext m ()
setYear :: Monad m => Integer -> JournalParser m ()
setYear y = modifyState (\ctx -> ctx{ctxYear=Just y})
getYear :: Stream [Char] m Char => ParsecT s JournalContext m (Maybe Integer)
getYear :: Monad m => JournalParser m (Maybe Integer)
getYear = liftM ctxYear getState
setDefaultCommodityAndStyle :: Stream [Char] m Char => (Commodity,AmountStyle) -> ParsecT [Char] JournalContext m ()
setDefaultCommodityAndStyle :: Monad m => (Commodity,AmountStyle) -> JournalParser m ()
setDefaultCommodityAndStyle cs = modifyState (\ctx -> ctx{ctxDefaultCommodityAndStyle=Just cs})
getDefaultCommodityAndStyle :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe (Commodity,AmountStyle))
getDefaultCommodityAndStyle :: Monad m => JournalParser m (Maybe (Commodity,AmountStyle))
getDefaultCommodityAndStyle = ctxDefaultCommodityAndStyle `fmap` getState
pushAccount :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m ()
pushAccount :: Monad m => String -> JournalParser m ()
pushAccount acct = modifyState addAccount
where addAccount ctx0 = ctx0 { ctxAccounts = acct : ctxAccounts ctx0 }
pushParentAccount :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m ()
pushParentAccount :: Monad m => String -> JournalParser m ()
pushParentAccount parent = modifyState addParentAccount
where addParentAccount ctx0 = ctx0 { ctxParentAccount = parent : ctxParentAccount ctx0 }
popParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
popParentAccount :: Monad m => JournalParser m ()
popParentAccount = do ctx0 <- getState
case ctxParentAccount ctx0 of
[] -> unexpected "End of apply account block with no beginning"
(_:rest) -> setState $ ctx0 { ctxParentAccount = rest }
getParentAccount :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
getParentAccount :: Monad m => JournalParser m String
getParentAccount = liftM (concatAccountNames . reverse . ctxParentAccount) getState
addAccountAlias :: Stream [Char] m Char => AccountAlias -> ParsecT [Char] JournalContext m ()
addAccountAlias :: Monad m => AccountAlias -> JournalParser m ()
addAccountAlias a = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=a:ctxAliases})
getAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m [AccountAlias]
getAccountAliases :: Monad m => JournalParser m [AccountAlias]
getAccountAliases = liftM ctxAliases getState
clearAccountAliases :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
clearAccountAliases :: Monad m => JournalParser m ()
clearAccountAliases = modifyState (\(ctx@Ctx{..}) -> ctx{ctxAliases=[]})
getIndex :: Stream [Char] m Char => ParsecT s JournalContext m Integer
getIndex :: Monad m => JournalParser m Integer
getIndex = liftM ctxTransactionIndex getState
setIndex :: Stream [Char] m Char => Integer -> ParsecT [Char] JournalContext m ()
setIndex :: Monad m => Integer -> JournalParser m ()
setIndex i = modifyState (\ctx -> ctx{ctxTransactionIndex=i})
--- * parsers
@ -242,7 +270,7 @@ setIndex i = modifyState (\ctx -> ctx{ctxTransactionIndex=i})
-- | Top-level journal parser. Returns a single composite, I/O performing,
-- error-raising "JournalUpdate" (and final "JournalContext") which can be
-- applied to an empty journal to get the final result.
journalp :: ParsecT [Char] JournalContext (ExceptT String IO) (JournalUpdate,JournalContext)
journalp :: ErroringJournalParser (JournalUpdate,JournalContext)
journalp = do
journalupdates <- many journalItem
eof
@ -264,7 +292,7 @@ journalp = do
--- ** directives
-- cf http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives
directivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
directivep :: ErroringJournalParser JournalUpdate
directivep = do
optional $ char '!'
choice' [
@ -283,7 +311,7 @@ directivep = do
]
<?> "directive"
includedirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
includedirectivep :: ErroringJournalParser JournalUpdate
includedirectivep = do
string "include"
many1 spacenonewline
@ -316,7 +344,7 @@ journalAddFile :: (FilePath,String) -> Journal -> Journal
journalAddFile f j@Journal{files=fs} = j{files=fs++[f]}
-- NOTE: first encountered file to left, to avoid a reverse
accountdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
accountdirectivep :: ErroringJournalParser JournalUpdate
accountdirectivep = do
string "account"
many1 spacenonewline
@ -327,7 +355,7 @@ accountdirectivep = do
pushAccount acct
return $ ExceptT $ return $ Right id
applyaccountdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
applyaccountdirectivep :: ErroringJournalParser JournalUpdate
applyaccountdirectivep = do
string "apply" >> many1 spacenonewline >> string "account"
many1 spacenonewline
@ -336,13 +364,13 @@ applyaccountdirectivep = do
pushParentAccount parent
return $ ExceptT $ return $ Right id
endapplyaccountdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
endapplyaccountdirectivep :: ErroringJournalParser JournalUpdate
endapplyaccountdirectivep = do
string "end" >> many1 spacenonewline >> string "apply" >> many1 spacenonewline >> string "account"
popParentAccount
return $ ExceptT $ return $ Right id
aliasdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
aliasdirectivep :: ErroringJournalParser JournalUpdate
aliasdirectivep = do
string "alias"
many1 spacenonewline
@ -350,10 +378,10 @@ aliasdirectivep = do
addAccountAlias alias
return $ return id
accountaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
accountaliasp :: Monad m => StringParser u m AccountAlias
accountaliasp = regexaliasp <|> basicaliasp
basicaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
basicaliasp :: Monad m => StringParser u m AccountAlias
basicaliasp = do
-- pdbg 0 "basicaliasp"
old <- rstrip <$> (many1 $ noneOf "=")
@ -362,7 +390,7 @@ basicaliasp = do
new <- rstrip <$> anyChar `manyTill` eolof -- don't require a final newline, good for cli options
return $ BasicAlias old new
regexaliasp :: Stream [Char] m Char => ParsecT [Char] st m AccountAlias
regexaliasp :: Monad m => StringParser u m AccountAlias
regexaliasp = do
-- pdbg 0 "regexaliasp"
char '/'
@ -374,13 +402,13 @@ regexaliasp = do
repl <- rstrip <$> anyChar `manyTill` eolof
return $ RegexAlias re repl
endaliasesdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
endaliasesdirectivep :: ErroringJournalParser JournalUpdate
endaliasesdirectivep = do
string "end aliases"
clearAccountAliases
return (return id)
tagdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
tagdirectivep :: ErroringJournalParser JournalUpdate
tagdirectivep = do
string "tag" <?> "tag directive"
many1 spacenonewline
@ -388,13 +416,13 @@ tagdirectivep = do
restofline
return $ return id
endtagdirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
endtagdirectivep :: ErroringJournalParser JournalUpdate
endtagdirectivep = do
(string "end tag" <|> string "pop") <?> "end tag or pop directive"
restofline
return $ return id
defaultyeardirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
defaultyeardirectivep :: ErroringJournalParser JournalUpdate
defaultyeardirectivep = do
char 'Y' <?> "default year"
many spacenonewline
@ -404,7 +432,7 @@ defaultyeardirectivep = do
setYear y'
return $ return id
defaultcommoditydirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
defaultcommoditydirectivep :: ErroringJournalParser JournalUpdate
defaultcommoditydirectivep = do
char 'D' <?> "default commodity"
many1 spacenonewline
@ -413,7 +441,7 @@ defaultcommoditydirectivep = do
restofline
return $ return id
marketpricedirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) MarketPrice
marketpricedirectivep :: ErroringJournalParser MarketPrice
marketpricedirectivep = do
char 'P' <?> "market price"
many spacenonewline
@ -425,7 +453,7 @@ marketpricedirectivep = do
restofline
return $ MarketPrice date symbol price
ignoredpricecommoditydirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
ignoredpricecommoditydirectivep :: ErroringJournalParser JournalUpdate
ignoredpricecommoditydirectivep = do
char 'N' <?> "ignored-price commodity"
many1 spacenonewline
@ -433,7 +461,7 @@ ignoredpricecommoditydirectivep = do
restofline
return $ return id
commodityconversiondirectivep :: ParsecT [Char] JournalContext (ExceptT String IO) JournalUpdate
commodityconversiondirectivep :: ErroringJournalParser JournalUpdate
commodityconversiondirectivep = do
char 'C' <?> "commodity conversion"
many1 spacenonewline
@ -447,24 +475,24 @@ commodityconversiondirectivep = do
--- ** transactions
modifiertransactionp :: ParsecT [Char] JournalContext (ExceptT String IO) ModifierTransaction
modifiertransactionp :: ErroringJournalParser ModifierTransaction
modifiertransactionp = do
char '=' <?> "modifier transaction"
many spacenonewline
valueexpr <- restofline
postings <- postingsp
postings <- postingsp Nothing
return $ ModifierTransaction valueexpr postings
periodictransactionp :: ParsecT [Char] JournalContext (ExceptT String IO) PeriodicTransaction
periodictransactionp :: ErroringJournalParser PeriodicTransaction
periodictransactionp = do
char '~' <?> "periodic transaction"
many spacenonewline
periodexpr <- restofline
postings <- postingsp
postings <- postingsp Nothing
return $ PeriodicTransaction periodexpr postings
-- | Parse a (possibly unbalanced) transaction.
transactionp :: ParsecT [Char] JournalContext (ExceptT String IO) Transaction
transactionp :: ErroringJournalParser Transaction
transactionp = do
-- ptrace "transactionp"
sourcepos <- genericSourcePos <$> getPosition
@ -475,8 +503,8 @@ transactionp = do
code <- codep <?> "transaction code"
description <- descriptionp >>= return . strip
comment <- try followingcommentp <|> (newline >> return "")
let tags = tagsInComment comment
postings <- postingsp
let tags = commentTags comment
postings <- postingsp (Just date)
i' <- (+1) <$> getIndex
setIndex i'
return $ txnTieKnot $ Transaction i' sourcepos date edate status code description comment tags postings ""
@ -574,7 +602,7 @@ test_transactionp = do
assertEqual 2 (let Right t = p in length $ tpostings t)
#endif
statusp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ClearedStatus
statusp :: Monad m => JournalParser m ClearedStatus
statusp =
choice'
[ many spacenonewline >> char '*' >> return Cleared
@ -583,7 +611,7 @@ statusp =
]
<?> "cleared status"
codep :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
codep :: Monad m => JournalParser m String
codep = try (do { many1 spacenonewline; char '(' <?> "codep"; code <- anyChar `manyTill` char ')'; return code } ) <|> return ""
descriptionp = many (noneOf ";\n")
@ -594,7 +622,7 @@ descriptionp = many (noneOf ";\n")
-- Hyphen (-) and period (.) are also allowed as separators.
-- The year may be omitted if a default year has been set.
-- Leading zeroes may be omitted.
datep :: Stream [Char] m t => ParsecT [Char] JournalContext m Day
datep :: Monad m => JournalParser m Day
datep = do
-- hacky: try to ensure precise errors for invalid dates
-- XXX reported error position is not too good
@ -624,7 +652,7 @@ datep = do
-- Seconds are optional.
-- The timezone is optional and ignored (the time is always interpreted as a local time).
-- Leading zeroes may be omitted (except in a timezone).
datetimep :: Stream [Char] m Char => ParsecT [Char] JournalContext m LocalTime
datetimep :: Monad m => JournalParser m LocalTime
datetimep = do
day <- datep
many1 spacenonewline
@ -652,7 +680,7 @@ datetimep = do
-- return $ localTimeToUTC tz' $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
return $ LocalTime day $ TimeOfDay h' m' (fromIntegral s')
secondarydatep :: Stream [Char] m Char => Day -> ParsecT [Char] JournalContext m Day
secondarydatep :: Monad m => Day -> JournalParser m Day
secondarydatep primarydate = do
char '='
-- kludgy way to use primary date for default year
@ -665,21 +693,33 @@ secondarydatep primarydate = do
edate <- withDefaultYear primarydate datep
return edate
-- |
-- >> parsewith twoorthreepartdatestringp "2016/01/2"
-- Right "2016/01/2"
-- twoorthreepartdatestringp = do
-- n1 <- many1 digit
-- c <- datesepchar
-- n2 <- many1 digit
-- mn3 <- optionMaybe $ char c >> many1 digit
-- return $ n1 ++ c:n2 ++ maybe "" (c:) mn3
--- ** postings
-- Parse the following whitespace-beginning lines as postings, posting tags, and/or comments.
postingsp :: Stream [Char] m Char => ParsecT [Char] JournalContext m [Posting]
postingsp = many (try postingp) <?> "postings"
-- Parse the following whitespace-beginning lines as postings, posting
-- tags, and/or comments (inferring year, if needed, from the given date).
postingsp :: Maybe Day -> ErroringJournalParser [Posting]
postingsp mdate = many (try $ postingp mdate) <?> "postings"
-- linebeginningwithspaces :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-- linebeginningwithspaces :: Monad m => JournalParser m String
-- linebeginningwithspaces = do
-- sp <- many1 spacenonewline
-- c <- nonspace
-- cs <- restofline
-- return $ sp ++ (c:cs) ++ "\n"
postingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m Posting
postingp = do
postingp :: Maybe Day -> ErroringJournalParser Posting
postingp mtdate = do
-- pdbg 0 "postingp"
many1 spacenonewline
status <- statusp
many spacenonewline
@ -689,23 +729,11 @@ postingp = do
massertion <- partialbalanceassertionp
_ <- fixedlotpricep
many spacenonewline
ctx <- getState
comment <- try followingcommentp <|> (newline >> return "")
let tags = tagsInComment comment
-- parse any dates specified with tags here for good parse errors
date <- case dateValueFromTags tags of
Nothing -> return Nothing
Just v -> case runParser (datep <* eof) ctx "" v of
Right d -> return $ Just d
Left err -> parserFail $ show err
date2 <- case date2ValueFromTags tags of
Nothing -> return Nothing
Just v -> case runParser (datep <* eof) ctx "" v of
Right d -> return $ Just d
Left err -> parserFail $ show err
(comment,tags,mdate,mdate2) <-
try (followingcommentandtagsp mtdate) <|> (newline >> return ("",[],Nothing,Nothing))
return posting
{ pdate=date
, pdate2=date2
{ pdate=mdate
, pdate2=mdate2
, pstatus=status
, paccount=account'
, pamount=amount
@ -718,7 +746,7 @@ postingp = do
#ifdef TESTS
test_postingp = do
let s `gives` ep = do
let parse = parseWithCtx nullctx postingp s
let parse = parseWithCtx nullctx (postingp Nothing) s
assertBool -- "postingp parser"
$ isRight parse
let Right ap = parse
@ -750,10 +778,10 @@ test_postingp = do
,pdate=parsedateM "2012/11/28"}
assertBool -- "postingp parses a quoted commodity with numbers"
(isRight $ parseWithCtx nullctx postingp " a 1 \"DE123\"\n")
(isRight $ parseWithCtx nullctx (postingp Nothing) " a 1 \"DE123\"\n")
-- ,"postingp parses balance assertions and fixed lot prices" ~: do
assertBool (isRight $ parseWithCtx nullctx postingp " a 1 \"DE123\" =$1 { =2.2 EUR} \n")
assertBool (isRight $ parseWithCtx nullctx (postingp Nothing) " a 1 \"DE123\" =$1 { =2.2 EUR} \n")
-- let parse = parseWithCtx nullctx postingp " a\n ;next-line comment\n"
-- assertRight parse
@ -765,7 +793,7 @@ test_postingp = do
--- ** account names
-- | Parse an account name, then apply any parent account prefix and/or account aliases currently in effect.
modifiedaccountnamep :: Stream [Char] m Char => ParsecT [Char] JournalContext m AccountName
modifiedaccountnamep :: Monad m => JournalParser m AccountName
modifiedaccountnamep = do
parent <- getParentAccount
aliases <- getAccountAliases
@ -781,7 +809,7 @@ modifiedaccountnamep = do
-- spaces (or end of input). Also they have one or more components of
-- at least one character, separated by the account separator char.
-- (This parser will also consume one following space, if present.)
accountnamep :: Stream [Char] m Char => ParsecT [Char] st m AccountName
accountnamep :: Monad m => StringParser u m AccountName
accountnamep = do
a <- do
c <- nonspace
@ -803,7 +831,7 @@ accountnamep = do
-- | Parse whitespace then an amount, with an optional left or right
-- currency symbol and optional price, or return the special
-- "missing" marker amount.
spaceandamountormissingp :: Stream [Char] m Char => ParsecT [Char] JournalContext m MixedAmount
spaceandamountormissingp :: Monad m => JournalParser m MixedAmount
spaceandamountormissingp =
try (do
many1 spacenonewline
@ -827,7 +855,7 @@ test_spaceandamountormissingp = do
-- | Parse a single-commodity amount, with optional symbol on the left or
-- right, optional unit or total price, and optional (ignored)
-- ledger-style balance assertion or fixed lot price declaration.
amountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
amountp :: Monad m => JournalParser m Amount
amountp = try leftsymbolamountp <|> try rightsymbolamountp <|> nosymbolamountp
#ifdef TESTS
@ -849,19 +877,19 @@ amountp' :: String -> Amount
amountp' s =
case runParser (amountp <* eof) nullctx "" s of
Right t -> t
Left err -> error' $ show err
Left err -> error' $ show err -- XXX should throwError
-- | Parse a mixed amount from a string, or get an error.
mamountp' :: String -> MixedAmount
mamountp' = Mixed . (:[]) . amountp'
signp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
signp :: Monad m => JournalParser m String
signp = do
sign <- optionMaybe $ oneOf "+-"
return $ case sign of Just '-' -> "-"
_ -> ""
leftsymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
leftsymbolamountp :: Monad m => JournalParser m Amount
leftsymbolamountp = do
sign <- signp
c <- commoditysymbolp
@ -873,7 +901,7 @@ leftsymbolamountp = do
return $ applysign $ Amount c q p s
<?> "left-symbol amount"
rightsymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
rightsymbolamountp :: Monad m => JournalParser m Amount
rightsymbolamountp = do
(q,prec,mdec,mgrps) <- numberp
sp <- many spacenonewline
@ -883,7 +911,7 @@ rightsymbolamountp = do
return $ Amount c q p s
<?> "right-symbol amount"
nosymbolamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Amount
nosymbolamountp :: Monad m => JournalParser m Amount
nosymbolamountp = do
(q,prec,mdec,mgrps) <- numberp
p <- priceamountp
@ -895,20 +923,20 @@ nosymbolamountp = do
return $ Amount c q p s
<?> "no-symbol amount"
commoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
commoditysymbolp :: Monad m => JournalParser m String
commoditysymbolp = (quotedcommoditysymbolp <|> simplecommoditysymbolp) <?> "commodity symbol"
quotedcommoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
quotedcommoditysymbolp :: Monad m => JournalParser m String
quotedcommoditysymbolp = do
char '"'
s <- many1 $ noneOf ";\n\""
char '"'
return s
simplecommoditysymbolp :: Stream [Char] m t => ParsecT [Char] JournalContext m String
simplecommoditysymbolp :: Monad m => JournalParser m String
simplecommoditysymbolp = many1 (noneOf nonsimplecommoditychars)
priceamountp :: Stream [Char] m t => ParsecT [Char] JournalContext m Price
priceamountp :: Monad m => JournalParser m Price
priceamountp =
try (do
many spacenonewline
@ -924,7 +952,7 @@ priceamountp =
return $ UnitPrice a))
<|> return NoPrice
partialbalanceassertionp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Maybe MixedAmount)
partialbalanceassertionp :: Monad m => JournalParser m (Maybe MixedAmount)
partialbalanceassertionp =
try (do
many spacenonewline
@ -934,7 +962,7 @@ partialbalanceassertionp =
return $ Just $ Mixed [a])
<|> return Nothing
-- balanceassertion :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe MixedAmount)
-- balanceassertion :: Monad m => JournalParser m (Maybe MixedAmount)
-- balanceassertion =
-- try (do
-- many spacenonewline
@ -945,7 +973,7 @@ partialbalanceassertionp =
-- <|> return Nothing
-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
fixedlotpricep :: Stream [Char] m Char => ParsecT [Char] JournalContext m (Maybe Amount)
fixedlotpricep :: Monad m => JournalParser m (Maybe Amount)
fixedlotpricep =
try (do
many spacenonewline
@ -971,7 +999,7 @@ fixedlotpricep =
-- seen following the decimal point), the decimal point character used if any,
-- and the digit group style if any.
--
numberp :: Stream [Char] m t => ParsecT [Char] JournalContext m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
numberp :: Monad m => JournalParser m (Quantity, Int, Maybe Char, Maybe DigitGroupStyle)
numberp = do
-- a number is an optional sign followed by a sequence of digits possibly
-- interspersed with periods, commas, or both
@ -1045,7 +1073,7 @@ numberp = do
--- ** comments
multilinecommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
multilinecommentp :: Monad m => JournalParser m ()
multilinecommentp = do
string "comment" >> many spacenonewline >> newline
go
@ -1054,28 +1082,83 @@ multilinecommentp = do
<|> (anyLine >> go)
anyLine = anyChar `manyTill` newline
emptyorcommentlinep :: Stream [Char] m Char => ParsecT [Char] JournalContext m ()
emptyorcommentlinep :: Monad m => JournalParser m ()
emptyorcommentlinep = do
many spacenonewline >> (commentp <|> (many spacenonewline >> newline >> return ""))
return ()
followingcommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-- | Parse a possibly multi-line comment following a semicolon.
followingcommentp :: Monad m => JournalParser m String
followingcommentp =
-- ptrace "followingcommentp"
do samelinecomment <- many spacenonewline >> (try semicoloncommentp <|> (newline >> return ""))
newlinecomments <- many (try (many1 spacenonewline >> semicoloncommentp))
return $ unlines $ samelinecomment:newlinecomments
commentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
-- | Parse a possibly multi-line comment following a semicolon, and
-- any tags and/or posting dates within it. Posting dates can be
-- expressed with "date"/"date2" tags and/or bracketed dates. The
-- dates are parsed in full here so that errors are reported in the
-- right position. Missing years can be inferred if a default date is
-- provided.
--
-- >>> rejp (followingcommentandtagsp (Just $ fromGregorian 2000 1 2)) "; a:b, date:3/4, [=5/6]"
-- Right ("a:b, date:3/4, [=5/6]\n",[("a","b"),("date","3/4")],Just 2000-03-04,Just 2000-05-06)
--
-- Year unspecified and no default provided -> unknown year error, at correct position:
-- >>> rejp (followingcommentandtagsp Nothing) " ; xxx date:3/4\n ; second line"
-- Left ...line 1, column 22...year is unknown...
--
-- Date tag value contains trailing text - forgot the comma, confused:
-- the syntaxes ? We'll accept the leading date anyway
-- >>> rejp (followingcommentandtagsp (Just $ fromGregorian 2000 1 2)) "; date:3/4=5/6"
-- Right ("date:3/4=5/6\n",[("date","3/4=5/6")],Just 2000-03-04,Nothing)
--
followingcommentandtagsp :: Maybe Day -> ErroringJournalParser (String, [Tag], Maybe Day, Maybe Day)
followingcommentandtagsp mdefdate = do
-- pdbg 0 "followingcommentandtagsp"
-- Parse a single or multi-line comment, starting on this line or the next one.
-- Save the starting position and preserve all whitespace for the subsequent re-parsing,
-- to get good error positions.
startpos <- getPosition
commentandwhitespace <- do
let semicoloncommentp' = (:) <$> char ';' <*> anyChar `manyTill` eolof
sp1 <- many spacenonewline
l1 <- try semicoloncommentp' <|> (newline >> return "")
ls <- many $ try ((++) <$> many1 spacenonewline <*> semicoloncommentp')
return $ unlines $ (sp1 ++ l1) : ls
let comment = unlines $ map (lstrip . dropWhile (==';') . strip) $ lines commentandwhitespace
-- pdbg 0 $ "commentws:"++show commentandwhitespace
-- pdbg 0 $ "comment:"++show comment
-- Reparse the comment for any tags.
tags <- case runStringParser (setPosition startpos >> tagsp) commentandwhitespace of
Right ts -> return ts
Left e -> throwError $ show e
-- pdbg 0 $ "tags: "++show tags
-- Reparse the comment for any posting dates. Use the transaction date for defaults, if provided.
epdates <- liftIO $ rejp (setPosition startpos >> postingdatesp mdefdate) commentandwhitespace
pdates <- case epdates of
Right ds -> return ds
Left e -> throwError e
-- pdbg 0 $ "pdates: "++show pdates
let mdate = headMay $ map snd $ filter ((=="date").fst) pdates
mdate2 = headMay $ map snd $ filter ((=="date2").fst) pdates
return (comment, tags, mdate, mdate2)
commentp :: Monad m => JournalParser m String
commentp = commentStartingWithp commentchars
commentchars :: [Char]
commentchars = "#;*"
semicoloncommentp :: Stream [Char] m Char => ParsecT [Char] JournalContext m String
semicoloncommentp :: Monad m => JournalParser m String
semicoloncommentp = commentStartingWithp ";"
commentStartingWithp :: Stream [Char] m Char => String -> ParsecT [Char] JournalContext m String
commentStartingWithp :: Monad m => String -> JournalParser m String
commentStartingWithp cs = do
-- ptrace "commentStartingWith"
oneOf cs
@ -1086,74 +1169,188 @@ commentStartingWithp cs = do
--- ** tags
tagsInComment :: String -> [Tag]
tagsInComment c = concatMap tagsInCommentLine $ lines c'
where
c' = ledgerDateSyntaxToTags c
-- | Extract any tags (name:value ended by comma or newline) embedded in a string.
--
-- >>> commentTags "a b:, c:c d:d, e"
-- [("b",""),("c","c d:d")]
--
-- >>> commentTags "a [1/1/1] [1/1] [1], [=1/1/1] [=1/1] [=1] [1/1=1/1/1] [1=1/1/1] b:c"
-- [("b","c")]
--
-- --[("date","1/1/1"),("date","1/1"),("date2","1/1/1"),("date2","1/1"),("date","1/1"),("date2","1/1/1"),("date","1"),("date2","1/1/1")]
--
-- >>> commentTags "\na b:, \nd:e, f"
-- [("b",""),("d","e")]
--
commentTags :: String -> [Tag]
commentTags s =
case runStringParser tagsp s of
Right r -> r
Left _ -> [] -- shouldn't happen
-- |
-- ==== __Examples__
-- >>> tagsInCommentLine ""
-- []
-- >>> tagsInCommentLine "a b"
-- []
-- >>> tagsInCommentLine "a b:, c:c d:d, e"
-- [("c","c d:d")]
tagsInCommentLine :: String -> [Tag]
tagsInCommentLine = catMaybes . map maybetag . map strip . splitAtElement ','
where
maybetag s = case runParser (tagp <* eof) nullctx "" s of
Right t -> Just t
Left _ -> Nothing
-- | Parse all tags found in a string.
tagsp :: StringParser u Identity [Tag]
tagsp = do
-- pdbg 0 $ "tagsp"
many (try (nontagp >> tagp))
-- | Parse everything up till the first tag.
--
-- >>> rsp nontagp "\na b:, \nd:e, f"
-- Right "\na "
nontagp :: StringParser u Identity String
nontagp = do
-- pdbg 0 "nontagp"
-- anyChar `manyTill` (lookAhead (try (tagorbracketeddatetagsp Nothing >> return ()) <|> eof))
anyChar `manyTill` (lookAhead (try (tagp >> return ()) <|> eof))
-- XXX costly ?
-- | Tags begin with a colon-suffixed tag name (a word beginning with
-- a letter) and are followed by a tag value (any text up to a comma
-- or newline, whitespace-stripped).
--
-- >>> rsp tagp "a:b b , c AuxDate: 4/2"
-- Right ("a","b b")
--
tagp :: Monad m => StringParser u m Tag
tagp = do
-- ptrace "tag"
-- pdbg 0 "tagp"
n <- tagnamep
v <- tagvaluep
return (n,v)
-- |
-- >>> rsp tagnamep "a:"
-- Right "a"
tagnamep :: Monad m => StringParser u m String
tagnamep = do
-- ptrace "tagname"
n <- many1 $ noneOf ": \t"
char ':'
return n
-- pdbg 0 "tagnamep"
many1 (noneOf ": \t\n") <* char ':'
tagvaluep :: Monad m => StringParser u m String
tagvaluep = do
-- ptrace "tagvalue"
v <- anyChar `manyTill` ((char ',' >> return ()) <|> eolof)
v <- anyChar `manyTill` ((try (char ',') >> return ()) <|> eolof)
return $ strip $ reverse $ dropWhile (==',') $ reverse $ strip v
ledgerDateSyntaxToTags :: String -> String
ledgerDateSyntaxToTags = regexReplaceBy "\\[[-.\\/0-9=]+\\]" replace
where
replace ('[':s) | lastDef ' ' s == ']' = replace' $ init s
replace s = s
--- ** posting dates
replace' s | isdate s = datetag s
replace' ('=':s) | isdate s = date2tag s
replace' s | last s =='=' && isdate (init s) = datetag (init s)
replace' s | length ds == 2 && isdate d1 && isdate d1 = datetag d1 ++ date2tag d2
where
ds = splitAtElement '=' s
d1 = headDef "" ds
d2 = lastDef "" ds
replace' s = s
-- | Parse all posting dates found in a string. Posting dates can be
-- expressed with date/date2 tags and/or bracketed dates. The dates
-- are parsed fully to give useful errors. Missing years can be
-- inferred only if a default date is provided.
--
postingdatesp :: Maybe Day -> ErroringJournalParser [(TagName,Day)]
postingdatesp mdefdate = do
-- pdbg 0 $ "postingdatesp"
let p = (datetagp mdefdate >>= return.(:[])) <|> bracketeddatetagsp mdefdate
nonp =
many (notFollowedBy p >> anyChar)
-- anyChar `manyTill` (lookAhead (try (p >> return ()) <|> eof))
concat <$> (many $ try (nonp >> p))
isdate = isJust . parsedateM
datetag s = "date:"++s++", "
date2tag s = "date2:"++s++", "
--- ** date tags
#ifdef TESTS
test_ledgerDateSyntaxToTags = do
assertEqual "date2:2012/11/28, " $ ledgerDateSyntaxToTags "[=2012/11/28]"
#endif
-- | Date tags are tags with name "date" or "date2". Their value is
-- parsed as a date, using the provided default date if any for
-- inferring a missing year if needed. Any error in date parsing is
-- reported and terminates parsing.
--
-- >>> rejp (datetagp Nothing) "date: 2000/1/2 "
-- Right ("date",2000-01-02)
--
-- >>> rejp (datetagp (Just $ fromGregorian 2001 2 3)) "date2:3/4"
-- Right ("date2",2001-03-04)
--
-- >>> rejp (datetagp Nothing) "date: 3/4"
-- Left ...line 1, column 9...year is unknown...
--
datetagp :: Maybe Day -> ErroringJournalParser (TagName,Day)
datetagp mdefdate = do
-- pdbg 0 "datetagp"
string "date"
n <- maybe "" id <$> optionMaybe (string "2")
char ':'
startpos <- getPosition
v <- tagvaluep
-- re-parse value as a date.
ctx <- getState
ep <- parseWithCtx
ctx{ctxYear=first3.toGregorian <$> mdefdate}
-- The value extends to a comma, newline, or end of file.
-- It seems like ignoring any extra stuff following a date
-- gives better errors here.
(do
setPosition startpos
datep) -- <* eof)
v
case ep
of Left e -> throwError $ show e
Right d -> return ("date"++n, d)
dateValueFromTags, date2ValueFromTags :: [Tag] -> Maybe String
dateValueFromTags ts = maybe Nothing (Just . snd) $ find ((=="date") . fst) ts
date2ValueFromTags ts = maybe Nothing (Just . snd) $ find ((=="date2") . fst) ts
--- ** bracketed dates
-- tagorbracketeddatetagsp :: Monad m => Maybe Day -> StringParser u m [Tag]
-- tagorbracketeddatetagsp mdefdate =
-- bracketeddatetagsp mdefdate <|> ((:[]) <$> tagp)
--- * tests
-- | Parse Ledger-style bracketed posting dates ([DATE=DATE2]), as
-- "date" and/or "date2" tags. Anything that looks like an attempt at
-- this (a square-bracketed sequence of 0123456789/-.= containing at
-- least one digit and one date separator) is also parsed, and will
-- throw an appropriate error.
--
-- The dates are parsed in full here so that errors are reported in
-- the right position. A missing year in DATE can be inferred if a
-- default date is provided. A missing year in DATE2 will be inferred
-- from DATE.
--
-- >>> rejp (bracketeddatetagsp Nothing) "[2016/1/2=3/4]"
-- Right [("date",2016-01-02),("date2",2016-03-04)]
--
-- >>> rejp (bracketeddatetagsp Nothing) "[1]"
-- Left ...not a bracketed date...
--
-- >>> rejp (bracketeddatetagsp Nothing) "[2016/1/32]"
-- Left ...line 1, column 11...bad date...
--
-- >>> rejp (bracketeddatetagsp Nothing) "[1/31]"
-- Left ...line 1, column 6...year is unknown...
--
-- >>> rejp (bracketeddatetagsp Nothing) "[0123456789/-.=/-.=]"
-- Left ...line 1, column 15...bad date, different separators...
--
bracketeddatetagsp :: Maybe Day -> ErroringJournalParser [(TagName, Day)]
bracketeddatetagsp mdefdate = do
-- pdbg 0 "bracketeddatetagsp"
char '['
startpos <- getPosition
let digits = "0123456789"
s <- many1 (oneOf $ '=':digits++datesepchars)
char ']'
unless (any (`elem` s) digits && any (`elem` datesepchars) s) $
parserFail "not a bracketed date"
-- looks sufficiently like a bracketed date, now we
-- re-parse as dates and throw any errors
ctx <- getState
ep <- parseWithCtx
ctx{ctxYear=first3.toGregorian <$> mdefdate}
(do
setPosition startpos
md1 <- optionMaybe datep
maybe (return ()) (setYear.first3.toGregorian) md1
md2 <- optionMaybe $ char '=' >> datep
eof
return (md1,md2)
)
s
case ep
of Left e -> throwError $ show e
Right (md1,md2) -> return $ catMaybes $
[maybe Nothing (Just.("date",)) md1, maybe Nothing (Just.("date2",)) md2]
--- * more tests
tests_Hledger_Read_JournalReader = TestList $ concat [
-- test_numberp

View File

@ -86,11 +86,9 @@ left blank, in which case it will be inferred.
### Simple dates
Within a journal file, transaction dates use Y/M/D (or Y-M-D or Y.M.D)
Leading zeroes are optional.
The year may be omitted, in which case it defaults to the current
year, or you can set the default year with a
[default year directive](#default-year).
Leading zeros are optional.
The year may be omitted, in which case it will be inferred from the context - the current transaction, the default year set with a
[default year directive](#default-year), or the current date when the command is run.
Some examples: `2010/01/31`, `1/31`, `2010-01-31`, `2010.1.31`.
### Secondary dates
@ -115,14 +113,12 @@ primary date if unspecified.
assets:checking
```
<div style="clear:both;"></div>
```{.shell}
```shell
$ hledger register checking
2010/02/23 movie ticket assets:checking $-10 $-10
```
<div style="clear:both;"></div>
```{.shell}
```shell
$ hledger register checking --date2
2010/02/19 movie ticket assets:checking $-10 $-10
```
@ -135,40 +131,39 @@ superseded by...
### Posting dates
You can give individual postings a different date from their parent
transaction, by adding a [posting tag](#tags) (see below) like
`date:DATE`, where DATE is a [simple date](#simple-dates). This is
probably the best way to control posting dates precisely. Eg in this
example the expense should appear in May reports, and the deduction
from checking should be reported on 6/1 for easy bank reconciliation:
transaction, by adding a [posting comment](#comments) containing a
[tag](#tags) (see below) like `date:DATE`. This is probably the best
way to control posting dates precisely. Eg in this example the expense
should appear in May reports, and the deduction from checking should
be reported on 6/1 for easy bank reconciliation:
``` {.journal}
```journal
2015/5/30
expenses:food $10 ; food purchased on saturday 5/30
assets:checking ; bank cleared it on monday, date:6/1
```
<div style="clear:both;"></div>
```{.shell}
$ hledger -f tt.j register food
```shell
$ hledger -f t.j register food
2015/05/30 expenses:food $10 $10
```
<div style="clear:both;"></div>
```{.shell}
$ hledger -f tt.j register checking
```shell
$ hledger -f t.j register checking
2015/06/01 assets:checking $-10 $-10
```
A posting date will use the year of the transaction date if unspecified.
DATE should be a [simple date](#simple-dates); if the year is not
specified it will use the year of the transaction's date. You can set
the secondary date similarly, with `date2:DATE2`. The `date:` or
`date2:` tags must have a valid simple date value if they are present,
eg a `date:` tag with no value is not allowed.
You can also set the secondary date, with `date2:DATE2`.
For compatibility, Ledger's older posting date syntax is also
supported: `[DATE]`, `[DATE=DATE2]` or `[=DATE2]` in a posting
comment.
When using any of these forms, be sure to provide a valid simple date
or you'll get a parse error. Eg a `date:` tag with no value is not
allowed.
Ledger's earlier, more compact bracketed date syntax is also
supported: `[DATE]`, `[DATE=DATE2]` or `[=DATE2]`. hledger will
attempt to parse any square-bracketed sequence of the `0123456789/-.=`
characters in this way. With this syntax, DATE infers its year from
the transaction and DATE2 infers its year from DATE.
## Account names
@ -249,7 +244,7 @@ These look like `=EXPECTEDBALANCE` following a posting's amount. Eg in
this example we assert the expected dollar balance in accounts a and b after
each posting:
``` {.journal}
```journal
2013/1/1
a $1 =$1
b =$-1
@ -308,7 +303,7 @@ for this kind of total balance assertion if there's demand.)
Balance assertions do not count the balance from subaccounts; they check
the posted account's exclusive balance. For example:
``` {.journal}
```journal
1/1
checking:fund 1 = 1 ; post to this subaccount, its balance is now 1
checking 1 = 1 ; post to the parent account, its exclusive balance is now 1
@ -472,7 +467,7 @@ while tags in a posting comment affect only that posting.
For example, the following transaction has three tags (A, TAG2, third-tag)
and the posting has four (A, TAG2, third-tag, posting-tag):
``` {.journal}
```journal
1/1 a transaction ; A:, TAG2:
; third-tag: a third transaction tag, this time with a value
(a) $1 ; posting-tag:
@ -480,7 +475,7 @@ and the posting has four (A, TAG2, third-tag, posting-tag):
Tags are like Ledger's
[metadata](http://ledger-cli.org/3.0/doc/ledger3.html#Metadata)
feature, except hledger's tag values are always simple strings.
feature, except hledger's tag values are simple strings.
## Directives
@ -503,7 +498,7 @@ This affects all subsequent journal entries in the current file or its
[included files](#including-other-files).
The spaces around the = are optional:
``` {.journal}
```journal
alias OLD = NEW
```
@ -514,7 +509,7 @@ OLD and NEW are full account names.
hledger will replace any occurrence of the old account name with the
new one. Subaccounts are also affected. Eg:
``` {.journal}
```journal
alias checking = assets:bank:wells fargo:checking
# rewrites "checking" to "assets:bank:wells fargo:checking", or "checking:a" to "assets:bank:wells fargo:checking:a"
```
@ -524,7 +519,7 @@ alias checking = assets:bank:wells fargo:checking
There is also a more powerful variant that uses a regular expression,
indicated by the forward slashes. (This was the default behaviour in hledger 0.24-0.25):
``` {.journal}
```journal
alias /REGEX/ = REPLACEMENT
```
@ -540,7 +535,7 @@ Note, currently regular expression aliases may cause noticeable slow-downs.
(And if you use Ledger on your hledger file, they will be ignored.)
Eg:
``` {.journal}
```journal
alias /^(.+):bank:([^:]+)(.*)/ = \1:\2 \3
# rewrites "assets:bank:wells fargo:checking" to "assets:wells fargo checking"
```
@ -559,7 +554,7 @@ Aliases are applied in the following order:
You can clear (forget) all currently defined aliases with the `end aliases` directive:
``` {.journal}
```journal
end aliases
```
@ -568,7 +563,7 @@ end aliases
The `account` directive predefines account names, as in Ledger and Beancount.
This may be useful for your own documentation; hledger doesn't make use of it yet.
``` {.journal}
```journal
; account ACCT
; OPTIONAL COMMENTS/TAGS...
@ -587,7 +582,7 @@ You can specify a parent account which will be prepended to all accounts
within a section of the journal. Use the `apply account` and `end apply account`
directives like so:
``` {.journal}
```journal
apply account home
2010/1/1
@ -597,7 +592,7 @@ apply account home
end apply account
```
which is equivalent to:
``` {.journal}
```journal
2010/01/01
home:food $10
home:cash $-10
@ -606,7 +601,7 @@ which is equivalent to:
If `end apply account` is omitted, the effect lasts to the end of the file.
Included files are also affected, eg:
``` {.journal}
```journal
apply account business
include biz.journal
end apply account
@ -644,7 +639,7 @@ D £1,000.00
c £1000
d
```
```{.shell}
```shell
$ hledger print
2010/01/01
a £2,340.00
@ -660,7 +655,7 @@ $ hledger print
You can set a default year to be used for subsequent dates which don't
specify a year. This is a line beginning with `Y` followed by the year. Eg:
``` {.journal}
```journal
Y2009 ; set default year to 2009
12/15 ; equivalent to 2009/12/15
@ -683,7 +678,7 @@ Y2010 ; change default year to 2010
You can pull in the content of additional journal files by writing an
include directive, like this:
``` {.journal}
```journal
include path/to/file.journal
```

View File

@ -35,17 +35,10 @@ hledger -f- print
b
>>>2 /bad date/
>>>= 1
# 5. dates should be followed by whitespace or newline
# 5. dates must be followed by whitespace or newline
hledger -f- print
<<<
2015/9/6:
a 0
>>>2 /unexpected ":"/
>>>= 1
# 6.
hledger -f- print
<<<
2015/9/6=9/6* x
2015/9/6*
a 0
>>>2 /unexpected "*"/
>>>= 1

View File

@ -0,0 +1,54 @@
# 1. posting dates can be set with a tag. Also the year can be
# inferred from the transaction. If there are multiple tags, the first
# is used. Date separators /-. are allowed.
hledger -f- register
<<<
2000/1/2
a 0 ; date: 3/4, date: 4-5, date:6.7
>>> /^2000\/03\/04/
>>>=0
# 2. If the date: or date2: tags do not have a valid simple date
# value, there should be a corresponding error at the right position
hledger -f- register
<<<
comment
Journal comment to prevent this being parsed as a timedot file
end comment
2000/1/1
a 0 ; date: 3.31
2000/1/2
b 0
; date: 3.32
>>>2 /line 10, column 19/
>>>=1
# 3. Ledger's bracketed date syntax is also supported: `[DATE]`,
# `[DATE=DATE2]` or `[=DATE2]`. This is equivalent to using `date:` or
# `date2:` tags.
hledger -f- register --date2
<<<
2000/1/2
a 0 ; [=3-4]
>>> /^2000\/03\/04/
>>>=0
# 4. Date parsing and error reporting activates for square brackets
# containing only `0123456789/-.=` characters.
hledger -f- register
<<<
comment
Journal comment to prevent this being parsed as a timedot file
end comment
2000/1/2
a 0 ; [3/4 ] space, causes this to be ignored
2000/1/2
b 0 ; [1/1=1/2/3/4] bad second date, should error
>>>2 /line 9, column 25/
>>>=1

View File

@ -16,7 +16,7 @@ hledger -f - print
; txntag2: txn val 2
a 1
; posting1tag1: posting 1 val 1
; posting1tag2:
; posting1tag2:
b -1 ; posting-2-tag-1: posting 2 val 1
; posting-2-tag-2: