This change provides more predictable and intuitive behaviour when using -S/--sort-amount with multiple commodities. It implements a custom Ord (and Eq) instance for MixedAmount which substitutes zero for any missing commodities. As a consequence, all the ways of representing zero with a MixedAmount ([], [A 0], [A 0, B 0, ...]) are now Eq-ual (==), whereas before they were not. We have not been able to find anything broken by this change. * imp: lib: Compare MixedAmounts by substituting zero for any missing commodities. (#1563) * ;doc: Update docs for new multicommodity sort by amount rules.
This commit is contained in:
parent
3380190d9a
commit
cf25d7d56d
@ -93,6 +93,7 @@ module Hledger.Data.Amount (
|
||||
-- * MixedAmount
|
||||
nullmixedamt,
|
||||
missingmixedamt,
|
||||
isMissingMixedAmount,
|
||||
mixed,
|
||||
mixedAmount,
|
||||
maAddAmount,
|
||||
@ -540,6 +541,10 @@ nullmixedamt = Mixed mempty
|
||||
missingmixedamt :: MixedAmount
|
||||
missingmixedamt = mixedAmount missingamt
|
||||
|
||||
-- | Whether a MixedAmount has a missing amount
|
||||
isMissingMixedAmount :: MixedAmount -> Bool
|
||||
isMissingMixedAmount (Mixed ma) = amountKey missingamt `M.member` ma
|
||||
|
||||
-- | Convert amounts in various commodities into a mixed amount.
|
||||
mixed :: Foldable t => t Amount -> MixedAmount
|
||||
mixed = maAddAmounts nullmixedamt
|
||||
@ -637,13 +642,12 @@ maIsNonZero = not . mixedAmountIsZero
|
||||
--
|
||||
amounts :: MixedAmount -> [Amount]
|
||||
amounts (Mixed ma)
|
||||
| missingkey `M.member` ma = [missingamt] -- missingamt should always be alone, but detect it even if not
|
||||
| isMissingMixedAmount (Mixed ma) = [missingamt] -- missingamt should always be alone, but detect it even if not
|
||||
| M.null nonzeros = [newzero]
|
||||
| otherwise = toList nonzeros
|
||||
where
|
||||
newzero = fromMaybe nullamt $ find (not . T.null . acommodity) zeros
|
||||
(zeros, nonzeros) = M.partition amountIsZero ma
|
||||
missingkey = amountKey missingamt
|
||||
|
||||
-- | 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
|
||||
@ -966,7 +970,14 @@ tests_Amount = tests "Amount" [
|
||||
|
||||
,tests "MixedAmount" [
|
||||
|
||||
test "adding mixed amounts to zero, the commodity and amount style are preserved" $
|
||||
test "comparing mixed amounts compares based on quantities" $ do
|
||||
let usdpos = mixed [usd 1]
|
||||
usdneg = mixed [usd (-1)]
|
||||
eurneg = mixed [eur (-12)]
|
||||
compare usdneg usdpos @?= LT
|
||||
compare eurneg usdpos @?= LT
|
||||
|
||||
,test "adding mixed amounts to zero, the commodity and amount style are preserved" $
|
||||
maSum (map mixedAmount
|
||||
[usd 1.25
|
||||
,usd (-1) `withPrecision` Precision 3
|
||||
|
||||
@ -183,7 +183,7 @@ isBalancedVirtual :: Posting -> Bool
|
||||
isBalancedVirtual p = ptype p == BalancedVirtualPosting
|
||||
|
||||
hasAmount :: Posting -> Bool
|
||||
hasAmount = (/= missingmixedamt) . pamount
|
||||
hasAmount = not . isMissingMixedAmount . pamount
|
||||
|
||||
hasBalanceAssignment :: Posting -> Bool
|
||||
hasBalanceAssignment p = not (hasAmount p) && isJust (pbalanceassertion p)
|
||||
|
||||
@ -27,8 +27,8 @@ module Hledger.Data.Types
|
||||
where
|
||||
|
||||
import GHC.Generics (Generic)
|
||||
import Data.Decimal
|
||||
import Data.Default
|
||||
import Data.Decimal (Decimal)
|
||||
import Data.Default (Default(..))
|
||||
import Data.Functor (($>))
|
||||
import Data.List (intercalate)
|
||||
import Text.Blaze (ToMarkup(..))
|
||||
@ -230,7 +230,27 @@ data Amount = Amount {
|
||||
aprice :: !(Maybe AmountPrice) -- ^ the (fixed, transaction-specific) price for this amount, if any
|
||||
} deriving (Eq,Ord,Generic,Show)
|
||||
|
||||
newtype MixedAmount = Mixed (M.Map MixedAmountKey Amount) deriving (Eq,Ord,Generic,Show)
|
||||
newtype MixedAmount = Mixed (M.Map MixedAmountKey Amount) deriving (Generic,Show)
|
||||
|
||||
instance Eq MixedAmount where a == b = maCompare a b == EQ
|
||||
instance Ord MixedAmount where compare = maCompare
|
||||
|
||||
-- | Compare two MixedAmounts, substituting 0 for the quantity of any missing
|
||||
-- commodities in either.
|
||||
maCompare :: MixedAmount -> MixedAmount -> Ordering
|
||||
maCompare (Mixed a) (Mixed b) = go (M.toList a) (M.toList b)
|
||||
where
|
||||
go xss@((kx,x):xs) yss@((ky,y):ys) = case compare kx ky of
|
||||
EQ -> compareQuantities (Just x) (Just y) <> go xs ys
|
||||
LT -> compareQuantities (Just x) Nothing <> go xs yss
|
||||
GT -> compareQuantities Nothing (Just y) <> go xss ys
|
||||
go ((_,x):xs) [] = compareQuantities (Just x) Nothing <> go xs []
|
||||
go [] ((_,y):ys) = compareQuantities Nothing (Just y) <> go [] ys
|
||||
go [] [] = EQ
|
||||
compareQuantities = comparing (maybe 0 aquantity) <> comparing (maybe 0 totalprice)
|
||||
totalprice x = case aprice x of
|
||||
Just (TotalPrice p) -> aquantity p
|
||||
_ -> 0
|
||||
|
||||
-- | Stores the CommoditySymbol of the Amount, along with the CommoditySymbol of
|
||||
-- the price, and its unit price if being used.
|
||||
|
||||
@ -259,6 +259,9 @@ Here are some ways to handle that:
|
||||
|
||||
With `-S/--sort-amount`, accounts with the largest (most positive) balances are shown first.
|
||||
Eg: `hledger bal expenses -MAS` shows your biggest averaged monthly expenses first.
|
||||
When more than one commodity is present, they will be sorted by the alphabetically earliest
|
||||
commodity first, and then by subsequent commodities (if an amount is missing a commodity, it
|
||||
is treated as 0).
|
||||
|
||||
Revenues and liability balances are typically negative, however, so `-S` shows these in reverse order.
|
||||
To work around this, you can add `--invert` to flip the signs.
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
2018/1/1
|
||||
(a:k) 1
|
||||
|
||||
# In tree mode, rows are sorted alphabetically by account name, at each tree level.
|
||||
# 1. In tree mode, rows are sorted alphabetically by account name, at each tree level.
|
||||
# Missing parent accounts are added (b).
|
||||
$ hledger -f- bal -N --tree
|
||||
1 a:k
|
||||
@ -24,7 +24,7 @@ $ hledger -f- bal -N --tree
|
||||
1 j
|
||||
1 c
|
||||
|
||||
# In flat mode, unused parent accounts are not added (b).
|
||||
# 2. In flat mode, unused parent accounts are not added (b).
|
||||
$ hledger -f- bal -N --flat
|
||||
1 a:k
|
||||
1 b:i
|
||||
@ -56,7 +56,7 @@ account d
|
||||
2018/1/1
|
||||
(d) 1
|
||||
|
||||
# With account directives, in tree mode, at each tree level
|
||||
# 3. With account directives, in tree mode, at each tree level
|
||||
# declared accounts are sorted first in declaration order,
|
||||
# followed by undeclared accounts sorted alphabetically.
|
||||
# Missing parent accounts are added (b).
|
||||
@ -70,7 +70,7 @@ $ hledger -f- bal -N --tree
|
||||
1 i
|
||||
1 c
|
||||
|
||||
# In flat mode, unused parent accounts are not added (b).
|
||||
# 4. In flat mode, unused parent accounts are not added (b).
|
||||
$ hledger -f- bal -N --flat
|
||||
1 d
|
||||
1 a:l
|
||||
@ -81,7 +81,7 @@ $ hledger -f- bal -N --flat
|
||||
|
||||
# ** Sort by amount
|
||||
|
||||
# In flat mode with -S, largest (most-positive) amounts are shown first:
|
||||
# 5. In flat mode with -S, largest (most-positive) amounts are shown first:
|
||||
<
|
||||
2018/1/1
|
||||
(b:j) 2
|
||||
@ -101,7 +101,7 @@ $ hledger -f- bal -N -S --flat
|
||||
1 b:i
|
||||
1 c
|
||||
|
||||
# In tree mode with -S, rows are sorted by largest amount, and then by account name, at each tree level.
|
||||
# 6. In tree mode with -S, rows are sorted by largest amount, and then by account name, at each tree level.
|
||||
$ hledger -f- bal -N -S --tree
|
||||
3 b
|
||||
2 j
|
||||
@ -109,8 +109,8 @@ $ hledger -f- bal -N -S --tree
|
||||
1 a:k
|
||||
1 c
|
||||
|
||||
# In hledger 1.4-1.10, when the larger amount was composed of differently-priced amounts,
|
||||
# it could get sorted as if smaller. Should work now:
|
||||
# 7. In hledger 1.4-1.10, when the larger amount was composed of differently-priced amounts,
|
||||
# it could get sorted as if smaller. Should work now. Test tree mode.
|
||||
<
|
||||
2018/1/1
|
||||
(a) 2X @ 1Y
|
||||
@ -119,11 +119,16 @@ $ hledger -f- bal -N -S --tree
|
||||
2018/1/1
|
||||
(b) 3X
|
||||
|
||||
$ hledger -f- bal -N -S
|
||||
$ hledger -f- bal -N -S --tree
|
||||
4X a
|
||||
3X b
|
||||
|
||||
# #1279 in hledger 1.11-1.18, bal -S did not respect the hierarchy. Should work now:
|
||||
# 8. Same in flat mode.
|
||||
$ hledger -f- bal -N -S --flat
|
||||
4X a
|
||||
3X b
|
||||
|
||||
# 9. #1279 in hledger 1.11-1.18, bal -S did not respect the hierarchy. Should work now:
|
||||
<
|
||||
2020-01-01
|
||||
(a:aa) 1
|
||||
@ -136,19 +141,19 @@ $ hledger -f- bal -N -S --tree
|
||||
1 aa
|
||||
2 b
|
||||
|
||||
# #1287 bal -S -H did not sort by amount, should work now:
|
||||
# 10. #1287 bal -S -H did not sort by amount, should work now:
|
||||
$ hledger -f- bal -N -S -H --flat
|
||||
3 a:ab
|
||||
2 b
|
||||
1 a:aa
|
||||
|
||||
# #1287 and bal -S --cumulative:
|
||||
# 11. #1287 and bal -S --cumulative:
|
||||
$ hledger -f- bal -N -S --cumulative --flat
|
||||
3 a:ab
|
||||
2 b
|
||||
1 a:aa
|
||||
|
||||
# #1283 most-negative amounts are sorted last, so eg largest revenues/liabilities are last:
|
||||
# 12. #1283 most-negative amounts are sorted last, so eg largest revenues/liabilities are last:
|
||||
<
|
||||
2020-01-01
|
||||
(revenues:a) -1
|
||||
@ -160,13 +165,13 @@ $ hledger -f- bal -N -S
|
||||
-2 revenues:c
|
||||
-3 revenues:b
|
||||
|
||||
# This can be worked around by using --invert (sorting happens after sign-flipping):
|
||||
# 13. This can be worked around by using --invert (sorting happens after sign-flipping):
|
||||
$ hledger -f- bal -N -S --invert
|
||||
3 revenues:b
|
||||
2 revenues:c
|
||||
1 revenues:a
|
||||
|
||||
# Or a sign-flipping command like incomestatement:
|
||||
# 14. Or a sign-flipping command like incomestatement:
|
||||
$ hledger -f- is -N -S
|
||||
Income Statement 2020-01-01
|
||||
|
||||
@ -180,3 +185,46 @@ Income Statement 2020-01-01
|
||||
============++============
|
||||
Expenses ||
|
||||
------------++------------
|
||||
|
||||
<
|
||||
2021-01-01 Post in X
|
||||
(f) 1 X
|
||||
|
||||
2021-01-01 Post in X
|
||||
(b) -1 X
|
||||
|
||||
2021-01-01 Post in X
|
||||
(e) 1 X
|
||||
(e) -1 Y
|
||||
|
||||
2021-01-01 Post in Y
|
||||
(d) -1 Y
|
||||
|
||||
2021-01-01 Post in X
|
||||
(c) -1 X
|
||||
|
||||
2021-01-01 Post in X
|
||||
(a) -1 X
|
||||
|
||||
# 15. When sorting by amount with different commodities, missing commodities are
|
||||
# treated as 0, so negative amounts go before positive amounts. (#1563)
|
||||
#
|
||||
# Explanation: We treats these amounts as
|
||||
# 1 X, 0 Y f
|
||||
# 1 X, -1 Y e
|
||||
# 0 X, -1 Y d
|
||||
# -1 X, 0 Y a
|
||||
# -1 X, 0 Y b
|
||||
# -1 X, 0 Y c
|
||||
# X is alphabetically before Y, so the X amount determines the order.
|
||||
# If X amounts are equal, the Y amount will decide, and so on.
|
||||
# If all commodity amounts are equal (a, b, and c above), their sort order is
|
||||
# determined by their account name.
|
||||
$ hledger -f- bal -N -S
|
||||
1 X f
|
||||
1 X
|
||||
-1 Y e
|
||||
-1 Y d
|
||||
-1 X a
|
||||
-1 X b
|
||||
-1 X c
|
||||
|
||||
Loading…
Reference in New Issue
Block a user