safe-money 0.9 → 0.9.1
raw patch · 5 files changed
+107/−13 lines, 5 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
+ Money: HalfAwayFromZero :: Approximation
+ Money: exchangeRate' :: Rational -> ExchangeRate src dst
- Money: type GoodScale (scale :: (Nat, Nat)) = (CmpNat 0 (Fst scale) ~ 'LT, CmpNat 0 (Snd scale) ~ 'LT, KnownNat (Fst scale), KnownNat (Snd scale))
+ Money: type GoodScale (scale :: (Nat, Nat)) = (CmpNat 0 (Fst scale) ~ 'LT, CmpNat 0 (Snd scale) ~ 'LT, KnownNat (Fst scale), KnownNat (Snd scale))
Files
- changelog.md +9/−0
- safe-money.cabal +1/−1
- src/Money.hs +1/−0
- src/Money/Internal.hs +59/−12
- test/Main.hs +37/−0
changelog.md view
@@ -1,3 +1,12 @@+# Version 0.9.1++* Add approximation method `HalfAwayFromZero`, also known as “kaufmännisches Runden”+ in German speaking countries. See issue #55.+ +* Fix compilation with GHC 9. See issue #59.++* Added `exchangeRate'`. See issue #61.+ # Version 0.9 * _BREAKING CHANGE, POSSIBLY REQUIRING HUMAN INTERVENTION_. Changed
safe-money.cabal view
@@ -1,5 +1,5 @@ name: safe-money-version: 0.9+version: 0.9.1 license: BSD3 license-file: LICENSE copyright: Copyright (c) Renzo Carbonara 2016-2019
src/Money.hs view
@@ -73,6 +73,7 @@ -- * Currency exchange , I.ExchangeRate , I.exchangeRate+ , I.exchangeRate' , I.exchange , I.exchangeRateRecip , I.exchangeRateFromDecimal
src/Money/Internal.hs view
@@ -21,7 +21,7 @@ {-# OPTIONS_GHC -Wno-redundant-constraints #-} -- | This is an internal module. You may use stuff exported from here, but we--- can't garantee their stability.+-- can't guarantee their stability. module Money.Internal ( -- * Dense monetary values Dense@@ -53,6 +53,7 @@ -- * Currency exchange , ExchangeRate , exchangeRate+ , exchangeRate' , exchange , exchangeRateFromDecimal , exchangeRateToDecimal@@ -96,7 +97,7 @@ , rationalToDecimal , rationalFromDecimal -- * Miscellaneous- , Approximation(Round, Floor, Ceiling, Truncate, HalfEven)+ , Approximation(Round, Floor, Ceiling, Truncate, HalfEven, HalfAwayFromZero) , approximate -- ** Decimal config , DecimalConf(..)@@ -163,7 +164,7 @@ -- of @USD 3.41@ is @USD 1.705@, which is not an amount that can't be -- represented as a number of USD cents (the smallest unit that can -- represent USD amounts). Nevertheless, if you do manage to represent @USD--- 1.709@ somehow, and you eventually multiply @USD 1.705@ by @4@ for+-- 1.705@ somehow, and you eventually multiply @USD 1.705@ by @4@ for -- example, then you end up with @USD 6.82@, which is again a value -- representable as USD cents. In other words, 'Dense' monetary values -- allow us to perform precise calculations deferring the conversion to a@@ -335,7 +336,7 @@ -- 'discrete' 2105 :: 'Discrete' \"GBP\" \"penny\" -- @ ----- Because @2015 / 100 == 20.15@.+-- Because @2105 / 100 == 21.05@. type Discrete (currency :: Symbol) (unit :: Symbol) = Discrete' currency (UnitScale currency unit) @@ -479,7 +480,7 @@ . (KnownSymbol currency, GoodScale scale) => Discrete' currency scale -> String -- ^-discreteCurrency' = \_ -> symbolVal (Proxy @ currency)+discreteCurrency' = \_ -> symbolVal (Proxy @currency) {-# INLINE discreteCurrency' #-} -- | Method for approximating a fractional number to an integer number.@@ -496,6 +497,10 @@ | HalfEven -- ^ Approximate @x@ to the nearest even integer, when equidistant from the -- nearest two integers. This is also known as “Bankers Rounding”.+ | HalfAwayFromZero+ -- ^ Approximate @x@ to the nearest integer, or to the nearest integer away+ -- from zero if @x@ is equidistant between to integers. This is known+ -- as “kaufmännisches Runden” in German speaking countries. deriving (Eq, Ord, Show, Read, GHC.Generic) approximate :: Approximation -> Rational -> Integer@@ -506,6 +511,7 @@ Ceiling -> ceiling Truncate -> truncate HalfEven -> halfEven+ HalfAwayFromZero -> halfAwayFromZero -- | Approximate to the nearest even integer, when equidistant from the nearest -- two integers. This is also known as “Bankers Rounding”.@@ -518,7 +524,20 @@ | even tr -> tr | otherwise -> tr + signum tr +-- | Approximate @x@ to the nearest integer, or to the nearest integer away+-- from zero if @x@ is equidistant between to integers. This is known+-- as “kaufmännisches Runden” in German speaking countries. +halfAwayFromZero :: Rational -> Integer+{-# INLINABLE halfAwayFromZero #-}+halfAwayFromZero = \r -> -- 1.5 -1.5+ let s :: Integer = truncate (signum r)+ ar :: Rational = abs r -- 1.5 1.5+ tr :: Integer = truncate ar -- 1.0 1.0+ rr :: Rational = ar - toRational tr -- 0.5 0.5+ in if | rr < 1%2 -> s * tr+ | otherwise -> s * (tr + 1)+ -- | Approximate a 'Dense' value @x@ to the nearest value fully representable a -- given @scale@. --@@ -797,6 +816,34 @@ else Nothing {-# INLINE exchangeRate #-} +-- | Unsafely build an 'ExchageRate' monetary value from a 'Rational' value.+-- Contrary to 'exchangeRate', this function *crashes* if the given 'Rational'+-- a value has zero as a denominator or when it is negative, with the former+-- case being something very unlikely to happen unless the given 'Rational'+-- was itself unsafely constructed. Other than that, 'exchangeRate' and+-- 'exchangeRate'' behave the same.+--+-- Prefer to use 'exchangeRate' when dealing with 'Rational' inputs from+-- untrusted sources.+--+-- @+-- 'denominator' x /= 0 && x > 0+-- ⇒ 'exchangeRate' x == 'Just' ('exchangeRate'' x)+-- @+--+-- @+-- 'denominator' x == 0 || x <= 0+-- ⇒ 'undefined' == 'exchangeRate'' x+-- @+exchangeRate' :: Rational -> ExchangeRate src dst+exchangeRate' = \r ->+ if denominator r /= 0 && r > 0+ then ExchangeRate r+ else if denominator r == 0+ then error "exchangeRate': malformed Rational given (denominator is zero)."+ else error "exchangeRate': malformed Rational given (is negative)."+{-# INLINABLE exchangeRate' #-}+ -- | Reciprocal 'ExchangeRate'. -- -- This function retuns the reciprocal or multiplicative inverse of the given@@ -895,7 +942,7 @@ -- | Convert a 'Dense' to a 'SomeDense' for ease of serialization. toSomeDense :: KnownSymbol currency => Dense currency -> SomeDense toSomeDense = \(Dense r0 :: Dense currency) ->- SomeDense (symbolVal (Proxy @ currency)) r0+ SomeDense (symbolVal (Proxy @currency)) r0 {-# INLINE toSomeDense #-} -- | Attempt to convert a 'SomeDense' to a 'Dense', provided you know the target@@ -979,7 +1026,7 @@ someDiscreteAmount = _someDiscreteAmount {-# INLINE someDiscreteAmount #-} --- | Internal. Build a 'SomeDiscrete' from raw values.+-- | Build a 'SomeDiscrete' from raw values. -- -- This function is intended for deserialization purposes. You need to convert -- this 'SomeDiscrete' value to a 'Discrete' vallue in order to do any@@ -1017,8 +1064,8 @@ => SomeDiscrete -> Maybe (Discrete' currency scale) -- ^ fromSomeDiscrete = \dr ->- if (_someDiscreteCurrency dr == symbolVal (Proxy @ currency)) &&- (someDiscreteScale dr == scale (Proxy @ scale))+ if (_someDiscreteCurrency dr == symbolVal (Proxy @currency)) &&+ (someDiscreteScale dr == scale (Proxy @scale)) then Just (Discrete (someDiscreteAmount dr)) else Nothing {-# INLINABLE fromSomeDiscrete #-}@@ -1119,7 +1166,7 @@ someExchangeRateRate = _someExchangeRateRate {-# INLINE someExchangeRateRate #-} --- | Internal. Build a 'SomeExchangeRate' from raw values.+-- | Build a 'SomeExchangeRate' from raw values. -- -- This function is intended for deserialization purposes. You need to convert -- this 'SomeExchangeRate' value to a 'ExchangeRate' value in order to do any@@ -1160,8 +1207,8 @@ => SomeExchangeRate -> Maybe (ExchangeRate src dst) -- ^ fromSomeExchangeRate = \x ->- if (_someExchangeRateSrcCurrency x == symbolVal (Proxy @ src)) &&- (_someExchangeRateDstCurrency x == symbolVal (Proxy @ dst))+ if (_someExchangeRateSrcCurrency x == symbolVal (Proxy @src)) &&+ (_someExchangeRateDstCurrency x == symbolVal (Proxy @dst)) then Just (ExchangeRate (someExchangeRateRate x)) else Nothing {-# INLINABLE fromSomeExchangeRate #-}
test/Main.hs view
@@ -72,6 +72,7 @@ Tasty.testGroup "root" [ testCurrencies , testHalfEvenRounding+ , testHalfAwayFromZeroRounding , testCurrencyUnits , testExchange , testRationalToDecimal@@ -1135,6 +1136,42 @@ f :: Rational -> Integer f = MoneyI.approximate MoneyI.HalfEven +testHalfAwayFromZeroRounding :: Tasty.TestTree+testHalfAwayFromZeroRounding =+ Tasty.testGroup "HalfAwayFromZero rounding"+ [ HU.testCase "-2.8" $ -3 @=? f (-3152519739159347 % 1125899906842624)++ , HU.testCase "-2.5" $ -3 @=? f (-5 % 2)+ , HU.testCase "-2.2" $ -2 @=? f (-2476979795053773 % 1125899906842624)+ , HU.testCase "-2" $ -2 @=? f (-2 % 1)+ , HU.testCase "-1.8" $ -2 @=? f (-8106479329266893 % 4503599627370496)+ , HU.testCase "-1.5" $ -2 @=? f (-3 % 2)++ , HU.testCase "-1.2" $ -1 @=? f (-5404319552844595 % 4503599627370496)+ , HU.testCase "-1" $ -1 @=? f (-1 % 1)+ , HU.testCase "-0.8" $ -1 @=? f (-3602879701896397 % 4503599627370496)++ , HU.testCase "-0.5" $ -1 @=? f (-1 % 2)+ , HU.testCase "-0.2" $ 0 @=? f (-3602879701896397 % 18014398509481984)+ , HU.testCase "0" $ 0 @=? f (0 % 1)+ , HU.testCase "0.2" $ 0 @=? f (3602879701896397 % 18014398509481984)+ , HU.testCase "0.5" $ 1 @=? f (1 % 2)++ , HU.testCase "0.8" $ 1 @=? f (3602879701896397 % 4503599627370496)+ , HU.testCase "1" $ 1 @=? f (1 % 1)+ , HU.testCase "1.2" $ 1 @=? f (5404319552844595 % 4503599627370496)++ , HU.testCase "1.5" $ 2 @=? f (3 % 2)+ , HU.testCase "1.8" $ 2 @=? f (8106479329266893 % 4503599627370496)+ , HU.testCase "2" $ 2 @=? f (2 % 1)+ , HU.testCase "2.2" $ 2 @=? f (2476979795053773 % 1125899906842624)+ , HU.testCase "2.5" $ 3 @=? f (5 % 2)++ , HU.testCase "2.8" $ 3 @=? f (3152519739159347 % 1125899906842624)+ ]+ where+ f :: Rational -> Integer+ f = MoneyI.approximate MoneyI.HalfAwayFromZero -- | Decimal dot, 2 decimals