big query/tests/show refactoring

- use new query system for command line too, filterspec is no more
- move unit tests near the code they test, run them in bottom up order, add more
- more precise Show instances, used for debugging not ui
This commit is contained in:
Simon Michael 2012-05-27 18:14:20 +00:00
parent 1654776f4a
commit 3ee6a351f3
27 changed files with 1405 additions and 791 deletions

View File

@ -873,73 +873,53 @@ Examples:
The following additional features and options allow for fine-grained The following additional features and options allow for fine-grained
reporting. They are common to most commands, where applicable. reporting. They are common to most commands, where applicable.
### Filter patterns ### Queries
Most commands accept one or more filter pattern arguments after the Most commands accept an optional query expression, written as arguments
command name, to select a subset of transactions or postings. There are after the command name (or entered in the hledger-web search field), to
two kinds of pattern: filter the data. The syntax is similar a Google search expression: one or
more space separated search terms, optional prefixes to match specific
fields, quotes to enclose whitespace etc. Here are the kinds of query term
currently supported:
- an account pattern, which is a regular expression. This is - `REGEX` (no prefix) - match account names by this regular expression
matched against postings' accounts. Optionally, it may be prefixed - `acct:REGEX` - same as above
with `not:` in which case the match is negated. - `desc:REGEX` - match transaction descriptions by regular expression
- `date:PERIODEXPR` - match dates within the specified [period] (which may not contain a reporting interval)
- a description pattern, like the above but prefixed with - `edate:DATEEXPR` - as above, but match the effective date
`desc:`. This is matched against transactions' descriptions. Note, - `status:1` or `status:0` - match cleared/uncleared transactions
when negating a desc: pattern, not: goes last, eg: - `depth:N` - match (or display, depending on command) accounts at or above this depth
`desc:not:someregexp`. - `not:` before any of the above negates the match
<!-- <!--
New:
Most commands accept one or more filter pattern arguments after the
command name, to select a subset of the data. There are several kinds
of filter pattern:
- `acct:ACCTREGEX` - match account names by regular expression
- `desc:DESCREGEX` - match transaction descriptions by regular expression
- `tag:TAGNAMEREGEX[:TAGVALUEREGEX]` - match a [tag](#tags) name, and - `tag:TAGNAMEREGEX[:TAGVALUEREGEX]` - match a [tag](#tags) name, and
optionally the value, by regular expression optionally the value, by regular expression
- `TAGNAME:[TAGVALUEREGEX]` - match a tag name exactly, and optionally - `TAGNAME:[TAGVALUEREGEX]` - match a tag name exactly, and optionally
the value by regular expression. the value by regular expression.
- `code:CODEREGEX` -->
- `ACCTREGEX` - match account names by regular expression
Later:
- `status:[*]`
- `code:CODEREGEX`
- `date:DATEEXPR`
- `edate:DATEEXPR`
- `type:regular|virtual|balancedvirtual` - `type:regular|virtual|balancedvirtual`
- `comment:COMMENTREGEX` - `comment:COMMENTREGEX`
- `amount:AMOUNTEXPR` - `amount:AMOUNTEXPR`
- `commodity:COMMODITYSYMBOLREGEX` - `commodity:COMMODITYSYMBOLREGEX`
Any of these can be prefixed with `not:` or `!` to negate the match. Any of these can be prefixed with `not:` or `!` to negate the match.
--> -->
When you specify multiple filter patterns, hledger generally selects the Note these query terms can also be expressed as command-line flags; you
transactions or postings which match (or negatively match) can use either, or both.
> *any of the account patterns* AND With multiple query terms, most commands select the
> *any of the description patterns* transactions/postings/accounts which match (or negatively match)
> *any of the account terms* AND
> *any of the description terms* AND
> *all the other terms*
The [print](#print) command selects transactions which The [print](#print) command selects transactions which
> *match any of the description patterns* AND > *match any of the description terms* AND
> *have any postings matching any of the positive account patterns* > *have any postings matching any of the positive account terms* AND
> AND > *have no postings matching any of the negative account terms* AND
> *have no postings matching any of the negative account patterns* > *match all the other terms*
### Smart dates ### Smart dates

View File

@ -11,7 +11,7 @@ import Test.HUnit
import Hledger.Data import Hledger.Data
import Hledger.Data.Query import Hledger.Data.Query
import Hledger.Read import Hledger.Read hiding (samplejournal)
import Hledger.Reports import Hledger.Reports
import Hledger.Utils import Hledger.Utils

View File

@ -20,7 +20,7 @@ import Hledger.Data.Types
instance Show Account where instance Show Account where
show (Account a ts b) = printf "Account %s with %d txns and %s balance" a (length ts) (showMixedAmount b) show (Account a ps b) = printf "Account %s with %d postings and %s balance" a (length ps) (showMixedAmountDebug b)
instance Eq Account where instance Eq Account where
(==) (Account n1 t1 b1) (Account n2 t2 b2) = n1 == n2 && t1 == t2 && b1 == b2 (==) (Account n1 t1 b1) (Account n2 t2 b2) = n1 == n2 && t1 == t2 && b1 == b2

View File

@ -44,6 +44,7 @@ exchange rates.
module Hledger.Data.Amount ( module Hledger.Data.Amount (
-- * Amount -- * Amount
nullamt, nullamt,
missingamt,
amountWithCommodity, amountWithCommodity,
canonicaliseAmountCommodity, canonicaliseAmountCommodity,
setAmountPrecision, setAmountPrecision,
@ -58,7 +59,7 @@ module Hledger.Data.Amount (
maxprecisionwithpoint, maxprecisionwithpoint,
-- * MixedAmount -- * MixedAmount
nullmixedamt, nullmixedamt,
missingamt, missingmixedamt,
amounts, amounts,
normaliseMixedAmountPreservingFirstPrice, normaliseMixedAmountPreservingFirstPrice,
canonicaliseMixedAmountCommodity, canonicaliseMixedAmountCommodity,
@ -96,7 +97,7 @@ deriving instance Show HistoricalPrice
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- Amount -- Amount
instance Show Amount where show = showAmount instance Show Amount where show = showAmountDebug
instance Num Amount where instance Num Amount where
abs (Amount c q p) = Amount c (abs q) p abs (Amount c q p) = Amount c (abs q) p
@ -147,12 +148,14 @@ isNegativeAmount Amount{quantity=q} = q < 0
-- | Does this amount appear to be zero when displayed with its given precision ? -- | Does this amount appear to be zero when displayed with its given precision ?
isZeroAmount :: Amount -> Bool isZeroAmount :: Amount -> Bool
isZeroAmount = null . filter (`elem` "123456789") . showAmountWithoutPriceOrCommodity isZeroAmount a -- | a==missingamt = False
| otherwise = (null . filter (`elem` "123456789") . showAmountWithoutPriceOrCommodity) a
-- | Is this amount "really" zero, regardless of the display precision ? -- | Is this amount "really" zero, regardless of the display precision ?
-- Since we are using floating point, for now just test to some high precision. -- Since we are using floating point, for now just test to some high precision.
isReallyZeroAmount :: Amount -> Bool isReallyZeroAmount :: Amount -> Bool
isReallyZeroAmount = null . filter (`elem` "123456789") . printf ("%."++show zeroprecision++"f") . quantity isReallyZeroAmount a -- | a==missingamt = False
| otherwise = (null . filter (`elem` "123456789") . printf ("%."++show zeroprecision++"f") . quantity) a
where zeroprecision = 8 where zeroprecision = 8
-- | Get the string representation of an amount, based on its commodity's -- | Get the string representation of an amount, based on its commodity's
@ -166,8 +169,9 @@ setAmountPrecision p a@Amount{commodity=c} = a{commodity=c{precision=p}}
-- | Get the unambiguous string representation of an amount, for debugging. -- | Get the unambiguous string representation of an amount, for debugging.
showAmountDebug :: Amount -> String showAmountDebug :: Amount -> String
showAmountDebug (Amount (Commodity {symbol="AUTO"}) _ _) = "(missing)"
showAmountDebug (Amount c q pri) = printf "Amount {commodity = %s, quantity = %s, price = %s}" showAmountDebug (Amount c q pri) = printf "Amount {commodity = %s, quantity = %s, price = %s}"
(show c) (show q) (maybe "" showPriceDebug pri) (show c) (show q) (maybe "Nothing" showPriceDebug pri)
-- | Get the string representation of an amount, without any \@ price. -- | Get the string representation of an amount, without any \@ price.
showAmountWithoutPrice :: Amount -> String showAmountWithoutPrice :: Amount -> String
@ -189,7 +193,7 @@ showPriceDebug (TotalPrice pa) = " @@ " ++ showMixedAmountDebug pa
-- display settings. String representations equivalent to zero are -- display settings. String representations equivalent to zero are
-- converted to just \"0\". -- converted to just \"0\".
showAmount :: Amount -> String showAmount :: Amount -> String
showAmount (Amount (Commodity {symbol="AUTO"}) _ _) = "" -- can appear in an error message showAmount (Amount (Commodity {symbol="AUTO"}) _ _) = ""
showAmount a@(Amount (Commodity {symbol=sym,side=side,spaced=spaced}) _ pri) = showAmount a@(Amount (Commodity {symbol=sym,side=side,spaced=spaced}) _ pri) =
case side of case side of
L -> printf "%s%s%s%s" sym' space quantity' price L -> printf "%s%s%s%s" sym' space quantity' price
@ -257,7 +261,7 @@ canonicaliseAmountCommodity (Just canonicalcommoditymap) = fixamount
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- MixedAmount -- MixedAmount
instance Show MixedAmount where show = showMixedAmount instance Show MixedAmount where show = showMixedAmountDebug
instance Num MixedAmount where instance Num MixedAmount where
fromInteger i = Mixed [Amount (comm "") (fromInteger i) Nothing] fromInteger i = Mixed [Amount (comm "") (fromInteger i) Nothing]
@ -272,22 +276,31 @@ nullmixedamt :: MixedAmount
nullmixedamt = Mixed [] nullmixedamt = Mixed []
-- | A temporary value for parsed transactions which had no amount specified. -- | A temporary value for parsed transactions which had no amount specified.
missingamt :: MixedAmount missingamt :: Amount
missingamt = Mixed [Amount unknown{symbol="AUTO"} 0 Nothing] missingamt = Amount unknown{symbol="AUTO"} 0 Nothing
-- | Simplify a mixed amount's component amounts: combine amounts with missingmixedamt :: MixedAmount
-- the same commodity and price. Also remove any zero amounts and missingmixedamt = Mixed [missingamt]
-- | Simplify a mixed amount's component amounts: combine amounts with the
-- same commodity and price. Also remove any zero or missing amounts and
-- replace an empty amount list with a single zero amount. -- replace an empty amount list with a single zero amount.
normaliseMixedAmountPreservingPrices :: MixedAmount -> MixedAmount normaliseMixedAmountPreservingPrices :: MixedAmount -> MixedAmount
normaliseMixedAmountPreservingPrices (Mixed as) = Mixed as'' normaliseMixedAmountPreservingPrices (Mixed as) = Mixed as''
where where
as'' = if null nonzeros then [nullamt] else nonzeros as'' = if null nonzeros then [nullamt] else nonzeros
(_,nonzeros) = partition (\a -> isReallyZeroAmount a && Mixed [a] /= missingamt) as' (_,nonzeros) = partition isReallyZeroAmount $ filter (/= missingamt) as'
as' = map sumAmountsUsingFirstPrice $ group $ sort as as' = map sumAmountsUsingFirstPrice $ group $ sort as
sort = sortBy (\a1 a2 -> compare (sym a1,price a1) (sym a2,price a2)) sort = sortBy (\a1 a2 -> compare (sym a1,price a1) (sym a2,price a2))
group = groupBy (\a1 a2 -> sym a1 == sym a2 && price a1 == price a2) group = groupBy (\a1 a2 -> sym a1 == sym a2 && price a1 == price a2)
sym = symbol . commodity sym = symbol . commodity
tests_normaliseMixedAmountPreservingPrices = [
"normaliseMixedAmountPreservingPrices" ~: do
-- assertEqual "" (Mixed [dollars 2]) (normaliseMixedAmountPreservingPrices $ Mixed [dollars 0, dollars 2])
assertEqual "" (Mixed [nullamt]) (normaliseMixedAmountPreservingPrices $ Mixed [dollars 0, missingamt])
]
-- | Simplify a mixed amount's component amounts: combine amounts with -- | Simplify a mixed amount's component amounts: combine amounts with
-- the same commodity, using the first amount's price for subsequent -- the same commodity, using the first amount's price for subsequent
-- amounts in each commodity (ie, this function alters the amount and -- amounts in each commodity (ie, this function alters the amount and
@ -297,7 +310,7 @@ normaliseMixedAmountPreservingFirstPrice :: MixedAmount -> MixedAmount
normaliseMixedAmountPreservingFirstPrice (Mixed as) = Mixed as'' normaliseMixedAmountPreservingFirstPrice (Mixed as) = Mixed as''
where where
as'' = if null nonzeros then [nullamt] else nonzeros as'' = if null nonzeros then [nullamt] else nonzeros
(_,nonzeros) = partition (\a -> isReallyZeroAmount a && Mixed [a] /= missingamt) as' (_,nonzeros) = partition (\a -> isReallyZeroAmount a && a /= missingamt) as'
as' = map sumAmountsUsingFirstPrice $ group $ sort as as' = map sumAmountsUsingFirstPrice $ group $ sort as
sort = sortBy (\a1 a2 -> compare (sym a1) (sym a2)) sort = sortBy (\a1 a2 -> compare (sym a1) (sym a2))
group = groupBy (\a1 a2 -> sym a1 == sym a2) group = groupBy (\a1 a2 -> sym a1 == sym a2)
@ -362,7 +375,7 @@ mixedAmountWithCommodity c (Mixed as) = Amount c total Nothing
-- its component amounts. NB a mixed amount can have an empty amounts -- its component amounts. NB a mixed amount can have an empty amounts
-- list in which case it shows as \"\". -- list in which case it shows as \"\".
showMixedAmount :: MixedAmount -> String showMixedAmount :: MixedAmount -> String
showMixedAmount m = vConcatRightAligned $ map show $ amounts $ normaliseMixedAmountPreservingFirstPrice m showMixedAmount m = vConcatRightAligned $ map showAmount $ amounts $ normaliseMixedAmountPreservingFirstPrice m
-- | Set the display precision in the amount's commodities. -- | Set the display precision in the amount's commodities.
setMixedAmountPrecision :: Int -> MixedAmount -> MixedAmount setMixedAmountPrecision :: Int -> MixedAmount -> MixedAmount
@ -377,8 +390,9 @@ showMixedAmountWithPrecision p m =
-- | Get an unambiguous string representation of a mixed amount for debugging. -- | Get an unambiguous string representation of a mixed amount for debugging.
showMixedAmountDebug :: MixedAmount -> String showMixedAmountDebug :: MixedAmount -> String
showMixedAmountDebug m = printf "Mixed [%s]" as showMixedAmountDebug m | m == missingmixedamt = "(missing)"
where as = intercalate "\n " $ map showAmountDebug $ amounts $ normaliseMixedAmountPreservingFirstPrice m | otherwise = printf "Mixed [%s]" as
where as = intercalate "\n " $ map showAmountDebug $ amounts m -- $ normaliseMixedAmountPreservingFirstPrice m
-- | Get the string representation of a mixed amount, but without -- | Get the string representation of a mixed amount, but without
-- any \@ prices. -- any \@ prices.
@ -387,7 +401,7 @@ showMixedAmountWithoutPrice m = concat $ intersperse "\n" $ map showfixedwidth a
where where
(Mixed as) = normaliseMixedAmountPreservingFirstPrice $ stripPrices m (Mixed as) = normaliseMixedAmountPreservingFirstPrice $ stripPrices m
stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{price=Nothing} stripPrices (Mixed as) = Mixed $ map stripprice as where stripprice a = a{price=Nothing}
width = maximum $ map (length . show) as width = maximum $ map (length . showAmount) as
showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice showfixedwidth = printf (printf "%%%ds" width) . showAmountWithoutPrice
-- | Replace a mixed amount's commodity with the canonicalised version from -- | Replace a mixed amount's commodity with the canonicalised version from
@ -398,7 +412,9 @@ canonicaliseMixedAmountCommodity canonicalcommoditymap (Mixed as) = Mixed $ map
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- misc -- misc
tests_Hledger_Data_Amount = TestList [ tests_Hledger_Data_Amount = TestList $
tests_normaliseMixedAmountPreservingPrices
++ [
-- Amount -- Amount
@ -461,7 +477,7 @@ tests_Hledger_Data_Amount = TestList [
showMixedAmount (Mixed [(dollars 1){price=Just $ UnitPrice $ Mixed [euros 2]}]) `is` "$1.00 @ €2.00" showMixedAmount (Mixed [(dollars 1){price=Just $ UnitPrice $ Mixed [euros 2]}]) `is` "$1.00 @ €2.00"
showMixedAmount (Mixed [dollars 0]) `is` "0" showMixedAmount (Mixed [dollars 0]) `is` "0"
showMixedAmount (Mixed []) `is` "0" showMixedAmount (Mixed []) `is` "0"
showMixedAmount missingamt `is` "" showMixedAmount missingmixedamt `is` ""
,"showMixedAmountWithoutPrice" ~: do ,"showMixedAmountWithoutPrice" ~: do
let a = (dollars 1){price=Just $ UnitPrice $ Mixed [euros 2]} let a = (dollars 1){price=Just $ UnitPrice $ Mixed [euros 2]}

View File

@ -14,18 +14,17 @@ module Hledger.Data.Journal (
addTimeLogEntry, addTimeLogEntry,
addTransaction, addTransaction,
journalApplyAliases, journalApplyAliases,
journalBalanceTransactions,
journalCanonicaliseAmounts, journalCanonicaliseAmounts,
journalConvertAmountsToCost, journalConvertAmountsToCost,
journalFinalise, journalFinalise,
journalSelectingDate, journalSelectingDate,
-- * Filtering -- * Filtering
filterJournalPostings, filterJournalPostings,
filterJournalPostings2,
filterJournalTransactions, filterJournalTransactions,
filterJournalTransactions2,
filterJournalTransactionsByAccount,
-- * Querying -- * Querying
journalAccountInfo, journalAccountInfo,
journalAccountNames,
journalAccountNamesUsed, journalAccountNamesUsed,
journalAmountAndPriceCommodities, journalAmountAndPriceCommodities,
journalAmounts, journalAmounts,
@ -46,14 +45,14 @@ module Hledger.Data.Journal (
groupPostings, groupPostings,
matchpats, matchpats,
nullctx, nullctx,
nullfilterspec,
nulljournal, nulljournal,
-- * Tests -- * Tests
samplejournal,
tests_Hledger_Data_Journal, tests_Hledger_Data_Journal,
) )
where where
import Data.List import Data.List
import Data.Map (findWithDefault, (!)) import Data.Map (findWithDefault, (!), toAscList)
import Data.Ord import Data.Ord
import Data.Time.Calendar import Data.Time.Calendar
import Data.Time.LocalTime import Data.Time.LocalTime
@ -67,10 +66,11 @@ import qualified Data.Map as Map
import Hledger.Utils import Hledger.Utils
import Hledger.Data.Types import Hledger.Data.Types
import Hledger.Data.AccountName import Hledger.Data.AccountName
import Hledger.Data.Account()
import Hledger.Data.Amount import Hledger.Data.Amount
import Hledger.Data.Commodity (canonicaliseCommodities) import Hledger.Data.Commodity
import Hledger.Data.Dates (nulldatespan) import Hledger.Data.Dates
import Hledger.Data.Transaction (journalTransactionWithDate,balanceTransaction) -- nulltransaction, import Hledger.Data.Transaction
import Hledger.Data.Posting import Hledger.Data.Posting
import Hledger.Data.TimeLog import Hledger.Data.TimeLog
import Hledger.Data.Query import Hledger.Data.Query
@ -114,18 +114,6 @@ nulljournal = Journal { jmodifiertxns = []
nullctx :: JournalContext nullctx :: JournalContext
nullctx = Ctx { ctxYear = Nothing, ctxCommodity = Nothing, ctxAccount = [], ctxAliases = [] } nullctx = Ctx { ctxYear = Nothing, ctxCommodity = Nothing, ctxAccount = [], ctxAliases = [] }
nullfilterspec :: FilterSpec
nullfilterspec = FilterSpec {
datespan=nulldatespan
,cleared=Nothing
,real=False
,empty=False
,acctpats=[]
,descpats=[]
,depth=Nothing
,fMetadata=[]
}
journalFilePath :: Journal -> FilePath journalFilePath :: Journal -> FilePath
journalFilePath = fst . mainfile journalFilePath = fst . mainfile
@ -213,15 +201,16 @@ journalEquityAccountQuery _ = Acct "^equity(:|$)"
-- | Keep only postings matching the query expression. -- | Keep only postings matching the query expression.
-- This can leave unbalanced transactions. -- This can leave unbalanced transactions.
filterJournalPostings2 :: Query -> Journal -> Journal filterJournalPostings :: Query -> Journal -> Journal
filterJournalPostings2 m j@Journal{jtxns=ts} = j{jtxns=map filtertransactionpostings ts} filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map filtertransactionpostings ts}
where where
filtertransactionpostings t@Transaction{tpostings=ps} = t{tpostings=filter (m `matchesPosting`) ps} filtertransactionpostings t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps}
-- | Keep only transactions matching the query expression. -- | Keep only transactions matching the query expression.
filterJournalTransactions2 :: Query -> Journal -> Journal filterJournalTransactions :: Query -> Journal -> Journal
filterJournalTransactions2 m j@Journal{jtxns=ts} = j{jtxns=filter (m `matchesTransaction`) ts} filterJournalTransactions q j@Journal{jtxns=ts} = j{jtxns=filter (q `matchesTransaction`) ts}
{-
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
-- filtering V1 -- filtering V1
@ -324,6 +313,12 @@ filterJournalPostingsByDepth (Just d) j@Journal{jtxns=ts} =
where filtertxns t@Transaction{tpostings=ps} = where filtertxns t@Transaction{tpostings=ps} =
t{tpostings=filter ((<= d) . accountNameLevel . paccount) ps} t{tpostings=filter ((<= d) . accountNameLevel . paccount) ps}
-- | Keep only postings which affect accounts matched by the account patterns.
-- This can leave transactions unbalanced.
filterJournalPostingsByAccount :: [String] -> Journal -> Journal
filterJournalPostingsByAccount apats j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}
where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter (matchpats apats . paccount) ps}
-- | Keep only transactions which affect accounts matched by the account patterns. -- | Keep only transactions which affect accounts matched by the account patterns.
-- More precisely: each positive account pattern excludes transactions -- More precisely: each positive account pattern excludes transactions
-- which do not contain a posting to a matched account, and each negative -- which do not contain a posting to a matched account, and each negative
@ -338,11 +333,7 @@ filterJournalTransactionsByAccount apats j@Journal{jtxns=ts} = j{jtxns=filter tm
amatch pat a = regexMatchesCI (abspat pat) a amatch pat a = regexMatchesCI (abspat pat) a
(negatives,positives) = partition isnegativepat apats (negatives,positives) = partition isnegativepat apats
-- | Keep only postings which affect accounts matched by the account patterns. -}
-- This can leave transactions unbalanced.
filterJournalPostingsByAccount :: [String] -> Journal -> Journal
filterJournalPostingsByAccount apats j@Journal{jtxns=ts} = j{jtxns=map filterpostings ts}
where filterpostings t@Transaction{tpostings=ps} = t{tpostings=filter (matchpats apats . paccount) ps}
-- | Convert this journal's transactions' primary date to either the -- | Convert this journal's transactions' primary date to either the
-- actual or effective date. -- actual or effective date.
@ -487,13 +478,168 @@ journalAccountInfo j = (ant, amap)
amap = Map.fromList [(a, acctinfo a) | a <- flatten ant] amap = Map.fromList [(a, acctinfo a) | a <- flatten ant]
acctinfo a = Account a (psof a) (inclbalof a) acctinfo a = Account a (psof a) (inclbalof a)
tests_journalAccountInfo = [
"journalAccountInfo" ~: do
let (t,m) = journalAccountInfo samplejournal
assertEqual "account tree"
(Node "top" [
Node "assets" [
Node "assets:bank" [
Node "assets:bank:checking" [],
Node "assets:bank:saving" []
],
Node "assets:cash" []
],
Node "expenses" [
Node "expenses:food" [],
Node "expenses:supplies" []
],
Node "income" [
Node "income:gifts" [],
Node "income:salary" []
],
Node "liabilities" [
Node "liabilities:debts" []
]
]
)
t
mapM_
(\(e,a) -> assertEqual "" e a)
(zip [
("assets",Account "assets" [] (Mixed [dollars (-1)]))
,("assets:bank",Account "assets:bank" [] (Mixed [dollars 1]))
,("assets:bank:checking",Account "assets:bank:checking" [
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars (-1)]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars (-1)]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [nullamt]))
,("assets:bank:saving",Account "assets:bank:saving" [
Posting {
pstatus=False,
paccount="assets:bank:saving",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [dollars 1]))
,("assets:cash",Account "assets:cash" [
Posting {
pstatus=False,
paccount="assets:cash",
pamount=(Mixed [dollars (-2)]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [dollars (-2)]))
,("expenses",Account "expenses" [] (Mixed [dollars 2]))
,("expenses:food",Account "expenses:food" [
Posting {
pstatus=False,
paccount="expenses:food",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [dollars 1]))
,("expenses:supplies",Account "expenses:supplies" [
Posting {
pstatus=False,
paccount="expenses:supplies",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [dollars 1]))
,("income",Account "income" [] (Mixed [dollars (-2)]))
,("income:gifts",Account "income:gifts" [
Posting {
pstatus=False,
paccount="income:gifts",
pamount=(Mixed [dollars (-1)]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [dollars (-1)]))
,("income:salary",Account "income:salary" [
Posting {
pstatus=False,
paccount="income:salary",
pamount=(Mixed [dollars (-1)]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [dollars (-1)]))
,("liabilities",Account "liabilities" [] (Mixed [dollars 1]))
,("liabilities:debts",Account "liabilities:debts" [
Posting {
pstatus=False,
paccount="liabilities:debts",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
] (Mixed [dollars 1]))
,("top",Account "top" [] (Mixed [nullamt]))
]
(toAscList m)
)
]
-- | Given a list of postings, return an account name tree and three query -- | Given a list of postings, return an account name tree and three query
-- functions that fetch postings, subaccount-excluding-balance and -- functions that fetch postings, subaccount-excluding-balance and
-- subaccount-including-balance by account name. -- subaccount-including-balance by account name.
groupPostings :: [Posting] -> (Tree AccountName, groupPostings :: [Posting] -> (Tree AccountName,
(AccountName -> [Posting]), (AccountName -> [Posting]),
(AccountName -> MixedAmount), (AccountName -> MixedAmount),
(AccountName -> MixedAmount)) (AccountName -> MixedAmount))
groupPostings ps = (ant, psof, exclbalof, inclbalof) groupPostings ps = (ant, psof, exclbalof, inclbalof)
where where
anames = sort $ nub $ map paccount ps anames = sort $ nub $ map paccount ps
@ -532,37 +678,210 @@ postingsByAccount ps = m'
-- tests -- tests
tests_Hledger_Data_Journal = TestList [ -- A sample journal for testing, similar to data/sample.journal:
--
-- 2008/01/01 income
-- assets:bank:checking $1
-- income:salary
--
-- 2008/06/01 gift
-- assets:bank:checking $1
-- income:gifts
--
-- 2008/06/02 save
-- assets:bank:saving $1
-- assets:bank:checking
--
-- 2008/06/03 * eat & shop
-- expenses:food $1
-- expenses:supplies $1
-- assets:cash
--
-- 2008/12/31 * pay off
-- liabilities:debts $1
-- assets:bank:checking
--
Right samplejournal = journalBalanceTransactions $ Journal
[]
[]
[
txnTieKnot $ Transaction {
tdate=parsedate "2008/01/01",
teffectivedate=Nothing,
tstatus=False,
tcode="",
tdescription="income",
tcomment="",
tmetadata=[],
tpostings=[
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="income:salary",
pamount=(missingmixedamt),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
],
tpreceding_comment_lines=""
}
,
txnTieKnot $ Transaction {
tdate=parsedate "2008/06/01",
teffectivedate=Nothing,
tstatus=False,
tcode="",
tdescription="gift",
tcomment="",
tmetadata=[],
tpostings=[
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="income:gifts",
pamount=(missingmixedamt),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
],
tpreceding_comment_lines=""
}
,
txnTieKnot $ Transaction {
tdate=parsedate "2008/06/02",
teffectivedate=Nothing,
tstatus=False,
tcode="",
tdescription="save",
tcomment="",
tmetadata=[],
tpostings=[
Posting {
pstatus=False,
paccount="assets:bank:saving",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars (-1)]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
],
tpreceding_comment_lines=""
}
,
txnTieKnot $ Transaction {
tdate=parsedate "2008/06/03",
teffectivedate=Nothing,
tstatus=True,
tcode="",
tdescription="eat & shop",
tcomment="",
tmetadata=[],
tpostings=[
Posting {
pstatus=False,
paccount="expenses:food",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="expenses:supplies",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="assets:cash",
pamount=(missingmixedamt),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
],
tpreceding_comment_lines=""
}
,
txnTieKnot $ Transaction {
tdate=parsedate "2008/12/31",
teffectivedate=Nothing,
tstatus=False,
tcode="",
tdescription="pay off",
tcomment="",
tmetadata=[],
tpostings=[
Posting {
pstatus=False,
paccount="liabilities:debts",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars (-1)]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
],
tpreceding_comment_lines=""
}
]
[]
[]
""
nullctx
[]
(TOD 0 0)
tests_Hledger_Data_Journal = TestList $
tests_journalAccountInfo
-- [
-- "query standard account types" ~: -- "query standard account types" ~:
-- do -- do
-- let j = journal1 -- let j = journal1
-- journalBalanceSheetAccountNames j `is` ["assets","assets:a","equity","equity:q","equity:q:qq","liabilities","liabilities:l"] -- journalBalanceSheetAccountNames j `is` ["assets","assets:a","equity","equity:q","equity:q:qq","liabilities","liabilities:l"]
-- journalProfitAndLossAccountNames j `is` ["expenses","expenses:e","income","income:i"] -- journalProfitAndLossAccountNames j `is` ["expenses","expenses:e","income","income:i"]
-- ]
]
-- journal1 =
-- Journal
-- []
-- []
-- [
-- nulltransaction{
-- tpostings=[
-- nullposting{paccount="liabilities:l"}
-- ,nullposting{paccount="expenses:e"}
-- ]
-- }
-- ,nulltransaction{
-- tpostings=[
-- nullposting{paccount="income:i"}
-- ,nullposting{paccount="assets:a"}
-- ,nullposting{paccount="equity:q:qq"}
-- ]
-- }
-- ]
-- []
-- []
-- ""
-- nullctx
-- []
-- (TOD 0 0)

View File

@ -41,20 +41,18 @@ nullledger = Ledger{
-- | Filter a journal's transactions as specified, and then process them -- | Filter a journal's transactions as specified, and then process them
-- to derive a ledger containing all balances, the chart of accounts, -- to derive a ledger containing all balances, the chart of accounts,
-- canonicalised commodities etc. -- canonicalised commodities etc.
journalToLedger :: FilterSpec -> Journal -> Ledger journalToLedger :: Query -> Journal -> Ledger
journalToLedger fs j = nullledger{ledgerJournal=j',ledgerAccountNameTree=t,ledgerAccountMap=m} journalToLedger q j = nullledger{ledgerJournal=j',ledgerAccountNameTree=t,ledgerAccountMap=amap}
where j' = filterJournalPostings fs{depth=Nothing} j where j' = filterJournalPostings q j
(t, m) = journalAccountInfo j'
-- | Filter a journal's transactions as specified, and then process them
-- to derive a ledger containing all balances, the chart of accounts,
-- canonicalised commodities etc.
-- Like journalToLedger but uses the new queries.
journalToLedger2 :: Query -> Journal -> Ledger
journalToLedger2 m j = nullledger{ledgerJournal=j',ledgerAccountNameTree=t,ledgerAccountMap=amap}
where j' = filterJournalPostings2 m j
(t, amap) = journalAccountInfo j' (t, amap) = journalAccountInfo j'
tests_journalToLedger = [
"journalToLedger" ~: do
assertEqual "" (0) (length $ ledgerPostings $ journalToLedger Any nulljournal)
assertEqual "" (11) (length $ ledgerPostings $ journalToLedger Any samplejournal)
assertEqual "" (6) (length $ ledgerPostings $ journalToLedger (Depth 2) samplejournal)
]
-- | List a ledger's account names. -- | List a ledger's account names.
ledgerAccountNames :: Ledger -> [AccountName] ledgerAccountNames :: Ledger -> [AccountName]
ledgerAccountNames = drop 1 . flatten . ledgerAccountNameTree ledgerAccountNames = drop 1 . flatten . ledgerAccountNameTree
@ -105,7 +103,6 @@ ledgerDateSpan = postingsDateSpan . ledgerPostings
ledgerCommodities :: Ledger -> Map String Commodity ledgerCommodities :: Ledger -> Map String Commodity
ledgerCommodities = journalCanonicalCommodities . ledgerJournal ledgerCommodities = journalCanonicalCommodities . ledgerJournal
tests_Hledger_Data_Ledger = TestList tests_Hledger_Data_Ledger = TestList $
[ tests_journalToLedger
]

View File

@ -107,7 +107,7 @@ isBalancedVirtual :: Posting -> Bool
isBalancedVirtual p = ptype p == BalancedVirtualPosting isBalancedVirtual p = ptype p == BalancedVirtualPosting
hasAmount :: Posting -> Bool hasAmount :: Posting -> Bool
hasAmount = (/= missingamt) . pamount hasAmount = (/= missingmixedamt) . pamount
accountNamesFromPostings :: [Posting] -> [AccountName] accountNamesFromPostings :: [Posting] -> [AccountName]
accountNamesFromPostings = nub . map paccount accountNamesFromPostings = nub . map paccount

View File

@ -1,7 +1,7 @@
{-| {-|
A general query system for matching items by standard criteria, in one A general query system for matching things (accounts, postings,
step unlike FilterSpec and filterJournal*. Currently used by hledger-web. transactions..) by various criteria, and a parser for query expressions.
-} -}
@ -12,15 +12,22 @@ module Hledger.Data.Query (
-- * parsing -- * parsing
parseQuery, parseQuery,
simplifyQuery, simplifyQuery,
filterQuery,
-- * accessors -- * accessors
queryIsNull, queryIsNull,
queryStartDate, queryIsDepth,
queryIsDate,
queryIsStartDateOnly, queryIsStartDateOnly,
queryStartDate,
queryDateSpan,
queryDepth,
queryEmpty,
inAccount, inAccount,
inAccountQuery, inAccountQuery,
-- * matching -- * matching
matchesTransaction, matchesAccount,
matchesPosting, matchesPosting,
matchesTransaction,
-- * tests -- * tests
tests_Hledger_Data_Query tests_Hledger_Data_Query
) )
@ -55,14 +62,12 @@ data Query = Any -- ^ always match
| EDate DateSpan -- ^ match if effective date in this date span | EDate DateSpan -- ^ match if effective date in this date span
| Status Bool -- ^ match if cleared status has this value | Status Bool -- ^ match if cleared status has this value
| Real Bool -- ^ match if "realness" (involves a real non-virtual account ?) has this value | Real Bool -- ^ match if "realness" (involves a real non-virtual account ?) has this value
| Empty Bool -- ^ match if "emptiness" (from the --empty command-line flag) has this value. | Empty Bool -- ^ if true, show zero-amount postings/accounts which are usually not shown
-- Currently this means a posting with zero amount. -- more of a query option than a query criteria ?
| Depth Int -- ^ match if account depth is less than or equal to this value | Depth Int -- ^ match if account depth is less than or equal to this value
deriving (Show, Eq) deriving (Show, Eq)
-- | A query option changes a query's/report's behaviour and output in some way. -- | A query option changes a query's/report's behaviour and output in some way.
-- XXX could use regular CliOpts ?
data QueryOpt = QueryOptInAcctOnly AccountName -- ^ show an account register focussed on this account data QueryOpt = QueryOptInAcctOnly AccountName -- ^ show an account register focussed on this account
| QueryOptInAcct AccountName -- ^ as above but include sub-accounts in the account register | QueryOptInAcct AccountName -- ^ as above but include sub-accounts in the account register
-- | QueryOptCostBasis -- ^ show amounts converted to cost where possible -- | QueryOptCostBasis -- ^ show amounts converted to cost where possible
@ -77,36 +82,54 @@ data QueryOpt = QueryOptInAcctOnly AccountName -- ^ show an account register fo
-- showAccountMatcher (QueryOptInAcctSubsOnly a:_) = Just $ Acct True $ accountNameToAccountRegex a -- showAccountMatcher (QueryOptInAcctSubsOnly a:_) = Just $ Acct True $ accountNameToAccountRegex a
-- showAccountMatcher _ = Nothing -- showAccountMatcher _ = Nothing
-- | Convert a query expression containing zero or more space-separated -- | Convert a query expression containing zero or more space-separated
-- terms to a query and zero or more query options. A query term is either: -- terms to a query and zero or more query options. A query term is either:
-- --
-- 1. a search criteria, used to match transactions. This is usually a prefixed pattern such as: -- 1. a search pattern, which matches on one or more fields, eg:
-- acct:REGEXP
-- date:PERIODEXP
-- not:desc:REGEXP
-- --
-- 2. a query option, which changes behaviour in some way. There is currently one of these: -- acct:REGEXP - match the account name with a regular expression
-- inacct:FULLACCTNAME - should appear only once -- desc:REGEXP - match the transaction description
-- date:PERIODEXP - match the date with a period expression
-- --
-- Multiple search criteria are AND'ed together. -- The prefix indicates the field to match, or if there is no prefix
-- When a pattern contains spaces, it or the whole term should be enclosed in single or double quotes. -- account name is assumed.
-- A reference date is required to interpret relative dates in period expressions.
-- --
-- 2. a query option, which modifies the reporting behaviour in some
-- way. There is currently one of these, which may appear only once:
--
-- inacct:FULLACCTNAME
--
-- The usual shell quoting rules are assumed. When a pattern contains
-- whitespace, it (or the whole term including prefix) should be enclosed
-- in single or double quotes.
--
-- Period expressions may contain relative dates, so a reference date is
-- required to fully parse these.
--
-- Multiple terms are combined as follows:
-- 1. multiple account patterns are OR'd together
-- 2. multiple description patterns are OR'd together
-- 3. then all terms are AND'd together
parseQuery :: Day -> String -> (Query,[QueryOpt]) parseQuery :: Day -> String -> (Query,[QueryOpt])
parseQuery d s = (m,qopts) parseQuery d s = (q, opts)
where where
terms = words'' prefixes s terms = words'' prefixes s
(queries, qopts) = partitionEithers $ map (parseQueryTerm d) terms (pats, opts) = partitionEithers $ map (parseQueryTerm d) terms
m = case queries of [] -> Any (descpats, pats') = partition queryIsDesc pats
(m':[]) -> m' (acctpats, otherpats) = partition queryIsAcct pats'
ms -> And ms q = simplifyQuery $ And $ [Or acctpats, Or descpats] ++ otherpats
tests_parseQuery = [ tests_parseQuery = [
"parseQuery" ~: do "parseQuery" ~: do
let d = parsedate "2011/1/1" let d = nulldate -- parsedate "2011/1/1"
parseQuery d "acct:'expenses:autres d\233penses' desc:b" `is` (And [Acct "expenses:autres d\233penses", Desc "b"], []) parseQuery d "acct:'expenses:autres d\233penses' desc:b" `is` (And [Acct "expenses:autres d\233penses", Desc "b"], [])
parseQuery d "inacct:a desc:\"b b\"" `is` (Desc "b b", [QueryOptInAcct "a"]) parseQuery d "inacct:a desc:\"b b\"" `is` (Desc "b b", [QueryOptInAcct "a"])
parseQuery d "inacct:a inacct:b" `is` (Any, [QueryOptInAcct "a", QueryOptInAcct "b"]) parseQuery d "inacct:a inacct:b" `is` (Any, [QueryOptInAcct "a", QueryOptInAcct "b"])
parseQuery d "desc:'x x'" `is` (Desc "x x", [])
parseQuery d "'a a' 'b" `is` (Or [Acct "a a",Acct "'b"], [])
-- parseQuery d "a b desc:x desc:y status:1" `is`
-- (And [Or [Acct "a", Acct "b"], Or [Desc "x", Desc "y"], Status True], [])
] ]
-- keep synced with patterns below, excluding "not" -- keep synced with patterns below, excluding "not"
@ -209,26 +232,83 @@ truestrings :: [String]
truestrings = ["1","t","true"] truestrings = ["1","t","true"]
simplifyQuery :: Query -> Query simplifyQuery :: Query -> Query
simplifyQuery (And [q]) = q simplifyQuery q =
simplifyQuery q = q let q' = simplify q
in if q' == q then q else simplifyQuery q'
where
simplify (And []) = Any
simplify (And [q]) = simplify q
simplify (And qs) | same qs = simplify $ head qs
| any (==None) qs = None
| all queryIsDate qs = Date $ spansIntersect $ mapMaybe queryTermDateSpan qs
| otherwise = And $ concat $ [map simplify dateqs, map simplify otherqs]
where (dateqs, otherqs) = partition queryIsDate $ filter (/=Any) qs
simplify (Or []) = Any
simplify (Or [q]) = simplifyQuery q
simplify (Or qs) | same qs = simplify $ head qs
| any (==Any) qs = Any
-- all queryIsDate qs = Date $ spansUnion $ mapMaybe queryTermDateSpan qs ?
| otherwise = Or $ map simplify $ filter (/=None) qs
simplify (Date (DateSpan Nothing Nothing)) = Any
simplify q = q
tests_simplifyQuery = [
"simplifyQuery" ~: do
let q `gives` r = assertEqual "" r (simplifyQuery q)
Or [Acct "a"] `gives` Acct "a"
Or [Any,None] `gives` Any
And [Any,None] `gives` None
And [Any,Any] `gives` Any
And [Acct "b",Any] `gives` Acct "b"
And [Any,And [Date (DateSpan Nothing Nothing)]] `gives` Any
And [Date (DateSpan Nothing (Just $ parsedate "2013-01-01")), Date (DateSpan (Just $ parsedate "2012-01-01") Nothing)]
`gives` Date (DateSpan (Just $ parsedate "2012-01-01") (Just $ parsedate "2013-01-01"))
And [Or [],Or [Desc "b b"]] `gives` Desc "b b"
]
same [] = True
same (a:as) = all (a==) as
-- | Remove query terms (or whole sub-expressions) not matching the given
-- predicate from this query. XXX Semantics not yet clear.
filterQuery :: (Query -> Bool) -> Query -> Query
filterQuery p (And qs) = And $ filter p qs
filterQuery p (Or qs) = Or $ filter p qs
-- filterQuery p (Not q) = Not $ filterQuery p q
filterQuery p q = if p q then q else Any
tests_filterQuery = [
"filterQuery" ~: do
let (q,p) `gives` r = assertEqual "" r (filterQuery p q)
(Any, queryIsDepth) `gives` Any
(Depth 1, queryIsDepth) `gives` Depth 1
-- (And [Date nulldatespan, Not (Or [Any, Depth 1])], queryIsDepth) `gives` And [Not (Or [Depth 1])]
]
-- * accessors -- * accessors
-- | Does this query match everything ? -- | Does this query match everything ?
queryIsNull :: Query -> Bool
queryIsNull Any = True queryIsNull Any = True
queryIsNull (And []) = True queryIsNull (And []) = True
queryIsNull (Not (Or [])) = True queryIsNull (Not (Or [])) = True
queryIsNull _ = False queryIsNull _ = False
-- | What start date does this query specify, if any ? queryIsDepth :: Query -> Bool
-- If the query is an OR expression, returns the earliest of the alternatives. queryIsDepth (Depth _) = True
-- When the flag is true, look for a starting effective date instead. queryIsDepth _ = False
queryStartDate :: Bool -> Query -> Maybe Day
queryStartDate effective (Or ms) = earliestMaybeDate $ map (queryStartDate effective) ms queryIsDate :: Query -> Bool
queryStartDate effective (And ms) = latestMaybeDate $ map (queryStartDate effective) ms queryIsDate (Date _) = True
queryStartDate False (Date (DateSpan (Just d) _)) = Just d queryIsDate _ = False
queryStartDate True (EDate (DateSpan (Just d) _)) = Just d
queryStartDate _ _ = Nothing queryIsDesc :: Query -> Bool
queryIsDesc (Desc _) = True
queryIsDesc _ = False
queryIsAcct :: Query -> Bool
queryIsAcct (Acct _) = True
queryIsAcct _ = False
-- | Does this query specify a start date and nothing else (that would -- | Does this query specify a start date and nothing else (that would
-- filter postings prior to the date) ? -- filter postings prior to the date) ?
@ -242,6 +322,32 @@ queryIsStartDateOnly False (Date (DateSpan (Just _) _)) = True
queryIsStartDateOnly True (EDate (DateSpan (Just _) _)) = True queryIsStartDateOnly True (EDate (DateSpan (Just _) _)) = True
queryIsStartDateOnly _ _ = False queryIsStartDateOnly _ _ = False
-- | What start date (or effective date) does this query specify, if any ?
-- For OR expressions, use the earliest of the dates. NOT is ignored.
queryStartDate :: Bool -> Query -> Maybe Day
queryStartDate effective (Or ms) = earliestMaybeDate $ map (queryStartDate effective) ms
queryStartDate effective (And ms) = latestMaybeDate $ map (queryStartDate effective) ms
queryStartDate False (Date (DateSpan (Just d) _)) = Just d
queryStartDate True (EDate (DateSpan (Just d) _)) = Just d
queryStartDate _ _ = Nothing
queryTermDateSpan (Date span) = Just span
queryTermDateSpan _ = Nothing
-- | What date span (or effective date span) does this query specify ?
-- For OR expressions, use the widest possible span. NOT is ignored.
queryDateSpan :: Bool -> Query -> DateSpan
queryDateSpan effective q = spansUnion $ queryDateSpans effective q
-- | Extract all date (or effective date) spans specified in this query.
-- NOT is ignored.
queryDateSpans :: Bool -> Query -> [DateSpan]
queryDateSpans effective (Or qs) = concatMap (queryDateSpans effective) qs
queryDateSpans effective (And qs) = concatMap (queryDateSpans effective) qs
queryDateSpans False (Date span) = [span]
queryDateSpans True (EDate span) = [span]
queryDateSpans _ _ = []
-- | What is the earliest of these dates, where Nothing is earliest ? -- | What is the earliest of these dates, where Nothing is earliest ?
earliestMaybeDate :: [Maybe Day] -> Maybe Day earliestMaybeDate :: [Maybe Day] -> Maybe Day
earliestMaybeDate = headDef Nothing . sortBy compareMaybeDates earliestMaybeDate = headDef Nothing . sortBy compareMaybeDates
@ -257,6 +363,33 @@ compareMaybeDates Nothing (Just _) = LT
compareMaybeDates (Just _) Nothing = GT compareMaybeDates (Just _) Nothing = GT
compareMaybeDates (Just a) (Just b) = compare a b compareMaybeDates (Just a) (Just b) = compare a b
-- | The depth limit this query specifies, or a large number if none.
queryDepth :: Query -> Int
queryDepth q = case queryDepth' q of [] -> 99999
ds -> minimum ds
where
queryDepth' (Depth d) = [d]
queryDepth' (Or qs) = concatMap queryDepth' qs
queryDepth' (And qs) = concatMap queryDepth' qs
queryDepth' _ = []
-- | The empty (zero amount) status specified by this query, defaulting to false.
queryEmpty :: Query -> Bool
queryEmpty = headDef False . queryEmpty'
where
queryEmpty' (Empty v) = [v]
queryEmpty' (Or qs) = concatMap queryEmpty' qs
queryEmpty' (And qs) = concatMap queryEmpty' qs
queryEmpty' _ = []
-- -- | The "include empty" option specified by this query, defaulting to false.
-- emptyQueryOpt :: [QueryOpt] -> Bool
-- emptyQueryOpt = headDef False . emptyQueryOpt'
-- where
-- emptyQueryOpt' [] = False
-- emptyQueryOpt' (QueryOptEmpty v:_) = v
-- emptyQueryOpt' (_:vs) = emptyQueryOpt' vs
-- | The account we are currently focussed on, if any, and whether subaccounts are included. -- | The account we are currently focussed on, if any, and whether subaccounts are included.
-- Just looks at the first query option. -- Just looks at the first query option.
inAccount :: [QueryOpt] -> Maybe (AccountName,Bool) inAccount :: [QueryOpt] -> Maybe (AccountName,Bool)
@ -277,13 +410,37 @@ inAccountQuery (QueryOptInAcct a:_) = Just $ Acct $ accountNameToAccountRegex a
-- matching -- matching
-- | Does the match expression match this account ?
-- A matching in: clause is also considered a match.
matchesAccount :: Query -> AccountName -> Bool
matchesAccount (None) _ = False
matchesAccount (Not m) a = not $ matchesAccount m a
matchesAccount (Or ms) a = any (`matchesAccount` a) ms
matchesAccount (And ms) a = all (`matchesAccount` a) ms
matchesAccount (Acct r) a = regexMatchesCI r a
matchesAccount (Depth d) a = accountNameLevel a <= d
matchesAccount _ _ = True
tests_matchesAccount = [
"matchesAccount" ~: do
assertBool "positive acct match" $ matchesAccount (Acct "b:c") "a:bb:c:d"
-- assertBool "acct should match at beginning" $ not $ matchesAccount (Acct True "a:b") "c:a:b"
let q `matches` a = assertBool "" $ q `matchesAccount` a
Depth 2 `matches` "a:b"
assertBool "" $ Depth 2 `matchesAccount` "a"
assertBool "" $ Depth 2 `matchesAccount` "a:b"
assertBool "" $ not $ Depth 2 `matchesAccount` "a:b:c"
assertBool "" $ Date nulldatespan `matchesAccount` "a"
assertBool "" $ EDate nulldatespan `matchesAccount` "a"
]
-- | Does the match expression match this posting ? -- | Does the match expression match this posting ?
matchesPosting :: Query -> Posting -> Bool matchesPosting :: Query -> Posting -> Bool
matchesPosting (Not m) p = not $ matchesPosting m p matchesPosting (Not q) p = not $ q `matchesPosting` p
matchesPosting (Any) _ = True matchesPosting (Any) _ = True
matchesPosting (None) _ = False matchesPosting (None) _ = False
matchesPosting (Or ms) p = any (`matchesPosting` p) ms matchesPosting (Or qs) p = any (`matchesPosting` p) qs
matchesPosting (And ms) p = all (`matchesPosting` p) ms matchesPosting (And qs) p = all (`matchesPosting` p) qs
matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" tdescription $ ptransaction p matchesPosting (Desc r) p = regexMatchesCI r $ maybe "" tdescription $ ptransaction p
matchesPosting (Acct r) p = regexMatchesCI r $ paccount p matchesPosting (Acct r) p = regexMatchesCI r $ paccount p
matchesPosting (Date span) p = matchesPosting (Date span) p =
@ -295,8 +452,12 @@ matchesPosting (EDate span) p =
Nothing -> False Nothing -> False
matchesPosting (Status v) p = v == postingCleared p matchesPosting (Status v) p = v == postingCleared p
matchesPosting (Real v) p = v == isReal p matchesPosting (Real v) p = v == isReal p
matchesPosting (Empty v) Posting{pamount=a} = v == isZeroMixedAmount a matchesPosting (Depth d) Posting{paccount=a} = Depth d `matchesAccount` a
matchesPosting _ _ = False -- matchesPosting (Empty v) Posting{pamount=a} = v == isZeroMixedAmount a
-- matchesPosting (Empty False) Posting{pamount=a} = True
-- matchesPosting (Empty True) Posting{pamount=a} = isZeroMixedAmount a
matchesPosting (Empty _) _ = True
-- matchesPosting _ _ = False
tests_matchesPosting = [ tests_matchesPosting = [
"matchesPosting" ~: do "matchesPosting" ~: do
@ -314,50 +475,47 @@ tests_matchesPosting = [
assertBool "real:1 on real posting" $ (Real True) `matchesPosting` nullposting{ptype=RegularPosting} assertBool "real:1 on real posting" $ (Real True) `matchesPosting` nullposting{ptype=RegularPosting}
assertBool "real:1 on virtual posting fails" $ not $ (Real True) `matchesPosting` nullposting{ptype=VirtualPosting} assertBool "real:1 on virtual posting fails" $ not $ (Real True) `matchesPosting` nullposting{ptype=VirtualPosting}
assertBool "real:1 on balanced virtual posting fails" $ not $ (Real True) `matchesPosting` nullposting{ptype=BalancedVirtualPosting} assertBool "real:1 on balanced virtual posting fails" $ not $ (Real True) `matchesPosting` nullposting{ptype=BalancedVirtualPosting}
assertBool "" $ (Acct "'b") `matchesPosting` nullposting{paccount="'b"}
] ]
-- | Does the match expression match this transaction ? -- | Does the match expression match this transaction ?
matchesTransaction :: Query -> Transaction -> Bool matchesTransaction :: Query -> Transaction -> Bool
matchesTransaction (Not m) t = not $ matchesTransaction m t matchesTransaction (Not q) t = not $ q `matchesTransaction` t
matchesTransaction (Any) _ = True matchesTransaction (Any) _ = True
matchesTransaction (None) _ = False matchesTransaction (None) _ = False
matchesTransaction (Or ms) t = any (`matchesTransaction` t) ms matchesTransaction (Or qs) t = any (`matchesTransaction` t) qs
matchesTransaction (And ms) t = all (`matchesTransaction` t) ms matchesTransaction (And qs) t = all (`matchesTransaction` t) qs
matchesTransaction (Desc r) t = regexMatchesCI r $ tdescription t matchesTransaction (Desc r) t = regexMatchesCI r $ tdescription t
matchesTransaction m@(Acct _) t = any (m `matchesPosting`) $ tpostings t matchesTransaction q@(Acct _) t = any (q `matchesPosting`) $ tpostings t
matchesTransaction (Date span) t = spanContainsDate span $ tdate t matchesTransaction (Date span) t = spanContainsDate span $ tdate t
matchesTransaction (EDate span) t = spanContainsDate span $ transactionEffectiveDate t matchesTransaction (EDate span) t = spanContainsDate span $ transactionEffectiveDate t
matchesTransaction (Status v) t = v == tstatus t matchesTransaction (Status v) t = v == tstatus t
matchesTransaction (Real v) t = v == hasRealPostings t matchesTransaction (Real v) t = v == hasRealPostings t
matchesTransaction _ _ = False matchesTransaction (Empty _) _ = True
matchesTransaction (Depth d) t = any (Depth d `matchesPosting`) $ tpostings t
-- matchesTransaction _ _ = False
tests_matchesTransaction = [
"matchesTransaction" ~: do
let q `matches` t = assertBool "" $ q `matchesTransaction` t
Any `matches` nulltransaction
assertBool "" $ not $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x"}
assertBool "" $ (Desc "x x") `matchesTransaction` nulltransaction{tdescription="x x"}
]
postingEffectiveDate :: Posting -> Maybe Day postingEffectiveDate :: Posting -> Maybe Day
postingEffectiveDate p = maybe Nothing (Just . transactionEffectiveDate) $ ptransaction p postingEffectiveDate p = maybe Nothing (Just . transactionEffectiveDate) $ ptransaction p
-- | Does the match expression match this account ?
-- A matching in: clause is also considered a match.
matchesAccount :: Query -> AccountName -> Bool
matchesAccount (Not m) a = not $ matchesAccount m a
matchesAccount (Any) _ = True
matchesAccount (None) _ = False
matchesAccount (Or ms) a = any (`matchesAccount` a) ms
matchesAccount (And ms) a = all (`matchesAccount` a) ms
matchesAccount (Acct r) a = regexMatchesCI r a
matchesAccount _ _ = False
tests_matchesAccount = [
"matchesAccount" ~: do
assertBool "positive acct match" $ matchesAccount (Acct "b:c") "a:bb:c:d"
-- assertBool "acct should match at beginning" $ not $ matchesAccount (Acct True "a:b") "c:a:b"
]
-- tests -- tests
tests_Hledger_Data_Query :: Test tests_Hledger_Data_Query :: Test
tests_Hledger_Data_Query = TestList $ tests_Hledger_Data_Query = TestList $
tests_words'' tests_simplifyQuery
++ tests_words''
++ tests_filterQuery
++ tests_parseQueryTerm ++ tests_parseQueryTerm
++ tests_parseQuery ++ tests_parseQuery
++ tests_matchesAccount ++ tests_matchesAccount
++ tests_matchesPosting ++ tests_matchesPosting
++ tests_matchesTransaction

View File

@ -337,9 +337,9 @@ nonzerobalanceerror t = printf "could not balance this transaction (%s%s%s)" rms
where where
(rsum, _, bvsum) = transactionPostingBalances t (rsum, _, bvsum) = transactionPostingBalances t
rmsg | isReallyZeroMixedAmountCost rsum = "" rmsg | isReallyZeroMixedAmountCost rsum = ""
| otherwise = "real postings are off by " ++ show (costOfMixedAmount rsum) | otherwise = "real postings are off by " ++ showMixedAmount (costOfMixedAmount rsum)
bvmsg | isReallyZeroMixedAmountCost bvsum = "" bvmsg | isReallyZeroMixedAmountCost bvsum = ""
| otherwise = "balanced virtual postings are off by " ++ show (costOfMixedAmount bvsum) | otherwise = "balanced virtual postings are off by " ++ showMixedAmount (costOfMixedAmount bvsum)
sep = if not (null rmsg) && not (null bvmsg) then "; " else "" sep = if not (null rmsg) && not (null bvmsg) then "; " else ""
transactionActualDate :: Transaction -> Day transactionActualDate :: Transaction -> Day
@ -431,7 +431,7 @@ tests_Hledger_Data_Transaction = TestList $ concat [
]) ])
(showTransaction (showTransaction
(txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" [] (txnTieKnot $ Transaction (parsedate "2007/01/28") Nothing False "" "coopportunity" "" []
[Posting False "expenses:food:groceries" missingamt "" RegularPosting [] Nothing [Posting False "expenses:food:groceries" missingmixedamt "" RegularPosting [] Nothing
] "")) ] ""))
,"showTransaction" ~: do ,"showTransaction" ~: do
@ -445,7 +445,7 @@ tests_Hledger_Data_Transaction = TestList $ concat [
(showTransaction (showTransaction
(txnTieKnot $ Transaction (parsedate "2010/01/01") Nothing False "" "x" "" [] (txnTieKnot $ Transaction (parsedate "2010/01/01") Nothing False "" "x" "" []
[Posting False "a" (Mixed [Amount unknown 1 (Just $ UnitPrice $ Mixed [Amount dollar{precision=0} 2 Nothing])]) "" RegularPosting [] Nothing [Posting False "a" (Mixed [Amount unknown 1 (Just $ UnitPrice $ Mixed [Amount dollar{precision=0} 2 Nothing])]) "" RegularPosting [] Nothing
,Posting False "b" missingamt "" RegularPosting [] Nothing ,Posting False "b" missingmixedamt "" RegularPosting [] Nothing
] "")) ] ""))
,"balanceTransaction" ~: do ,"balanceTransaction" ~: do
@ -458,12 +458,12 @@ tests_Hledger_Data_Transaction = TestList $ concat [
assertBool "detect unbalanced entry, multiple missing amounts" assertBool "detect unbalanced entry, multiple missing amounts"
(isLeft $ balanceTransaction Nothing (isLeft $ balanceTransaction Nothing
(Transaction (parsedate "2007/01/28") Nothing False "" "test" "" [] (Transaction (parsedate "2007/01/28") Nothing False "" "test" "" []
[Posting False "a" missingamt "" RegularPosting [] Nothing, [Posting False "a" missingmixedamt "" RegularPosting [] Nothing,
Posting False "b" missingamt "" RegularPosting [] Nothing Posting False "b" missingmixedamt "" RegularPosting [] Nothing
] "")) ] ""))
let e = balanceTransaction Nothing (Transaction (parsedate "2007/01/28") Nothing False "" "" "" [] let e = balanceTransaction Nothing (Transaction (parsedate "2007/01/28") Nothing False "" "" "" []
[Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing, [Posting False "a" (Mixed [dollars 1]) "" RegularPosting [] Nothing,
Posting False "b" missingamt "" RegularPosting [] Nothing Posting False "b" missingmixedamt "" RegularPosting [] Nothing
] "") ] "")
assertBool "balanceTransaction allows one missing amount" (isRight e) assertBool "balanceTransaction allows one missing amount" (isRight e)
assertEqual "balancing amount is inferred" assertEqual "balancing amount is inferred"

View File

@ -254,17 +254,5 @@ data Account = Account {
aname :: AccountName, aname :: AccountName,
apostings :: [Posting], -- ^ postings in this account apostings :: [Posting], -- ^ postings in this account
abalance :: MixedAmount -- ^ sum of postings in this account and subaccounts abalance :: MixedAmount -- ^ sum of postings in this account and subaccounts
} } -- deriving (Eq) XXX
-- | A generic, pure specification of how to filter (or search) transactions and postings.
data FilterSpec = FilterSpec {
datespan :: DateSpan -- ^ only include if in this date span
,cleared :: Maybe Bool -- ^ only include if cleared\/uncleared\/don't care
,real :: Bool -- ^ only include if real\/don't care
,empty :: Bool -- ^ include if empty (ie amount is zero)
,acctpats :: [String] -- ^ only include if matching these account patterns
,descpats :: [String] -- ^ only include if matching these description patterns
,depth :: Maybe Int
,fMetadata :: [(String,String)] -- ^ only include if matching these metadata
} deriving (Show)

View File

@ -19,7 +19,9 @@ module Hledger.Read (
-- * Parsers used elsewhere -- * Parsers used elsewhere
accountname, accountname,
amount, amount,
amount',
-- * Tests -- * Tests
samplejournal,
tests_Hledger_Read, tests_Hledger_Read,
) )
where where
@ -94,6 +96,13 @@ readerForFormat s | null rs = Nothing
readJournal' :: String -> IO Journal readJournal' :: String -> IO Journal
readJournal' s = readJournal Nothing Nothing Nothing s >>= either error' return readJournal' s = readJournal Nothing Nothing Nothing s >>= either error' return
tests_readJournal' = [
"readJournal' parses sample journal" ~: do
_ <- samplejournal
assertBool "" True
]
-- | Read a Journal from this string or give an error message, using the -- | Read a Journal from this string or give an error message, using the
-- specified data format or trying all known formats. A CSV conversion -- specified data format or trying all known formats. A CSV conversion
-- rules file may be specified for better conversion of that format, -- rules file may be specified for better conversion of that format,
@ -177,8 +186,34 @@ newJournalContent = do
d <- getCurrentDay d <- getCurrentDay
return $ printf "; journal created %s by hledger\n" (show d) return $ printf "; journal created %s by hledger\n" (show d)
tests_Hledger_Read = TestList -- tests
[
samplejournal = readJournal' $ unlines
["2008/01/01 income"
," assets:bank:checking $1"
," income:salary"
,""
,"2008/06/01 gift"
," assets:bank:checking $1"
," income:gifts"
,""
,"2008/06/02 save"
," assets:bank:saving $1"
," assets:bank:checking"
,""
,"2008/06/03 * eat & shop"
," expenses:food $1"
," expenses:supplies $1"
," assets:cash"
,""
,"2008/12/31 * pay off"
," liabilities:debts $1"
," assets:bank:checking"
]
tests_Hledger_Read = TestList $
tests_readJournal'
++ [
tests_Hledger_Read_JournalReader, tests_Hledger_Read_JournalReader,
tests_Hledger_Read_TimelogReader, tests_Hledger_Read_TimelogReader,
tests_Hledger_Read_CsvReader, tests_Hledger_Read_CsvReader,

View File

@ -28,6 +28,7 @@ module Hledger.Read.JournalReader (
datetime, datetime,
accountname, accountname,
amount, amount,
amount',
emptyline, emptyline,
-- * Tests -- * Tests
tests_Hledger_Read_JournalReader tests_Hledger_Read_JournalReader
@ -383,7 +384,13 @@ tests_transaction = [
let t = parseWithCtx nullctx transaction "2009/1/1 a ;comment\n b 1\n" let t = parseWithCtx nullctx transaction "2009/1/1 a ;comment\n b 1\n"
assertBool "transaction should not include a comment in the description" assertBool "transaction should not include a comment in the description"
$ either (const False) ((== "a") . tdescription) t $ either (const False) ((== "a") . tdescription) t
assertBool "parse transaction with following whitespace line" $
isRight $ parseWithCtx nullctx transaction $ unlines [
"2012/1/1"
," a 1"
," b"
," "
]
] ]
-- | Parse a date in YYYY/MM/DD format. Fewer digits are allowed. The year -- | Parse a date in YYYY/MM/DD format. Fewer digits are allowed. The year
@ -461,7 +468,7 @@ code = try (do { many1 spacenonewline; char '(' <?> "code"; code <- anyChar `man
-- Parse the following whitespace-beginning lines as postings, posting metadata, and/or comments. -- Parse the following whitespace-beginning lines as postings, posting metadata, and/or comments.
-- complicated to handle intermixed comment and metadata lines.. make me better ? -- complicated to handle intermixed comment and metadata lines.. make me better ?
postings :: GenParser Char JournalContext [Posting] postings :: GenParser Char JournalContext [Posting]
postings = many1 posting <?> "postings" postings = many1 (try posting) <?> "postings"
-- linebeginningwithspaces :: GenParser Char JournalContext String -- linebeginningwithspaces :: GenParser Char JournalContext String
-- linebeginningwithspaces = do -- linebeginningwithspaces = do
@ -543,15 +550,15 @@ spaceandamountormissing :: GenParser Char JournalContext MixedAmount
spaceandamountormissing = spaceandamountormissing =
try (do try (do
many1 spacenonewline many1 spacenonewline
amount <|> return missingamt amount <|> return missingmixedamt
) <|> return missingamt ) <|> return missingmixedamt
tests_spaceandamountormissing = [ tests_spaceandamountormissing = [
"spaceandamountormissing" ~: do "spaceandamountormissing" ~: do
assertParseEqual (parseWithCtx nullctx spaceandamountormissing " $47.18") (Mixed [dollars 47.18]) assertParseEqual (parseWithCtx nullctx spaceandamountormissing " $47.18") (Mixed [dollars 47.18])
assertParseEqual (parseWithCtx nullctx spaceandamountormissing "$47.18") missingamt assertParseEqual (parseWithCtx nullctx spaceandamountormissing "$47.18") missingmixedamt
assertParseEqual (parseWithCtx nullctx spaceandamountormissing " ") missingamt assertParseEqual (parseWithCtx nullctx spaceandamountormissing " ") missingmixedamt
assertParseEqual (parseWithCtx nullctx spaceandamountormissing "") missingamt assertParseEqual (parseWithCtx nullctx spaceandamountormissing "") missingmixedamt
] ]
-- | Parse an amount, with an optional left or right currency symbol and -- | Parse an amount, with an optional left or right currency symbol and
@ -582,6 +589,10 @@ tests_amount = [
price=Nothing}])}]) price=Nothing}])}])
] ]
-- | Run the amount parser on a string to get the result or an error.
amount' :: String -> MixedAmount
amount' s = either (error' . show) id $ parseWithCtx nullctx amount s
leftsymbolamount :: GenParser Char JournalContext MixedAmount leftsymbolamount :: GenParser Char JournalContext MixedAmount
leftsymbolamount = do leftsymbolamount = do
sign <- optionMaybe $ string "-" sign <- optionMaybe $ string "-"
@ -865,7 +876,6 @@ tests_Hledger_Read_JournalReader = TestList $ concat [
,"endtagdirective" ~: do ,"endtagdirective" ~: do
assertParse (parseWithCtx nullctx endtagdirective "end tag \n") assertParse (parseWithCtx nullctx endtagdirective "end tag \n")
,"endtagdirective" ~: do
assertParse (parseWithCtx nullctx endtagdirective "pop \n") assertParse (parseWithCtx nullctx endtagdirective "pop \n")
,"accountname" ~: do ,"accountname" ~: do
@ -874,13 +884,6 @@ tests_Hledger_Read_JournalReader = TestList $ concat [
assertBool "accountname rejects an empty leading component" (isLeft $ parsewith accountname ":b:c") assertBool "accountname rejects an empty leading component" (isLeft $ parsewith accountname ":b:c")
assertBool "accountname rejects an empty trailing component" (isLeft $ parsewith accountname "a:b:") assertBool "accountname rejects an empty trailing component" (isLeft $ parsewith accountname "a:b:")
,"amount" ~: do
let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity
assertMixedAmountParse parseresult mixedamount =
(either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)
assertMixedAmountParse (parseWithCtx nullctx amount "1 @ $2")
(Mixed [Amount unknown 1 (Just $ UnitPrice $ Mixed [Amount dollar{precision=0} 2 Nothing])])
,"leftsymbolamount" ~: do ,"leftsymbolamount" ~: do
assertParseEqual (parseWithCtx nullctx leftsymbolamount "$1") assertParseEqual (parseWithCtx nullctx leftsymbolamount "$1")
(Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} 1 Nothing]) (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} 1 Nothing])
@ -889,6 +892,13 @@ tests_Hledger_Read_JournalReader = TestList $ concat [
assertParseEqual (parseWithCtx nullctx leftsymbolamount "-$1") assertParseEqual (parseWithCtx nullctx leftsymbolamount "-$1")
(Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} (-1) Nothing]) (Mixed [Amount Commodity {symbol="$",side=L,spaced=False,decimalpoint='.',precision=0,separator=',',separatorpositions=[]} (-1) Nothing])
,"amount" ~: do
let -- | compare a parse result with a MixedAmount, showing the debug representation for clarity
assertMixedAmountParse parseresult mixedamount =
(either (const "parse error") showMixedAmountDebug parseresult) ~?= (showMixedAmountDebug mixedamount)
assertMixedAmountParse (parseWithCtx nullctx amount "1 @ $2")
(Mixed [Amount unknown 1 (Just $ UnitPrice $ Mixed [Amount dollar{precision=0} 2 Nothing])])
]] ]]
entry1_str = unlines entry1_str = unlines

View File

@ -20,8 +20,8 @@ module Hledger.Reports (
whichDateFromOpts, whichDateFromOpts,
journalSelectingDateFromOpts, journalSelectingDateFromOpts,
journalSelectingAmountFromOpts, journalSelectingAmountFromOpts,
filterSpecFromOpts,
queryFromOpts, queryFromOpts,
queryOptsFromOpts,
-- * Entries report -- * Entries report
EntriesReport, EntriesReport,
EntriesReportItem, EntriesReportItem,
@ -42,7 +42,6 @@ module Hledger.Reports (
AccountsReport, AccountsReport,
AccountsReportItem, AccountsReportItem,
accountsReport, accountsReport,
accountsReport2,
isInteresting, isInteresting,
-- * Tests -- * Tests
tests_Hledger_Reports tests_Hledger_Reports
@ -54,17 +53,22 @@ import Data.List
import Data.Maybe import Data.Maybe
import Data.Ord import Data.Ord
import Data.Time.Calendar import Data.Time.Calendar
import Data.Tree -- import Data.Tree
import Safe (headMay, lastMay) import Safe (headMay, lastMay)
import System.Console.CmdArgs -- for defaults support import System.Console.CmdArgs -- for defaults support
import System.Time (ClockTime(TOD))
import Test.HUnit import Test.HUnit
import Text.ParserCombinators.Parsec import Text.ParserCombinators.Parsec
import Text.Printf import Text.Printf
import Hledger.Data import Hledger.Data
import Hledger.Read (amount')
import Hledger.Data.Query
import Hledger.Utils import Hledger.Utils
-- standard report options, used in hledger-lib and above -- | Standard options for customising report filtering and output,
-- corresponding to hledger's command-line options and query language
-- arguments. Used in hledger-lib and above.
data ReportOpts = ReportOpts { data ReportOpts = ReportOpts {
begin_ :: Maybe Day begin_ :: Maybe Day
,end_ :: Maybe Day ,end_ :: Maybe Day
@ -78,16 +82,16 @@ data ReportOpts = ReportOpts {
,empty_ :: Bool ,empty_ :: Bool
,no_elide_ :: Bool ,no_elide_ :: Bool
,real_ :: Bool ,real_ :: Bool
,flat_ :: Bool -- balance ,flat_ :: Bool -- for balance command
,drop_ :: Int -- balance ,drop_ :: Int -- "
,no_total_ :: Bool -- balance ,no_total_ :: Bool -- "
,daily_ :: Bool ,daily_ :: Bool
,weekly_ :: Bool ,weekly_ :: Bool
,monthly_ :: Bool ,monthly_ :: Bool
,quarterly_ :: Bool ,quarterly_ :: Bool
,yearly_ :: Bool ,yearly_ :: Bool
,format_ :: Maybe FormatStr ,format_ :: Maybe FormatStr
,patterns_ :: [String] ,query_ :: String -- all arguments, as a string
} deriving (Show) } deriving (Show)
type DisplayExp = String type DisplayExp = String
@ -167,59 +171,48 @@ journalSelectingAmountFromOpts opts
| cost_ opts = journalConvertAmountsToCost | cost_ opts = journalConvertAmountsToCost
| otherwise = id | otherwise = id
-- | Convert report options to a (old) filter specification. -- | Convert report options and arguments to a query.
filterSpecFromOpts :: ReportOpts -> Day -> FilterSpec queryFromOpts :: Day -> ReportOpts -> Query
filterSpecFromOpts opts@ReportOpts{..} d = FilterSpec { queryFromOpts d opts@ReportOpts{..} = simplifyQuery $ And $ [flagsq, argsq]
datespan=dateSpanFromOpts d opts where
,cleared= clearedValueFromOpts opts flagsq = And $
,real=real_ [Date $ dateSpanFromOpts d opts]
,empty=empty_ ++ (if real_ then [Real True] else [])
,acctpats=apats ++ (if empty_ then [Empty True] else []) -- ?
,descpats=dpats ++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts))
,depth = depth_ ++ (maybe [] ((:[]) . Depth) depth_)
,fMetadata = mds argsq = fst $ parseQuery d query_
}
where (apats,dpats,mds) = parsePatternArgs patterns_
-- | Convert report options to a (new) query. tests_queryFromOpts = [
queryFromOpts :: ReportOpts -> Day -> (Query, [QueryOpt]) "queryFromOpts" ~: do
queryFromOpts opts@ReportOpts{..} d = -- strace $ assertEqual "" Any (queryFromOpts nulldate defreportopts)
(And $ assertEqual "" (Acct "a") (queryFromOpts nulldate defreportopts{query_="a"})
[Date $ dateSpanFromOpts d opts] assertEqual "" (Desc "a a") (queryFromOpts nulldate defreportopts{query_="desc:'a a'"})
++ (if null apats then [] else [Or $ map Acct apats]) assertEqual "" (Date $ mkdatespan "2012/01/01" "2013/01/01")
++ (if null dpats then [] else [Or $ map Desc dpats]) (queryFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")
-- ++ (if null mds then [] else [Or $ map MatchMetadata mds]) ,query_="date:'to 2013'"
++ (if real_ then [Real True] else []) })
++ (if empty_ then [Empty True] else []) assertEqual "" (EDate $ mkdatespan "2012/01/01" "2013/01/01")
++ (maybe [] ((:[]) . Status) (clearedValueFromOpts opts)) (queryFromOpts nulldate defreportopts{query_="edate:'in 2012'"})
++ (maybe [] ((:[]) . Depth) depth_) assertEqual "" (Or [Acct "a a", Acct "'b"])
,[]) (queryFromOpts nulldate defreportopts{query_="'a a' 'b"})
where ]
(apats,dpats,_) = parsePatternArgs patterns_
-- queryFromOpts :: ReportOpts -> Day -> (Query, [QueryOpt]) -- | Convert report options and arguments to query options.
-- queryFromOpts opts d = parseQuery d (unwords $ patterns_ opts) queryOptsFromOpts :: Day -> ReportOpts -> [QueryOpt]
queryOptsFromOpts d ReportOpts{..} = flagsqopts ++ argsqopts
where
flagsqopts = []
argsqopts = snd $ parseQuery d query_
-- | Gather filter pattern arguments into a list of account patterns and a tests_queryOptsFromOpts = [
-- list of description patterns. We interpret pattern arguments as "queryOptsFromOpts" ~: do
-- follows: those prefixed with "desc:" are description patterns, all assertEqual "" [] (queryOptsFromOpts nulldate defreportopts)
-- others are account patterns; also patterns prefixed with "not:" are assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{query_="a"})
-- negated. not: should come after desc: if both are used. assertEqual "" [] (queryOptsFromOpts nulldate defreportopts{begin_=Just (parsedate "2012/01/01")
-- pattern "tag" means the word after it should be interpreted as metadata ,query_="date:'to 2013'"
-- constraint. })
parsePatternArgs :: [String] -> ([String],[String],[(String,String)]) ]
parsePatternArgs args = (as, ds', mds)
where
(tags, args') = filterOutTags False [] [] args
descprefix = "desc:"
(ds, as) = partition (descprefix `isPrefixOf`) args'
ds' = map (drop (length descprefix)) ds
mds = map (\(a,b)->(a,tail b)) $ map (\t->span (/='=') t) tags
filterOutTags _ tags args' [] = (reverse tags, reverse args')
filterOutTags False tags args' ("tag":xs) = filterOutTags True tags args' xs
filterOutTags False tags args' (x:xs) = filterOutTags False tags (x:args') xs
filterOutTags True tags args' (x:xs) = filterOutTags False (x:tags) args' xs
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -230,23 +223,24 @@ type EntriesReport = [EntriesReportItem]
type EntriesReportItem = Transaction type EntriesReportItem = Transaction
-- | Select transactions for an entries report. -- | Select transactions for an entries report.
entriesReport :: ReportOpts -> FilterSpec -> Journal -> EntriesReport -- "The print command selects transactions which
entriesReport opts fspec j = sortBy (comparing f) $ jtxns $ filterJournalTransactions fspec j' -- @
-- match any of the description patterns
-- and have any postings matching any of the positive account patterns
-- and have no postings matching any of the negative account patterns"
-- @
entriesReport :: ReportOpts -> Query -> Journal -> EntriesReport
entriesReport opts q j =
sortBy (comparing date) $ filter (q `matchesTransaction`) ts
where where
f = transactionDateFn opts date = transactionDateFn opts
j' = journalSelectingAmountFromOpts opts j ts = jtxns $ journalSelectingAmountFromOpts opts j
-- | Select transactions for an entries report. tests_entriesReport = [
entriesReport2 :: ReportOpts -> Query -> Journal -> EntriesReport "entriesReport" ~: do
entriesReport2 opts q j = assertEqual "not acct" 1 (length $ entriesReport defreportopts (Not $ Acct "bank") samplejournal)
sortBy (comparing f) $ filter (not . null . tpostings) $ map (filterTransactionPostings q) $ jtxns j' let span = mkdatespan "2008/06/01" "2008/07/01"
where assertEqual "date" 3 (length $ entriesReport defreportopts (Date $ span) samplejournal)
f = transactionDateFn opts
j' = journalSelectingAmountFromOpts opts j
tests_entriesReport2 = [
"entriesReport2" ~: do
assertEqual "" [] (entriesReport2 defreportopts Any nulljournal)
] ]
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
@ -257,32 +251,37 @@ type PostingsReport = (String -- label for the running balance col
,[PostingsReportItem] -- line items, one per posting ,[PostingsReportItem] -- line items, one per posting
) )
type PostingsReportItem = (Maybe (Day, String) -- transaction date and description if this is the first posting type PostingsReportItem = (Maybe (Day, String) -- transaction date and description if this is the first posting
,Posting -- the posting ,Posting -- the posting, possibly with account name depth-clipped
,MixedAmount -- the running total after this posting ,MixedAmount -- the running total after this posting
) )
-- | Select postings from the journal and add running balance and other -- | Select postings from the journal and add running balance and other
-- information to make a postings report. Used by eg hledger's register command. -- information to make a postings report. Used by eg hledger's register command.
postingsReport :: ReportOpts -> FilterSpec -> Journal -> PostingsReport postingsReport :: ReportOpts -> Query -> Journal -> PostingsReport
postingsReport opts fspec j = (totallabel, postingsReportItems ps nullposting startbal (+)) postingsReport opts q j = (totallabel, postingsReportItems ps nullposting depth startbal (+))
where where
ps | interval == NoInterval = displayableps ps | interval == NoInterval = displayableps
| otherwise = summarisePostingsByInterval interval depth empty reportspan displayableps | otherwise = summarisePostingsByInterval interval depth empty reportspan displayableps
j' = journalSelectingDateFromOpts opts j' = journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j
$ journalSelectingAmountFromOpts opts -- don't do depth filtering until the end
j (depth, q') = (queryDepth q, filterQuery (not . queryIsDepth) q)
(precedingps, displayableps, _) = postingsMatchingDisplayExpr (display_ opts) (precedingps, displayableps, _) = dbg "ps3" $ postingsMatchingDisplayExpr (display_ opts)
$ depthClipPostings depth $ dbg "ps2" $ filter (q' `matchesPosting`)
$ journalPostings $ dbg "ps1" $ journalPostings j'
$ filterJournalPostings fspec{depth=Nothing} dbg :: Show a => String -> a -> a
j' -- dbg = ltrace
(interval, depth, empty, displayexpr) = (intervalFromOpts opts, depth_ opts, empty_ opts, display_ opts) dbg = flip const
empty = queryEmpty q
displayexpr = display_ opts -- XXX
interval = intervalFromOpts opts -- XXX
journalspan = journalDateSpan j' journalspan = journalDateSpan j'
-- requestedspan should be the intersection of any span specified -- requestedspan should be the intersection of any span specified
-- with period options and any span specified with display option. -- with period options and any span specified with display option.
-- The latter is not easily available, fake it for now. -- The latter is not easily available, fake it for now.
requestedspan = periodspan `spanIntersect` displayspan requestedspan = periodspan `spanIntersect` displayspan
periodspan = datespan fspec periodspan = queryDateSpan effectivedate q
effectivedate = whichDateFromOpts opts == EffectiveDate
displayspan = postingsDateSpan ps displayspan = postingsDateSpan ps
where (_,ps,_) = postingsMatchingDisplayExpr displayexpr $ journalPostings j' where (_,ps,_) = postingsMatchingDisplayExpr displayexpr $ journalPostings j'
matchedspan = postingsDateSpan displayableps matchedspan = postingsDateSpan displayableps
@ -290,21 +289,184 @@ postingsReport opts fspec j = (totallabel, postingsReportItems ps nullposting st
| otherwise = requestedspan `spanIntersect` matchedspan | otherwise = requestedspan `spanIntersect` matchedspan
startbal = sumPostings precedingps startbal = sumPostings precedingps
tests_postingsReport = [
"postingsReport" ~: do
let (query, journal) `gives` n = (length $ snd $ postingsReport defreportopts query journal) `is` n
(Any, nulljournal) `gives` 0
(Any, samplejournal) `gives` 11
-- register --depth just clips account names
(Depth 2, samplejournal) `gives` 11
-- (Depth 2, samplejournal) `gives` 6
-- (Depth 1, samplejournal) `gives` 4
assertEqual "" 11 (length $ snd $ postingsReport defreportopts Any samplejournal)
assertEqual "" 9 (length $ snd $ postingsReport defreportopts{monthly_=True} Any samplejournal)
assertEqual "" 19 (length $ snd $ postingsReport defreportopts{monthly_=True} (Empty True) samplejournal)
-- (defreportopts, And [Acct "a a", Acct "'b"], samplejournal2) `gives` 0
-- [(Just (parsedate "2008-01-01","income"),assets:bank:checking $1,$1)
-- ,(Nothing,income:salary $-1,0)
-- ,(Just (2008-06-01,"gift"),assets:bank:checking $1,$1)
-- ,(Nothing,income:gifts $-1,0)
-- ,(Just (2008-06-02,"save"),assets:bank:saving $1,$1)
-- ,(Nothing,assets:bank:checking $-1,0)
-- ,(Just (2008-06-03,"eat & shop"),expenses:food $1,$1)
-- ,(Nothing,expenses:supplies $1,$2)
-- ,(Nothing,assets:cash $-2,0)
-- ,(Just (2008-12-31,"pay off"),liabilities:debts $1,$1)
-- ,(Nothing,assets:bank:checking $-1,0)
-- ]
{-
let opts = defreportopts
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
["2008/01/01 income assets:bank:checking $1 $1"
," income:salary $-1 0"
,"2008/06/01 gift assets:bank:checking $1 $1"
," income:gifts $-1 0"
,"2008/06/02 save assets:bank:saving $1 $1"
," assets:bank:checking $-1 0"
,"2008/06/03 eat & shop expenses:food $1 $1"
," expenses:supplies $1 $2"
," assets:cash $-2 0"
,"2008/12/31 pay off liabilities:debts $1 $1"
," assets:bank:checking $-1 0"
]
,"postings report with cleared option" ~:
do
let opts = defreportopts{cleared_=True}
j <- readJournal' sample_journal_str
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
["2008/06/03 eat & shop expenses:food $1 $1"
," expenses:supplies $1 $2"
," assets:cash $-2 0"
,"2008/12/31 pay off liabilities:debts $1 $1"
," assets:bank:checking $-1 0"
]
,"postings report with uncleared option" ~:
do
let opts = defreportopts{uncleared_=True}
j <- readJournal' sample_journal_str
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
["2008/01/01 income assets:bank:checking $1 $1"
," income:salary $-1 0"
,"2008/06/01 gift assets:bank:checking $1 $1"
," income:gifts $-1 0"
,"2008/06/02 save assets:bank:saving $1 $1"
," assets:bank:checking $-1 0"
]
,"postings report sorts by date" ~:
do
j <- readJournal' $ unlines
["2008/02/02 a"
," b 1"
," c"
,""
,"2008/01/01 d"
," e 1"
," f"
]
let opts = defreportopts
registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/02/02"]
,"postings report with account pattern" ~:
do
j <- samplejournal
let opts = defreportopts{patterns_=["cash"]}
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
["2008/06/03 eat & shop assets:cash $-2 $-2"
]
,"postings report with account pattern, case insensitive" ~:
do
j <- samplejournal
let opts = defreportopts{patterns_=["cAsH"]}
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
["2008/06/03 eat & shop assets:cash $-2 $-2"
]
,"postings report with display expression" ~:
do
j <- samplejournal
let gives displayexpr =
(registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is`)
where opts = defreportopts{display_=Just displayexpr}
"d<[2008/6/2]" `gives` ["2008/01/01","2008/06/01"]
"d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"]
"d=[2008/6/2]" `gives` ["2008/06/02"]
"d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"]
"d>[2008/6/2]" `gives` ["2008/06/03","2008/12/31"]
,"postings report with period expression" ~:
do
j <- samplejournal
let periodexpr `gives` dates = do
j' <- samplejournal
registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j') `is` dates
where opts = defreportopts{period_=maybePeriod date1 periodexpr}
"" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]
"2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]
"2007" `gives` []
"june" `gives` ["2008/06/01","2008/06/02","2008/06/03"]
"monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"]
"quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"]
let opts = defreportopts{period_=maybePeriod date1 "yearly"}
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
["2008/01/01 - 2008/12/31 assets:bank:saving $1 $1"
," assets:cash $-2 $-1"
," expenses:food $1 0"
," expenses:supplies $1 $1"
," income:gifts $-1 0"
," income:salary $-1 $-1"
," liabilities:debts $1 0"
]
let opts = defreportopts{period_=maybePeriod date1 "quarterly"}
registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/10/01"]
let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True}
registerdates (postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]
]
, "postings report with depth arg" ~:
do
j <- samplejournal
let opts = defreportopts{depth_=Just 2}
(postingsReportAsText opts $ postingsReport opts (queryFromOpts date1 opts) j) `is` unlines
["2008/01/01 income assets:bank $1 $1"
," income:salary $-1 0"
,"2008/06/01 gift assets:bank $1 $1"
," income:gifts $-1 0"
,"2008/06/02 save assets:bank $1 $1"
," assets:bank $-1 0"
,"2008/06/03 eat & shop expenses:food $1 $1"
," expenses:supplies $1 $2"
," assets:cash $-2 0"
,"2008/12/31 pay off liabilities:debts $1 $1"
," assets:bank $-1 0"
]
-}
]
totallabel = "Total" totallabel = "Total"
balancelabel = "Balance" balancelabel = "Balance"
-- | Generate postings report line items. -- | Generate postings report line items.
postingsReportItems :: [Posting] -> Posting -> MixedAmount -> (MixedAmount -> MixedAmount -> MixedAmount) -> [PostingsReportItem] postingsReportItems :: [Posting] -> Posting -> Int -> MixedAmount -> (MixedAmount -> MixedAmount -> MixedAmount) -> [PostingsReportItem]
postingsReportItems [] _ _ _ = [] postingsReportItems [] _ _ _ _ = []
postingsReportItems (p:ps) pprev b sumfn = i:(postingsReportItems ps p b' sumfn) postingsReportItems (p:ps) pprev d b sumfn = i:(postingsReportItems ps p d b' sumfn)
where where
i = mkpostingsReportItem isfirst p b' i = mkpostingsReportItem isfirst p' b'
p' = p{paccount=clipAccountName d $ paccount p}
isfirst = ptransaction p /= ptransaction pprev isfirst = ptransaction p /= ptransaction pprev
b' = b `sumfn` pamount p b' = b `sumfn` pamount p
-- | Generate one postings report line item, from a flag indicating -- | Generate one postings report line item, given a flag indicating
-- whether to include transaction info, a posting, and the current running -- whether to include transaction info, the posting, and the current
-- balance. -- running balance.
mkpostingsReportItem :: Bool -> Posting -> MixedAmount -> PostingsReportItem mkpostingsReportItem :: Bool -> Posting -> MixedAmount -> PostingsReportItem
mkpostingsReportItem False p b = (Nothing, p, b) mkpostingsReportItem False p b = (Nothing, p, b)
mkpostingsReportItem True p b = (ds, p, b) mkpostingsReportItem True p b = (ds, p, b)
@ -348,26 +510,31 @@ datedisplayexpr = do
where where
compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"] compareop = choice $ map (try . string) ["<=",">=","==","<","=",">"]
-- | Clip the account names to the specified depth in a list of postings. -- -- | Clip the account names to the specified depth in a list of postings.
depthClipPostings :: Maybe Int -> [Posting] -> [Posting] -- depthClipPostings :: Maybe Int -> [Posting] -> [Posting]
depthClipPostings depth = map (depthClipPosting depth) -- depthClipPostings depth = map (depthClipPosting depth)
-- | Clip a posting's account name to the specified depth. -- -- | Clip a posting's account name to the specified depth.
depthClipPosting :: Maybe Int -> Posting -> Posting -- depthClipPosting :: Maybe Int -> Posting -> Posting
depthClipPosting Nothing p = p -- depthClipPosting Nothing p = p
depthClipPosting (Just d) p@Posting{paccount=a} = p{paccount=clipAccountName d a} -- depthClipPosting (Just d) p@Posting{paccount=a} = p{paccount=clipAccountName d a}
-- XXX confusing, refactor -- XXX confusing, refactor
-- | Convert a list of postings into summary postings. Summary postings -- | Convert a list of postings into summary postings. Summary postings
-- are one per account per interval and aggregated to the specified depth -- are one per account per interval and aggregated to the specified depth
-- if any. -- if any.
summarisePostingsByInterval :: Interval -> Maybe Int -> Bool -> DateSpan -> [Posting] -> [Posting] summarisePostingsByInterval :: Interval -> Int -> Bool -> DateSpan -> [Posting] -> [Posting]
summarisePostingsByInterval interval depth empty reportspan ps = concatMap summarisespan $ splitSpan interval reportspan summarisePostingsByInterval interval depth empty reportspan ps = concatMap summarisespan $ splitSpan interval reportspan
where where
summarisespan s = summarisePostingsInDateSpan s depth empty (postingsinspan s) summarisespan s = summarisePostingsInDateSpan s depth empty (postingsinspan s)
postingsinspan s = filter (isPostingInDateSpan s) ps postingsinspan s = filter (isPostingInDateSpan s) ps
tests_summarisePostingsByInterval = [
"summarisePostingsByInterval" ~: do
summarisePostingsByInterval (Quarters 1) 99999 False (DateSpan Nothing Nothing) [] ~?= []
]
-- | Given a date span (representing a reporting interval) and a list of -- | Given a date span (representing a reporting interval) and a list of
-- postings within it: aggregate the postings so there is only one per -- postings within it: aggregate the postings so there is only one per
-- account, and adjust their date/description so that they will render -- account, and adjust their date/description so that they will render
@ -381,7 +548,7 @@ summarisePostingsByInterval interval depth empty reportspan ps = concatMap summa
-- --
-- The showempty flag includes spans with no postings and also postings -- The showempty flag includes spans with no postings and also postings
-- with 0 amount. -- with 0 amount.
summarisePostingsInDateSpan :: DateSpan -> Maybe Int -> Bool -> [Posting] -> [Posting] summarisePostingsInDateSpan :: DateSpan -> Int -> Bool -> [Posting] -> [Posting]
summarisePostingsInDateSpan (DateSpan b e) depth showempty ps summarisePostingsInDateSpan (DateSpan b e) depth showempty ps
| null ps && (isNothing b || isNothing e) = [] | null ps && (isNothing b || isNothing e) = []
| null ps && showempty = [summaryp] | null ps && showempty = [summaryp]
@ -397,9 +564,8 @@ summarisePostingsInDateSpan (DateSpan b e) depth showempty ps
anames = sort $ nub $ map paccount ps anames = sort $ nub $ map paccount ps
-- aggregate balances by account, like journalToLedger, then do depth-clipping -- aggregate balances by account, like journalToLedger, then do depth-clipping
(_,_,exclbalof,inclbalof) = groupPostings ps (_,_,exclbalof,inclbalof) = groupPostings ps
clippedanames = nub $ map (clipAccountName d) anames clippedanames = nub $ map (clipAccountName depth) anames
isclipped a = accountNameLevel a >= d isclipped a = accountNameLevel a >= depth
d = fromMaybe 99999 $ depth
balancetoshowfor a = balancetoshowfor a =
(if isclipped a then inclbalof else exclbalof) (if null a then "top" else a) (if isclipped a then inclbalof else exclbalof) (if null a then "top" else a)
@ -534,29 +700,18 @@ type AccountsReportItem = (AccountName -- full account name
,MixedAmount) -- account balance, includes subs unless --flat is present ,MixedAmount) -- account balance, includes subs unless --flat is present
-- | Select accounts, and get their balances at the end of the selected -- | Select accounts, and get their balances at the end of the selected
-- period, and misc. display information, for an accounts report. Used by -- period, and misc. display information, for an accounts report.
-- eg hledger's balance command. accountsReport :: ReportOpts -> Query -> Journal -> AccountsReport
accountsReport :: ReportOpts -> FilterSpec -> Journal -> AccountsReport accountsReport opts query j = (items, total)
accountsReport opts filterspec j = accountsReport' opts j (journalToLedger filterspec)
-- | Select accounts, and get their balances at the end of the selected
-- period, and misc. display information, for an accounts report. Like
-- "accountsReport" but uses the new queries. Used by eg hledger-web's
-- accounts sidebar.
accountsReport2 :: ReportOpts -> Query -> Journal -> AccountsReport
accountsReport2 opts query j = accountsReport' opts j (journalToLedger2 query)
-- Accounts report helper.
accountsReport' :: ReportOpts -> Journal -> (Journal -> Ledger) -> AccountsReport
accountsReport' opts j jtol = (items, total)
where where
items = map mkitem interestingaccts -- don't do depth filtering until the end
q' = filterQuery (not . queryIsDepth) query
l = journalToLedger q' $ journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j
acctnames = filter (query `matchesAccount`) $ journalAccountNames j
interestingaccts | no_elide_ opts = acctnames interestingaccts | no_elide_ opts = acctnames
| otherwise = filter (isInteresting opts l) acctnames | otherwise = filter (isInteresting opts l) acctnames
acctnames = sort $ tail $ flatten $ treemap aname accttree items = map mkitem interestingaccts
accttree = ledgerAccountTree (fromMaybe 99999 $ depth_ opts) l
total = sum $ map abalance $ ledgerTopAccounts l total = sum $ map abalance $ ledgerTopAccounts l
l = jtol $ journalSelectingDateFromOpts opts $ journalSelectingAmountFromOpts opts j
-- | Get data for one balance report line item. -- | Get data for one balance report line item.
mkitem :: AccountName -> AccountsReportItem mkitem :: AccountName -> AccountsReportItem
@ -573,6 +728,223 @@ accountsReport' opts j jtol = (items, total)
| otherwise = abalance acct | otherwise = abalance acct
where acct = ledgerAccount l a where acct = ledgerAccount l a
tests_accountsReport = [
"accountsReport" ~: do
let (opts,journal) `gives` r = do
let (eitems, etotal) = r
(aitems, atotal) = accountsReport opts (queryFromOpts nulldate opts) journal
assertEqual "items" eitems aitems
-- assertEqual "" (length eitems) (length aitems)
-- mapM (\(e,a) -> assertEqual "" e a) $ zip eitems aitems
assertEqual "total" etotal atotal
-- "accounts report with no args" ~:
(defreportopts, nulljournal) `gives` ([], Mixed [nullamt])
(defreportopts, samplejournal) `gives`
([
("assets","assets",0, amount' "$-1.00")
,("assets:bank:saving","bank:saving",1, amount' "$1.00")
,("assets:cash","cash",1, amount' "$-2.00")
,("expenses","expenses",0, amount' "$2.00")
,("expenses:food","food",1, amount' "$1.00")
,("expenses:supplies","supplies",1, amount' "$1.00")
,("income","income",0, amount' "$-2.00")
,("income:gifts","gifts",1, amount' "$-1.00")
,("income:salary","salary",1, amount' "$-1.00")
,("liabilities:debts","liabilities:debts",0, amount' "$1.00")
],
Mixed [nullamt])
-- "accounts report can be limited with --depth=N" ~:
(defreportopts{depth_=Just 1}, samplejournal) `gives`
([
("assets", "assets", 0, amount' "$-1.00")
,("expenses", "expenses", 0, amount' "$2.00")
,("income", "income", 0, amount' "$-2.00")
,("liabilities", "liabilities", 0, amount' "$1.00")
],
Mixed [nullamt])
-- or with depth:N
(defreportopts{query_="depth:1"}, samplejournal) `gives`
([
("assets", "assets", 0, amount' "$-1.00")
,("expenses", "expenses", 0, amount' "$2.00")
,("income", "income", 0, amount' "$-2.00")
,("liabilities", "liabilities", 0, amount' "$1.00")
],
Mixed [nullamt])
-- with a date span
(defreportopts{query_="date:'in 2009'"}, samplejournal2) `gives`
([],
Mixed [nullamt])
(defreportopts{query_="edate:'in 2009'"}, samplejournal2) `gives`
([
("assets:bank:checking","assets:bank:checking",0,amount' "$1.00")
,("income:salary","income:salary",0,amount' "$-1.00")
],
Mixed [nullamt])
{-
,"accounts report with account pattern o" ~:
defreportopts{patterns_=["o"]} `gives`
[" $1 expenses:food"
," $-2 income"
," $-1 gifts"
," $-1 salary"
,"--------------------"
," $-1"
]
,"accounts report with account pattern o and --depth 1" ~:
defreportopts{patterns_=["o"],depth_=Just 1} `gives`
[" $1 expenses"
," $-2 income"
,"--------------------"
," $-1"
]
,"accounts report with account pattern a" ~:
defreportopts{patterns_=["a"]} `gives`
[" $-1 assets"
," $1 bank:saving"
," $-2 cash"
," $-1 income:salary"
," $1 liabilities:debts"
,"--------------------"
," $-1"
]
,"accounts report with account pattern e" ~:
defreportopts{patterns_=["e"]} `gives`
[" $-1 assets"
," $1 bank:saving"
," $-2 cash"
," $2 expenses"
," $1 food"
," $1 supplies"
," $-2 income"
," $-1 gifts"
," $-1 salary"
," $1 liabilities:debts"
,"--------------------"
," 0"
]
,"accounts report with unmatched parent of two matched subaccounts" ~:
defreportopts{patterns_=["cash","saving"]} `gives`
[" $-1 assets"
," $1 bank:saving"
," $-2 cash"
,"--------------------"
," $-1"
]
,"accounts report with multi-part account name" ~:
defreportopts{patterns_=["expenses:food"]} `gives`
[" $1 expenses:food"
,"--------------------"
," $1"
]
,"accounts report with negative account pattern" ~:
defreportopts{patterns_=["not:assets"]} `gives`
[" $2 expenses"
," $1 food"
," $1 supplies"
," $-2 income"
," $-1 gifts"
," $-1 salary"
," $1 liabilities:debts"
,"--------------------"
," $1"
]
,"accounts report negative account pattern always matches full name" ~:
defreportopts{patterns_=["not:e"]} `gives`
["--------------------"
," 0"
]
,"accounts report negative patterns affect totals" ~:
defreportopts{patterns_=["expenses","not:food"]} `gives`
[" $1 expenses:supplies"
,"--------------------"
," $1"
]
,"accounts report with -E shows zero-balance accounts" ~:
defreportopts{patterns_=["assets"],empty_=True} `gives`
[" $-1 assets"
," $1 bank"
," 0 checking"
," $1 saving"
," $-2 cash"
,"--------------------"
," $-1"
]
,"accounts report with cost basis" ~: do
j <- (readJournal Nothing Nothing Nothing $ unlines
[""
,"2008/1/1 test "
," a:b 10h @ $50"
," c:d "
]) >>= either error' return
let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment
accountsReportAsText defreportopts (accountsReport defreportopts Any j') `is`
[" $500 a:b"
," $-500 c:d"
,"--------------------"
," 0"
]
-}
]
Right samplejournal2 = journalBalanceTransactions $ Journal
[]
[]
[
txnTieKnot $ Transaction {
tdate=parsedate "2008/01/01",
teffectivedate=Just $ parsedate "2009/01/01",
tstatus=False,
tcode="",
tdescription="income",
tcomment="",
tmetadata=[],
tpostings=[
Posting {
pstatus=False,
paccount="assets:bank:checking",
pamount=(Mixed [dollars 1]),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
},
Posting {
pstatus=False,
paccount="income:salary",
pamount=(missingmixedamt),
pcomment="",
ptype=RegularPosting,
pmetadata=[],
ptransaction=Nothing
}
],
tpreceding_comment_lines=""
}
]
[]
[]
""
nullctx
[]
(TOD 0 0)
exclusiveBalance :: Account -> MixedAmount exclusiveBalance :: Account -> MixedAmount
exclusiveBalance = sumPostings . apostings exclusiveBalance = sumPostings . apostings
@ -598,7 +970,7 @@ isInterestingIndented opts l a
| numinterestingsubs < 2 && zerobalance && not emptyflag = False | numinterestingsubs < 2 && zerobalance && not emptyflag = False
| otherwise = True | otherwise = True
where where
atmaxdepth = isJust d && Just (accountNameLevel a) == d where d = depth_ opts atmaxdepth = accountNameLevel a == depthFromOpts opts
emptyflag = empty_ opts emptyflag = empty_ opts
acct = ledgerAccount l a acct = ledgerAccount l a
zerobalance = isZeroMixedAmount inclbalance where inclbalance = abalance acct zerobalance = isZeroMixedAmount inclbalance where inclbalance = abalance acct
@ -608,16 +980,29 @@ isInterestingIndented opts l a
isInterestingTree = treeany (isInteresting opts l . aname) isInterestingTree = treeany (isInteresting opts l . aname)
subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a subtrees = map (fromJust . ledgerAccountTreeAt l) $ ledgerSubAccounts l $ ledgerAccount l a
tests_isInterestingIndented = [
"isInterestingIndented" ~: do
let (opts, journal, acctname) `gives` r = isInterestingIndented opts l acctname `is` r
where l = journalToLedger (queryFromOpts nulldate opts) journal
(defreportopts, samplejournal, "expenses") `gives` True
]
depthFromOpts :: ReportOpts -> Int
depthFromOpts opts = min (fromMaybe 99999 $ depth_ opts) (queryDepth $ queryFromOpts nulldate opts)
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
tests_Hledger_Reports :: Test tests_Hledger_Reports :: Test
tests_Hledger_Reports = TestList $ tests_Hledger_Reports = TestList $
tests_entriesReport2 ++ tests_queryFromOpts
[ ++ tests_queryOptsFromOpts
++ tests_entriesReport
"summarisePostingsByInterval" ~: do ++ tests_summarisePostingsByInterval
summarisePostingsByInterval (Quarters 1) Nothing False (DateSpan Nothing Nothing) [] ~?= [] ++ tests_postingsReport
++ tests_isInterestingIndented
++ tests_accountsReport
++ [
-- ,"summarisePostingsInDateSpan" ~: do -- ,"summarisePostingsInDateSpan" ~: do
-- let gives (b,e,depth,showempty,ps) = -- let gives (b,e,depth,showempty,ps) =
-- (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`) -- (summarisePostingsInDateSpan (mkdatespan b e) depth showempty ps `is`)

View File

@ -74,7 +74,12 @@ quoteIfSpaced :: String -> String
quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s quoteIfSpaced s | isSingleQuoted s || isDoubleQuoted s = s
| not $ any (`elem` s) whitespacechars = s | not $ any (`elem` s) whitespacechars = s
| otherwise = "'"++escapeSingleQuotes s++"'" | otherwise = "'"++escapeSingleQuotes s++"'"
where escapeSingleQuotes = regexReplace "'" "\'"
escapeSingleQuotes :: String -> String
escapeSingleQuotes = regexReplace "'" "\'"
escapeQuotes :: String -> String
escapeQuotes = regexReplace "([\"'])" "\\1"
-- | Quote-aware version of words - don't split on spaces which are inside quotes. -- | Quote-aware version of words - don't split on spaces which are inside quotes.
-- NB correctly handles "a'b" but not "''a''". -- NB correctly handles "a'b" but not "''a''".
@ -91,6 +96,7 @@ words' = map stripquotes . fromparse . parsewith p
unwords' :: [String] -> String unwords' :: [String] -> String
unwords' = unwords . map singleQuoteIfNeeded unwords' = unwords . map singleQuoteIfNeeded
-- | Single-quote this string if it contains whitespace or double-quotes
singleQuoteIfNeeded s | any (`elem` s) whitespacechars = "'"++s++"'" singleQuoteIfNeeded s | any (`elem` s) whitespacechars = "'"++s++"'"
| otherwise = s | otherwise = s

View File

@ -18,7 +18,6 @@ module Hledger.Cli (
tests_Hledger_Cli tests_Hledger_Cli
) )
where where
import Control.Monad
import qualified Data.Map as Map import qualified Data.Map as Map
import Data.Time.Calendar import Data.Time.Calendar
import System.Time (ClockTime(TOD)) import System.Time (ClockTime(TOD))
@ -105,158 +104,6 @@ tests_Hledger_Cli = TestList
"expenses","expenses:food","expenses:food:dining","expenses:phone","expenses:vacation", "expenses","expenses:food","expenses:food:dining","expenses:phone","expenses:vacation",
"liabilities","liabilities:credit cards","liabilities:credit cards:discover"] "liabilities","liabilities:credit cards","liabilities:credit cards:discover"]
,"sample journal parses" ~: do
_ <- samplejournal
assertBool "" True
,"balance report tests" ~:
let opts `gives` es = do
j <- samplejournal
d <- getCurrentDay
accountsReportAsText opts (accountsReport opts (filterSpecFromOpts opts d) j) `is` es
in TestList
[
"balance report with no args" ~:
defreportopts `gives`
[" $-1 assets"
," $1 bank:saving"
," $-2 cash"
," $2 expenses"
," $1 food"
," $1 supplies"
," $-2 income"
," $-1 gifts"
," $-1 salary"
," $1 liabilities:debts"
,"--------------------"
," 0"
]
,"balance report can be limited with --depth" ~:
defreportopts{depth_=Just 1} `gives`
[" $-1 assets"
," $2 expenses"
," $-2 income"
," $1 liabilities"
,"--------------------"
," 0"
]
,"balance report with account pattern o" ~:
defreportopts{patterns_=["o"]} `gives`
[" $1 expenses:food"
," $-2 income"
," $-1 gifts"
," $-1 salary"
,"--------------------"
," $-1"
]
,"balance report with account pattern o and --depth 1" ~:
defreportopts{patterns_=["o"],depth_=Just 1} `gives`
[" $1 expenses"
," $-2 income"
,"--------------------"
," $-1"
]
,"balance report with account pattern a" ~:
defreportopts{patterns_=["a"]} `gives`
[" $-1 assets"
," $1 bank:saving"
," $-2 cash"
," $-1 income:salary"
," $1 liabilities:debts"
,"--------------------"
," $-1"
]
,"balance report with account pattern e" ~:
defreportopts{patterns_=["e"]} `gives`
[" $-1 assets"
," $1 bank:saving"
," $-2 cash"
," $2 expenses"
," $1 food"
," $1 supplies"
," $-2 income"
," $-1 gifts"
," $-1 salary"
," $1 liabilities:debts"
,"--------------------"
," 0"
]
,"balance report with unmatched parent of two matched subaccounts" ~:
defreportopts{patterns_=["cash","saving"]} `gives`
[" $-1 assets"
," $1 bank:saving"
," $-2 cash"
,"--------------------"
," $-1"
]
,"balance report with multi-part account name" ~:
defreportopts{patterns_=["expenses:food"]} `gives`
[" $1 expenses:food"
,"--------------------"
," $1"
]
,"balance report with negative account pattern" ~:
defreportopts{patterns_=["not:assets"]} `gives`
[" $2 expenses"
," $1 food"
," $1 supplies"
," $-2 income"
," $-1 gifts"
," $-1 salary"
," $1 liabilities:debts"
,"--------------------"
," $1"
]
,"balance report negative account pattern always matches full name" ~:
defreportopts{patterns_=["not:e"]} `gives`
["--------------------"
," 0"
]
,"balance report negative patterns affect totals" ~:
defreportopts{patterns_=["expenses","not:food"]} `gives`
[" $1 expenses:supplies"
,"--------------------"
," $1"
]
,"balance report with -E shows zero-balance accounts" ~:
defreportopts{patterns_=["assets"],empty_=True} `gives`
[" $-1 assets"
," $1 bank"
," 0 checking"
," $1 saving"
," $-2 cash"
,"--------------------"
," $-1"
]
,"balance report with cost basis" ~: do
j <- (readJournal Nothing Nothing Nothing $ unlines
[""
,"2008/1/1 test "
," a:b 10h @ $50"
," c:d "
]) >>= either error' return
let j' = journalCanonicaliseAmounts $ journalConvertAmountsToCost j -- enable cost basis adjustment
accountsReportAsText defreportopts (accountsReport defreportopts nullfilterspec j') `is`
[" $500 a:b"
," $-500 c:d"
,"--------------------"
," 0"
]
]
,"journalCanonicaliseAmounts" ~: ,"journalCanonicaliseAmounts" ~:
"use the greatest precision" ~: "use the greatest precision" ~:
(map precision $ journalAmountAndPriceCommodities $ journalCanonicaliseAmounts $ journalWithAmounts ["1","2.00"]) `is` [2,2] (map precision $ journalAmountAndPriceCommodities $ journalCanonicaliseAmounts $ journalWithAmounts ["1","2.00"]) `is` [2,2]
@ -276,211 +123,13 @@ tests_Hledger_Cli = TestList
tdate (head $ jtxns j) `is` fromGregorian 2009 1 1 tdate (head $ jtxns j) `is` fromGregorian 2009 1 1
return () return ()
,"print report tests" ~: TestList ,"show dollars" ~: showAmount (dollars 1) ~?= "$1.00"
[
"print expenses" ~: ,"show hours" ~: showAmount (hours 1) ~?= "1.0h"
do
let opts = defreportopts{patterns_=["expenses"]}
j <- samplejournal
d <- getCurrentDay
showTransactions opts (filterSpecFromOpts opts d) j `is` unlines
["2008/06/03 * eat & shop"
," expenses:food $1"
," expenses:supplies $1"
," assets:cash $-2"
,""
]
, "print report with depth arg" ~:
do
let opts = defreportopts{depth_=Just 2}
j <- samplejournal
d <- getCurrentDay
showTransactions opts (filterSpecFromOpts opts d) j `is` unlines
["2008/01/01 income"
," income:salary $-1"
,""
,"2008/06/01 gift"
," income:gifts $-1"
,""
,"2008/06/03 * eat & shop"
," expenses:food $1"
," expenses:supplies $1"
," assets:cash $-2"
,""
,"2008/12/31 * pay off"
," liabilities:debts $1"
,""
]
]
,"register report tests" ~:
let registerdates = filter (not . null) . map (strip . take 10) . lines
in
TestList
[
"register report with no args" ~:
do
j <- samplejournal
let opts = defreportopts
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2008/01/01 income assets:bank:checking $1 $1"
," income:salary $-1 0"
,"2008/06/01 gift assets:bank:checking $1 $1"
," income:gifts $-1 0"
,"2008/06/02 save assets:bank:saving $1 $1"
," assets:bank:checking $-1 0"
,"2008/06/03 eat & shop expenses:food $1 $1"
," expenses:supplies $1 $2"
," assets:cash $-2 0"
,"2008/12/31 pay off liabilities:debts $1 $1"
," assets:bank:checking $-1 0"
]
,"register report with cleared option" ~:
do
let opts = defreportopts{cleared_=True}
j <- readJournal' sample_journal_str
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2008/06/03 eat & shop expenses:food $1 $1"
," expenses:supplies $1 $2"
," assets:cash $-2 0"
,"2008/12/31 pay off liabilities:debts $1 $1"
," assets:bank:checking $-1 0"
]
,"register report with uncleared option" ~:
do
let opts = defreportopts{uncleared_=True}
j <- readJournal' sample_journal_str
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2008/01/01 income assets:bank:checking $1 $1"
," income:salary $-1 0"
,"2008/06/01 gift assets:bank:checking $1 $1"
," income:gifts $-1 0"
,"2008/06/02 save assets:bank:saving $1 $1"
," assets:bank:checking $-1 0"
]
,"register report sorts by date" ~:
do
j <- readJournal' $ unlines
["2008/02/02 a"
," b 1"
," c"
,""
,"2008/01/01 d"
," e 1"
," f"
]
let opts = defreportopts
registerdates (postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` ["2008/01/01","2008/02/02"]
,"register report with account pattern" ~:
do
j <- samplejournal
let opts = defreportopts{patterns_=["cash"]}
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2008/06/03 eat & shop assets:cash $-2 $-2"
]
,"register report with account pattern, case insensitive" ~:
do
j <- samplejournal
let opts = defreportopts{patterns_=["cAsH"]}
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2008/06/03 eat & shop assets:cash $-2 $-2"
]
,"register report with display expression" ~:
do
j <- samplejournal
let gives displayexpr =
(registerdates (postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is`)
where opts = defreportopts{display_=Just displayexpr}
"d<[2008/6/2]" `gives` ["2008/01/01","2008/06/01"]
"d<=[2008/6/2]" `gives` ["2008/01/01","2008/06/01","2008/06/02"]
"d=[2008/6/2]" `gives` ["2008/06/02"]
"d>=[2008/6/2]" `gives` ["2008/06/02","2008/06/03","2008/12/31"]
"d>[2008/6/2]" `gives` ["2008/06/03","2008/12/31"]
,"register report with period expression" ~:
do
j <- samplejournal
let periodexpr `gives` dates = do
j' <- samplejournal
registerdates (postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j') `is` dates
where opts = defreportopts{period_=maybePeriod date1 periodexpr}
"" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]
"2008" `gives` ["2008/01/01","2008/06/01","2008/06/02","2008/06/03","2008/12/31"]
"2007" `gives` []
"june" `gives` ["2008/06/01","2008/06/02","2008/06/03"]
"monthly" `gives` ["2008/01/01","2008/06/01","2008/12/01"]
"quarterly" `gives` ["2008/01/01","2008/04/01","2008/10/01"]
let opts = defreportopts{period_=maybePeriod date1 "yearly"}
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2008/01/01 - 2008/12/31 assets:bank:saving $1 $1"
," assets:cash $-2 $-1"
," expenses:food $1 0"
," expenses:supplies $1 $1"
," income:gifts $-1 0"
," income:salary $-1 $-1"
," liabilities:debts $1 0"
]
let opts = defreportopts{period_=maybePeriod date1 "quarterly"}
registerdates (postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` ["2008/01/01","2008/04/01","2008/10/01"]
let opts = defreportopts{period_=maybePeriod date1 "quarterly",empty_=True}
registerdates (postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` ["2008/01/01","2008/04/01","2008/07/01","2008/10/01"]
]
, "register report with depth arg" ~:
do
j <- samplejournal
let opts = defreportopts{depth_=Just 2}
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2008/01/01 income assets:bank $1 $1"
," income:salary $-1 0"
,"2008/06/01 gift assets:bank $1 $1"
," income:gifts $-1 0"
,"2008/06/02 save assets:bank $1 $1"
," assets:bank $-1 0"
,"2008/06/03 eat & shop expenses:food $1 $1"
," expenses:supplies $1 $2"
," assets:cash $-2 0"
,"2008/12/31 pay off liabilities:debts $1 $1"
," assets:bank $-1 0"
]
,"show dollars" ~: show (dollars 1) ~?= "$1.00"
,"show hours" ~: show (hours 1) ~?= "1.0h"
,"unicode in balance layout" ~: do
j <- readJournal'
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
let opts = defreportopts
accountsReportAsText opts (accountsReport opts (filterSpecFromOpts opts date1) j) `is`
[" -100 актив:наличные"
," 100 расходы:покупки"
,"--------------------"
," 0"
]
,"unicode in register layout" ~: do
j <- readJournal'
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
let opts = defreportopts
(postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts date1) j) `is` unlines
["2009/01/01 медвежья шкура расходы:покупки 100 100"
," актив:наличные -100 0"]
,"subAccounts" ~: do ,"subAccounts" ~: do
l <- liftM (journalToLedger nullfilterspec) samplejournal let l = journalToLedger Any samplejournal
let a = ledgerAccount l "assets" a = ledgerAccount l "assets"
map aname (ledgerSubAccounts l a) `is` ["assets:bank","assets:cash"] map aname (ledgerSubAccounts l a) `is` ["assets:bank","assets:cash"]
] ]
@ -488,9 +137,10 @@ tests_Hledger_Cli = TestList
-- fixtures/test data -- fixtures/test data
date1 = parsedate "2008/11/26" -- date1 = parsedate "2008/11/26"
-- t1 = LocalTime date1 midday -- t1 = LocalTime date1 midday
{-
samplejournal = readJournal' sample_journal_str samplejournal = readJournal' sample_journal_str
sample_journal_str = unlines sample_journal_str = unlines
@ -535,6 +185,7 @@ sample_journal_str = unlines
,"" ,""
,";final comment" ,";final comment"
] ]
-}
defaultyear_journal_str = unlines defaultyear_journal_str = unlines
["Y2009" ["Y2009"
@ -882,7 +533,7 @@ journal7 = Journal
[] []
(TOD 0 0) (TOD 0 0)
ledger7 = journalToLedger nullfilterspec journal7 ledger7 = journalToLedger Any journal7
-- journal8_str = unlines -- journal8_str = unlines
-- ["2008/1/1 test " -- ["2008/1/1 test "

View File

@ -81,7 +81,7 @@ getTransaction j opts defaultDate = do
|| isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s)) || isRight (parse (smartdate >> many spacenonewline >> eof) "" $ lowercase s))
when (datestr == ".") $ ioError $ mkIOError eofErrorType "" Nothing Nothing when (datestr == ".") $ ioError $ mkIOError eofErrorType "" Nothing Nothing
description <- runInteractionDefault $ askFor "description" (Just "") Nothing description <- runInteractionDefault $ askFor "description" (Just "") Nothing
let historymatches = transactionsSimilarTo j (patterns_ $ reportopts_ opts) description let historymatches = transactionsSimilarTo j (queryFromOpts today $ reportopts_ opts) description
bestmatch | null historymatches = Nothing bestmatch | null historymatches = Nothing
| otherwise = Just $ snd $ head historymatches | otherwise = Just $ snd $ head historymatches
bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch bestmatchpostings = maybe Nothing (Just . tpostings) bestmatch
@ -149,8 +149,8 @@ getPostings st enteredps = do
-- I think 1 or 4, whichever would show the most decimal places -- I think 1 or 4, whichever would show the most decimal places
p = maxprecisionwithpoint p = maxprecisionwithpoint
amountstr <- runInteractionDefault $ askFor (printf "amount %d" n) defaultamountstr validateamount amountstr <- runInteractionDefault $ askFor (printf "amount %d" n) defaultamountstr validateamount
let a = fromparse $ runParser (amount <|> return missingamt) ctx "" amountstr let a = fromparse $ runParser (amount <|> return missingmixedamt) ctx "" amountstr
a' = fromparse $ runParser (amount <|> return missingamt) nullctx "" amountstr a' = fromparse $ runParser (amount <|> return missingmixedamt) nullctx "" amountstr
defaultamtused = Just (showMixedAmount a) == defaultamountstr defaultamtused = Just (showMixedAmount a) == defaultamountstr
commodityadded | c == cwithnodef = Nothing commodityadded | c == cwithnodef = Nothing
| otherwise = c | otherwise = c
@ -228,7 +228,7 @@ registerFromString :: String -> IO String
registerFromString s = do registerFromString s = do
d <- getCurrentDay d <- getCurrentDay
j <- readJournal' s j <- readJournal' s
return $ postingsReportAsText opts $ postingsReport opts (filterSpecFromOpts opts d) j return $ postingsReportAsText opts $ postingsReport opts (queryFromOpts d opts) j
where opts = defreportopts{empty_=True} where opts = defreportopts{empty_=True}
-- | Return a similarity measure, from 0 to 1, for two strings. -- | Return a similarity measure, from 0 to 1, for two strings.
@ -256,14 +256,14 @@ compareDescriptions s t = compareStrings s' t'
t' = simplify t t' = simplify t
simplify = filter (not . (`elem` "0123456789")) simplify = filter (not . (`elem` "0123456789"))
transactionsSimilarTo :: Journal -> [String] -> String -> [(Double,Transaction)] transactionsSimilarTo :: Journal -> Query -> String -> [(Double,Transaction)]
transactionsSimilarTo j apats s = transactionsSimilarTo j q s =
sortBy compareRelevanceAndRecency sortBy compareRelevanceAndRecency
$ filter ((> threshold).fst) $ filter ((> threshold).fst)
[(compareDescriptions s $ tdescription t, t) | t <- ts] [(compareDescriptions s $ tdescription t, t) | t <- ts]
where where
compareRelevanceAndRecency (n1,t1) (n2,t2) = compare (n2,tdate t2) (n1,tdate t1) compareRelevanceAndRecency (n1,t1) (n2,t2) = compare (n2,tdate t2) (n1,tdate t1)
ts = jtxns $ filterJournalTransactionsByAccount apats j ts = filter (q `matchesTransaction`) $ jtxns j
threshold = 0 threshold = 0
runInteraction :: Journal -> InputT IO a -> IO a runInteraction :: Journal -> InputT IO a -> IO a

View File

@ -117,7 +117,7 @@ balance CliOpts{reportopts_=ropts} j = do
d <- getCurrentDay d <- getCurrentDay
let lines = case formatFromOpts ropts of let lines = case formatFromOpts ropts of
Left err -> [err] Left err -> [err]
Right _ -> accountsReportAsText ropts $ accountsReport ropts (filterSpecFromOpts ropts d) j Right _ -> accountsReportAsText ropts $ accountsReport ropts (queryFromOpts d ropts) j
putStr $ unlines lines putStr $ unlines lines
-- | Render a balance report as plain text suitable for console output. -- | Render a balance report as plain text suitable for console output.
@ -134,6 +134,20 @@ accountsReportAsText opts (items, total) = concat lines ++ t
,padleft 20 $ showMixedAmountWithoutPrice total ,padleft 20 $ showMixedAmountWithoutPrice total
] ]
tests_accountsReportAsText = [
"accountsReportAsText" ~: do
-- "unicode in balance layout" ~: do
j <- readJournal'
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
let opts = defreportopts
accountsReportAsText opts (accountsReport opts (queryFromOpts (parsedate "2008/11/26") opts) j) `is`
[" -100 актив:наличные"
," 100 расходы:покупки"
,"--------------------"
," 0"
]
]
{- {-
This implementation turned out to be a bit convoluted but implements the following algorithm for formatting: This implementation turned out to be a bit convoluted but implements the following algorithm for formatting:
@ -180,5 +194,4 @@ formatField opts accountName depth total ljust min max field = case field of
_ -> "" _ -> ""
tests_Hledger_Cli_Balance = TestList tests_Hledger_Cli_Balance = TestList
[ tests_accountsReportAsText
]

View File

@ -27,10 +27,10 @@ balancesheet :: CliOpts -> Journal -> IO ()
balancesheet CliOpts{reportopts_=ropts} j = do balancesheet CliOpts{reportopts_=ropts} j = do
-- let lines = case formatFromOpts ropts of Left err, Right ... -- let lines = case formatFromOpts ropts of Left err, Right ...
d <- getCurrentDay d <- getCurrentDay
let (m,_) = queryFromOpts (withoutBeginDate ropts) d let q = queryFromOpts d (withoutBeginDate ropts)
assetreport@(_,assets) = accountsReport2 ropts (And [m, journalAssetAccountQuery j]) j assetreport@(_,assets) = accountsReport ropts (And [q, journalAssetAccountQuery j]) j
liabilityreport@(_,liabilities) = accountsReport2 ropts (And [m, journalLiabilityAccountQuery j]) j liabilityreport@(_,liabilities) = accountsReport ropts (And [q, journalLiabilityAccountQuery j]) j
equityreport@(_,equity) = accountsReport2 ropts (And [m, journalEquityAccountQuery j]) j equityreport@(_,equity) = accountsReport ropts (And [q, journalEquityAccountQuery j]) j
total = assets + liabilities + equity total = assets + liabilities + equity
LT.putStr $ [lt|Balance Sheet LT.putStr $ [lt|Balance Sheet

View File

@ -30,11 +30,11 @@ cashflow :: CliOpts -> Journal -> IO ()
cashflow CliOpts{reportopts_=ropts} j = do cashflow CliOpts{reportopts_=ropts} j = do
-- let lines = case formatFromOpts ropts of Left err, Right ... -- let lines = case formatFromOpts ropts of Left err, Right ...
d <- getCurrentDay d <- getCurrentDay
let (m,_) = queryFromOpts (withoutBeginDate ropts) d let q = queryFromOpts d (withoutBeginDate ropts)
cashreport@(_,total) = accountsReport2 ropts (And [m, journalCashAccountQuery j]) j cashreport@(_,total) = accountsReport ropts (And [q, journalCashAccountQuery j]) j
-- operatingreport@(_,operating) = accountsReport2 ropts (And [m, journalOperatingAccountMatcher j]) j -- operatingreport@(_,operating) = accountsReport ropts (And [q, journalOperatingAccountMatcher j]) j
-- investingreport@(_,investing) = accountsReport2 ropts (And [m, journalInvestingAccountMatcher j]) j -- investingreport@(_,investing) = accountsReport ropts (And [q, journalInvestingAccountMatcher j]) j
-- financingreport@(_,financing) = accountsReport2 ropts (And [m, journalFinancingAccountMatcher j]) j -- financingreport@(_,financing) = accountsReport ropts (And [q, journalFinancingAccountMatcher j]) j
-- total = operating + investing + financing -- total = operating + investing + financing
LT.putStr $ [lt|Cashflow Statement LT.putStr $ [lt|Cashflow Statement

View File

@ -14,6 +14,7 @@ import Text.Printf
import Hledger.Cli.Options import Hledger.Cli.Options
import Hledger.Data import Hledger.Data
import Hledger.Reports import Hledger.Reports
import Hledger.Data.Query
import Prelude hiding (putStr) import Prelude hiding (putStr)
import Hledger.Utils.UTF8IOCompat (putStr) import Hledger.Utils.UTF8IOCompat (putStr)
@ -23,30 +24,26 @@ barchar = '*'
-- | Print a histogram of some statistic per reporting interval, such as -- | Print a histogram of some statistic per reporting interval, such as
-- number of postings per day. -- number of postings per day.
histogram :: CliOpts -> Journal -> IO () histogram :: CliOpts -> Journal -> IO ()
histogram CliOpts{reportopts_=reportopts_} j = do histogram CliOpts{reportopts_=ropts} j = do
d <- getCurrentDay d <- getCurrentDay
putStr $ showHistogram reportopts_ (filterSpecFromOpts reportopts_ d) j putStr $ showHistogram ropts (queryFromOpts d ropts) j
showHistogram :: ReportOpts -> FilterSpec -> Journal -> String showHistogram :: ReportOpts -> Query -> Journal -> String
showHistogram opts filterspec j = concatMap (printDayWith countBar) spanps showHistogram opts q j = concatMap (printDayWith countBar) spanps
where where
i = intervalFromOpts opts i = intervalFromOpts opts
interval | i == NoInterval = Days 1 interval | i == NoInterval = Days 1
| otherwise = i | otherwise = i
span = datespan filterspec `orDatesFrom` journalDateSpan j span = queryDateSpan (effective_ opts) q `orDatesFrom` journalDateSpan j
spans = filter (DateSpan Nothing Nothing /=) $ splitSpan interval span spans = filter (DateSpan Nothing Nothing /=) $ splitSpan interval span
spanps = [(s, filter (isPostingInDateSpan s) ps) | s <- spans] spanps = [(s, filter (isPostingInDateSpan s) ps) | s <- spans]
-- same as Register -- same as Register
-- should count transactions, not postings ? -- should count transactions, not postings ?
ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j -- ps = sortBy (comparing postingDate) $ filterempties $ filter matchapats $ filterdepth $ journalPostings j
ps = sortBy (comparing postingDate) $ filterempties $ filter (q `matchesPosting`) $ journalPostings j
filterempties filterempties
| empty_ opts = id | queryEmpty q = id
| otherwise = filter (not . isZeroMixedAmount . pamount) | otherwise = filter (not . isZeroMixedAmount . pamount)
matchapats = matchpats apats . paccount
apats = acctpats filterspec
filterdepth | interval == NoInterval = filter (\p -> accountNameLevel (paccount p) <= depth)
| otherwise = id
depth = fromMaybe 99999 $ depth_ opts
printDayWith f (DateSpan b _, ts) = printf "%s %s\n" (show $ fromJust b) (f ts) printDayWith f (DateSpan b _, ts) = printf "%s %s\n" (show $ fromJust b) (f ts)

View File

@ -22,9 +22,9 @@ import Hledger.Cli.Balance
incomestatement :: CliOpts -> Journal -> IO () incomestatement :: CliOpts -> Journal -> IO ()
incomestatement CliOpts{reportopts_=ropts} j = do incomestatement CliOpts{reportopts_=ropts} j = do
d <- getCurrentDay d <- getCurrentDay
let (m,_) = queryFromOpts ropts d let q = queryFromOpts d ropts
incomereport@(_,income) = accountsReport2 ropts (And [m, journalIncomeAccountQuery j]) j incomereport@(_,income) = accountsReport ropts (And [q, journalIncomeAccountQuery j]) j
expensereport@(_,expenses) = accountsReport2 ropts (And [m, journalExpenseAccountQuery j]) j expensereport@(_,expenses) = accountsReport ropts (And [q, journalExpenseAccountQuery j]) j
total = income + expenses total = income + expenses
LT.putStr $ [lt|Income Statement LT.putStr $ [lt|Income Statement

View File

@ -60,13 +60,21 @@ import Hledger.Cli.Tests
import Hledger.Cli.Utils import Hledger.Cli.Utils
import Hledger.Cli.Version import Hledger.Cli.Version
import Hledger.Utils import Hledger.Utils
import Hledger.Reports
import Hledger.Data.Dates
main :: IO () main :: IO ()
main = do main = do
args <- getArgs args <- getArgs
addons <- getHledgerAddonCommands addons <- getHledgerAddonCommands
opts <- getHledgerCliOpts addons opts <- getHledgerCliOpts addons
when (debug_ opts) $ printf "%s\n" prognameandversion >> printf "opts: %s\n" (show opts) when (debug_ opts) $ do
printf "%s\n" prognameandversion
printf "args: %s\n" (show args)
printf "opts: %s\n" (show opts)
d <- getCurrentDay
printf "query: %s\n" (show $ queryFromOpts d $ reportopts_ opts)
run' opts addons args run' opts addons args
where where
run' opts@CliOpts{command_=cmd} addons args run' opts@CliOpts{command_=cmd} addons args

View File

@ -27,7 +27,7 @@ import Hledger.Cli.Version
-- 1. cmdargs mode and flag definitions, for the main and subcommand modes. -- 1. cmdargs mode and flag definitions, for the main and subcommand modes.
-- Flag values are parsed initially to simple RawOpts to permit reuse. -- Flag values are parsed initially to a simple association list to allow reuse.
type RawOpts = [(String,String)] type RawOpts = [(String,String)]
@ -306,7 +306,7 @@ toCliOpts rawopts = do
,command_ = stringopt "command" rawopts ,command_ = stringopt "command" rawopts
,file_ = maybestringopt "file" rawopts ,file_ = maybestringopt "file" rawopts
,rules_file_ = maybestringopt "rules-file" rawopts ,rules_file_ = maybestringopt "rules-file" rawopts
,alias_ = listofstringopt "alias" rawopts ,alias_ = map stripquotes $ listofstringopt "alias" rawopts
,debug_ = boolopt "debug" rawopts ,debug_ = boolopt "debug" rawopts
,no_new_accounts_ = boolopt "no-new-accounts" rawopts -- add ,no_new_accounts_ = boolopt "no-new-accounts" rawopts -- add
,reportopts_ = defreportopts { ,reportopts_ = defreportopts {
@ -331,7 +331,7 @@ toCliOpts rawopts = do
,quarterly_ = boolopt "quarterly" rawopts ,quarterly_ = boolopt "quarterly" rawopts
,yearly_ = boolopt "yearly" rawopts ,yearly_ = boolopt "yearly" rawopts
,format_ = maybestringopt "format" rawopts ,format_ = maybestringopt "format" rawopts
,patterns_ = listofstringopt "args" rawopts ,query_ = unwords $ listofstringopt "args" rawopts
} }
} }
@ -387,7 +387,7 @@ maybestringopt name = maybe Nothing (Just . stripquotes) . lookup name
stringopt name = fromMaybe "" . maybestringopt name stringopt name = fromMaybe "" . maybestringopt name
listofstringopt name rawopts = [stripquotes v | (n,v) <- rawopts, n==name] listofstringopt name rawopts = [v | (k,v) <- rawopts, k==name]
maybeintopt :: String -> RawOpts -> Maybe Int maybeintopt :: String -> RawOpts -> Maybe Int
maybeintopt name rawopts = maybeintopt name rawopts =

View File

@ -7,8 +7,10 @@ A ledger-compatible @print@ command.
module Hledger.Cli.Print ( module Hledger.Cli.Print (
print' print'
,showTransactions ,showTransactions
,tests_Hledger_Cli_Print
) where ) where
import Data.List import Data.List
import Test.HUnit
import Hledger import Hledger
import Prelude hiding (putStr) import Prelude hiding (putStr)
@ -19,11 +21,53 @@ import Hledger.Cli.Options
print' :: CliOpts -> Journal -> IO () print' :: CliOpts -> Journal -> IO ()
print' CliOpts{reportopts_=ropts} j = do print' CliOpts{reportopts_=ropts} j = do
d <- getCurrentDay d <- getCurrentDay
putStr $ showTransactions ropts (filterSpecFromOpts ropts d) j putStr $ showTransactions ropts (queryFromOpts d ropts) j
showTransactions :: ReportOpts -> FilterSpec -> Journal -> String showTransactions :: ReportOpts -> Query -> Journal -> String
showTransactions opts fspec j = entriesReportAsText opts fspec $ entriesReport opts fspec j showTransactions opts q j = entriesReportAsText opts q $ entriesReport opts q j
entriesReportAsText :: ReportOpts -> FilterSpec -> EntriesReport -> String tests_showTransactions = [
"showTransactions" ~: do
-- "print expenses" ~:
do
let opts = defreportopts{query_="expenses"}
d <- getCurrentDay
showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines
["2008/06/03 * eat & shop"
," expenses:food $1"
," expenses:supplies $1"
," assets:cash $-2"
,""
]
-- , "print report with depth arg" ~:
do
let opts = defreportopts{depth_=Just 2}
d <- getCurrentDay
showTransactions opts (queryFromOpts d opts) samplejournal `is` unlines
["2008/01/01 income"
," assets:bank:checking $1"
," income:salary $-1"
,""
,"2008/06/01 gift"
," assets:bank:checking $1"
," income:gifts $-1"
,""
,"2008/06/03 * eat & shop"
," expenses:food $1"
," expenses:supplies $1"
," assets:cash $-2"
,""
,"2008/12/31 * pay off"
," liabilities:debts $1"
," assets:bank:checking $-1"
,""
]
]
entriesReportAsText :: ReportOpts -> Query -> EntriesReport -> String
entriesReportAsText _ _ items = concatMap showTransactionUnelided items entriesReportAsText _ _ items = concatMap showTransactionUnelided items
tests_Hledger_Cli_Print = TestList
tests_showTransactions

View File

@ -26,12 +26,23 @@ import Hledger.Cli.Options
register :: CliOpts -> Journal -> IO () register :: CliOpts -> Journal -> IO ()
register CliOpts{reportopts_=ropts} j = do register CliOpts{reportopts_=ropts} j = do
d <- getCurrentDay d <- getCurrentDay
putStr $ postingsReportAsText ropts $ postingsReport ropts (filterSpecFromOpts ropts d) j putStr $ postingsReportAsText ropts $ postingsReport ropts (queryFromOpts d ropts) j
-- | Render a register report as plain text suitable for console output. -- | Render a register report as plain text suitable for console output.
postingsReportAsText :: ReportOpts -> PostingsReport -> String postingsReportAsText :: ReportOpts -> PostingsReport -> String
postingsReportAsText opts = unlines . map (postingsReportItemAsText opts) . snd postingsReportAsText opts = unlines . map (postingsReportItemAsText opts) . snd
tests_postingsReportAsText = [
"postingsReportAsText" ~: do
-- "unicode in register layout" ~: do
j <- readJournal'
"2009/01/01 * медвежья шкура\n расходы:покупки 100\n актив:наличные\n"
let opts = defreportopts
(postingsReportAsText opts $ postingsReport opts (queryFromOpts (parsedate "2008/11/26") opts) j) `is` unlines
["2009/01/01 медвежья шкура расходы:покупки 100 100"
," актив:наличные -100 0"]
]
-- | Render one register report line item as plain text. Eg: -- | Render one register report line item as plain text. Eg:
-- @ -- @
-- date (10) description (20) account (22) amount (11) balance (12) -- date (10) description (20) account (22) amount (11) balance (12)
@ -59,6 +70,4 @@ showPostingWithBalanceForVty showtxninfo p b = postingsReportItemAsText defrepor
tests_Hledger_Cli_Register :: Test tests_Hledger_Cli_Register :: Test
tests_Hledger_Cli_Register = TestList tests_Hledger_Cli_Register = TestList
[ tests_postingsReportAsText
]

View File

@ -24,9 +24,9 @@ import Hledger.Utils.UTF8IOCompat (putStr)
stats :: CliOpts -> Journal -> IO () stats :: CliOpts -> Journal -> IO ()
stats CliOpts{reportopts_=reportopts_} j = do stats CliOpts{reportopts_=reportopts_} j = do
d <- getCurrentDay d <- getCurrentDay
let filterspec = filterSpecFromOpts reportopts_ d let q = queryFromOpts d reportopts_
l = journalToLedger filterspec j l = journalToLedger q j
reportspan = (ledgerDateSpan l) `orDatesFrom` (datespan filterspec) reportspan = (ledgerDateSpan l) `orDatesFrom` (queryDateSpan False q)
intervalspans = splitSpan (intervalFromOpts reportopts_) reportspan intervalspans = splitSpan (intervalFromOpts reportopts_) reportspan
showstats = showLedgerStats l d showstats = showLedgerStats l d
s = intercalate "\n" $ map showstats intervalspans s = intercalate "\n" $ map showstats intervalspans

View File

@ -57,9 +57,7 @@ runTestsTillFailure opts = undefined -- do
-- firstproblem = find (\counts -> ) -- firstproblem = find (\counts -> )
-- | All or pattern-matched tests, as a flat list to show simple names. -- | All or pattern-matched tests, as a flat list to show simple names.
flatTests opts = TestList $ filter (matcherFromOpts opts) $ flattenTests tests_Hledger_Cli flatTests opts = TestList $ filter (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) $ flattenTests tests_Hledger_Cli
-- | All or pattern-matched tests, in the original suites to show hierarchical names. -- | All or pattern-matched tests, in the original suites to show hierarchical names.
hierarchicalTests opts = filterTests (matcherFromOpts opts) tests_Hledger_Cli hierarchicalTests opts = filterTests (matchesAccount (queryFromOpts nulldate $ reportopts_ opts) . testName) tests_Hledger_Cli
matcherFromOpts opts = matchpats (patterns_ $ reportopts_ opts) . testName