lib: (amount|mixedAmount)(Looks|Is)Zero functions now check whether

both the quantity and the cost are zero. This is usually what you want,
but if you do only want to check whether the quantity is zero, you
can run mixedAmountStripPrices (or similar) before this.

(multiply|divide)(Mixed)?Amount now also multiply or divide the
TotalPrice if it is present, and the old
(multiply|divide)(Mixed)?AmountAndPrice functions are removed.
This commit is contained in:
Stephen Morgan 2021-02-20 10:06:51 +11:00 committed by Simon Michael
parent 9d527a9926
commit f0655d1c7f
4 changed files with 33 additions and 75 deletions

View File

@ -64,12 +64,8 @@ module Hledger.Data.Amount (
amountCost, amountCost,
amountIsZero, amountIsZero,
amountLooksZero, amountLooksZero,
amountAndPriceIsZero,
amountAndPriceLooksZero,
divideAmount, divideAmount,
multiplyAmount, multiplyAmount,
divideAmountAndPrice,
multiplyAmountAndPrice,
amountTotalPriceToUnitPrice, amountTotalPriceToUnitPrice,
-- ** rendering -- ** rendering
AmountDisplayOpts(..), AmountDisplayOpts(..),
@ -110,15 +106,11 @@ module Hledger.Data.Amount (
mixedAmountCost, mixedAmountCost,
divideMixedAmount, divideMixedAmount,
multiplyMixedAmount, multiplyMixedAmount,
divideMixedAmountAndPrice,
multiplyMixedAmountAndPrice,
averageMixedAmounts, averageMixedAmounts,
isNegativeAmount, isNegativeAmount,
isNegativeMixedAmount, isNegativeMixedAmount,
mixedAmountIsZero, mixedAmountIsZero,
mixedAmountLooksZero, mixedAmountLooksZero,
mixedAmountAndPriceIsZero,
mixedAmountAndPriceLooksZero,
mixedAmountTotalPriceToUnitPrice, mixedAmountTotalPriceToUnitPrice,
-- ** rendering -- ** rendering
styleMixedAmount, styleMixedAmount,
@ -212,7 +204,7 @@ instance Num Amount where
abs a@Amount{aquantity=q} = a{aquantity=abs q} abs a@Amount{aquantity=q} = a{aquantity=abs q}
signum a@Amount{aquantity=q} = a{aquantity=signum q} signum a@Amount{aquantity=q} = a{aquantity=signum q}
fromInteger i = nullamt{aquantity=fromInteger i} fromInteger i = nullamt{aquantity=fromInteger i}
negate a = transformAmountAndPrice negate a negate a = transformAmount negate a
(+) = similarAmountsOp (+) (+) = similarAmountsOp (+)
(-) = similarAmountsOp (-) (-) = similarAmountsOp (-)
(*) = similarAmountsOp (*) (*) = similarAmountsOp (*)
@ -288,28 +280,20 @@ amountTotalPriceToUnitPrice
Precision p -> Precision $ if p == maxBound then maxBound else p + 1 Precision p -> Precision $ if p == maxBound then maxBound else p + 1
amountTotalPriceToUnitPrice a = a amountTotalPriceToUnitPrice a = a
-- | Divide an amount's quantity by a constant.
divideAmount :: Quantity -> Amount -> Amount
divideAmount n a@Amount{aquantity=q} = a{aquantity=q/n}
-- | Multiply an amount's quantity by a constant.
multiplyAmount :: Quantity -> Amount -> Amount
multiplyAmount n a@Amount{aquantity=q} = a{aquantity=q*n}
-- | Apply a function to an amount's quantity (and its total price, if it has one). -- | Apply a function to an amount's quantity (and its total price, if it has one).
transformAmountAndPrice :: (Quantity -> Quantity) -> Amount -> Amount transformAmount :: (Quantity -> Quantity) -> Amount -> Amount
transformAmountAndPrice f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p} transformAmount f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p}
where where
f' (TotalPrice a@Amount{aquantity=pq}) = TotalPrice a{aquantity = f pq} f' (TotalPrice a@Amount{aquantity=pq}) = TotalPrice a{aquantity = f pq}
f' p = p f' p = p
-- | Divide an amount's quantity (and its total price, if it has one) by a constant. -- | Divide an amount's quantity (and its total price, if it has one) by a constant.
divideAmountAndPrice :: Quantity -> Amount -> Amount divideAmount :: Quantity -> Amount -> Amount
divideAmountAndPrice n = transformAmountAndPrice (/n) divideAmount n = transformAmount (/n)
-- | Multiply an amount's quantity (and its total price, if it has one) by a constant. -- | Multiply an amount's quantity (and its total price, if it has one) by a constant.
multiplyAmountAndPrice :: Quantity -> Amount -> Amount multiplyAmount :: Quantity -> Amount -> Amount
multiplyAmountAndPrice n = transformAmountAndPrice (*n) multiplyAmount n = transformAmount (*n)
-- | Is this amount negative ? The price is ignored. -- | Is this amount negative ? The price is ignored.
isNegativeAmount :: Amount -> Bool isNegativeAmount :: Amount -> Bool
@ -322,31 +306,20 @@ amountRoundedQuantity Amount{aquantity=q, astyle=AmountStyle{asprecision=p}} = c
NaturalPrecision -> q NaturalPrecision -> q
Precision p' -> roundTo p' q Precision p' -> roundTo p' q
-- | Does mixed amount appear to be zero when rendered with its -- | Apply a test to both an Amount and its total price, if it has one.
testAmountAndTotalPrice :: (Amount -> Bool) -> Amount -> Bool
testAmountAndTotalPrice f amt = case aprice amt of
Just (TotalPrice price) -> f amt && f price
_ -> f amt
-- | Do this Amount and (and its total price, if it has one) appear to be zero when rendered with its
-- display precision ? -- display precision ?
amountLooksZero :: Amount -> Bool amountLooksZero :: Amount -> Bool
amountLooksZero = (0==) . amountRoundedQuantity amountLooksZero = testAmountAndTotalPrice ((0==) . amountRoundedQuantity)
-- | Does mixed amount and its price appear to be zero when rendered with its -- | Is this Amount (and its total price, if it has one) exactly zero, ignoring its display precision ?
-- display precision ?
amountAndPriceLooksZero :: Amount -> Bool
amountAndPriceLooksZero amt = amountLooksZero amt && priceLooksZero
where
priceLooksZero = case aprice amt of
Just (TotalPrice p) -> amountLooksZero p
_ -> True
-- | Is this amount exactly zero, ignoring its display precision ?
amountIsZero :: Amount -> Bool amountIsZero :: Amount -> Bool
amountIsZero Amount{aquantity=q} = q == 0 amountIsZero = testAmountAndTotalPrice ((0==) . aquantity)
-- | Are this amount and its price exactly zero, ignoring its display precision ?
amountAndPriceIsZero :: Amount -> Bool
amountAndPriceIsZero amt@Amount{aquantity=q} = q == 0 && priceIsZero
where
priceIsZero = case aprice amt of
Just (TotalPrice p) -> amountIsZero p
_ -> True
-- | Set an amount's display precision, flipped. -- | Set an amount's display precision, flipped.
withPrecision :: Amount -> AmountPrecision -> Amount withPrecision :: Amount -> AmountPrecision -> Amount
@ -563,7 +536,7 @@ normaliseHelper squashprices (Mixed as)
| otherwise = Mixed $ toList nonzeros | otherwise = Mixed $ toList nonzeros
where where
newzero = maybe nullamt snd . M.lookupMin $ M.filter (not . T.null . acommodity) zeros newzero = maybe nullamt snd . M.lookupMin $ M.filter (not . T.null . acommodity) zeros
(zeros, nonzeros) = M.partition amountAndPriceIsZero amtMap (zeros, nonzeros) = M.partition amountIsZero amtMap
amtMap = foldr (\a -> M.insertWith sumSimilarAmountsUsingFirstPrice (key a) a) mempty as amtMap = foldr (\a -> M.insertWith sumSimilarAmountsUsingFirstPrice (key a) a) mempty as
key Amount{acommodity=c,aprice=p} = (c, if squashprices then Nothing else priceKey <$> p) key Amount{acommodity=c,aprice=p} = (c, if squashprices then Nothing else priceKey <$> p)
where where
@ -636,24 +609,14 @@ mapMixedAmount f (Mixed as) = Mixed $ map f as
mixedAmountCost :: MixedAmount -> MixedAmount mixedAmountCost :: MixedAmount -> MixedAmount
mixedAmountCost = mapMixedAmount amountCost mixedAmountCost = mapMixedAmount amountCost
-- | Divide a mixed amount's quantities by a constant. -- | Divide a mixed amount's quantities (and total prices, if any) by a constant.
divideMixedAmount :: Quantity -> MixedAmount -> MixedAmount divideMixedAmount :: Quantity -> MixedAmount -> MixedAmount
divideMixedAmount n = mapMixedAmount (divideAmount n) divideMixedAmount n = mapMixedAmount (divideAmount n)
-- | Multiply a mixed amount's quantities by a constant. -- | Multiply a mixed amount's quantities (and total prices, if any) by a constant.
multiplyMixedAmount :: Quantity -> MixedAmount -> MixedAmount multiplyMixedAmount :: Quantity -> MixedAmount -> MixedAmount
multiplyMixedAmount n = mapMixedAmount (multiplyAmount n) multiplyMixedAmount n = mapMixedAmount (multiplyAmount n)
-- | Divide a mixed amount's quantities (and total prices, if any) by a constant.
-- The total prices will be kept positive regardless of the multiplier's sign.
divideMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount
divideMixedAmountAndPrice n = mapMixedAmount (divideAmountAndPrice n)
-- | Multiply a mixed amount's quantities (and total prices, if any) by a constant.
-- The total prices will be kept positive regardless of the multiplier's sign.
multiplyMixedAmountAndPrice :: Quantity -> MixedAmount -> MixedAmount
multiplyMixedAmountAndPrice n = mapMixedAmount (multiplyAmountAndPrice n)
-- | Calculate the average of some mixed amounts. -- | Calculate the average of some mixed amounts.
averageMixedAmounts :: [MixedAmount] -> MixedAmount averageMixedAmounts :: [MixedAmount] -> MixedAmount
averageMixedAmounts [] = 0 averageMixedAmounts [] = 0
@ -670,24 +633,18 @@ isNegativeMixedAmount m =
as | not (any isNegativeAmount as) -> Just False as | not (any isNegativeAmount as) -> Just False
_ -> Nothing -- multiple amounts with different signs _ -> Nothing -- multiple amounts with different signs
-- | Does this mixed amount appear to be zero when rendered with its -- | Does this mixed amount appear to be zero when rendered with its display precision?
-- display precision ? -- i.e. does it have zero quantity with no price, zero quantity with a total price (which is also zero),
-- and zero quantity for each unit price?
mixedAmountLooksZero :: MixedAmount -> Bool mixedAmountLooksZero :: MixedAmount -> Bool
mixedAmountLooksZero = all amountLooksZero . amounts . normaliseMixedAmountSquashPricesForDisplay mixedAmountLooksZero = all amountLooksZero . amounts . normaliseMixedAmountSquashPricesForDisplay
-- | Does this mixed amount and its price appear to be zero when rendered with its -- | Is this mixed amount exactly to be zero, ignoring its display precision?
-- display precision ? -- i.e. does it have zero quantity with no price, zero quantity with a total price (which is also zero),
mixedAmountAndPriceLooksZero :: MixedAmount -> Bool -- and zero quantity for each unit price?
mixedAmountAndPriceLooksZero = all amountAndPriceLooksZero . amounts . normaliseMixedAmountSquashPricesForDisplay
-- | Is this mixed amount exactly zero, ignoring display precisions ?
mixedAmountIsZero :: MixedAmount -> Bool mixedAmountIsZero :: MixedAmount -> Bool
mixedAmountIsZero = all amountIsZero . amounts . normaliseMixedAmountSquashPricesForDisplay mixedAmountIsZero = all amountIsZero . amounts . normaliseMixedAmountSquashPricesForDisplay
-- | Is this mixed amount exactly zero, ignoring display precisions ?
mixedAmountAndPriceIsZero :: MixedAmount -> Bool
mixedAmountAndPriceIsZero = all amountAndPriceIsZero . amounts . normaliseMixedAmountSquashPricesForDisplay
-- -- | MixedAmount derived Eq instance in Types.hs doesn't know that we -- -- | MixedAmount derived Eq instance in Types.hs doesn't know that we
-- -- want $0 = EUR0 = 0. Yet we don't want to drag all this code over there. -- -- want $0 = EUR0 = 0. Yet we don't want to drag all this code over there.
-- -- For now, use this when cross-commodity zero equality is important. -- -- For now, use this when cross-commodity zero equality is important.
@ -767,10 +724,11 @@ showMixedAmountDebug m | m == missingmixedamt = "(missing)"
-- maximum width will be elided. -- maximum width will be elided.
showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder
showMixedAmountB opts ma showMixedAmountB opts ma
| displayOneLine opts = showMixedAmountOneLineB opts ma | displayOneLine opts = showMixedAmountOneLineB opts ma'
| otherwise = WideBuilder (wbBuilder . mconcat $ intersperse sep lines) width | otherwise = WideBuilder (wbBuilder . mconcat $ intersperse sep lines) width
where where
lines = showMixedAmountLinesB opts ma ma' = if displayPrice opts then ma else mixedAmountStripPrices ma
lines = showMixedAmountLinesB opts ma'
width = headDef 0 $ map wbWidth lines width = headDef 0 $ map wbWidth lines
sep = WideBuilder (TB.singleton '\n') 0 sep = WideBuilder (TB.singleton '\n') 0

