diff --git a/hledger-lib/Hledger/Data/Amount.hs b/hledger-lib/Hledger/Data/Amount.hs index b9b44059f..775c06bca 100644 --- a/hledger-lib/Hledger/Data/Amount.hs +++ b/hledger-lib/Hledger/Data/Amount.hs @@ -102,6 +102,7 @@ module Hledger.Data.Amount ( maAddAmounts, amounts, amountsRaw, + amountsPreservingZeros, maCommodities, filterMixedAmount, filterMixedAmountByCommodity, @@ -170,6 +171,7 @@ import Hledger.Data.Types import Hledger.Utils (colorB, numDigitsInt) import Hledger.Utils.Text (textQuoteIfNeeded) import Text.WideString (WideBuilder(..), wbFromText, wbToText, wbUnpack) +import Data.Functor ((<&>)) -- A 'Commodity' is a symbol representing a currency or some other kind of @@ -208,10 +210,10 @@ data AmountDisplayOpts = AmountDisplayOpts , displayOrder :: Maybe [CommoditySymbol] } deriving (Show) --- | Display Amount and MixedAmount with no colour. +-- | By default, display Amount and MixedAmount using @noColour@ amount display options. instance Default AmountDisplayOpts where def = noColour --- | Display Amount and MixedAmount with no colour. +-- | Display amounts without colour, and with various other defaults. noColour :: AmountDisplayOpts noColour = AmountDisplayOpts { displayPrice = True , displayColour = False @@ -406,8 +408,8 @@ amountStripPrices a = a{aprice=Nothing} showAmountPrice :: Amount -> WideBuilder showAmountPrice amt = case aprice amt of Nothing -> mempty - Just (UnitPrice pa) -> WideBuilder (TB.fromString " @ ") 3 <> showAmountB noColour pa - Just (TotalPrice pa) -> WideBuilder (TB.fromString " @@ ") 4 <> showAmountB noColour (sign pa) + Just (UnitPrice pa) -> WideBuilder (TB.fromString " @ ") 3 <> showAmountB noColour{displayZeroCommodity=True} pa + Just (TotalPrice pa) -> WideBuilder (TB.fromString " @@ ") 4 <> showAmountB noColour{displayZeroCommodity=True} (sign pa) where sign = if aquantity amt < 0 then negate else id showAmountPriceDebug :: Maybe AmountPrice -> String @@ -460,14 +462,16 @@ showAmountB :: AmountDisplayOpts -> Amount -> WideBuilder showAmountB _ Amount{acommodity="AUTO"} = mempty showAmountB opts a@Amount{astyle=style} = color $ case ascommodityside style of - L -> showC (wbFromText c) space <> quantity' <> price - R -> quantity' <> showC space (wbFromText c) <> price + L -> showC (wbFromText comm) space <> quantity' <> price + R -> quantity' <> showC space (wbFromText comm) <> price where color = if displayColour opts && isNegativeAmount a then colorB Dull Red else id - quantity = showamountquantity $ if displayThousandsSep opts then a else a{astyle=(astyle a){asdigitgroups=Nothing}} - (quantity',c) | amountLooksZero a && not (displayZeroCommodity opts) = (WideBuilder (TB.singleton '0') 1,"") - | otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a) - space = if not (T.null c) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty + quantity = showamountquantity $ + if displayThousandsSep opts then a else a{astyle=(astyle a){asdigitgroups=Nothing}} + (quantity', comm) + | amountLooksZero a && not (displayZeroCommodity opts) = (WideBuilder (TB.singleton '0') 1, "") + | otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a) + space = if not (T.null comm) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty -- concatenate these texts, -- or return the empty text if there's a commodity display order. XXX why ? showC l r = if isJust (displayOrder opts) then mempty else l <> r @@ -672,7 +676,8 @@ maIsZero = mixedAmountIsZero maIsNonZero :: MixedAmount -> Bool maIsNonZero = not . mixedAmountIsZero --- | Get a mixed amount's component amounts. +-- | Get a mixed amount's component amounts, with some cleanups. +-- The following descriptions are old and possibly wrong: -- -- * amounts in the same commodity are combined unless they have different prices or total prices -- @@ -686,13 +691,37 @@ maIsNonZero = not . mixedAmountIsZero -- amounts :: MixedAmount -> [Amount] amounts (Mixed ma) - | isMissingMixedAmount (Mixed ma) = [missingamt] -- missingamt should always be alone, but detect it even if not + | isMissingMixedAmount (Mixed ma) = [missingamt] | M.null nonzeros = [newzero] | otherwise = toList nonzeros where newzero = fromMaybe nullamt $ find (not . T.null . acommodity) zeros (zeros, nonzeros) = M.partition amountIsZero ma +-- | Get a mixed amount's component amounts, with some cleanups. +-- This is a new version of @amounts@, with updated descriptions +-- and optimised for @print@ to show commodityful zeros. +-- +-- * If it contains the "missing amount" marker, only that is returned +-- (discarding any additional amounts). +-- +-- * Or if it contains any non-zero amounts, only those are returned +-- (discarding any zeroes). +-- +-- * Or if it contains any zero amounts (possibly more than one, +-- possibly in different commodities), all of those are returned. +-- +-- * Otherwise the null amount is returned. +-- +amountsPreservingZeros :: MixedAmount -> [Amount] +amountsPreservingZeros (Mixed ma) + | isMissingMixedAmount (Mixed ma) = [missingamt] + | not $ M.null nonzeros = toList nonzeros + | not $ M.null zeros = toList zeros + | otherwise = [nullamt] + where + (zeros, nonzeros) = M.partition amountIsZero ma + -- | Get a mixed amount's component amounts without normalising zero and missing -- amounts. This is used for JSON serialisation, so the order is important. In -- particular, we want the Amounts given in the order of the MixedAmountKeys, @@ -913,11 +942,12 @@ showMixedAmountOneLineB opts@AmountDisplayOpts{displayMaxWidth=mmax,displayMinWi -- Add the elision strings (if any) to each amount withElided = zipWith (\n2 amt -> (amt, elisionDisplay Nothing (wbWidth sep) n2 amt)) [n-1,n-2..0] --- Get a mixed amount's component amounts with a bit of cleanup (like @amounts@), --- and if a commodity display order is provided, sort them according to that. +-- Get a mixed amount's component amounts with a bit of cleanup, +-- optionally preserving multiple zeros in different commodities, +-- optionally sorting them according to a commodity display order. orderedAmounts :: AmountDisplayOpts -> MixedAmount -> [Amount] -orderedAmounts AmountDisplayOpts{displayOrder=mcommodityorder} = - amounts +orderedAmounts AmountDisplayOpts{displayZeroCommodity=preservezeros, displayOrder=mcommodityorder} = + if preservezeros then amountsPreservingZeros else amounts <&> maybe id (mapM findfirst) mcommodityorder -- maybe sort them (somehow..) where -- Find the first amount with the given commodity, otherwise a null amount in that commodity. diff --git a/hledger-lib/Hledger/Data/Posting.hs b/hledger-lib/Hledger/Data/Posting.hs index 352f5656c..5f148ec25 100644 --- a/hledger-lib/Hledger/Data/Posting.hs +++ b/hledger-lib/Hledger/Data/Posting.hs @@ -220,6 +220,9 @@ postingsAsLines onelineamounts ps = concatMap first3 linesWithWidths -- Or if onelineamounts is true, such amounts are shown on one line, comma-separated -- (and the output will not be valid journal syntax). -- +-- If an amount is zero, any commodity symbol attached to it is shown +-- (and the corresponding commodity display style is used). +-- -- By default, 4 spaces (2 if there's a status flag) are shown between -- account name and start of amount area, which is typically 12 chars wide -- and contains a right-aligned amount (so 10-12 visible spaces between @@ -262,7 +265,7 @@ postingAsLines elideamount onelineamounts acctwidth amtwidth p = -- amtwidth at all. shownAmounts | elideamount = [mempty] - | otherwise = showMixedAmountLinesB noColour{displayOneLine=onelineamounts} $ pamount p + | otherwise = showMixedAmountLinesB noColour{displayZeroCommodity=True, displayOneLine=onelineamounts} $ pamount p thisamtwidth = maximumBound 0 $ map wbWidth shownAmounts -- when there is a balance assertion, show it only on the last posting line diff --git a/hledger/test/amount-rendering.test b/hledger/test/amount-rendering.test index deaf3393c..2142f2c3e 100644 --- a/hledger/test/amount-rendering.test +++ b/hledger/test/amount-rendering.test @@ -46,8 +46,8 @@ $ hledger -f - balance -N b $ hledger -f- print --explicit --empty 2010-03-01 x - a 0 @ 3EUR - b 0 + a $0.00 @ 3EUR + b 0EUR >= 0 diff --git a/hledger/test/csv.test b/hledger/test/csv.test index feb9fcf78..58948dea4 100644 --- a/hledger/test/csv.test +++ b/hledger/test/csv.test @@ -1006,8 +1006,8 @@ account1 assets:bank:checking $ ./csvtest.sh 2020-01-21 Client card point of sale fee - assets:bank:checking 0 = $1068.94 - expenses:unknown 0 + assets:bank:checking $0 = $1068.94 + expenses:unknown $0 >=0 diff --git a/hledger/test/journal/parse-errors.test b/hledger/test/journal/parse-errors.test index c0e4aa1ab..d75c44f70 100644 --- a/hledger/test/journal/parse-errors.test +++ b/hledger/test/journal/parse-errors.test @@ -96,8 +96,8 @@ $ hledger -f- print b B 0 $ hledger -f- print 2020-01-01 - a 0 - b 0 + a A 0 + b B 0 >=0 diff --git a/hledger/test/journal/precision.test b/hledger/test/journal/precision.test index c04df95b7..54b45f777 100644 --- a/hledger/test/journal/precision.test +++ b/hledger/test/journal/precision.test @@ -23,7 +23,7 @@ $ hledger -f - print a $ hledger -f - print --explicit 2010-01-01 - a 0 + a $0.00 a 1C @ $1.0049 a $-1.0049 diff --git a/hledger/test/journal/valuation2.test b/hledger/test/journal/valuation2.test index 82661e16f..526723543 100644 --- a/hledger/test/journal/valuation2.test +++ b/hledger/test/journal/valuation2.test @@ -62,8 +62,8 @@ $ hledger -f- print -x --value=now,Z # and sign are not shown either. $ hledger -f- print -x --value=now,C 2019-06-01 - a 0 - b 0 + a C0 + b C0 >= # # There's nothing setting C display style, so the default style is used, diff --git a/hledger/test/print/explicit.test b/hledger/test/print/explicit.test index 59f702341..123953e5d 100644 --- a/hledger/test/print/explicit.test +++ b/hledger/test/print/explicit.test @@ -92,8 +92,8 @@ $ hledger -f - print equity $ hledger -f - print --explicit 2017-01-01 - assets 0 - equity 0 + assets $0 + equity $0 >= 0 diff --git a/hledger/test/print/print.test b/hledger/test/print/print.test index 796cf8e6d..c5cd8f60d 100644 --- a/hledger/test/print/print.test +++ b/hledger/test/print/print.test @@ -1,13 +1,27 @@ -# test -B and #551 -#hledger -f- print -B -#<<< -#2009/1/1 -# assets:foreign currency €100 -# assets:cash $-135 -#>>> -#2009/01/01 -# assets:foreign currency $135 -# assets:cash $-135 -# -#>>>2 -#>>>= 0 +# 1. print preserves the commodity symbol of zero amounts. +< +2023-01-01 + (a) 0 A @ 0 B == 0 A @ 0 B + +$ hledger -f- print +2023-01-01 + (a) 0 A @ 0 B == 0 A @ 0 B + +>= + +# 2. The inferred balancing amount for zeros in multiple commodities +# is preserved and shown accurately, with a posting for each commodity. +< +2023-01-01 + a 0 A + b 0 B + z + +$ hledger -f- print -x +2023-01-01 + a 0 A + b 0 B + z 0 A + z 0 B + +>= diff --git a/hledger/test/rewrite.test b/hledger/test/rewrite.test index a8c7832b7..35ebff656 100644 --- a/hledger/test/rewrite.test +++ b/hledger/test/rewrite.test @@ -178,6 +178,7 @@ $ hledger rewrite -f- assets:bank and 'amt:<0' --add-posting 'expenses:fee $5' ; but relative order matters to refer-rewritten transactions = ^expenses not:housing not:grocery not:food (budget:misc) *-1 + $ hledger rewrite -f- date:2017/1 --add-posting 'Here comes Santa $0' --verbose-tags 2016-12-31 ; modified: expenses:housing $600.00 @@ -187,24 +188,24 @@ $ hledger rewrite -f- date:2017/1 --add-posting 'Here comes Santa $0' --verbos 2017-01-01 ; modified: expenses:food $20.00 (budget:food) $-20.00 ; generated-posting: = ^expenses:grocery ^expenses:food - Here comes Santa 0 ; generated-posting: = date:2017/1 + Here comes Santa $0 ; generated-posting: = date:2017/1 expenses:leisure $15.00 (budget:misc) $-15.00 ; generated-posting: = ^expenses not:housing not:grocery not:food - Here comes Santa 0 ; generated-posting: = date:2017/1 + Here comes Santa $0 ; generated-posting: = date:2017/1 expenses:grocery $30.00 (budget:food) $-30.00 ; generated-posting: = ^expenses:grocery ^expenses:food - Here comes Santa 0 ; generated-posting: = date:2017/1 + Here comes Santa $0 ; generated-posting: = date:2017/1 assets:cash - Here comes Santa 0 ; generated-posting: = date:2017/1 + Here comes Santa $0 ; generated-posting: = date:2017/1 2017-01-02 ; modified: assets:cash $200.00 - Here comes Santa 0 ; generated-posting: = date:2017/1 + Here comes Santa $0 ; generated-posting: = date:2017/1 assets:bank assets:bank $-1.60 ; generated-posting: = ^assets:bank$ date:2017/1 amt:<0 expenses:fee $1.60 ; cash withdraw fee, generated-posting: = ^assets:bank$ date:2017/1 amt:<0 (budget:misc) $-1.60 ; generated-posting: = ^expenses not:housing not:grocery not:food - Here comes Santa 0 ; generated-posting: = date:2017/1 + Here comes Santa $0 ; generated-posting: = date:2017/1 2017-02-01 assets:cash $100.00