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 |   -- * MixedAmount | ||||||
|   nullmixedamt, |   nullmixedamt, | ||||||
|   missingmixedamt, |   missingmixedamt, | ||||||
|  |   isMissingMixedAmount, | ||||||
|   mixed, |   mixed, | ||||||
|   mixedAmount, |   mixedAmount, | ||||||
|   maAddAmount, |   maAddAmount, | ||||||
| @ -540,6 +541,10 @@ nullmixedamt = Mixed mempty | |||||||
| missingmixedamt :: MixedAmount | missingmixedamt :: MixedAmount | ||||||
| missingmixedamt = mixedAmount missingamt | 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. | -- | Convert amounts in various commodities into a mixed amount. | ||||||
| mixed :: Foldable t => t Amount -> MixedAmount | mixed :: Foldable t => t Amount -> MixedAmount | ||||||
| mixed = maAddAmounts nullmixedamt | mixed = maAddAmounts nullmixedamt | ||||||
| @ -637,13 +642,12 @@ maIsNonZero = not . mixedAmountIsZero | |||||||
| -- | -- | ||||||
| amounts :: MixedAmount -> [Amount] | amounts :: MixedAmount -> [Amount] | ||||||
| amounts (Mixed ma) | 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] |   | 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 | ||||||
|     missingkey = amountKey missingamt |  | ||||||
| 
 | 
 | ||||||