View File

@ -257,7 +257,7 @@ isPostingInDateSpan' PrimaryDate s = spanContainsDate s . postingDate
isPostingInDateSpan' SecondaryDate s = spanContainsDate s . postingDate2 isPostingInDateSpan' SecondaryDate s = spanContainsDate s . postingDate2
isEmptyPosting :: Posting -> Bool isEmptyPosting :: Posting -> Bool
isEmptyPosting = mixedAmountAndPriceLooksZero . pamount isEmptyPosting = mixedAmountLooksZero . pamount
-- AccountName stuff that depends on PostingType -- AccountName stuff that depends on PostingType

View File

@ -368,7 +368,7 @@ transactionCheckBalanced mstyles t = errs
-- check for mixed signs, detecting nonzeros at display precision -- check for mixed signs, detecting nonzeros at display precision
canonicalise = maybe id canonicaliseMixedAmount mstyles canonicalise = maybe id canonicaliseMixedAmount mstyles
signsOk ps = signsOk ps =
case filter (not.mixedAmountAndPriceLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of case filter (not.mixedAmountLooksZero) $ map (canonicalise.mixedAmountCost.pamount) ps of
nonzeros | length nonzeros >= 2 nonzeros | length nonzeros >= 2
-> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1 -> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1
_ -> True _ -> True
@ -378,7 +378,7 @@ transactionCheckBalanced mstyles t = errs
(rsum, bvsum) = (sumPostings rps, sumPostings bvps) (rsum, bvsum) = (sumPostings rps, sumPostings bvps)
(rsumcost, bvsumcost) = (mixedAmountCost rsum, mixedAmountCost bvsum) (rsumcost, bvsumcost) = (mixedAmountCost rsum, mixedAmountCost bvsum)
(rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost) (rsumdisplay, bvsumdisplay) = (canonicalise rsumcost, canonicalise bvsumcost)
(rsumok, bvsumok) = (mixedAmountAndPriceLooksZero rsumdisplay, mixedAmountAndPriceLooksZero bvsumdisplay) (rsumok, bvsumok) = (mixedAmountLooksZero rsumdisplay, mixedAmountLooksZero bvsumdisplay)
-- generate error messages, showing amounts with their original precision -- generate error messages, showing amounts with their original precision
errs = filter (not.null) [rmsg, bvmsg] errs = filter (not.null) [rmsg, bvmsg]

View File

@ -120,7 +120,7 @@ tmPostingRuleToFunction querytxt pr =
-- Approach 1: convert to a unit price and increase the display precision slightly -- Approach 1: convert to a unit price and increase the display precision slightly
-- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount -- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount
-- Approach 2: multiply the total price (keeping it positive) as well as the quantity -- Approach 2: multiply the total price (keeping it positive) as well as the quantity
Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmountAndPrice` matchedamount Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` matchedamount
in in
case acommodity pramount of case acommodity pramount of
"" -> Mixed as "" -> Mixed as