2009-04-03 14:58:05 +04:00
|
|
|
{-|
|
|
|
|
|
2012-05-14 22:52:22 +04:00
|
|
|
A 'Transaction' represents a movement of some commodity(ies) between two
|
|
|
|
or more accounts. It consists of multiple account 'Posting's which balance
|
2012-05-28 02:59:06 +04:00
|
|
|
to zero, a date, and optional extras like description, cleared status, and
|
|
|
|
tags.
|
2009-04-03 14:58:05 +04:00
|
|
|
|
|
|
|
-}
|
|
|
|
|
2019-02-15 21:34:40 +03:00
|
|
|
{-# LANGUAGE FlexibleContexts #-}
|
|
|
|
{-# LANGUAGE LambdaCase #-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE Rank2Types #-}
|
|
|
|
{-# LANGUAGE RecordWildCards #-}
|
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
|
|
|
|
2012-05-07 18:36:40 +04:00
|
|
|
module Hledger.Data.Transaction (
|
|
|
|
-- * Transaction
|
|
|
|
nulltransaction,
|
2019-02-22 03:20:04 +03:00
|
|
|
transaction,
|
2012-05-07 18:36:40 +04:00
|
|
|
txnTieKnot,
|
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
|
|
|
txnUntieKnot,
|
2012-05-07 18:36:40 +04:00
|
|
|
-- * operations
|
|
|
|
showAccountName,
|
|
|
|
hasRealPostings,
|
|
|
|
realPostings,
|
2016-12-10 18:04:48 +03:00
|
|
|
assignmentPostings,
|
2012-05-07 18:36:40 +04:00
|
|
|
virtualPostings,
|
|
|
|
balancedVirtualPostings,
|
|
|
|
transactionsPostings,
|
|
|
|
isTransactionBalanced,
|
2019-02-15 21:34:40 +03:00
|
|
|
balanceTransaction,
|
2019-02-18 23:11:07 +03:00
|
|
|
balanceTransactionHelper,
|
2019-10-20 05:41:21 +03:00
|
|
|
transactionTransformPostings,
|
|
|
|
transactionApplyValuation,
|
|
|
|
transactionToCost,
|
2020-11-24 20:17:01 +03:00
|
|
|
transactionApplyAliases,
|
2012-05-07 18:36:40 +04:00
|
|
|
-- nonzerobalanceerror,
|
|
|
|
-- * date operations
|
2012-12-06 08:43:41 +04:00
|
|
|
transactionDate2,
|
2019-07-10 13:04:05 +03:00
|
|
|
-- * transaction description parts
|
|
|
|
transactionPayee,
|
|
|
|
transactionNote,
|
|
|
|
-- payeeAndNoteFromDescription,
|
2012-05-07 18:36:40 +04:00
|
|
|
-- * rendering
|
|
|
|
showTransaction,
|
2019-11-16 14:23:38 +03:00
|
|
|
showTransactionOneLineAmounts,
|
2012-05-07 18:36:40 +04:00
|
|
|
showTransactionUnelided,
|
2015-10-30 04:05:02 +03:00
|
|
|
showTransactionUnelidedOneLineAmounts,
|
2019-01-08 19:51:11 +03:00
|
|
|
-- showPostingLine,
|
2017-01-20 18:33:24 +03:00
|
|
|
showPostingLines,
|
|
|
|
-- * GenericSourcePos
|
|
|
|
sourceFilePath,
|
|
|
|
sourceFirstLine,
|
|
|
|
showGenericSourcePos,
|
2019-02-18 23:11:07 +03:00
|
|
|
annotateErrorWithTransaction,
|
2018-09-04 21:31:31 +03:00
|
|
|
-- * tests
|
2018-09-06 23:08:26 +03:00
|
|
|
tests_Transaction
|
2012-05-07 18:36:40 +04:00
|
|
|
)
|
2009-04-03 14:58:05 +04:00
|
|
|
where
|
2011-05-28 08:11:44 +04:00
|
|
|
import Data.List
|
2020-05-29 23:09:17 +03:00
|
|
|
import Data.List.Extra (nubSort)
|
2011-05-28 08:11:44 +04:00
|
|
|
import Data.Maybe
|
lib: textification: comments and tags
No change.
hledger -f data/100x100x10.journal stats
<<ghc: 42859576 bytes, 84 GCs, 193781/269984 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.016 MUT (0.020 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
<<ghc: 42859576 bytes, 84 GCs, 193781/269984 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.018 elapsed), 0.009 GC (0.013 elapsed) :ghc>>
hledger -f data/1000x1000x10.journal stats
<<ghc: 349576344 bytes, 681 GCs, 1407388/4091680 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.124 MUT (0.130 elapsed), 0.047 GC (0.055 elapsed) :ghc>>
<<ghc: 349576280 bytes, 681 GCs, 1407388/4091680 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.126 MUT (0.132 elapsed), 0.049 GC (0.058 elapsed) :ghc>>
hledger -f data/10000x1000x10.journal stats
<<ghc: 3424030664 bytes, 6658 GCs, 11403359/41071624 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.000 elapsed), 1.207 MUT (1.228 elapsed), 0.473 GC (0.528 elapsed) :ghc>>
<<ghc: 3424030760 bytes, 6658 GCs, 11403874/41077288 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.002 elapsed), 1.234 MUT (1.256 elapsed), 0.470 GC (0.520 elapsed) :ghc>>
hledger -f data/100000x1000x10.journal stats
<<ghc: 34306547448 bytes, 66727 GCs, 76805504/414629288 avg/max bytes residency (14 samples), 1009M in use, 0.000 INIT (0.003 elapsed), 12.615 MUT (12.813 elapsed), 4.656 GC (5.291 elapsed) :ghc>>
<<ghc: 34306547320 bytes, 66727 GCs, 76805504/414629288 avg/max bytes residency (14 samples), 1009M in use, 0.000 INIT (0.009 elapsed), 12.802 MUT (13.065 elapsed), 4.774 GC (5.441 elapsed) :ghc>>
2016-05-25 03:09:20 +03:00
|
|
|
import Data.Text (Text)
|
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
|
|
|
import qualified Data.Text as T
|
2011-09-23 18:50:20 +04:00
|
|
|
import Data.Time.Calendar
|
2011-05-28 08:11:44 +04:00
|
|
|
import Text.Printf
|
2019-10-20 05:17:35 +03:00
|
|
|
import qualified Data.Map as M
|
2010-11-15 01:44:37 +03:00
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
import Hledger.Utils
|
2010-05-20 03:08:53 +04:00
|
|
|
import Hledger.Data.Types
|
|
|
|
import Hledger.Data.Dates
|
|
|
|
import Hledger.Data.Posting
|
|
|
|
import Hledger.Data.Amount
|
2019-10-20 05:41:21 +03:00
|
|
|
import Hledger.Data.Valuation
|
2009-04-03 14:58:05 +04:00
|
|
|
|
2017-01-20 18:33:24 +03:00
|
|
|
sourceFilePath :: GenericSourcePos -> FilePath
|
|
|
|
sourceFilePath = \case
|
|
|
|
GenericSourcePos fp _ _ -> fp
|
|
|
|
JournalSourcePos fp _ -> fp
|
|
|
|
|
|
|
|
sourceFirstLine :: GenericSourcePos -> Int
|
|
|
|
sourceFirstLine = \case
|
|
|
|
GenericSourcePos _ line _ -> line
|
|
|
|
JournalSourcePos _ (line, _) -> line
|
|
|
|
|
2018-10-25 10:05:29 +03:00
|
|
|
-- | Render source position in human-readable form.
|
|
|
|
-- Keep in sync with Hledger.UI.ErrorScreen.hledgerparseerrorpositionp (temporary). XXX
|
2017-01-20 18:33:24 +03:00
|
|
|
showGenericSourcePos :: GenericSourcePos -> String
|
|
|
|
showGenericSourcePos = \case
|
|
|
|
GenericSourcePos fp line column -> show fp ++ " (line " ++ show line ++ ", column " ++ show column ++ ")"
|
|
|
|
JournalSourcePos fp (line, line') -> show fp ++ " (lines " ++ show line ++ "-" ++ show line' ++ ")"
|
|
|
|
|
2009-12-19 08:57:54 +03:00
|
|
|
nulltransaction :: Transaction
|
|
|
|
nulltransaction = Transaction {
|
2015-10-30 06:12:46 +03:00
|
|
|
tindex=0,
|
2014-08-01 04:32:42 +04:00
|
|
|
tsourcepos=nullsourcepos,
|
2009-12-19 08:57:54 +03:00
|
|
|
tdate=nulldate,
|
2012-12-06 08:43:41 +04:00
|
|
|
tdate2=Nothing,
|
2017-06-16 02:25:37 +03:00
|
|
|
tstatus=Unmarked,
|
2014-09-11 00:07:53 +04:00
|
|
|
tcode="",
|
|
|
|
tdescription="",
|
2009-12-19 08:57:54 +03:00
|
|
|
tcomment="",
|
2012-05-28 02:59:06 +04:00
|
|
|
ttags=[],
|
2009-12-19 08:57:54 +03:00
|
|
|
tpostings=[],
|
2019-01-04 23:01:45 +03:00
|
|
|
tprecedingcomment=""
|
2009-12-19 08:57:54 +03:00
|
|
|
}
|
2009-04-03 14:58:05 +04:00
|
|
|
|
2019-02-22 03:20:04 +03:00
|
|
|
-- | Make a simple transaction with the given date and postings.
|
2020-08-26 11:11:20 +03:00
|
|
|
transaction :: Day -> [Posting] -> Transaction
|
|
|
|
transaction day ps = txnTieKnot $ nulltransaction{tdate=day, tpostings=ps}
|
2019-02-22 03:20:04 +03:00
|
|
|
|
2019-07-10 13:04:05 +03:00
|
|
|
transactionPayee :: Transaction -> Text
|
|
|
|
transactionPayee = fst . payeeAndNoteFromDescription . tdescription
|
|
|
|
|
|
|
|
transactionNote :: Transaction -> Text
|
|
|
|
transactionNote = snd . payeeAndNoteFromDescription . tdescription
|
|
|
|
|
|
|
|
-- | Parse a transaction's description into payee and note (aka narration) fields,
|
|
|
|
-- assuming a convention of separating these with | (like Beancount).
|
|
|
|
-- Ie, everything up to the first | is the payee, everything after it is the note.
|
|
|
|
-- When there's no |, payee == note == description.
|
|
|
|
payeeAndNoteFromDescription :: Text -> (Text,Text)
|
|
|
|
payeeAndNoteFromDescription t
|
|
|
|
| T.null n = (t, t)
|
2020-07-16 13:51:48 +03:00
|
|
|
| otherwise = (T.strip p, T.strip $ T.drop 1 n)
|
2019-07-10 13:04:05 +03:00
|
|
|
where
|
|
|
|
(p, n) = T.span (/= '|') t
|
|
|
|
|
2009-04-03 14:58:05 +04:00
|
|
|
{-|
|
2020-02-08 16:06:52 +03:00
|
|
|
Render a journal transaction as text similar to the style of Ledger's print command.
|
2018-10-22 16:46:31 +03:00
|
|
|
|
2020-02-08 16:06:52 +03:00
|
|
|
Adapted from Ledger 2.x and 3.x standard format:
|
2009-04-03 14:58:05 +04:00
|
|
|
|
|
|
|
@
|
2020-02-08 16:06:52 +03:00
|
|
|
yyyy-mm-dd[ *][ CODE] description......... [ ; comment...............]
|
2009-04-03 14:58:05 +04:00
|
|
|
account name 1..................... ...$amount1[ ; comment...............]
|
|
|
|
account name 2..................... ..$-amount1[ ; comment...............]
|
|
|
|
|
|
|
|
pcodewidth = no limit -- 10 -- mimicking ledger layout.
|
|
|
|
pdescwidth = no limit -- 20 -- I don't remember what these mean,
|
|
|
|
pacctwidth = 35 minimum, no maximum -- they were important at the time.
|
|
|
|
pamtwidth = 11
|
|
|
|
pcommentwidth = no limit -- 22
|
|
|
|
@
|
2018-10-22 16:46:31 +03:00
|
|
|
|
|
|
|
The output will be parseable journal syntax.
|
2019-07-15 13:28:52 +03:00
|
|
|
To facilitate this, postings with explicit multi-commodity amounts
|
2018-10-22 16:46:31 +03:00
|
|
|
are displayed as multiple similar postings, one per commodity.
|
|
|
|
(Normally does not happen with this function).
|
2009-04-03 14:58:05 +04:00
|
|
|
-}
|
2009-12-16 11:07:26 +03:00
|
|
|
showTransaction :: Transaction -> String
|
2019-11-16 14:23:38 +03:00
|
|
|
showTransaction = showTransactionHelper False
|
|
|
|
|
|
|
|
-- | Deprecated alias for 'showTransaction'
|
2009-12-16 11:07:26 +03:00
|
|
|
showTransactionUnelided :: Transaction -> String
|
2019-11-16 14:23:38 +03:00
|
|
|
showTransactionUnelided = showTransaction -- TODO: drop it
|
2009-04-03 14:58:05 +04:00
|
|
|
|
2019-11-16 14:23:38 +03:00
|
|
|
-- | Like showTransaction, but explicit multi-commodity amounts
|
2019-07-15 13:28:52 +03:00
|
|
|
-- are shown on one line, comma-separated. In this case the output will
|
2018-10-22 16:46:31 +03:00
|
|
|
-- not be parseable journal syntax.
|
2019-11-16 14:23:38 +03:00
|
|
|
showTransactionOneLineAmounts :: Transaction -> String
|
|
|
|
showTransactionOneLineAmounts = showTransactionHelper True
|
|
|
|
|
|
|
|
-- | Deprecated alias for 'showTransactionOneLineAmounts'
|
2019-12-07 20:06:52 +03:00
|
|
|
showTransactionUnelidedOneLineAmounts :: Transaction -> String
|
2019-11-16 14:23:38 +03:00
|
|
|
showTransactionUnelidedOneLineAmounts = showTransactionOneLineAmounts -- TODO: drop it
|
2015-10-30 04:05:02 +03:00
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
-- | Helper for showTransaction*.
|
2019-11-16 14:23:38 +03:00
|
|
|
showTransactionHelper :: Bool -> Transaction -> String
|
|
|
|
showTransactionHelper onelineamounts t =
|
2012-05-15 05:49:05 +04:00
|
|
|
unlines $ [descriptionline]
|
2013-09-10 21:32:49 +04:00
|
|
|
++ newlinecomments
|
2019-11-16 14:23:38 +03:00
|
|
|
++ (postingsAsLines onelineamounts (tpostings t))
|
2012-05-15 05:49:05 +04:00
|
|
|
++ [""]
|
2009-04-03 14:58:05 +04:00
|
|
|
where
|
2013-09-10 21:32:49 +04:00
|
|
|
descriptionline = rstrip $ concat [date, status, code, desc, samelinecomment]
|
2015-10-10 21:53:28 +03:00
|
|
|
date = showDate (tdate t) ++ maybe "" (("="++) . showDate) (tdate2 t)
|
2015-05-16 21:51:35 +03:00
|
|
|
status | tstatus t == Cleared = " *"
|
|
|
|
| tstatus t == Pending = " !"
|
|
|
|
| otherwise = ""
|
lib: textification: descriptions & codes
Slightly higher (with small files) and lower (with large files) maximum
residency, and slightly quicker for all.
hledger -f data/100x100x10.journal stats
<<ghc: 42858472 bytes, 84 GCs, 193712/269608 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.015 elapsed), 0.016 MUT (0.042 elapsed), 0.011 GC (0.119 elapsed) :ghc>>
<<ghc: 42891776 bytes, 84 GCs, 190816/260920 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.004 elapsed), 0.017 MUT (0.025 elapsed), 0.010 GC (0.015 elapsed) :ghc>>
hledger -f data/1000x1000x10.journal stats
<<ghc: 349575240 bytes, 681 GCs, 1396425/4091680 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.137 MUT (0.146 elapsed), 0.050 GC (0.057 elapsed) :ghc>>
<<ghc: 349927568 bytes, 681 GCs, 1397825/4097248 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.126 MUT (0.133 elapsed), 0.050 GC (0.057 elapsed) :ghc>>
hledger -f data/10000x1000x10.journal stats
<<ghc: 3424029496 bytes, 6658 GCs, 11403141/41077288 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.000 elapsed), 1.278 MUT (1.310 elapsed), 0.493 GC (0.546 elapsed) :ghc>>
<<ghc: 3427418064 bytes, 6665 GCs, 11127869/37790168 avg/max bytes residency (11 samples), 109M in use, 0.000 INIT (0.001 elapsed), 1.212 MUT (1.229 elapsed), 0.466 GC (0.519 elapsed) :ghc>>
hledger -f data/100000x1000x10.journal stats
<<ghc: 34306546248 bytes, 66727 GCs, 77030638/414617944 avg/max bytes residency (14 samples), 1012M in use, 0.000 INIT (0.000 elapsed), 12.965 MUT (13.164 elapsed), 4.771 GC (5.447 elapsed) :ghc>>
<<ghc: 34340246056 bytes, 66779 GCs, 76983178/416011480 avg/max bytes residency (14 samples), 1011M in use, 0.000 INIT (0.008 elapsed), 12.666 MUT (12.836 elapsed), 4.595 GC (5.175 elapsed) :ghc>>
2016-05-25 04:51:52 +03:00
|
|
|
code = if T.length (tcode t) > 0 then printf " (%s)" $ T.unpack $ tcode t else ""
|
|
|
|
desc = if null d then "" else " " ++ d where d = T.unpack $ tdescription t
|
2013-09-10 21:32:49 +04:00
|
|
|
(samelinecomment, newlinecomments) =
|
|
|
|
case renderCommentLines (tcomment t) of [] -> ("",[])
|
|
|
|
c:cs -> (c,cs)
|
|
|
|
|
2018-10-22 16:46:31 +03:00
|
|
|
-- | Render a transaction or posting's comment as indented, semicolon-prefixed comment lines.
|
2019-06-12 11:38:05 +03:00
|
|
|
-- The first line (unless empty) will have leading space, subsequent lines will have a larger indent.
|
lib: textification: comments and tags
No change.
hledger -f data/100x100x10.journal stats
<<ghc: 42859576 bytes, 84 GCs, 193781/269984 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.016 MUT (0.020 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
<<ghc: 42859576 bytes, 84 GCs, 193781/269984 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.018 elapsed), 0.009 GC (0.013 elapsed) :ghc>>
hledger -f data/1000x1000x10.journal stats
<<ghc: 349576344 bytes, 681 GCs, 1407388/4091680 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.124 MUT (0.130 elapsed), 0.047 GC (0.055 elapsed) :ghc>>
<<ghc: 349576280 bytes, 681 GCs, 1407388/4091680 avg/max bytes residency (7 samples), 11M in use, 0.000 INIT (0.000 elapsed), 0.126 MUT (0.132 elapsed), 0.049 GC (0.058 elapsed) :ghc>>
hledger -f data/10000x1000x10.journal stats
<<ghc: 3424030664 bytes, 6658 GCs, 11403359/41071624 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.000 elapsed), 1.207 MUT (1.228 elapsed), 0.473 GC (0.528 elapsed) :ghc>>
<<ghc: 3424030760 bytes, 6658 GCs, 11403874/41077288 avg/max bytes residency (11 samples), 111M in use, 0.000 INIT (0.002 elapsed), 1.234 MUT (1.256 elapsed), 0.470 GC (0.520 elapsed) :ghc>>
hledger -f data/100000x1000x10.journal stats
<<ghc: 34306547448 bytes, 66727 GCs, 76805504/414629288 avg/max bytes residency (14 samples), 1009M in use, 0.000 INIT (0.003 elapsed), 12.615 MUT (12.813 elapsed), 4.656 GC (5.291 elapsed) :ghc>>
<<ghc: 34306547320 bytes, 66727 GCs, 76805504/414629288 avg/max bytes residency (14 samples), 1009M in use, 0.000 INIT (0.009 elapsed), 12.802 MUT (13.065 elapsed), 4.774 GC (5.441 elapsed) :ghc>>
2016-05-25 03:09:20 +03:00
|
|
|
renderCommentLines :: Text -> [String]
|
2019-06-12 11:38:05 +03:00
|
|
|
renderCommentLines t =
|
|
|
|
case lines $ T.unpack t of
|
|
|
|
[] -> []
|
|
|
|
[l] -> [(commentSpace . comment) l] -- single-line comment
|
|
|
|
("":ls) -> "" : map (lineIndent . comment) ls -- multi-line comment with empty first line
|
|
|
|
(l:ls) -> (commentSpace . comment) l : map (lineIndent . comment) ls
|
|
|
|
where
|
|
|
|
comment = ("; "++)
|
2013-09-10 21:32:49 +04:00
|
|
|
|
2018-10-22 16:46:31 +03:00
|
|
|
-- | Given a transaction and its postings, render the postings, suitable
|
2018-11-30 00:32:59 +03:00
|
|
|
-- for `print` output. Normally this output will be valid journal syntax which
|
|
|
|
-- hledger can reparse (though it may include no-longer-valid balance assertions).
|
2018-10-22 16:46:31 +03:00
|
|
|
--
|
2019-07-15 13:28:52 +03:00
|
|
|
-- Explicit amounts are shown, any implicit amounts are not.
|
2018-10-22 16:46:31 +03:00
|
|
|
--
|
2018-11-30 00:32:59 +03:00
|
|
|
-- Postings with multicommodity explicit amounts are handled as follows:
|
|
|
|
-- if onelineamounts is true, these amounts are shown on one line,
|
|
|
|
-- comma-separated, and the output will not be valid journal syntax.
|
|
|
|
-- Otherwise, they are shown as several similar postings, one per commodity.
|
2019-07-15 13:28:52 +03:00
|
|
|
--
|
2018-11-30 00:44:38 +03:00
|
|
|
-- The output will appear to be a balanced transaction.
|
|
|
|
-- Amounts' display precisions, which may have been limited by commodity
|
|
|
|
-- directives, will be increased if necessary to ensure this.
|
|
|
|
--
|
2018-11-30 00:32:59 +03:00
|
|
|
-- Posting amounts will be aligned with each other, starting about 4 columns
|
|
|
|
-- beyond the widest account name (see postingAsLines for details).
|
2019-07-15 13:28:52 +03:00
|
|
|
--
|
2019-11-16 14:23:38 +03:00
|
|
|
postingsAsLines :: Bool -> [Posting] -> [String]
|
|
|
|
postingsAsLines onelineamounts ps = concatMap (postingAsLines False onelineamounts ps) ps
|
2012-05-15 05:49:05 +04:00
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
-- | Render one posting, on one or more lines, suitable for `print` output.
|
2018-10-22 16:46:31 +03:00
|
|
|
-- There will be an indented account name, plus one or more of status flag,
|
|
|
|
-- posting amount, balance assertion, same-line comment, next-line comments.
|
2019-07-15 13:28:52 +03:00
|
|
|
--
|
2018-10-22 16:46:31 +03:00
|
|
|
-- If the posting's amount is implicit or if elideamount is true, no amount is shown.
|
|
|
|
--
|
2019-07-15 13:28:52 +03:00
|
|
|
-- If the posting's amount is explicit and multi-commodity, multiple similar
|
2018-10-22 16:46:31 +03:00
|
|
|
-- postings are shown, one for each commodity, to help produce parseable journal syntax.
|
|
|
|
-- Or if onelineamounts is true, such amounts are shown on one line, comma-separated
|
|
|
|
-- (and the output will not be valid journal syntax).
|
|
|
|
--
|
2019-07-15 13:28:52 +03:00
|
|
|
-- By default, 4 spaces (2 if there's a status flag) are shown between
|
2018-10-22 16:46:31 +03:00
|
|
|
-- account name and start of amount area, which is typically 12 chars wide
|
2019-07-15 13:28:52 +03:00
|
|
|
-- and contains a right-aligned amount (so 10-12 visible spaces between
|
2018-10-22 16:46:31 +03:00
|
|
|
-- account name and amount is typical).
|
2019-07-15 13:28:52 +03:00
|
|
|
-- When given a list of postings to be aligned with, the whitespace will be
|
2018-10-22 16:46:31 +03:00
|
|
|
-- increased if needed to match the posting with the longest account name.
|
|
|
|
-- This is used to align the amounts of a transaction's postings.
|
|
|
|
--
|
2015-10-30 04:05:02 +03:00
|
|
|
postingAsLines :: Bool -> Bool -> [Posting] -> Posting -> [String]
|
2018-10-22 16:46:31 +03:00
|
|
|
postingAsLines elideamount onelineamounts pstoalignwith p = concat [
|
2017-01-11 07:44:10 +03:00
|
|
|
postingblock
|
2013-09-10 21:32:49 +04:00
|
|
|
++ newlinecomments
|
2017-01-11 07:44:10 +03:00
|
|
|
| postingblock <- postingblocks]
|
2012-05-15 05:49:05 +04:00
|
|
|
where
|
2019-12-07 20:06:52 +03:00
|
|
|
postingblocks = [map rstrip $ lines $ concatTopPadded [statusandaccount, " ", amt, assertion, samelinecomment] | amt <- shownAmounts]
|
2019-07-15 13:28:52 +03:00
|
|
|
assertion = maybe "" ((' ':).showBalanceAssertion) $ pbalanceassertion p
|
2019-06-12 11:38:05 +03:00
|
|
|
statusandaccount = lineIndent $ fitString (Just $ minwidth) Nothing False True $ pstatusandacct p
|
2012-05-15 05:49:05 +04:00
|
|
|
where
|
2019-07-15 13:28:52 +03:00
|
|
|
-- pad to the maximum account name width, plus 2 to leave room for status flags, to keep amounts aligned
|
2018-10-22 16:46:31 +03:00
|
|
|
minwidth = maximum $ map ((2+) . textWidth . T.pack . pacctstr) pstoalignwith
|
2017-06-05 03:41:42 +03:00
|
|
|
pstatusandacct p' = pstatusprefix p' ++ pacctstr p'
|
|
|
|
pstatusprefix p' | null s = ""
|
|
|
|
| otherwise = s ++ " "
|
|
|
|
where s = show $ pstatus p'
|
|
|
|
pacctstr p' = showAccountName Nothing (ptype p') (paccount p')
|
2015-10-11 01:09:42 +03:00
|
|
|
|
|
|
|
-- currently prices are considered part of the amount string when right-aligning amounts
|
2017-01-11 07:44:10 +03:00
|
|
|
shownAmounts
|
|
|
|
| elideamount = [""]
|
2020-09-22 09:31:09 +03:00
|
|
|
| onelineamounts = [fst . showMixedOneLineUnnormalised showAmount (Just amtwidth) Nothing False $ pamount p]
|
2017-12-13 02:51:20 +03:00
|
|
|
| null (amounts $ pamount p) = [""]
|
2020-09-22 09:31:09 +03:00
|
|
|
| otherwise = lines . fst . showMixedUnnormalised showAmount (Just amtwidth) Nothing False $ pamount p
|
2015-10-11 01:09:42 +03:00
|
|
|
where
|
2020-11-02 07:12:09 +03:00
|
|
|
amtwidth = maximum $ 12 : map (snd . showMixedUnnormalised showAmount Nothing Nothing False . pamount) pstoalignwith -- min. 12 for backwards compatibility
|
2015-10-11 01:09:42 +03:00
|
|
|
|
|
|
|
(samelinecomment, newlinecomments) =
|
|
|
|
case renderCommentLines (pcomment p) of [] -> ("",[])
|
|
|
|
c:cs -> (c,cs)
|
2012-05-15 05:49:05 +04:00
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
-- | Render a balance assertion, as the =[=][*] symbol and expected amount.
|
2019-12-07 20:06:52 +03:00
|
|
|
showBalanceAssertion :: BalanceAssertion -> [Char]
|
2019-07-15 13:28:52 +03:00
|
|
|
showBalanceAssertion BalanceAssertion{..} =
|
2019-02-18 06:50:22 +03:00
|
|
|
"=" ++ ['=' | batotal] ++ ['*' | bainclusive] ++ " " ++ showAmountWithZeroCommodity baamount
|
|
|
|
|
2019-01-08 19:51:11 +03:00
|
|
|
-- | Render a posting, simply. Used in balance assertion errors.
|
|
|
|
-- showPostingLine p =
|
2019-06-12 11:38:05 +03:00
|
|
|
-- lineIndent $
|
2019-01-08 19:51:11 +03:00
|
|
|
-- if pstatus p == Cleared then "* " else "" ++ -- XXX show !
|
|
|
|
-- showAccountName Nothing (ptype p) (paccount p) ++
|
|
|
|
-- " " ++
|
|
|
|
-- showMixedAmountOneLine (pamount p) ++
|
|
|
|
-- assertion
|
|
|
|
-- where
|
|
|
|
-- -- XXX extract, handle ==
|
|
|
|
-- assertion = maybe "" ((" = " ++) . showAmountWithZeroCommodity . baamount) $ pbalanceassertion p
|
2016-02-10 18:39:03 +03:00
|
|
|
|
2018-10-22 16:46:31 +03:00
|
|
|
-- | Render a posting, at the appropriate width for aligning with
|
2019-07-15 13:28:52 +03:00
|
|
|
-- its siblings if any. Used by the rewrite command.
|
2017-01-20 18:33:24 +03:00
|
|
|
showPostingLines :: Posting -> [String]
|
|
|
|
showPostingLines p = postingAsLines False False ps p where
|
|
|
|
ps | Just t <- ptransaction p = tpostings t
|
|
|
|
| otherwise = [p]
|
|
|
|
|
2019-06-12 11:38:05 +03:00
|
|
|
-- | Prepend a suitable indent for a posting (or transaction/posting comment) line.
|
|
|
|
lineIndent :: String -> String
|
|
|
|
lineIndent = (" "++)
|
|
|
|
|
|
|
|
-- | Prepend the space required before a same-line comment.
|
|
|
|
commentSpace :: String -> String
|
|
|
|
commentSpace = (" "++)
|
2009-04-03 14:58:05 +04:00
|
|
|
|
2009-05-24 10:22:44 +04:00
|
|
|
-- | Show an account name, clipped to the given width if any, and
|
|
|
|
-- appropriately bracketed/parenthesised for the given posting type.
|
|
|
|
showAccountName :: Maybe Int -> PostingType -> AccountName -> String
|
|
|
|
showAccountName w = fmt
|
2020-07-16 13:28:48 +03:00
|
|
|
where
|
|
|
|
fmt RegularPosting = maybe id take w . T.unpack
|
|
|
|
fmt VirtualPosting = parenthesise . maybe id (takeEnd . subtract 2) w . T.unpack
|
|
|
|
fmt BalancedVirtualPosting = bracket . maybe id (takeEnd . subtract 2) w . T.unpack
|
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
|
|
|
|
|
|
|
parenthesise :: String -> String
|
|
|
|
parenthesise s = "("++s++")"
|
|
|
|
|
|
|
|
bracket :: String -> String
|
|
|
|
bracket s = "["++s++"]"
|
2009-05-24 10:22:44 +04:00
|
|
|
|
2011-06-29 04:31:37 +04:00
|
|
|
hasRealPostings :: Transaction -> Bool
|
|
|
|
hasRealPostings = not . null . realPostings
|
|
|
|
|
2010-02-27 21:06:29 +03:00
|
|
|
realPostings :: Transaction -> [Posting]
|
|
|
|
realPostings = filter isReal . tpostings
|
|
|
|
|
2016-12-10 18:04:48 +03:00
|
|
|
assignmentPostings :: Transaction -> [Posting]
|
2019-02-18 23:11:07 +03:00
|
|
|
assignmentPostings = filter hasBalanceAssignment . tpostings
|
2016-12-10 18:04:48 +03:00
|
|
|
|
2010-02-27 21:06:29 +03:00
|
|
|
virtualPostings :: Transaction -> [Posting]
|
|
|
|
virtualPostings = filter isVirtual . tpostings
|
|
|
|
|
|
|
|
balancedVirtualPostings :: Transaction -> [Posting]
|
|
|
|
balancedVirtualPostings = filter isBalancedVirtual . tpostings
|
|
|
|
|
2011-06-11 20:11:38 +04:00
|
|
|
transactionsPostings :: [Transaction] -> [Posting]
|
2019-02-14 16:14:52 +03:00
|
|
|
transactionsPostings = concatMap tpostings
|
2011-06-11 20:11:38 +04:00
|
|
|
|
2020-05-30 02:31:15 +03:00
|
|
|
-- | Check that this transaction would appear balanced to a human when displayed.
|
|
|
|
-- On success, returns the empty list, otherwise one or more error messages.
|
2020-05-29 23:09:17 +03:00
|
|
|
--
|
|
|
|
-- In more detail:
|
|
|
|
-- For the real postings, and separately for the balanced virtual postings:
|
|
|
|
--
|
|
|
|
-- 1. Convert amounts to cost where possible
|
|
|
|
--
|
2020-05-30 03:57:10 +03:00
|
|
|
-- 2. When there are two or more non-zero amounts
|
|
|
|
-- (appearing non-zero when displayed, using the given display styles if provided),
|
2020-05-29 23:09:17 +03:00
|
|
|
-- are they a mix of positives and negatives ?
|
|
|
|
-- This is checked separately to give a clearer error message.
|
2020-05-30 03:57:10 +03:00
|
|
|
-- (Best effort; could be confused by postings with multicommodity amounts.)
|
2020-05-29 23:09:17 +03:00
|
|
|
--
|
|
|
|
-- 3. Does the amounts' sum appear non-zero when displayed ?
|
|
|
|
-- (using the given display styles if provided)
|
|
|
|
--
|
2020-05-30 02:31:15 +03:00
|
|
|
transactionCheckBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> [String]
|
|
|
|
transactionCheckBalanced mstyles t = errs
|
2020-05-29 21:11:47 +03:00
|
|
|
where
|
2020-05-29 23:09:17 +03:00
|
|
|
(rps, bvps) = (realPostings t, balancedVirtualPostings t)
|
|
|
|
|
2020-05-30 03:57:10 +03:00
|
|
|
-- check for mixed signs, detecting nonzeros at display precision
|
|
|
|
canonicalise = maybe id canonicaliseMixedAmount mstyles
|
2020-05-29 23:09:17 +03:00
|
|
|
signsOk ps =
|
2020-06-01 01:48:08 +03:00
|
|
|
case filter (not.mixedAmountLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of
|
2020-05-30 03:57:10 +03:00
|
|
|
nonzeros | length nonzeros >= 2
|
|
|
|
-> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1
|
|
|
|
_ -> True
|
2020-05-29 23:09:17 +03:00
|
|
|
(rsignsok, bvsignsok) = (signsOk rps, signsOk bvps)
|
|
|
|
|
2020-05-30 03:57:10 +03:00
|
|
|
-- check for zero sum, at display precision
|
2020-05-29 23:09:17 +03:00
|
|
|
(rsum, bvsum) = (sumPostings rps, sumPostings bvps)
|
2020-06-01 01:48:08 +03:00
|
|
|
(rsumcost, bvsumcost) = (mixedAmountCost rsum, mixedAmountCost bvsum)
|
2020-05-29 21:11:47 +03:00
|
|
|
(rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost)
|
2020-05-30 04:57:22 +03:00
|
|
|
(rsumok, bvsumok) = (mixedAmountLooksZero rsumdisplay, mixedAmountLooksZero bvsumdisplay)
|
2020-05-30 03:57:10 +03:00
|
|
|
|
|
|
|
-- generate error messages, showing amounts with their original precision
|
2020-05-30 02:31:15 +03:00
|
|
|
errs = filter (not.null) [rmsg, bvmsg]
|
2020-05-30 03:57:10 +03:00
|
|
|
where
|
|
|
|
rmsg
|
|
|
|
| not rsignsok = "real postings all have the same sign"
|
|
|
|
| not rsumok = "real postings' sum should be 0 but is: " ++ showMixedAmount rsumcost
|
|
|
|
| otherwise = ""
|
|
|
|
bvmsg
|
|
|
|
| not bvsignsok = "balanced virtual postings all have the same sign"
|
|
|
|
| not bvsumok = "balanced virtual postings' sum should be 0 but is: " ++ showMixedAmount bvsumcost
|
|
|
|
| otherwise = ""
|
2020-05-29 21:11:47 +03:00
|
|
|
|
|
|
|
-- | Legacy form of transactionCheckBalanced.
|
2019-10-20 05:17:35 +03:00
|
|
|
isTransactionBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> Bool
|
2020-05-30 02:31:15 +03:00
|
|
|
isTransactionBalanced mstyles = null . transactionCheckBalanced mstyles
|
2010-11-15 01:44:37 +03:00
|
|
|
|
2019-07-15 13:28:52 +03:00
|
|
|
-- | Balance this transaction, ensuring that its postings
|
2019-02-18 23:11:07 +03:00
|
|
|
-- (and its balanced virtual postings) sum to 0,
|
2019-07-15 13:28:52 +03:00
|
|
|
-- by inferring a missing amount or conversion price(s) if needed.
|
2019-02-18 23:11:07 +03:00
|
|
|
-- Or if balancing is not possible, because the amounts don't sum to 0 or
|
|
|
|
-- because there's more than one missing amount, return an error message.
|
|
|
|
--
|
|
|
|
-- Transactions with balance assignments can have more than one
|
2019-07-15 13:28:52 +03:00
|
|
|
-- missing amount; to balance those you should use the more powerful
|
2019-02-18 23:11:07 +03:00
|
|
|
-- journalBalanceTransactions.
|
|
|
|
--
|
|
|
|
-- The "sum to 0" test is done using commodity display precisions,
|
|
|
|
-- if provided, so that the result agrees with the numbers users can see.
|
|
|
|
--
|
2019-02-15 21:34:40 +03:00
|
|
|
balanceTransaction ::
|
2019-10-20 05:17:35 +03:00
|
|
|
Maybe (M.Map CommoditySymbol AmountStyle) -- ^ commodity display styles
|
2019-02-15 21:34:40 +03:00
|
|
|
-> Transaction
|
|
|
|
-> Either String Transaction
|
2019-07-15 13:28:52 +03:00
|
|
|
balanceTransaction mstyles = fmap fst . balanceTransactionHelper mstyles
|
2019-02-15 21:34:40 +03:00
|
|
|
|
2019-02-18 23:11:07 +03:00
|
|
|
-- | Helper used by balanceTransaction and balanceTransactionWithBalanceAssignmentAndCheckAssertionsB;
|
2019-07-15 13:28:52 +03:00
|
|
|
-- use one of those instead. It also returns a list of accounts
|
2019-02-18 23:11:07 +03:00
|
|
|
-- and amounts that were inferred.
|
2019-02-15 21:34:40 +03:00
|
|
|
balanceTransactionHelper ::
|
2019-10-20 05:17:35 +03:00
|
|
|
Maybe (M.Map CommoditySymbol AmountStyle) -- ^ commodity display styles
|
2019-02-15 21:34:40 +03:00
|
|
|
-> Transaction
|
|
|
|
-> Either String (Transaction, [(AccountName, MixedAmount)])
|
|
|
|
balanceTransactionHelper mstyles t = do
|
2019-07-15 13:28:52 +03:00
|
|
|
(t', inferredamtsandaccts) <-
|
2019-10-20 05:17:35 +03:00
|
|
|
inferBalancingAmount (fromMaybe M.empty mstyles) $ inferBalancingPrices t
|
2020-05-29 21:11:47 +03:00
|
|
|
case transactionCheckBalanced mstyles t' of
|
2020-05-30 02:31:15 +03:00
|
|
|
[] -> Right (txnTieKnot t', inferredamtsandaccts)
|
|
|
|
errs -> Left $ transactionBalanceError t' errs
|
|
|
|
|
|
|
|
-- | Generate a transaction balancing error message, given the transaction
|
|
|
|
-- and one or more suberror messages.
|
|
|
|
transactionBalanceError :: Transaction -> [String] -> String
|
|
|
|
transactionBalanceError t errs =
|
|
|
|
annotateErrorWithTransaction t $
|
|
|
|
intercalate "\n" $ "could not balance this transaction:" : errs
|
2015-06-29 00:13:11 +03:00
|
|
|
|
2019-02-18 23:11:07 +03:00
|
|
|
annotateErrorWithTransaction :: Transaction -> String -> String
|
2020-05-30 02:31:15 +03:00
|
|
|
annotateErrorWithTransaction t s =
|
|
|
|
unlines [showGenericSourcePos $ tsourcepos t, s, rstrip $ showTransaction t]
|
2018-10-24 01:23:26 +03:00
|
|
|
|
2015-06-29 00:13:11 +03:00
|
|
|
-- | Infer up to one missing amount for this transactions's real postings, and
|
|
|
|
-- likewise for its balanced virtual postings, if needed; or return an error
|
2019-02-15 21:34:40 +03:00
|
|
|
-- message if we can't. Returns the updated transaction and any inferred posting amounts,
|
|
|
|
-- with the corresponding accounts, in order).
|
2015-06-29 00:13:11 +03:00
|
|
|
--
|
|
|
|
-- We can infer a missing amount when there are multiple postings and exactly
|
|
|
|
-- one of them is amountless. If the amounts had price(s) the inferred amount
|
|
|
|
-- have the same price(s), and will be converted to the price commodity.
|
2019-07-15 13:28:52 +03:00
|
|
|
inferBalancingAmount ::
|
2019-10-20 05:17:35 +03:00
|
|
|
M.Map CommoditySymbol AmountStyle -- ^ commodity display styles
|
2019-02-15 21:34:40 +03:00
|
|
|
-> Transaction
|
|
|
|
-> Either String (Transaction, [(AccountName, MixedAmount)])
|
|
|
|
inferBalancingAmount styles t@Transaction{tpostings=ps}
|
2015-06-28 22:02:33 +03:00
|
|
|
| length amountlessrealps > 1
|
2020-05-30 02:31:15 +03:00
|
|
|
= Left $ transactionBalanceError t
|
|
|
|
["can't have more than one real posting with no amount"
|
|
|
|
,"(remember to put two or more spaces between account and amount)"]
|
2015-06-28 22:02:33 +03:00
|
|
|
| length amountlessbvps > 1
|
2020-05-30 02:31:15 +03:00
|
|
|
= Left $ transactionBalanceError t
|
|
|
|
["can't have more than one balanced virtual posting with no amount"
|
|
|
|
,"(remember to put two or more spaces between account and amount)"]
|
2015-06-28 22:02:33 +03:00
|
|
|
| otherwise
|
2019-02-15 21:34:40 +03:00
|
|
|
= let psandinferredamts = map inferamount ps
|
|
|
|
inferredacctsandamts = [(paccount p, amt) | (p, Just amt) <- psandinferredamts]
|
|
|
|
in Right (t{tpostings=map fst psandinferredamts}, inferredacctsandamts)
|
2015-06-28 22:02:33 +03:00
|
|
|
where
|
2017-01-13 03:24:53 +03:00
|
|
|
(amountfulrealps, amountlessrealps) = partition hasAmount (realPostings t)
|
|
|
|
realsum = sumStrict $ map pamount amountfulrealps
|
|
|
|
(amountfulbvps, amountlessbvps) = partition hasAmount (balancedVirtualPostings t)
|
|
|
|
bvsum = sumStrict $ map pamount amountfulbvps
|
2019-02-15 21:34:40 +03:00
|
|
|
|
|
|
|
inferamount :: Posting -> (Posting, Maybe MixedAmount)
|
|
|
|
inferamount p =
|
|
|
|
let
|
|
|
|
minferredamt = case ptype p of
|
2019-07-15 13:28:52 +03:00
|
|
|
RegularPosting | not (hasAmount p) -> Just realsum
|
|
|
|
BalancedVirtualPosting | not (hasAmount p) -> Just bvsum
|
|
|
|
_ -> Nothing
|
2019-02-15 21:34:40 +03:00
|
|
|
in
|
|
|
|
case minferredamt of
|
|
|
|
Nothing -> (p, Nothing)
|
2019-07-15 13:28:52 +03:00
|
|
|
Just a -> (p{pamount=a', poriginal=Just $ originalPosting p}, Just a')
|
2019-02-15 21:34:40 +03:00
|
|
|
where
|
|
|
|
-- Inferred amounts are converted to cost.
|
2019-07-15 13:28:52 +03:00
|
|
|
-- Also ensure the new amount has the standard style for its commodity
|
2019-02-15 21:34:40 +03:00
|
|
|
-- (since the main amount styling pass happened before this balancing pass);
|
2020-06-01 01:48:08 +03:00
|
|
|
a' = styleMixedAmount styles $ normaliseMixedAmount $ mixedAmountCost (-a)
|
2009-04-10 12:05:56 +04:00
|
|
|
|
2015-06-29 00:13:11 +03:00
|
|
|
-- | Infer prices for this transaction's posting amounts, if needed to make
|
2015-07-03 03:36:09 +03:00
|
|
|
-- the postings balance, and if possible. This is done once for the real
|
|
|
|
-- postings and again (separately) for the balanced virtual postings. When
|
|
|
|
-- it's not possible, the transaction is left unchanged.
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-06-29 00:13:11 +03:00
|
|
|
-- The simplest example is a transaction with two postings, each in a
|
|
|
|
-- different commodity, with no prices specified. In this case we'll add a
|
|
|
|
-- price to the first posting such that it can be converted to the commodity
|
|
|
|
-- of the second posting (with -B), and such that the postings balance.
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-06-29 00:13:11 +03:00
|
|
|
-- In general, we can infer a conversion price when the sum of posting amounts
|
2015-07-03 04:06:03 +03:00
|
|
|
-- contains exactly two different commodities and no explicit prices. Also
|
|
|
|
-- all postings are expected to contain an explicit amount (no missing
|
|
|
|
-- amounts) in a single commodity. Otherwise no price inferring is attempted.
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-07-03 04:06:03 +03:00
|
|
|
-- The transaction itself could contain more than two commodities, and/or
|
|
|
|
-- prices, if they cancel out; what matters is that the sum of posting amounts
|
|
|
|
-- contains exactly two commodities and zero prices.
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-06-29 00:13:11 +03:00
|
|
|
-- There can also be more than two postings in either of the commodities.
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-06-29 00:13:11 +03:00
|
|
|
-- We want to avoid excessive display of digits when the calculated price is
|
2015-07-03 03:36:09 +03:00
|
|
|
-- an irrational number, while hopefully also ensuring the displayed numbers
|
|
|
|
-- make sense if the user does a manual calculation. This is (mostly) achieved
|
|
|
|
-- in two ways:
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-06-29 00:13:11 +03:00
|
|
|
-- - when there is only one posting in the "from" commodity, a total price
|
|
|
|
-- (@@) is used, and all available decimal digits are shown
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-06-29 00:13:11 +03:00
|
|
|
-- - otherwise, a suitable averaged unit price (@) is applied to the relevant
|
2015-07-03 03:36:09 +03:00
|
|
|
-- postings, with display precision equal to the summed display precisions
|
|
|
|
-- of the two commodities being converted between, or 2, whichever is larger.
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-07-03 03:36:09 +03:00
|
|
|
-- (We don't always calculate a good-looking display precision for unit prices
|
|
|
|
-- when the commodity display precisions are low, eg when a journal doesn't
|
|
|
|
-- use any decimal places. The minimum of 2 helps make the prices shown by the
|
|
|
|
-- print command a bit less surprising in this case. Could do better.)
|
2016-12-10 18:04:48 +03:00
|
|
|
--
|
2015-06-29 00:13:11 +03:00
|
|
|
inferBalancingPrices :: Transaction -> Transaction
|
|
|
|
inferBalancingPrices t@Transaction{tpostings=ps} = t{tpostings=ps'}
|
|
|
|
where
|
2019-02-14 16:14:52 +03:00
|
|
|
ps' = map (priceInferrerFor t BalancedVirtualPosting . priceInferrerFor t RegularPosting) ps
|
2015-06-29 00:13:11 +03:00
|
|
|
|
|
|
|
-- | Generate a posting update function which assigns a suitable balancing
|
|
|
|
-- price to the posting, if and as appropriate for the given transaction and
|
|
|
|
-- posting type (real or balanced virtual).
|
|
|
|
priceInferrerFor :: Transaction -> PostingType -> (Posting -> Posting)
|
|
|
|
priceInferrerFor t pt = inferprice
|
|
|
|
where
|
|
|
|
postings = filter ((==pt).ptype) $ tpostings t
|
|
|
|
pmixedamounts = map pamount postings
|
|
|
|
pamounts = concatMap amounts pmixedamounts
|
|
|
|
pcommodities = map acommodity pamounts
|
2017-01-13 03:24:53 +03:00
|
|
|
sumamounts = amounts $ sumStrict pmixedamounts -- sum normalises to one amount per commodity & price
|
2015-06-29 00:13:11 +03:00
|
|
|
sumcommodities = map acommodity sumamounts
|
2019-06-08 00:23:19 +03:00
|
|
|
sumprices = filter (/=Nothing) $ map aprice sumamounts
|
2015-06-29 00:13:11 +03:00
|
|
|
caninferprices = length sumcommodities == 2 && null sumprices
|
|
|
|
|
|
|
|
inferprice p@Posting{pamount=Mixed [a]}
|
|
|
|
| caninferprices && ptype p == pt && acommodity a == fromcommodity
|
2019-06-08 00:23:19 +03:00
|
|
|
= p{pamount=Mixed [a{aprice=Just conversionprice}], poriginal=Just $ originalPosting p}
|
2015-06-29 00:13:11 +03:00
|
|
|
where
|
|
|
|
fromcommodity = head $ filter (`elem` sumcommodities) pcommodities -- these heads are ugly but should be safe
|
|
|
|
conversionprice
|
2020-08-13 14:15:41 +03:00
|
|
|
| fromcount==1 = TotalPrice $ abs toamount `withPrecision` NaturalPrecision
|
2015-07-03 03:36:09 +03:00
|
|
|
| otherwise = UnitPrice $ abs unitprice `withPrecision` unitprecision
|
2015-06-29 00:13:11 +03:00
|
|
|
where
|
2015-07-03 03:36:09 +03:00
|
|
|
fromcount = length $ filter ((==fromcommodity).acommodity) pamounts
|
|
|
|
fromamount = head $ filter ((==fromcommodity).acommodity) sumamounts
|
2020-08-13 14:15:41 +03:00
|
|
|
fromprecision = asprecision $ astyle fromamount
|
2015-07-03 03:36:09 +03:00
|
|
|
tocommodity = head $ filter (/=fromcommodity) sumcommodities
|
|
|
|
toamount = head $ filter ((==tocommodity).acommodity) sumamounts
|
2020-08-13 14:15:41 +03:00
|
|
|
toprecision = asprecision $ astyle toamount
|
2018-11-14 04:25:32 +03:00
|
|
|
unitprice = (aquantity fromamount) `divideAmount` toamount
|
2020-08-13 14:15:41 +03:00
|
|
|
-- Sum two display precisions, capping the result at the maximum bound
|
|
|
|
unitprecision = case (fromprecision, toprecision) of
|
|
|
|
(Precision a, Precision b) -> Precision $ if maxBound - a < b then maxBound else max 2 (a + b)
|
|
|
|
_ -> NaturalPrecision
|
2015-06-29 00:13:11 +03:00
|
|
|
inferprice p = p
|
2009-07-09 23:22:27 +04:00
|
|
|
|
2012-12-06 08:43:41 +04:00
|
|
|
-- Get a transaction's secondary date, defaulting to the primary date.
|
|
|
|
transactionDate2 :: Transaction -> Day
|
|
|
|
transactionDate2 t = fromMaybe (tdate t) $ tdate2 t
|
2011-09-23 18:50:20 +04:00
|
|
|
|
2015-06-28 22:02:33 +03:00
|
|
|
-- | Ensure a transaction's postings refer back to it, so that eg
|
|
|
|
-- relatedPostings works right.
|
2009-12-19 06:44:52 +03:00
|
|
|
txnTieKnot :: Transaction -> Transaction
|
2017-01-16 18:42:41 +03:00
|
|
|
txnTieKnot t@Transaction{tpostings=ps} = t' where
|
|
|
|
t' = t{tpostings=map (postingSetTransaction t') ps}
|
2009-12-19 06:44:52 +03:00
|
|
|
|
lib: textification begins! account names
The first of several conversions from String to (strict) Text, hopefully
reducing space and time usage.
This one shows a small improvement, with GHC 7.10.3 and text-1.2.2.1:
hledger -f data/100x100x10.journal stats
string: <<ghc: 39471064 bytes, 77 GCs, 198421/275048 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.001 elapsed), 0.015 MUT (0.020 elapsed), 0.010 GC (0.014 elapsed) :ghc>>
text: <<ghc: 39268024 bytes, 77 GCs, 197018/270840 avg/max bytes residency (3 samples), 2M in use, 0.000 INIT (0.002 elapsed), 0.016 MUT (0.022 elapsed), 0.009 GC (0.011 elapsed) :ghc>>
hledger -f data/1000x100x10.journal stats
string: <<ghc: 318555920 bytes, 617 GCs, 2178997/7134472 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.001 elapsed), 0.129 MUT (0.136 elapsed), 0.067 GC (0.077 elapsed) :ghc>>
text: <<ghc: 314248496 bytes, 612 GCs, 2074045/6617960 avg/max bytes residency (7 samples), 16M in use, 0.000 INIT (0.003 elapsed), 0.137 MUT (0.145 elapsed), 0.067 GC (0.079 elapsed) :ghc>>
hledger -f data/10000x100x10.journal stats
string: <<ghc: 3114763608 bytes, 6026 GCs, 18858950/75552024 avg/max bytes residency (11 samples), 201M in use, 0.000 INIT (0.000 elapsed), 1.331 MUT (1.372 elapsed), 0.699 GC (0.812 elapsed) :ghc>>
text: <<ghc: 3071468920 bytes, 5968 GCs, 14120344/62951360 avg/max bytes residency (9 samples), 124M in use, 0.000 INIT (0.003 elapsed), 1.272 MUT (1.349 elapsed), 0.513 GC (0.578 elapsed) :ghc>>
hledger -f data/100000x100x10.journal stats
string: <<ghc: 31186579432 bytes, 60278 GCs, 135332581/740228992 avg/max bytes residency (13 samples), 1697M in use, 0.000 INIT (0.008 elapsed), 14.677 MUT (15.508 elapsed), 7.081 GC (8.074 elapsed) :ghc>>
text: <<ghc: 30753427672 bytes, 59763 GCs, 117595958/666457240 avg/max bytes residency (14 samples), 1588M in use, 0.000 INIT (0.008 elapsed), 13.713 MUT (13.966 elapsed), 6.220 GC (7.108 elapsed) :ghc>>
2016-05-24 04:16:21 +03:00
|
|
|
-- | Ensure a transaction's postings do not refer back to it, so that eg
|
|
|
|
-- recursiveSize and GHCI's :sprint work right.
|
|
|
|
txnUntieKnot :: Transaction -> Transaction
|
|
|
|
txnUntieKnot t@Transaction{tpostings=ps} = t{tpostings=map (\p -> p{ptransaction=Nothing}) ps}
|
|
|
|
|
2009-12-19 06:44:52 +03:00
|
|
|
-- | Set a posting's parent transaction.
|
2016-08-14 22:44:19 +03:00
|
|
|
postingSetTransaction :: Transaction -> Posting -> Posting
|
|
|
|
postingSetTransaction t p = p{ptransaction=Just t}
|
2009-12-21 08:23:07 +03:00
|
|
|
|
2019-10-20 05:41:21 +03:00
|
|
|
-- | Apply a transform function to this transaction's amounts.
|
|
|
|
transactionTransformPostings :: (Posting -> Posting) -> Transaction -> Transaction
|
|
|
|
transactionTransformPostings f t@Transaction{tpostings=ps} = t{tpostings=map f ps}
|
|
|
|
|
|
|
|
-- | Apply a specified valuation to this transaction's amounts, using
|
|
|
|
-- the provided price oracle, commodity styles, reference dates, and
|
|
|
|
-- whether this is for a multiperiod report or not. See
|
|
|
|
-- amountApplyValuation.
|
2020-12-17 09:13:06 +03:00
|
|
|
transactionApplyValuation :: PriceOracle -> M.Map CommoditySymbol AmountStyle -> Day -> Day -> Transaction -> ValuationType -> Transaction
|
|
|
|
transactionApplyValuation priceoracle styles periodlast today t v =
|
|
|
|
transactionTransformPostings (\p -> postingApplyValuation priceoracle styles periodlast today p v) t
|
2019-10-20 05:41:21 +03:00
|
|
|
|
|
|
|
-- | Convert this transaction's amounts to cost, and apply the appropriate amount styles.
|
|
|
|
transactionToCost :: M.Map CommoditySymbol AmountStyle -> Transaction -> Transaction
|
|
|
|
transactionToCost styles t@Transaction{tpostings=ps} = t{tpostings=map (postingToCost styles) ps}
|
|
|
|
|
2020-11-24 20:17:01 +03:00
|
|
|
-- | Apply some account aliases to all posting account names in the transaction, as described by accountNameApplyAliases.
|
2020-11-26 07:59:07 +03:00
|
|
|
-- This can fail due to a bad replacement pattern in a regular expression alias.
|
|
|
|
transactionApplyAliases :: [AccountAlias] -> Transaction -> Either RegexError Transaction
|
2020-11-24 20:17:01 +03:00
|
|
|
transactionApplyAliases aliases t =
|
2020-11-26 07:59:07 +03:00
|
|
|
case mapM (postingApplyAliases aliases) $ tpostings t of
|
|
|
|
Right ps -> Right $ txnTieKnot $ t{tpostings=ps}
|
|
|
|
Left err -> Left err
|
2020-11-24 20:17:01 +03:00
|
|
|
|
2018-09-04 21:31:31 +03:00
|
|
|
-- tests
|
|
|
|
|
2019-12-07 20:06:52 +03:00
|
|
|
tests_Transaction :: TestTree
|
2019-02-14 16:14:52 +03:00
|
|
|
tests_Transaction =
|
2019-11-27 23:46:29 +03:00
|
|
|
tests "Transaction" [
|
|
|
|
|
|
|
|
tests "postingAsLines" [
|
2019-11-29 02:29:03 +03:00
|
|
|
test "null posting" $ postingAsLines False False [posting] posting @?= [""]
|
|
|
|
, test "non-null posting" $
|
2019-11-27 23:46:29 +03:00
|
|
|
let p =
|
2019-02-14 16:14:52 +03:00
|
|
|
posting
|
|
|
|
{ pstatus = Cleared
|
|
|
|
, paccount = "a"
|
|
|
|
, pamount = Mixed [usd 1, hrs 2]
|
|
|
|
, pcomment = "pcomment1\npcomment2\n tag3: val3 \n"
|
|
|
|
, ptype = RegularPosting
|
|
|
|
, ptags = [("ptag1", "val1"), ("ptag2", "val2")]
|
|
|
|
}
|
2019-11-27 23:46:29 +03:00
|
|
|
in postingAsLines False False [p] p @?=
|
2019-06-12 16:01:17 +03:00
|
|
|
[ " * a $1.00 ; pcomment1"
|
2019-02-14 16:14:52 +03:00
|
|
|
, " ; pcomment2"
|
|
|
|
, " ; tag3: val3 "
|
2019-06-12 16:01:17 +03:00
|
|
|
, " * a 2.00h ; pcomment1"
|
2019-02-14 16:14:52 +03:00
|
|
|
, " ; pcomment2"
|
|
|
|
, " ; tag3: val3 "
|
|
|
|
]
|
|
|
|
]
|
2019-11-27 23:46:29 +03:00
|
|
|
|
|
|
|
, let
|
|
|
|
-- one implicit amount
|
|
|
|
timp = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` missingamt]}
|
|
|
|
-- explicit amounts, balanced
|
|
|
|
texp = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` usd (-1)]}
|
|
|
|
-- explicit amount, only one posting
|
|
|
|
texp1 = nulltransaction {tpostings = ["(a)" `post` usd 1]}
|
|
|
|
-- explicit amounts, two commodities, explicit balancing price
|
|
|
|
texp2 = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` (hrs (-1) `at` usd 1)]}
|
|
|
|
-- explicit amounts, two commodities, implicit balancing price
|
|
|
|
texp2b = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` hrs (-1)]}
|
|
|
|
-- one missing amount, not the last one
|
|
|
|
t3 = nulltransaction {tpostings = ["a" `post` usd 1, "b" `post` missingamt, "c" `post` usd (-1)]}
|
|
|
|
-- unbalanced amounts when precision is limited (#931)
|
|
|
|
-- t4 = nulltransaction {tpostings = ["a" `post` usd (-0.01), "b" `post` usd (0.005), "c" `post` usd (0.005)]}
|
|
|
|
in tests "postingsAsLines" [
|
2019-11-29 02:29:03 +03:00
|
|
|
test "null-transaction" $ postingsAsLines False (tpostings nulltransaction) @?= []
|
|
|
|
, test "implicit-amount" $ postingsAsLines False (tpostings timp) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
[ " a $1.00"
|
|
|
|
, " b" -- implicit amount remains implicit
|
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "explicit-amounts" $ postingsAsLines False (tpostings texp) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
[ " a $1.00"
|
2019-11-16 14:23:38 +03:00
|
|
|
, " b $-1.00"
|
2019-02-14 16:14:52 +03:00
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "one-explicit-amount" $ postingsAsLines False (tpostings texp1) @?=
|
2019-11-16 14:23:38 +03:00
|
|
|
[ " (a) $1.00"
|
2019-02-14 16:14:52 +03:00
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "explicit-amounts-two-commodities" $ postingsAsLines False (tpostings texp2) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
[ " a $1.00"
|
2019-11-16 14:23:38 +03:00
|
|
|
, " b -1.00h @ $1.00"
|
2019-02-14 16:14:52 +03:00
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "explicit-amounts-not-explicitly-balanced" $ postingsAsLines False (tpostings texp2b) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
[ " a $1.00"
|
2019-11-16 14:23:38 +03:00
|
|
|
, " b -1.00h"
|
2019-02-14 16:14:52 +03:00
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "implicit-amount-not-last" $ postingsAsLines False (tpostings t3) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
[" a $1.00", " b", " c $-1.00"]
|
2019-11-29 02:29:03 +03:00
|
|
|
-- , test "ensure-visibly-balanced" $
|
2019-11-27 23:46:29 +03:00
|
|
|
-- in postingsAsLines False (tpostings t4) @?=
|
2019-11-27 00:56:14 +03:00
|
|
|
-- [" a $-0.01", " b $0.005", " c $0.005"]
|
|
|
|
|
2019-02-14 16:14:52 +03:00
|
|
|
]
|
2019-11-27 23:46:29 +03:00
|
|
|
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "inferBalancingAmount" $ do
|
2019-11-27 23:46:29 +03:00
|
|
|
(fst <$> inferBalancingAmount M.empty nulltransaction) @?= Right nulltransaction
|
|
|
|
(fst <$> inferBalancingAmount M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` missingamt]}) @?=
|
2019-02-15 21:34:40 +03:00
|
|
|
Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` usd 5]}
|
2019-11-27 23:46:29 +03:00
|
|
|
(fst <$> inferBalancingAmount M.empty nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` missingamt]}) @?=
|
2019-02-15 21:34:40 +03:00
|
|
|
Right nulltransaction{tpostings = ["a" `post` usd (-5), "b" `post` (eur 3 @@ usd 4), "c" `post` usd 1]}
|
2019-11-27 23:46:29 +03:00
|
|
|
|
|
|
|
, tests "showTransaction" [
|
2019-12-30 01:36:32 +03:00
|
|
|
test "null transaction" $ showTransaction nulltransaction @?= "0000-01-01\n\n"
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "non-null transaction" $ showTransaction
|
2019-11-16 14:23:38 +03:00
|
|
|
nulltransaction
|
2020-08-26 11:11:20 +03:00
|
|
|
{ tdate = fromGregorian 2012 05 14
|
|
|
|
, tdate2 = Just $ fromGregorian 2012 05 15
|
2019-11-16 14:23:38 +03:00
|
|
|
, tstatus = Unmarked
|
|
|
|
, tcode = "code"
|
|
|
|
, tdescription = "desc"
|
|
|
|
, tcomment = "tcomment1\ntcomment2\n"
|
|
|
|
, ttags = [("ttag1", "val1")]
|
|
|
|
, tpostings =
|
|
|
|
[ nullposting
|
|
|
|
{ pstatus = Cleared
|
|
|
|
, paccount = "a"
|
|
|
|
, pamount = Mixed [usd 1, hrs 2]
|
|
|
|
, pcomment = "\npcomment2\n"
|
|
|
|
, ptype = RegularPosting
|
|
|
|
, ptags = [("ptag1", "val1"), ("ptag2", "val2")]
|
|
|
|
}
|
2019-02-14 16:14:52 +03:00
|
|
|
]
|
2019-11-27 23:46:29 +03:00
|
|
|
} @?=
|
2019-11-16 14:23:38 +03:00
|
|
|
unlines
|
2019-12-30 01:36:32 +03:00
|
|
|
[ "2012-05-14=2012-05-15 (code) desc ; tcomment1"
|
2019-11-16 14:23:38 +03:00
|
|
|
, " ; tcomment2"
|
|
|
|
, " * a $1.00"
|
|
|
|
, " ; pcomment2"
|
|
|
|
, " * a 2.00h"
|
|
|
|
, " ; pcomment2"
|
|
|
|
, ""
|
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "show a balanced transaction" $
|
2019-02-14 16:14:52 +03:00
|
|
|
(let t =
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2007 01 28)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"coopportunity"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "expenses:food:groceries", pamount = Mixed [usd 47.18], ptransaction = Just t}
|
|
|
|
, posting {paccount = "assets:checking", pamount = Mixed [usd (-47.18)], ptransaction = Just t}
|
|
|
|
]
|
2019-11-27 23:46:29 +03:00
|
|
|
in showTransaction t) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
(unlines
|
2019-12-30 01:36:32 +03:00
|
|
|
[ "2007-01-28 coopportunity"
|
2019-02-14 16:14:52 +03:00
|
|
|
, " expenses:food:groceries $47.18"
|
|
|
|
, " assets:checking $-47.18"
|
|
|
|
, ""
|
|
|
|
])
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "show an unbalanced transaction, should not elide" $
|
2019-02-14 16:14:52 +03:00
|
|
|
(showTransaction
|
|
|
|
(txnTieKnot $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2007 01 28)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"coopportunity"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "expenses:food:groceries", pamount = Mixed [usd 47.18]}
|
|
|
|
, posting {paccount = "assets:checking", pamount = Mixed [usd (-47.19)]}
|
2019-11-27 23:46:29 +03:00
|
|
|
])) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
(unlines
|
2019-12-30 01:36:32 +03:00
|
|
|
[ "2007-01-28 coopportunity"
|
2019-02-14 16:14:52 +03:00
|
|
|
, " expenses:food:groceries $47.18"
|
|
|
|
, " assets:checking $-47.19"
|
|
|
|
, ""
|
|
|
|
])
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "show a transaction with one posting and a missing amount" $
|
2019-02-14 16:14:52 +03:00
|
|
|
(showTransaction
|
|
|
|
(txnTieKnot $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2007 01 28)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"coopportunity"
|
|
|
|
""
|
|
|
|
[]
|
2019-11-27 23:46:29 +03:00
|
|
|
[posting {paccount = "expenses:food:groceries", pamount = missingmixedamt}])) @?=
|
2019-12-30 01:36:32 +03:00
|
|
|
(unlines ["2007-01-28 coopportunity", " expenses:food:groceries", ""])
|
2019-11-29 02:29:03 +03:00
|
|
|
, test "show a transaction with a priced commodityless amount" $
|
2019-02-14 16:14:52 +03:00
|
|
|
(showTransaction
|
|
|
|
(txnTieKnot $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2010 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"x"
|
|
|
|
""
|
|
|
|
[]
|
2020-08-13 14:15:41 +03:00
|
|
|
[ posting {paccount = "a", pamount = Mixed [num 1 `at` (usd 2 `withPrecision` Precision 0)]}
|
2019-02-14 16:14:52 +03:00
|
|
|
, posting {paccount = "b", pamount = missingmixedamt}
|
2019-11-27 23:46:29 +03:00
|
|
|
])) @?=
|
2019-12-30 01:36:32 +03:00
|
|
|
(unlines ["2010-01-01 x", " a 1 @ $2", " b", ""])
|
2019-02-14 16:14:52 +03:00
|
|
|
]
|
2019-11-27 23:46:29 +03:00
|
|
|
, tests "balanceTransaction" [
|
2019-11-29 02:29:03 +03:00
|
|
|
test "detect unbalanced entry, sign error" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertLeft
|
2019-02-14 16:14:52 +03:00
|
|
|
(balanceTransaction
|
|
|
|
Nothing
|
|
|
|
(Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2007 01 28)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"test"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[posting {paccount = "a", pamount = Mixed [usd 1]}, posting {paccount = "b", pamount = Mixed [usd 1]}]))
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "detect unbalanced entry, multiple missing amounts" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertLeft $
|
2019-02-14 16:14:52 +03:00
|
|
|
balanceTransaction
|
|
|
|
Nothing
|
|
|
|
(Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2007 01 28)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"test"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "a", pamount = missingmixedamt}
|
|
|
|
, posting {paccount = "b", pamount = missingmixedamt}
|
|
|
|
])
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "one missing amount is inferred" $
|
2019-02-14 16:14:52 +03:00
|
|
|
(pamount . last . tpostings <$>
|
|
|
|
balanceTransaction
|
|
|
|
Nothing
|
|
|
|
(Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2007 01 28)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
""
|
|
|
|
""
|
|
|
|
[]
|
2019-11-27 23:46:29 +03:00
|
|
|
[posting {paccount = "a", pamount = Mixed [usd 1]}, posting {paccount = "b", pamount = missingmixedamt}])) @?=
|
2019-02-14 16:14:52 +03:00
|
|
|
Right (Mixed [usd (-1)])
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "conversion price is inferred" $
|
2019-02-14 16:14:52 +03:00
|
|
|
(pamount . head . tpostings <$>
|
|
|
|
balanceTransaction
|
|
|
|
Nothing
|
|
|
|
(Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2007 01 28)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
""
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "a", pamount = Mixed [usd 1.35]}
|
|
|
|
, posting {paccount = "b", pamount = Mixed [eur (-1)]}
|
2019-11-27 23:46:29 +03:00
|
|
|
])) @?=
|
2020-08-13 14:15:41 +03:00
|
|
|
Right (Mixed [usd 1.35 @@ (eur 1 `withPrecision` NaturalPrecision)])
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "balanceTransaction balances based on cost if there are unit prices" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertRight $
|
2019-02-14 16:14:52 +03:00
|
|
|
balanceTransaction
|
|
|
|
Nothing
|
|
|
|
(Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2011 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
""
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "a", pamount = Mixed [usd 1 `at` eur 2]}
|
|
|
|
, posting {paccount = "a", pamount = Mixed [usd (-2) `at` eur 1]}
|
|
|
|
])
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "balanceTransaction balances based on cost if there are total prices" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertRight $
|
2019-02-14 16:14:52 +03:00
|
|
|
balanceTransaction
|
|
|
|
Nothing
|
|
|
|
(Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2011 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
""
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "a", pamount = Mixed [usd 1 @@ eur 1]}
|
|
|
|
, posting {paccount = "a", pamount = Mixed [usd (-2) @@ eur 1]}
|
|
|
|
])
|
|
|
|
]
|
2019-11-27 23:46:29 +03:00
|
|
|
, tests "isTransactionBalanced" [
|
2019-11-29 02:29:03 +03:00
|
|
|
test "detect balanced" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertBool "" $
|
2019-02-14 16:14:52 +03:00
|
|
|
isTransactionBalanced Nothing $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2009 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"a"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "b", pamount = Mixed [usd 1.00]}
|
|
|
|
, posting {paccount = "c", pamount = Mixed [usd (-1.00)]}
|
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "detect unbalanced" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertBool "" $
|
2019-02-14 16:14:52 +03:00
|
|
|
not $
|
|
|
|
isTransactionBalanced Nothing $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2009 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"a"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "b", pamount = Mixed [usd 1.00]}
|
|
|
|
, posting {paccount = "c", pamount = Mixed [usd (-1.01)]}
|
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "detect unbalanced, one posting" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertBool "" $
|
2019-02-14 16:14:52 +03:00
|
|
|
not $
|
|
|
|
isTransactionBalanced Nothing $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2009 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"a"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[posting {paccount = "b", pamount = Mixed [usd 1.00]}]
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "one zero posting is considered balanced for now" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertBool "" $
|
2019-02-14 16:14:52 +03:00
|
|
|
isTransactionBalanced Nothing $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2009 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"a"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[posting {paccount = "b", pamount = Mixed [usd 0]}]
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "virtual postings don't need to balance" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertBool "" $
|
2019-02-14 16:14:52 +03:00
|
|
|
isTransactionBalanced Nothing $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2009 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"a"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "b", pamount = Mixed [usd 1.00]}
|
|
|
|
, posting {paccount = "c", pamount = Mixed [usd (-1.00)]}
|
|
|
|
, posting {paccount = "d", pamount = Mixed [usd 100], ptype = VirtualPosting}
|
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "balanced virtual postings need to balance among themselves" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertBool "" $
|
2019-02-14 16:14:52 +03:00
|
|
|
not $
|
|
|
|
isTransactionBalanced Nothing $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2009 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"a"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "b", pamount = Mixed [usd 1.00]}
|
|
|
|
, posting {paccount = "c", pamount = Mixed [usd (-1.00)]}
|
|
|
|
, posting {paccount = "d", pamount = Mixed [usd 100], ptype = BalancedVirtualPosting}
|
|
|
|
]
|
2019-11-29 02:29:03 +03:00
|
|
|
,test "balanced virtual postings need to balance among themselves (2)" $
|
2019-11-27 23:46:29 +03:00
|
|
|
assertBool "" $
|
2019-02-14 16:14:52 +03:00
|
|
|
isTransactionBalanced Nothing $
|
|
|
|
Transaction
|
|
|
|
0
|
|
|
|
""
|
|
|
|
nullsourcepos
|
2020-08-26 11:11:20 +03:00
|
|
|
(fromGregorian 2009 01 01)
|
2019-02-14 16:14:52 +03:00
|
|
|
Nothing
|
|
|
|
Unmarked
|
|
|
|
""
|
|
|
|
"a"
|
|
|
|
""
|
|
|
|
[]
|
|
|
|
[ posting {paccount = "b", pamount = Mixed [usd 1.00]}
|
|
|
|
, posting {paccount = "c", pamount = Mixed [usd (-1.00)]}
|
|
|
|
, posting {paccount = "d", pamount = Mixed [usd 100], ptype = BalancedVirtualPosting}
|
|
|
|
, posting {paccount = "3", pamount = Mixed [usd (-100)], ptype = BalancedVirtualPosting}
|
|
|
|
]
|
|
|
|
]
|
2018-09-04 21:31:31 +03:00
|
|
|
]
|