| -- | 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 | ||||||
| @ -966,7 +970,14 @@ tests_Amount = tests "Amount" [ | |||||||
| 
 | 
 | ||||||
|   ,tests "MixedAmount" [ |   ,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 |       maSum (map mixedAmount | ||||||
|         [usd 1.25 |         [usd 1.25 | ||||||
|         ,usd (-1) `withPrecision` Precision 3 |         ,usd (-1) `withPrecision` Precision 3 | ||||||
|  | |||||||
| @ -183,7 +183,7 @@ isBalancedVirtual :: Posting -> Bool | |||||||
| isBalancedVirtual p = ptype p == BalancedVirtualPosting | isBalancedVirtual p = ptype p == BalancedVirtualPosting | ||||||
| 
 | 
 | ||||||
| hasAmount :: Posting -> Bool | hasAmount :: Posting -> Bool | ||||||
| hasAmount = (/= missingmixedamt) . pamount | hasAmount = not . isMissingMixedAmount . pamount | ||||||
| 
 | 
 | ||||||
| hasBalanceAssignment :: Posting -> Bool | hasBalanceAssignment :: Posting -> Bool | ||||||
| hasBalanceAssignment p = not (hasAmount p) && isJust (pbalanceassertion p) | hasBalanceAssignment p = not (hasAmount p) && isJust (pbalanceassertion p) | ||||||
|  | |||||||
| @ -27,8 +27,8 @@ module Hledger.Data.Types | |||||||
| where | where | ||||||
| 
 | 
 | ||||||
| import GHC.Generics (Generic) | import GHC.Generics (Generic) | ||||||
| import Data.Decimal | import Data.Decimal (Decimal) | ||||||
| import Data.Default | import Data.Default (Default(..)) | ||||||
| import Data.Functor (($>)) | import Data.Functor (($>)) | ||||||
| import Data.List (intercalate) | import Data.List (intercalate) | ||||||
| import Text.Blaze (ToMarkup(..)) | import Text.Blaze (ToMarkup(..)) | ||||||
| @ -230,7 +230,27 @@ data Amount = Amount { | |||||||
|       aprice      :: !(Maybe AmountPrice)  -- ^ the (fixed, transaction-specific) price for this amount, if any |       aprice      :: !(Maybe AmountPrice)  -- ^ the (fixed, transaction-specific) price for this amount, if any | ||||||
|     } deriving (Eq,Ord,Generic,Show) |     } 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 | -- | Stores the CommoditySymbol of the Amount, along with the CommoditySymbol of | ||||||
| -- the price, and its unit price if being used. | -- 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. | 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. | 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. | 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. | To work around this, you can add `--invert` to flip the signs. | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ | |||||||
| 2018/1/1 | 2018/1/1 | ||||||
|   (a:k)  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). | # Missing parent accounts are added (b). | ||||||
| $ hledger -f- bal -N --tree | $ hledger -f- bal -N --tree | ||||||
|                    1  a:k |                    1  a:k | ||||||
| @ -24,7 +24,7 @@ $ hledger -f- bal -N --tree | |||||||
|                    1    j |                    1    j | ||||||
|                    1  c |                    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 | $ hledger -f- bal -N --flat | ||||||
|                    1  a:k |                    1  a:k | ||||||
|                    1  b:i |                    1  b:i | ||||||
| @ -56,7 +56,7 @@ account d | |||||||
| 2018/1/1 | 2018/1/1 | ||||||
|   (d)    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,  | # declared accounts are sorted first in declaration order,  | ||||||
| # followed by undeclared accounts sorted alphabetically. | # followed by undeclared accounts sorted alphabetically. | ||||||
| # Missing parent accounts are added (b). | # Missing parent accounts are added (b). | ||||||
| @ -70,7 +70,7 @@ $ hledger -f- bal -N --tree | |||||||
|                    1    i |                    1    i | ||||||
|                    1  c |                    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 | $ hledger -f- bal -N --flat | ||||||
|                    1  d |                    1  d | ||||||
|                    1  a:l |                    1  a:l | ||||||
| @ -81,7 +81,7 @@ $ hledger -f- bal -N --flat | |||||||
| 
 | 
 | ||||||
| # ** Sort by amount | # ** 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 | 2018/1/1 | ||||||
|   (b:j)  2 |   (b:j)  2 | ||||||
| @ -101,7 +101,7 @@ $ hledger -f- bal -N -S --flat | |||||||
|                    1  b:i |                    1  b:i | ||||||
|                    1  c |                    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 | $ hledger -f- bal -N -S --tree | ||||||
|                    3  b |                    3  b | ||||||
|                    2    j |                    2    j | ||||||
| @ -109,8 +109,8 @@ $ hledger -f- bal -N -S --tree | |||||||
|                    1  a:k |                    1  a:k | ||||||
|                    1  c |                    1  c | ||||||
| 
 | 
 | ||||||
| # In hledger 1.4-1.10, when the larger amount was composed of differently-priced amounts, | # 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: | # it could get sorted as if smaller. Should work now. Test tree mode. | ||||||
| < | < | ||||||
| 2018/1/1 | 2018/1/1 | ||||||
|   (a)  2X @ 1Y |   (a)  2X @ 1Y | ||||||
| @ -119,11 +119,16 @@ $ hledger -f- bal -N -S --tree | |||||||
| 2018/1/1 | 2018/1/1 | ||||||
|   (b)  3X |   (b)  3X | ||||||
| 
 | 
 | ||||||
| $ hledger -f- bal -N -S | $ hledger -f- bal -N -S --tree | ||||||
|                   4X  a |                   4X  a | ||||||
|                   3X  b |                   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 | 2020-01-01 | ||||||
|     (a:aa)                                         1 |     (a:aa)                                         1 | ||||||
| @ -136,19 +141,19 @@ $ hledger -f- bal -N -S --tree | |||||||
|                    1    aa |                    1    aa | ||||||
|                    2  b |                    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 | $ hledger -f- bal -N -S -H --flat | ||||||
|                    3  a:ab |                    3  a:ab | ||||||
|                    2  b |                    2  b | ||||||
|                    1  a:aa |                    1  a:aa | ||||||
| 
 | 
 | ||||||
| # #1287 and bal -S --cumulative: | # 11. #1287 and bal -S --cumulative: | ||||||
| $ hledger -f- bal -N -S --cumulative --flat | $ hledger -f- bal -N -S --cumulative --flat | ||||||
|                    3  a:ab |                    3  a:ab | ||||||
|                    2  b |                    2  b | ||||||
|                    1  a:aa |                    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 | 2020-01-01 | ||||||
|   (revenues:a)  -1 |   (revenues:a)  -1 | ||||||
| @ -160,13 +165,13 @@ $ hledger -f- bal -N -S | |||||||
|                   -2  revenues:c |                   -2  revenues:c | ||||||
|                   -3  revenues:b |                   -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 | $ hledger -f- bal -N -S --invert | ||||||
|                    3  revenues:b |                    3  revenues:b | ||||||
|                    2  revenues:c |                    2  revenues:c | ||||||
|                    1  revenues:a |                    1  revenues:a | ||||||
| 
 | 
 | ||||||
| # Or a sign-flipping command like incomestatement: | # 14. Or a sign-flipping command like incomestatement: | ||||||
| $ hledger -f- is -N -S | $ hledger -f- is -N -S | ||||||
| Income Statement 2020-01-01 | Income Statement 2020-01-01 | ||||||
| 
 | 
 | ||||||
| @ -180,3 +185,46 @@ Income Statement 2020-01-01 | |||||||
| ============++============ | ============++============ | ||||||
|  Expenses   ||             |  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