summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES54
-rw-r--r--Hledger/Data/Amount.hs71
-rw-r--r--Hledger/Data/Dates.hs24
-rw-r--r--Hledger/Data/Journal.hs258
-rw-r--r--Hledger/Data/Posting.hs14
-rw-r--r--Hledger/Data/Transaction.hs239
-rw-r--r--Hledger/Data/TransactionModifier.hs74
-rw-r--r--Hledger/Data/Types.hs69
-rw-r--r--Hledger/Read/Common.hs247
-rw-r--r--Hledger/Read/CsvReader.hs31
-rw-r--r--Hledger/Read/JournalReader.hs274
-rw-r--r--Hledger/Read/TimeclockReader.hs5
-rw-r--r--Hledger/Read/TimedotReader.hs4
-rw-r--r--Hledger/Reports/BudgetReport.hs1
-rw-r--r--Hledger/Reports/MultiBalanceReports.hs42
-rw-r--r--Hledger/Reports/PostingsReport.hs6
-rw-r--r--Hledger/Reports/ReportOptions.hs12
-rw-r--r--Hledger/Utils.hs17
-rw-r--r--Hledger/Utils/Debug.hs11
-rw-r--r--Hledger/Utils/Parse.hs36
-rw-r--r--Hledger/Utils/Test.hs86
-rw-r--r--Text/Megaparsec/Custom.hs512
-rw-r--r--hledger-lib.cabal35
-rw-r--r--hledger_csv.52
-rw-r--r--hledger_csv.info60
-rw-r--r--hledger_csv.txt2
-rw-r--r--hledger_journal.5301
-rw-r--r--hledger_journal.info490
-rw-r--r--hledger_journal.txt417
-rw-r--r--hledger_timeclock.52
-rw-r--r--hledger_timeclock.info4
-rw-r--r--hledger_timeclock.txt2
-rw-r--r--hledger_timedot.52
-rw-r--r--hledger_timedot.info8
-rw-r--r--hledger_timedot.txt2
35 files changed, 2313 insertions, 1101 deletions
diff --git a/CHANGES b/CHANGES
index 4f50096..7eb0729 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,5 +1,54 @@
-API-ish changes in the hledger-lib package.
-Most user-visible changes are noted in the hledger changelog, instead.
+Developer-ish changes in the hledger-lib package.
+User-visible changes are noted in the hledger package changelog instead.
+
+
+# 1.12 (2018/12/02)
+
+* switch to megaparsec 7 (Alex Chen)
+ We now track the stack of include files in Journal ourselves, since
+ megaparsec dropped this feature.
+
+* add 'ExceptT' layer to our parser monad again (Alex Chen)
+ We previously had a parser type, 'type ErroringJournalParser = ExceptT
+ String ...' for throwing parse errors without allowing further
+ backtracking. This parser type was removed under the assumption that it
+ would be possible to write our parser without this capability. However,
+ after a hairy backtracking bug, we would now prefer to have the option to
+ prevent backtracking.
+
+ - Define a 'FinalParseError' type specifically for the 'ExceptT' layer
+ - Any parse error can be raised as a "final" parse error
+ - Tracks the stack of include files for parser errors, anticipating the
+ removal of the tracking of stacks of include files in megaparsec 7
+ - Although a stack of include files is also tracked in the 'StateT
+ Journal' layer of the parser, it seems easier to guarantee correct
+ error messages in the 'ExceptT FinalParserError' layer
+ - This does not make the 'StateT Journal' stack redundant because the
+ 'ExceptT FinalParseError' stack cannot be used to detect cycles of
+ include files
+
+* more support for location-aware parse errors when re-parsing (Alex Chen)
+
+* make 'includedirectivep' an 'ErroringJournalParser' (Alex Chen)
+
+* drop Ord instance breaking GHC 8.6 build (Peter Simons)
+
+* flip the arguments of (divide|multiply)[Mixed]Amount
+
+* showTransaction: fix a case showing multiple missing amounts
+ showTransaction could sometimes hide the last posting's amount even if
+ one of the other posting amounts was already implcit, producing invalid
+ transaction output.
+
+* plog, plogAt: add missing newline
+
+* split up journalFinalise, reorder journal finalisation steps (#893) (Jesse Rosenthal)
+ The `journalFinalise` function has been split up, allowing more granular
+ control.
+
+* journalSetTime --> journalSetLastReadTime
+
+* journalSetFilePath has been removed, use journalAddFile instead
# 1.11.1 (2018/10/06)
@@ -7,7 +56,6 @@ Most user-visible changes are noted in the hledger changelog, instead.
* add, lib: fix wrong transaction rendering in balance assertion errors
and when using the add command
-
# 1.11 (2018/9/30)
* compilation now works when locale is unset (#849)
diff --git a/Hledger/Data/Amount.hs b/Hledger/Data/Amount.hs
index 2817373..a64876a 100644
--- a/Hledger/Data/Amount.hs
+++ b/Hledger/Data/Amount.hs
@@ -59,7 +59,10 @@ module Hledger.Data.Amount (
costOfAmount,
divideAmount,
multiplyAmount,
+ divideAmountAndPrice,
+ multiplyAmountAndPrice,
amountValue,
+ amountTotalPriceToUnitPrice,
-- ** rendering
amountstyle,
styleAmount,
@@ -90,6 +93,8 @@ module Hledger.Data.Amount (
costOfMixedAmount,
divideMixedAmount,
multiplyMixedAmount,
+ divideMixedAmountAndPrice,
+ multiplyMixedAmountAndPrice,
averageMixedAmounts,
isNegativeAmount,
isNegativeMixedAmount,
@@ -99,6 +104,7 @@ module Hledger.Data.Amount (
isReallyZeroMixedAmount,
isReallyZeroMixedAmountCost,
mixedAmountValue,
+ mixedAmountTotalPriceToUnitPrice,
-- ** rendering
styleMixedAmount,
showMixedAmount,
@@ -209,13 +215,40 @@ costOfAmount a@Amount{aquantity=q, aprice=price} =
UnitPrice p@Amount{aquantity=pq} -> p{aquantity=pq * q}
TotalPrice p@Amount{aquantity=pq} -> p{aquantity=pq * signum q}
+-- | Replace an amount's TotalPrice, if it has one, with an equivalent UnitPrice.
+-- Has no effect on amounts without one.
+-- Also increases the unit price's display precision to show one extra decimal place,
+-- to help keep transaction amounts balancing.
+-- Does Decimal division, might be some rounding/irrational number issues.
+amountTotalPriceToUnitPrice :: Amount -> Amount
+amountTotalPriceToUnitPrice
+ a@Amount{aquantity=q, aprice=TotalPrice pa@Amount{aquantity=pq, astyle=ps@AmountStyle{asprecision=pp}}}
+ = a{aprice = UnitPrice pa{aquantity=abs (pq/q), astyle=ps{asprecision=pp+1}}}
+amountTotalPriceToUnitPrice a = a
+
-- | Divide an amount's quantity by a constant.
-divideAmount :: Amount -> Quantity -> Amount
-divideAmount a@Amount{aquantity=q} d = a{aquantity=q/d}
+divideAmount :: Quantity -> Amount -> Amount
+divideAmount n a@Amount{aquantity=q} = a{aquantity=q/n}
-- | Multiply an amount's quantity by a constant.
-multiplyAmount :: Amount -> Quantity -> Amount
-multiplyAmount a@Amount{aquantity=q} d = a{aquantity=q*d}
+multiplyAmount :: Quantity -> Amount -> Amount
+multiplyAmount n a@Amount{aquantity=q} = a{aquantity=q*n}
+
+-- | Divide an amount's quantity (and its total price, if it has one) by a constant.
+-- The total price will be kept positive regardless of the multiplier's sign.
+divideAmountAndPrice :: Quantity -> Amount -> Amount
+divideAmountAndPrice n a@Amount{aquantity=q,aprice=p} = a{aquantity=q/n, aprice=f p}
+ where
+ f (TotalPrice a) = TotalPrice $ abs $ n `divideAmount` a
+ f p = p
+
+-- | Multiply an amount's quantity (and its total price, if it has one) by a constant.
+-- The total price will be kept positive regardless of the multiplier's sign.
+multiplyAmountAndPrice :: Quantity -> Amount -> Amount
+multiplyAmountAndPrice n a@Amount{aquantity=q,aprice=p} = a{aquantity=q*n, aprice=f p}
+ where
+ f (TotalPrice a) = TotalPrice $ abs $ n `multiplyAmount` a
+ f p = p
-- | Is this amount negative ? The price is ignored.
isNegativeAmount :: Amount -> Bool
@@ -523,23 +556,37 @@ filterMixedAmountByCommodity c (Mixed as) = Mixed as'
[] -> [nullamt{acommodity=c}]
as'' -> [sum as'']
+-- | Apply a transform to a mixed amount's component 'Amount's.
+mapMixedAmount :: (Amount -> Amount) -> MixedAmount -> MixedAmount
+mapMixedAmount f (Mixed as) = Mixed $ map f as
+
-- | Convert a mixed amount's component amounts to the commodity of their
-- assigned price, if any.
costOfMixedAmount :: MixedAmount -> MixedAmount
costOfMixedAmount (Mixed as) = Mixed $ map costOfAmount as
-- | Divide a mixed amount's quantities by a constant.
-divideMixedAmount :: MixedAmount -> Quantity -> MixedAmount
-divideMixedAmount (Mixed as) d = Mixed $ map (`divideAmount` d) as
+divideMixedAmount :: Quantity -> MixedAmount -> MixedAmount
+divideMixedAmount n = mapMixedAmount (divideAmount n)
-- | Multiply a mixed amount's quantities by a constant.
-multiplyMixedAmount :: MixedAmount -> Quantity -> MixedAmount
-multiplyMixedAmount (Mixed as) d = Mixed $ map (`multiplyAmount` d) as
+multiplyMixedAmount :: Quantity -> MixedAmount -> MixedAmount
+multiplyMixedAmount n = mapMixedAmount (multiplyAmount n)
+
+-- | Divide a mixed amount's quantities (and total prices, if any) by a constant.
+-- The total prices will be kept positive regardless of the multiplier's sign.
+divideMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount
+divideMixedAmountAndPrice n = mapMixedAmount (divideAmountAndPrice n)
+
+-- | Multiply a mixed amount's quantities (and total prices, if any) by a constant.
+-- The total prices will be kept positive regardless of the multiplier's sign.
+multiplyMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount
+multiplyMixedAmountAndPrice n = mapMixedAmount (multiplyAmountAndPrice n)
-- | Calculate the average of some mixed amounts.
averageMixedAmounts :: [MixedAmount] -> MixedAmount
averageMixedAmounts [] = 0
-averageMixedAmounts as = sum as `divideMixedAmount` fromIntegral (length as)
+averageMixedAmounts as = fromIntegral (length as) `divideMixedAmount` sum as
-- | Is this mixed amount negative, if it can be normalised to a single commodity ?
isNegativeMixedAmount :: MixedAmount -> Maybe Bool
@@ -665,6 +712,12 @@ canonicaliseMixedAmount styles (Mixed as) = Mixed $ map (canonicaliseAmount styl
mixedAmountValue :: Journal -> Day -> MixedAmount -> MixedAmount
mixedAmountValue j d (Mixed as) = Mixed $ map (amountValue j d) as
+-- | Replace each component amount's TotalPrice, if it has one, with an equivalent UnitPrice.
+-- Has no effect on amounts without one.
+-- Does Decimal division, might be some rounding/irrational number issues.
+mixedAmountTotalPriceToUnitPrice :: MixedAmount -> MixedAmount
+mixedAmountTotalPriceToUnitPrice (Mixed as) = Mixed $ map amountTotalPriceToUnitPrice as
+
-------------------------------------------------------------------------------
-- tests
diff --git a/Hledger/Data/Dates.hs b/Hledger/Data/Dates.hs
index 0e53343..ef31f4c 100644
--- a/Hledger/Data/Dates.hs
+++ b/Hledger/Data/Dates.hs
@@ -48,6 +48,7 @@ module Hledger.Data.Dates (
parsePeriodExpr,
parsePeriodExpr',
nulldatespan,
+ emptydatespan,
failIfInvalidYear,
failIfInvalidMonth,
failIfInvalidDay,
@@ -77,6 +78,7 @@ where
import Prelude ()
import "base-compat-batteries" Prelude.Compat
+import Control.Applicative.Permutations
import Control.Monad
import "base-compat-batteries" Data.List.Compat
import Data.Default
@@ -96,7 +98,7 @@ import Data.Time.LocalTime
import Safe (headMay, lastMay, readMay)
import Text.Megaparsec
import Text.Megaparsec.Char
-import Text.Megaparsec.Perm
+import Text.Megaparsec.Custom
import Text.Printf
import Hledger.Data.Types
@@ -314,13 +316,14 @@ earliest (Just d1) (Just d2) = Just $ min d1 d2
-- | Parse a period expression to an Interval and overall DateSpan using
-- the provided reference date, or return a parse error.
-parsePeriodExpr :: Day -> Text -> Either (ParseError Char CustomErr) (Interval, DateSpan)
+parsePeriodExpr
+ :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) (Interval, DateSpan)
parsePeriodExpr refdate s = parsewith (periodexprp refdate <* eof) (T.toLower s)
-- | Like parsePeriodExpr, but call error' on failure.
parsePeriodExpr' :: Day -> Text -> (Interval, DateSpan)
parsePeriodExpr' refdate s =
- either (error' . ("failed to parse:" ++) . parseErrorPretty) id $
+ either (error' . ("failed to parse:" ++) . customErrorBundlePretty) id $
parsePeriodExpr refdate s
maybePeriod :: Day -> Text -> Maybe (Interval,DateSpan)
@@ -380,13 +383,14 @@ fixSmartDateStr :: Day -> Text -> String
fixSmartDateStr d s = either
(\e->error' $ printf "could not parse date %s %s" (show s) (show e))
id
- $ (fixSmartDateStrEither d s :: Either (ParseError Char CustomErr) String)
+ $ (fixSmartDateStrEither d s :: Either (ParseErrorBundle Text CustomErr) String)
-- | A safe version of fixSmartDateStr.
-fixSmartDateStrEither :: Day -> Text -> Either (ParseError Char CustomErr) String
+fixSmartDateStrEither :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) String
fixSmartDateStrEither d = either Left (Right . showDate) . fixSmartDateStrEither' d
-fixSmartDateStrEither' :: Day -> Text -> Either (ParseError Char CustomErr) Day
+fixSmartDateStrEither'
+ :: Day -> Text -> Either (ParseErrorBundle Text CustomErr) Day
fixSmartDateStrEither' d s = case parsewith smartdateonly (T.toLower s) of
Right sd -> Right $ fixSmartDate d sd
Left e -> Left e
@@ -987,7 +991,9 @@ reportingintervalp = choice' [
return $ DayOfMonth n,
do string' "every"
let mnth = choice' [month, mon] >>= \(_,m,_) -> return (read m)
- d_o_y <- makePermParser $ DayOfYear <$$> try (skipMany spacenonewline *> mnth) <||> try (skipMany spacenonewline *> nth)
+ d_o_y <- runPermutation $
+ DayOfYear <$> toPermutation (try (skipMany spacenonewline *> mnth))
+ <*> toPermutation (try (skipMany spacenonewline *> nth))
optOf_ "year"
return d_o_y,
do string' "every"
@@ -1092,5 +1098,9 @@ mkdatespan b = DateSpan (Just $ parsedate b) . Just . parsedate
nulldatespan :: DateSpan
nulldatespan = DateSpan Nothing Nothing
+-- | A datespan of zero length, that matches no date.
+emptydatespan :: DateSpan
+emptydatespan = DateSpan (Just $ addDays 1 nulldate) (Just nulldate)
+
nulldate :: Day
nulldate = fromGregorian 0 1 1
diff --git a/Hledger/Data/Journal.hs b/Hledger/Data/Journal.hs
index 9969d28..e6b776f 100644
--- a/Hledger/Data/Journal.hs
+++ b/Hledger/Data/Journal.hs
@@ -21,7 +21,8 @@ module Hledger.Data.Journal (
commodityStylesFromAmounts,
journalCommodityStyles,
journalConvertAmountsToCost,
- journalFinalise,
+ journalReverse,
+ journalSetLastReadTime,
journalPivot,
-- * Filtering
filterJournalTransactions,
@@ -53,7 +54,7 @@ module Hledger.Data.Journal (
-- * Standard account types
journalBalanceSheetAccountQuery,
journalProfitAndLossAccountQuery,
- journalIncomeAccountQuery,
+ journalRevenueAccountQuery,
journalExpenseAccountQuery,
journalAssetAccountQuery,
journalLiabilityAccountQuery,
@@ -96,6 +97,7 @@ import Data.Tree
import System.Time (ClockTime(TOD))
import Text.Printf
import qualified Data.Map as M
+import qualified Data.Set as S
import Hledger.Utils
import Hledger.Data.Types
@@ -162,15 +164,17 @@ instance Sem.Semigroup Journal where
,jparseparentaccounts = jparseparentaccounts j2
,jparsealiases = jparsealiases j2
-- ,jparsetransactioncount = jparsetransactioncount j1 + jparsetransactioncount j2
- ,jparsetimeclockentries = jparsetimeclockentries j1 <> jparsetimeclockentries j2
- ,jdeclaredaccounts = jdeclaredaccounts j1 <> jdeclaredaccounts j2
+ ,jparsetimeclockentries = jparsetimeclockentries j1 <> jparsetimeclockentries j2
+ ,jincludefilestack = jincludefilestack j2
+ ,jdeclaredaccounts = jdeclaredaccounts j1 <> jdeclaredaccounts j2
+ ,jdeclaredaccounttypes = jdeclaredaccounttypes j1 <> jdeclaredaccounttypes j2
,jcommodities = jcommodities j1 <> jcommodities j2
,jinferredcommodities = jinferredcommodities j1 <> jinferredcommodities j2
,jmarketprices = jmarketprices j1 <> jmarketprices j2
,jtxnmodifiers = jtxnmodifiers j1 <> jtxnmodifiers j2
,jperiodictxns = jperiodictxns j1 <> jperiodictxns j2
,jtxns = jtxns j1 <> jtxns j2
- ,jfinalcommentlines = jfinalcommentlines j2
+ ,jfinalcommentlines = jfinalcommentlines j2 -- XXX discards j1's ?
,jfiles = jfiles j1 <> jfiles j2
,jlastreadtime = max (jlastreadtime j1) (jlastreadtime j2)
}
@@ -189,10 +193,12 @@ nulljournal = Journal {
,jparseparentaccounts = []
,jparsealiases = []
-- ,jparsetransactioncount = 0
- ,jparsetimeclockentries = []
- ,jdeclaredaccounts = []
- ,jcommodities = M.fromList []
- ,jinferredcommodities = M.fromList []
+ ,jparsetimeclockentries = []
+ ,jincludefilestack = []
+ ,jdeclaredaccounts = []
+ ,jdeclaredaccounttypes = M.empty
+ ,jcommodities = M.empty
+ ,jinferredcommodities = M.empty
,jmarketprices = []
,jtxnmodifiers = []
,jperiodictxns = []
@@ -275,24 +281,65 @@ journalAccountNames = journalAccountNamesDeclaredOrImplied
journalAccountNameTree :: Journal -> Tree AccountName
journalAccountNameTree = accountNameTreeFrom . journalAccountNames
--- standard account types
-
--- | A query for Profit & Loss accounts in this journal.
--- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Profit_.26_Loss_accounts>.
-journalProfitAndLossAccountQuery :: Journal -> Query
-journalProfitAndLossAccountQuery j = Or [journalIncomeAccountQuery j
- ,journalExpenseAccountQuery j
- ]
-
--- | A query for Income (Revenue) accounts in this journal.
--- This is currently hard-coded to the case-insensitive regex @^(income|revenue)s?(:|$)@.
-journalIncomeAccountQuery :: Journal -> Query
-journalIncomeAccountQuery _ = Acct "^(income|revenue)s?(:|$)"
+-- queries for standard account types
+
+-- | Get a query for accounts of a certain type (Asset, Liability..) in this journal.
+-- The query will match all accounts which were declared as that type by account directives,
+-- plus all their subaccounts which have not been declared as a different type.
+-- If no accounts were declared as this type, the query will instead match accounts
+-- with names matched by the provided case-insensitive regular expression.
+journalAccountTypeQuery :: AccountType -> Regexp -> Journal -> Query
+journalAccountTypeQuery atype fallbackregex j =
+ case M.lookup atype (jdeclaredaccounttypes j) of
+ Nothing -> Acct fallbackregex
+ Just as ->
+ -- XXX Query isn't able to match account type since that requires extra info from the journal.
+ -- So we do a hacky search by name instead.
+ And [
+ Or $ map (Acct . accountNameToAccountRegex) as
+ ,Not $ Or $ map (Acct . accountNameToAccountRegex) differentlytypedsubs
+ ]
+ where
+ differentlytypedsubs = concat
+ [subs | (t,bs) <- M.toList (jdeclaredaccounttypes j)
+ , t /= atype
+ , let subs = [b | b <- bs, any (`isAccountNamePrefixOf` b) as]
+ ]
--- | A query for Expense accounts in this journal.
--- This is currently hard-coded to the case-insensitive regex @^expenses?(:|$)@.
+-- | A query for accounts in this journal which have been
+-- declared as Asset by account directives, or otherwise for
+-- accounts with names matched by the case-insensitive regular expression
+-- @^assets?(:|$)@.
+journalAssetAccountQuery :: Journal -> Query
+journalAssetAccountQuery = journalAccountTypeQuery Asset "^assets?(:|$)"
+
+-- | A query for accounts in this journal which have been
+-- declared as Liability by account directives, or otherwise for
+-- accounts with names matched by the case-insensitive regular expression
+-- @^(debts?|liabilit(y|ies))(:|$)@.
+journalLiabilityAccountQuery :: Journal -> Query
+journalLiabilityAccountQuery = journalAccountTypeQuery Liability "^(debts?|liabilit(y|ies))(:|$)"
+
+-- | A query for accounts in this journal which have been
+-- declared as Equity by account directives, or otherwise for
+-- accounts with names matched by the case-insensitive regular expression
+-- @^equity(:|$)@.
+journalEquityAccountQuery :: Journal -> Query
+journalEquityAccountQuery = journalAccountTypeQuery Equity "^equity(:|$)"
+
+-- | A query for accounts in this journal which have been
+-- declared as Revenue by account directives, or otherwise for
+-- accounts with names matched by the case-insensitive regular expression
+-- @^(income|revenue)s?(:|$)@.
+journalRevenueAccountQuery :: Journal -> Query
+journalRevenueAccountQuery = journalAccountTypeQuery Revenue "^(income|revenue)s?(:|$)"
+
+-- | A query for accounts in this journal which have been
+-- declared as Expense by account directives, or otherwise for
+-- accounts with names matched by the case-insensitive regular expression
+-- @^(income|revenue)s?(:|$)@.
journalExpenseAccountQuery :: Journal -> Query
-journalExpenseAccountQuery _ = Acct "^expenses?(:|$)"
+journalExpenseAccountQuery = journalAccountTypeQuery Expense "^expenses?(:|$)"
-- | A query for Asset, Liability & Equity accounts in this journal.
-- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Balance_Sheet_Accounts>.
@@ -302,25 +349,17 @@ journalBalanceSheetAccountQuery j = Or [journalAssetAccountQuery j
,journalEquityAccountQuery j
]
--- | A query for Asset accounts in this journal.
--- This is currently hard-coded to the case-insensitive regex @^assets?(:|$)@.
-journalAssetAccountQuery :: Journal -> Query
-journalAssetAccountQuery _ = Acct "^assets?(:|$)"
-
--- | A query for Liability accounts in this journal.
--- This is currently hard-coded to the case-insensitive regex @^(debts?|liabilit(y|ies))(:|$)@.
-journalLiabilityAccountQuery :: Journal -> Query
-journalLiabilityAccountQuery _ = Acct "^(debts?|liabilit(y|ies))(:|$)"
-
--- | A query for Equity accounts in this journal.
--- This is currently hard-coded to the case-insensitive regex @^equity(:|$)@.
-journalEquityAccountQuery :: Journal -> Query
-journalEquityAccountQuery _ = Acct "^equity(:|$)"
+-- | A query for Profit & Loss accounts in this journal.
+-- Cf <http://en.wikipedia.org/wiki/Chart_of_accounts#Profit_.26_Loss_accounts>.
+journalProfitAndLossAccountQuery :: Journal -> Query
+journalProfitAndLossAccountQuery j = Or [journalRevenueAccountQuery j
+ ,journalExpenseAccountQuery j
+ ]
-- | A query for Cash (-equivalent) accounts in this journal (ie,
-- accounts which appear on the cashflow statement.) This is currently
--- hard-coded to be all the Asset accounts except for those containing the
--- case-insensitive regex @(receivable|:A/R|:fixed)@.
+-- hard-coded to be all the Asset accounts except for those with names
+-- containing the case-insensitive regular expression @(receivable|:A/R|:fixed)@.
journalCashAccountQuery :: Journal -> Query
journalCashAccountQuery j = And [journalAssetAccountQuery j, Not $ Acct "(receivable|:A/R|:fixed)"]
@@ -482,23 +521,23 @@ filterJournalTransactionsByAccount apats j@Journal{jtxns=ts} = j{jtxns=filter tm
-}
--- | Do post-parse processing on a parsed journal to make it ready for
--- use. Reverse parsed data to normal order, standardise amount
--- formats, check/ensure that transactions are balanced, and maybe
--- check balance assertions.
-journalFinalise :: ClockTime -> FilePath -> Text -> Bool -> ParsedJournal -> Either String Journal
-journalFinalise t path txt assrt j@Journal{jfiles=fs} =
- journalTieTransactions <$>
- (journalBalanceTransactions assrt $
- journalApplyCommodityStyles $
- j {jfiles = (path,txt) : reverse fs
- ,jlastreadtime = t
- ,jdeclaredaccounts = reverse $ jdeclaredaccounts j
- ,jtxns = reverse $ jtxns j -- NOTE: see addTransaction
- ,jtxnmodifiers = reverse $ jtxnmodifiers j -- NOTE: see addTransactionModifier
- ,jperiodictxns = reverse $ jperiodictxns j -- NOTE: see addPeriodicTransaction
- ,jmarketprices = reverse $ jmarketprices j -- NOTE: see addMarketPrice
- })
+-- | Reverse parsed data to normal order. This is used for post-parse
+-- processing, since data is added to the head of the list during
+-- parsing.
+journalReverse :: Journal -> Journal
+journalReverse j =
+ j {jfiles = reverse $ jfiles j
+ ,jdeclaredaccounts = reverse $ jdeclaredaccounts j
+ ,jtxns = reverse $ jtxns j
+ ,jtxnmodifiers = reverse $ jtxnmodifiers j
+ ,jperiodictxns = reverse $ jperiodictxns j
+ ,jmarketprices = reverse $ jmarketprices j
+ }
+
+-- | Set this journal's last read time, ie when its files were last read.
+journalSetLastReadTime :: ClockTime -> Journal -> Journal
+journalSetLastReadTime t j = j{ jlastreadtime = t }
+
journalNumberAndTieTransactions = journalTieTransactions . journalNumberTransactions
@@ -530,12 +569,24 @@ journalCheckBalanceAssertions j =
-- | Check a posting's balance assertion and return an error if it
-- fails.
checkBalanceAssertion :: Posting -> MixedAmount -> Either String ()
-checkBalanceAssertion p@Posting{ pbalanceassertion = Just (ass,_)} amt
+checkBalanceAssertion p@Posting{ pbalanceassertion = Just ass } bal =
+ foldl' fold (Right ()) amts
+ where fold (Right _) cass = checkBalanceAssertionCommodity p cass bal
+ fold err _ = err
+ amt = baamount ass
+ amts = amt : if baexact ass
+ then map (\a -> a{ aquantity = 0 }) $ amounts $ filterMixedAmount (\a -> acommodity a /= assertedcomm) bal
+ else []
+ assertedcomm = acommodity amt
+checkBalanceAssertion _ _ = Right ()
+
+checkBalanceAssertionCommodity :: Posting -> Amount -> MixedAmount -> Either String ()
+checkBalanceAssertionCommodity p amt bal
| isReallyZeroAmount diff = Right ()
| True = Left err
- where assertedcomm = acommodity ass
- actualbal = fromMaybe nullamt $ find ((== assertedcomm) . acommodity) (amounts amt)
- diff = ass - actualbal
+ where assertedcomm = acommodity amt
+ actualbal = fromMaybe nullamt $ find ((== assertedcomm) . acommodity) (amounts bal)
+ diff = amt - actualbal
diffplus | isNegativeAmount diff == False = "+"
| otherwise = ""
err = printf (unlines
@@ -553,15 +604,14 @@ checkBalanceAssertion p@Posting{ pbalanceassertion = Just (ass,_)} amt
Nothing -> ":" -- shouldn't happen
Just t -> printf " in %s:\nin transaction:\n%s"
(showGenericSourcePos pos) (chomp $ showTransaction t) :: String
- where pos = snd $ fromJust $ pbalanceassertion p)
+ where pos = baposition $ fromJust $ pbalanceassertion p)
(showPostingLine p)
(showDate $ postingDate p)
(T.unpack $ paccount p) -- XXX pack
assertedcomm
(showAmount actualbal)
- (showAmount ass)
+ (showAmount amt)
(diffplus ++ showAmount diff)
-checkBalanceAssertion _ _ = Right ()
-- | Fill in any missing amounts and check that all journal transactions
-- balance, or return an error message. This is done after parsing all
@@ -592,6 +642,7 @@ journalBalanceTransactionsST assrt j createStore storeIn extract =
(storeIn txStore)
assrt
(Just $ journalCommodityStyles j)
+ (getModifierAccountNames j)
flip R.runReaderT env $ do
dated <- fmap snd . sortBy (comparing fst) . concat
<$> mapM' discriminateByDate (jtxns j)
@@ -600,16 +651,25 @@ journalBalanceTransactionsST assrt j createStore storeIn extract =
where
size = genericLength $ journalPostings j
+
+-- | Collect account names in account modifiers into a set
+getModifierAccountNames :: Journal -> S.Set AccountName
+getModifierAccountNames j = S.fromList $
+ map paccount $
+ concatMap tmpostingrules $
+ jtxnmodifiers j
+
-- | Monad transformer stack with a reference to a mutable hashtable
-- of current account balances and a mutable array of finished
-- transactions in original parsing order.
type CurrentBalancesModifier s = R.ReaderT (Env s) (ExceptT String (ST s))
-- | Environment for 'CurrentBalancesModifier'
-data Env s = Env { eBalances :: HT.HashTable s AccountName MixedAmount
- , eStoreTx :: Transaction -> ST s ()
- , eAssrt :: Bool
- , eStyles :: Maybe (M.Map CommoditySymbol AmountStyle)
+data Env s = Env { eBalances :: HT.HashTable s AccountName MixedAmount
+ , eStoreTx :: Transaction -> ST s ()
+ , eAssrt :: Bool
+ , eStyles :: Maybe (M.Map CommoditySymbol AmountStyle)
+ , eUnassignable :: S.Set AccountName
}
-- | This converts a transaction into a list of transactions or
@@ -634,12 +694,27 @@ discriminateByDate tx
| True = do
when (any (isJust . pdate) $ tpostings tx) $
throwError $ unlines $
- ["Not supported: Transactions with balance assignments "
- ,"AND dated postings without amount:\n"
+ ["postings may not have both a custom date and a balance assignment."
+ ,"Write the posting amount explicitly, or remove the posting date:\n"
, showTransaction tx]
return
[(tdate tx, Right $ tx { tpostings = removePrices <$> tpostings tx })]
+-- | Throw an error if a posting is in the unassignable set.
+checkUnassignablePosting :: Posting -> CurrentBalancesModifier s ()
+checkUnassignablePosting p = do
+ unassignable <- R.asks eUnassignable
+ if (isAssignment p && paccount p `S.member` unassignable)
+ then throwError $ unlines $
+ [ "cannot assign amount to account "
+ , ""
+ , " " ++ (T.unpack $ paccount p)
+ , ""
+ , "because it is also included in transaction modifiers."
+ ]
+ else return ()
+
+
-- | This function takes an object describing changes to
-- account balances on a single day - either a single posting
-- (from an already balanced transaction without assignments)
@@ -668,19 +743,36 @@ discriminateByDate tx
-- and then balance and store the transaction.
checkInferAndRegisterAmounts :: Either Posting Transaction
-> CurrentBalancesModifier s ()
-checkInferAndRegisterAmounts (Left p) =
+checkInferAndRegisterAmounts (Left p) = do
+ checkUnassignablePosting p
void $ addAmountAndCheckBalance return p
checkInferAndRegisterAmounts (Right oldTx) = do
let ps = tpostings oldTx
+ mapM_ checkUnassignablePosting ps
styles <- R.reader $ eStyles
newPostings <- forM ps $ addAmountAndCheckBalance inferFromAssignment
storeTransaction =<< balanceTransactionUpdate
(fmap void . addToBalance) styles oldTx { tpostings = newPostings }
where
inferFromAssignment :: Posting -> CurrentBalancesModifier s Posting
- inferFromAssignment p = maybe (return p)
- (fmap (\a -> p { pamount = a, porigin = Just $ originalPosting p }) . setBalance (paccount p) . fst)
- $ pbalanceassertion p
+ inferFromAssignment p = do
+ let acc = paccount p
+ case pbalanceassertion p of
+ Just ba | baexact ba -> do
+ diff <- setMixedBalance acc $ Mixed [baamount ba]
+ fullPosting diff p
+ Just ba | otherwise -> do
+ old <- liftModifier $ \Env{ eBalances = bals } -> HT.lookup bals acc
+ let amt = baamount ba
+ assertedcomm = acommodity amt
+ diff <- setMixedBalance acc $
+ Mixed [amt] + filterMixedAmount (\a -> acommodity a /= assertedcomm) (fromMaybe nullmixedamt old)
+ fullPosting diff p
+ Nothing -> return p
+ fullPosting amt p = return p
+ { pamount = amt
+ , porigin = Just $ originalPosting p
+ }
-- | Adds a posting's amount to the posting's account balance and
-- checks a possible balance assertion. Or if there is no amount,
@@ -696,15 +788,13 @@ addAmountAndCheckBalance _ p | hasAmount p = do
return p
addAmountAndCheckBalance fallback p = fallback p
--- | Sets an account's balance to a given amount and returns the
--- difference of new and old amount.
-setBalance :: AccountName -> Amount -> CurrentBalancesModifier s MixedAmount
-setBalance acc amt = liftModifier $ \Env{ eBalances = bals } -> do
+-- | Sets all commodities comprising an account's balance to the given
+-- amounts and returns the difference from the previous balance.
+setMixedBalance :: AccountName -> MixedAmount -> CurrentBalancesModifier s MixedAmount
+setMixedBalance acc amt = liftModifier $ \Env{ eBalances = bals } -> do
old <- HT.lookup bals acc
- let new = Mixed $ (amt :) $ maybe []
- (filter ((/= acommodity amt) . acommodity) . amounts) old
- HT.insert bals acc new
- return $ maybe new (new -) old
+ HT.insert bals acc amt
+ return $ maybe amt (amt -) old
-- | Adds an amount to an account's balance and returns the resulting balance.
addToBalance :: AccountName -> MixedAmount -> CurrentBalancesModifier s MixedAmount
@@ -1085,7 +1175,7 @@ tests_Journal = tests "Journal" [
test "assets" $ expectEq (namesfrom journalAssetAccountQuery) ["assets","assets:bank","assets:bank:checking","assets:bank:saving","assets:cash"]
,test "liabilities" $ expectEq (namesfrom journalLiabilityAccountQuery) ["liabilities","liabilities:debts"]
,test "equity" $ expectEq (namesfrom journalEquityAccountQuery) []
- ,test "income" $ expectEq (namesfrom journalIncomeAccountQuery) ["income","income:gifts","income:salary"]
+ ,test "income" $ expectEq (namesfrom journalRevenueAccountQuery) ["income","income:gifts","income:salary"]
,test "expenses" $ expectEq (namesfrom journalExpenseAccountQuery) ["expenses","expenses:food","expenses:supplies"]
]
diff --git a/Hledger/Data/Posting.hs b/Hledger/Data/Posting.hs
index cb0cc4d..48921c8 100644
--- a/Hledger/Data/Posting.hs
+++ b/Hledger/Data/Posting.hs
@@ -15,6 +15,9 @@ module Hledger.Data.Posting (
nullposting,
posting,
post,
+ nullsourcepos,
+ nullassertion,
+ assertion,
-- * operations
originalPosting,
postingStatus,
@@ -96,6 +99,17 @@ posting = nullposting
post :: AccountName -> Amount -> Posting
post acct amt = posting {paccount=acct, pamount=Mixed [amt]}
+nullsourcepos :: GenericSourcePos
+nullsourcepos = JournalSourcePos "" (1,1)
+
+nullassertion, assertion :: BalanceAssertion
+nullassertion = BalanceAssertion
+ {baamount=nullamt
+ ,baexact=False
+ ,baposition=nullsourcepos
+ }
+assertion = nullassertion
+
-- Get the original posting, if any.
originalPosting :: Posting -> Posting
originalPosting p = fromMaybe p $ porigin p
diff --git a/Hledger/Data/Transaction.hs b/Hledger/Data/Transaction.hs
index 20ed704..e9d4e46 100644
--- a/Hledger/Data/Transaction.hs
+++ b/Hledger/Data/Transaction.hs
@@ -12,7 +12,6 @@ tags.
module Hledger.Data.Transaction (
-- * Transaction
- nullsourcepos,
nulltransaction,
txnTieKnot,
txnUntieKnot,
@@ -72,14 +71,13 @@ sourceFirstLine = \case
GenericSourcePos _ line _ -> line
JournalSourcePos _ (line, _) -> line
+-- | Render source position in human-readable form.
+-- Keep in sync with Hledger.UI.ErrorScreen.hledgerparseerrorpositionp (temporary). XXX
showGenericSourcePos :: GenericSourcePos -> String
showGenericSourcePos = \case
GenericSourcePos fp line column -> show fp ++ " (line " ++ show line ++ ", column " ++ show column ++ ")"
JournalSourcePos fp (line, line') -> show fp ++ " (lines " ++ show line ++ "-" ++ show line' ++ ")"
-nullsourcepos :: GenericSourcePos
-nullsourcepos = JournalSourcePos "" (1,1)
-
nulltransaction :: Transaction
nulltransaction = Transaction {
tindex=0,
@@ -96,8 +94,9 @@ nulltransaction = Transaction {
}
{-|
-Show a journal transaction, formatted for the print command. ledger 2.x's
-standard format looks like this:
+Render a journal transaction as text in the style of Ledger's print command.
+
+Ledger 2.x's standard format looks like this:
@
yyyy/mm/dd[ *][ CODE] description......... [ ; comment...............]
@@ -110,17 +109,40 @@ pacctwidth = 35 minimum, no maximum -- they were important at the time.
pamtwidth = 11
pcommentwidth = no limit -- 22
@
+
+The output will be parseable journal syntax.
+To facilitate this, postings with explicit multi-commodity amounts
+are displayed as multiple similar postings, one per commodity.
+(Normally does not happen with this function).
+
+If there are multiple postings, all with explicit amounts,
+and the transaction appears obviously balanced
+(postings sum to 0, without needing to infer conversion prices),
+the last posting's amount will not be shown.
-}
+-- XXX why that logic ?
+-- XXX where is/should this be still used ?
+-- XXX rename these, after amount expressions/mixed posting amounts lands
+-- eg showTransactionSimpleAmountsElidingLast, showTransactionSimpleAmounts, showTransaction
showTransaction :: Transaction -> String
showTransaction = showTransactionHelper True False
+-- | Like showTransaction, but does not change amounts' explicitness.
+-- Explicit amounts are shown and implicit amounts are not.
+-- The output will be parseable journal syntax.
+-- To facilitate this, postings with explicit multi-commodity amounts
+-- are displayed as multiple similar postings, one per commodity.
+-- Most often, this is the one you want to use.
showTransactionUnelided :: Transaction -> String
showTransactionUnelided = showTransactionHelper False False
+-- | Like showTransactionUnelided, but explicit multi-commodity amounts
+-- are shown on one line, comma-separated. In this case the output will
+-- not be parseable journal syntax.
showTransactionUnelidedOneLineAmounts :: Transaction -> String
showTransactionUnelidedOneLineAmounts = showTransactionHelper False True
--- cf showPosting
+-- | Helper for showTransaction*.
showTransactionHelper :: Bool -> Bool -> Transaction -> String
showTransactionHelper elide onelineamounts t =
unlines $ [descriptionline]
@@ -139,42 +161,72 @@ showTransactionHelper elide onelineamounts t =
case renderCommentLines (tcomment t) of [] -> ("",[])
c:cs -> (c,cs)
--- Render a transaction or posting's comment as indented, semicolon-prefixed comment lines.
+-- | Render a transaction or posting's comment as indented, semicolon-prefixed comment lines.
renderCommentLines :: Text -> [String]
renderCommentLines t = case lines $ T.unpack t of ("":ls) -> "":map commentprefix ls
ls -> map commentprefix ls
where
commentprefix = indent . ("; "++)
--- -- Render a transaction or posting's comment as semicolon-prefixed comment lines -
--- -- an inline (same-line) comment if it's a single line, otherwise multiple indented lines.
--- commentLines' :: String -> (String, [String])
--- commentLines' s
--- | null s = ("", [])
--- | length ls == 1 = (prefix $ head ls, [])
--- | otherwise = ("", (prefix $ head ls):(map prefix $ tail ls))
--- where
--- ls = lines s
--- prefix = indent . (";"++)
-
+-- | Given a transaction and its postings, render the postings, suitable
+-- for `print` output. Normally this output will be valid journal syntax which
+-- hledger can reparse (though it may include no-longer-valid balance assertions).
+--
+-- Explicit amounts are shown, any implicit amounts are not.
+--
+-- Setting elide to true forces the last posting's amount to be implicit, if:
+-- there are other postings, all with explicit amounts, and the transaction
+-- appears balanced.
+--
+-- Postings with multicommodity explicit amounts are handled as follows:
+-- if onelineamounts is true, these amounts are shown on one line,
+-- comma-separated, and the output will not be valid journal syntax.
+-- Otherwise, they are shown as several similar postings, one per commodity.
+--
+-- The output will appear to be a balanced transaction.
+-- Amounts' display precisions, which may have been limited by commodity
+-- directives, will be increased if necessary to ensure this.
+--
+-- Posting amounts will be aligned with each other, starting about 4 columns
+-- beyond the widest account name (see postingAsLines for details).
+--
postingsAsLines :: Bool -> Bool -> Transaction -> [Posting] -> [String]
postingsAsLines elide onelineamounts t ps
- | elide && length ps > 1 && isTransactionBalanced Nothing t -- imprecise balanced check
+ | elide && length ps > 1 && all hasAmount ps && isTransactionBalanced Nothing t -- imprecise balanced check
= (concatMap (postingAsLines False onelineamounts ps) $ init ps) ++ postingAsLines True onelineamounts ps (last ps)
| otherwise = concatMap (postingAsLines False onelineamounts ps) ps
+-- | Render one posting, on one or more lines, suitable for `print` output.
+-- There will be an indented account name, plus one or more of status flag,
+-- posting amount, balance assertion, same-line comment, next-line comments.
+--
+-- If the posting's amount is implicit or if elideamount is true, no amount is shown.
+--
+-- If the posting's amount is explicit and multi-commodity, multiple similar
+-- postings are shown, one for each commodity, to help produce parseable journal syntax.
+-- Or if onelineamounts is true, such amounts are shown on one line, comma-separated
+-- (and the output will not be valid journal syntax).
+--
+-- By default, 4 spaces (2 if there's a status flag) are shown between
+-- account name and start of amount area, which is typically 12 chars wide
+-- and contains a right-aligned amount (so 10-12 visible spaces between
+-- account name and amount is typical).
+-- When given a list of postings to be aligned with, the whitespace will be
+-- increased if needed to match the posting with the longest account name.
+-- This is used to align the amounts of a transaction's postings.
+--
postingAsLines :: Bool -> Bool -> [Posting] -> Posting -> [String]
-postingAsLines elideamount onelineamounts ps p = concat [
+postingAsLines elideamount onelineamounts pstoalignwith p = concat [
postingblock
++ newlinecomments
| postingblock <- postingblocks]
where
postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, " ", amount, assertion, samelinecomment] | amount <- shownAmounts]
- assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . fst) $ pbalanceassertion p
+ assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . baamount) $ pbalanceassertion p
statusandaccount = indent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p
where
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
- minwidth = maximum $ map ((2+) . textWidth . T.pack . pacctstr) ps
+ minwidth = maximum $ map ((2+) . textWidth . T.pack . pacctstr) pstoalignwith
pstatusandacct p' = pstatusprefix p' ++ pacctstr p'
pstatusprefix p' | null s = ""
| otherwise = s ++ " "
@@ -188,14 +240,14 @@ postingAsLines elideamount onelineamounts ps p = concat [
| null (amounts $ pamount p) = [""]
| otherwise = map (fitStringMulti (Just amtwidth) Nothing False False . showAmount ) . amounts $ pamount p
where
- amtwidth = maximum $ 12 : map (strWidth . showMixedAmount . pamount) ps -- min. 12 for backwards compatibility
+ amtwidth = maximum $ 12 : map (strWidth . showMixedAmount . pamount) pstoalignwith -- min. 12 for backwards compatibility
(samelinecomment, newlinecomments) =
case renderCommentLines (pcomment p) of [] -> ("",[])
c:cs -> (c,cs)
-
--- used in balance assertion error
+-- | Show a posting's status, account name and amount on one line.
+-- Used in balance assertion errors.
showPostingLine p =
indent $
if pstatus p == Cleared then "* " else "" ++
@@ -203,7 +255,8 @@ showPostingLine p =
" " ++
showMixedAmountOneLine (pamount p)
--- | Produce posting line with all comment lines associated with it
+-- | Render a posting, at the appropriate width for aligning with
+-- its siblings if any. Used by the rewrite command.
showPostingLines :: Posting -> [String]
showPostingLines p = postingAsLines False False ps p where
ps | Just t <- ptransaction p = tpostings t
@@ -252,9 +305,12 @@ transactionPostingBalances t = (sumPostings $ realPostings t
,sumPostings $ virtualPostings t
,sumPostings $ balancedVirtualPostings t)
--- | Is this transaction balanced ? A balanced transaction's real
--- (non-virtual) postings sum to 0, and any balanced virtual postings
--- also sum to 0.
+-- | Does this transaction appear balanced when rendered, optionally with the
+-- given commodity display styles ? More precisely:
+-- after converting amounts to cost using explicit transaction prices if any;
+-- and summing the real postings, and summing the balanced virtual postings;
+-- and applying the given display styles if any (maybe affecting decimal places);
+-- do both totals appear to be zero when rendered ?
isTransactionBalanced :: Maybe (Map.Map CommoditySymbol AmountStyle) -> Transaction -> Bool
isTransactionBalanced styles t =
-- isReallyZeroMixedAmountCost rsum && isReallyZeroMixedAmountCost bvsum
@@ -286,13 +342,13 @@ balanceTransactionUpdate :: MonadError String m
-> Maybe (Map.Map CommoditySymbol AmountStyle)
-> Transaction -> m Transaction
balanceTransactionUpdate update mstyles t =
- finalize =<< inferBalancingAmount update (fromMaybe Map.empty mstyles) t
+ (finalize =<< inferBalancingAmount update (fromMaybe Map.empty mstyles) t)
+ `catchError` (throwError . annotateErrorWithTxn t)
where
finalize t' = let t'' = inferBalancingPrices t'
in if isTransactionBalanced mstyles t''
then return $ txnTieKnot t''
- else throwError $ printerr $ nonzerobalanceerror t''
- printerr s = intercalate "\n" [s, showTransactionUnelided t]
+ else throwError $ nonzerobalanceerror t''
nonzerobalanceerror :: Transaction -> String
nonzerobalanceerror t = printf "could not balance this transaction (%s%s%s)" rmsg sep bvmsg
where
@@ -305,6 +361,8 @@ balanceTransactionUpdate update mstyles t =
++ showMixedAmount (costOfMixedAmount bvsum)
sep = if not (null rmsg) && not (null bvmsg) then "; " else "" :: String
+ annotateErrorWithTxn t e = intercalate "\n" [showGenericSourcePos $ tsourcepos t, e, showTransactionUnelided t]
+
-- | Infer up to one missing amount for this transactions's real postings, and
-- likewise for its balanced virtual postings, if needed; or return an error
-- message if we can't.
@@ -319,14 +377,13 @@ inferBalancingAmount :: MonadError String m =>
-> m Transaction
inferBalancingAmount update styles t@Transaction{tpostings=ps}
| length amountlessrealps > 1
- = throwError $ printerr "could not balance this transaction - can't have more than one real posting with no amount (remember to put 2 or more spaces before amounts)"
+ = throwError "could not balance this transaction - can't have more than one real posting with no amount (remember to put 2 or more spaces before amounts)"
| length amountlessbvps > 1
- = throwError $ printerr "could not balance this transaction - can't have more than one balanced virtual posting with no amount (remember to put 2 or more spaces before amounts)"
+ = throwError "could not balance this transaction - can't have more than one balanced virtual posting with no amount (remember to put 2 or more spaces before amounts)"
| otherwise
= do postings <- mapM inferamount ps
return t{tpostings=postings}
where
- printerr s = intercalate "\n" [s, showTransactionUnelided t]
(amountfulrealps, amountlessrealps) = partition hasAmount (realPostings t)
realsum = sumStrict $ map pamount amountfulrealps
(amountfulbvps, amountlessbvps) = partition hasAmount (balancedVirtualPostings t)
@@ -417,7 +474,7 @@ priceInferrerFor t pt = inferprice
fromamount = head $ filter ((==fromcommodity).acommodity) sumamounts
tocommodity = head $ filter (/=fromcommodity) sumcommodities
toamount = head $ filter ((==tocommodity).acommodity) sumamounts
- unitprice = toamount `divideAmount` (aquantity fromamount)
+ unitprice = (aquantity fromamount) `divideAmount` toamount
unitprecision = max 2 ((asprecision $ astyle $ toamount) + (asprecision $ astyle $ fromamount))
inferprice p = p
@@ -445,7 +502,7 @@ postingSetTransaction t p = p{ptransaction=Just t}
tests_Transaction = tests "Transaction" [
tests "showTransactionUnelided" [
- showTransactionUnelided nulltransaction `is` "0000/01/01\n\n"
+ showTransactionUnelided nulltransaction `is` "0000/01/01\n\n"
,showTransactionUnelided nulltransaction{
tdate=parsedate "2012/05/14",
tdate2=Just $ parsedate "2012/05/15",
@@ -497,6 +554,108 @@ tests_Transaction = tests "Transaction" [
]
]
+ -- postingsAsLines
+ ,let
+ -- one implicit amount
+ timp = nulltransaction{tpostings=[
+ "a" `post` usd 1,
+ "b" `post` missingamt
+ ]}
+ -- explicit amounts, balanced
+ texp = nulltransaction{tpostings=[
+ "a" `post` usd 1,
+ "b" `post` usd (-1)
+ ]}
+ -- explicit amount, only one posting
+ texp1 = nulltransaction{tpostings=[
+ "(a)" `post` usd 1
+ ]}
+ -- explicit amounts, two commodities, explicit balancing price
+ texp2 = nulltransaction{tpostings=[
+ "a" `post` usd 1,
+ "b" `post` (hrs (-1) `at` usd 1)
+ ]}
+ -- explicit amounts, two commodities, implicit balancing price
+ texp2b = nulltransaction{tpostings=[
+ "a" `post` usd 1,
+ "b" `post` hrs (-1)
+ ]}
+ -- one missing amount, not the last one
+ t3 = nulltransaction{tpostings=[
+ "a" `post` usd 1
+ ,"b" `post` missingamt
+ ,"c" `post` usd (-1)
+ ]}
+ -- unbalanced amounts when precision is limited (#931)
+ t4 = nulltransaction{tpostings=[
+ "a" `post` usd (-0.01)
+ ,"b" `post` usd (0.005)
+ ,"c" `post` usd (0.005)
+ ]}
+ in
+ tests "postingsAsLines" [
+
+ test "null-transaction" $
+ let t = nulltransaction
+ in postingsAsLines True False t (tpostings t) `is` []
+
+ ,test "implicit-amount-elide-false" $
+ let t = timp in postingsAsLines False False t (tpostings t) `is` [
+ " a $1.00"
+ ," b" -- implicit amount remains implicit
+ ]
+
+ ,test "implicit-amount-elide-true" $
+ let t = timp in postingsAsLines True False t (tpostings t) `is` [
+ " a $1.00"
+ ," b" -- implicit amount remains implicit
+ ]
+
+ ,test "explicit-amounts-elide-false" $
+ let t = texp in postingsAsLines False False t (tpostings t) `is` [
+ " a $1.00"
+ ," b $-1.00" -- both amounts remain explicit
+ ]
+
+ ,test "explicit-amounts-elide-true" $
+ let t = texp in postingsAsLines True False t (tpostings t) `is` [
+ " a $1.00"
+ ," b" -- explicit amount is made implicit
+ ]
+
+ ,test "one-explicit-amount-elide-true" $
+ let t = texp1 in postingsAsLines True False t (tpostings t) `is` [
+ " (a) $1.00" -- explicit amount remains explicit since only one posting
+ ]
+
+ ,test "explicit-amounts-two-commodities-elide-true" $
+ let t = texp2 in postingsAsLines True False t (tpostings t) `is` [
+ " a $1.00"
+ ," b" -- explicit amount is made implicit since txn is explicitly balanced
+ ]
+
+ ,test "explicit-amounts-not-explicitly-balanced-elide-true" $
+ let t = texp2b in postingsAsLines True False t (tpostings t) `is` [
+ " a $1.00"
+ ," b -1.00h" -- explicit amount remains explicit since a conversion price would have be inferred to balance
+ ]
+
+ ,test "implicit-amount-not-last" $
+ let t = t3 in postingsAsLines True False t (tpostings t) `is` [
+ " a $1.00"
+ ," b"
+ ," c $-1.00"
+ ]
+
+ ,_test "ensure-visibly-balanced" $
+ let t = t4 in postingsAsLines False False t (tpostings t) `is` [
+ " a $-0.01"
+ ," b $0.005"
+ ," c $0.005"
+ ]
+
+ ]
+
,do
let inferTransaction :: Transaction -> Either String Transaction
inferTransaction = runIdentity . runExceptT . inferBalancingAmount (\_ _ -> return ()) Map.empty
@@ -543,10 +702,8 @@ tests_Transaction = tests "Transaction" [
," assets:checking"
,""
]
- ]
- ,tests "showTransaction" [
- test "show a balanced transaction, no eliding" $
+ ,test "show a balanced transaction, no eliding" $
(let t = Transaction 0 nullsourcepos (parsedate "2007/01/28") Nothing Unmarked "" "coopportunity" "" []
[posting{paccount="expenses:food:groceries", pamount=Mixed [usd 47.18], ptransaction=Just t}
,posting{paccount="assets:checking", pamount=Mixed [usd (-47.18)], ptransaction=Just t}
diff --git a/Hledger/Data/TransactionModifier.hs b/Hledger/Data/TransactionModifier.hs
index 9095208..6eb9563 100644
--- a/Hledger/Data/TransactionModifier.hs
+++ b/Hledger/Data/TransactionModifier.hs
@@ -24,7 +24,7 @@ import Hledger.Data.Amount
import Hledger.Data.Transaction
import Hledger.Query
import Hledger.Utils.UTF8IOCompat (error')
--- import Hledger.Utils.Debug
+import Hledger.Utils.Debug
-- $setup
-- >>> :set -XOverloadedStrings
@@ -58,7 +58,7 @@ transactionModifierToFunction mt =
\t@(tpostings -> ps) -> txnTieKnot t{ tpostings=generatePostings ps } -- TODO add modifier txn comment/tags ?
where
q = simplifyQuery $ tmParseQuery mt (error' "a transaction modifier's query cannot depend on current date")
- mods = map tmPostingToFunction $ tmpostings mt
+ mods = map tmPostingRuleToFunction $ tmpostingrules mt
generatePostings ps = [p' | p <- ps
, p' <- if q `matchesPosting` p then p:[ m p | m <- mods] else [p]]
@@ -76,51 +76,43 @@ transactionModifierToFunction mt =
tmParseQuery :: TransactionModifier -> (Day -> Query)
tmParseQuery mt = fst . flip parseQuery (tmquerytxt mt)
----- | 'DateSpan' of all dates mentioned in 'Journal'
-----
----- >>> jdatespan nulljournal
----- DateSpan -
----- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01"}] }
----- DateSpan 2016/01/01
----- >>> jdatespan nulljournal{jtxns=[nulltransaction{tdate=read "2016-01-01", tpostings=[nullposting{pdate=Just $ read "2016-02-01"}]}] }
----- DateSpan 2016/01/01-2016/02/01
---jdatespan :: Journal -> DateSpan
---jdatespan j
--- | null dates = nulldatespan
--- | otherwise = DateSpan (Just $ minimum dates) (Just $ 1 `addDays` maximum dates)
--- where
--- dates = concatMap tdates $ jtxns j
-
----- | 'DateSpan' of all dates mentioned in 'Transaction'
-----
----- >>> tdates nulltransaction
----- [0000-01-01]
---tdates :: Transaction -> [Day]
---tdates t = tdate t : concatMap pdates (tpostings t) ++ maybeToList (tdate2 t) where
--- pdates p = catMaybes [pdate p, pdate2 p]
-
-postingScale :: Posting -> Maybe Quantity
-postingScale p =
- case amounts $ pamount p of
- [a] | amultiplier a -> Just $ aquantity a
- _ -> Nothing
-
--- | Converts a 'TransactionModifier''s posting to a 'Posting'-generating function,
+-- | Converts a 'TransactionModifier''s posting rule to a 'Posting'-generating function,
-- which will be used to make a new posting based on the old one (an "automated posting").
-tmPostingToFunction :: Posting -> (Posting -> Posting)
-tmPostingToFunction p' =
- \p -> renderPostingCommentDates $ p'
+-- The new posting's amount can optionally be the old posting's amount multiplied by a constant.
+-- If the old posting had a total-priced amount, the new posting's multiplied amount will be unit-priced.
+tmPostingRuleToFunction :: TMPostingRule -> (Posting -> Posting)
+tmPostingRuleToFunction pr =
+ \p -> renderPostingCommentDates $ pr
{ pdate = pdate p
, pdate2 = pdate2 p
, pamount = amount' p
}
where
- amount' = case postingScale p' of
- Nothing -> const $ pamount p'
- Just n -> \p -> withAmountType (head $ amounts $ pamount p') $ pamount p `divideMixedAmount` (1/n)
- withAmountType amount (Mixed as) = case acommodity amount of
- "" -> Mixed as
- c -> Mixed [a{acommodity = c, astyle = astyle amount, aprice = aprice amount} | a <- as]
+ amount' = case postingRuleMultiplier pr of
+ Nothing -> const $ pamount pr
+ Just n -> \p ->
+ -- Multiply the old posting's amount by the posting rule's multiplier.
+ let
+ pramount = dbg6 "pramount" $ head $ amounts $ pamount pr
+ matchedamount = dbg6 "matchedamount" $ pamount p
+ -- Handle a matched amount with a total price carefully so as to keep the transaction balanced (#928).
+ -- Approach 1: convert to a unit price and increase the display precision slightly
+ -- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount
+ -- Approach 2: multiply the total price (keeping it positive) as well as the quantity
+ Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmountAndPrice` matchedamount
+ in
+ case acommodity pramount of
+ "" -> Mixed as
+ -- TODO multipliers with commodity symbols are not yet a documented feature.
+ -- For now: in addition to multiplying the quantity, it also replaces the
+ -- matched amount's commodity, display style, and price with those of the posting rule.
+ c -> Mixed [a{acommodity = c, astyle = astyle pramount, aprice = aprice pramount} | a <- as]
+
+postingRuleMultiplier :: TMPostingRule -> Maybe Quantity
+postingRuleMultiplier p =
+ case amounts $ pamount p of
+ [a] | amultiplier a -> Just $ aquantity a
+ _ -> Nothing
renderPostingCommentDates :: Posting -> Posting
renderPostingCommentDates p = p { pcomment = comment' }
diff --git a/Hledger/Data/Types.hs b/Hledger/Data/Types.hs
index ee24fcf..be28992 100644
--- a/Hledger/Data/Types.hs
+++ b/Hledger/Data/Types.hs
@@ -43,6 +43,9 @@ import Text.Printf
import Hledger.Utils.Regex
+-- | A possibly incomplete date, whose missing parts will be filled from a reference date.
+-- A numeric year, month, and day of month, or the empty string for any of these.
+-- See the smartdate parser.
type SmartDate = (String,String,String)
data WhichDate = PrimaryDate | SecondaryDate deriving (Eq,Show)
@@ -110,7 +113,26 @@ instance Default Interval where def = NoInterval
instance NFData Interval
type AccountName = Text
-type AccountCode = Int
+
+data AccountType =
+ Asset
+ | Liability
+ | Equity
+ | Revenue
+ | Expense
+ deriving (Show,Eq,Ord,Data,Generic)
+
+instance NFData AccountType
+
+-- not worth the trouble, letters defined in accountdirectivep for now
+--instance Read AccountType
+-- where
+-- readsPrec _ ('A' : xs) = [(Asset, xs)]
+-- readsPrec _ ('L' : xs) = [(Liability, xs)]
+-- readsPrec _ ('E' : xs) = [(Equity, xs)]
+-- readsPrec _ ('R' : xs) = [(Revenue, xs)]
+-- readsPrec _ ('X' : xs) = [(Expense, xs)]
+-- readsPrec _ _ = []
data AccountAlias = BasicAlias AccountName AccountName
| RegexAlias Regexp Replacement
@@ -132,7 +154,7 @@ instance ToMarkup Quantity
toMarkup = toMarkup . show
-- | An amount's price (none, per unit, or total) in another commodity.
--- Note the price should be a positive number, although this is not enforced.
+-- The price amount should always be positive.
data Price = NoPrice | UnitPrice Amount | TotalPrice Amount
deriving (Eq,Ord,Typeable,Data,Generic,Show)
@@ -183,7 +205,8 @@ data Amount = Amount {
aquantity :: Quantity,
aprice :: Price, -- ^ the (fixed) price for this amount, if any
astyle :: AmountStyle,
- amultiplier :: Bool -- ^ amount is a multipier used in TransactionModifier postings
+ amultiplier :: Bool -- ^ kludge: a flag marking this amount and posting as a multiplier
+ -- in a TMPostingRule. In a regular Posting, should always be false.
} deriving (Eq,Ord,Typeable,Data,Generic,Show)
instance NFData Amount
@@ -214,7 +237,15 @@ instance Show Status where -- custom show.. bad idea.. don't do it..
show Pending = "!"
show Cleared = "*"
-type BalanceAssertion = Maybe (Amount, GenericSourcePos)
+-- | The amount to compare an account's balance to, to verify that the history
+-- leading to a given point is correct or to set the account to a known value.
+data BalanceAssertion = BalanceAssertion {
+ baamount :: Amount, -- ^ the expected value of a particular commodity
+ baexact :: Bool, -- ^ whether the assertion is exclusive, and doesn't allow other commodities alongside 'baamount'
+ baposition :: GenericSourcePos
+ } deriving (Eq,Typeable,Data,Generic,Show)
+
+instance NFData BalanceAssertion
data Posting = Posting {
pdate :: Maybe Day, -- ^ this posting's date, if different from the transaction's
@@ -224,11 +255,14 @@ data Posting = Posting {
pamount :: MixedAmount,
pcomment :: Text, -- ^ this posting's comment lines, as a single non-indented multi-line string
ptype :: PostingType,
- ptags :: [Tag], -- ^ tag names and values, extracted from the comment
- pbalanceassertion :: BalanceAssertion, -- ^ optional: the expected balance in this commodity in the account after this posting
- ptransaction :: Maybe Transaction, -- ^ this posting's parent transaction (co-recursive types).
- -- Tying this knot gets tedious, Maybe makes it easier/optional.
- porigin :: Maybe Posting -- ^ original posting if this one is result of any transformations (one level only)
+ ptags :: [Tag], -- ^ tag names and values, extracted from the comment
+ pbalanceassertion :: Maybe BalanceAssertion, -- ^ optional: the expected balance in this commodity in the account after this posting
+ ptransaction :: Maybe Transaction, -- ^ this posting's parent transaction (co-recursive types).
+ -- Tying this knot gets tedious, Maybe makes it easier/optional.
+ porigin :: Maybe Posting -- ^ When this posting has been transformed in some way
+ -- (eg its amount or price was inferred, or the account name was
+ -- changed by a pivot or budget report), this references the original
+ -- untransformed posting (which will have Nothing in this field).
} deriving (Typeable,Data,Generic)
instance NFData Posting
@@ -283,18 +317,29 @@ data Transaction = Transaction {
instance NFData Transaction
+-- | A transaction modifier rule. This has a query which matches postings
+-- in the journal, and a list of transformations to apply to those
+-- postings or their transactions. Currently there is one kind of transformation:
+-- the TMPostingRule, which adds a posting ("auto posting") to the transaction,
+-- optionally setting its amount to the matched posting's amount multiplied by a constant.
data TransactionModifier = TransactionModifier {
tmquerytxt :: Text,
- tmpostings :: [Posting]
+ tmpostingrules :: [TMPostingRule]
} deriving (Eq,Typeable,Data,Generic,Show)
instance NFData TransactionModifier
nulltransactionmodifier = TransactionModifier{
tmquerytxt = ""
- ,tmpostings = []
+ ,tmpostingrules = []
}
+-- | A transaction modifier transformation, which adds an extra posting
+-- to the matched posting's transaction.
+-- Can be like a regular posting, or the amount can have the amultiplier flag set,
+-- indicating that it's a multiplier for the matched posting's amount.
+type TMPostingRule = Posting
+
-- | A periodic transaction rule, describing a transaction that recurs.
data PeriodicTransaction = PeriodicTransaction {
ptperiodexpr :: Text, -- ^ the period expression as written
@@ -363,8 +408,10 @@ data Journal = Journal {
,jparsealiases :: [AccountAlias] -- ^ the current account name aliases in effect, specified by alias directives (& options ?)
-- ,jparsetransactioncount :: Integer -- ^ the current count of transactions parsed so far (only journal format txns, currently)
,jparsetimeclockentries :: [TimeclockEntry] -- ^ timeclock sessions which have not been clocked out
+ ,jincludefilestack :: [FilePath]
-- principal data
,jdeclaredaccounts :: [AccountName] -- ^ Accounts declared by account directives, in parse order (after journal finalisation)
+ ,jdeclaredaccounttypes :: M.Map AccountType [AccountName] -- ^ Accounts whose type has been declared in account directives (usually 5 top-level accounts)
,jcommodities :: M.Map CommoditySymbol Commodity -- ^ commodities and formats declared by commodity directives
,jinferredcommodities :: M.Map CommoditySymbol AmountStyle -- ^ commodities and formats inferred from journal amounts TODO misnamed - jusedstyles
,jmarketprices :: [MarketPrice]
diff --git a/Hledger/Read/Common.hs b/Hledger/Read/Common.hs
index c760c3d..20f026f 100644
--- a/Hledger/Read/Common.hs
+++ b/Hledger/Read/Common.hs
@@ -29,10 +29,13 @@ module Hledger.Read.Common (
rtp,
runJournalParser,
rjp,
+ runErroringJournalParser,
+ rejp,
genericSourcePos,
journalSourcePos,
applyTransactionModifiers,
parseAndFinaliseJournal,
+ parseAndFinaliseJournal',
setYear,
getYear,
setDefaultCommodityAndStyle,
@@ -40,6 +43,7 @@ module Hledger.Read.Common (
getDefaultAmountStyle,
getAmountStyle,
pushDeclaredAccount,
+ addDeclaredAccountType,
pushParentAccount,
popParentAccount,
getParentAccount,
@@ -70,7 +74,7 @@ module Hledger.Read.Common (
mamountp',
commoditysymbolp,
priceamountp,
- partialbalanceassertionp,
+ balanceassertionp,
fixedlotpricep,
numberp,
fromRawNumber,
@@ -89,6 +93,7 @@ module Hledger.Read.Common (
-- ** misc
singlespacedtextp,
+ singlespacedtextsatisfyingp,
singlespacep,
-- * tests
@@ -99,7 +104,7 @@ where
import Prelude ()
import "base-compat-batteries" Prelude.Compat hiding (readFile)
import "base-compat-batteries" Control.Monad.Compat
-import Control.Monad.Except (ExceptT(..), throwError)
+import Control.Monad.Except (ExceptT(..), runExceptT, throwError)
import Control.Monad.State.Strict
import Data.Bifunctor (bimap, second)
import Data.Char
@@ -191,15 +196,28 @@ rawOptsToInputOpts rawopts = InputOpts{
--- * parsing utilities
-- | Run a text parser in the identity monad. See also: parseWithState.
-runTextParser, rtp :: TextParser Identity a -> Text -> Either (ParseError Char CustomErr) a
+runTextParser, rtp
+ :: TextParser Identity a -> Text -> Either (ParseErrorBundle Text CustomErr) a
runTextParser p t = runParser p "" t
rtp = runTextParser
-- | Run a journal parser in some monad. See also: parseWithState.
-runJournalParser, rjp :: Monad m => JournalParser m a -> Text -> m (Either (ParseError Char CustomErr) a)
+runJournalParser, rjp
+ :: Monad m
+ => JournalParser m a -> Text -> m (Either (ParseErrorBundle Text CustomErr) a)
runJournalParser p t = runParserT (evalStateT p mempty) "" t
rjp = runJournalParser
+-- | Run an erroring journal parser in some monad. See also: parseWithState.
+runErroringJournalParser, rejp
+ :: Monad m
+ => ErroringJournalParser m a
+ -> Text
+ -> m (Either FinalParseError (Either (ParseErrorBundle Text CustomErr) a))
+runErroringJournalParser p t =
+ runExceptT $ runParserT (evalStateT p mempty) "" t
+rejp = runErroringJournalParser
+
genericSourcePos :: SourcePos -> GenericSourcePos
genericSourcePos p = GenericSourcePos (sourceName p) (fromIntegral . unPos $ sourceLine p) (fromIntegral . unPos $ sourceColumn p)
@@ -221,19 +239,88 @@ applyTransactionModifiers j = j { jtxns = map applyallmodifiers $ jtxns j }
-- | Given a megaparsec ParsedJournal parser, input options, file
-- path and file content: parse and post-process a Journal, or give an error.
-parseAndFinaliseJournal :: JournalParser IO ParsedJournal -> InputOpts
+parseAndFinaliseJournal :: ErroringJournalParser IO ParsedJournal -> InputOpts
-> FilePath -> Text -> ExceptT String IO Journal
parseAndFinaliseJournal parser iopts f txt = do
t <- liftIO getClockTime
y <- liftIO getCurrentYear
- ep <- liftIO $ runParserT (evalStateT parser nulljournal {jparsedefaultyear=Just y}) f txt
+ let initJournal = nulljournal
+ { jparsedefaultyear = Just y
+ , jincludefilestack = [f] }
+ eep <- liftIO $ runExceptT $
+ runParserT (evalStateT parser initJournal) f txt
+ case eep of
+ Left finalParseError ->
+ throwError $ finalErrorBundlePretty $ attachSource f txt finalParseError
+
+ Right ep -> case ep of
+ Left e -> throwError $ customErrorBundlePretty e
+
+ Right pj ->
+ -- If we are using automated transactions, we finalize twice:
+ -- once before and once after. However, if we are running it
+ -- twice, we don't check assertions the first time (they might
+ -- be false pending modifiers) and we don't reorder the second
+ -- time. If we are only running once, we reorder and follow
+ -- the options for checking assertions.
+ let fj = if auto_ iopts && (not . null . jtxnmodifiers) pj
+ then applyTransactionModifiers <$>
+ (journalBalanceTransactions False $
+ journalReverse $
+ journalApplyCommodityStyles pj) >>=
+ (\j -> journalBalanceTransactions (not $ ignore_assertions_ iopts) $
+ journalAddFile (f, txt) $
+ journalSetLastReadTime t $
+ j)
+ else journalBalanceTransactions (not $ ignore_assertions_ iopts) $
+ journalReverse $
+ journalAddFile (f, txt) $
+ journalApplyCommodityStyles $
+ journalSetLastReadTime t $
+ pj
+ in
+ case fj of
+ Right j -> return j
+ Left e -> throwError e
+
+parseAndFinaliseJournal' :: JournalParser IO ParsedJournal -> InputOpts
+ -> FilePath -> Text -> ExceptT String IO Journal
+parseAndFinaliseJournal' parser iopts f txt = do
+ t <- liftIO getClockTime
+ y <- liftIO getCurrentYear
+ let initJournal = nulljournal
+ { jparsedefaultyear = Just y
+ , jincludefilestack = [f] }
+ ep <- liftIO $ runParserT (evalStateT parser initJournal) f txt
case ep of
- Right pj ->
- let pj' = if auto_ iopts then applyTransactionModifiers pj else pj in
- case journalFinalise t f txt (not $ ignore_assertions_ iopts) pj' of
- Right j -> return j
- Left e -> throwError e
- Left e -> throwError $ customParseErrorPretty txt e
+ Left e -> throwError $ customErrorBundlePretty e
+
+ Right pj ->
+ -- If we are using automated transactions, we finalize twice:
+ -- once before and once after. However, if we are running it
+ -- twice, we don't check assertions the first time (they might
+ -- be false pending modifiers) and we don't reorder the second
+ -- time. If we are only running once, we reorder and follow the
+ -- options for checking assertions.
+ let fj = if auto_ iopts && (not . null . jtxnmodifiers) pj
+ then applyTransactionModifiers <$>
+ (journalBalanceTransactions False $
+ journalReverse $
+ journalApplyCommodityStyles pj) >>=
+ (\j -> journalBalanceTransactions (not $ ignore_assertions_ iopts) $
+ journalAddFile (f, txt) $
+ journalSetLastReadTime t $
+ j)
+ else journalBalanceTransactions (not $ ignore_assertions_ iopts) $
+ journalReverse $
+ journalAddFile (f, txt) $
+ journalApplyCommodityStyles $
+ journalSetLastReadTime t $
+ pj
+ in
+ case fj of
+ Right j -> return j
+ Left e -> throwError e
setYear :: Year -> JournalParser m ()
setYear y = modify' (\j -> j{jparsedefaultyear=Just y})
@@ -268,6 +355,10 @@ getAmountStyle commodity = do
pushDeclaredAccount :: AccountName -> JournalParser m ()
pushDeclaredAccount acct = modify' (\j -> j{jdeclaredaccounts = acct : jdeclaredaccounts j})
+addDeclaredAccountType :: AccountName -> AccountType -> JournalParser m ()
+addDeclaredAccountType acct atype =
+ modify' (\j -> j{jdeclaredaccounttypes = M.insertWith (++) atype [acct] (jdeclaredaccounttypes j)})
+
pushParentAccount :: AccountName -> JournalParser m ()
pushParentAccount acct = modify' (\j -> j{jparseparentaccounts = acct : jparseparentaccounts j})
@@ -345,43 +436,43 @@ datep = do
datep' :: Maybe Year -> TextParser m Day
datep' mYear = do
- startPos <- getPosition
+ startOffset <- getOffset
d1 <- decimal <?> "year or month"
sep <- satisfy isDateSepChar <?> "date separator"
d2 <- decimal <?> "month or day"
- fullDate startPos d1 sep d2 <|> partialDate startPos mYear d1 sep d2
+ fullDate startOffset d1 sep d2 <|> partialDate startOffset mYear d1 sep d2
<?> "full or partial date"
where
- fullDate :: SourcePos -> Integer -> Char -> Int -> TextParser m Day
- fullDate startPos year sep1 month = do
+ fullDate :: Int -> Integer -> Char -> Int -> TextParser m Day
+ fullDate startOffset year sep1 month = do
sep2 <- satisfy isDateSepChar <?> "date separator"
day <- decimal <?> "day"
- endPos <- getPosition
+ endOffset <- getOffset
let dateStr = show year ++ [sep1] ++ show month ++ [sep2] ++ show day
- when (sep1 /= sep2) $ parseErrorAtRegion startPos endPos $
+ when (sep1 /= sep2) $ customFailure $ parseErrorAtRegion startOffset endOffset $
"invalid date (mixing date separators is not allowed): " ++ dateStr
case fromGregorianValid year month day of
- Nothing -> parseErrorAtRegion startPos endPos $
+ Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $
"well-formed but invalid date: " ++ dateStr
Just date -> pure $! date
partialDate
- :: SourcePos -> Maybe Year -> Integer -> Char -> Int -> TextParser m Day
- partialDate startPos mYear month sep day = do
- endPos <- getPosition
+ :: Int -> Maybe Year -> Integer -> Char -> Int -> TextParser m Day
+ partialDate startOffset mYear month sep day = do
+ endOffset <- getOffset
case mYear of
Just year ->
case fromGregorianValid year (fromIntegral month) day of
- Nothing -> parseErrorAtRegion startPos endPos $
+ Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $
"well-formed but invalid date: " ++ dateStr
Just date -> pure $! date
where dateStr = show year ++ [sep] ++ show month ++ [sep] ++ show day
- Nothing -> parseErrorAtRegion startPos endPos $
+ Nothing -> customFailure $ parseErrorAtRegion startOffset endOffset $
"partial date "++dateStr++" found, but the current year is unknown"
where dateStr = show month ++ [sep] ++ show day
@@ -409,26 +500,27 @@ datetimep' mYear = do
where
timeOfDay :: TextParser m TimeOfDay
timeOfDay = do
- pos1 <- getPosition
+ off1 <- getOffset
h' <- twoDigitDecimal <?> "hour"
- pos2 <- getPosition
- unless (h' >= 0 && h' <= 23) $ parseErrorAtRegion pos1 pos2
- "invalid time (bad hour)"
+ off2 <- getOffset
+ unless (h' >= 0 && h' <= 23) $ customFailure $
+ parseErrorAtRegion off1 off2 "invalid time (bad hour)"
char ':' <?> "':' (hour-minute separator)"
- pos3 <- getPosition
+ off3 <- getOffset
m' <- twoDigitDecimal <?> "minute"
- pos4 <- getPosition
- unless (m' >= 0 && m' <= 59) $ parseErrorAtRegion pos3 pos4
- "invalid time (bad minute)"
+ off4 <- getOffset
+ unless (m' >= 0 && m' <= 59) $ customFailure $
+ parseErrorAtRegion off3 off4 "invalid time (bad minute)"
s' <- option 0 $ do
char ':' <?> "':' (minute-second separator)"
- pos5 <- getPosition
+ off5 <- getOffset
s' <- twoDigitDecimal <?> "second"
- pos6 <- getPosition
- unless (s' >= 0 && s' <= 59) $ parseErrorAtRegion pos5 pos6
- "invalid time (bad second)" -- we do not support leap seconds
+ off6 <- getOffset
+ unless (s' >= 0 && s' <= 59) $ customFailure $
+ parseErrorAtRegion off5 off6 "invalid time (bad second)"
+ -- we do not support leap seconds
pure s'
pure $ TimeOfDay h' m' (fromIntegral s')
@@ -477,17 +569,24 @@ modifiedaccountnamep = do
accountnamep :: TextParser m AccountName
accountnamep = singlespacedtextp
--- | Parse any text beginning with a non-whitespace character, until a double space or the end of input.
--- Consumes one of the following spaces, if present.
+
+-- | Parse any text beginning with a non-whitespace character, until a
+-- double space or the end of input.
singlespacedtextp :: TextParser m T.Text
-singlespacedtextp = do
- firstPart <- part
- otherParts <- many $ try $ singlespacep *> part
+singlespacedtextp = singlespacedtextsatisfyingp (const True)
+
+-- | Similar to 'singlespacedtextp', except that the text must only contain
+-- characters satisfying the given predicate.
+singlespacedtextsatisfyingp :: (Char -> Bool) -> TextParser m T.Text
+singlespacedtextsatisfyingp pred = do
+ firstPart <- partp
+ otherParts <- many $ try $ singlespacep *> partp
pure $! T.unwords $ firstPart : otherParts
where
- part = takeWhile1P Nothing (not . isSpace)
+ partp = takeWhile1P Nothing (\c -> pred c && not (isSpace c))
-- | Parse one non-newline whitespace character that is not followed by another one.
+singlespacep :: TextParser m ()
singlespacep = void spacenonewline *> notFollowedBy spacenonewline
--- ** amounts
@@ -524,22 +623,22 @@ amountwithoutpricep = do
suggestedStyle <- getAmountStyle c
commodityspaced <- lift $ skipMany' spacenonewline
sign2 <- lift $ signp
- posBeforeNum <- getPosition
+ offBeforeNum <- getOffset
ambiguousRawNum <- lift rawnumberp
mExponent <- lift $ optional $ try exponentp
- posAfterNum <- getPosition
- let numRegion = (posBeforeNum, posAfterNum)
+ offAfterNum <- getOffset
+ let numRegion = (offBeforeNum, offAfterNum)
(q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent
let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalpoint=mdec, asdigitgroups=mgrps}
return $ Amount c (sign (sign2 q)) NoPrice s mult
rightornosymbolamountp :: Bool -> (Decimal -> Decimal) -> JournalParser m Amount
rightornosymbolamountp mult sign = label "amount" $ do
- posBeforeNum <- getPosition
+ offBeforeNum <- getOffset
ambiguousRawNum <- lift rawnumberp
mExponent <- lift $ optional $ try exponentp
- posAfterNum <- getPosition
- let numRegion = (posBeforeNum, posAfterNum)
+ offAfterNum <- getOffset
+ let numRegion = (offBeforeNum, offAfterNum)
mSpaceAndCommodity <- lift $ optional $ try $ (,) <$> skipMany' spacenonewline <*> commoditysymbolp
case mSpaceAndCommodity of
-- right symbol amount
@@ -563,7 +662,7 @@ amountwithoutpricep = do
-- For reducing code duplication. Doesn't parse anything. Has the type
-- of a parser only in order to throw parse errors (for convenience).
interpretNumber
- :: (SourcePos, SourcePos)
+ :: (Int, Int) -- offsets
-> Maybe AmountStyle
-> Either AmbiguousNumber RawNumber
-> Maybe Int
@@ -571,7 +670,8 @@ amountwithoutpricep = do
interpretNumber posRegion suggestedStyle ambiguousNum mExp =
let rawNum = either (disambiguateNumber suggestedStyle) id ambiguousNum
in case fromRawNumber rawNum mExp of
- Left errMsg -> uncurry parseErrorAtRegion posRegion errMsg
+ Left errMsg -> customFailure $
+ uncurry parseErrorAtRegion posRegion errMsg
Right res -> pure res
-- | Parse an amount from a string, or get an error.
@@ -625,26 +725,18 @@ priceamountp = option NoPrice $ do
pure $ priceConstructor priceAmount
-partialbalanceassertionp :: JournalParser m BalanceAssertion
-partialbalanceassertionp = optional $ do
- sourcepos <- try $ do
- lift (skipMany spacenonewline)
- sourcepos <- genericSourcePos <$> lift getPosition
- char '='
- pure sourcepos
+balanceassertionp :: JournalParser m BalanceAssertion
+balanceassertionp = do
+ sourcepos <- genericSourcePos <$> lift getSourcePos
+ char '='
+ exact <- optional $ try $ char '='
lift (skipMany spacenonewline)
a <- amountp <?> "amount (for a balance assertion or assignment)" -- XXX should restrict to a simple amount
- return (a, sourcepos)
-
--- balanceassertion :: Monad m => TextParser m (Maybe MixedAmount)
--- balanceassertion =
--- try (do
--- lift (skipMany spacenonewline)
--- string "=="
--- lift (skipMany spacenonewline)
--- a <- amountp -- XXX should restrict to a simple amount
--- return $ Just $ Mixed [a])
--- <|> return Nothing
+ return BalanceAssertion
+ { baamount = a
+ , baexact = isJust exact
+ , baposition = sourcepos
+ }
-- http://ledger-cli.org/3.0/doc/ledger3.html#Fixing-Lot-Prices
fixedlotpricep :: JournalParser m (Maybe Amount)
@@ -788,9 +880,10 @@ rawnumberp = label "number" $ do
fail "invalid number (invalid use of separator)"
mExtraFragment <- optional $ lookAhead $ try $
- char ' ' *> getPosition <* digitChar
+ char ' ' *> getOffset <* digitChar
case mExtraFragment of
- Just pos -> parseErrorAt pos "invalid number (excessive trailing digits)"
+ Just off -> customFailure $
+ parseErrorAt off "invalid number (excessive trailing digits)"
Nothing -> pure ()
return $ dbg8 "rawnumberp" rawNumber
@@ -1150,19 +1243,19 @@ commenttagsanddatesp mYear = do
-- default date is provided. A missing year in DATE2 will be inferred
-- from DATE.
--
--- >>> either (Left . parseErrorPretty) Right $ rtp (bracketeddatetagsp Nothing) "[2016/1/2=3/4]"
+-- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[2016/1/2=3/4]"
-- Right [("date",2016-01-02),("date2",2016-03-04)]
--
--- >>> either (Left . parseErrorPretty) Right $ rtp (bracketeddatetagsp Nothing) "[1]"
+-- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[1]"
-- Left ...not a bracketed date...
--
--- >>> either (Left . parseErrorPretty) Right $ rtp (bracketeddatetagsp Nothing) "[2016/1/32]"
--- Left ...1:11:...well-formed but invalid date: 2016/1/32...
+-- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[2016/1/32]"
+-- Left ...1:2:...well-formed but invalid date: 2016/1/32...
--
--- >>> either (Left . parseErrorPretty) Right $ rtp (bracketeddatetagsp Nothing) "[1/31]"
--- Left ...1:6:...partial date 1/31 found, but the current year is unknown...
+-- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[1/31]"
+-- Left ...1:2:...partial date 1/31 found, but the current year is unknown...
--
--- >>> either (Left . parseErrorPretty) Right $ rtp (bracketeddatetagsp Nothing) "[0123456789/-.=/-.=]"
+-- >>> either (Left . customErrorBundlePretty) Right $ rtp (bracketeddatetagsp Nothing) "[0123456789/-.=/-.=]"
-- Left ...1:13:...expecting month or day...
--
bracketeddatetagsp
diff --git a/Hledger/Read/CsvReader.hs b/Hledger/Read/CsvReader.hs
index 34dae98..440ce8e 100644
--- a/Hledger/Read/CsvReader.hs
+++ b/Hledger/Read/CsvReader.hs
@@ -38,7 +38,6 @@ import Control.Monad.Except
import Control.Monad.State.Strict (StateT, get, modify', evalStateT)
import Data.Char (toLower, isDigit, isSpace, ord)
import "base-compat-batteries" Data.List.Compat
-import Data.List.NonEmpty (fromList)
import Data.Maybe
import Data.Ord
import qualified Data.Set as S
@@ -59,12 +58,12 @@ import System.FilePath
import qualified Data.Csv as Cassava
import qualified Data.Csv.Parser.Megaparsec as CassavaMP
import qualified Data.ByteString as B
-import Data.ByteString.Lazy (fromStrict)
+import qualified Data.ByteString.Lazy as BL
import Data.Foldable
import Text.Megaparsec hiding (parse)
import Text.Megaparsec.Char
+import Text.Megaparsec.Custom
import Text.Printf (printf)
-import Data.Word
import Hledger.Data
import Hledger.Utils
@@ -76,7 +75,7 @@ type Record = [Field]
type Field = String
-data CSVError = CSVError (ParseError Word8 CassavaMP.ConversionError)
+data CSVError = CSVError (ParseErrorBundle BL.ByteString CassavaMP.ConversionError)
deriving Show
reader :: Reader
@@ -193,7 +192,7 @@ parseCassava separator path content =
Left msg -> Left $ CSVError msg
Right a -> Right a
where parseResult = fmap parseResultToCsv $ CassavaMP.decodeWith (decodeOptions separator) Cassava.NoHeader path lazyContent
- lazyContent = fromStrict $ T.encodeUtf8 content
+ lazyContent = BL.fromStrict $ T.encodeUtf8 content
decodeOptions :: Char -> Cassava.DecodeOptions
decodeOptions separator = Cassava.defaultDecodeOptions {
@@ -431,19 +430,19 @@ parseAndValidateCsvRules :: FilePath -> T.Text -> ExceptT String IO CsvRules
parseAndValidateCsvRules rulesfile s = do
let rules = parseCsvRules rulesfile s
case rules of
- Left e -> ExceptT $ return $ Left $ parseErrorPretty e
+ Left e -> ExceptT $ return $ Left $ customErrorBundlePretty e
Right r -> do
r_ <- liftIO $ runExceptT $ validateRules r
ExceptT $ case r_ of
- Left s -> return $ Left $ parseErrorPretty $ makeParseError rulesfile s
+ Left s -> return $ Left $ parseErrorPretty $ makeParseError s
Right r -> return $ Right r
where
- makeParseError :: FilePath -> String -> ParseError Char String
- makeParseError f s = FancyError (fromList [initialPos f]) (S.singleton $ ErrorFail s)
+ makeParseError :: String -> ParseError T.Text String
+ makeParseError s = FancyError 0 (S.singleton $ ErrorFail s)
-- | Parse this text as CSV conversion rules. The file path is for error messages.
-parseCsvRules :: FilePath -> T.Text -> Either (ParseError Char CustomErr) CsvRules
+parseCsvRules :: FilePath -> T.Text -> Either (ParseErrorBundle T.Text CustomErr) CsvRules
-- parseCsvRules rulesfile s = runParser csvrulesfile nullrules{baseAccount=takeBaseName rulesfile} rulesfile s
parseCsvRules rulesfile s =
runParser (evalStateT rulesp rules) rulesfile s
@@ -513,7 +512,7 @@ directives =
]
directivevalp :: CsvRulesParser String
-directivevalp = anyChar `manyTill` lift eolof
+directivevalp = anySingle `manyTill` lift eolof
fieldnamelistp :: CsvRulesParser [CsvFieldName]
fieldnamelistp = (do
@@ -588,7 +587,7 @@ assignmentseparatorp = do
fieldvalp :: CsvRulesParser String
fieldvalp = do
lift $ dbgparse 2 "trying fieldvalp"
- anyChar `manyTill` lift eolof
+ anySingle `manyTill` lift eolof
conditionalblockp :: CsvRulesParser ConditionalBlock
conditionalblockp = do
@@ -631,7 +630,7 @@ regexp = do
lift $ dbgparse 3 "trying regexp"
notFollowedBy matchoperatorp
c <- lift nonspace
- cs <- anyChar `manyTill` lift eolof
+ cs <- anySingle `manyTill` lift eolof
return $ strip $ c:cs
-- fieldmatcher = do
@@ -749,10 +748,14 @@ transactionFromCsvRecord sourcepos rules record = t
tcomment = T.pack comment,
tpreceding_comment_lines = T.pack precomment,
tpostings =
- [posting {paccount=account1, pamount=amount1, ptransaction=Just t, pbalanceassertion=balance}
+ [posting {paccount=account1, pamount=amount1, ptransaction=Just t, pbalanceassertion=toAssertion <$> balance}
,posting {paccount=account2, pamount=amount2, ptransaction=Just t}
]
}
+ toAssertion (a, b) = assertion{
+ baamount = a,
+ baposition = b
+ }
getAmountStr :: CsvRules -> CsvRecord -> Maybe String
getAmountStr rules record =
diff --git a/Hledger/Read/JournalReader.hs b/Hledger/Read/JournalReader.hs
index c4024ef..abbff18 100644
--- a/Hledger/Read/JournalReader.hs
+++ b/Hledger/Read/JournalReader.hs
@@ -68,7 +68,7 @@ import qualified Control.Exception as C
import Control.Monad
import Control.Monad.Except (ExceptT(..))
import Control.Monad.State.Strict
-import Data.Bifunctor (first)
+import Data.Maybe
import qualified Data.Map.Strict as M
import Data.Text (Text)
import Data.String
@@ -124,10 +124,10 @@ aliasesFromOpts = map (\a -> fromparse $ runParser accountaliasp ("--alias "++qu
-- | A journal parser. Accumulates and returns a "ParsedJournal",
-- which should be finalised/validated before use.
--
--- >>> rjp (journalp <* eof) "2015/1/1\n a 0\n"
--- Right Journal with 1 transactions, 1 accounts
+-- >>> rejp (journalp <* eof) "2015/1/1\n a 0\n"
+-- Right (Right Journal with 1 transactions, 1 accounts)
--
-journalp :: MonadIO m => JournalParser m ParsedJournal
+journalp :: MonadIO m => ErroringJournalParser m ParsedJournal
journalp = do
many addJournalItemP
eof
@@ -135,7 +135,7 @@ journalp = do
-- | A side-effecting parser; parses any kind of journal item
-- and updates the parse state accordingly.
-addJournalItemP :: MonadIO m => JournalParser m ()
+addJournalItemP :: MonadIO m => ErroringJournalParser m ()
addJournalItemP =
-- all journal line types can be distinguished by the first
-- character, can use choice without backtracking
@@ -154,7 +154,7 @@ addJournalItemP =
-- | Parse any journal directive and update the parse state accordingly.
-- Cf http://hledger.org/manual.html#directives,
-- http://ledger-cli.org/3.0/doc/ledger3.html#Command-Directives
-directivep :: MonadIO m => JournalParser m ()
+directivep :: MonadIO m => ErroringJournalParser m ()
directivep = (do
optional $ char '!'
choice [
@@ -174,78 +174,75 @@ directivep = (do
]
) <?> "directive"
-includedirectivep :: MonadIO m => JournalParser m ()
+includedirectivep :: MonadIO m => ErroringJournalParser m ()
includedirectivep = do
string "include"
lift (skipSome spacenonewline)
filename <- T.unpack <$> takeWhileP Nothing (/= '\n') -- don't consume newline yet
- parentpos <- getPosition
+ parentoff <- getOffset
+ parentpos <- getSourcePos
- filepaths <- getFilePaths parentpos filename
+ filepaths <- getFilePaths parentoff parentpos filename
forM_ filepaths $ parseChild parentpos
void newline
where
- getFilePaths parserpos filename = do
- curdir <- lift $ expandPath (takeDirectory $ sourceName parserpos) ""
+ getFilePaths
+ :: MonadIO m => Int -> SourcePos -> FilePath -> JournalParser m [FilePath]
+ getFilePaths parseroff parserpos filename = do
+ let curdir = takeDirectory (sourceName parserpos)
+ filename' <- lift $ expandHomePath filename
`orRethrowIOError` (show parserpos ++ " locating " ++ filename)
-- Compiling filename as a glob pattern works even if it is a literal
- fileglob <- case tryCompileWith compDefault{errorRecovery=False} filename of
+ fileglob <- case tryCompileWith compDefault{errorRecovery=False} filename' of
Right x -> pure x
- Left e -> parseErrorAt parserpos $ "Invalid glob pattern: " ++ e
+ Left e -> customFailure $
+ parseErrorAt parseroff $ "Invalid glob pattern: " ++ e
-- Get all matching files in the current working directory, sorting in
-- lexicographic order to simulate the output of 'ls'.
filepaths <- liftIO $ sort <$> globDir1 fileglob curdir
if (not . null) filepaths
then pure filepaths
- else parseErrorAt parserpos $ "No existing files match pattern: " ++ filename
+ else customFailure $ parseErrorAt parseroff $
+ "No existing files match pattern: " ++ filename
+ parseChild :: MonadIO m => SourcePos -> FilePath -> ErroringJournalParser m ()
parseChild parentpos filepath = do
- parentfilestack <- fmap sourceName . statePos <$> getParserState
- when (filepath `elem` parentfilestack)
- $ parseErrorAt parentpos ("Cyclic include: " ++ filepath)
-
- childInput <- lift $ readFilePortably filepath
- `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)
-
- -- save parent state
- parentParserState <- getParserState
- parentj <- get
-
- let childj = newJournalWithParseStateFrom parentj
-
- -- set child state
- setInput childInput
- pushPosition $ initialPos filepath
- put childj
-
- -- parse include file
- let parsers = [ journalp
- , timeclockfilep
- , timedotfilep
- ] -- can't include a csv file yet, that reader is special
- updatedChildj <- journalAddFile (filepath, childInput) <$>
- region (withSource childInput) (choiceInState parsers)
-
- -- restore parent state, prepending the child's parse info
- setParserState parentParserState
- put $ updatedChildj <> parentj
- -- discard child's parse info, prepend its (reversed) list data, combine other fields
-
-
-newJournalWithParseStateFrom :: Journal -> Journal
-newJournalWithParseStateFrom j = mempty{
- jparsedefaultyear = jparsedefaultyear j
- ,jparsedefaultcommodity = jparsedefaultcommodity j
- ,jparseparentaccounts = jparseparentaccounts j
- ,jparsealiases = jparsealiases j
- ,jcommodities = jcommodities j
- -- ,jparsetransactioncount = jparsetransactioncount j
- ,jparsetimeclockentries = jparsetimeclockentries j
- }
+ parentj <- get
+
+ let parentfilestack = jincludefilestack parentj
+ when (filepath `elem` parentfilestack) $
+ fail ("Cyclic include: " ++ filepath)
+
+ childInput <- lift $ readFilePortably filepath
+ `orRethrowIOError` (show parentpos ++ " reading " ++ filepath)
+ let initChildj = newJournalWithParseStateFrom filepath parentj
+
+ let parser = choiceInState
+ [ journalp
+ , timeclockfilep
+ , timedotfilep
+ ] -- can't include a csv file yet, that reader is special
+ updatedChildj <- journalAddFile (filepath, childInput) <$>
+ parseIncludeFile parser initChildj filepath childInput
+
+ -- discard child's parse info, combine other fields
+ put $ updatedChildj <> parentj
+
+ newJournalWithParseStateFrom :: FilePath -> Journal -> Journal
+ newJournalWithParseStateFrom filepath j = mempty{
+ jparsedefaultyear = jparsedefaultyear j
+ ,jparsedefaultcommodity = jparsedefaultcommodity j
+ ,jparseparentaccounts = jparseparentaccounts j
+ ,jparsealiases = jparsealiases j
+ ,jcommodities = jcommodities j
+ -- ,jparsetransactioncount = jparsetransactioncount j
+ ,jparsetimeclockentries = jparsetimeclockentries j
+ ,jincludefilestack = filepath : jincludefilestack j
+ }
-- | Lift an IO action into the exception monad, rethrowing any IO
-- error with the given message prepended.
@@ -260,10 +257,30 @@ accountdirectivep :: JournalParser m ()
accountdirectivep = do
string "account"
lift (skipSome spacenonewline)
- acct <- modifiedaccountnamep -- account directives can be modified by alias/apply account
- _ :: Maybe String <- (optional $ lift $ skipSome spacenonewline >> some digitChar) -- compatibility: ignore account codes supported in 1.9/1.10
- newline
+ -- the account name, possibly modified by preceding alias or apply account directives
+ acct <- modifiedaccountnamep
+ -- and maybe something else after two or more spaces ?
+ matype :: Maybe AccountType <- lift $ fmap (fromMaybe Nothing) $ optional $ try $ do
+ skipSome spacenonewline -- at least one more space in addition to the one consumed by modifiedaccountp
+ choice [
+ -- a numeric account code, as supported in 1.9-1.10 ? currently ignored
+ some digitChar >> return Nothing
+ -- a letter account type code (ALERX), as added in 1.11 ?
+ ,char 'A' >> return (Just Asset)
+ ,char 'L' >> return (Just Liability)
+ ,char 'E' >> return (Just Equity)
+ ,char 'R' >> return (Just Revenue)
+ ,char 'X' >> return (Just Expense)
+ ]
+ -- and maybe a comment on this and/or following lines ? (ignore for now)
+ (_cmt, _tags) <- lift transactioncommentp
+ -- and maybe Ledger-style subdirectives ? (ignore)
skipMany indentedlinep
+
+ -- update the journal
+ case matype of
+ Nothing -> return ()
+ Just atype -> addDeclaredAccountType acct atype
pushDeclaredAccount acct
indentedlinep :: JournalParser m String
@@ -284,17 +301,17 @@ commoditydirectivep = commoditydirectiveonelinep <|> commoditydirectivemultiline
-- >>> Right _ <- rjp commoditydirectiveonelinep "commodity $1.00 ; blah\n"
commoditydirectiveonelinep :: JournalParser m ()
commoditydirectiveonelinep = do
- (pos, Amount{acommodity,astyle}) <- try $ do
+ (off, Amount{acommodity,astyle}) <- try $ do
string "commodity"
lift (skipSome spacenonewline)
- pos <- getPosition
+ off <- getOffset
amount <- amountp
- pure $ (pos, amount)
+ pure $ (off, amount)
lift (skipMany spacenonewline)
_ <- lift followingcommentp
let comm = Commodity{csymbol=acommodity, cformat=Just $ dbg2 "style from commodity directive" astyle}
if asdecimalpoint astyle == Nothing
- then parseErrorAt pos pleaseincludedecimalpoint
+ then customFailure $ parseErrorAt off pleaseincludedecimalpoint
else modify' (\j -> j{jcommodities=M.insert acommodity comm $ jcommodities j})
pleaseincludedecimalpoint :: String
@@ -321,15 +338,15 @@ formatdirectivep :: CommoditySymbol -> JournalParser m AmountStyle
formatdirectivep expectedsym = do
string "format"
lift (skipSome spacenonewline)
- pos <- getPosition
+ off <- getOffset
Amount{acommodity,astyle} <- amountp
_ <- lift followingcommentp
if acommodity==expectedsym
then
if asdecimalpoint astyle == Nothing
- then parseErrorAt pos pleaseincludedecimalpoint
+ then customFailure $ parseErrorAt off pleaseincludedecimalpoint
else return $ dbg2 "style from format subdirective" astyle
- else parseErrorAt pos $
+ else customFailure $ parseErrorAt off $
printf "commodity directive symbol \"%s\" and format directive symbol \"%s\" should be the same" expectedsym acommodity
keywordp :: String -> JournalParser m ()
@@ -371,7 +388,7 @@ basicaliasp = do
old <- rstrip <$> (some $ noneOf ("=" :: [Char]))
char '='
skipMany spacenonewline
- new <- rstrip <$> anyChar `manyTill` eolof -- eol in journal, eof in command lines, normally
+ new <- rstrip <$> anySingle `manyTill` eolof -- eol in journal, eof in command lines, normally
return $ BasicAlias (T.pack old) (T.pack new)
regexaliasp :: TextParser m AccountAlias
@@ -383,7 +400,7 @@ regexaliasp = do
skipMany spacenonewline
char '='
skipMany spacenonewline
- repl <- anyChar `manyTill` eolof
+ repl <- anySingle `manyTill` eolof
return $ RegexAlias re repl
endaliasesdirectivep :: JournalParser m ()
@@ -418,11 +435,11 @@ defaultcommoditydirectivep :: JournalParser m ()
defaultcommoditydirectivep = do
char 'D' <?> "default commodity"
lift (skipSome spacenonewline)
- pos <- getPosition
+ off <- getOffset
Amount{acommodity,astyle} <- amountp
lift restofline
if asdecimalpoint astyle == Nothing
- then parseErrorAt pos pleaseincludedecimalpoint
+ then customFailure $ parseErrorAt off pleaseincludedecimalpoint
else setDefaultCommodityAndStyle (acommodity, astyle)
marketpricedirectivep :: JournalParser m MarketPrice
@@ -469,6 +486,14 @@ transactionmodifierp = do
return $ TransactionModifier querytxt postings
-- | Parse a periodic transaction
+--
+-- This reuses periodexprp which parses period expressions on the command line.
+-- This is awkward because periodexprp supports relative and partial dates,
+-- which we don't really need here, and it doesn't support the notion of a
+-- default year set by a Y directive, which we do need to consider here.
+-- We resolve it as follows: in periodic transactions' period expressions,
+-- if there is a default year Y in effect, partial/relative dates are calculated
+-- relative to Y/1/1. If not, they are calculated related to today as usual.
periodictransactionp :: MonadIO m => JournalParser m PeriodicTransaction
periodictransactionp = do
@@ -476,29 +501,49 @@ periodictransactionp = do
char '~' <?> "periodic transaction"
lift $ skipMany spacenonewline
-- a period expression
- pos <- getPosition
- d <- liftIO getCurrentDay
- (periodtxt, (interval, span)) <- lift $ first T.strip <$> match (periodexprp d)
+ off <- getOffset
+
+ -- if there's a default year in effect, use Y/1/1 as base for partial/relative dates
+ today <- liftIO getCurrentDay
+ mdefaultyear <- getYear
+ let refdate = case mdefaultyear of
+ Nothing -> today
+ Just y -> fromGregorian y 1 1
+ periodExcerpt <- lift $ excerpt_ $
+ singlespacedtextsatisfyingp (\c -> c /= ';' && c /= '\n')
+ let periodtxt = T.strip $ getExcerptText periodExcerpt
+
+ -- first parsing with 'singlespacedtextp', then "re-parsing" with
+ -- 'periodexprp' saves 'periodexprp' from having to respect the single-
+ -- and double-space parsing rules
+ (interval, span) <- lift $ reparseExcerpt periodExcerpt $ do
+ pexp <- periodexprp refdate
+ (<|>) eof $ do
+ offset1 <- getOffset
+ void takeRest
+ offset2 <- getOffset
+ customFailure $ parseErrorAtRegion offset1 offset2 $
+ "remainder of period expression cannot be parsed"
+ <> "\nperhaps you need to terminate the period expression with a double space?"
+ pure pexp
+
-- In periodic transactions, the period expression has an additional constraint:
case checkPeriodicTransactionStartDate interval span periodtxt of
- Just e -> parseErrorAt pos e
+ Just e -> customFailure $ parseErrorAt off e
Nothing -> pure ()
-- The line can end here, or it can continue with one or more spaces
-- and then zero or more of the following fields. A bit awkward.
- (status, code, description, (comment, tags)) <-
- (lift eolof >> return (Unmarked, "", "", ("", [])))
- <|>
- (do
- lift $ skipSome spacenonewline
- s <- lift statusp
- c <- lift codep
- desc <- lift $ T.strip <$> descriptionp
- (cmt, ts) <- lift transactioncommentp
+ (status, code, description, (comment, tags)) <- lift $
+ (<|>) (eolof >> return (Unmarked, "", "", ("", []))) $ do
+ skipSome spacenonewline
+ s <- statusp
+ c <- codep
+ desc <- T.strip <$> descriptionp
+ (cmt, ts) <- transactioncommentp
return (s,c,desc,(cmt,ts))
- )
- -- next lines
- postings <- postingsp (Just $ first3 $ toGregorian d)
+ -- next lines; use same year determined above
+ postings <- postingsp (Just $ first3 $ toGregorian refdate)
return $ nullperiodictransaction{
ptperiodexpr=periodtxt
@@ -516,7 +561,7 @@ periodictransactionp = do
transactionp :: JournalParser m Transaction
transactionp = do
-- dbgparse 0 "transactionp"
- startpos <- getPosition
+ startpos <- getSourcePos
date <- datep <?> "transaction"
edate <- optional (lift $ secondarydatep date) <?> "secondary date"
lookAhead (lift spacenonewline <|> newline) <?> "whitespace or newline"
@@ -526,7 +571,7 @@ transactionp = do
(comment, tags) <- lift transactioncommentp
let year = first3 $ toGregorian date
postings <- postingsp (Just year)
- endpos <- getPosition
+ endpos <- getSourcePos
let sourcepos = journalSourcePos startpos endpos
return $ txnTieKnot $ Transaction 0 sourcepos date edate status code description comment tags postings ""
@@ -556,7 +601,8 @@ postingp mTransactionYear = do
let (ptype, account') = (accountNamePostingType account, textUnbracket account)
lift (skipMany spacenonewline)
amount <- option missingmixedamt $ Mixed . (:[]) <$> amountp
- massertion <- partialbalanceassertionp
+ lift (skipMany spacenonewline)
+ massertion <- optional $ balanceassertionp
_ <- fixedlotpricep
lift (skipMany spacenonewline)
(comment,tags,mdate,mdate2) <- lift $ postingcommentp mTransactionYear
@@ -594,8 +640,9 @@ tests_JournalReader = tests "JournalReader" [
test "YYYY.MM.DD" $ expectParse datep "2018.01.01"
test "yearless date with no default year" $ expectParseError datep "1/1" "current year is unknown"
test "yearless date with default year" $ do
- ep <- parseWithState mempty{jparsedefaultyear=Just 2018} datep "1/1"
- either (fail.("parse error at "++).parseErrorPretty) (const ok) ep
+ let s = "1/1"
+ ep <- parseWithState mempty{jparsedefaultyear=Just 2018} datep s
+ either (fail.("parse error at "++).customErrorBundlePretty) (const ok) ep
test "no leading zero" $ expectParse datep "2018/1/1"
,test "datetimep" $ do
@@ -622,40 +669,28 @@ tests_JournalReader = tests "JournalReader" [
ptperiodexpr = "monthly from 2018/6"
,ptinterval = Months 1
,ptspan = DateSpan (Just $ fromGregorian 2018 6 1) Nothing
- ,ptstatus = Unmarked
- ,ptcode = ""
,ptdescription = ""
,ptcomment = "In 2019 we will change this\n"
- ,pttags = []
- ,ptpostings = []
}
- -- TODO #807
- ,_test "more period text in description after two spaces" $ expectParseEq periodictransactionp
+ ,test "more period text in description after two spaces" $ expectParseEq periodictransactionp
"~ monthly from 2018/6 In 2019 we will change this\n"
nullperiodictransaction {
ptperiodexpr = "monthly from 2018/6"
,ptinterval = Months 1
,ptspan = DateSpan (Just $ fromGregorian 2018 6 1) Nothing
- ,ptdescription = "In 2019 we will change this\n"
+ ,ptdescription = "In 2019 we will change this"
+ ,ptcomment = ""
}
- ,_test "more period text in description after one space" $ expectParseEq periodictransactionp
- "~ monthly from 2018/6 In 2019 we will change this\n"
- nullperiodictransaction {
- ptperiodexpr = "monthly from 2018/6"
- ,ptinterval = Months 1
- ,ptspan = DateSpan (Just $ fromGregorian 2018 6 1) Nothing
- ,ptdescription = "In 2019 we will change this\n"
- }
-
- ,_test "Next year in description" $ expectParseEq periodictransactionp
+ ,test "Next year in description" $ expectParseEq periodictransactionp
"~ monthly Next year blah blah\n"
nullperiodictransaction {
ptperiodexpr = "monthly"
,ptinterval = Months 1
,ptspan = DateSpan Nothing Nothing
- ,ptdescription = "Next year blah blah\n"
+ ,ptdescription = "Next year blah blah"
+ ,ptcomment = ""
}
]
@@ -695,6 +730,8 @@ tests_JournalReader = tests "JournalReader" [
,test "quoted commodity symbol with digits" $ expectParse (postingp Nothing) " a 1 \"DE123\"\n"
,test "balance assertion and fixed lot price" $ expectParse (postingp Nothing) " a 1 \"DE123\" =$1 { =2.2 EUR} \n"
+
+ ,test "balance assertion over entire contents of account" $ expectParse (postingp Nothing) " a $1 == $1\n"
]
,tests "transactionmodifierp" [
@@ -703,7 +740,7 @@ tests_JournalReader = tests "JournalReader" [
"= (some value expr)\n some:postings 1.\n"
nulltransactionmodifier {
tmquerytxt = "(some value expr)"
- ,tmpostings = [nullposting{paccount="some:postings", pamount=Mixed[num 1]}]
+ ,tmpostingrules = [nullposting{paccount="some:postings", pamount=Mixed[num 1]}]
}
]
@@ -782,13 +819,16 @@ tests_JournalReader = tests "JournalReader" [
,tests "directivep" [
test "supports !" $ do
- expectParse directivep "!account a\n"
- expectParse directivep "!D 1.0\n"
+ expectParseE directivep "!account a\n"
+ expectParseE directivep "!D 1.0\n"
]
,test "accountdirectivep" $ do
- test "account" $ expectParse accountdirectivep "account a:b\n"
- test "does not support !" $ expectParseError accountdirectivep "!account a:b\n" ""
+ test "with-comment" $ expectParse accountdirectivep "account a:b ; a comment\n"
+ test "does-not-support-!" $ expectParseError accountdirectivep "!account a:b\n" ""
+ test "account-sort-code" $ expectParse accountdirectivep "account a:b 1000\n"
+ test "account-type-code" $ expectParse accountdirectivep "account a:b A\n"
+ test "account-type-tag" $ expectParse accountdirectivep "account a:b ; type:asset\n"
,test "commodityconversiondirectivep" $ do
expectParse commodityconversiondirectivep "C 1h = $50.00\n"
@@ -806,8 +846,8 @@ tests_JournalReader = tests "JournalReader" [
expectParse ignoredpricecommoditydirectivep "N $\n"
,test "includedirectivep" $ do
- test "include" $ expectParseError includedirectivep "include nosuchfile\n" "No existing files match pattern: nosuchfile"
- test "glob" $ expectParseError includedirectivep "include nosuchfile*\n" "No existing files match pattern: nosuchfile*"
+ test "include" $ expectParseErrorE includedirectivep "include nosuchfile\n" "No existing files match pattern: nosuchfile"
+ test "glob" $ expectParseErrorE includedirectivep "include nosuchfile*\n" "No existing files match pattern: nosuchfile*"
,test "marketpricedirectivep" $ expectParseEq marketpricedirectivep
"P 2017/01/30 BTC $922.83\n"
@@ -826,7 +866,7 @@ tests_JournalReader = tests "JournalReader" [
,tests "journalp" [
- test "empty file" $ expectParseEq journalp "" nulljournal
+ test "empty file" $ expectParseEqE journalp "" nulljournal
]
]
diff --git a/Hledger/Read/TimeclockReader.hs b/Hledger/Read/TimeclockReader.hs
index 22bc290..e997b59 100644
--- a/Hledger/Read/TimeclockReader.hs
+++ b/Hledger/Read/TimeclockReader.hs
@@ -58,7 +58,6 @@ import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Text.Megaparsec hiding (parse)
-import Text.Megaparsec.Char
import Hledger.Data
-- XXX too much reuse ?
@@ -78,7 +77,7 @@ reader = Reader
-- format, saving the provided file path and the current time, or give an
-- error.
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
-parse = parseAndFinaliseJournal timeclockfilep
+parse = parseAndFinaliseJournal' timeclockfilep
timeclockfilep :: MonadIO m => JournalParser m ParsedJournal
timeclockfilep = do many timeclockitemp
@@ -105,7 +104,7 @@ timeclockfilep = do many timeclockitemp
-- | Parse a timeclock entry.
timeclockentryp :: JournalParser m TimeclockEntry
timeclockentryp = do
- sourcepos <- genericSourcePos <$> lift getPosition
+ sourcepos <- genericSourcePos <$> lift getSourcePos
code <- oneOf ("bhioO" :: [Char])
lift (skipSome spacenonewline)
datetime <- datetimep
diff --git a/Hledger/Read/TimedotReader.hs b/Hledger/Read/TimedotReader.hs
index 77fb37b..2eafd09 100644
--- a/Hledger/Read/TimedotReader.hs
+++ b/Hledger/Read/TimedotReader.hs
@@ -64,7 +64,7 @@ reader = Reader
-- | Parse and post-process a "Journal" from the timedot format, or give an error.
parse :: InputOpts -> FilePath -> Text -> ExceptT String IO Journal
-parse = parseAndFinaliseJournal timedotfilep
+parse = parseAndFinaliseJournal' timedotfilep
timedotfilep :: JournalParser m ParsedJournal
timedotfilep = do many timedotfileitemp
@@ -104,7 +104,7 @@ timedotdayp = do
timedotentryp :: JournalParser m Transaction
timedotentryp = do
traceParse " timedotentryp"
- pos <- genericSourcePos <$> getPosition
+ pos <- genericSourcePos <$> getSourcePos
lift (skipMany spacenonewline)
a <- modifiedaccountnamep
lift (skipMany spacenonewline)
diff --git a/Hledger/Reports/BudgetReport.hs b/Hledger/Reports/BudgetReport.hs
index b123dde..12dd3d0 100644
--- a/Hledger/Reports/BudgetReport.hs
+++ b/Hledger/Reports/BudgetReport.hs
@@ -341,6 +341,7 @@ budgetReportAsTable
))
-- XXX here for now
+-- TODO: does not work for flat-by-default reports with --flat not specified explicitly
-- | Drop leading components of accounts names as specified by --drop, but only in --flat mode.
maybeAccountNameDrop :: ReportOpts -> AccountName -> AccountName
maybeAccountNameDrop opts a | flat_ opts = accountNameDrop (drop_ opts) a
diff --git a/Hledger/Reports/MultiBalanceReports.hs b/Hledger/Reports/MultiBalanceReports.hs
index 03fd5e1..0ee1bf2 100644
--- a/Hledger/Reports/MultiBalanceReports.hs
+++ b/Hledger/Reports/MultiBalanceReports.hs
@@ -93,15 +93,37 @@ multiBalanceReport opts q j =
depthless = dbg1 "depthless" . filterQuery (not . queryIsDepth)
datelessq = dbg1 "datelessq" $ filterQuery (not . queryIsDateOrDate2) q
dateqcons = if date2_ opts then Date2 else Date
- precedingq = dbg1 "precedingq" $ And [datelessq, dateqcons $ DateSpan Nothing (spanStart reportspan)]
- requestedspan = dbg1 "requestedspan" $ queryDateSpan (date2_ opts) q -- span specified by -b/-e/-p options and query args
- requestedspan' = dbg1 "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan (date2_ opts) j -- if open-ended, close it using the journal's end dates
- intervalspans = dbg1 "intervalspans" $ splitSpan (interval_ opts) requestedspan' -- interval spans enclosing it
- reportspan = dbg1 "reportspan" $ DateSpan (maybe Nothing spanStart $ headMay intervalspans) -- the requested span enlarged to a whole number of intervals
- (maybe Nothing spanEnd $ lastMay intervalspans)
- newdatesq = dbg1 "newdateq" $ dateqcons reportspan
- reportq = dbg1 "reportq" $ depthless $ And [datelessq, newdatesq] -- user's query enlarged to whole intervals and with no depth limit
-
+ -- The date span specified by -b/-e/-p options and query args if any.
+ requestedspan = dbg1 "requestedspan" $ queryDateSpan (date2_ opts) q
+ -- If the requested span is open-ended, close it using the journal's end dates.
+ -- This can still be the null (open) span if the journal is empty.
+ requestedspan' = dbg1 "requestedspan'" $ requestedspan `spanDefaultsFrom` journalDateSpan (date2_ opts) j
+ -- The list of interval spans enclosing the requested span.
+ -- This list can be empty if the journal was empty,
+ -- or if hledger-ui has added its special date:-tomorrow to the query
+ -- and all txns are in the future.
+ intervalspans = dbg1 "intervalspans" $ splitSpan (interval_ opts) requestedspan'
+ -- The requested span enlarged to enclose a whole number of intervals.
+ -- This can be the null span if there were no intervals.
+ reportspan = dbg1 "reportspan" $ DateSpan (maybe Nothing spanStart $ headMay intervalspans)
+ (maybe Nothing spanEnd $ lastMay intervalspans)
+ -- The user's query with no depth limit, and expanded to the report span
+ -- if there is one (otherwise any date queries are left as-is, which
+ -- handles the hledger-ui+future txns case above).
+ reportq = dbg1 "reportq" $ depthless $
+ if reportspan == nulldatespan
+ then q
+ else And [datelessq, reportspandatesq]
+ where
+ reportspandatesq = dbg1 "reportspandatesq" $ dateqcons reportspan
+ -- q projected back before the report start date, to calculate starting balances.
+ -- When there's no report start date, in case there are future txns (the hledger-ui case above),
+ -- we use emptydatespan to make sure they aren't counted as starting balance.
+ startbalq = dbg1 "startbalq" $ And [datelessq, dateqcons precedingspan]
+ where
+ precedingspan = case spanStart reportspan of
+ Just d -> DateSpan Nothing (Just d)
+ Nothing -> emptydatespan
ps :: [Posting] =
dbg1 "ps" $
journalPostings $
@@ -139,7 +161,7 @@ multiBalanceReport opts q j =
-- starting balances and accounts from transactions before the report start date
startacctbals = dbg1 "startacctbals" $ map (\(a,_,_,b) -> (a,b)) startbalanceitems
where
- (startbalanceitems,_) = dbg1 "starting balance report" $ balanceReport opts' precedingq j
+ (startbalanceitems,_) = dbg1 "starting balance report" $ balanceReport opts' startbalq j
where
opts' | tree_ opts = opts{no_elide_=True}
| otherwise = opts{accountlistmode_=ALFlat}
diff --git a/Hledger/Reports/PostingsReport.hs b/Hledger/Reports/PostingsReport.hs
index 1aec322..1a93f09 100644
--- a/Hledger/Reports/PostingsReport.hs
+++ b/Hledger/Reports/PostingsReport.hs
@@ -77,12 +77,12 @@ postingsReport opts q j = (totallabel, items)
historical = balancetype_ opts == HistoricalBalance
precedingsum = sumPostings precedingps
precedingavg | null precedingps = 0
- | otherwise = precedingsum `divideMixedAmount` (fromIntegral $ length precedingps)
+ | otherwise = divideMixedAmount (fromIntegral $ length precedingps) precedingsum
startbal | average_ opts = if historical then precedingavg else 0
| otherwise = if historical then precedingsum else 0
startnum = if historical then length precedingps + 1 else 1
- runningcalc | average_ opts = \i avg amt -> avg + (amt - avg) `divideMixedAmount` (fromIntegral i) -- running average
- | otherwise = \_ bal amt -> bal + amt -- running total
+ runningcalc | average_ opts = \i avg amt -> divideMixedAmount (fromIntegral i) avg + amt - avg -- running average
+ | otherwise = \_ bal amt -> bal + amt -- running total
totallabel = "Total"
diff --git a/Hledger/Reports/ReportOptions.hs b/Hledger/Reports/ReportOptions.hs
index 2710603..17687b8 100644
--- a/Hledger/Reports/ReportOptions.hs
+++ b/Hledger/Reports/ReportOptions.hs
@@ -48,7 +48,7 @@ import Data.Default
import Safe
import System.Console.ANSI (hSupportsANSI)
import System.IO (stdout)
-import Text.Megaparsec.Error
+import Text.Megaparsec.Custom
import Hledger.Data
import Hledger.Query
@@ -240,11 +240,11 @@ beginDatesFromRawOpts d = catMaybes . map (begindatefromrawopt d)
where
begindatefromrawopt d (n,v)
| n == "begin" =
- either (\e -> usageError $ "could not parse "++n++" date: "++parseErrorPretty e) Just $
+ either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
fixSmartDateStrEither' d (T.pack v)
| n == "period" =
case
- either (\e -> usageError $ "could not parse period option: "++parseErrorPretty e) id $
+ either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
parsePeriodExpr d (stripquotes $ T.pack v)
of
(_, DateSpan (Just b) _) -> Just b
@@ -258,11 +258,11 @@ endDatesFromRawOpts d = catMaybes . map (enddatefromrawopt d)
where
enddatefromrawopt d (n,v)
| n == "end" =
- either (\e -> usageError $ "could not parse "++n++" date: "++parseErrorPretty e) Just $
+ either (\e -> usageError $ "could not parse "++n++" date: "++customErrorBundlePretty e) Just $
fixSmartDateStrEither' d (T.pack v)
| n == "period" =
case
- either (\e -> usageError $ "could not parse period option: "++parseErrorPretty e) id $
+ either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) id $
parsePeriodExpr d (stripquotes $ T.pack v)
of
(_, DateSpan _ (Just e)) -> Just e
@@ -276,7 +276,7 @@ intervalFromRawOpts = lastDef NoInterval . catMaybes . map intervalfromrawopt
where
intervalfromrawopt (n,v)
| n == "period" =
- either (\e -> usageError $ "could not parse period option: "++parseErrorPretty e) (Just . fst) $
+ either (\e -> usageError $ "could not parse period option: "++customErrorBundlePretty e) (Just . fst) $
parsePeriodExpr nulldate (stripquotes $ T.pack v) -- reference date does not affect the interval
| n == "daily" = Just $ Days 1
| n == "weekly" = Just $ Weeks 1
diff --git a/Hledger/Utils.hs b/Hledger/Utils.hs
index 3654547..cabeac0 100644
--- a/Hledger/Utils.hs
+++ b/Hledger/Utils.hs
@@ -4,7 +4,7 @@ Standard imports and utilities which are useful everywhere, or needed low
in the module hierarchy. This is the bottom of hledger's module graph.
-}
-{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE OverloadedStrings, LambdaCase #-}
module Hledger.Utils (---- provide these frequently used modules - or not, for clearer api:
-- module Control.Monad,
@@ -143,12 +143,15 @@ applyN n f | n < 1 = id
-- Can raise an error.
expandPath :: FilePath -> FilePath -> IO FilePath -- general type sig for use in reader parsers
expandPath _ "-" = return "-"
-expandPath curdir p = (if isRelative p then (curdir </>) else id) `liftM` expandPath' p
- where
- expandPath' ('~':'/':p) = (</> p) <$> getHomeDirectory
- expandPath' ('~':'\\':p) = (</> p) <$> getHomeDirectory
- expandPath' ('~':_) = ioError $ userError "~USERNAME in paths is not supported"
- expandPath' p = return p
+expandPath curdir p = (if isRelative p then (curdir </>) else id) `liftM` expandHomePath p
+
+-- | Expand user home path indicated by tilde prefix
+expandHomePath :: FilePath -> IO FilePath
+expandHomePath = \case
+ ('~':'/':p) -> (</> p) <$> getHomeDirectory
+ ('~':'\\':p) -> (</> p) <$> getHomeDirectory
+ ('~':_) -> ioError $ userError "~USERNAME in paths is not supported"
+ p -> return p
firstJust ms = case dropWhile (==Nothing) ms of
[] -> Nothing
diff --git a/Hledger/Utils/Debug.hs b/Hledger/Utils/Debug.hs
index 567a3db..131b404 100644
--- a/Hledger/Utils/Debug.hs
+++ b/Hledger/Utils/Debug.hs
@@ -192,12 +192,15 @@ dbg9IO :: (MonadIO m, Show a) => String -> a -> m ()
dbg9IO = ptraceAtIO 9
-- | Log a message and a pretty-printed showable value to ./debug.log, then return it.
+-- Can fail, see plogAt.
plog :: Show a => String -> a -> a
plog = plogAt 0
-- | Log a message and a pretty-printed showable value to ./debug.log,
-- if the global debug level is at or above the specified level.
-- At level 0, always logs. Otherwise, uses unsafePerformIO.
+-- Tends to fail if called more than once, at least when built with -threaded
+-- (Exception: debug.log: openFile: resource busy (file is locked)).
plogAt :: Show a => Int -> String -> a -> a
plogAt lvl
| lvl > 0 && debugLevel < lvl = flip const
@@ -208,13 +211,11 @@ plogAt lvl
| otherwise = " " ++ take (10 - length s) (repeat ' ')
ls' | length ls > 1 = map (" "++) ls
| otherwise = ls
- output = s++":"++nlorspace++intercalate "\n" ls'
+ output = s++":"++nlorspace++intercalate "\n" ls'++"\n"
in unsafePerformIO $ appendFile "debug.log" output >> return a
--- XXX redundant ? More/less robust than log0 ?
+-- XXX redundant ? More/less robust than plogAt ?
-- -- | Like dbg, but writes the output to "debug.log" in the current directory.
--- -- Uses unsafePerformIO. Can fail due to log file contention if called too quickly
--- -- ("*** Exception: debug.log: openFile: resource busy (file is locked)").
-- dbglog :: Show a => String -> a -> a
-- dbglog label a =
-- (unsafePerformIO $
@@ -225,7 +226,7 @@ plogAt lvl
-- (position and next input) to the console. (See also megaparsec's dbg.)
traceParse :: String -> TextParser m ()
traceParse msg = do
- pos <- getPosition
+ pos <- getSourcePos
next <- (T.take peeklength) `fmap` getInput
let (l,c) = (sourceLine pos, sourceColumn pos)
s = printf "at line %2d col %2d: %s" (unPos l) (unPos c) (show next) :: String
diff --git a/Hledger/Utils/Parse.hs b/Hledger/Utils/Parse.hs
index 409ee40..3a7c0bb 100644
--- a/Hledger/Utils/Parse.hs
+++ b/Hledger/Utils/Parse.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
module Hledger.Utils.Parse (
@@ -5,6 +6,7 @@ module Hledger.Utils.Parse (
SimpleTextParser,
TextParser,
JournalParser,
+ ErroringJournalParser,
choice',
choiceInState,
@@ -27,6 +29,7 @@ module Hledger.Utils.Parse (
)
where
+import Control.Monad.Except (ExceptT)
import Control.Monad.State.Strict (StateT, evalStateT)
import Data.Char
import Data.Functor.Identity (Identity(..))
@@ -52,6 +55,11 @@ type TextParser m a = ParsecT CustomErr Text m a
-- | A parser of text in some monad, with a journal as state.
type JournalParser m a = StateT Journal (ParsecT CustomErr Text m) a
+-- | A parser of text in some monad, with a journal as state, that can throw a
+-- "final" parse error that does not backtrack.
+type ErroringJournalParser m a =
+ StateT Journal (ParsecT CustomErr Text (ExceptT FinalParseError m)) a
+
-- | Backtracking choice, use this when alternatives share a prefix.
-- Consumes no input if all choices fail.
choice' :: [TextParser m a] -> TextParser m a
@@ -65,15 +73,21 @@ choiceInState = choice . map try
surroundedBy :: Applicative m => m openclose -> m a -> m a
surroundedBy p = between p p
-parsewith :: Parsec e Text a -> Text -> Either (ParseError Char e) a
+parsewith :: Parsec e Text a -> Text -> Either (ParseErrorBundle Text e) a
parsewith p = runParser p ""
-parsewithString :: Parsec e String a -> String -> Either (ParseError Char e) a
+parsewithString
+ :: Parsec e String a -> String -> Either (ParseErrorBundle String e) a
parsewithString p = runParser p ""
-- | Run a stateful parser with some initial state on a text.
-- See also: runTextParser, runJournalParser.
-parseWithState :: Monad m => st -> StateT st (ParsecT CustomErr Text m) a -> Text -> m (Either (ParseError Char CustomErr) a)
+parseWithState
+ :: Monad m
+ => st
+ -> StateT st (ParsecT CustomErr Text m) a
+ -> Text
+ -> m (Either (ParseErrorBundle Text CustomErr) a)
parseWithState ctx p s = runParserT (evalStateT p ctx) "" s
parseWithState'
@@ -81,19 +95,23 @@ parseWithState'
=> st
-> StateT st (ParsecT e s Identity) a
-> s
- -> (Either (ParseError (Token s) e) a)
+ -> (Either (ParseErrorBundle s e) a)
parseWithState' ctx p s = runParser (evalStateT p ctx) "" s
-fromparse :: (Show t, Show e) => Either (ParseError t e) a -> a
+fromparse
+ :: (Show t, Show (Token t), Show e) => Either (ParseErrorBundle t e) a -> a
fromparse = either parseerror id
-parseerror :: (Show t, Show e) => ParseError t e -> a
+parseerror :: (Show t, Show (Token t), Show e) => ParseErrorBundle t e -> a
parseerror e = error' $ showParseError e
-showParseError :: (Show t, Show e) => ParseError t e -> String
+showParseError
+ :: (Show t, Show (Token t), Show e)
+ => ParseErrorBundle t e -> String
showParseError e = "parse error at " ++ show e
-showDateParseError :: (Show t, Show e) => ParseError t e -> String
+showDateParseError
+ :: (Show t, Show (Token t), Show e) => ParseErrorBundle t e -> String
showDateParseError e = printf "date parse error (%s)" (intercalate ", " $ tail $ lines $ show e)
nonspace :: TextParser m Char
@@ -106,7 +124,7 @@ spacenonewline :: (Stream s, Char ~ Token s) => ParsecT CustomErr s m Char
spacenonewline = satisfy isNonNewlineSpace
restofline :: TextParser m String
-restofline = anyChar `manyTill` newline
+restofline = anySingle `manyTill` newline
eolof :: TextParser m ()
eolof = (newline >> return ()) <|> eof
diff --git a/Hledger/Utils/Test.hs b/Hledger/Utils/Test.hs
index 208d5e6..98f939b 100644
--- a/Hledger/Utils/Test.hs
+++ b/Hledger/Utils/Test.hs
@@ -16,13 +16,18 @@ module Hledger.Utils.Test (
,is
,expectEqPP
,expectParse
+ ,expectParseE
,expectParseError
+ ,expectParseErrorE
,expectParseEq
+ ,expectParseEqE
,expectParseEqOn
+ ,expectParseEqOnE
)
where
import Control.Exception
+import Control.Monad.Except (ExceptT, runExceptT)
import Control.Monad.State.Strict (StateT, evalStateT)
#if !(MIN_VERSION_base(4,11,0))
import Data.Monoid ((<>))
@@ -101,12 +106,34 @@ is = flip expectEqPP
-- | Test that this stateful parser runnable in IO successfully parses
-- all of the given input text, showing the parse error if it fails.
+
-- Suitable for hledger's JournalParser parsers.
expectParse :: (Monoid st, Eq a, Show a, HasCallStack) =>
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> E.Test ()
expectParse parser input = do
ep <- E.io (runParserT (evalStateT (parser <* eof) mempty) "" input)
- either (fail.(++"\n").("\nparse error at "++).parseErrorPretty) (const ok) ep
+ either (fail.(++"\n").("\nparse error at "++).customErrorBundlePretty)
+ (const ok)
+ ep
+
+-- Suitable for hledger's ErroringJournalParser parsers.
+expectParseE
+ :: (Monoid st, Eq a, Show a, HasCallStack)
+ => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
+ -> T.Text
+ -> E.Test ()
+expectParseE parser input = do
+ let filepath = ""
+ eep <- E.io $ runExceptT $
+ runParserT (evalStateT (parser <* eof) mempty) filepath input
+ case eep of
+ Left finalErr ->
+ let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
+ in fail $ "parse error at " <> prettyErr
+ Right ep ->
+ either (fail.(++"\n").("\nparse error at "++).customErrorBundlePretty)
+ (const ok)
+ ep
-- | Test that this stateful parser runnable in IO fails to parse
-- the given input text, with a parse error containing the given string.
@@ -117,22 +144,75 @@ expectParseError parser input errstr = do
case ep of
Right v -> fail $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n"
Left e -> do
- let e' = parseErrorPretty e
+ let e' = customErrorBundlePretty e
if errstr `isInfixOf` e'
then ok
else fail $ "\nparse error is not as expected:\n" ++ e' ++ "\n"
+expectParseErrorE
+ :: (Monoid st, Eq a, Show a, HasCallStack)
+ => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
+ -> T.Text
+ -> String
+ -> E.Test ()
+expectParseErrorE parser input errstr = do
+ let filepath = ""
+ eep <- E.io $ runExceptT $ runParserT (evalStateT parser mempty) filepath input
+ case eep of
+ Left finalErr -> do
+ let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
+ if errstr `isInfixOf` prettyErr
+ then ok
+ else fail $ "\nparse error is not as expected:\n" ++ prettyErr ++ "\n"
+ Right ep -> case ep of
+ Right v -> fail $ "\nparse succeeded unexpectedly, producing:\n" ++ pshow v ++ "\n"
+ Left e -> do
+ let e' = customErrorBundlePretty e
+ if errstr `isInfixOf` e'
+ then ok
+ else fail $ "\nparse error is not as expected:\n" ++ e' ++ "\n"
+
-- | Like expectParse, but also test the parse result is an expected value,
-- pretty-printing both if it fails.
expectParseEq :: (Monoid st, Eq a, Show a, HasCallStack) =>
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> a -> E.Test ()
expectParseEq parser input expected = expectParseEqOn parser input id expected
+expectParseEqE
+ :: (Monoid st, Eq a, Show a, HasCallStack)
+ => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
+ -> T.Text
+ -> a
+ -> E.Test ()
+expectParseEqE parser input expected = expectParseEqOnE parser input id expected
+
-- | Like expectParseEq, but transform the parse result with the given function
-- before comparing it.
expectParseEqOn :: (Monoid st, Eq b, Show b, HasCallStack) =>
StateT st (ParsecT CustomErr T.Text IO) a -> T.Text -> (a -> b) -> b -> E.Test ()
expectParseEqOn parser input f expected = do
ep <- E.io $ runParserT (evalStateT (parser <* eof) mempty) "" input
- either (fail . (++"\n") . ("\nparse error at "++) . parseErrorPretty) (expectEqPP expected . f) ep
+ either (fail . (++"\n") . ("\nparse error at "++) . customErrorBundlePretty)
+ (expectEqPP expected . f)
+ ep
+
+expectParseEqOnE
+ :: (Monoid st, Eq b, Show b, HasCallStack)
+ => StateT st (ParsecT CustomErr T.Text (ExceptT FinalParseError IO)) a
+ -> T.Text
+ -> (a -> b)
+ -> b
+ -> E.Test ()
+expectParseEqOnE parser input f expected = do
+ let filepath = ""
+ eep <- E.io $ runExceptT $
+ runParserT (evalStateT (parser <* eof) mempty) filepath input
+ case eep of
+ Left finalErr ->
+ let prettyErr = finalErrorBundlePretty $ attachSource filepath input finalErr
+ in fail $ "parse error at " <> prettyErr
+ Right ep ->
+ either (fail . (++"\n") . ("\nparse error at "++) . customErrorBundlePretty)
+ (expectEqPP expected . f)
+ ep
diff --git a/Text/Megaparsec/Custom.hs b/Text/Megaparsec/Custom.hs
index 5dce6f7..fece9a6 100644
--- a/Text/Megaparsec/Custom.hs
+++ b/Text/Megaparsec/Custom.hs
@@ -1,33 +1,60 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-} -- new
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE ScopedTypeVariables #-}
-{-# LANGUAGE StandaloneDeriving #-}
+{-# LANGUAGE StandaloneDeriving #-} -- new
module Text.Megaparsec.Custom (
-- * Custom parse error type
CustomErr,
- -- * Throwing custom parse errors
+ -- * Failing with an arbitrary source position
parseErrorAt,
parseErrorAtRegion,
- withSource,
+
+ -- * Re-parsing
+ SourceExcerpt,
+ getExcerptText,
+
+ excerpt_,
+ reparseExcerpt,
-- * Pretty-printing custom parse errors
- customParseErrorPretty
+ customErrorBundlePretty,
+
+
+ -- * "Final" parse errors
+ FinalParseError,
+ FinalParseError',
+ FinalParseErrorBundle,
+ FinalParseErrorBundle',
+
+ -- * Constructing "final" parse errors
+ finalError,
+ finalFancyFailure,
+ finalFail,
+ finalCustomFailure,
+
+ -- * Pretty-printing "final" parse errors
+ finalErrorBundlePretty,
+ attachSource,
+
+ -- * Handling parse errors from include files with "final" parse errors
+ parseIncludeFile,
)
where
import Prelude ()
import "base-compat-batteries" Prelude.Compat hiding (readFile)
+import Control.Monad.Except
+import Control.Monad.State.Strict (StateT, evalStateT)
import Data.Foldable (asum, toList)
import qualified Data.List.NonEmpty as NE
-import Data.Proxy (Proxy (Proxy))
import qualified Data.Set as S
import Data.Text (Text)
-import Data.Void (Void)
import Text.Megaparsec
@@ -39,90 +66,169 @@ import Text.Megaparsec
data CustomErr
-- | Fail with a message at a specific source position interval. The
-- interval must be contained within a single line.
- = ErrorFailAt SourcePos -- Starting position
- Pos -- Ending position (column; same line as start)
+ = ErrorFailAt Int -- Starting offset
+ Int -- Ending offset
String -- Error message
- -- | Attach a source file to a parse error (for error reporting from
- -- include files, e.g. with the 'region' parser combinator)
- | ErrorWithSource Text -- Source file contents
- (ParseError Char CustomErr) -- The original
+ -- | Re-throw parse errors obtained from the "re-parsing" of an excerpt
+ -- of the source text.
+ | ErrorReparsing
+ (NE.NonEmpty (ParseError Text CustomErr)) -- Source fragment parse errors
deriving (Show, Eq, Ord)
-- We require an 'Ord' instance for 'CustomError' so that they may be
-- stored in a 'Set'. The actual instance is inconsequential, so we just
--- derive it, but this requires an (orphan) instance for 'ParseError'.
--- Hopefully this does not cause any trouble.
+-- derive it, but the derived instance requires an (orphan) instance for
+-- 'ParseError'. Hopefully this does not cause any trouble.
+
+deriving instance Ord (ParseError Text CustomErr)
-deriving instance (Ord c, Ord e) => Ord (ParseError c e)
+-- Note: the pretty-printing of our 'CustomErr' type is only partally
+-- defined in its 'ShowErrorComponent' instance; we perform additional
+-- adjustments in 'customErrorBundlePretty'.
instance ShowErrorComponent CustomErr where
showErrorComponent (ErrorFailAt _ _ errMsg) = errMsg
- showErrorComponent (ErrorWithSource _ e) = parseErrorTextPretty e
+ showErrorComponent (ErrorReparsing _) = "" -- dummy value
+ errorComponentLen (ErrorFailAt startOffset endOffset _) =
+ endOffset - startOffset
+ errorComponentLen (ErrorReparsing _) = 1 -- dummy value
---- * Throwing custom parse errors
--- | Fail at a specific source position.
+--- * Failing with an arbitrary source position
-parseErrorAt :: MonadParsec CustomErr s m => SourcePos -> String -> m a
-parseErrorAt pos msg = customFailure (ErrorFailAt pos (sourceColumn pos) msg)
-{-# INLINABLE parseErrorAt #-}
+-- | Fail at a specific source position, given by the raw offset from the
+-- start of the input stream (the number of tokens processed at that
+-- point).
--- | Fail at a specific source interval (within a single line). The
--- interval is inclusive on the left and exclusive on the right; that is,
--- it spans from the start position to just before (and not including) the
--- end position.
+parseErrorAt :: Int -> String -> CustomErr
+parseErrorAt offset msg = ErrorFailAt offset (offset+1) msg
+
+-- | Fail at a specific source interval, given by the raw offsets of its
+-- endpoints from the start of the input stream (the numbers of tokens
+-- processed at those points).
+--
+-- Note that care must be taken to ensure that the specified interval does
+-- not span multiple lines of the input source. This will not be checked.
parseErrorAtRegion
- :: MonadParsec CustomErr s m
- => SourcePos -- ^ Start position
- -> SourcePos -- ^ End position
- -> String -- ^ Error message
- -> m a
-parseErrorAtRegion startPos endPos msg =
- let startCol = sourceColumn startPos
- endCol' = mkPos $ subtract 1 $ unPos $ sourceColumn endPos
- endCol = if startCol <= endCol'
- && sourceLine startPos == sourceLine endPos
- then endCol' else startCol
- in customFailure (ErrorFailAt startPos endCol msg)
-{-# INLINABLE parseErrorAtRegion #-}
-
--- | Attach a source file to a parse error. Intended for use with the
--- 'region' parser combinator.
-
-withSource :: Text -> ParseError Char CustomErr -> ParseError Char CustomErr
-withSource s e =
- FancyError (errorPos e) $ S.singleton $ ErrorCustom $ ErrorWithSource s e
+ :: Int -- ^ Start offset
+ -> Int -- ^ End end offset
+ -> String -- ^ Error message
+ -> CustomErr
+parseErrorAtRegion startOffset endOffset msg =
+ if startOffset < endOffset
+ then ErrorFailAt startOffset endOffset msg
+ else ErrorFailAt startOffset (startOffset+1) msg
---- * Pretty-printing custom parse errors
+--- * Re-parsing
--- | Pretty-print our custom parse errors and display the line on which
--- the parse error occured. Use this instead of 'parseErrorPretty'.
---
--- If any custom errors are present, arbitrarily take the first one (since
--- only one custom error should be used at a time).
+-- | A fragment of source suitable for "re-parsing". The purpose of this
+-- data type is to preserve the content and source position of the excerpt
+-- so that parse errors raised during "re-parsing" may properly reference
+-- the original source.
+
+data SourceExcerpt = SourceExcerpt Int -- Offset of beginning of excerpt
+ Text -- Fragment of source file
+
+-- | Get the raw text of a source excerpt.
-customParseErrorPretty :: Text -> ParseError Char CustomErr -> String
-customParseErrorPretty source err = case findCustomError err of
- Nothing -> customParseErrorPretty' source err pos1
+getExcerptText :: SourceExcerpt -> Text
+getExcerptText (SourceExcerpt _ txt) = txt
- Just (ErrorWithSource customSource customErr) ->
- customParseErrorPretty customSource customErr
+-- | 'excerpt_ p' applies the given parser 'p' and extracts the portion of
+-- the source consumed by 'p', along with the source position of this
+-- portion. This is the only way to create a source excerpt suitable for
+-- "re-parsing" by 'reparseExcerpt'.
- Just (ErrorFailAt sourcePos col errMsg) ->
- let newPositionStack = sourcePos NE.:| NE.tail (errorPos err)
- errorIntervalLength = mkPos $ max 1 $
- unPos col - unPos (sourceColumn sourcePos) + 1
+-- This function could be extended to return the result of 'p', but we don't
+-- currently need this.
- newErr :: ParseError Char Void
- newErr = FancyError newPositionStack (S.singleton (ErrorFail errMsg))
+excerpt_ :: MonadParsec CustomErr Text m => m a -> m SourceExcerpt
+excerpt_ p = do
+ offset <- getOffset
+ (!txt, _) <- match p
+ pure $ SourceExcerpt offset txt
- in customParseErrorPretty' source newErr errorIntervalLength
+-- | 'reparseExcerpt s p' "re-parses" the source excerpt 's' using the
+-- parser 'p'. Parse errors raised by 'p' will be re-thrown at the source
+-- position of the source excerpt.
+--
+-- In order for the correct source file to be displayed when re-throwing
+-- parse errors, we must ensure that the source file during the use of
+-- 'reparseExcerpt s p' is the same as that during the use of 'excerpt_'
+-- that generated the source excerpt 's'. However, we can usually expect
+-- this condition to be satisfied because, at the time of writing, the
+-- only changes of source file in the codebase take place through include
+-- files, and the parser for include files neither accepts nor returns
+-- 'SourceExcerpt's.
+
+reparseExcerpt
+ :: Monad m
+ => SourceExcerpt
+ -> ParsecT CustomErr Text m a
+ -> ParsecT CustomErr Text m a
+reparseExcerpt (SourceExcerpt offset txt) p = do
+ (_, res) <- lift $ runParserT' p (offsetInitialState offset txt)
+ case res of
+ Right result -> pure result
+ Left errBundle -> customFailure $ ErrorReparsing $ bundleErrors errBundle
+
+ where
+ offsetInitialState :: Int -> s -> State s
+ offsetInitialState initialOffset s = State
+ { stateInput = s
+ , stateOffset = initialOffset
+ , statePosState = PosState
+ { pstateInput = s
+ , pstateOffset = initialOffset
+ , pstateSourcePos = initialPos ""
+ , pstateTabWidth = defaultTabWidth
+ , pstateLinePrefix = ""
+ }
+ }
+
+--- * Pretty-printing custom parse errors
+
+-- | Pretty-print our custom parse errors. It is necessary to use this
+-- instead of 'errorBundlePretty' when custom parse errors are thrown.
+--
+-- This function intercepts our custom parse errors and applies final
+-- adjustments ('finalizeCustomError') before passing them to
+-- 'errorBundlePretty'. These adjustments are part of the implementation
+-- of the behaviour of our custom parse errors.
+--
+-- Note: We must ensure that the offset of the 'PosState' of the provided
+-- 'ParseErrorBundle' is no larger than the offset specified by a
+-- 'ErrorFailAt' constructor. This is guaranteed if this offset is set to
+-- 0 (that is, the beginning of the source file), which is the
+-- case for 'ParseErrorBundle's returned from 'runParserT'.
+
+customErrorBundlePretty :: ParseErrorBundle Text CustomErr -> String
+customErrorBundlePretty errBundle =
+ let errBundle' = errBundle { bundleErrors =
+ NE.sortWith errorOffset $ -- megaparsec requires that the list of errors be sorted by their offsets
+ bundleErrors errBundle >>= finalizeCustomError }
+ in errorBundlePretty errBundle'
where
- findCustomError :: ParseError Char CustomErr -> Maybe CustomErr
+ finalizeCustomError
+ :: ParseError Text CustomErr -> NE.NonEmpty (ParseError Text CustomErr)
+ finalizeCustomError err = case findCustomError err of
+ Nothing -> pure err
+
+ Just errFailAt@(ErrorFailAt startOffset _ _) ->
+ -- Adjust the offset
+ pure $ FancyError startOffset $ S.singleton $ ErrorCustom errFailAt
+
+ Just (ErrorReparsing errs) ->
+ -- Extract and finalize the inner errors
+ errs >>= finalizeCustomError
+
+ -- If any custom errors are present, arbitrarily take the first one
+ -- (since only one custom error should be used at a time).
+ findCustomError :: ParseError Text CustomErr -> Maybe CustomErr
findCustomError err = case err of
FancyError _ errSet ->
finds (\case {ErrorCustom e -> Just e; _ -> Nothing}) errSet
@@ -132,117 +238,183 @@ customParseErrorPretty source err = case findCustomError err of
finds f = asum . map f . toList
---- * Modified Megaparsec source
-
--- The below code has been copied from Megaparsec (v.6.4.1,
--- Text.Megaparsec.Error) and modified to suit our needs. These changes are
--- indicated by square brackets. The following copyright notice, conditions,
--- and disclaimer apply to all code below this point.
+--- * "Final" parse errors
--
--- Copyright © 2015–2018 Megaparsec contributors<br>
--- Copyright © 2007 Paolo Martini<br>
--- Copyright © 1999–2000 Daan Leijen
+-- | A type representing "final" parse errors that cannot be backtracked
+-- from and are guaranteed to halt parsing. The anti-backtracking
+-- behaviour is implemented by an 'ExceptT' layer in the parser's monad
+-- stack, using this type as the 'ExceptT' error type.
--
--- All rights reserved.
+-- We have three goals for this type:
+-- (1) it should be possible to convert any parse error into a "final"
+-- parse error,
+-- (2) it should be possible to take a parse error thrown from an include
+-- file and re-throw it in the parent file, and
+-- (3) the pretty-printing of "final" parse errors should be consistent
+-- with that of ordinary parse errors, but should also report a stack of
+-- files for errors thrown from include files.
--
--- Redistribution and use in source and binary forms, with or without
--- modification, are permitted provided that the following conditions are met:
+-- In order to pretty-print a "final" parse error (goal 3), it must be
+-- bundled with include filepaths and its full source text. When a "final"
+-- parse error is thrown from within a parser, we do not have access to
+-- the full source, so we must hold the parse error until it can be joined
+-- with its source (and include filepaths, if it was thrown from an
+-- include file) by the parser's caller.
--
--- * Redistributions of source code must retain the above copyright notice,
--- this list of conditions and the following disclaimer.
+-- A parse error with include filepaths and its full source text is
+-- represented by the 'FinalParseErrorBundle' type, while a parse error in
+-- need of either include filepaths, full source text, or both is
+-- represented by the 'FinalParseError' type.
+
+data FinalParseError' e
+ -- a parse error thrown as a "final" parse error
+ = FinalError (ParseError Text e)
+ -- a parse error obtained from running a parser, e.g. using 'runParserT'
+ | FinalBundle (ParseErrorBundle Text e)
+ -- a parse error thrown from an include file
+ | FinalBundleWithStack (FinalParseErrorBundle' e)
+ deriving (Show)
+
+type FinalParseError = FinalParseError' CustomErr
+
+-- We need a 'Monoid' instance for 'FinalParseError' so that 'ExceptT
+-- FinalParseError m' is an instance of Alternative and MonadPlus, which
+-- is needed to use some parser combinators, e.g. 'many'.
--
--- * Redistributions in binary form must reproduce the above copyright notice,
--- this list of conditions and the following disclaimer in the documentation
--- and/or other materials provided with the distribution.
+-- This monoid instance simply takes the first (left-most) error.
+
+instance Semigroup (FinalParseError' e) where
+ e <> _ = e
+
+instance Monoid (FinalParseError' e) where
+ mempty = FinalError $ FancyError 0 $
+ S.singleton (ErrorFail "default parse error")
+ mappend = (<>)
+
+-- | A type bundling a 'ParseError' with its full source text, filepath,
+-- and stack of include files. Suitable for pretty-printing.
--
--- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS
--- OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
--- OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
--- NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
--- INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
--- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
--- OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
--- LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
--- NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
--- EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-
--- | Pretty-print a 'ParseError Char CustomErr' and display the line on
--- which the parse error occurred. The rendered 'String' always ends with
--- a newline.
-
-customParseErrorPretty'
- :: ( ShowToken (Token s)
- , LineToken (Token s)
- , ShowErrorComponent e
- , Stream s )
- => s -- ^ Original input stream
- -> ParseError (Token s) e -- ^ Parse error to render
- -> Pos -- ^ Length of error interval [added]
- -> String -- ^ Result of rendering
-customParseErrorPretty' = customParseErrorPretty_ defaultTabWidth
-
-
-customParseErrorPretty_
- :: forall s e.
- ( ShowToken (Token s)
- , LineToken (Token s)
- , ShowErrorComponent e
- , Stream s )
- => Pos -- ^ Tab width
- -> s -- ^ Original input stream
- -> ParseError (Token s) e -- ^ Parse error to render
- -> Pos -- ^ Length of error interval [added]
- -> String -- ^ Result of rendering
-customParseErrorPretty_ w s e l =
- sourcePosStackPretty (errorPos e) <> ":\n" <>
- padding <> "|\n" <>
- lineNumber <> " | " <> rline <> "\n" <>
- padding <> "| " <> rpadding <> highlight <> "\n" <> -- [added `highlight`]
- parseErrorTextPretty e
- where
- epos = NE.head (errorPos e) -- [changed from NE.last to NE.head]
- lineNumber = (show . unPos . sourceLine) epos
- padding = replicate (length lineNumber + 1) ' '
- rpadding = replicate (unPos (sourceColumn epos) - 1) ' '
- highlight = replicate (unPos l) '^' -- [added]
- rline =
- case rline' of
- [] -> "<empty line>"
- xs -> expandTab w xs
- rline' = fmap tokenAsChar . chunkToTokens (Proxy :: Proxy s) $
- selectLine (sourceLine epos) s
-
--- | Select a line from input stream given its number.
-
-selectLine
- :: forall s. (LineToken (Token s), Stream s)
- => Pos -- ^ Number of line to select
- -> s -- ^ Input stream
- -> Tokens s -- ^ Selected line
-selectLine l = go pos1
+-- Megaparsec's 'ParseErrorBundle' type already bundles a parse error with
+-- its full source text and filepath, so we just add a stack of include
+-- files.
+
+data FinalParseErrorBundle' e = FinalParseErrorBundle'
+ { finalErrorBundle :: ParseErrorBundle Text e
+ , includeFileStack :: [FilePath]
+ } deriving (Show)
+
+type FinalParseErrorBundle = FinalParseErrorBundle' CustomErr
+
+
+--- * Constructing and throwing final parse errors
+
+-- | Convert a "regular" parse error into a "final" parse error.
+
+finalError :: ParseError Text e -> FinalParseError' e
+finalError = FinalError
+
+-- | Like megaparsec's 'fancyFailure', but as a "final" parse error.
+
+finalFancyFailure
+ :: (MonadParsec e s m, MonadError (FinalParseError' e) m)
+ => S.Set (ErrorFancy e) -> m a
+finalFancyFailure errSet = do
+ offset <- getOffset
+ throwError $ FinalError $ FancyError offset errSet
+
+-- | Like 'fail', but as a "final" parse error.
+
+finalFail
+ :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => String -> m a
+finalFail = finalFancyFailure . S.singleton . ErrorFail
+
+-- | Like megaparsec's 'customFailure', but as a "final" parse error.
+
+finalCustomFailure
+ :: (MonadParsec e s m, MonadError (FinalParseError' e) m) => e -> m a
+finalCustomFailure = finalFancyFailure . S.singleton . ErrorCustom
+
+
+--- * Pretty-printing "final" parse errors
+
+-- | Pretty-print a "final" parse error: print the stack of include files,
+-- then apply the pretty-printer for parse error bundles. Note that
+-- 'attachSource' must be used on a "final" parse error before it can be
+-- pretty-printed.
+
+finalErrorBundlePretty :: FinalParseErrorBundle' CustomErr -> String
+finalErrorBundlePretty bundle =
+ concatMap showIncludeFilepath (includeFileStack bundle)
+ <> customErrorBundlePretty (finalErrorBundle bundle)
where
- go !n !s =
- if n == l
- then fst (takeWhile_ notNewline s)
- else go (n <> pos1) (stripNewline $ snd (takeWhile_ notNewline s))
- notNewline = not . tokenIsNewline
- stripNewline s =
- case take1_ s of
- Nothing -> s
- Just (_, s') -> s'
-
--- | Replace tab characters with given number of spaces.
-
-expandTab
- :: Pos
- -> String
- -> String
-expandTab w' = go 0
+ showIncludeFilepath path = "in file included from " <> path <> ",\n"
+
+-- | Supply a filepath and source text to a "final" parse error so that it
+-- can be pretty-printed. You must ensure that you provide the appropriate
+-- source text and filepath.
+
+attachSource
+ :: FilePath -> Text -> FinalParseError' e -> FinalParseErrorBundle' e
+attachSource filePath sourceText finalParseError = case finalParseError of
+
+ -- A parse error thrown directly with the 'FinalError' constructor
+ -- requires both source and filepath.
+ FinalError parseError ->
+ let bundle = ParseErrorBundle
+ { bundleErrors = parseError NE.:| []
+ , bundlePosState = initialPosState filePath sourceText }
+ in FinalParseErrorBundle'
+ { finalErrorBundle = bundle
+ , includeFileStack = [] }
+
+ -- A 'ParseErrorBundle' already has the appropriate source and filepath
+ -- and so needs neither.
+ FinalBundle peBundle -> FinalParseErrorBundle'
+ { finalErrorBundle = peBundle
+ , includeFileStack = [] }
+
+ -- A parse error from a 'FinalParseErrorBundle' was thrown from an
+ -- include file, so we add the filepath to the stack.
+ FinalBundleWithStack fpeBundle -> fpeBundle
+ { includeFileStack = filePath : includeFileStack fpeBundle }
+
+
+--- * Handling parse errors from include files with "final" parse errors
+
+-- | Parse a file with the given parser and initial state, discarding the
+-- final state and re-throwing any parse errors as "final" parse errors.
+
+parseIncludeFile
+ :: Monad m
+ => StateT st (ParsecT CustomErr Text (ExceptT FinalParseError m)) a
+ -> st
+ -> FilePath
+ -> Text
+ -> StateT st (ParsecT CustomErr Text (ExceptT FinalParseError m)) a
+parseIncludeFile parser initialState filepath text =
+ catchError parser' handler
where
- go 0 [] = []
- go 0 ('\t':xs) = go w xs
- go 0 (x:xs) = x : go 0 xs
- go !n xs = ' ' : go (n - 1) xs
- w = unPos w'
-
+ parser' = do
+ eResult <- lift $ lift $
+ runParserT (evalStateT parser initialState) filepath text
+ case eResult of
+ Left parseErrorBundle -> throwError $ FinalBundle parseErrorBundle
+ Right result -> pure result
+
+ -- Attach source and filepath of the include file to its parse errors
+ handler e = throwError $ FinalBundleWithStack $ attachSource filepath text e
+
+
+--- * Helpers
+
+-- Like megaparsec's 'initialState', but instead for 'PosState'. Used when
+-- constructing 'ParseErrorBundle's. The values for "tab width" and "line
+-- prefix" are taken from 'initialState'.
+
+initialPosState :: FilePath -> Text -> PosState Text
+initialPosState filePath sourceText = PosState
+ { pstateInput = sourceText
+ , pstateOffset = 0
+ , pstateSourcePos = initialPos filePath
+ , pstateTabWidth = defaultTabWidth
+ , pstateLinePrefix = "" }
diff --git a/hledger-lib.cabal b/hledger-lib.cabal
index 7a8d947..9e0a152 100644
--- a/hledger-lib.cabal
+++ b/hledger-lib.cabal
@@ -1,11 +1,13 @@
--- This file has been generated from package.yaml by hpack version 0.28.2.
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.31.0.
--
-- see: https://github.com/sol/hpack
--
--- hash: 9a9cfa4db514bd283b078f02541d8bd7fa0249dc63085faedd9e1b5840bf7853
+-- hash: 8e0ce73c7c86c909a78d4d06e8566f8b66bc1df89f508da0b05df073c4ecd7c9
name: hledger-lib
-version: 1.11.1
+version: 1.12
synopsis: Core data types, parsers and functionality for the hledger accounting tools
description: This is a reusable library containing hledger's core functionality.
.
@@ -25,22 +27,21 @@ license: GPL-3
license-file: LICENSE
tested-with: GHC==7.10.3, GHC==8.0.2, GHC==8.2.2, GHC==8.4.3
build-type: Simple
-cabal-version: >= 1.10
extra-source-files:
CHANGES
+ README
hledger_csv.5
- hledger_csv.info
hledger_csv.txt
+ hledger_csv.info
hledger_journal.5
- hledger_journal.info
hledger_journal.txt
- hledger_timeclock.5
- hledger_timeclock.info
- hledger_timeclock.txt
+ hledger_journal.info
hledger_timedot.5
- hledger_timedot.info
hledger_timedot.txt
- README
+ hledger_timedot.info
+ hledger_timeclock.5
+ hledger_timeclock.txt
+ hledger_timeclock.info
source-repository head
type: git
@@ -106,7 +107,7 @@ library
, Glob >=0.9
, ansi-terminal >=0.6.2.3
, array
- , base >=4.8 && <4.12
+ , base >=4.8 && <4.13
, base-compat-batteries >=0.10.1 && <0.11
, blaze-markup >=0.5.1
, bytestring
@@ -122,7 +123,7 @@ library
, extra
, filepath
, hashtables >=1.2.3.1
- , megaparsec >=6.4.1 && <7
+ , megaparsec >=7.0.0 && <8
, mtl
, mtl-compat
, old-time
@@ -205,7 +206,7 @@ test-suite doctests
, Glob >=0.7
, ansi-terminal >=0.6.2.3
, array
- , base >=4.8 && <4.12
+ , base >=4.8 && <4.13
, base-compat-batteries >=0.10.1 && <0.11
, blaze-markup >=0.5.1
, bytestring
@@ -222,7 +223,7 @@ test-suite doctests
, extra
, filepath
, hashtables >=1.2.3.1
- , megaparsec >=6.4.1 && <7
+ , megaparsec >=7.0.0 && <8
, mtl
, mtl-compat
, old-time
@@ -305,7 +306,7 @@ test-suite easytests
, Glob >=0.9
, ansi-terminal >=0.6.2.3
, array
- , base >=4.8 && <4.12
+ , base >=4.8 && <4.13
, base-compat-batteries >=0.10.1 && <0.11
, blaze-markup >=0.5.1
, bytestring
@@ -322,7 +323,7 @@ test-suite easytests
, filepath
, hashtables >=1.2.3.1
, hledger-lib
- , megaparsec >=6.4.1 && <7
+ , megaparsec >=7.0.0 && <8
, mtl
, mtl-compat
, old-time
diff --git a/hledger_csv.5 b/hledger_csv.5
index a4c19e7..b04c67a 100644
--- a/hledger_csv.5
+++ b/hledger_csv.5
@@ -1,5 +1,5 @@
-.TH "hledger_csv" "5" "September 2018" "hledger 1.11.1" "hledger User Manuals"
+.TH "hledger_csv" "5" "December 2018" "hledger 1.12" "hledger User Manuals"
diff --git a/hledger_csv.info b/hledger_csv.info
index 61e5b68..d8ea27c 100644
--- a/hledger_csv.info
+++ b/hledger_csv.info
@@ -3,8 +3,8 @@ This is hledger_csv.info, produced by makeinfo version 6.5 from stdin.

File: hledger_csv.info, Node: Top, Next: CSV RULES, Up: (dir)
-hledger_csv(5) hledger 1.11.1
-*****************************
+hledger_csv(5) hledger 1.12
+***************************
hledger can read CSV (comma-separated value) files as if they were
journal files, automatically converting each CSV record into a
@@ -317,33 +317,33 @@ one rules file will be used for all the CSV files being read.

Tag Table:
Node: Top72
-Node: CSV RULES2167
-Ref: #csv-rules2275
-Node: skip2537
-Ref: #skip2631
-Node: date-format2803
-Ref: #date-format2930
-Node: field list3436
-Ref: #field-list3573
-Node: field assignment4278
-Ref: #field-assignment4433
-Node: conditional block4937
-Ref: #conditional-block5091
-Node: include5987
-Ref: #include6117
-Node: newest-first6348
-Ref: #newest-first6462
-Node: CSV TIPS6873
-Ref: #csv-tips6967
-Node: CSV ordering7085
-Ref: #csv-ordering7203
-Node: CSV accounts7384
-Ref: #csv-accounts7522
-Node: CSV amounts7776
-Ref: #csv-amounts7922
-Node: CSV balance assertions8697
-Ref: #csv-balance-assertions8879
-Node: Reading multiple CSV files9084
-Ref: #reading-multiple-csv-files9254
+Node: CSV RULES2163
+Ref: #csv-rules2271
+Node: skip2533
+Ref: #skip2627
+Node: date-format2799
+Ref: #date-format2926
+Node: field list3432
+Ref: #field-list3569
+Node: field assignment4274
+Ref: #field-assignment4429
+Node: conditional block4933
+Ref: #conditional-block5087
+Node: include5983
+Ref: #include6113
+Node: newest-first6344
+Ref: #newest-first6458
+Node: CSV TIPS6869
+Ref: #csv-tips6963
+Node: CSV ordering7081
+Ref: #csv-ordering7199
+Node: CSV accounts7380
+Ref: #csv-accounts7518
+Node: CSV amounts7772
+Ref: #csv-amounts7918
+Node: CSV balance assertions8693
+Ref: #csv-balance-assertions8875
+Node: Reading multiple CSV files9080
+Ref: #reading-multiple-csv-files9250

End Tag Table
diff --git a/hledger_csv.txt b/hledger_csv.txt
index 20bd12b..80b1bf7 100644
--- a/hledger_csv.txt
+++ b/hledger_csv.txt
@@ -249,4 +249,4 @@ SEE ALSO
-hledger 1.11.1 September 2018 hledger_csv(5)
+hledger 1.12 December 2018 hledger_csv(5)
diff --git a/hledger_journal.5 b/hledger_journal.5
index a3bdf37..c0fa106 100644
--- a/hledger_journal.5
+++ b/hledger_journal.5
@@ -1,6 +1,6 @@
.\"t
-.TH "hledger_journal" "5" "September 2018" "hledger 1.11.1" "hledger User Manuals"
+.TH "hledger_journal" "5" "December 2018" "hledger 1.12" "hledger User Manuals"
@@ -547,16 +547,57 @@ Use include or concatenate the files instead.
The asserted balance must be a simple single\-commodity amount, and in
fact the assertion checks only this commodity's balance within the
(possibly multi\-commodity) account balance.
-We could call this a partial balance assertion.
-This is compatible with Ledger, and makes it possible to make assertions
-about accounts containing multiple commodities.
-.PP
-To assert each commodity's balance in such a multi\-commodity account,
-you can add multiple postings (with amount 0 if necessary).
-But note that no matter how many assertions you add, you can't be sure
-the account does not contain some unexpected commodity.
-(We'll add support for this kind of total balance assertion if there's
-demand.)
+.PD 0
+.P
+.PD
+This is how assertions work in Ledger also.
+We could call this a \[lq]partial\[rq] balance assertion.
+.PP
+To assert the balance of more than one commodity in an account, you can
+write multiple postings, each asserting one commodity's balance.
+.PP
+You can make a stronger kind of balance assertion, by writing a double
+equals sign (\f[C]==EXPECTEDBALANCE\f[]).
+This \[lq]complete\[rq] balance assertion asserts the absence of other
+commodities (or, that their balance is 0, which to hledger is
+equivalent.)
+.IP
+.nf
+\f[C]
+2013/1/1
+\ \ a\ \ \ $1
+\ \ a\ \ \ \ 1€
+\ \ b\ \ $\-1
+\ \ c\ \ \ \-1€
+
+2013/1/2\ \ ;\ These\ assertions\ succeed
+\ \ a\ \ \ \ 0\ \ =\ \ $1
+\ \ a\ \ \ \ 0\ \ =\ \ \ 1€
+\ \ b\ \ \ \ 0\ ==\ $\-1
+\ \ c\ \ \ \ 0\ ==\ \ \-1€
+
+2013/1/3\ \ ;\ This\ assertion\ fails\ as\ \[aq]a\[aq]\ also\ contains\ 1€
+\ \ a\ \ \ \ 0\ ==\ \ $1
+\f[]
+.fi
+.PP
+It's not yet possible to make a complete assertion about a balance that
+has multiple commodities.
+One workaround is to isolate each commodity into its own subaccount:
+.IP
+.nf
+\f[C]
+2013/1/1
+\ \ a:usd\ \ \ $1
+\ \ a:euro\ \ \ 1€
+\ \ b
+
+2013/1/2
+\ \ a\ \ \ \ \ \ \ \ 0\ ==\ \ 0
+\ \ a:usd\ \ \ \ 0\ ==\ $1
+\ \ a:euro\ \ \ 0\ ==\ \ 1€
+\f[]
+.fi
.SS Assertions and subaccounts
.PP
Balance assertions do not count the balance from subaccounts; they check
@@ -853,10 +894,9 @@ T}@T{
T}@T{
any text
T}@T{
-declare an account name & optional account code
+document account names, declare account types & display order
T}@T{
-account code: balance reports (except \f[C]balance\f[] single\-column
-mode)
+all entries in all files, before or after
T}
T{
\f[C]alias\f[]
@@ -951,13 +991,8 @@ lw(8.9n) lw(61.1n).
T{
subdirective
T}@T{
-optional indented directive or unparsed text lines immediately following
-a parent directive
-T}
-T{
-account code
-T}@T{
-numeric code influencing account display order in most balance reports
+optional indented directive line immediately following a parent
+directive
T}
T{
number notation
@@ -1143,58 +1178,108 @@ The \f[C]\-V/\-\-value\f[] flag can be used to convert reported amounts
to another commodity using these prices.
.SS Declaring accounts
.PP
-The \f[C]account\f[] directive predeclares account names.
-The simplest form is \f[C]account\ ACCTNAME\f[], eg:
+\f[C]account\f[] directives can be used to pre\-declare some or all
+accounts.
+Though not required, they can provide several benefits:
+.IP \[bu] 2
+They can document your intended chart of accounts, providing a
+reference.
+.IP \[bu] 2
+They can store extra information about accounts (account numbers, notes,
+etc.)
+.IP \[bu] 2
+They can help hledger know your accounts' types (asset, liability,
+equity, revenue, expense), useful for reports like balancesheet and
+incomestatement.
+.IP \[bu] 2
+They control account display order in reports, allowing non\-alphabetic
+sorting (eg Revenues to appear above Expenses).
+.IP \[bu] 2
+They help with account name completion in the add command,
+hledger\-iadd, hledger\-web, ledger\-mode etc.
+.PP
+Here is the full syntax:
+.IP
+.nf
+\f[C]
+account\ ACCTNAME\ \ [ACCTTYPE]
+\ \ [COMMENTS]
+\f[]
+.fi
+.PP
+The simplest form just declares a hledger\-style account name, eg:
.IP
.nf
\f[C]
account\ assets:bank:checking
\f[]
.fi
+.SS Account types
.PP
-Currently this mainly helps with account name autocompletion in eg
-hledger add, hledger\-iadd, hledger\-web, and ledger\-mode.
-.PD 0
-.P
-.PD
-In future it will also help detect misspelled accounts.
+hledger recognises five types of account: asset, liability, equity,
+revenue, expense.
+This is useful for certain accounting\-aware reports, in particular
+balancesheet, incomestatement and cashflow.
.PP
-An account directive can also have indented subdirectives following it,
-which are currently ignored.
-Here is the full syntax:
+If you name your top\-level accounts with some variation of
+\f[C]assets\f[], \f[C]liabilities\f[]/\f[C]debts\f[], \f[C]equity\f[],
+\f[C]revenues\f[]/\f[C]income\f[], or \f[C]expenses\f[], their types are
+detected automatically.
+.PP
+More generally, you can declare an account's type by adding one of the
+letters \f[C]ALERX\f[] to its account directive, separated from the
+account name by two or more spaces.
+Eg:
+.IP
+.nf
+\f[C]
+account\ assets\ \ \ \ \ \ \ A
+account\ liabilities\ \ L
+account\ equity\ \ \ \ \ \ \ E
+account\ revenues\ \ \ \ \ R
+account\ expenses\ \ \ \ \ X
+\f[]
+.fi
+.PP
+Note: if you ever override the types of those auto\-detected english
+account names mentioned above, you might need to help the reports a bit:
.IP
.nf
\f[C]
-;\ account\ ACCTNAME
-;\ \ \ [OPTIONALSUBDIRECTIVES]
+;\ make\ "liabilities"\ not\ have\ the\ liability\ type,\ who\ knows\ why
+account\ liabilities\ \ \ E
-account\ assets:bank:checking
-\ \ a\ comment
-\ \ some\-tag:12345
+;\ better\ ensure\ some\ other\ account\ has\ the\ liability\ type,\
+;\ otherwise\ balancesheet\ would\ still\ show\ "liabilities"\ under\ Liabilities\
+account\ \-\ \ \ \ \ \ \ \ \ \ \ \ \ L
\f[]
.fi
-.SS Account display order
.PP
-Account directives have another purpose: they set the order in which
-accounts are displayed, in hledger reports, hledger\-ui accounts screen,
-hledger\-web sidebar etc.
-For example, say you have these top\-level accounts:
+)
+.SS Account comments
+.PP
+An account directive can also have indented comments on following lines,
+eg:
.IP
.nf
\f[C]
-$\ accounts\ \-1
-assets
-equity
-expenses
-liabilities
-misc
-other
-revenues
+account\ assets:bank:checking
+\ \ ;\ acctno:12345
+\ \ ;\ a\ comment
\f[]
.fi
.PP
-By default, they are displayed in alphabetical order.
-But if you add the following account directives to the journal:
+We also allow (and ignore) Ledger\-style subdirectives, with no leading
+semicolon, for compatibility.
+.PP
+Tags in account comments, like \f[C]acctno\f[] above, currently have no
+effect.
+.SS Account display order
+.PP
+Account directives also set the order in which accounts are displayed in
+reports, the hledger\-ui accounts screen, the hledger\-web sidebar, etc.
+Normally accounts are listed in alphabetical order, but if you have eg
+these account directives in the journal:
.IP
.nf
\f[C]
@@ -1206,27 +1291,25 @@ account\ expenses
\f[]
.fi
.PP
-the display order changes to:
+you'll see those accounts listed in declaration order, not
+alphabetically:
.IP
.nf
\f[C]
-$\ accounts\ \-1
+$\ hledger\ accounts\ \-1
assets
liabilities
equity
revenues
expenses
-misc
-other
\f[]
.fi
.PP
-Ie, declared accounts first, in the order they were declared, followed
-by any undeclared accounts in alphabetic order.
+Undeclared accounts, if any, are displayed last, in alphabetical order.
.PP
Note that sorting is done at each level of the account tree (within each
group of sibling accounts under the same parent).
-This directive:
+And currently, this directive:
.IP
.nf
\f[C]
@@ -1237,6 +1320,10 @@ account\ other:zoo
would influence the position of \f[C]zoo\f[] among \f[C]other\f[]'s
subaccounts, but not the position of \f[C]other\f[] among the top\-level
accounts.
+This means: \- you will sometimes declare parent accounts (eg
+\f[C]account\ other\f[] above) that you don't intend to post to, just to
+customize their display order \- sibling accounts stay together (you
+couldn't display \f[C]x:y\f[] in between \f[C]a:b\f[] and \f[C]a:c\f[]).
.SS Rewriting accounts
.PP
You can define account alias rules which rewrite your account names, or
@@ -1419,16 +1506,25 @@ date must fall on a natural boundary of the interval.
Eg \f[C]monthly\ from\ 2018/1/1\f[] is valid, but
\f[C]monthly\ from\ 2018/1/15\f[] is not.
.PP
-If you write a transaction description or same\-line comment, it must be
-separated from the period expression by \f[B]two or more spaces\f[].
-Eg:
+Partial or relative dates (M/D, D, tomorrow, last week) in the period
+expression can work (useful or not).
+They will be relative to today's date, unless a Y default year directive
+is in effect, in which case they will be relative to Y/1/1.
+.PP
+Period expressions must be terminated by \f[B]two or more spaces\f[] if
+followed by additional fields.
+For example, the periodic transaction given below includes a transaction
+description \[lq]paycheck\[rq], which is separated from the period
+expression by a double space.
+If not for the second space, hledger would attempt (and fail) to parse
+\[lq]paycheck\[rq] as a part of the period expression.
.IP
.nf
\f[C]
-;\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 2\ or\ more\ spaces
-;\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ||
-;\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ vv
-~\ every\ 2\ weeks\ from\ 2018/6\ to\ 2018/9\ \ paycheck
+;\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ 2\ or\ more\ spaces
+;\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ ||
+;\ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ vv
+~\ every\ 2\ weeks\ from\ 2018/6/4\ to\ 2018/9\ \ paycheck
\ \ \ \ assets:bank:checking\ \ \ $1500
\ \ \ \ income:acme\ inc
\f[]
@@ -1495,50 +1591,81 @@ Currently, this means adding extra postings (also known as
\[lq]automated postings\[rq]).
Transaction modifiers are enabled by the \f[C]\-\-auto\f[] flag.
.PP
-A transaction modifier rule looks a bit like a normal journal entry,
-except the first line is an equal sign (\f[C]=\f[]) followed by a query
-(mnemonic: \f[C]=\f[] suggests matching something.):
+A transaction modifier rule looks quite like a normal transaction,
+except the first line is an equals sign followed by a query that matches
+certain postings (mnemonic: \f[C]=\f[] suggests matching).
+And each \[lq]posting\[rq] is actually a posting\-generating rule:
.IP
.nf
\f[C]
-=\ expenses:gifts
-\ \ \ \ budget:gifts\ \ *\-1
-\ \ \ \ assets:budget\ \ *1
+=\ QUERY
+\ \ \ \ ACCT\ \ AMT
+\ \ \ \ ACCT\ \ [AMT]
+\ \ \ \ ...
\f[]
.fi
.PP
-The posting amounts can be of the form \f[C]*N\f[], which means \[lq]the
-amount of the matched transaction's first posting, multiplied by N\[rq].
-They can also be ordinary fixed amounts.
-Fixed amounts with no commodity symbol will be given the same commodity
-as the matched transaction's first posting.
+The posting rules look just like normal postings, except the amount can
+be:
+.IP \[bu] 2
+a normal amount with a commodity symbol, eg \f[C]$2\f[].
+This will be used as\-is.
+.IP \[bu] 2
+a number, eg \f[C]2\f[].
+The commodity symbol (if any) from the matched posting will be added to
+this.
+.IP \[bu] 2
+a numeric multiplier, eg \f[C]*2\f[] (a star followed by a number N).
+The matched posting's amount (and total price, if any) will be
+multiplied by N.
+.IP \[bu] 2
+a multiplier with a commodity symbol, eg \f[C]*$2\f[] (a star, number N,
+and symbol S).
+The matched posting's amount will be multiplied by N, and its commodity
+symbol will be replaced with S.
.PP
-This example adds a corresponding (unbalanced) budget posting to every
-transaction involving the \f[C]expenses:gifts\f[] account:
+Some examples:
.IP
.nf
\f[C]
+;\ every\ time\ I\ buy\ food,\ schedule\ a\ dollar\ donation
+=\ expenses:food
+\ \ \ \ (liabilities:charity)\ \ \ $\-1
+
+;\ when\ I\ buy\ a\ gift,\ also\ deduct\ that\ amount\ from\ a\ budget\ envelope\ subaccount
=\ expenses:gifts
-\ \ \ \ (budget:gifts)\ \ *\-1
+\ \ \ \ assets:checking:gifts\ \ *\-1
+\ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ *1
-2017\-12\-14
-\ \ expenses:gifts\ \ $20
-\ \ assets
+2017/12/1
+\ \ expenses:food\ \ \ \ $10
+\ \ assets:checking
+
+2017/12/14
+\ \ expenses:gifts\ \ \ $20
+\ \ assets:checking
\f[]
.fi
.IP
.nf
\f[C]
$\ hledger\ print\ \-\-auto
+2017/12/01
+\ \ \ \ expenses:food\ \ \ \ \ \ \ \ \ \ \ \ \ \ $10
+\ \ \ \ assets:checking
+\ \ \ \ (liabilities:charity)\ \ \ \ \ \ $\-1
+
2017/12/14
\ \ \ \ expenses:gifts\ \ \ \ \ \ \ \ \ \ \ \ \ $20
-\ \ \ \ (budget:gifts)\ \ \ \ \ \ \ \ \ \ \ \ $\-20
-\ \ \ \ assets
+\ \ \ \ assets:checking
+\ \ \ \ assets:checking:gifts\ \ \ \ \ \-$20
+\ \ \ \ assets:checking\ \ \ \ \ \ \ \ \ \ \ \ $20
\f[]
.fi
.PP
-Like postings recorded by hand, automated postings participate in
-transaction balancing, missing amount inference and balance assertions.
+Postings added by transaction modifiers participate in transaction
+balancing, missing amount inference and balance assertions, like regular
+postings.
.SH EDITOR SUPPORT
.PP
Add\-on modes exist for various text editors, to make working with
diff --git a/hledger_journal.info b/hledger_journal.info
index c217c58..7a4148f 100644
--- a/hledger_journal.info
+++ b/hledger_journal.info
@@ -4,8 +4,8 @@ stdin.

File: hledger_journal.info, Node: Top, Next: FILE FORMAT, Up: (dir)
-hledger_journal(5) hledger 1.11.1
-*********************************
+hledger_journal(5) hledger 1.12
+*******************************
hledger's usual data source is a plain text file containing journal
entries in hledger journal format. This file represents a standard
@@ -524,16 +524,46 @@ File: hledger_journal.info, Node: Assertions and commodities, Next: Assertions
The asserted balance must be a simple single-commodity amount, and in
fact the assertion checks only this commodity's balance within the
-(possibly multi-commodity) account balance. We could call this a
-partial balance assertion. This is compatible with Ledger, and makes it
-possible to make assertions about accounts containing multiple
-commodities.
+(possibly multi-commodity) account balance.
+This is how assertions work in Ledger also. We could call this a
+"partial" balance assertion.
- To assert each commodity's balance in such a multi-commodity account,
-you can add multiple postings (with amount 0 if necessary). But note
-that no matter how many assertions you add, you can't be sure the
-account does not contain some unexpected commodity. (We'll add support
-for this kind of total balance assertion if there's demand.)
+ To assert the balance of more than one commodity in an account, you
+can write multiple postings, each asserting one commodity's balance.
+
+ You can make a stronger kind of balance assertion, by writing a
+double equals sign ('==EXPECTEDBALANCE'). This "complete" balance
+assertion asserts the absence of other commodities (or, that their
+balance is 0, which to hledger is equivalent.)
+
+2013/1/1
+ a $1
+ a 1€
+ b $-1
+ c -1€
+
+2013/1/2 ; These assertions succeed
+ a 0 = $1
+ a 0 = 1€
+ b 0 == $-1
+ c 0 == -1€
+
+2013/1/3 ; This assertion fails as 'a' also contains 1€
+ a 0 == $1
+
+ It's not yet possible to make a complete assertion about a balance
+that has multiple commodities. One workaround is to isolate each
+commodity into its own subaccount:
+
+2013/1/1
+ a:usd $1
+ a:euro 1€
+ b
+
+2013/1/2
+ a 0 == 0
+ a:usd 0 == $1
+ a:euro 0 == 1€

File: hledger_journal.info, Node: Assertions and subaccounts, Next: Assertions and virtual postings, Prev: Assertions and commodities, Up: Balance Assertions
@@ -766,11 +796,9 @@ links to more detailed docs.
directiveend subdirectivespurpose can affect (as of
directive 2018/06)
-----------------------------------------------------------------------------
-'account' any declare an account name & account code:
- text optional account code balance reports
- (except 'balance'
- single-column
- mode)
+'account' any document account names, all entries in
+ text declare account types & all files, before
+ display order or after
'alias' 'end rewrite account names following
aliases' inline/included
entries until end
@@ -821,10 +849,8 @@ account' apply account names inline/included
And some definitions:
-subdirectiveoptional indented directive or unparsed text lines
- immediately following a parent directive
-account numeric code influencing account display order in most
-code balance reports
+subdirectiveoptional indented directive line immediately following a
+ parent directive
number how to interpret numbers when parsing journal entries (the
notation identity of the decimal separator character). (Currently
each commodity can have its own notation, even in the same
@@ -853,7 +879,6 @@ times though.
* Default commodity::
* Market prices::
* Declaring accounts::
-* Account display order::
* Rewriting accounts::
* Default parent account::
@@ -1003,52 +1028,106 @@ P 2010/1/1 € $1.40
another commodity using these prices.

-File: hledger_journal.info, Node: Declaring accounts, Next: Account display order, Prev: Market prices, Up: Directives
+File: hledger_journal.info, Node: Declaring accounts, Next: Rewriting accounts, Prev: Market prices, Up: Directives
1.14.7 Declaring accounts
-------------------------
-The 'account' directive predeclares account names. The simplest form is
-'account ACCTNAME', eg:
+'account' directives can be used to pre-declare some or all accounts.
+Though not required, they can provide several benefits:
-account assets:bank:checking
+ * They can document your intended chart of accounts, providing a
+ reference.
+ * They can store extra information about accounts (account numbers,
+ notes, etc.)
+ * They can help hledger know your accounts' types (asset, liability,
+ equity, revenue, expense), useful for reports like balancesheet and
+ incomestatement.
+ * They control account display order in reports, allowing
+ non-alphabetic sorting (eg Revenues to appear above Expenses).
+ * They help with account name completion in the add command,
+ hledger-iadd, hledger-web, ledger-mode etc.
- Currently this mainly helps with account name autocompletion in eg
-hledger add, hledger-iadd, hledger-web, and ledger-mode.
-In future it will also help detect misspelled accounts.
+ Here is the full syntax:
- An account directive can also have indented subdirectives following
-it, which are currently ignored. Here is the full syntax:
+account ACCTNAME [ACCTTYPE]
+ [COMMENTS]
-; account ACCTNAME
-; [OPTIONALSUBDIRECTIVES]
+ The simplest form just declares a hledger-style account name, eg:
account assets:bank:checking
- a comment
- some-tag:12345
+
+* Menu:
+
+* Account types::
+* Account comments::
+* Account display order::

-File: hledger_journal.info, Node: Account display order, Next: Rewriting accounts, Prev: Declaring accounts, Up: Directives
+File: hledger_journal.info, Node: Account types, Next: Account comments, Up: Declaring accounts
-1.14.8 Account display order
-----------------------------
+1.14.7.1 Account types
+......................
-Account directives have another purpose: they set the order in which
-accounts are displayed, in hledger reports, hledger-ui accounts screen,
-hledger-web sidebar etc. For example, say you have these top-level
-accounts:
+hledger recognises five types of account: asset, liability, equity,
+revenue, expense. This is useful for certain accounting-aware reports,
+in particular balancesheet, incomestatement and cashflow.
-$ accounts -1
-assets
-equity
-expenses
-liabilities
-misc
-other
-revenues
+ If you name your top-level accounts with some variation of 'assets',
+'liabilities'/'debts', 'equity', 'revenues'/'income', or 'expenses',
+their types are detected automatically.
+
+ More generally, you can declare an account's type by adding one of
+the letters 'ALERX' to its account directive, separated from the account
+name by two or more spaces. Eg:
+
+account assets A
+account liabilities L
+account equity E
+account revenues R
+account expenses X
+
+ Note: if you ever override the types of those auto-detected english
+account names mentioned above, you might need to help the reports a bit:
- By default, they are displayed in alphabetical order. But if you add
-the following account directives to the journal:
+; make "liabilities" not have the liability type, who knows why
+account liabilities E
+
+; better ensure some other account has the liability type,
+; otherwise balancesheet would still show "liabilities" under Liabilities
+account - L
+
+ )
+
+
+File: hledger_journal.info, Node: Account comments, Next: Account display order, Prev: Account types, Up: Declaring accounts
+
+1.14.7.2 Account comments
+.........................
+
+An account directive can also have indented comments on following lines,
+eg:
+
+account assets:bank:checking
+ ; acctno:12345
+ ; a comment
+
+ We also allow (and ignore) Ledger-style subdirectives, with no
+leading semicolon, for compatibility.
+
+ Tags in account comments, like 'acctno' above, currently have no
+effect.
+
+
+File: hledger_journal.info, Node: Account display order, Prev: Account comments, Up: Declaring accounts
+
+1.14.7.3 Account display order
+..............................
+
+Account directives also set the order in which accounts are displayed in
+reports, the hledger-ui accounts screen, the hledger-web sidebar, etc.
+Normally accounts are listed in alphabetical order, but if you have eg
+these account directives in the journal:
account assets
account liabilities
@@ -1056,32 +1135,36 @@ account equity
account revenues
account expenses
- the display order changes to:
+ you'll see those accounts listed in declaration order, not
+alphabetically:
-$ accounts -1
+$ hledger accounts -1
assets
liabilities
equity
revenues
expenses
-misc
-other
- Ie, declared accounts first, in the order they were declared,
-followed by any undeclared accounts in alphabetic order.
+ Undeclared accounts, if any, are displayed last, in alphabetical
+order.
Note that sorting is done at each level of the account tree (within
-each group of sibling accounts under the same parent). This directive:
+each group of sibling accounts under the same parent). And currently,
+this directive:
account other:zoo
would influence the position of 'zoo' among 'other''s subaccounts,
-but not the position of 'other' among the top-level accounts.
+but not the position of 'other' among the top-level accounts. This
+means: - you will sometimes declare parent accounts (eg 'account other'
+above) that you don't intend to post to, just to customize their display
+order - sibling accounts stay together (you couldn't display 'x:y' in
+between 'a:b' and 'a:c').

-File: hledger_journal.info, Node: Rewriting accounts, Next: Default parent account, Prev: Account display order, Up: Directives
+File: hledger_journal.info, Node: Rewriting accounts, Next: Default parent account, Prev: Declaring accounts, Up: Directives
-1.14.9 Rewriting accounts
+1.14.8 Rewriting accounts
-------------------------
You can define account alias rules which rewrite your account names, or
@@ -1109,7 +1192,7 @@ hledger-web.

File: hledger_journal.info, Node: Basic aliases, Next: Regex aliases, Up: Rewriting accounts
-1.14.9.1 Basic aliases
+1.14.8.1 Basic aliases
......................
To set an account alias, use the 'alias' directive in your journal file.
@@ -1132,7 +1215,7 @@ alias checking = assets:bank:wells fargo:checking

File: hledger_journal.info, Node: Regex aliases, Next: Multiple aliases, Prev: Basic aliases, Up: Rewriting accounts
-1.14.9.2 Regex aliases
+1.14.8.2 Regex aliases
......................
There is also a more powerful variant that uses a regular expression,
@@ -1157,7 +1240,7 @@ whitespace.

File: hledger_journal.info, Node: Multiple aliases, Next: end aliases, Prev: Regex aliases, Up: Rewriting accounts
-1.14.9.3 Multiple aliases
+1.14.8.3 Multiple aliases
.........................
You can define as many aliases as you like using directives or
@@ -1173,7 +1256,7 @@ following order:

File: hledger_journal.info, Node: end aliases, Prev: Multiple aliases, Up: Rewriting accounts
-1.14.9.4 'end aliases'
+1.14.8.4 'end aliases'
......................
You can clear (forget) all currently defined aliases with the 'end
@@ -1184,8 +1267,8 @@ end aliases

File: hledger_journal.info, Node: Default parent account, Prev: Rewriting accounts, Up: Directives
-1.14.10 Default parent account
-------------------------------
+1.14.9 Default parent account
+-----------------------------
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
@@ -1245,13 +1328,22 @@ the date replaced by a tilde ('~') followed by a period expression
date must fall on a natural boundary of the interval. Eg 'monthly from
2018/1/1' is valid, but 'monthly from 2018/1/15' is not.
- If you write a transaction description or same-line comment, it must
-be separated from the period expression by *two or more spaces*. Eg:
-
-; 2 or more spaces
-; ||
-; vv
-~ every 2 weeks from 2018/6 to 2018/9 paycheck
+ Partial or relative dates (M/D, D, tomorrow, last week) in the period
+expression can work (useful or not). They will be relative to today's
+date, unless a Y default year directive is in effect, in which case they
+will be relative to Y/1/1.
+
+ Period expressions must be terminated by *two or more spaces* if
+followed by additional fields. For example, the periodic transaction
+given below includes a transaction description "paycheck", which is
+separated from the period expression by a double space. If not for the
+second space, hledger would attempt (and fail) to parse "paycheck" as a
+part of the period expression.
+
+; 2 or more spaces
+; ||
+; vv
+~ every 2 weeks from 2018/6/4 to 2018/9 paycheck
assets:bank:checking $1500
income:acme inc
@@ -1330,38 +1422,64 @@ automatically to certain transactions. Currently, this means adding
extra postings (also known as "automated postings"). Transaction
modifiers are enabled by the '--auto' flag.
- A transaction modifier rule looks a bit like a normal journal entry,
-except the first line is an equal sign ('=') followed by a query
-(mnemonic: '=' suggests matching something.):
+ A transaction modifier rule looks quite like a normal transaction,
+except the first line is an equals sign followed by a query that matches
+certain postings (mnemonic: '=' suggests matching). And each "posting"
+is actually a posting-generating rule:
+
+= QUERY
+ ACCT AMT
+ ACCT [AMT]
+ ...
+
+ The posting rules look just like normal postings, except the amount
+can be:
+
+ * a normal amount with a commodity symbol, eg '$2'. This will be
+ used as-is.
+ * a number, eg '2'. The commodity symbol (if any) from the matched
+ posting will be added to this.
+ * a numeric multiplier, eg '*2' (a star followed by a number N). The
+ matched posting's amount (and total price, if any) will be
+ multiplied by N.
+ * a multiplier with a commodity symbol, eg '*$2' (a star, number N,
+ and symbol S). The matched posting's amount will be multiplied by
+ N, and its commodity symbol will be replaced with S.
-= expenses:gifts
- budget:gifts *-1
- assets:budget *1
-
- The posting amounts can be of the form '*N', which means "the amount
-of the matched transaction's first posting, multiplied by N". They can
-also be ordinary fixed amounts. Fixed amounts with no commodity symbol
-will be given the same commodity as the matched transaction's first
-posting.
+ Some examples:
- This example adds a corresponding (unbalanced) budget posting to
-every transaction involving the 'expenses:gifts' account:
+; every time I buy food, schedule a dollar donation
+= expenses:food
+ (liabilities:charity) $-1
+; when I buy a gift, also deduct that amount from a budget envelope subaccount
= expenses:gifts
- (budget:gifts) *-1
+ assets:checking:gifts *-1
+ assets:checking *1
-2017-12-14
- expenses:gifts $20
- assets
+2017/12/1
+ expenses:food $10
+ assets:checking
+
+2017/12/14
+ expenses:gifts $20
+ assets:checking
$ hledger print --auto
+2017/12/01
+ expenses:food $10
+ assets:checking
+ (liabilities:charity) $-1
+
2017/12/14
expenses:gifts $20
- (budget:gifts) $-20
- assets
+ assets:checking
+ assets:checking:gifts -$20
+ assets:checking $20
- Like postings recorded by hand, automated postings participate in
-transaction balancing, missing amount inference and balance assertions.
+ Postings added by transaction modifiers participate in transaction
+balancing, missing amount inference and balance assertions, like regular
+postings.

File: hledger_journal.info, Node: EDITOR SUPPORT, Prev: FILE FORMAT, Up: Top
@@ -1393,93 +1511,97 @@ Code

Tag Table:
Node: Top76
-Node: FILE FORMAT2376
-Ref: #file-format2500
-Node: Transactions2787
-Ref: #transactions2908
-Node: Postings3592
-Ref: #postings3719
-Node: Dates4714
-Ref: #dates4829
-Node: Simple dates4894
-Ref: #simple-dates5020
-Node: Secondary dates5386
-Ref: #secondary-dates5540
-Node: Posting dates7103
-Ref: #posting-dates7232
-Node: Status8606
-Ref: #status8726
-Node: Description10434
-Ref: #description10572
-Node: Payee and note10891
-Ref: #payee-and-note11005
-Node: Account names11247
-Ref: #account-names11390
-Node: Amounts11877
-Ref: #amounts12013
-Node: Virtual Postings15030
-Ref: #virtual-postings15189
-Node: Balance Assertions16409
-Ref: #balance-assertions16584
-Node: Assertions and ordering17480
-Ref: #assertions-and-ordering17666
-Node: Assertions and included files18366
-Ref: #assertions-and-included-files18607
-Node: Assertions and multiple -f options18940
-Ref: #assertions-and-multiple--f-options19194
-Node: Assertions and commodities19326
-Ref: #assertions-and-commodities19561
-Node: Assertions and subaccounts20257
-Ref: #assertions-and-subaccounts20489
-Node: Assertions and virtual postings21010
-Ref: #assertions-and-virtual-postings21217
-Node: Balance Assignments21359
-Ref: #balance-assignments21540
-Node: Transaction prices22660
-Ref: #transaction-prices22829
-Node: Comments25097
-Ref: #comments25231
-Node: Tags26401
-Ref: #tags26519
-Node: Directives27921
-Ref: #directives28064
-Node: Comment blocks33946
-Ref: #comment-blocks34091
-Node: Including other files34267
-Ref: #including-other-files34447
-Node: Default year34855
-Ref: #default-year35024
-Node: Declaring commodities35447
-Ref: #declaring-commodities35630
-Node: Default commodity36857
-Ref: #default-commodity37033
-Node: Market prices37669
-Ref: #market-prices37834
-Node: Declaring accounts38675
-Ref: #declaring-accounts38854
-Node: Account display order39404
-Ref: #account-display-order39594
-Node: Rewriting accounts40615
-Ref: #rewriting-accounts40803
-Node: Basic aliases41537
-Ref: #basic-aliases41683
-Node: Regex aliases42387
-Ref: #regex-aliases42558
-Node: Multiple aliases43276
-Ref: #multiple-aliases43451
-Node: end aliases43949
-Ref: #end-aliases44096
-Node: Default parent account44197
-Ref: #default-parent-account44365
-Node: Periodic transactions45249
-Ref: #periodic-transactions45431
-Node: Forecasting with periodic transactions46642
-Ref: #forecasting-with-periodic-transactions46885
-Node: Budgeting with periodic transactions48572
-Ref: #budgeting-with-periodic-transactions48811
-Node: Transaction Modifiers49270
-Ref: #transaction-modifiers49433
-Node: EDITOR SUPPORT50689
-Ref: #editor-support50807
+Node: FILE FORMAT2372
+Ref: #file-format2496
+Node: Transactions2783
+Ref: #transactions2904
+Node: Postings3588
+Ref: #postings3715
+Node: Dates4710
+Ref: #dates4825
+Node: Simple dates4890
+Ref: #simple-dates5016
+Node: Secondary dates5382
+Ref: #secondary-dates5536
+Node: Posting dates7099
+Ref: #posting-dates7228
+Node: Status8602
+Ref: #status8722
+Node: Description10430
+Ref: #description10568
+Node: Payee and note10887
+Ref: #payee-and-note11001
+Node: Account names11243
+Ref: #account-names11386
+Node: Amounts11873
+Ref: #amounts12009
+Node: Virtual Postings15026
+Ref: #virtual-postings15185
+Node: Balance Assertions16405
+Ref: #balance-assertions16580
+Node: Assertions and ordering17476
+Ref: #assertions-and-ordering17662
+Node: Assertions and included files18362
+Ref: #assertions-and-included-files18603
+Node: Assertions and multiple -f options18936
+Ref: #assertions-and-multiple--f-options19190
+Node: Assertions and commodities19322
+Ref: #assertions-and-commodities19557
+Node: Assertions and subaccounts20745
+Ref: #assertions-and-subaccounts20977
+Node: Assertions and virtual postings21498
+Ref: #assertions-and-virtual-postings21705
+Node: Balance Assignments21847
+Ref: #balance-assignments22028
+Node: Transaction prices23148
+Ref: #transaction-prices23317
+Node: Comments25585
+Ref: #comments25719
+Node: Tags26889
+Ref: #tags27007
+Node: Directives28409
+Ref: #directives28552
+Node: Comment blocks34159
+Ref: #comment-blocks34304
+Node: Including other files34480
+Ref: #including-other-files34660
+Node: Default year35068
+Ref: #default-year35237
+Node: Declaring commodities35660
+Ref: #declaring-commodities35843
+Node: Default commodity37070
+Ref: #default-commodity37246
+Node: Market prices37882
+Ref: #market-prices38047
+Node: Declaring accounts38888
+Ref: #declaring-accounts39064
+Node: Account types40021
+Ref: #account-types40170
+Node: Account comments41244
+Ref: #account-comments41429
+Node: Account display order41750
+Ref: #account-display-order41923
+Node: Rewriting accounts43045
+Ref: #rewriting-accounts43230
+Node: Basic aliases43964
+Ref: #basic-aliases44110
+Node: Regex aliases44814
+Ref: #regex-aliases44985
+Node: Multiple aliases45703
+Ref: #multiple-aliases45878
+Node: end aliases46376
+Ref: #end-aliases46523
+Node: Default parent account46624
+Ref: #default-parent-account46790
+Node: Periodic transactions47674
+Ref: #periodic-transactions47856
+Node: Forecasting with periodic transactions49559
+Ref: #forecasting-with-periodic-transactions49802
+Node: Budgeting with periodic transactions51489
+Ref: #budgeting-with-periodic-transactions51728
+Node: Transaction Modifiers52187
+Ref: #transaction-modifiers52350
+Node: EDITOR SUPPORT54331
+Ref: #editor-support54449

End Tag Table
diff --git a/hledger_journal.txt b/hledger_journal.txt
index 76b757a..6fbf8e5 100644
--- a/hledger_journal.txt
+++ b/hledger_journal.txt
@@ -405,16 +405,46 @@ FILE FORMAT
Assertions and commodities
The asserted balance must be a simple single-commodity amount, and in
fact the assertion checks only this commodity's balance within the
- (possibly multi-commodity) account balance. We could call this a par-
- tial balance assertion. This is compatible with Ledger, and makes it
- possible to make assertions about accounts containing multiple commodi-
- ties.
+ (possibly multi-commodity) account balance.
+ This is how assertions work in Ledger also. We could call this a "par-
+ tial" balance assertion.
- To assert each commodity's balance in such a multi-commodity account,
- you can add multiple postings (with amount 0 if necessary). But note
- that no matter how many assertions you add, you can't be sure the
- account does not contain some unexpected commodity. (We'll add support
- for this kind of total balance assertion if there's demand.)
+ To assert the balance of more than one commodity in an account, you can
+ write multiple postings, each asserting one commodity's balance.
+
+ You can make a stronger kind of balance assertion, by writing a double
+ equals sign (==EXPECTEDBALANCE). This "complete" balance assertion
+ asserts the absence of other commodities (or, that their balance is 0,
+ which to hledger is equivalent.)
+
+ 2013/1/1
+ a $1
+ a 1
+ b $-1
+ c -1
+
+ 2013/1/2 ; These assertions succeed
+ a 0 = $1
+ a 0 = 1
+ b 0 == $-1
+ c 0 == -1
+
+ 2013/1/3 ; This assertion fails as 'a' also contains 1
+ a 0 == $1
+
+ It's not yet possible to make a complete assertion about a balance that
+ has multiple commodities. One workaround is to isolate each commodity
+ into its own subaccount:
+
+ 2013/1/1
+ a:usd $1
+ a:euro 1
+ b
+
+ 2013/1/2
+ a 0 == 0
+ a:usd 0 == $1
+ a:euro 0 == 1
Assertions and subaccounts
Balance assertions do not count the balance from subaccounts; they
@@ -614,40 +644,51 @@ FILE FORMAT
tive directive rec- 2018/06)
tives
-------------------------------------------------------------------------------------------------
- account any declare an account name & account code: bal-
- text optional account code ance reports
- (except balance
- single-column mode)
+ account any document account names, all entries in all
+ text declare account types & dis- files, before or
+ play order after
alias end aliases rewrite account names following
inline/included
entries until end
- of current file or
+ of current file or
end directive
- apply account end apply account prepend a common parent to following
+ apply account end apply account prepend a common parent to following
account names inline/included
entries until end
- of current file or
+ of current file or
end directive
comment end comment ignore part of journal following
inline/included
entries until end
- of current file or
+ of current file or
end directive
- commodity format declare a commodity and its number notation:
+ commodity format declare a commodity and its number notation:
number notation & display following entries
style in that commodity
- in all files; dis-
+ in all files; dis-
play style: amounts
of that commodity
in reports
- D declare a commodity, number commodity: all com-
+
+
+
+
+
+
+
+
+
+
+
+
+ D declare a commodity, number commodity: all com-
notation & display style for modityless entries
- commodityless amounts in all files; num-
- ber notation: fol-
+ commodityless amounts in all files; num-
+ ber notation: fol-
lowing commodity-
- less entries and
+ less entries and
entries in that
- commodity in all
+ commodity in all
files; display
style: amounts of
that commodity in
@@ -658,7 +699,7 @@ FILE FORMAT
commodity commodity in
reports, when -V is
used
- Y declare a year for yearless following
+ Y declare a year for yearless following
dates inline/included
entries until end
of current file
@@ -666,16 +707,11 @@ FILE FORMAT
And some definitions:
- subdirec- optional indented directive or unparsed text lines immedi-
- tive ately following a parent directive
- account numeric code influencing account display order in most bal-
- code ance reports
-
-
-
- number how to interpret numbers when parsing journal entries (the
- notation identity of the decimal separator character). (Currently
- each commodity can have its own notation, even in the same
+ subdirec- optional indented directive line immediately following a par-
+ tive ent directive
+ number how to interpret numbers when parsing journal entries (the
+ notation identity of the decimal separator character). (Currently
+ each commodity can have its own notation, even in the same
file.)
display how to display amounts of a commodity in reports (symbol side
style and spacing, digit groups, decimal separator, decimal places)
@@ -683,37 +719,37 @@ FILE FORMAT
scope are affected by a directive
As you can see, directives vary in which journal entries and files they
- affect, and whether they are focussed on input (parsing) or output
+ affect, and whether they are focussed on input (parsing) or output
(reports). Some directives have multiple effects.
- If you have a journal made up of multiple files, or pass multiple -f
- options on the command line, note that directives which affect input
- typically last only until the end of their defining file. This pro-
+ If you have a journal made up of multiple files, or pass multiple -f
+ options on the command line, note that directives which affect input
+ typically last only until the end of their defining file. This pro-
vides more simplicity and predictability, eg reports are not changed by
- writing file options in a different order. It can be surprising at
+ writing file options in a different order. It can be surprising at
times though.
Comment blocks
- A line containing just comment starts a commented region of the file,
+ A line containing just comment starts a commented region of the file,
and a line containing just end comment (or the end of the current file)
ends it. See also comments.
Including other files
- You can pull in the content of additional files by writing an include
+ You can pull in the content of additional files by writing an include
directive, like this:
include path/to/file.journal
- If the path does not begin with a slash, it is relative to the current
- file. The include file path may contain common glob patterns (e.g.
+ If the path does not begin with a slash, it is relative to the current
+ file. The include file path may contain common glob patterns (e.g.
*).
- The include directive can only be used in journal files. It can
+ The include directive can only be used in journal files. It can
include journal, timeclock or timedot files, but not CSV files.
Default year
- 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.
+ 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:
Y2009 ; set default year to 2009
@@ -733,8 +769,8 @@ FILE FORMAT
assets
Declaring commodities
- The commodity directive declares commodities which may be used in the
- journal (though currently we do not enforce this). It may be written
+ The commodity directive declares commodities which may be used in the
+ journal (though currently we do not enforce this). It may be written
on a single line, like this:
; commodity EXAMPLEAMOUNT
@@ -744,8 +780,8 @@ FILE FORMAT
; separating thousands with comma.
commodity 1,000.0000 AAAA
- or on multiple lines, using the "format" subdirective. In this case
- the commodity symbol appears twice and should be the same in both
+ or on multiple lines, using the "format" subdirective. In this case
+ the commodity symbol appears twice and should be the same in both
places:
; commodity SYMBOL
@@ -757,19 +793,19 @@ FILE FORMAT
commodity INR
format INR 9,99,99,999.00
- Commodity directives have a second purpose: they define the standard
+ Commodity directives have a second purpose: they define the standard
display format for amounts in the commodity. Normally the display for-
- mat is inferred from journal entries, but this can be unpredictable;
- declaring it with a commodity directive overrides this and removes
- ambiguity. Towards this end, amounts in commodity directives must
- always be written with a decimal point (a period or comma, followed by
+ mat is inferred from journal entries, but this can be unpredictable;
+ declaring it with a commodity directive overrides this and removes
+ ambiguity. Towards this end, amounts in commodity directives must
+ always be written with a decimal point (a period or comma, followed by
0 or more decimal digits).
Default commodity
- The D directive sets a default commodity (and display format), to be
+ The D directive sets a default commodity (and display format), to be
used for amounts without a commodity symbol (ie, plain numbers). (Note
- this differs from Ledger's default commodity directive.) The commodity
- and display format will be applied to all subsequent commodity-less
+ this differs from Ledger's default commodity directive.) The commodity
+ and display format will be applied to all subsequent commodity-less
amounts, or until the next D directive.
# commodity-less amounts should be treated as dollars
@@ -784,9 +820,9 @@ FILE FORMAT
a decimal point.
Market prices
- The P directive declares a market price, which is an exchange rate
+ The P directive declares a market price, which is an exchange rate
between two commodities on a certain date. (In Ledger, they are called
- "historical prices".) These are often obtained from a stock exchange,
+ "historical prices".) These are often obtained from a stock exchange,
cryptocurrency exchange, or the foreign exchange market.
Here is the format:
@@ -797,55 +833,97 @@ FILE FORMAT
o COMMODITYA is the symbol of the commodity being priced
- o COMMODITYBAMOUNT is an amount (symbol and quantity) in a second com-
+ o COMMODITYBAMOUNT is an amount (symbol and quantity) in a second com-
modity, giving the price in commodity B of one unit of commodity A.
- These two market price directives say that one euro was worth 1.35 US
+ These two market price directives say that one euro was worth 1.35 US
dollars during 2009, and $1.40 from 2010 onward:
P 2009/1/1 $1.35
P 2010/1/1 $1.40
- The -V/--value flag can be used to convert reported amounts to another
+ The -V/--value flag can be used to convert reported amounts to another
commodity using these prices.
Declaring accounts
- The account directive predeclares account names. The simplest form is
- account ACCTNAME, eg:
+ account directives can be used to pre-declare some or all accounts.
+ Though not required, they can provide several benefits:
+
+ o They can document your intended chart of accounts, providing a refer-
+ ence.
+
+ o They can store extra information about accounts (account numbers,
+ notes, etc.)
+
+ o They can help hledger know your accounts' types (asset, liability,
+ equity, revenue, expense), useful for reports like balancesheet and
+ incomestatement.
+
+ o They control account display order in reports, allowing non-alpha-
+ betic sorting (eg Revenues to appear above Expenses).
+
+ o They help with account name completion in the add command,
+ hledger-iadd, hledger-web, ledger-mode etc.
+
+ Here is the full syntax:
+
+ account ACCTNAME [ACCTTYPE]
+ [COMMENTS]
+
+ The simplest form just declares a hledger-style account name, eg:
account assets:bank:checking
- Currently this mainly helps with account name autocompletion in eg
- hledger add, hledger-iadd, hledger-web, and ledger-mode.
- In future it will also help detect misspelled accounts.
+ Account types
+ hledger recognises five types of account: asset, liability, equity,
+ revenue, expense. This is useful for certain accounting-aware reports,
+ in particular balancesheet, incomestatement and cashflow.
+
+ If you name your top-level accounts with some variation of assets, lia-
+ bilities/debts, equity, revenues/income, or expenses, their types are
+ detected automatically.
+
+ More generally, you can declare an account's type by adding one of the
+ letters ALERX to its account directive, separated from the account name
+ by two or more spaces. Eg:
+
+ account assets A
+ account liabilities L
+ account equity E
+ account revenues R
+ account expenses X
- An account directive can also have indented subdirectives following it,
- which are currently ignored. Here is the full syntax:
+ Note: if you ever override the types of those auto-detected english
+ account names mentioned above, you might need to help the reports a
+ bit:
- ; account ACCTNAME
- ; [OPTIONALSUBDIRECTIVES]
+ ; make "liabilities" not have the liability type, who knows why
+ account liabilities E
+
+ ; better ensure some other account has the liability type,
+ ; otherwise balancesheet would still show "liabilities" under Liabilities
+ account - L
+
+ )
+
+ Account comments
+ An account directive can also have indented comments on following
+ lines, eg:
account assets:bank:checking
- a comment
- some-tag:12345
+ ; acctno:12345
+ ; a comment
- Account display order
- Account directives have another purpose: they set the order in which
- accounts are displayed, in hledger reports, hledger-ui accounts screen,
- hledger-web sidebar etc. For example, say you have these top-level
- accounts:
+ We also allow (and ignore) Ledger-style subdirectives, with no leading
+ semicolon, for compatibility.
- $ accounts -1
- assets
- equity
- expenses
- liabilities
- misc
- other
- revenues
+ Tags in account comments, like acctno above, currently have no effect.
- By default, they are displayed in alphabetical order. But if you add
- the following account directives to the journal:
+ Account display order
+ Account directives also set the order in which accounts are displayed
+ in reports, the hledger-ui accounts screen, the hledger-web sidebar,
+ etc. Normally accounts are listed in alphabetical order, but if you
+ have eg these account directives in the journal:
account assets
account liabilities
@@ -853,27 +931,30 @@ FILE FORMAT
account revenues
account expenses
- the display order changes to:
+ you'll see those accounts listed in declaration order, not alphabeti-
+ cally:
- $ accounts -1
+ $ hledger accounts -1
assets
liabilities
equity
revenues
expenses
- misc
- other
- Ie, declared accounts first, in the order they were declared, followed
- by any undeclared accounts in alphabetic order.
+ Undeclared accounts, if any, are displayed last, in alphabetical order.
Note that sorting is done at each level of the account tree (within
- each group of sibling accounts under the same parent). This directive:
+ each group of sibling accounts under the same parent). And currently,
+ this directive:
account other:zoo
would influence the position of zoo among other's subaccounts, but not
- the position of other among the top-level accounts.
+ the position of other among the top-level accounts. This means: - you
+ will sometimes declare parent accounts (eg account other above) that
+ you don't intend to post to, just to customize their display order -
+ sibling accounts stay together (you couldn't display x:y in between a:b
+ and a:c).
Rewriting accounts
You can define account alias rules which rewrite your account names, or
@@ -1004,105 +1085,143 @@ FILE FORMAT
date must fall on a natural boundary of the interval. Eg
monthly from 2018/1/1 is valid, but monthly from 2018/1/15 is not.
- If you write a transaction description or same-line comment, it must be
- separated from the period expression by two or more spaces. Eg:
-
- ; 2 or more spaces
- ; ||
- ; vv
- ~ every 2 weeks from 2018/6 to 2018/9 paycheck
+ Partial or relative dates (M/D, D, tomorrow, last week) in the period
+ expression can work (useful or not). They will be relative to today's
+ date, unless a Y default year directive is in effect, in which case
+ they will be relative to Y/1/1.
+
+ Period expressions must be terminated by two or more spaces if followed
+ by additional fields. For example, the periodic transaction given
+ below includes a transaction description "paycheck", which is separated
+ from the period expression by a double space. If not for the second
+ space, hledger would attempt (and fail) to parse "paycheck" as a part
+ of the period expression.
+
+ ; 2 or more spaces
+ ; ||
+ ; vv
+ ~ every 2 weeks from 2018/6/4 to 2018/9 paycheck
assets:bank:checking $1500
income:acme inc
Forecasting with periodic transactions
- With the --forecast flag, each periodic transaction rule generates
+ With the --forecast flag, each periodic transaction rule generates
future transactions recurring at the specified interval. These are not
- saved in the journal, but appear in all reports. They will look like
- normal transactions, but with an extra tag named recur, whose value is
+ saved in the journal, but appear in all reports. They will look like
+ normal transactions, but with an extra tag named recur, whose value is
the generating period expression.
- Forecast transactions start on the first occurrence, and end on the
- last occurrence, of their interval within the forecast period. The
+ Forecast transactions start on the first occurrence, and end on the
+ last occurrence, of their interval within the forecast period. The
forecast period:
o begins on the later of
o the report start date if specified with -b/-p/date:
- o the day after the latest normal (non-periodic) transaction in the
+ o the day after the latest normal (non-periodic) transaction in the
journal, or today if there are no normal transactions.
- o ends on the report end date if specified with -e/-p/date:, or 180
+ o ends on the report end date if specified with -e/-p/date:, or 180
days from today.
- where "today" means the current date at report time. The "later of"
- rule ensures that forecast transactions do not overlap normal transac-
+ where "today" means the current date at report time. The "later of"
+ rule ensures that forecast transactions do not overlap normal transac-
tions in time; they will begin only after normal transactions end.
- Forecasting can be useful for estimating balances into the future, and
- experimenting with different scenarios. Note the start date logic
+ Forecasting can be useful for estimating balances into the future, and
+ experimenting with different scenarios. Note the start date logic
means that forecasted transactions are automatically replaced by normal
transactions as you add those.
Forecasting can also help with data entry: describe most of your trans-
- actions with periodic rules, and every so often copy the output of
+ actions with periodic rules, and every so often copy the output of
print --forecast to the journal.
You can generate one-time transactions too: just write a period expres-
- sion specifying a date with no report interval. (You could also write
- a normal transaction with a future date, but remember this disables
+ sion specifying a date with no report interval. (You could also write
+ a normal transaction with a future date, but remember this disables
forecast transactions on previous dates.)
Budgeting with periodic transactions
- With the --budget flag, currently supported by the balance command,
- each periodic transaction rule declares recurring budget goals for the
- specified accounts. Eg the first example above declares a goal of
- spending $2000 on rent (and also, a goal of depositing $2000 into
- checking) every month. Goals and actual performance can then be com-
+ With the --budget flag, currently supported by the balance command,
+ each periodic transaction rule declares recurring budget goals for the
+ specified accounts. Eg the first example above declares a goal of
+ spending $2000 on rent (and also, a goal of depositing $2000 into
+ checking) every month. Goals and actual performance can then be com-
pared in budget reports.
- For more details, see: balance: Budget report and Cookbook: Budgeting
+ For more details, see: balance: Budget report and Cookbook: Budgeting
and Forecasting.
Transaction Modifiers
- Transaction modifier rules describe changes that should be applied
- automatically to certain transactions. Currently, this means adding
+ Transaction modifier rules describe changes that should be applied
+ automatically to certain transactions. Currently, this means adding
extra postings (also known as "automated postings"). Transaction modi-
fiers are enabled by the --auto flag.
- A transaction modifier rule looks a bit like a normal journal entry,
- except the first line is an equal sign (=) followed by a query
- (mnemonic: = suggests matching something.):
+ A transaction modifier rule looks quite like a normal transaction,
+ except the first line is an equals sign followed by a query that
+ matches certain postings (mnemonic: = suggests matching). And each
+ "posting" is actually a posting-generating rule:
- = expenses:gifts
- budget:gifts *-1
- assets:budget *1
+ = QUERY
+ ACCT AMT
+ ACCT [AMT]
+ ...
+
+ The posting rules look just like normal postings, except the amount can
+ be:
- The posting amounts can be of the form *N, which means "the amount of
- the matched transaction's first posting, multiplied by N". They can
- also be ordinary fixed amounts. Fixed amounts with no commodity symbol
- will be given the same commodity as the matched transaction's first
- posting.
+ o a normal amount with a commodity symbol, eg $2. This will be used
+ as-is.
- This example adds a corresponding (unbalanced) budget posting to every
- transaction involving the expenses:gifts account:
+ o a number, eg 2. The commodity symbol (if any) from the matched post-
+ ing will be added to this.
+
+ o a numeric multiplier, eg *2 (a star followed by a number N). The
+ matched posting's amount (and total price, if any) will be multiplied
+ by N.
+
+ o a multiplier with a commodity symbol, eg *$2 (a star, number N, and
+ symbol S). The matched posting's amount will be multiplied by N, and
+ its commodity symbol will be replaced with S.
+
+ Some examples:
+ ; every time I buy food, schedule a dollar donation
+ = expenses:food
+ (liabilities:charity) $-1
+
+ ; when I buy a gift, also deduct that amount from a budget envelope subaccount
= expenses:gifts
- (budget:gifts) *-1
+ assets:checking:gifts *-1
+ assets:checking *1
- 2017-12-14
- expenses:gifts $20
- assets
+ 2017/12/1
+ expenses:food $10
+ assets:checking
+
+ 2017/12/14
+ expenses:gifts $20
+ assets:checking
$ hledger print --auto
+ 2017/12/01
+ expenses:food $10
+ assets:checking
+ (liabilities:charity) $-1
+
2017/12/14
expenses:gifts $20
- (budget:gifts) $-20
- assets
+ assets:checking
+ assets:checking:gifts -$20
+ assets:checking $20
- Like postings recorded by hand, automated postings participate in
- transaction balancing, missing amount inference and balance assertions.
+ Postings added by transaction modifiers participate in transaction bal-
+ ancing, missing amount inference and balance assertions, like regular
+ postings.
EDITOR SUPPORT
Add-on modes exist for various text editors, to make working with jour-
@@ -1151,4 +1270,4 @@ SEE ALSO
-hledger 1.11.1 September 2018 hledger_journal(5)
+hledger 1.12 December 2018 hledger_journal(5)
diff --git a/hledger_timeclock.5 b/hledger_timeclock.5
index 24b71db..769f386 100644
--- a/hledger_timeclock.5
+++ b/hledger_timeclock.5
@@ -1,5 +1,5 @@
-.TH "hledger_timeclock" "5" "September 2018" "hledger 1.11.1" "hledger User Manuals"
+.TH "hledger_timeclock" "5" "December 2018" "hledger 1.12" "hledger User Manuals"
diff --git a/hledger_timeclock.info b/hledger_timeclock.info
index b2bb155..17696ba 100644
--- a/hledger_timeclock.info
+++ b/hledger_timeclock.info
@@ -4,8 +4,8 @@ stdin.

File: hledger_timeclock.info, Node: Top, Up: (dir)
-hledger_timeclock(5) hledger 1.11.1
-***********************************
+hledger_timeclock(5) hledger 1.12
+*********************************
hledger can read timeclock files. As with Ledger, these are (a subset
of) timeclock.el's format, containing clock-in and clock-out entries as
diff --git a/hledger_timeclock.txt b/hledger_timeclock.txt
index c734fb4..044727f 100644
--- a/hledger_timeclock.txt
+++ b/hledger_timeclock.txt
@@ -77,4 +77,4 @@ SEE ALSO
-hledger 1.11.1 September 2018 hledger_timeclock(5)
+hledger 1.12 December 2018 hledger_timeclock(5)
diff --git a/hledger_timedot.5 b/hledger_timedot.5
index 06bc2c3..56ec37a 100644
--- a/hledger_timedot.5
+++ b/hledger_timedot.5
@@ -1,5 +1,5 @@
-.TH "hledger_timedot" "5" "September 2018" "hledger 1.11.1" "hledger User Manuals"
+.TH "hledger_timedot" "5" "December 2018" "hledger 1.12" "hledger User Manuals"
diff --git a/hledger_timedot.info b/hledger_timedot.info
index 7fc5c99..2bd7d15 100644
--- a/hledger_timedot.info
+++ b/hledger_timedot.info
@@ -4,8 +4,8 @@ stdin.

File: hledger_timedot.info, Node: Top, Next: FILE FORMAT, Up: (dir)
-hledger_timedot(5) hledger 1.11.1
-*********************************
+hledger_timedot(5) hledger 1.12
+*******************************
Timedot is a plain text format for logging dated, categorised quantities
(of time, usually), supported by hledger. It is convenient for
@@ -110,7 +110,7 @@ $ hledger -f t.timedot --alias /\\./=: bal date:2016/2/4

Tag Table:
Node: Top76
-Node: FILE FORMAT811
-Ref: #file-format912
+Node: FILE FORMAT807
+Ref: #file-format908

End Tag Table
diff --git a/hledger_timedot.txt b/hledger_timedot.txt
index 1986816..d669f6d 100644
--- a/hledger_timedot.txt
+++ b/hledger_timedot.txt
@@ -124,4 +124,4 @@ SEE ALSO
-hledger 1.11.1 September 2018 hledger_timedot(5)
+hledger 1.12 December 2018 hledger_timedot(5)