diff --git a/hledger-lib/Hledger/Data/Amount.hs b/hledger-lib/Hledger/Data/Amount.hs index 8027219b1af..1987c6f080a 100644 --- a/hledger-lib/Hledger/Data/Amount.hs +++ b/hledger-lib/Hledger/Data/Amount.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE NamedFieldPuns #-} {-| A simple 'Amount' is some quantity of money, shares, or anything else. It has a (possibly null) 'CommoditySymbol' and a numeric quantity: @@ -74,6 +75,7 @@ module Hledger.Data.Amount ( noPrice, oneLine, amountstyle, + commodityStylesFromAmounts, styleAmount, styleAmountExceptPrecision, amountUnstyled, @@ -105,6 +107,7 @@ module Hledger.Data.Amount ( mixedAmountStripPrices, -- ** arithmetic mixedAmountCost, + mixedAmountCostPreservingPrecision, divideMixedAmount, multiplyMixedAmount, divideMixedAmountAndPrice, @@ -153,12 +156,14 @@ import Data.Semigroup ((<>)) import qualified Data.Text as T import qualified Data.Text.Lazy.Builder as TB import Data.Word (Word8) -import Safe (headDef, lastDef, lastMay) +import Safe (headDef, lastDef, lastMay, headMay) import Text.Printf (printf) import Hledger.Data.Types import Hledger.Data.Commodity import Hledger.Utils +import Data.List.Extra (groupSort) +import Data.Maybe (mapMaybe) deriving instance Show MarketPrice @@ -202,6 +207,53 @@ oneLine = def{displayOneLine=True, displayPrice=False} -- | Default amount style amountstyle = AmountStyle L False (Precision 0) (Just '.') Nothing +-- | Given an ordered list of amounts (typically in parse order), +-- build a map from their commodity names to standard commodity +-- display styles, inferring styles as per docs, eg: +-- "the format of the first amount, adjusted to the highest precision of all amounts". +-- Can return an error message eg if inconsistent number formats are found. +-- (Though, these amounts may have come from multiple files, so we +-- shouldn't assume they use consistent number formats. +-- Currently we don't enforce that even within a single file, +-- and this function never reports an error.) +commodityStylesFromAmounts :: [Amount] -> Either String (M.Map CommoditySymbol AmountStyle) +commodityStylesFromAmounts amts = + Right $ M.fromList commstyles + where + commamts = groupSort [(acommodity as, as) | as <- amts] + commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts] + +-- TODO: should probably detect and report inconsistencies here. +-- Though, we don't have the info for a good error message, so maybe elsewhere. +-- | Given a list of amount styles (assumed to be from parsed amounts +-- in a single commodity), in parse order, choose a canonical style. +-- This is: +-- the general style of the first amount, +-- with the first digit group style seen, +-- with the maximum precision of all. +-- +canonicalStyleFrom :: [AmountStyle] -> AmountStyle +canonicalStyleFrom [] = amountstyle +canonicalStyleFrom ss@(s:_) = + s{asprecision=prec, asdecimalpoint=Just decmark, asdigitgroups=mgrps} + where + -- precision is maximum of all precisions + prec = maximumStrict $ map asprecision ss + -- identify the digit group mark (& group sizes) + mgrps = headMay $ mapMaybe asdigitgroups ss + -- if a digit group mark was identified above, we can rely on that; + -- make sure the decimal mark is different. If not, default to period. + defdecmark = + case mgrps of + Just (DigitGroups '.' _) -> ',' + _ -> '.' + -- identify the decimal mark: the first one used, or the above default, + -- but never the same character as the digit group mark. + -- urgh.. refactor.. + decmark = case mgrps of + Just _ -> defdecmark + _ -> headDef defdecmark $ mapMaybe asdecimalpoint ss + ------------------------------------------------------------------------------- -- Amount @@ -269,6 +321,13 @@ amountCost a@Amount{aquantity=q, aprice=mp} = Just (UnitPrice p@Amount{aquantity=pq}) -> p{aquantity=pq * q} Just (TotalPrice p@Amount{aquantity=pq}) -> p{aquantity=pq * signum q} +-- | Like amountCost, but then re-apply the display precision of the +-- original commodity. +amountCostPreservingPrecision :: Amount -> Amount +amountCostPreservingPrecision a@Amount{astyle=AmountStyle{asprecision}} = + a'{astyle=astyle'{asprecision=asprecision}} + where a'@Amount{astyle=astyle'} = amountCost a + -- | Replace an amount's TotalPrice, if it has one, with an equivalent UnitPrice. -- Has no effect on amounts without one. -- Also increases the unit price's display precision to show one extra decimal place, @@ -618,6 +677,11 @@ mapMixedAmount f (Mixed as) = Mixed $ map f as mixedAmountCost :: MixedAmount -> MixedAmount mixedAmountCost = mapMixedAmount amountCost +-- | Like mixedAmountCost, but then re-apply the display precision of the +-- original commodity. +mixedAmountCostPreservingPrecision :: MixedAmount -> MixedAmount +mixedAmountCostPreservingPrecision = mapMixedAmount amountCostPreservingPrecision + -- | Divide a mixed amount's quantities by a constant. divideMixedAmount :: Quantity -> MixedAmount -> MixedAmount divideMixedAmount n = mapMixedAmount (divideAmount n) @@ -953,6 +1017,41 @@ tests_Amount = tests "Amount" [ ,usd (-10) @@ eur 7 ]) + ,tests "commodityStylesFromAmounts" $ [ + + -- Journal similar to the one on #1091: + -- 2019/09/24 + -- (a) 1,000.00 + -- + -- 2019/09/26 + -- (a) 1000,000 + -- + test "1091a" $ do + commodityStylesFromAmounts [ + nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 3) (Just ',') Nothing} + ,nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 2) (Just '.') (Just (DigitGroups ',' [3]))} + ] + @?= + -- The commodity style should have period as decimal mark + -- and comma as digit group mark. + Right (M.fromList [ + ("", AmountStyle L False (Precision 3) (Just '.') (Just (DigitGroups ',' [3]))) + ]) + -- same journal, entries in reverse order + ,test "1091b" $ do + commodityStylesFromAmounts [ + nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 2) (Just '.') (Just (DigitGroups ',' [3]))} + ,nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 3) (Just ',') Nothing} + ] + @?= + -- The commodity style should have period as decimal mark + -- and comma as digit group mark. + Right (M.fromList [ + ("", AmountStyle L False (Precision 3) (Just '.') (Just (DigitGroups ',' [3]))) + ]) + + ] + ] ] diff --git a/hledger-lib/Hledger/Data/Journal.hs b/hledger-lib/Hledger/Data/Journal.hs index 56d9007b04e..ca1ff548df7 100644 --- a/hledger-lib/Hledger/Data/Journal.hs +++ b/hledger-lib/Hledger/Data/Journal.hs @@ -23,8 +23,9 @@ module Hledger.Data.Journal ( addTransaction, journalBalanceTransactions, journalInferMarketPricesFromTransactions, + journalInferCommodityStyles, journalApplyCommodityStyles, - commodityStylesFromAmounts, + journalInferAndApplyCommodityStyles, journalCommodityStyles, journalToCost, journalReverse, @@ -78,7 +79,6 @@ module Hledger.Data.Journal ( journalEquityAccountQuery, journalCashAccountQuery, -- * Misc - canonicalStyleFrom, nulljournal, journalCheckBalanceAssertions, journalNumberAndTieTransactions, @@ -87,7 +87,7 @@ module Hledger.Data.Journal ( journalApplyAliases, -- * Tests samplejournal, - tests_Journal, + tests_Journal ) where @@ -101,7 +101,7 @@ import Data.Function ((&)) import qualified Data.HashTable.Class as H (toList) import qualified Data.HashTable.ST.Cuckoo as H import Data.List (find, sortOn) -import Data.List.Extra (groupSort, nubSort) +import Data.List.Extra (nubSort) import qualified Data.Map as M import Data.Maybe (catMaybes, fromJust, fromMaybe, isJust, mapMaybe) #if !(MIN_VERSION_base(4,11,0)) @@ -653,7 +653,8 @@ journalModifyTransactions d j = -- | Check any balance assertions in the journal and return an error message -- if any of them fail (or if the transaction balancing they require fails). journalCheckBalanceAssertions :: Journal -> Maybe String -journalCheckBalanceAssertions = either Just (const Nothing) . journalBalanceTransactions True +journalCheckBalanceAssertions = either Just (const Nothing) . journalBalanceTransactions False True + -- TODO: not using global display styles here, do we need to for BC ? -- "Transaction balancing", including: inferring missing amounts, -- applying balance assignments, checking transaction balancedness, @@ -748,18 +749,20 @@ updateTransactionB t = withRunningBalance $ \BalancingState{bsTransactions} -> -- and (optional) all balance assertions pass. Or return an error message -- (just the first error encountered). -- --- Assumes journalInferCommodityStyles has been called, since those --- affect transaction balancing. +-- Assumes the journal amounts' display styles still have the original number +-- of decimal places that was parsed (ie, display styles have not yet been normalised), +-- since this affects transaction balancing. -- -- This does multiple things at once because amount inferring, balance -- assignments, balance assertions and posting dates are interdependent. -journalBalanceTransactions :: Bool -> Journal -> Either String Journal -journalBalanceTransactions assrt j' = +-- +journalBalanceTransactions :: Bool -> Bool -> Journal -> Either String Journal +journalBalanceTransactions usedisplaystyles assrt j' = let -- ensure transactions are numbered, so we can store them by number j@Journal{jtxns=ts} = journalNumberTransactions j' -- display precisions used in balanced checking - styles = Just $ journalCommodityStyles j + styles = if usedisplaystyles then Just $ journalCommodityStyles j else Nothing -- balance assignments will not be allowed on these txnmodifieraccts = S.fromList $ map paccount $ concatMap tmpostingrules $ jtxnmodifiers j in @@ -991,25 +994,66 @@ checkBalanceAssignmentUnassignableAccountB p = do -- +-- | Get an ordered list of amounts in this journal which can +-- influence canonical amount display styles. Those are, in +-- the following order: +-- +-- * amounts in market price (P) directives (in parse order) +-- * posting amounts in transactions (in parse order) +-- * the amount in the final default commodity (D) directive +-- +-- Transaction price amounts (posting amounts' aprice field) are not included. +-- +journalStyleInfluencingAmounts :: Journal -> [Amount] +journalStyleInfluencingAmounts j = + dbg7 "journalStyleInfluencingAmounts" $ + catMaybes $ concat [ + [mdefaultcommodityamt] + ,map (Just . pdamount) $ jpricedirectives j + ,map Just $ concatMap amounts $ map pamount $ journalPostings j + ] + where + -- D's amount style isn't actually stored as an amount, make it into one + mdefaultcommodityamt = + case jparsedefaultcommodity j of + Just (symbol,style) -> Just nullamt{acommodity=symbol,astyle=style} + Nothing -> Nothing + +-- | Infer commodity display styles for each commodity (see commodityStylesFromAmounts) +-- based on the amounts in this journal (see journalStyleInfluencingAmounts), +-- and save those inferred styles in the journal. +-- Can return an error message eg if inconsistent number formats are found. +journalInferCommodityStyles :: Journal -> Either String Journal +journalInferCommodityStyles j = + case commodityStylesFromAmounts $ journalStyleInfluencingAmounts j of + Left e -> Left e + Right cs -> Right j{jinferredcommodities = dbg7 "journalInferCommodityStyles" cs} + +-- | Apply the given commodity display styles to the posting amounts in this journal. +journalApplyCommodityStyles :: M.Map CommoditySymbol AmountStyle -> Journal -> Journal +journalApplyCommodityStyles styles j@Journal{jtxns=ts, jpricedirectives=pds} = + j {jtxns=map fixtransaction ts + ,jpricedirectives=map fixpricedirective pds + } + where + fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps} + fixposting p = p{pamount=styleMixedAmount styles $ pamount p + ,pbalanceassertion=fixbalanceassertion <$> pbalanceassertion p} + -- balance assertion/assignment amounts, and price amounts, are always displayed + -- (eg by print) at full precision + fixbalanceassertion ba = ba{baamount=styleAmountExceptPrecision styles $ baamount ba} + fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmountExceptPrecision styles a} + -- | Choose and apply a consistent display style to the posting -- amounts in each commodity (see journalCommodityStyles). -- Can return an error message eg if inconsistent number formats are found. -journalApplyCommodityStyles :: Journal -> Either String Journal -journalApplyCommodityStyles j@Journal{jtxns=ts, jpricedirectives=pds} = +journalInferAndApplyCommodityStyles :: Journal -> Either String Journal +journalInferAndApplyCommodityStyles j = case journalInferCommodityStyles j of Left e -> Left e - Right j' -> Right j'' + Right j' -> Right $ journalApplyCommodityStyles allstyles j' where - styles = journalCommodityStyles j' - j'' = j'{jtxns=map fixtransaction ts - ,jpricedirectives=map fixpricedirective pds - } - fixtransaction t@Transaction{tpostings=ps} = t{tpostings=map fixposting ps} - fixposting p = p{pamount=styleMixedAmount styles $ pamount p - ,pbalanceassertion=fixbalanceassertion <$> pbalanceassertion p} - -- balance assertion amounts are always displayed (by print) at full precision, per docs - fixbalanceassertion ba = ba{baamount=styleAmountExceptPrecision styles $ baamount ba} - fixpricedirective pd@PriceDirective{pdamount=a} = pd{pdamount=styleAmountExceptPrecision styles a} + allstyles = journalCommodityStyles j' -- | Get the canonical amount styles for this journal, whether (in order of precedence): -- set globally in InputOpts, @@ -1028,66 +1072,6 @@ journalCommodityStyles j = defaultcommoditystyle = M.fromList $ catMaybes [jparsedefaultcommodity j] inferredstyles = jinferredcommodities j --- | Collect and save inferred amount styles for each commodity based on --- the posting amounts in that commodity (excluding price amounts), ie: --- "the format of the first amount, adjusted to the highest precision of all amounts". --- Can return an error message eg if inconsistent number formats are found. -journalInferCommodityStyles :: Journal -> Either String Journal -journalInferCommodityStyles j = - case - commodityStylesFromAmounts $ journalStyleInfluencingAmounts j - of - Left e -> Left e - Right cs -> Right j{jinferredcommodities = dbg7 "journalInferCommodityStyles" cs} - --- | Given a list of amounts, in parse order (roughly speaking; see journalStyleInfluencingAmounts), --- build a map from their commodity names to standard commodity --- display formats. Can return an error message eg if inconsistent --- number formats are found. --- --- Though, these amounts may have come from multiple files, so we --- shouldn't assume they use consistent number formats. --- Currently we don't enforce that even within a single file, --- and this function never reports an error. --- -commodityStylesFromAmounts :: [Amount] -> Either String (M.Map CommoditySymbol AmountStyle) -commodityStylesFromAmounts amts = - Right $ M.fromList commstyles - where - commamts = groupSort [(acommodity as, as) | as <- amts] - commstyles = [(c, canonicalStyleFrom $ map astyle as) | (c,as) <- commamts] - --- TODO: should probably detect and report inconsistencies here. --- Though, we don't have the info for a good error message, so maybe elsewhere. --- | Given a list of amount styles (assumed to be from parsed amounts --- in a single commodity), in parse order, choose a canonical style. --- This is: --- the general style of the first amount, --- with the first digit group style seen, --- with the maximum precision of all. --- -canonicalStyleFrom :: [AmountStyle] -> AmountStyle -canonicalStyleFrom [] = amountstyle -canonicalStyleFrom ss@(s:_) = - s{asprecision=prec, asdecimalpoint=Just decmark, asdigitgroups=mgrps} - where - -- precision is maximum of all precisions - prec = maximumStrict $ map asprecision ss - -- identify the digit group mark (& group sizes) - mgrps = headMay $ mapMaybe asdigitgroups ss - -- if a digit group mark was identified above, we can rely on that; - -- make sure the decimal mark is different. If not, default to period. - defdecmark = - case mgrps of - Just (DigitGroups '.' _) -> ',' - _ -> '.' - -- identify the decimal mark: the first one used, or the above default, - -- but never the same character as the digit group mark. - -- urgh.. refactor.. - decmark = case mgrps of - Just _ -> defdecmark - _ -> headDef defdecmark $ mapMaybe asdecimalpoint ss - -- -- | Apply this journal's historical price records to unpriced amounts where possible. -- journalApplyPriceDirectives :: Journal -> Journal -- journalApplyPriceDirectives j@Journal{jtxns=ts} = j{jtxns=map fixtransaction ts} @@ -1160,31 +1144,6 @@ journalToCost j@Journal{jtxns=ts} = j{jtxns=map (transactionToCost styles) ts} -- Just (UnitPrice ma) -> c:(concatMap amountCommodities $ amounts ma) -- Just (TotalPrice ma) -> c:(concatMap amountCommodities $ amounts ma) --- | Get an ordered list of amounts in this journal which can --- influence canonical amount display styles. Those amounts are, in --- the following order: --- --- * amounts in market price (P) directives (in parse order) --- * posting amounts in transactions (in parse order) --- * the amount in the final default commodity (D) directive --- --- Transaction price amounts (posting amounts' aprice field) are not included. --- -journalStyleInfluencingAmounts :: Journal -> [Amount] -journalStyleInfluencingAmounts j = - dbg7 "journalStyleInfluencingAmounts" $ - catMaybes $ concat [ - [mdefaultcommodityamt] - ,map (Just . pdamount) $ jpricedirectives j - ,map Just $ concatMap amounts $ map pamount $ journalPostings j - ] - where - -- D's amount style isn't actually stored as an amount, make it into one - mdefaultcommodityamt = - case jparsedefaultcommodity j of - Just (symbol,style) -> Just nullamt{acommodity=symbol,astyle=style} - Nothing -> Nothing - -- overcomplicated/unused amount traversal stuff -- -- | Get an ordered list of 'AmountStyle's from the amounts in this @@ -1341,7 +1300,7 @@ journalApplyAliases aliases j = -- liabilities:debts $1 -- assets:bank:checking -- -Right samplejournal = journalBalanceTransactions False $ +Right samplejournal = journalBalanceTransactions False False $ nulljournal {jtxns = [ txnTieKnot $ Transaction { @@ -1484,7 +1443,7 @@ tests_Journal = tests "Journal" [ ,tests "journalBalanceTransactions" [ test "balance-assignment" $ do - let ej = journalBalanceTransactions True $ + let ej = journalBalanceTransactions False True $ --2019/01/01 -- (a) = 1 nulljournal{ jtxns = [ @@ -1495,7 +1454,7 @@ tests_Journal = tests "Journal" [ (jtxns j & head & tpostings & head & pamount) @?= Mixed [num 1] ,test "same-day-1" $ do - assertRight $ journalBalanceTransactions True $ + assertRight $ journalBalanceTransactions False True $ --2019/01/01 -- (a) = 1 --2019/01/01 @@ -1506,7 +1465,7 @@ tests_Journal = tests "Journal" [ ]} ,test "same-day-2" $ do - assertRight $ journalBalanceTransactions True $ + assertRight $ journalBalanceTransactions False True $ --2019/01/01 -- (a) 2 = 2 --2019/01/01 @@ -1524,7 +1483,7 @@ tests_Journal = tests "Journal" [ ]} ,test "out-of-order" $ do - assertRight $ journalBalanceTransactions True $ + assertRight $ journalBalanceTransactions False True $ --2019/1/2 -- (a) 1 = 2 --2019/1/1 @@ -1536,39 +1495,4 @@ tests_Journal = tests "Journal" [ ] - ,tests "commodityStylesFromAmounts" $ [ - - -- Journal similar to the one on #1091: - -- 2019/09/24 - -- (a) 1,000.00 - -- - -- 2019/09/26 - -- (a) 1000,000 - -- - test "1091a" $ do - commodityStylesFromAmounts [ - nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 3) (Just ',') Nothing} - ,nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 2) (Just '.') (Just (DigitGroups ',' [3]))} - ] - @?= - -- The commodity style should have period as decimal mark - -- and comma as digit group mark. - Right (M.fromList [ - ("", AmountStyle L False (Precision 3) (Just '.') (Just (DigitGroups ',' [3]))) - ]) - -- same journal, entries in reverse order - ,test "1091b" $ do - commodityStylesFromAmounts [ - nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 2) (Just '.') (Just (DigitGroups ',' [3]))} - ,nullamt{aquantity=1000, astyle=AmountStyle L False (Precision 3) (Just ',') Nothing} - ] - @?= - -- The commodity style should have period as decimal mark - -- and comma as digit group mark. - Right (M.fromList [ - ("", AmountStyle L False (Precision 3) (Just '.') (Just (DigitGroups ',' [3]))) - ]) - - ] - ] diff --git a/hledger-lib/Hledger/Data/Transaction.hs b/hledger-lib/Hledger/Data/Transaction.hs index 222f4e6c748..f13b998b4cf 100644 --- a/hledger-lib/Hledger/Data/Transaction.hs +++ b/hledger-lib/Hledger/Data/Transaction.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE NamedFieldPuns #-} {-| A 'Transaction' represents a movement of some commodity(ies) between two @@ -82,6 +83,8 @@ import Hledger.Data.Amount import Hledger.Data.Valuation import Text.Tabular import Text.Tabular.AsciiWide +import Control.Applicative ((<|>)) +import Text.Printf (printf) sourceFilePath :: GenericSourcePos -> FilePath sourceFilePath = \case @@ -358,15 +361,62 @@ transactionsPostings = concatMap tpostings -- (Best effort; could be confused by postings with multicommodity amounts.) -- -- 3. Does the amounts' sum appear non-zero when displayed ? --- (using the given display styles if provided) +-- We have two ways of checking this: +-- +-- Old way, supported for compatibility: if global display styles are provided, +-- in each commodity, render the sum using the precision from the +-- global display styles, and see if it looks like exactly zero. +-- +-- New way, preferred: in each commodity, render the sum using the max precision +-- that was used in this transaction's journal entry, and see if it looks +-- like exactly zero. -- transactionCheckBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> [String] -transactionCheckBalanced mstyles t = errs +transactionCheckBalanced mglobalstyles t = errs where (rps, bvps) = (realPostings t, balancedVirtualPostings t) + -- For testing each commodity's zero sum, we'll render it with the number + -- of decimal places specified by its display style, from either the + -- provided global display styles, or local styles inferred from just + -- this transaction. + + -- Which local styles (& thence, precisions) exactly should we + -- infer from this transaction ? Since amounts are going to be + -- converted to cost, we may end up with the commodity of + -- transaction prices, so we'll need to pick a style for those too. + -- + -- Option 1: also infer styles from the price amounts, which normally isn't done. + -- canonicalise = maybe id canonicaliseMixedAmount (mglobalstyles <|> mtxnstyles) + -- where + -- mtxnstyles = dbg0 "transactionCheckBalanced mtxnstyles" $ + -- either (const Nothing) Just $ -- shouldn't get any error here, but if so just.. carry on, comparing uncanonicalised amounts XXX + -- commodityStylesFromAmounts $ concatMap postingAllAmounts $ rps ++ bvps + -- where + -- -- | Get all the individual Amounts from a posting's MixedAmount, + -- -- and all their price Amounts as well. + -- postingAllAmounts :: Posting -> [Amount] + -- postingAllAmounts p = catMaybes $ concat [[Just a, priceamount a] | a <- amounts $ pamount p] + -- where + -- priceamount Amount{aprice} = + -- case aprice of + -- Just (UnitPrice a) -> Just a + -- Just (TotalPrice a) -> Just a + -- Nothing -> Nothing + -- + -- Option 2, for amounts converted to cost, where the new commodity appears only in prices, + -- use the precision of their original commodity (by using mixedAmountCostPreservingPrecision). + (tocost,costlabel) = case mglobalstyles of + Just _ -> (mixedAmountCost,"") -- --balancing=styled + Nothing -> (mixedAmountCostPreservingPrecision,"withorigprecision") -- --balancing=exact + canonicalise = maybe id canonicaliseMixedAmount (mglobalstyles <|> mtxnstyles) + where + mtxnstyles = dbg9 "transactionCheckBalanced mtxnstyles" $ + either (const Nothing) Just $ -- shouldn't get an error here, but if so just don't canonicalise + commodityStylesFromAmounts $ concatMap (amounts.pamount) $ rps ++ bvps + + -- check for mixed signs, detecting nonzeros at display precision - canonicalise = maybe id canonicaliseMixedAmount mstyles signsOk ps = case filter (not.mixedAmountLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of nonzeros | length nonzeros >= 2 @@ -375,22 +425,28 @@ transactionCheckBalanced mstyles t = errs (rsignsok, bvsignsok) = (signsOk rps, signsOk bvps) -- check for zero sum, at display precision - (rsum, bvsum) = (sumPostings rps, sumPostings bvps) - (rsumcost, bvsumcost) = (mixedAmountCost rsum, mixedAmountCost bvsum) - (rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost) - (rsumok, bvsumok) = (mixedAmountLooksZero rsumdisplay, mixedAmountLooksZero bvsumdisplay) + (rsum, bvsum) = (dbg9 "transactionCheckBalanced rsum" $ sumPostings rps, sumPostings bvps) + (rsumcost, bvsumcost) = (dbg9 ("transactionCheckBalanced rsumcost"++costlabel) $ tocost rsum, tocost bvsum) + (rsumstyled, bvsumstyled) = (dbg9 "transactionCheckBalanced rsumstyled" $ canonicalise rsumcost, canonicalise bvsumcost) + (rsumok, bvsumok) = (mixedAmountLooksZero rsumstyled, mixedAmountLooksZero bvsumstyled) - -- generate error messages, showing amounts with their original precision + -- generate error messages errs = filter (not.null) [rmsg, bvmsg] where rmsg | not rsignsok = "real postings all have the same sign" - | not rsumok = "real postings' sum should be 0 but is: " ++ showMixedAmount rsumcost + | not rsumok = printf "real postings' sum should be %s but is %s (rounded from %s)" rsumexpected rsumactual rsumfull | 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 + | not bvsumok = printf "balanced virtual postings' sum should be %s but is %s (rounded from %s)" bvsumexpected bvsumactual bvsumfull | otherwise = "" + rsumexpected = showMixedAmountWithZeroCommodity $ mapMixedAmount (\a -> a{aquantity=0}) rsumstyled + rsumactual = showMixedAmount rsumstyled + rsumfull = showMixedAmount (mixedAmountSetFullPrecision rsumcost) + bvsumexpected = showMixedAmountWithZeroCommodity $ mapMixedAmount (\a -> a{aquantity=0}) rsumstyled + bvsumactual = showMixedAmount bvsumstyled + bvsumfull = showMixedAmount (mixedAmountSetFullPrecision bvsumcost) -- | Legacy form of transactionCheckBalanced. isTransactionBalanced :: Maybe (M.Map CommoditySymbol AmountStyle) -> Transaction -> Bool @@ -454,7 +510,7 @@ inferBalancingAmount :: M.Map CommoditySymbol AmountStyle -- ^ commodity display styles -> Transaction -> Either String (Transaction, [(AccountName, MixedAmount)]) -inferBalancingAmount styles t@Transaction{tpostings=ps} +inferBalancingAmount _globalstyles t@Transaction{tpostings=ps} | length amountlessrealps > 1 = Left $ transactionBalanceError t ["can't have more than one real posting with no amount" @@ -486,9 +542,7 @@ inferBalancingAmount styles t@Transaction{tpostings=ps} Just a -> (p{pamount=a', poriginal=Just $ originalPosting p}, Just a') where -- Inferred amounts are converted to cost. - -- Also ensure the new amount has the standard style for its commodity - -- (since the main amount styling pass happened before this balancing pass); - a' = styleMixedAmount styles $ normaliseMixedAmount $ mixedAmountCost (-a) + a' = normaliseMixedAmount $ mixedAmountCost (-a) -- | Infer prices for this transaction's posting amounts, if needed to make -- the postings balance, and if possible. This is done once for the real @@ -554,7 +608,11 @@ priceInferrerFor t pt = inferprice where fromcommodity = head $ filter (`elem` sumcommodities) pcommodities -- these heads are ugly but should be safe conversionprice + -- Use a total price when we can, as it's more exact. | fromcount==1 = TotalPrice $ abs toamount `withPrecision` NaturalPrecision + -- When there are multiple posting amounts to be converted, + -- it's easiest to have them all use the same unit price. + -- Floating-point error and rounding becomes an issue though. | otherwise = UnitPrice $ abs unitprice `withPrecision` unitprecision where fromcount = length $ filter ((==fromcommodity).acommodity) pamounts @@ -564,9 +622,20 @@ priceInferrerFor t pt = inferprice toamount = head $ filter ((==tocommodity).acommodity) sumamounts toprecision = asprecision $ astyle toamount unitprice = (aquantity fromamount) `divideAmount` toamount - -- Sum two display precisions, capping the result at the maximum bound + -- The number of decimal places that will be shown for an + -- inferred unit price. Often, the underlying Decimal will + -- have the maximum number of decimal places (255). We + -- don't want to show that many to the user; we'd prefer + -- to show the minimum number of digits that makes the + -- print-ed transaction appear balanced if you did the + -- arithmetic by hand, and also makes the print-ed transaction + -- parseable by hledger. How many decimal places is that ? I'm not sure. + -- Currently we heuristically use 2 * the total number of decimal places + -- from the amounts to be converted to and from (and at least 2, at most 255), + -- which experimentally seems to be sufficient so far. unitprecision = case (fromprecision, toprecision) of - (Precision a, Precision b) -> Precision $ if maxBound - a < b then maxBound else max 2 (a + b) + (Precision a, Precision b) -> Precision $ if maxBound - a < b then maxBound else + max 2 (2 * (a+b)) _ -> NaturalPrecision inferprice p = p diff --git a/hledger-lib/Hledger/Read/Common.hs b/hledger-lib/Hledger/Read/Common.hs index cfe34a7015b..d579c89375c 100644 --- a/hledger-lib/Hledger/Read/Common.hs +++ b/hledger-lib/Hledger/Read/Common.hs @@ -30,6 +30,7 @@ Some of these might belong in Hledger.Read.JournalReader or Hledger.Read. module Hledger.Read.Common ( Reader (..), InputOpts (..), + BalancingType (..), definputopts, rawOptsToInputOpts, @@ -151,7 +152,7 @@ import Text.Megaparsec.Custom import Hledger.Data import Hledger.Utils -import Safe (headMay) +import Safe (headMay, lastMay) import Text.Printf (printf) --- ** doctest setup @@ -204,6 +205,7 @@ data InputOpts = InputOpts { ,auto_ :: Bool -- ^ generate automatic postings when journal is parsed ,commoditystyles_ :: Maybe (M.Map CommoditySymbol AmountStyle) -- ^ optional commodity display styles affecting all files ,strict_ :: Bool -- ^ do extra error checking (eg, all posted accounts are declared) + ,balancingtype_ :: BalancingType -- ^ which transaction balancing strategy to use } deriving (Show) instance Default InputOpts where def = definputopts @@ -221,6 +223,7 @@ definputopts = InputOpts , auto_ = False , commoditystyles_ = Nothing , strict_ = False + , balancingtype_ = StyledBalancing } rawOptsToInputOpts :: RawOpts -> InputOpts @@ -237,8 +240,28 @@ rawOptsToInputOpts rawopts = InputOpts{ ,auto_ = boolopt "auto" rawopts ,commoditystyles_ = Nothing ,strict_ = boolopt "strict" rawopts + ,balancingtype_ = fromMaybe ExactBalancing $ balancingTypeFromRawOpts rawopts } +-- | How should transactions be checked for balancedness ? +-- Ie, to how many decimal places should we check each commodity's zero balance ? +data BalancingType = + ExactBalancing -- ^ render the sum with the max precision used in the transaction + | StyledBalancing -- ^ render the sum with the precision from the journal's display styles, eg from commodity directives + deriving (Eq,Show) + +-- | Parse the transaction balancing strategy, specified by --balancing. +balancingTypeFromRawOpts :: RawOpts -> Maybe BalancingType +balancingTypeFromRawOpts rawopts = lastMay $ collectopts balancingfromrawopt rawopts + where + balancingfromrawopt (name,value) + | name == "balancing" = Just $ balancing value + | otherwise = Nothing + balancing value + | not (null value) && value `isPrefixOf` "exact" = ExactBalancing + | not (null value) && value `isPrefixOf` "styled" = StyledBalancing + | otherwise = usageError $ "could not parse \""++value++"\" as balancing type, should be: exact|styled" + --- ** parsing utilities -- | Run a text parser in the identity monad. See also: parseWithState. @@ -325,7 +348,7 @@ parseAndFinaliseJournal' parser iopts f txt = do -- - infer transaction-implied market prices from transaction prices -- journalFinalise :: InputOpts -> FilePath -> Text -> ParsedJournal -> ExceptT String IO Journal -journalFinalise InputOpts{auto_,ignore_assertions_,commoditystyles_,strict_} f txt pj = do +journalFinalise InputOpts{auto_,ignore_assertions_,commoditystyles_,strict_,balancingtype_} f txt pj = do t <- liftIO getClockTime d <- liftIO getCurrentDay let pj' = @@ -342,35 +365,43 @@ journalFinalise InputOpts{auto_,ignore_assertions_,commoditystyles_,strict_} f t -- and using declared commodities case if strict_ then journalCheckCommoditiesDeclared pj' else Right () of Left e -> throwError e - Right () -> - - -- Infer and apply canonical styles for each commodity (or throw an error). - -- This affects transaction balancing/assertions/assignments, so needs to be done early. - case journalApplyCommodityStyles pj' of - Left e -> throwError e - Right pj'' -> either throwError return $ - pj'' - & (if not auto_ || null (jtxnmodifiers pj'') - then - -- Auto postings are not active. - -- Balance all transactions and maybe check balance assertions. - journalBalanceTransactions (not ignore_assertions_) - else \j -> do -- Either monad - -- Auto postings are active. - -- Balance all transactions without checking balance assertions, - j' <- journalBalanceTransactions False j - -- then add the auto postings - -- (Note adding auto postings after balancing means #893b fails; - -- adding them before balancing probably means #893a, #928, #938 fail.) - case journalModifyTransactions d j' of - Left e -> throwError e - Right j'' -> do - -- then apply commodity styles once more, to style the auto posting amounts. (XXX inefficient ?) - j''' <- journalApplyCommodityStyles j'' - -- then check balance assertions. - journalBalanceTransactions (not ignore_assertions_) j''' - ) - & fmap journalInferMarketPricesFromTransactions -- infer market prices from commodity-exchanging transactions + Right () -> do + -- Infer and save canonical commodity display styles here, before transaction balancing. + case journalInferCommodityStyles pj' of + Left e -> throwError e + Right pj'' -> do + let + allstyles = journalCommodityStyles pj'' + useglobalstyles = balancingtype_ == StyledBalancing + -- Balance transactions, and possibly add auto postings and check balance assertions. + case (pj'' + & (if not auto_ || null (jtxnmodifiers pj'') + then + -- Auto postings are not active. + -- Balance all transactions and maybe check balance assertions. + journalBalanceTransactions useglobalstyles (not ignore_assertions_) + else \j -> do -- Either monad + -- Auto postings are active. + -- Balance all transactions without checking balance assertions, + j' <- journalBalanceTransactions useglobalstyles False j + -- then add the auto postings + -- (Note adding auto postings after balancing means #893b fails; + -- adding them before balancing probably means #893a, #928, #938 fail.) + case journalModifyTransactions d j' of + Left e -> throwError e + Right j'' -> do + -- then check balance assertions. + journalBalanceTransactions useglobalstyles (not ignore_assertions_) j'' + )) of + Left e -> throwError e + Right pj''' -> do + let + pj'''' = pj''' + -- Apply the (pre-transaction-balancing) commodity styles to all amounts. + & journalApplyCommodityStyles allstyles + -- Infer market prices from commodity-exchanging transactions. + & journalInferMarketPricesFromTransactions + return pj'''' -- | Check that all the journal's transactions have payees declared with -- payee directives, returning an error message otherwise. diff --git a/hledger-lib/Hledger/Reports/BalanceReport.hs b/hledger-lib/Hledger/Reports/BalanceReport.hs index 78f67881ab1..2a0a1f336f7 100644 --- a/hledger-lib/Hledger/Reports/BalanceReport.hs +++ b/hledger-lib/Hledger/Reports/BalanceReport.hs @@ -76,7 +76,7 @@ balanceReport rspec j = (rows, total) -- tests Right samplejournal2 = - journalBalanceTransactions False + journalBalanceTransactions False False nulljournal{ jtxns = [ txnTieKnot Transaction{ diff --git a/hledger-lib/Hledger/Reports/BudgetReport.hs b/hledger-lib/Hledger/Reports/BudgetReport.hs index 1081ab4ee2c..3827c991330 100644 --- a/hledger-lib/Hledger/Reports/BudgetReport.hs +++ b/hledger-lib/Hledger/Reports/BudgetReport.hs @@ -110,7 +110,8 @@ budgetReport rspec assrt reportspan j = dbg4 "sortedbudgetreport" budgetreport -- for BudgetReport. journalAddBudgetGoalTransactions :: Bool -> ReportOpts -> DateSpan -> Journal -> Journal journalAddBudgetGoalTransactions assrt _ropts reportspan j = - either error' id $ journalBalanceTransactions assrt j{ jtxns = budgetts } -- PARTIAL: + -- TODO: always using exact balancing, do we need to support styled balancing for BC ? + either error' id $ journalBalanceTransactions False assrt j{ jtxns = budgetts } -- PARTIAL: where budgetspan = dbg3 "budget span" $ reportspan budgetts = diff --git a/hledger/Hledger/Cli/CliOptions.hs b/hledger/Hledger/Cli/CliOptions.hs index e4dabca848e..c932b603607 100644 --- a/hledger/Hledger/Cli/CliOptions.hs +++ b/hledger/Hledger/Cli/CliOptions.hs @@ -127,6 +127,7 @@ inputflags = [ ,flagReq ["alias"] (\s opts -> Right $ setopt "alias" s opts) "OLD=NEW" "rename accounts named OLD to NEW" ,flagNone ["anon"] (setboolopt "anon") "anonymize accounts and payees" ,flagReq ["pivot"] (\s opts -> Right $ setopt "pivot" s opts) "TAGNAME" "use some other field/tag for account names" + ,flagReq ["balancing"] (\s opts -> Right $ setopt "balancing" s opts) "exact|styled" "balance transactions using transaction's exact precisions (default, recommended) or commodity display styles' precisions (like hledger <=1.20)" ,flagNone ["ignore-assertions","I"] (setboolopt "ignore-assertions") "ignore any balance assertions" ,flagNone ["strict","s"] (setboolopt "strict") "do extra error checking (check that all posted accounts are declared)" ] diff --git a/hledger/Hledger/Cli/Utils.hs b/hledger/Hledger/Cli/Utils.hs index b8bbb2e60b1..3ad8f5db3bb 100644 --- a/hledger/Hledger/Cli/Utils.hs +++ b/hledger/Hledger/Cli/Utils.hs @@ -121,7 +121,7 @@ journalAddForecast :: CliOpts -> Journal -> Journal journalAddForecast CliOpts{inputopts_=iopts, reportspec_=rspec} j = case forecast_ ropts of Nothing -> j - Just _ -> either (error') id . journalApplyCommodityStyles $ -- PARTIAL: + Just _ -> either (error') id . journalInferAndApplyCommodityStyles $ -- PARTIAL: journalBalanceTransactions' iopts j{ jtxns = concat [jtxns j, forecasttxns'] } where today = rsToday rspec @@ -151,9 +151,11 @@ journalAddForecast CliOpts{inputopts_=iopts, reportspec_=rspec} j = forecasttxns journalBalanceTransactions' iopts j = - let assrt = not . ignore_assertions_ $ iopts + let + assrt = not . ignore_assertions_ $ iopts + styledbalancing = balancingtype_ iopts == StyledBalancing in - either error' id $ journalBalanceTransactions assrt j -- PARTIAL: + either error' id $ journalBalanceTransactions styledbalancing assrt j -- PARTIAL: -- | Write some output to stdout or to a file selected by --output-file. -- If the file exists it will be overwritten. diff --git a/hledger/hledger.m4.md b/hledger/hledger.m4.md index 0c89c413d71..7e95123198a 100644 --- a/hledger/hledger.m4.md +++ b/hledger/hledger.m4.md @@ -1465,6 +1465,10 @@ Here's a simple journal file containing one transaction: income:salary $-1 ``` +Note a transaction's postings have an important property: their +amounts are required to add up to zero, showing that money has not +been created or destroyed, only moved. +This is discussed in more detail below. ## Dates @@ -1725,6 +1729,8 @@ without using a balancing equity account: (assets:savings) $2000 ``` +### Balanced virtual postings + A posting with a bracketed account name is called a *balanced virtual posting*. The balanced virtual postings in a transaction must add up to zero (separately from other postings). Eg: @@ -1977,6 +1983,54 @@ hledger will parse these, for compatibility with Ledger journals, but currently A [transaction price](#transaction-prices), lot price and/or lot date may appear in any order, after the posting amount and before the balance assertion if any. +## Balanced transactions + +As mentioned above, the amounts of a transaction's posting are required to add up to zero. +This shows that "money was conserved" during the transaction, ie no +funds were created or destroyed. +We call this a balanced transaction. + +If you want the full detail of how exactly this works in hledger, read on... + +Transactions can contain [ordinary (real) postings](#postings), +[balanced virtual postings](#balanced-virtual-postings), and/or +[unbalanced virtual postings](#virtual-postings). +hledger checks that the real postings sum to zero, +the balanced virtual postings (separately) sum to zero, +and does not check unbalanced virtual postings. + +Because computers generally don't represent decimal (real) numbers +exactly, "sum to zero" is a little more complicated. +hledger aims to always agree with a human who is looking at the +[`print`](#print)-ed transaction and doing the arithmetic by hand. +Specifically, it does this: + +- for each commodity referenced in the transaction, +- sum the amounts of that commodity, +- render that sum with a certain precision (number of decimal places), +- and check that the rendered number is all zeros. + +What is that precision (for each commodity) ? +Since hledger 1.21, by default it is the maximum precision used +in the transaction's journal entry (which is also what the `print` +command shows). + +However in hledger 1.20 and before, it was the precision specified by +the journal's [declared](#declaring-commodities) or inferred +[commodity display styles](#commodity-display-style) +(because that's what the `print` command showed). + +You may have some existing journals which are dependent on this older behaviour. +Ie, hledger <=1.20 accepts them but hledger >=1.21 reports "unbalanced transaction". +So we provide the `--balancing=styled` option, which restores the old balanced transaction checking +(but not the old `print` behaviour, so balanced checking might not always agree with what `print` shows.) +Note this is just a convenience to ease migration, and may be dropped in future, +so we recommend that you update your journal entries to satisfy the new balanced checking +(`--balancing=exact`, which is the default). +(Advantages of the new way: it agrees with `print` output; +it is simpler, depending only on the transaction's journal entry; +and it is more robust when `print`-ing transactions to be re-parsed by hledger.) + ## Balance assertions hledger supports diff --git a/hledger/test/balance/budget.test b/hledger/test/balance/budget.test index 4d6376ac8a8..9f15fa14675 100644 --- a/hledger/test/balance/budget.test +++ b/hledger/test/balance/budget.test @@ -365,49 +365,48 @@ Budget performance in 2018-05-01..2018-06-30, valued at period ends: $ hledger -f- bal --budget Budget performance in 2019-01-01..2019-01-03: - || 2019-01-01..2019-01-03 -===================++=========================== - expenses:personal || $50.00 [5% of $1,000.00] - liabilities || $-50.00 [5% of $-1000.00] --------------------++--------------------------- - || 0 [ 0] + || 2019-01-01..2019-01-03 +===================++============================ + expenses:personal || $50.00 [5% of $1,000.00] + liabilities || $-50.00 [5% of $-1,000.00] +-------------------++---------------------------- + || 0 [ 0] # 17. $ hledger -f- bal --budget -E Budget performance in 2019-01-01..2019-01-03: - || 2019-01-01..2019-01-03 -========================================++=========================== - expenses:personal || $50.00 [5% of $1,000.00] - expenses:personal:electronics || $20.00 - expenses:personal:electronics:upgrades || $10.00 - liabilities || $-50.00 [5% of $-1000.00] -----------------------------------------++--------------------------- - || 0 [ 0] + || 2019-01-01..2019-01-03 +========================================++============================ + expenses:personal || $50.00 [5% of $1,000.00] + expenses:personal:electronics || $20.00 + expenses:personal:electronics:upgrades || $10.00 + liabilities || $-50.00 [5% of $-1,000.00] +----------------------------------------++---------------------------- + || 0 [ 0] # 18. $ hledger -f- bal --budget --tree Budget performance in 2019-01-01..2019-01-03: - || 2019-01-01..2019-01-03 -===================++=========================== - expenses:personal || $50.00 [5% of $1,000.00] - liabilities || $-50.00 [5% of $-1000.00] --------------------++--------------------------- - || 0 [ 0] - + || 2019-01-01..2019-01-03 +===================++============================ + expenses:personal || $50.00 [5% of $1,000.00] + liabilities || $-50.00 [5% of $-1,000.00] +-------------------++---------------------------- + || 0 [ 0] # 19. $ hledger -f- bal --budget --tree -E Budget performance in 2019-01-01..2019-01-03: - || 2019-01-01..2019-01-03 -===================++=========================== - expenses:personal || $50.00 [5% of $1,000.00] - electronics || $20.00 - upgrades || $10.00 - liabilities || $-50.00 [5% of $-1000.00] --------------------++--------------------------- - || 0 [ 0] + || 2019-01-01..2019-01-03 +===================++============================ + expenses:personal || $50.00 [5% of $1,000.00] + electronics || $20.00 + upgrades || $10.00 + liabilities || $-50.00 [5% of $-1,000.00] +-------------------++---------------------------- + || 0 [ 0] # 20. Subaccounts + nested budgets < diff --git a/hledger/test/close.test b/hledger/test/close.test index 9d86d4beeed..d5dc61c4ec2 100644 --- a/hledger/test/close.test +++ b/hledger/test/close.test @@ -271,20 +271,20 @@ commodity AAA 0.00000000 $ hledger -f- close -p 2019 assets --show-costs -x 2019-12-31 closing balances - assets:aaa AAA -510.00000000 = AAA 0.00000000 - assets:usd $-49.50 - assets:usd $49.390001 @ AAA 10.3528242505 = $0.00 - equity:opening/closing balances $49.50 - equity:opening/closing balances $-49.390001 @ AAA 10.3528242505 - equity:opening/closing balances AAA 510.00000000 + assets:aaa AAA -510.00000000 = AAA 0.00000000 + assets:usd $-49.50 + assets:usd $49.390001 @ AAA 10.35282425045552 = $0.00 + equity:opening/closing balances $49.50 + equity:opening/closing balances $-49.390001 @ AAA 10.35282425045552 + equity:opening/closing balances AAA 510.00000000 2020-01-01 opening balances - assets:aaa AAA 510.00000000 = AAA 510.00000000 - assets:usd $49.50 - assets:usd $-49.390001 @ AAA 10.3528242505 = $0.109999 - equity:opening/closing balances $-49.50 - equity:opening/closing balances $49.390001 @ AAA 10.3528242505 - equity:opening/closing balances AAA -510.00000000 + assets:aaa AAA 510.00000000 = AAA 510.00000000 + assets:usd $49.50 + assets:usd $-49.390001 @ AAA 10.35282425045552 = $0.109999 + equity:opening/closing balances $-49.50 + equity:opening/closing balances $49.390001 @ AAA 10.35282425045552 + equity:opening/closing balances AAA -510.00000000 >=0 diff --git a/hledger/test/journal/precision.test b/hledger/test/journal/precision.test index 18ced3ac315..ef70c06c46f 100644 --- a/hledger/test/journal/precision.test +++ b/hledger/test/journal/precision.test @@ -123,9 +123,9 @@ hledger -f- print --explicit d D -320.00 >>> 2015-01-01 - c C 10.00 @ D 15.2381 - c C 11.00 @ D 15.2381 - d D -320.00 + c C 10.00 @ D 15.23809524 + c C 11.00 @ D 15.23809524 + d D -320.00 >>>=0 @@ -140,8 +140,8 @@ hledger -f- print --explicit f F -320.000 >>> 2015-01-01 - e E 10.0000 @ F 15.2380952 - e E 11.0000 @ F 15.2380952 - f F -320.000 + e E 10.0000 @ F 15.23809523809524 + e E 11.0000 @ F 15.23809523809524 + f F -320.000 >>>=0 diff --git a/hledger/test/journal/transaction-balancing.test b/hledger/test/journal/transaction-balancing.test new file mode 100644 index 00000000000..1b1f2ba3fbe --- /dev/null +++ b/hledger/test/journal/transaction-balancing.test @@ -0,0 +1,43 @@ +# Test cases for balanced-transaction checking, cf #1479 + +< +commodity $0.00 + +2021-01-01 move a lot elsewhere, adjusting cost basis due to fees + assets:investments1 A -11.0 @ B 0.093735 + expenses:fees A 0.6 + equity:basis adjustment A -0.6 + assets:investments2 A 10.4 @ B 0.099143 + +# 1, 2. succeeds with old "styled" and new "exact" balanced checking +$ hledger -f- check --balancing=styled +$ hledger -f- check + +< +commodity $0.00 + +2021-01-01 + a A -11.0 @ B 0.093735 + b A 10.4 @ B 0.099143 + +# 3, 4. succeeds with old and new balanced checking +$ hledger -f- check --balancing=styled +$ hledger -f- check + +< +commodity B0.00 + +2021-01-01 + a A -9514.405544 @ B 0.104314 + b A 9513.805544 @ B 0.1043206 + +# 5, 6. succeeds and fails with old and new balanced checking +$ hledger -f- check --balancing=styled +$ hledger -f- check +>2 // +>= 1 + +# < +# 2021-01-01 +# a 1C @ $1.0049 +# b $-1.000 diff --git a/hledger/test/journal/transaction-prices.test b/hledger/test/journal/transaction-prices.test index eff5c3be4ba..6b90e5c7e15 100644 --- a/hledger/test/journal/transaction-prices.test +++ b/hledger/test/journal/transaction-prices.test @@ -51,12 +51,12 @@ hledger -f - print --explicit misc $-2.1 >>> 2011-01-01 - expenses:foreign currency €100 @ $1.35 - misc $2.10 - assets $-135.00 - misc €1 @ $1.35 - misc €-1 @ $1.35 - misc $-2.10 + expenses:foreign currency €100 @ $1.3500 + misc $2.10 + assets $-135.00 + misc €1 @ $1.3500 + misc €-1 @ $1.3500 + misc $-2.10 >>>=0