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:
parent
1654776f4a
commit
3ee6a351f3
76
MANUAL.md
76
MANUAL.md
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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]}
|
||||||
|
|||||||
@ -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)
|
|
||||||
|
|||||||
@ -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
|
||||||
]
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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)
|
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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`)
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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 "
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
]
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 =
|
||||||
|
|||||||
@ -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
|
||||||
@ -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
|
||||||
|
|
||||||
]
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user