From 856c0b304279e1804ddecf3b4c4e5b03dfee268e Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Thu, 28 Apr 2016 13:23:20 -0700 Subject: [PATCH] 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. --- hledger-lib/Hledger/Data/Types.hs | 4 +- hledger-lib/Hledger/Read/JournalReader.hs | 485 +++++++++++++++------- hledger-lib/doc/hledger_journal.5.m4.md | 87 ++-- tests/journal/dates.test | 11 +- tests/journal/posting-dates.test | 54 +++ tests/journal/tags.test | 2 +- 6 files changed, 442 insertions(+), 201 deletions(-) create mode 100644 tests/journal/posting-dates.test diff --git a/hledger-lib/Hledger/Data/Types.hs b/hledger-lib/Hledger/Data/Types.hs index 830ae903c..e1955a5c8 100644 --- a/hledger-lib/Hledger/Data/Types.hs +++ b/hledger-lib/Hledger/Data/Types.hs @@ -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) diff --git a/hledger-lib/Hledger/Read/JournalReader.hs b/hledger-lib/Hledger/Read/JournalReader.hs index 887397e0a..91c295d9f 100644 --- a/hledger-lib/Hledger/Read/JournalReader.hs +++ b/hledger-lib/Hledger/Read/JournalReader.hs @@ -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 diff --git a/hledger-lib/doc/hledger_journal.5.m4.md b/hledger-lib/doc/hledger_journal.5.m4.md index a5ba7bb4e..7c0e03c99 100644 --- a/hledger-lib/doc/hledger_journal.5.m4.md +++ b/hledger-lib/doc/hledger_journal.5.m4.md @@ -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 ``` -
-```{.shell} +```shell $ hledger register checking 2010/02/23 movie ticket assets:checking $-10 $-10 ``` -
-```{.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 ``` -
-```{.shell} -$ hledger -f tt.j register food +```shell +$ hledger -f t.j register food 2015/05/30 expenses:food $10 $10 ``` -
-```{.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 ``` diff --git a/tests/journal/dates.test b/tests/journal/dates.test index d6b06b97f..679ba6511 100644 --- a/tests/journal/dates.test +++ b/tests/journal/dates.test @@ -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 diff --git a/tests/journal/posting-dates.test b/tests/journal/posting-dates.test new file mode 100644 index 000000000..d8376eff7 --- /dev/null +++ b/tests/journal/posting-dates.test @@ -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 diff --git a/tests/journal/tags.test b/tests/journal/tags.test index a28cd8ff2..b616da23a 100644 --- a/tests/journal/tags.test +++ b/tests/journal/tags.test @@ -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: