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,
amountIsZero,
amountLooksZero,
amountAndPriceIsZero,
amountAndPriceLooksZero,
divideAmount,
multiplyAmount,
divideAmountAndPrice,
multiplyAmountAndPrice,
amountTotalPriceToUnitPrice,
-- ** rendering
AmountDisplayOpts(..),
@ -110,15 +106,11 @@ module Hledger.Data.Amount (
mixedAmountCost,
divideMixedAmount,
multiplyMixedAmount,
divideMixedAmountAndPrice,
multiplyMixedAmountAndPrice,
averageMixedAmounts,
isNegativeAmount,
isNegativeMixedAmount,
mixedAmountIsZero,
mixedAmountLooksZero,
mixedAmountAndPriceIsZero,
mixedAmountAndPriceLooksZero,
mixedAmountTotalPriceToUnitPrice,
-- ** rendering
styleMixedAmount,
@ -212,7 +204,7 @@ instance Num Amount where
abs a@Amount{aquantity=q} = a{aquantity=abs q}
signum a@Amount{aquantity=q} = a{aquantity=signum q}
fromInteger i = nullamt{aquantity=fromInteger i}
negate a = transformAmountAndPrice negate a
negate a = transformAmount negate a
(+) = similarAmountsOp (+)
(-) = similarAmountsOp (-)
(*) = similarAmountsOp (*)
@ -288,28 +280,20 @@ amountTotalPriceToUnitPrice
Precision p -> Precision $ if p == maxBound then maxBound else p + 1
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).
transformAmountAndPrice :: (Quantity -> Quantity) -> Amount -> Amount
transformAmountAndPrice f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p}
transformAmount :: (Quantity -> Quantity) -> Amount -> Amount
transformAmount f a@Amount{aquantity=q,aprice=p} = a{aquantity=f q, aprice=f' <$> p}
where
f' (TotalPrice a@Amount{aquantity=pq}) = TotalPrice a{aquantity = f pq}
f' p = p
-- | Divide an amount's quantity (and its total price, if it has one) by a constant.
divideAmountAndPrice :: Quantity -> Amount -> Amount
divideAmountAndPrice n = transformAmountAndPrice (/n)
divideAmount :: Quantity -> Amount -> Amount
divideAmount n = transformAmount (/n)
-- | Multiply an amount's quantity (and its total price, if it has one) by a constant.
multiplyAmountAndPrice :: Quantity -> Amount -> Amount
multiplyAmountAndPrice n = transformAmountAndPrice (*n)
multiplyAmount :: Quantity -> Amount -> Amount
multiplyAmount n = transformAmount (*n)
-- | Is this amount negative ? The price is ignored.
isNegativeAmount :: Amount -> Bool
@ -322,31 +306,20 @@ amountRoundedQuantity Amount{aquantity=q, astyle=AmountStyle{asprecision=p}} = c
NaturalPrecision -> 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 ?
amountLooksZero :: Amount -> Bool
amountLooksZero = (0==) . amountRoundedQuantity
amountLooksZero = testAmountAndTotalPrice ((0==) . amountRoundedQuantity)
-- | Does mixed amount and its price appear to be zero when rendered with its
-- 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 ?
-- | Is this Amount (and its total price, if it has one) exactly zero, ignoring its display precision ?
amountIsZero :: Amount -> Bool
amountIsZero Amount{aquantity=q} = q == 0
-- | 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
amountIsZero = testAmountAndTotalPrice ((0==) . aquantity)
-- | Set an amount's display precision, flipped.
withPrecision :: Amount -> AmountPrecision -> Amount
@ -563,7 +536,7 @@ normaliseHelper squashprices (Mixed as)
| otherwise = Mixed $ toList nonzeros
where
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
key Amount{acommodity=c,aprice=p} = (c, if squashprices then Nothing else priceKey <$> p)
where
@ -636,24 +609,14 @@ mapMixedAmount f (Mixed as) = Mixed $ map f as
mixedAmountCost :: MixedAmount -> MixedAmount
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 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 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.
averageMixedAmounts :: [MixedAmount] -> MixedAmount
averageMixedAmounts [] = 0
@ -670,24 +633,18 @@ isNegativeMixedAmount m =
as | not (any isNegativeAmount as) -> Just False
_ -> Nothing -- multiple amounts with different signs
-- | Does this mixed amount appear to be zero when rendered with its
-- display precision ?
-- | Does this mixed amount appear to be zero when rendered with its 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 = all amountLooksZero . amounts . normaliseMixedAmountSquashPricesForDisplay
-- | Does this mixed amount and its price appear to be zero when rendered with its
-- display precision ?
mixedAmountAndPriceLooksZero :: MixedAmount -> Bool
mixedAmountAndPriceLooksZero = all amountAndPriceLooksZero . amounts . normaliseMixedAmountSquashPricesForDisplay
-- | Is this mixed amount exactly zero, ignoring display precisions ?
-- | Is this mixed amount exactly to be zero, ignoring its 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?
mixedAmountIsZero :: MixedAmount -> Bool
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
-- -- 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.
@ -767,10 +724,11 @@ showMixedAmountDebug m | m == missingmixedamt = "(missing)"
-- maximum width will be elided.
showMixedAmountB :: AmountDisplayOpts -> MixedAmount -> WideBuilder
showMixedAmountB opts ma
| displayOneLine opts = showMixedAmountOneLineB opts ma
| displayOneLine opts = showMixedAmountOneLineB opts ma'
| otherwise = WideBuilder (wbBuilder . mconcat $ intersperse sep lines) width
where
lines = showMixedAmountLinesB opts ma
ma' = if displayPrice opts then ma else mixedAmountStripPrices ma
lines = showMixedAmountLinesB opts ma'
width = headDef 0 $ map wbWidth lines
sep = WideBuilder (TB.singleton '\n') 0

View File

@ -257,7 +257,7 @@ isPostingInDateSpan' PrimaryDate s = spanContainsDate s . postingDate
isPostingInDateSpan' SecondaryDate s = spanContainsDate s . postingDate2
isEmptyPosting :: Posting -> Bool
isEmptyPosting = mixedAmountAndPriceLooksZero . pamount
isEmptyPosting = mixedAmountLooksZero . pamount
-- 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
canonicalise = maybe id canonicaliseMixedAmount mstyles
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
-> length (nubSort $ mapMaybe isNegativeMixedAmount nonzeros) > 1
_ -> True
@ -378,7 +378,7 @@ transactionCheckBalanced mstyles t = errs
(rsum, bvsum) = (sumPostings rps, sumPostings bvps)
(rsumcost, bvsumcost) = (mixedAmountCost rsum, mixedAmountCost bvsum)
(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
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
-- Mixed as = dbg6 "multipliedamount" $ n `multiplyMixedAmount` mixedAmountTotalPriceToUnitPrice matchedamount
-- 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
case acommodity pramount of
"" -> Mixed as