imp:print: zero posting amounts are now shown with commodity & style

print now shows zero posting amounts with their original commodity
symbol and the corresponding style (instead of stripping the symbol).

If an inferred amount has multiple zeroes in different commodities,
a posting is displayed for each of these.

Possible breaking changes:

showMixedAmountLinesB, showAmountB, showAmountPrice now preserve
commodityful zeroes when rendering. This is intended to improve print output,
but it seems possible it might also affect balance and register reports,
though our tests show no change in those.
This commit is contained in:
Simon Michael 2023-08-24 14:57:19 +01:00
parent 35c0fd692c
commit ff730f775b
10 changed files with 95 additions and 47 deletions

View File

@ -102,6 +102,7 @@ module Hledger.Data.Amount (
maAddAmounts, maAddAmounts,
amounts, amounts,
amountsRaw, amountsRaw,
amountsPreservingZeros,
maCommodities, maCommodities,
filterMixedAmount, filterMixedAmount,
filterMixedAmountByCommodity, filterMixedAmountByCommodity,
@ -170,6 +171,7 @@ import Hledger.Data.Types
import Hledger.Utils (colorB, numDigitsInt) import Hledger.Utils (colorB, numDigitsInt)
import Hledger.Utils.Text (textQuoteIfNeeded) import Hledger.Utils.Text (textQuoteIfNeeded)
import Text.WideString (WideBuilder(..), wbFromText, wbToText, wbUnpack) import Text.WideString (WideBuilder(..), wbFromText, wbToText, wbUnpack)
import Data.Functor ((<&>))
-- A 'Commodity' is a symbol representing a currency or some other kind of -- A 'Commodity' is a symbol representing a currency or some other kind of
@ -208,10 +210,10 @@ data AmountDisplayOpts = AmountDisplayOpts
, displayOrder :: Maybe [CommoditySymbol] , displayOrder :: Maybe [CommoditySymbol]
} deriving (Show) } 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 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
noColour = AmountDisplayOpts { displayPrice = True noColour = AmountDisplayOpts { displayPrice = True
, displayColour = False , displayColour = False
@ -406,8 +408,8 @@ amountStripPrices a = a{aprice=Nothing}
showAmountPrice :: Amount -> WideBuilder showAmountPrice :: Amount -> WideBuilder
showAmountPrice amt = case aprice amt of showAmountPrice amt = case aprice amt of
Nothing -> mempty Nothing -> mempty
Just (UnitPrice pa) -> WideBuilder (TB.fromString " @ ") 3 <> showAmountB noColour pa Just (UnitPrice pa) -> WideBuilder (TB.fromString " @ ") 3 <> showAmountB noColour{displayZeroCommodity=True} pa
Just (TotalPrice pa) -> WideBuilder (TB.fromString " @@ ") 4 <> showAmountB noColour (sign pa) Just (TotalPrice pa) -> WideBuilder (TB.fromString " @@ ") 4 <> showAmountB noColour{displayZeroCommodity=True} (sign pa)
where sign = if aquantity amt < 0 then negate else id where sign = if aquantity amt < 0 then negate else id
showAmountPriceDebug :: Maybe AmountPrice -> String showAmountPriceDebug :: Maybe AmountPrice -> String
@ -460,14 +462,16 @@ showAmountB :: AmountDisplayOpts -> Amount -> WideBuilder
showAmountB _ Amount{acommodity="AUTO"} = mempty showAmountB _ Amount{acommodity="AUTO"} = mempty
showAmountB opts a@Amount{astyle=style} = showAmountB opts a@Amount{astyle=style} =
color $ case ascommodityside style of color $ case ascommodityside style of
L -> showC (wbFromText c) space <> quantity' <> price L -> showC (wbFromText comm) space <> quantity' <> price
R -> quantity' <> showC space (wbFromText c) <> price R -> quantity' <> showC space (wbFromText comm) <> price
where where
color = if displayColour opts && isNegativeAmount a then colorB Dull Red else id 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 = showamountquantity $
(quantity',c) | amountLooksZero a && not (displayZeroCommodity opts) = (WideBuilder (TB.singleton '0') 1,"") if displayThousandsSep opts then a else a{astyle=(astyle a){asdigitgroups=Nothing}}
| otherwise = (quantity, quoteCommoditySymbolIfNeeded $ acommodity a) (quantity', comm)
space = if not (T.null c) && ascommodityspaced style then WideBuilder (TB.singleton ' ') 1 else mempty | 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, -- concatenate these texts,
-- or return the empty text if there's a commodity display order. XXX why ? -- 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 showC l r = if isJust (displayOrder opts) then mempty else l <> r
@ -672,7 +676,8 @@ maIsZero = mixedAmountIsZero
maIsNonZero :: MixedAmount -> Bool maIsNonZero :: MixedAmount -> Bool
maIsNonZero = not . mixedAmountIsZero 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 -- * 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 :: MixedAmount -> [Amount]
amounts (Mixed ma) 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] | M.null nonzeros = [newzero]
| otherwise = toList nonzeros | otherwise = toList nonzeros
where where
newzero = fromMaybe nullamt $ find (not . T.null . acommodity) zeros newzero = fromMaybe nullamt $ find (not . T.null . acommodity) zeros
(zeros, nonzeros) = M.partition amountIsZero ma (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 -- | 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 -- 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, -- 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 -- 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] 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@), -- Get a mixed amount's component amounts with a bit of cleanup,
-- and if a commodity display order is provided, sort them according to that. -- optionally preserving multiple zeros in different commodities,
-- optionally sorting them according to a commodity display order.
orderedAmounts :: AmountDisplayOpts -> MixedAmount -> [Amount] orderedAmounts :: AmountDisplayOpts -> MixedAmount -> [Amount]
orderedAmounts AmountDisplayOpts{displayOrder=mcommodityorder} = orderedAmounts AmountDisplayOpts{displayZeroCommodity=preservezeros, displayOrder=mcommodityorder} =
amounts if preservezeros then amountsPreservingZeros else amounts
<&> maybe id (mapM findfirst) mcommodityorder -- maybe sort them (somehow..) <&> maybe id (mapM findfirst) mcommodityorder -- maybe sort them (somehow..)
where where
-- Find the first amount with the given commodity, otherwise a null amount in that commodity. -- Find the first amount with the given commodity, otherwise a null amount in that commodity.

View File

@ -220,6 +220,9 @@ postingsAsLines onelineamounts ps = concatMap first3 linesWithWidths
-- Or if onelineamounts is true, such amounts are shown on one line, comma-separated -- Or if onelineamounts is true, such amounts are shown on one line, comma-separated
-- (and the output will not be valid journal syntax). -- (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 -- 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 -- 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 -- 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. -- amtwidth at all.
shownAmounts shownAmounts
| elideamount = [mempty] | elideamount = [mempty]
| otherwise = showMixedAmountLinesB noColour{displayOneLine=onelineamounts} $ pamount p | otherwise = showMixedAmountLinesB noColour{displayZeroCommodity=True, displayOneLine=onelineamounts} $ pamount p
thisamtwidth = maximumBound 0 $ map wbWidth shownAmounts thisamtwidth = maximumBound 0 $ map wbWidth shownAmounts
-- when there is a balance assertion, show it only on the last posting line -- when there is a balance assertion, show it only on the last posting line

View File

@ -46,8 +46,8 @@ $ hledger -f - balance -N
b b
$ hledger -f- print --explicit --empty $ hledger -f- print --explicit --empty
2010-03-01 x 2010-03-01 x
a 0 @ 3EUR a $0.00 @ 3EUR
b 0 b 0EUR
>= 0 >= 0

View File

@ -1006,8 +1006,8 @@ account1 assets:bank:checking
$ ./csvtest.sh $ ./csvtest.sh
2020-01-21 Client card point of sale fee 2020-01-21 Client card point of sale fee
assets:bank:checking 0 = $1068.94 assets:bank:checking $0 = $1068.94
expenses:unknown 0 expenses:unknown $0
>=0 >=0

View File

@ -96,8 +96,8 @@ $ hledger -f- print
b B 0 b B 0
$ hledger -f- print $ hledger -f- print
2020-01-01 2020-01-01
a 0 a A 0
b 0 b B 0
>=0 >=0

View File

@ -23,7 +23,7 @@ $ hledger -f - print
a a
$ hledger -f - print --explicit $ hledger -f - print --explicit
2010-01-01 2010-01-01
a 0 a $0.00
a 1C @ $1.0049 a 1C @ $1.0049
a $-1.0049 a $-1.0049

View File

@ -62,8 +62,8 @@ $ hledger -f- print -x --value=now,Z
# and sign are not shown either. # and sign are not shown either.
$ hledger -f- print -x --value=now,C $ hledger -f- print -x --value=now,C
2019-06-01 2019-06-01
a 0 a C0
b 0 b C0
>= >=
# # There's nothing setting C display style, so the default style is used, # # There's nothing setting C display style, so the default style is used,

View File

@ -92,8 +92,8 @@ $ hledger -f - print
equity equity
$ hledger -f - print --explicit $ hledger -f - print --explicit
2017-01-01 2017-01-01
assets 0 assets $0
equity 0 equity $0
>= 0 >= 0

View File

@ -1,13 +1,27 @@
# test -B and #551 # 1. print preserves the commodity symbol of zero amounts.
#hledger -f- print -B <
#<<< 2023-01-01
#2009/1/1 (a) 0 A @ 0 B == 0 A @ 0 B
# assets:foreign currency €100
# assets:cash $-135 $ hledger -f- print
#>>> 2023-01-01
#2009/01/01 (a) 0 A @ 0 B == 0 A @ 0 B
# assets:foreign currency $135
# assets:cash $-135 >=
#
#>>>2 # 2. The inferred balancing amount for zeros in multiple commodities
#>>>= 0 # 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
>=

View File

@ -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 ; but relative order matters to refer-rewritten transactions
= ^expenses not:housing not:grocery not:food = ^expenses not:housing not:grocery not:food
(budget:misc) *-1 (budget:misc) *-1
$ hledger rewrite -f- date:2017/1 --add-posting 'Here comes Santa $0' --verbose-tags $ hledger rewrite -f- date:2017/1 --add-posting 'Here comes Santa $0' --verbose-tags
2016-12-31 ; modified: 2016-12-31 ; modified:
expenses:housing $600.00 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: 2017-01-01 ; modified:
expenses:food $20.00 expenses:food $20.00
(budget:food) $-20.00 ; generated-posting: = ^expenses:grocery ^expenses:food (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 expenses:leisure $15.00
(budget:misc) $-15.00 ; generated-posting: = ^expenses not:housing not:grocery not:food (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 expenses:grocery $30.00
(budget:food) $-30.00 ; generated-posting: = ^expenses:grocery ^expenses:food (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 assets:cash
Here comes Santa 0 ; generated-posting: = date:2017/1 Here comes Santa $0 ; generated-posting: = date:2017/1
2017-01-02 ; modified: 2017-01-02 ; modified:
assets:cash $200.00 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
assets:bank $-1.60 ; generated-posting: = ^assets:bank$ date:2017/1 amt:<0 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 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 (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 2017-02-01
assets:cash $100.00 assets:cash $100.00