hledger-lib 1.22 → 1.22.1
raw patch · 11 files changed
+138/−97 lines, 11 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Hledger.Data.Journal: filterJournalRelatedPostings :: Query -> Journal -> Journal
+ Hledger.Data.Journal: filterTransactionRelatedPostings :: Query -> Transaction -> Transaction
+ Hledger.Data.Journal: journalLastDay :: Bool -> Journal -> Maybe Day
+ Hledger.Data.Transaction: transactionCheckBalanced :: BalancingOpts -> Transaction -> [String]
- Hledger.Reports.AccountTransactionsReport: accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [AccountTransactionsReportItem]
+ Hledger.Reports.AccountTransactionsReport: accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [(Day, Transaction)] -> [AccountTransactionsReportItem]
Files
- CHANGES.md +9/−0
- Hledger/Data/Journal.hs +21/−3
- Hledger/Data/Transaction.hs +1/−0
- Hledger/Read.hs +1/−1
- Hledger/Reports/AccountTransactionsReport.hs +77/−66
- Hledger/Reports/EntriesReport.hs +2/−1
- Hledger/Reports/MultiBalanceReport.hs +5/−5
- Hledger/Reports/PostingsReport.hs +15/−14
- Hledger/Reports/ReportOptions.hs +2/−2
- Hledger/Reports/TransactionsReport.hs +1/−1
- hledger-lib.cabal +4/−4
CHANGES.md view
@@ -9,6 +9,15 @@ Internal/api/developer-ish changes in the hledger-lib (and hledger) packages. For user-visible changes, see the hledger package changelog. +# 1.22.1 2021-08-02++- Allow megaparsec 9.1++- journalEndDate's behaviour has been clarified, journalLastDay has+ been added.++- transactionCheckBalanced is now exported. (#1596)+ # 1.22 2021-07-03 - GHC 9.0 is now officially supported, and GHC 8.0, 8.2, 8.4 are not;
Hledger/Data/Journal.hs view
@@ -31,9 +31,11 @@ -- * Filtering filterJournalTransactions, filterJournalPostings,+ filterJournalRelatedPostings, filterJournalAmounts, filterTransactionAmounts, filterTransactionPostings,+ filterTransactionRelatedPostings, filterPostingAmount, -- * Mapping journalMapTransactions,@@ -60,6 +62,7 @@ journalDateSpanBothDates, journalStartDate, journalEndDate,+ journalLastDay, journalDescriptions, journalFilePath, journalFilePaths,@@ -104,7 +107,7 @@ import Data.Function ((&)) import qualified Data.HashTable.Class as H (toList) import qualified Data.HashTable.ST.Cuckoo as H-import Data.List (find, foldl', sortOn)+import Data.List ((\\), find, foldl', sortOn) import Data.List.Extra (nubSort) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromJust, fromMaybe, isJust, mapMaybe, maybeToList)@@ -516,6 +519,11 @@ filterJournalPostings :: Query -> Journal -> Journal filterJournalPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionPostings q) ts} +-- | Keep only postings which do not match the query expression, but for which a related posting does.+-- This can leave unbalanced transactions.+filterJournalRelatedPostings :: Query -> Journal -> Journal+filterJournalRelatedPostings q j@Journal{jtxns=ts} = j{jtxns=map (filterTransactionRelatedPostings q) ts}+ -- | Within each posting's amount, keep only the parts matching the query. -- This can leave unbalanced transactions. filterJournalAmounts :: Query -> Journal -> Journal@@ -533,6 +541,11 @@ filterTransactionPostings :: Query -> Transaction -> Transaction filterTransactionPostings q t@Transaction{tpostings=ps} = t{tpostings=filter (q `matchesPosting`) ps} +filterTransactionRelatedPostings :: Query -> Transaction -> Transaction+filterTransactionRelatedPostings q t@Transaction{tpostings=ps} =+ t{tpostings=if null matches then [] else ps \\ matches}+ where matches = filter (matchesPosting q) ps+ -- | Apply a transformation to a journal's transactions. journalMapTransactions :: (Transaction -> Transaction) -> Journal -> Journal journalMapTransactions f j@Journal{jtxns=ts} = j{jtxns=map f ts}@@ -1316,10 +1329,15 @@ journalStartDate :: Bool -> Journal -> Maybe Day journalStartDate secondary j = b where DateSpan b _ = journalDateSpan secondary j --- | The latest of this journal's transaction and posting dates, or--- Nothing if there are none.+-- | The "exclusive end date" of this journal: the day following its latest transaction +-- or posting date, or Nothing if there are none. journalEndDate :: Bool -> Journal -> Maybe Day journalEndDate secondary j = e where DateSpan _ e = journalDateSpan secondary j++-- | The latest of this journal's transaction and posting dates, or+-- Nothing if there are none.+journalLastDay :: Bool -> Journal -> Maybe Day+journalLastDay secondary j = addDays (-1) <$> journalEndDate secondary j -- | Apply the pivot transformation to all postings in a journal, -- replacing their account name by their value for the given field or tag.
Hledger/Data/Transaction.hs view
@@ -20,6 +20,7 @@ transaction, txnTieKnot, txnUntieKnot,+ transactionCheckBalanced, -- * operations showAccountName, hasRealPostings,
Hledger/Read.hs view
@@ -191,7 +191,7 @@ requireJournalFileExists f = do exists <- doesFileExist f when (not exists) $ do -- XXX might not be a journal file- hPutStr stderr $ "The hledger journal file \"" <> show f <> "\" was not found.\n"+ hPutStr stderr $ "The hledger journal file \"" <> f <> "\" was not found.\n" hPutStr stderr "Please create it first, eg with \"hledger add\" or a text editor.\n" hPutStr stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n" exitFailure
Hledger/Reports/AccountTransactionsReport.hs view
@@ -16,12 +16,12 @@ ) where -import Data.List (mapAccumL, nub, partition, sortBy)-import Data.Ord (comparing)+import Data.List (mapAccumR, nub, partition, sortBy) import Data.Maybe (catMaybes)+import Data.Ord (Down(..), comparing) import Data.Text (Text) import qualified Data.Text as T-import Data.Time.Calendar (Day)+import Data.Time.Calendar (Day, addDays) import Hledger.Data import Hledger.Query@@ -79,92 +79,103 @@ ) accountTransactionsReport :: ReportSpec -> Journal -> Query -> Query -> AccountTransactionsReport-accountTransactionsReport rspec@ReportSpec{rsOpts=ropts} j reportq thisacctq = items+accountTransactionsReport rspec@ReportSpec{rsOpts=ropts} j reportq' thisacctq = items where- -- a depth limit should not affect the account transactions report- -- seems unnecessary for some reason XXX- reportq' = reportq -- filterQuery (not . queryIsDepth)- symq = filterQuery queryIsSym reportq'- realq = filterQuery queryIsReal reportq'- statusq = filterQuery queryIsStatus reportq'+ -- A depth limit should not affect the account transactions report; it should show all transactions in/below this account.+ -- Queries on currency or amount are also ignored at this stage; they are handled earlier, before valuation.+ reportq = simplifyQuery $ And [aregisterq, periodq, excludeforecastq (forecast_ ropts)]+ where+ aregisterq = filterQuery (not . queryIsCurOrAmt) $ filterQuery (not . queryIsDepth) reportq'+ periodq = Date . periodAsDateSpan $ period_ ropts+ -- Except in forecast mode, exclude future/forecast transactions.+ excludeforecastq (Just _) = Any+ excludeforecastq Nothing = -- not:date:tomorrow- not:tag:generated-transaction+ And [ Not . Date $ DateSpan (Just . addDays 1 $ rsToday rspec) Nothing+ , Not generatedTransactionTag+ ]+ amtq = filterQuery queryIsCurOrAmt reportq'+ queryIsCurOrAmt q = queryIsSym q || queryIsAmt q - -- sort by the transaction's register date, for accurate starting balance- -- these are not yet filtered by tdate, we want to search them all for priorps- transactions =- ptraceAtWith 5 (("ts5:\n"++).pshowTransactions)- . sortBy (comparing (transactionRegisterDate reportq' thisacctq))- . jtxns- . ptraceAtWith 5 (("ts4:\n"++).pshowTransactions.jtxns)- -- keep just the transactions affecting this account (via possibly realness or status-filtered postings)- . traceAt 3 ("thisacctq: "++show thisacctq)- . ptraceAtWith 5 (("ts3:\n"++).pshowTransactions.jtxns)- . filterJournalTransactions thisacctq- . filterJournalPostings (And [realq, statusq])- -- apply any cur:SYM filters in reportq'- . ptraceAtWith 5 (("ts2:\n"++).pshowTransactions.jtxns)- . (if queryIsNull symq then id else filterJournalAmounts symq)- -- maybe convert these transactions to cost or value- $ journalApplyValuationFromOpts rspec j+ -- Note that within this functions, we are only allowed limited+ -- transformation of the transaction postings: this is due to the need to+ -- pass the original transactions into accountTransactionsReportItem.+ -- Generally, we either include a transaction in full, or not at all.+ -- Do some limited filtering and valuing of the journal's transactions:+ -- - filter them by the account query if any,+ -- - discard amounts not matched by the currency and amount query if any,+ -- - then apply valuation if any.+ acctJournal =+ ptraceAtWith 5 (("ts3:\n"++).pshowTransactions.jtxns)+ -- maybe convert these transactions to cost or value+ . journalApplyValuationFromOpts rspec+ . ptraceAtWith 5 (("ts2:\n"++).pshowTransactions.jtxns)+ -- apply any cur:SYM filters in reportq+ . (if queryIsNull amtq then id else filterJournalAmounts amtq)+ -- only consider transactions which match thisacctq (possibly excluding postings+ -- which are not real or have the wrong status)+ . traceAt 3 ("thisacctq: "++show thisacctq)+ $ ptraceAtWith 5 (("ts1:\n"++).pshowTransactions.jtxns)+ j{jtxns = filter (matchesTransaction thisacctq . relevantPostings) $ jtxns j}+ where+ relevantPostings+ | queryIsNull realq && queryIsNull statusq = id+ | otherwise = filterTransactionPostings . simplifyQuery $ And [realq, statusq]+ realq = filterQuery queryIsReal reportq+ statusq = filterQuery queryIsStatus reportq startbal | balancetype_ ropts == HistoricalBalance = sumPostings priorps | otherwise = nullmixedamt where- priorps = dbg5 "priorps" $- filter (matchesPosting- (dbg5 "priorq" $- And [thisacctq, tostartdateq, datelessreportq]))- $ transactionsPostings transactions+ priorps = dbg5 "priorps" . journalPostings $ filterJournalPostings priorq acctJournal+ priorq = dbg5 "priorq" $ And [thisacctq, tostartdateq, datelessreportq] tostartdateq = case mstartdate of Just _ -> Date (DateSpan Nothing mstartdate) Nothing -> None -- no start date specified, there are no prior postings- mstartdate = queryStartDate (date2_ ropts) reportq'- datelessreportq = filterQuery (not . queryIsDateOrDate2) reportq'-- -- accountTransactionsReportItem will keep transactions of any date which have any posting inside the report period.- -- Should we also require that transaction date is inside the report period ?- -- Should we be filtering by reportq here to apply other query terms (?)- -- Make it an option for now.- filtertxns = txn_dates_ ropts+ mstartdate = queryStartDate (date2_ ropts) reportq+ datelessreportq = filterQuery (not . queryIsDateOrDate2) reportq - items = reverse $- accountTransactionsReportItems reportq' thisacctq startbal maNegate $- (if filtertxns then filter (reportq' `matchesTransaction`) else id) $- transactions+ items =+ accountTransactionsReportItems reportq thisacctq startbal maNegate+ -- sort by the transaction's register date, for accurate starting balance+ . ptraceAtWith 5 (("ts4:\n"++).pshowTransactions.map snd)+ . sortBy (comparing $ Down . fst)+ . map (\t -> (transactionRegisterDate reportq thisacctq t, t))+ $ jtxns acctJournal pshowTransactions :: [Transaction] -> String pshowTransactions = pshow . map (\t -> unwords [show $ tdate t, T.unpack $ tdescription t]) -- | Generate transactions report items from a list of transactions, -- using the provided user-specified report query, a query specifying--- which account to use as the focus, a starting balance, a sign-setting--- function and a balance-summing function. Or with a None current account--- query, this can also be used for the transactionsReport.-accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount) -> [Transaction] -> [AccountTransactionsReportItem]+-- which account to use as the focus, a starting balance, and a sign-setting+-- function.+-- Each transaction is accompanied by the date that should be shown for it+-- in the report, which is not necessarily the transaction date; it is+-- the earliest of the posting dates which match both thisacctq and reportq,+-- otherwise the transaction's date if there are no matching postings.+accountTransactionsReportItems :: Query -> Query -> MixedAmount -> (MixedAmount -> MixedAmount)+ -> [(Day, Transaction)] -> [AccountTransactionsReportItem] accountTransactionsReportItems reportq thisacctq bal signfn =- catMaybes . snd .- mapAccumL (accountTransactionsReportItem reportq thisacctq signfn) bal+ catMaybes . snd . mapAccumR (accountTransactionsReportItem reportq thisacctq signfn) bal -accountTransactionsReportItem :: Query -> Query -> (MixedAmount -> MixedAmount) -> MixedAmount -> Transaction -> (MixedAmount, Maybe AccountTransactionsReportItem)-accountTransactionsReportItem reportq thisacctq signfn bal torig = balItem- -- 201403: This is used for both accountTransactionsReport and transactionsReport, which makes it a bit overcomplicated+accountTransactionsReportItem :: Query -> Query -> (MixedAmount -> MixedAmount) -> MixedAmount+ -> (Day, Transaction) -> (MixedAmount, Maybe AccountTransactionsReportItem)+accountTransactionsReportItem reportq thisacctq signfn bal (d, torig) -- 201407: I've lost my grip on this, let's just hope for the best -- 201606: we now calculate change and balance from filtered postings, check this still works well for all callers XXX+ | null reportps = (bal, Nothing) -- no matched postings in this transaction, skip it+ | otherwise = (b, Just (torig, tacct{tdate=d}, numotheraccts > 1, otheracctstr, a, b)) where- tfiltered@Transaction{tpostings=reportps} = filterTransactionPostings reportq torig- tacct = tfiltered{tdate=transactionRegisterDate reportq thisacctq tfiltered}- balItem = case reportps of- [] -> (bal, Nothing) -- no matched postings in this transaction, skip it- _ -> (b, Just (torig, tacct, numotheraccts > 1, otheracctstr, a, b))- where- (thisacctps, otheracctps) = partition (matchesPosting thisacctq) reportps- numotheraccts = length $ nub $ map paccount otheracctps- otheracctstr | thisacctq == None = summarisePostingAccounts reportps -- no current account ? summarise all matched postings- | numotheraccts == 0 = summarisePostingAccounts thisacctps -- only postings to current account ? summarise those- | otherwise = summarisePostingAccounts otheracctps -- summarise matched postings to other account(s)- a = signfn . maNegate $ sumPostings thisacctps- b = bal `maPlus` a+ tacct@Transaction{tpostings=reportps} = filterTransactionPostings reportq torig+ (thisacctps, otheracctps) = partition (matchesPosting thisacctq) reportps+ numotheraccts = length $ nub $ map paccount otheracctps+ otheracctstr | thisacctq == None = summarisePostingAccounts reportps -- no current account ? summarise all matched postings+ | numotheraccts == 0 = summarisePostingAccounts thisacctps -- only postings to current account ? summarise those+ | otherwise = summarisePostingAccounts otheracctps -- summarise matched postings to other account(s)+ a = signfn . maNegate $ sumPostings thisacctps+ b = bal `maPlus` a -- | What is the transaction's date in the context of a particular account -- (specified with a query) and report query, as in an account register ?
Hledger/Reports/EntriesReport.hs view
@@ -35,8 +35,9 @@ -- | Select transactions for an entries report. entriesReport :: ReportSpec -> Journal -> EntriesReport entriesReport rspec@ReportSpec{rsOpts=ropts} =- sortBy (comparing $ transactionDateFn ropts) . jtxns . filterJournalTransactions (rsQuery rspec)+ sortBy (comparing $ transactionDateFn ropts) . jtxns . journalApplyValuationFromOpts rspec{rsOpts=ropts{show_costs_=True}}+ . filterJournalTransactions (rsQuery rspec) tests_EntriesReport = tests "EntriesReport" [ tests "entriesReport" [
Hledger/Reports/MultiBalanceReport.hs view
@@ -244,9 +244,9 @@ getPostings rspec@ReportSpec{rsQuery=query,rsOpts=ropts} j priceoracle = map (\p -> (p, date p)) . journalPostings .- filterJournalAmounts symq . -- remove amount parts excluded by cur:- filterJournalPostings reportq $ -- remove postings not matched by (adjusted) query- valuedJournal+ valueJournal .+ filterJournalAmounts symq $ -- remove amount parts excluded by cur:+ filterJournalPostings reportq j -- remove postings not matched by (adjusted) query where symq = dbg3 "symq" . filterQuery queryIsSym $ dbg3 "requested q" query -- The user's query with no depth limit, and expanded to the report span@@ -254,8 +254,8 @@ -- handles the hledger-ui+future txns case above). reportq = dbg3 "reportq" $ depthless query depthless = dbg3 "depthless" . filterQuery (not . queryIsDepth)- valuedJournal | isJust (valuationAfterSum ropts) = j- | otherwise = journalApplyValuationFromOptsWith rspec j priceoracle+ valueJournal j' | isJust (valuationAfterSum ropts) = j'+ | otherwise = journalApplyValuationFromOptsWith rspec j' priceoracle date = case whichDateFromOpts ropts of PrimaryDate -> postingDate
Hledger/Reports/PostingsReport.hs view
@@ -119,21 +119,22 @@ where beforestartq = dbg3 "beforestartq" $ dateqtype $ DateSpan Nothing $ spanStart reportspan beforeandduringps =- dbg5 "ps5" $ sortOn sortdate $ -- sort postings by date or date2- dbg5 "ps4" $ (if invert_ ropts then map negatePostingAmount else id) $ -- with --invert, invert amounts- dbg5 "ps3" $ map (filterPostingAmount symq) $ -- remove amount parts which the query's cur: terms would exclude- dbg5 "ps2" $ (if related_ ropts then concatMap relatedPostings else id) $ -- with -r, replace each with its sibling postings- dbg5 "ps1" $ filter (beforeandduringq `matchesPosting`) $ -- filter postings by the query, with no start date or depth limit- journalPostings $- journalApplyValuationFromOpts rspec j -- convert to cost and apply valuation+ dbg5 "ps5" $ sortOn sortdate $ -- sort postings by date or date2+ dbg5 "ps4" $ (if invert_ ropts then map negatePostingAmount else id) $ -- with --invert, invert amounts+ journalPostings $+ journalApplyValuationFromOpts rspec $ -- convert to cost and apply valuation+ dbg5 "ps2" $ filterJournalAmounts symq $ -- remove amount parts which the query's cur: terms would exclude+ dbg5 "ps1" $ filterJournal beforeandduringq j -- filter postings by the query, with no start date or depth limit++ beforeandduringq = dbg4 "beforeandduringq" $ And [depthless $ dateless q, beforeendq] where- beforeandduringq = dbg4 "beforeandduringq" $ And [depthless $ dateless q, beforeendq]- where- depthless = filterQuery (not . queryIsDepth)- dateless = filterQuery (not . queryIsDateOrDate2)- beforeendq = dateqtype $ DateSpan Nothing $ spanEnd reportspan- sortdate = if date2_ ropts then postingDate2 else postingDate- symq = dbg4 "symq" $ filterQuery queryIsSym q+ depthless = filterQuery (not . queryIsDepth)+ dateless = filterQuery (not . queryIsDateOrDate2)+ beforeendq = dateqtype $ DateSpan Nothing $ spanEnd reportspan++ sortdate = if date2_ ropts then postingDate2 else postingDate+ filterJournal = if related_ ropts then filterJournalRelatedPostings else filterJournalPostings -- with -r, replace each posting with its sibling postings+ symq = dbg4 "symq" $ filterQuery queryIsSym q dateqtype | queryIsDate2 dateq || (queryIsDate dateq && date2_ ropts) = Date2 | otherwise = Date
Hledger/Reports/ReportOptions.hs view
@@ -636,8 +636,8 @@ reportPeriodOrJournalLastDay rspec j = reportPeriodLastDay rspec <|> journalOrPriceEnd where journalOrPriceEnd = case value_ $ rsOpts rspec of- Just (AtEnd _) -> max (journalEndDate False j) lastPriceDirective- _ -> journalEndDate False j+ Just (AtEnd _) -> max (journalLastDay False j) lastPriceDirective+ _ -> journalLastDay False j lastPriceDirective = fmap (addDays 1) . maximumMay . map pddate $ jpricedirectives j -- | Make a name for the given period in a multiperiod report, given
Hledger/Reports/TransactionsReport.hs view
@@ -66,7 +66,7 @@ where -- XXX items' first element should be the full transaction with all postings items = reverse $ accountTransactionsReportItems q None nullmixedamt id ts- ts = sortBy (comparing date) $ filter (q `matchesTransaction`) $ jtxns $ journalApplyValuationFromOpts rspec j+ ts = sortBy (comparing fst) $ map (\t -> (date t, t)) $ filter (q `matchesTransaction`) $ jtxns $ journalApplyValuationFromOpts rspec j date = transactionDateFn $ rsOpts rspec -- | Split a transactions report whose items may involve several commodities,
hledger-lib.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: hledger-lib-version: 1.22+version: 1.22.1 synopsis: A reusable library providing the core functionality of hledger description: A reusable library containing hledger's core functionality. This is used by most hledger* packages so that they support the same@@ -117,7 +117,7 @@ , file-embed >=0.0.10 , filepath , hashtables >=1.2.3.1- , megaparsec >=7.0.0 && <9.1+ , megaparsec >=7.0.0 && <9.2 , mtl >=2.2.1 , old-time , parser-combinators >=0.4.0@@ -167,7 +167,7 @@ , file-embed >=0.0.10 , filepath , hashtables >=1.2.3.1- , megaparsec >=7.0.0 && <9.1+ , megaparsec >=7.0.0 && <9.2 , mtl >=2.2.1 , old-time , parser-combinators >=0.4.0@@ -219,7 +219,7 @@ , filepath , hashtables >=1.2.3.1 , hledger-lib- , megaparsec >=7.0.0 && <9.1+ , megaparsec >=7.0.0 && <9.2 , mtl >=2.2.1 , old-time , parser-combinators >=0.4.0