dev:print: refactor, add AmountStyle "rounding strategy"
Changes to enable more control of "rounding" behaviour (ie, choosing display precisions for amounts). This reverts 1.31's change of asprecision, making it a non-Maybe again, and adds a new asrounding field providing more control over how a target display precision is applied to existing amounts (two options for now, more later). Functionality is in an interim state (reports do no rounding).
This commit is contained in:
parent
a0136a0b28
commit
f8027abb44
@ -79,7 +79,7 @@ module Hledger.Data.Amount (
|
|||||||
amountSetStylesExceptPrecision,
|
amountSetStylesExceptPrecision,
|
||||||
amountSetMainStyle,
|
amountSetMainStyle,
|
||||||
amountSetCostStyle,
|
amountSetCostStyle,
|
||||||
amountStyleUnsetPrecision,
|
amountStyleSetRounding,
|
||||||
amountUnstyled,
|
amountUnstyled,
|
||||||
showAmountB,
|
showAmountB,
|
||||||
showAmount,
|
showAmount,
|
||||||
@ -91,6 +91,7 @@ module Hledger.Data.Amount (
|
|||||||
amountSetPrecision,
|
amountSetPrecision,
|
||||||
withPrecision,
|
withPrecision,
|
||||||
amountSetFullPrecision,
|
amountSetFullPrecision,
|
||||||
|
-- amountInternalPrecision,
|
||||||
setAmountInternalPrecision,
|
setAmountInternalPrecision,
|
||||||
withInternalPrecision,
|
withInternalPrecision,
|
||||||
setAmountDecimalPoint,
|
setAmountDecimalPoint,
|
||||||
@ -203,7 +204,7 @@ quoteCommoditySymbolIfNeeded s
|
|||||||
|
|
||||||
|
|
||||||
-- | Options for the display of Amount and MixedAmount.
|
-- | Options for the display of Amount and MixedAmount.
|
||||||
-- (See also Types.AmountStyle)
|
-- (ee also Types.AmountStyle.
|
||||||
data AmountDisplayOpts = AmountDisplayOpts
|
data AmountDisplayOpts = AmountDisplayOpts
|
||||||
{ displayPrice :: Bool -- ^ Whether to display the Price of an Amount.
|
{ displayPrice :: Bool -- ^ Whether to display the Price of an Amount.
|
||||||
, displayZeroCommodity :: Bool -- ^ If the Amount rounds to 0, whether to display its commodity string.
|
, displayZeroCommodity :: Bool -- ^ If the Amount rounds to 0, whether to display its commodity string.
|
||||||
@ -248,7 +249,7 @@ csvDisplay = oneLine{displayThousandsSep=False}
|
|||||||
-- Amount styles
|
-- Amount styles
|
||||||
|
|
||||||
-- | Default amount style
|
-- | Default amount style
|
||||||
amountstyle = AmountStyle L False Nothing (Just '.') (Just $ Precision 0)
|
amountstyle = AmountStyle L False Nothing (Just '.') (Precision 0) NoRounding
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
-- Amount
|
-- Amount
|
||||||
@ -279,11 +280,11 @@ missingamt = nullamt{acommodity="AUTO"}
|
|||||||
-- usd/eur/gbp round their argument to a whole number of pennies/cents.
|
-- usd/eur/gbp round their argument to a whole number of pennies/cents.
|
||||||
-- XXX these are a bit clashy
|
-- XXX these are a bit clashy
|
||||||
num n = nullamt{acommodity="", aquantity=n}
|
num n = nullamt{acommodity="", aquantity=n}
|
||||||
hrs n = nullamt{acommodity="h", aquantity=n, astyle=amountstyle{asprecision=Just $ Precision 2, ascommodityside=R}}
|
hrs n = nullamt{acommodity="h", aquantity=n, astyle=amountstyle{asprecision=Precision 2, ascommodityside=R}}
|
||||||
usd n = nullamt{acommodity="$", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Just $ Precision 2}}
|
usd n = nullamt{acommodity="$", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}
|
||||||
eur n = nullamt{acommodity="€", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Just $ Precision 2}}
|
eur n = nullamt{acommodity="€", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}
|
||||||
gbp n = nullamt{acommodity="£", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Just $ Precision 2}}
|
gbp n = nullamt{acommodity="£", aquantity=roundTo 2 n, astyle=amountstyle{asprecision=Precision 2}}
|
||||||
per n = nullamt{acommodity="%", aquantity=n, astyle=amountstyle{asprecision=Just $ Precision 1, ascommodityside=R, ascommodityspaced=True}}
|
per n = nullamt{acommodity="%", aquantity=n, astyle=amountstyle{asprecision=Precision 1, ascommodityside=R, ascommodityspaced=True}}
|
||||||
amt `at` priceamt = amt{aprice=Just $ UnitPrice priceamt}
|
amt `at` priceamt = amt{aprice=Just $ UnitPrice priceamt}
|
||||||
amt @@ priceamt = amt{aprice=Just $ TotalPrice priceamt}
|
amt @@ priceamt = amt{aprice=Just $ TotalPrice priceamt}
|
||||||
|
|
||||||
@ -345,9 +346,8 @@ isNegativeAmount Amount{aquantity=q} = q < 0
|
|||||||
-- If that is unset or NaturalPrecision, this does nothing.
|
-- If that is unset or NaturalPrecision, this does nothing.
|
||||||
amountRoundedQuantity :: Amount -> Quantity
|
amountRoundedQuantity :: Amount -> Quantity
|
||||||
amountRoundedQuantity Amount{aquantity=q, astyle=AmountStyle{asprecision=mp}} = case mp of
|
amountRoundedQuantity Amount{aquantity=q, astyle=AmountStyle{asprecision=mp}} = case mp of
|
||||||
Nothing -> q
|
NaturalPrecision -> q
|
||||||
Just NaturalPrecision -> q
|
Precision p -> roundTo p q
|
||||||
Just (Precision p) -> roundTo p q
|
|
||||||
|
|
||||||
-- | Apply a test to both an Amount and its total price, if it has one.
|
-- | Apply a test to both an Amount and its total price, if it has one.
|
||||||
testAmountAndTotalPrice :: (Amount -> Bool) -> Amount -> Bool
|
testAmountAndTotalPrice :: (Amount -> Bool) -> Amount -> Bool
|
||||||
@ -363,9 +363,8 @@ amountLooksZero :: Amount -> Bool
|
|||||||
amountLooksZero = testAmountAndTotalPrice looksZero
|
amountLooksZero = testAmountAndTotalPrice looksZero
|
||||||
where
|
where
|
||||||
looksZero Amount{aquantity=Decimal e q, astyle=AmountStyle{asprecision=p}} = case p of
|
looksZero Amount{aquantity=Decimal e q, astyle=AmountStyle{asprecision=p}} = case p of
|
||||||
Just (Precision d) -> if e > d then abs q <= 5*10^(e-d-1) else q == 0
|
Precision d -> if e > d then abs q <= 5*10^(e-d-1) else q == 0
|
||||||
Just NaturalPrecision -> q == 0
|
NaturalPrecision -> q == 0
|
||||||
Nothing -> q == 0
|
|
||||||
|
|
||||||
-- | Is this Amount (and its total price, if it has one) 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 -> Bool
|
||||||
@ -377,7 +376,7 @@ withPrecision = flip amountSetPrecision
|
|||||||
|
|
||||||
-- | Set an amount's display precision.
|
-- | Set an amount's display precision.
|
||||||
amountSetPrecision :: AmountPrecision -> Amount -> Amount
|
amountSetPrecision :: AmountPrecision -> Amount -> Amount
|
||||||
amountSetPrecision p a@Amount{astyle=s} = a{astyle=s{asprecision=Just p}}
|
amountSetPrecision p a@Amount{astyle=s} = a{astyle=s{asprecision=p}}
|
||||||
|
|
||||||
-- | Increase an amount's display precision, if needed, to enough decimal places
|
-- | Increase an amount's display precision, if needed, to enough decimal places
|
||||||
-- to show it exactly (showing all significant decimal digits, without trailing zeros).
|
-- to show it exactly (showing all significant decimal digits, without trailing zeros).
|
||||||
@ -386,9 +385,13 @@ amountSetFullPrecision :: Amount -> Amount
|
|||||||
amountSetFullPrecision a = amountSetPrecision p a
|
amountSetFullPrecision a = amountSetPrecision p a
|
||||||
where
|
where
|
||||||
p = max displayprecision naturalprecision
|
p = max displayprecision naturalprecision
|
||||||
displayprecision = fromMaybe (Precision 0) $ asprecision $ astyle a
|
displayprecision = asprecision $ astyle a
|
||||||
naturalprecision = Precision . decimalPlaces . normalizeDecimal $ aquantity a
|
naturalprecision = Precision . decimalPlaces . normalizeDecimal $ aquantity a
|
||||||
|
|
||||||
|
-- -- | Get an amount's internal Decimal precision (not display precision).
|
||||||
|
-- amountInternalPrecision :: Amount -> Word8
|
||||||
|
-- amountInternalPrecision = decimalPlaces . normalizeDecimal . aquantity
|
||||||
|
|
||||||
-- | Set an amount's internal precision, ie rounds the Decimal representing
|
-- | Set an amount's internal precision, ie rounds the Decimal representing
|
||||||
-- the amount's quantity to some number of decimal places.
|
-- the amount's quantity to some number of decimal places.
|
||||||
-- Rounding is done with Data.Decimal's default roundTo function:
|
-- Rounding is done with Data.Decimal's default roundTo function:
|
||||||
@ -397,7 +400,7 @@ amountSetFullPrecision a = amountSetPrecision p a
|
|||||||
-- Intended mainly for internal use, eg when comparing amounts in tests.
|
-- Intended mainly for internal use, eg when comparing amounts in tests.
|
||||||
setAmountInternalPrecision :: Word8 -> Amount -> Amount
|
setAmountInternalPrecision :: Word8 -> Amount -> Amount
|
||||||
setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{
|
setAmountInternalPrecision p a@Amount{ aquantity=q, astyle=s } = a{
|
||||||
astyle=s{asprecision=Just $ Precision p}
|
astyle=s{asprecision=Precision p}
|
||||||
,aquantity=roundTo p q
|
,aquantity=roundTo p q
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,22 +471,49 @@ amountSetStylesExceptPrecision styles a@Amount{astyle=AmountStyle{asprecision=or
|
|||||||
case M.lookup (acommodity a) styles' of
|
case M.lookup (acommodity a) styles' of
|
||||||
Just s -> a{astyle=s{asprecision=origp}}
|
Just s -> a{astyle=s{asprecision=origp}}
|
||||||
Nothing -> a
|
Nothing -> a
|
||||||
where styles' = M.map amountStyleUnsetPrecision styles
|
where styles' = M.map (amountStyleSetRounding NoRounding) styles
|
||||||
|
|
||||||
amountStyleUnsetPrecision :: AmountStyle -> AmountStyle
|
amountStyleSetRounding :: Rounding -> AmountStyle -> AmountStyle
|
||||||
amountStyleUnsetPrecision as = as{asprecision=Nothing}
|
amountStyleSetRounding r as = as{asrounding=r}
|
||||||
|
|
||||||
-- | Find and apply the appropriate display style, if any, to this amount.
|
-- | Find and apply the appropriate display style, if any, to this amount.
|
||||||
-- The display precision is set or not, according to the style.
|
-- The display precision is adjusted or not, as determnined by the style's rounding strategy.
|
||||||
amountSetMainStyle :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount
|
amountSetMainStyle :: M.Map CommoditySymbol AmountStyle -> Amount -> Amount
|
||||||
amountSetMainStyle styles a@Amount{acommodity=comm, astyle=AmountStyle{asprecision=morigp}} =
|
amountSetMainStyle styles a@Amount{aquantity=q, acommodity=comm, astyle=s0} =
|
||||||
case M.lookup comm styles of
|
case M.lookup comm styles of
|
||||||
Nothing -> a
|
Nothing -> a
|
||||||
Just s@AmountStyle{asprecision=mp} -> a{astyle=s'}
|
Just s -> a{astyle=amountStyleApplyPrecision q s s0}
|
||||||
|
|
||||||
|
-- | A helper for updating an Amount's display precision, more carefully than amountSetPrecision.
|
||||||
|
-- Given an Amount's decimal quantity (for inspecting its internal representation),
|
||||||
|
-- its current display style, and a new display style,
|
||||||
|
-- apply the new style's display precision to the old style,
|
||||||
|
-- using the new style's rounding strategy, as follows:
|
||||||
|
--
|
||||||
|
-- NoRounding - the precision is left unchanged
|
||||||
|
--
|
||||||
|
-- SoftRounding -
|
||||||
|
--
|
||||||
|
-- if either precision is NaturalPrecision, use NaturalPrecision;
|
||||||
|
--
|
||||||
|
-- if the new precision is greater than the old, use the new (adds decimal zeros);
|
||||||
|
--
|
||||||
|
-- if the new precision is less than the old, use as close to the new as we can get
|
||||||
|
-- without dropping (more) non-zero digits (drops decimal zeros).
|
||||||
|
--
|
||||||
|
amountStyleApplyPrecision :: Quantity -> AmountStyle -> AmountStyle -> AmountStyle
|
||||||
|
amountStyleApplyPrecision q AmountStyle{asprecision=newp, asrounding=r} s@AmountStyle{asprecision=oldp} =
|
||||||
|
case r of
|
||||||
|
NoRounding -> s
|
||||||
|
SoftRounding -> s{asprecision=p}
|
||||||
where
|
where
|
||||||
s' = case mp of
|
p = case (newp, oldp) of
|
||||||
Nothing -> s{asprecision=morigp}
|
(Precision new, Precision old) ->
|
||||||
_ -> s
|
if new >= old
|
||||||
|
then Precision new
|
||||||
|
else Precision $ max (min old internal) new
|
||||||
|
where internal = decimalPlaces $ normalizeDecimal q
|
||||||
|
_ -> NaturalPrecision
|
||||||
|
|
||||||
-- | Find and apply the appropriate display style, if any, to this amount's cost, if any.
|
-- | Find and apply the appropriate display style, if any, to this amount's cost, if any.
|
||||||
-- The display precision is left unchanged, regardless of the style.
|
-- The display precision is left unchanged, regardless of the style.
|
||||||
@ -1120,8 +1150,8 @@ tests_Amount = testGroup "Amount" [
|
|||||||
(usd (-1.23) + usd (-1.23)) @?= usd (-2.46)
|
(usd (-1.23) + usd (-1.23)) @?= usd (-2.46)
|
||||||
sum [usd 1.23,usd (-1.23),usd (-1.23),-(usd (-1.23))] @?= usd 0
|
sum [usd 1.23,usd (-1.23),usd (-1.23),-(usd (-1.23))] @?= usd 0
|
||||||
-- highest precision is preserved
|
-- highest precision is preserved
|
||||||
asprecision (astyle $ sum [usd 1 `withPrecision` Precision 1, usd 1 `withPrecision` Precision 3]) @?= Just (Precision 3)
|
asprecision (astyle $ sum [usd 1 `withPrecision` Precision 1, usd 1 `withPrecision` Precision 3]) @?= Precision 3
|
||||||
asprecision (astyle $ sum [usd 1 `withPrecision` Precision 3, usd 1 `withPrecision` Precision 1]) @?= Just (Precision 3)
|
asprecision (astyle $ sum [usd 1 `withPrecision` Precision 3, usd 1 `withPrecision` Precision 1]) @?= Precision 3
|
||||||
-- adding different commodities assumes conversion rate 1
|
-- adding different commodities assumes conversion rate 1
|
||||||
assertBool "" $ amountLooksZero (usd 1.23 - eur 1.23)
|
assertBool "" $ amountLooksZero (usd 1.23 - eur 1.23)
|
||||||
|
|
||||||
|
|||||||
@ -337,8 +337,8 @@ costInferrerFor t pt = maybe id infercost inferFromAndTo
|
|||||||
|
|
||||||
unitprice = aquantity fromamount `divideAmount` toamount
|
unitprice = aquantity fromamount `divideAmount` toamount
|
||||||
unitprecision = case (asprecision $ astyle fromamount, asprecision $ astyle toamount) of
|
unitprecision = case (asprecision $ astyle fromamount, asprecision $ astyle toamount) of
|
||||||
(Just (Precision a), Just (Precision b)) -> Precision . max 2 $ saturatedAdd a b
|
(Precision a, Precision b) -> Precision . max 2 $ saturatedAdd a b
|
||||||
_ -> NaturalPrecision
|
_ -> NaturalPrecision
|
||||||
saturatedAdd a b = if maxBound - a < b then maxBound else a + b
|
saturatedAdd a b = if maxBound - a < b then maxBound else a + b
|
||||||
|
|
||||||
|
|
||||||
@ -1009,26 +1009,26 @@ tests_Balancing =
|
|||||||
--
|
--
|
||||||
testCase "1091a" $ do
|
testCase "1091a" $ do
|
||||||
commodityStylesFromAmounts [
|
commodityStylesFromAmounts [
|
||||||
nullamt{aquantity=1000, astyle=AmountStyle L False Nothing (Just ',') (Just $ Precision 3)}
|
nullamt{aquantity=1000, astyle=AmountStyle L False Nothing (Just ',') (Precision 3) NoRounding}
|
||||||
,nullamt{aquantity=1000, astyle=AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Just $ Precision 2)}
|
,nullamt{aquantity=1000, astyle=AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Precision 2) NoRounding}
|
||||||
]
|
]
|
||||||
@?=
|
@?=
|
||||||
-- The commodity style should have period as decimal mark
|
-- The commodity style should have period as decimal mark
|
||||||
-- and comma as digit group mark.
|
-- and comma as digit group mark.
|
||||||
Right (M.fromList [
|
Right (M.fromList [
|
||||||
("", AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Just $ Precision 3))
|
("", AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Precision 3) NoRounding)
|
||||||
])
|
])
|
||||||
-- same journal, entries in reverse order
|
-- same journal, entries in reverse order
|
||||||
,testCase "1091b" $ do
|
,testCase "1091b" $ do
|
||||||
commodityStylesFromAmounts [
|
commodityStylesFromAmounts [
|
||||||
nullamt{aquantity=1000, astyle=AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Just $ Precision 2)}
|
nullamt{aquantity=1000, astyle=AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Precision 2) NoRounding}
|
||||||
,nullamt{aquantity=1000, astyle=AmountStyle L False Nothing (Just ',') (Just $ Precision 3)}
|
,nullamt{aquantity=1000, astyle=AmountStyle L False Nothing (Just ',') (Precision 3) NoRounding}
|
||||||
]
|
]
|
||||||
@?=
|
@?=
|
||||||
-- The commodity style should have period as decimal mark
|
-- The commodity style should have period as decimal mark
|
||||||
-- and comma as digit group mark.
|
-- and comma as digit group mark.
|
||||||
Right (M.fromList [
|
Right (M.fromList [
|
||||||
("", AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Just $ Precision 3))
|
("", AmountStyle L False (Just (DigitGroups ',' [3])) (Just '.') (Precision 3) NoRounding)
|
||||||
])
|
])
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
@ -82,6 +82,7 @@ decimalKV d = let d' = if decimalPlaces d <= 10 then d else roundTo 10 d in
|
|||||||
]
|
]
|
||||||
|
|
||||||
instance ToJSON Amount
|
instance ToJSON Amount
|
||||||
|
instance ToJSON Rounding
|
||||||
instance ToJSON AmountStyle
|
instance ToJSON AmountStyle
|
||||||
|
|
||||||
-- Use the same JSON serialisation as Maybe Word8
|
-- Use the same JSON serialisation as Maybe Word8
|
||||||
@ -193,6 +194,7 @@ instance FromJSON Pos where
|
|||||||
parseJSON = fmap mkPos . parseJSON
|
parseJSON = fmap mkPos . parseJSON
|
||||||
|
|
||||||
instance FromJSON Amount
|
instance FromJSON Amount
|
||||||
|
instance FromJSON Rounding
|
||||||
instance FromJSON AmountStyle
|
instance FromJSON AmountStyle
|
||||||
|
|
||||||
-- Use the same JSON serialisation as Maybe Word8
|
-- Use the same JSON serialisation as Maybe Word8
|
||||||
|
|||||||
@ -378,11 +378,7 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
Nothing -> Left $ annotateWithPostings [p] "Conversion postings must have a single-commodity amount:"
|
Nothing -> Left $ annotateWithPostings [p] "Conversion postings must have a single-commodity amount:"
|
||||||
|
|
||||||
-- Do these amounts look the same when compared at the first's display precision ?
|
-- Do these amounts look the same when compared at the first's display precision ?
|
||||||
-- (Or if that's unset, compare as-is)
|
amountsMatch a b = amountLooksZero $ amountSetPrecision (asprecision $ astyle a) $ a - b
|
||||||
amountsMatch a b =
|
|
||||||
case asprecision $ astyle a of
|
|
||||||
Just p -> amountLooksZero $ amountSetPrecision p $ a - b
|
|
||||||
Nothing -> amountLooksZero $ a - b
|
|
||||||
|
|
||||||
-- Delete a posting from the indexed list of postings based on either its
|
-- Delete a posting from the indexed list of postings based on either its
|
||||||
-- index or its posting amount.
|
-- index or its posting amount.
|
||||||
@ -400,9 +396,8 @@ transactionInferCostsFromEquity dryrun acctTypes t = first (annotateErrorWithTra
|
|||||||
|
|
||||||
dbgShowAmountPrecision a =
|
dbgShowAmountPrecision a =
|
||||||
case asprecision $ astyle a of
|
case asprecision $ astyle a of
|
||||||
Just (Precision n) -> show n
|
Precision n -> show n
|
||||||
Just NaturalPrecision -> show $ decimalPlaces $ normalizeDecimal $ aquantity a
|
NaturalPrecision -> show $ decimalPlaces $ normalizeDecimal $ aquantity a
|
||||||
Nothing -> "unset"
|
|
||||||
|
|
||||||
-- Using the provided account types map, sort the given indexed postings
|
-- Using the provided account types map, sort the given indexed postings
|
||||||
-- into three lists of posting numbers (stored in two pairs), like so:
|
-- into three lists of posting numbers (stored in two pairs), like so:
|
||||||
|
|||||||
@ -249,16 +249,19 @@ deriving instance Generic (DecimalRaw a)
|
|||||||
data AmountPrice = UnitPrice !Amount | TotalPrice !Amount
|
data AmountPrice = UnitPrice !Amount | TotalPrice !Amount
|
||||||
deriving (Eq,Ord,Generic,Show)
|
deriving (Eq,Ord,Generic,Show)
|
||||||
|
|
||||||
-- | The display style for an amount.
|
-- | Every Amount has one of these, influencing how the amount is displayed.
|
||||||
-- (See also Amount.AmountDisplayOpts).
|
-- Also, each Commodity can have one, which can be applied to its amounts for consistent display.
|
||||||
|
-- See also Amount.AmountDisplayOpts.
|
||||||
data AmountStyle = AmountStyle {
|
data AmountStyle = AmountStyle {
|
||||||
ascommodityside :: !Side, -- ^ show the symbol on the left or the right ?
|
ascommodityside :: !Side, -- ^ show the symbol on the left or the right ?
|
||||||
ascommodityspaced :: !Bool, -- ^ show a space between symbol and quantity ?
|
ascommodityspaced :: !Bool, -- ^ show a space between symbol and quantity ?
|
||||||
asdigitgroups :: !(Maybe DigitGroupStyle), -- ^ show the integer part with these digit group marks, or not
|
asdigitgroups :: !(Maybe DigitGroupStyle), -- ^ show the integer part with these digit group marks, or not
|
||||||
asdecimalmark :: !(Maybe Char), -- ^ show this character (should be . or ,) as decimal mark, or use the default (.)
|
asdecimalmark :: !(Maybe Char), -- ^ show this character (should be . or ,) as decimal mark, or use the default (.)
|
||||||
asprecision :: !(Maybe AmountPrecision) -- ^ show this number of digits after the decimal point, or show as-is (leave precision unchanged)
|
asprecision :: !AmountPrecision, -- ^ "display precision" - show this number of digits after the decimal point
|
||||||
-- XXX Making asprecision a maybe simplifies code for styling with or without precision,
|
asrounding :: !Rounding -- ^ "rounding strategy" - kept here for convenience, for now:
|
||||||
-- but complicates the semantics (Nothing is useful only when setting style).
|
-- when displaying an amount, it is ignored,
|
||||||
|
-- but when applying this style to another amount, it determines
|
||||||
|
-- how hard we should try to adjust the amount's display precision.
|
||||||
} deriving (Eq,Ord,Read,Generic)
|
} deriving (Eq,Ord,Read,Generic)
|
||||||
|
|
||||||
instance Show AmountStyle where
|
instance Show AmountStyle where
|
||||||
@ -278,6 +281,16 @@ data AmountPrecision =
|
|||||||
| NaturalPrecision -- ^ show all significant decimal digits stored internally
|
| NaturalPrecision -- ^ show all significant decimal digits stored internally
|
||||||
deriving (Eq,Ord,Read,Show,Generic)
|
deriving (Eq,Ord,Read,Show,Generic)
|
||||||
|
|
||||||
|
-- | "Rounding strategy" - when applying the display precision from AmountStyle to another
|
||||||
|
-- (as when applying commodity styles to amounts), how much padding or rounding
|
||||||
|
-- of decimal digits should be done ?
|
||||||
|
data Rounding =
|
||||||
|
NoRounding -- ^ keep the amount precisions unchanged
|
||||||
|
| SoftRounding -- ^ add or remove trailing zeros to approach the desired precision
|
||||||
|
-- | HardRounding -- ^ also remove non-zero digits, in posting amounts (lossy)
|
||||||
|
-- | HardRoundingAndCost -- ^ also remove non-zero digits, in posting and cost amounts (lossy)
|
||||||
|
deriving (Eq,Ord,Read,Generic)
|
||||||
|
|
||||||
-- | A style for displaying digit groups in the integer part of a
|
-- | A style for displaying digit groups in the integer part of a
|
||||||
-- floating point number. It consists of the character used to
|
-- floating point number. It consists of the character used to
|
||||||
-- separate groups (comma or period, whichever is not used as decimal
|
-- separate groups (comma or period, whichever is not used as decimal
|
||||||
|
|||||||
@ -110,8 +110,8 @@ amountPriceDirectiveFromCost d amt@Amount{acommodity=fromcomm, aquantity=fromq}
|
|||||||
where
|
where
|
||||||
style' = (astyle a) { asprecision = precision' }
|
style' = (astyle a) { asprecision = precision' }
|
||||||
precision' = case asprecision (astyle a) of
|
precision' = case asprecision (astyle a) of
|
||||||
Just (Precision p) -> Just $ Precision $ (numDigitsInt $ truncate n) + p
|
NaturalPrecision -> NaturalPrecision
|
||||||
mp -> mp
|
Precision p -> Precision $ (numDigitsInt $ truncate n) + p
|
||||||
|
|
||||||
------------------------------------------------------------------------------
|
------------------------------------------------------------------------------
|
||||||
-- Converting things to value
|
-- Converting things to value
|
||||||
|
|||||||
@ -804,7 +804,7 @@ simpleamountp mult =
|
|||||||
offAfterNum <- getOffset
|
offAfterNum <- getOffset
|
||||||
let numRegion = (offBeforeNum, offAfterNum)
|
let numRegion = (offBeforeNum, offAfterNum)
|
||||||
(q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent
|
(q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion suggestedStyle ambiguousRawNum mExponent
|
||||||
let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=Just prec, asdecimalmark=mdec, asdigitgroups=mgrps}
|
let s = amountstyle{ascommodityside=L, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalmark=mdec, asdigitgroups=mgrps}
|
||||||
return nullamt{acommodity=c, aquantity=sign (sign2 q), astyle=s, aprice=Nothing}
|
return nullamt{acommodity=c, aquantity=sign (sign2 q), astyle=s, aprice=Nothing}
|
||||||
|
|
||||||
-- An amount with commodity symbol on the right or no commodity symbol.
|
-- An amount with commodity symbol on the right or no commodity symbol.
|
||||||
@ -826,7 +826,7 @@ simpleamountp mult =
|
|||||||
-- XXX amounts of this commodity in periodic transaction rules and auto posting rules ? #1461
|
-- XXX amounts of this commodity in periodic transaction rules and auto posting rules ? #1461
|
||||||
let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle
|
let msuggestedStyle = mdecmarkStyle <|> mcommodityStyle
|
||||||
(q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent
|
(q,prec,mdec,mgrps) <- lift $ interpretNumber numRegion msuggestedStyle ambiguousRawNum mExponent
|
||||||
let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=Just prec, asdecimalmark=mdec, asdigitgroups=mgrps}
|
let s = amountstyle{ascommodityside=R, ascommodityspaced=commodityspaced, asprecision=prec, asdecimalmark=mdec, asdigitgroups=mgrps}
|
||||||
return nullamt{acommodity=c, aquantity=sign q, astyle=s, aprice=Nothing}
|
return nullamt{acommodity=c, aquantity=sign q, astyle=s, aprice=Nothing}
|
||||||
-- no symbol amount
|
-- no symbol amount
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
@ -842,8 +842,8 @@ simpleamountp mult =
|
|||||||
-- (unless it's a multiplier in an automated posting)
|
-- (unless it's a multiplier in an automated posting)
|
||||||
defcs <- getDefaultCommodityAndStyle
|
defcs <- getDefaultCommodityAndStyle
|
||||||
let (c,s) = case (mult, defcs) of
|
let (c,s) = case (mult, defcs) of
|
||||||
(False, Just (defc,defs)) -> (defc, defs{asprecision=max (asprecision defs) (Just prec)})
|
(False, Just (defc,defs)) -> (defc, defs{asprecision=max (asprecision defs) prec})
|
||||||
_ -> ("", amountstyle{asprecision=Just prec, asdecimalmark=mdec, asdigitgroups=mgrps})
|
_ -> ("", amountstyle{asprecision=prec, asdecimalmark=mdec, asdigitgroups=mgrps})
|
||||||
return nullamt{acommodity=c, aquantity=sign q, astyle=s, aprice=Nothing}
|
return nullamt{acommodity=c, aquantity=sign q, astyle=s, aprice=Nothing}
|
||||||
|
|
||||||
-- For reducing code duplication. Doesn't parse anything. Has the type
|
-- For reducing code duplication. Doesn't parse anything. Has the type
|
||||||
@ -1070,7 +1070,7 @@ disambiguateNumber msuggestedStyle (AmbiguousNumber grp1 sep grp2) =
|
|||||||
isValidDecimalBy c = \case
|
isValidDecimalBy c = \case
|
||||||
AmountStyle{asdecimalmark = Just d} -> d == c
|
AmountStyle{asdecimalmark = Just d} -> d == c
|
||||||
AmountStyle{asdigitgroups = Just (DigitGroups g _)} -> g /= c
|
AmountStyle{asdigitgroups = Just (DigitGroups g _)} -> g /= c
|
||||||
AmountStyle{asprecision = Just (Precision 0)} -> False
|
AmountStyle{asprecision = Precision 0} -> False
|
||||||
_ -> True
|
_ -> True
|
||||||
|
|
||||||
-- | Parse and interpret the structure of a number without external hints.
|
-- | Parse and interpret the structure of a number without external hints.
|
||||||
@ -1574,24 +1574,24 @@ tests_Common = testGroup "Common" [
|
|||||||
nullamt{
|
nullamt{
|
||||||
acommodity="$"
|
acommodity="$"
|
||||||
,aquantity=10 -- need to test internal precision with roundTo ? I think not
|
,aquantity=10 -- need to test internal precision with roundTo ? I think not
|
||||||
,astyle=amountstyle{asprecision=Just $ Precision 0, asdecimalmark=Nothing}
|
,astyle=amountstyle{asprecision=Precision 0, asdecimalmark=Nothing}
|
||||||
,aprice=Just $ UnitPrice $
|
,aprice=Just $ UnitPrice $
|
||||||
nullamt{
|
nullamt{
|
||||||
acommodity="€"
|
acommodity="€"
|
||||||
,aquantity=0.5
|
,aquantity=0.5
|
||||||
,astyle=amountstyle{asprecision=Just $ Precision 1, asdecimalmark=Just '.'}
|
,astyle=amountstyle{asprecision=Precision 1, asdecimalmark=Just '.'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
,testCase "total price" $ assertParseEq amountp "$10 @@ €5"
|
,testCase "total price" $ assertParseEq amountp "$10 @@ €5"
|
||||||
nullamt{
|
nullamt{
|
||||||
acommodity="$"
|
acommodity="$"
|
||||||
,aquantity=10
|
,aquantity=10
|
||||||
,astyle=amountstyle{asprecision=Just $ Precision 0, asdecimalmark=Nothing}
|
,astyle=amountstyle{asprecision=Precision 0, asdecimalmark=Nothing}
|
||||||
,aprice=Just $ TotalPrice $
|
,aprice=Just $ TotalPrice $
|
||||||
nullamt{
|
nullamt{
|
||||||
acommodity="€"
|
acommodity="€"
|
||||||
,aquantity=5
|
,aquantity=5
|
||||||
,astyle=amountstyle{asprecision=Just $ Precision 0, asdecimalmark=Nothing}
|
,astyle=amountstyle{asprecision=Precision 0, asdecimalmark=Nothing}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
,testCase "unit price, parenthesised" $ assertParse amountp "$10 (@) €0.5"
|
,testCase "unit price, parenthesised" $ assertParse amountp "$10 (@) €0.5"
|
||||||
|
|||||||
@ -193,8 +193,8 @@ timedotentryp = do
|
|||||||
mcs <- getDefaultCommodityAndStyle
|
mcs <- getDefaultCommodityAndStyle
|
||||||
let
|
let
|
||||||
(c,s) = case mcs of
|
(c,s) = case mcs of
|
||||||
Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) (Just $ Precision 2)})
|
Just (defc,defs) -> (defc, defs{asprecision=max (asprecision defs) (Precision 2)})
|
||||||
_ -> ("", amountstyle{asprecision=Just $ Precision 2})
|
_ -> ("", amountstyle{asprecision=Precision 2})
|
||||||
-- lift $ traceparse' "timedotentryp end"
|
-- lift $ traceparse' "timedotentryp end"
|
||||||
return $ nullposting{paccount=a
|
return $ nullposting{paccount=a
|
||||||
,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hours, astyle=s}
|
,pamount=mixedAmount $ nullamt{acommodity=c, aquantity=hours, astyle=s}
|
||||||
|
|||||||
@ -618,7 +618,8 @@ tests_MultiBalanceReport = testGroup "MultiBalanceReport" [
|
|||||||
|
|
||||||
let
|
let
|
||||||
amt0 = Amount {acommodity="$", aquantity=0, aprice=Nothing,
|
amt0 = Amount {acommodity="$", aquantity=0, aprice=Nothing,
|
||||||
astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asdigitgroups = Nothing, asdecimalmark = Just '.', asprecision = Just $ Precision 2}}
|
astyle=AmountStyle {ascommodityside = L, ascommodityspaced = False, asdigitgroups = Nothing,
|
||||||
|
asdecimalmark = Just '.', asprecision = Precision 2, asrounding = NoRounding}}
|
||||||
(rspec,journal) `gives` r = do
|
(rspec,journal) `gives` r = do
|
||||||
let rspec' = rspec{_rsQuery=And [queryFromFlags $ _rsReportOpts rspec, _rsQuery rspec]}
|
let rspec' = rspec{_rsQuery=And [queryFromFlags $ _rsReportOpts rspec, _rsQuery rspec]}
|
||||||
(eitems, etotal) = r
|
(eitems, etotal) = r
|
||||||
|
|||||||
@ -70,16 +70,15 @@ print' opts j = do
|
|||||||
|
|
||||||
printEntries :: CliOpts -> Journal -> IO ()
|
printEntries :: CliOpts -> Journal -> IO ()
|
||||||
printEntries opts@CliOpts{reportspec_=rspec} j =
|
printEntries opts@CliOpts{reportspec_=rspec} j =
|
||||||
writeOutputLazyText opts . render $
|
writeOutputLazyText opts $ render $ entriesReport rspec j
|
||||||
styleAmounts styles $
|
|
||||||
entriesReport rspec j
|
|
||||||
where
|
where
|
||||||
styles = M.map amountStyleUnsetPrecision $ journalCommodityStyles j -- keep all precisions unchanged
|
stylesnorounding = M.map (amountStyleSetRounding NoRounding) $ journalCommodityStyles j
|
||||||
|
stylessoftrounding = M.map (amountStyleSetRounding SoftRounding) $ journalCommodityStyles j
|
||||||
fmt = outputFormatFromOpts opts
|
fmt = outputFormatFromOpts opts
|
||||||
render | fmt=="txt" = entriesReportAsText opts
|
render | fmt=="txt" = entriesReportAsText opts . styleAmounts stylesnorounding
|
||||||
| fmt=="csv" = printCSV . entriesReportAsCsv
|
| fmt=="csv" = printCSV . entriesReportAsCsv . styleAmounts stylessoftrounding
|
||||||
| fmt=="json" = toJsonText
|
| fmt=="json" = toJsonText . styleAmounts stylessoftrounding
|
||||||
| fmt=="sql" = entriesReportAsSql
|
| fmt=="sql" = entriesReportAsSql . styleAmounts stylessoftrounding
|
||||||
| otherwise = error' $ unsupportedOutputFormatError fmt -- PARTIAL:
|
| otherwise = error' $ unsupportedOutputFormatError fmt -- PARTIAL:
|
||||||
|
|
||||||
entriesReportAsText :: CliOpts -> EntriesReport -> TL.Text
|
entriesReportAsText :: CliOpts -> EntriesReport -> TL.Text
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user