packages feed

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 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