safe-money 0.3 → 0.4
raw patch · 7 files changed
+1944/−1350 lines, 7 filesdep +serialisedep +textdep +xmlbfPVP ok
version bump matches the API change (PVP)
Dependencies added: serialise, text, xmlbf
API changes (from Hackage documentation)
- Money: data DenseRep
- Money: data DiscreteRep
- Money: data ExchangeRateRep
- Money: denseRepAmount :: DenseRep -> Rational
- Money: denseRepAmountDenominator :: DenseRep -> Integer
- Money: denseRepAmountNumerator :: DenseRep -> Integer
- Money: denseRepCurrency :: DenseRep -> String
- Money: discreteRepAmount :: DiscreteRep -> Integer
- Money: discreteRepCurrency :: DiscreteRep -> String
- Money: discreteRepScale :: DiscreteRep -> Rational
- Money: discreteRepScaleDenominator :: DiscreteRep -> Integer
- Money: discreteRepScaleNumerator :: DiscreteRep -> Integer
- Money: exchangeRateRepDstCurrency :: ExchangeRateRep -> String
- Money: exchangeRateRepRate :: ExchangeRateRep -> Rational
- Money: exchangeRateRepRateDenominator :: ExchangeRateRep -> Integer
- Money: exchangeRateRepRateNumerator :: ExchangeRateRep -> Integer
- Money: exchangeRateRepSrcCurrency :: ExchangeRateRep -> String
- Money: fromDenseRep :: forall currency. KnownSymbol currency => DenseRep -> Maybe (Dense currency)
- Money: fromDiscreteRep :: forall currency scale. (KnownSymbol currency, GoodScale scale) => DiscreteRep -> Maybe (Discrete' currency scale)
- Money: fromExchangeRateRep :: forall src dst. (KnownSymbol src, KnownSymbol dst) => ExchangeRateRep -> Maybe (ExchangeRate src dst)
- Money: mkDenseRep :: String -> Integer -> Integer -> Maybe DenseRep
- Money: mkDiscreteRep :: String -> Integer -> Integer -> Integer -> Maybe DiscreteRep
- Money: mkExchangeRateRep :: String -> String -> Integer -> Integer -> Maybe ExchangeRateRep
- Money: toDenseRep :: KnownSymbol currency => Dense currency -> DenseRep
- Money: toDiscreteRep :: (KnownSymbol currency, GoodScale scale) => Discrete' currency scale -> DiscreteRep
- Money: toExchangeRateRep :: (KnownSymbol src, KnownSymbol dst) => ExchangeRate src dst -> ExchangeRateRep
- Money: withDenseRep :: DenseRep -> (forall currency. KnownSymbol currency => Dense currency -> r) -> r
- Money: withDiscreteRep :: forall r. DiscreteRep -> (forall currency scale. (KnownSymbol currency, GoodScale scale) => Discrete' currency scale -> r) -> r
- Money: withExchangeRateRep :: ExchangeRateRep -> (forall src dst. (KnownSymbol src, KnownSymbol dst) => ExchangeRate src dst -> r) -> r
+ Money: data SomeDense
+ Money: data SomeDiscrete
+ Money: data SomeExchangeRate
+ Money: fromSomeDense :: forall currency. KnownSymbol currency => SomeDense -> Maybe (Dense currency)
+ Money: fromSomeDiscrete :: forall currency scale. (KnownSymbol currency, GoodScale scale) => SomeDiscrete -> Maybe (Discrete' currency scale)
+ Money: fromSomeExchangeRate :: forall src dst. (KnownSymbol src, KnownSymbol dst) => SomeExchangeRate -> Maybe (ExchangeRate src dst)
+ Money: mkSomeDense :: String -> Rational -> Maybe SomeDense
+ Money: mkSomeDiscrete :: String -> Rational -> Integer -> Maybe SomeDiscrete
+ Money: mkSomeExchangeRate :: String -> String -> Rational -> Maybe SomeExchangeRate
+ Money: someDenseAmount :: SomeDense -> Rational
+ Money: someDenseCurrency :: SomeDense -> String
+ Money: someDiscreteAmount :: SomeDiscrete -> Integer
+ Money: someDiscreteCurrency :: SomeDiscrete -> String
+ Money: someDiscreteScale :: SomeDiscrete -> Rational
+ Money: someExchangeRateDstCurrency :: SomeExchangeRate -> String
+ Money: someExchangeRateRate :: SomeExchangeRate -> Rational
+ Money: someExchangeRateSrcCurrency :: SomeExchangeRate -> String
+ Money: toSomeDense :: KnownSymbol currency => Dense currency -> SomeDense
+ Money: toSomeDiscrete :: (KnownSymbol currency, GoodScale scale) => Discrete' currency scale -> SomeDiscrete
+ Money: toSomeExchangeRate :: (KnownSymbol src, KnownSymbol dst) => ExchangeRate src dst -> SomeExchangeRate
+ Money: withSomeDense :: SomeDense -> (forall currency. KnownSymbol currency => Dense currency -> r) -> r
+ Money: withSomeDiscrete :: forall r. SomeDiscrete -> (forall currency scale. (KnownSymbol currency, GoodScale scale) => Discrete' currency scale -> r) -> r
+ Money: withSomeExchangeRate :: SomeExchangeRate -> (forall src dst. (KnownSymbol src, KnownSymbol dst) => ExchangeRate src dst -> r) -> r
- Money: ceiling :: GoodScale scale => Dense currency -> (Discrete' currency scale, Maybe (Dense currency))
+ Money: ceiling :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
- Money: floor :: GoodScale scale => Dense currency -> (Discrete' currency scale, Maybe (Dense currency))
+ Money: floor :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
- Money: round :: GoodScale scale => Dense currency -> (Discrete' currency scale, Maybe (Dense currency))
+ Money: round :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
- Money: truncate :: GoodScale scale => Dense currency -> (Discrete' currency scale, Maybe (Dense currency))
+ Money: truncate :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
Files
- LICENSE +1/−1
- README.md +3/−0
- changelog.md +35/−0
- safe-money.cabal +50/−12
- src/Money.hs +50/−29
- src/Money/Internal.hs +1492/−1179
- test/Main.hs +313/−129
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2016, Renzo Carbonara+Copyright (c) 2016-2017, Renzo Carbonara All rights reserved.
README.md view
@@ -1,3 +1,6 @@ The Haskell `safe-money` library offers type-safe and lossless encoding and operations for monetary values in all world currencies, including fiat currencies, precious metals and crypto-currencies.++You can enter a development shell with `nix-shell`, or build with `nix-build+./release.nix -A ghc822.safe-money` (or similar, check `release.nix`).
changelog.md view
@@ -1,3 +1,38 @@+# Version 0.4++* BREAKING CHANGE. COMPILER WON'T COMPLAIN. HUMAN INTERVENTION POTENTIALLY+ REQUIRED. The JSON serializations for all of `Dense`, `SomeDense`, `Discrete`,+ `SomeDiscrete`, `ExchangeRate` and `SomeExchangeRate` changed. The `FromJSON`+ instances are backwards compatible with the old serializations, but the+ `ToJSON` instances will only generate the new format, which is the same as the+ old format except the leading strings `"Dense"`, `"Discrete"` and+ `"ExchangeRate"`, respectively, are not present in the rendered JSON array+ anymore. So, if you were manually relying on the `ToJSON` instance, please+ update your code.++* BREAKING CHANGE. COMPILER WILL COMPLAIN: Changed the `Rep` suffix for a `Some`+ prefix Everywhere. For example, `DenseRep` was renamed to `SomeDense`.++* BREAKING CHANGE. COMPILER WILL COMPLAIN: Replaced the+ `someDenseAmountNumerator` and `someDenseAmountDenominator` `Integers` with a+ single `someDenseAmount` `Rational` number. Similarly for `someDiscreteScale`+ and `someExchangeRateRate`. The `mkSomeDense`, `someDiscreteScale` and+ `mkSomeDense` also take a `Rational` now.++* BREAKING CHANGE. COMPILER WILL COMPLAIN: The `truncate`, `floor`, `celing` and+ `round` functions now return just `0` as a reminder if there's no significant+ reminder, instead of `Nothing`.++* Added instances for `serialise`.++* Added instances for `xmlbf`.++* Fixed `Show` instances so that surrounding parentheses are included when+ necessary.++* New currencies: Ripple, Litecoin, Ada, Monero.++ # Version 0.3 * BREAKING CHANGE: The `Data.Money` module was renamed to `Money`.
safe-money.cabal view
@@ -1,12 +1,12 @@ name: safe-money-version: 0.3+version: 0.4 license: BSD3 license-file: LICENSE copyright: Copyright (c) Renzo Carbonara 2016-2017 author: Renzo Carbonara maintainer: renλren!zone stability: Experimental-tested-with: GHC==8.0.1+tested-with: GHC==8.2.2, GHC==8.0.2, GHCJS homepage: https://github.com/k0001/safe-money bug-reports: https://github.com/k0001/safe-money/issues category: Money@@ -20,8 +20,8 @@ Type-safe and lossless encoding and manipulation of money, fiat currencies, crypto currencies and precious metals. .- Notice that the only mandatory dependencies of this package are @base@ and- @constraints@. The rest of the dependencies are optional but enabled by+ NOTICE that the only mandatory dependencies of this package are @base@ and+ @constraints@. The rest of the dependencies are OPTIONAL but enabled by default (except @store@ which is also disabled when building with GHCJS), they can be disabled with Cabal flags. @@ -39,16 +39,28 @@ if flag(aeson) build-depends: aeson (>=0.9)+ cpp-options: -DHAS_aeson if flag(binary) build-depends: binary (>=0.7)+ cpp-options: -DHAS_binary if flag(cereal) build-depends: cereal (>=0.5)+ cpp-options: -DHAS_cereal if flag(deepseq) build-depends: deepseq (>=1.4)+ cpp-options: -DHAS_deepseq if flag(hashable) build-depends: hashable (>=1.2)+ cpp-options: -DHAS_hashable+ if flag(serialise)+ build-depends: serialise (>=0.2)+ cpp-options: -DHAS_serialise if (flag(store) && !impl(ghcjs)) build-depends: store (>=0.2)+ cpp-options: -DHAS_store+ if flag(xmlbf)+ build-depends: xmlbf (>=0.2), text+ cpp-options: -DHAS_xmlbf test-suite test default-language: Haskell2010@@ -56,20 +68,38 @@ hs-source-dirs: test main-is: Main.hs build-depends:- aeson, base,- binary, bytestring,- cereal, constraints,- deepseq,- hashable, safe-money, tasty, tasty-hunit, tasty-quickcheck- if (flag(store) || !impl(ghcjs))++ if flag(aeson)+ build-depends: aeson+ cpp-options: -DHAS_aeson+ if flag(binary)+ build-depends: binary+ cpp-options: -DHAS_binary+ if flag(cereal)+ build-depends: cereal+ cpp-options: -DHAS_cereal+ if flag(deepseq)+ build-depends: deepseq+ cpp-options: -DHAS_deepseq+ if flag(hashable)+ build-depends: hashable+ cpp-options: -DHAS_hashable+ if flag(serialise)+ build-depends: serialise+ cpp-options: -DHAS_serialise+ if (flag(store) && !impl(ghcjs)) build-depends: store+ cpp-options: -DHAS_store+ if flag(xmlbf)+ build-depends: xmlbf, text+ cpp-options: -DHAS_xmlbf flag aeson description: Provide instances for @aeson@@@ -87,12 +117,20 @@ description: Provide instances for @store@ default: True manual: True+flag deepseq+ description: Provide instances for @deepseq@+ default: True+ manual: True flag hashable description: Provide instances for @hashable@ default: True manual: True-flag deepseq- description: Provide instances for @deepseq@+flag serialise+ description: Provide instances for @serialise@+ default: True+ manual: True+flag xmlbf+ description: Provide instances for @xmlbf@ default: True manual: True
src/Money.hs view
@@ -37,35 +37,29 @@ , I.flipExchangeRate , I.exchange -- * Serializable representations- , I.DenseRep- , I.toDenseRep- , I.mkDenseRep- , I.fromDenseRep- , I.withDenseRep- , I.denseRepCurrency- , I.denseRepAmount- , I.denseRepAmountNumerator- , I.denseRepAmountDenominator- , I.DiscreteRep- , I.toDiscreteRep- , I.mkDiscreteRep- , I.fromDiscreteRep- , I.withDiscreteRep- , I.discreteRepCurrency- , I.discreteRepScale- , I.discreteRepScaleNumerator- , I.discreteRepScaleDenominator- , I.discreteRepAmount- , I.ExchangeRateRep- , I.toExchangeRateRep- , I.mkExchangeRateRep- , I.fromExchangeRateRep- , I.withExchangeRateRep- , I.exchangeRateRepSrcCurrency- , I.exchangeRateRepDstCurrency- , I.exchangeRateRepRate- , I.exchangeRateRepRateNumerator- , I.exchangeRateRepRateDenominator+ , I.SomeDense+ , I.toSomeDense+ , I.mkSomeDense+ , I.fromSomeDense+ , I.withSomeDense+ , I.someDenseCurrency+ , I.someDenseAmount+ , I.SomeDiscrete+ , I.toSomeDiscrete+ , I.mkSomeDiscrete+ , I.fromSomeDiscrete+ , I.withSomeDiscrete+ , I.someDiscreteCurrency+ , I.someDiscreteScale+ , I.someDiscreteAmount+ , I.SomeExchangeRate+ , I.toSomeExchangeRate+ , I.mkSomeExchangeRate+ , I.fromSomeExchangeRate+ , I.withSomeExchangeRate+ , I.someExchangeRateSrcCurrency+ , I.someExchangeRateDstCurrency+ , I.someExchangeRateRate ) where import qualified Money.Internal as I@@ -791,3 +785,30 @@ type instance I.Scale "ETH" "finney" = '(1000000000000000, 1) type instance I.Scale "ETH" "milliether" = '(1000000000000000, 1) type instance I.Scale "ETH" "wei" = '(1000000000000000000, 1)++-- | Ada+type instance I.Scale "ADA" "ADA" = '(1000000, 1)+type instance I.Scale "ADA" "ada" = '(1, 1)+type instance I.Scale "ADA" "lovelace" = '(1000000, 1)++-- | Litecoin+type instance I.Scale "LTC" "LTC" = '(100000000, 1)+type instance I.Scale "LTC" "litecoin" = '(1, 1)+type instance I.Scale "LTC" "lite" = '(1000, 1)+type instance I.Scale "LTC" "photon" = '(100000000, 1)++-- | Ripple+type instance I.Scale "XRP" "XRP" = '(1000000, 1)+type instance I.Scale "XRP" "ripple" = '(1, 1)+type instance I.Scale "XRP" "drop" = '(1000000, 1)++-- | Monero+type instance I.Scale "XMR" "XMR" = '(1000000000000, 1)+type instance I.Scale "XMR" "monero" = '(1, 1)+type instance I.Scale "XMR" "decinero" = '(10, 1)+type instance I.Scale "XMR" "centinero" = '(100, 1)+type instance I.Scale "XMR" "millinero" = '(1000, 1)+type instance I.Scale "XMR" "micronero" = '(1000000, 1)+type instance I.Scale "XMR" "nanonero" = '(1000000000, 1)+type instance I.Scale "XMR" "piconero" = '(1000000000000, 1)+
src/Money/Internal.hs view
@@ -7,1182 +7,1495 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE KindSignatures #-} {-# LANGUAGE LambdaCase #-}-{-# LANGUAGE PolyKinds #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE ScopedTypeVariables #-}-{-# LANGUAGE StandaloneDeriving #-}-{-# LANGUAGE TypeFamilies #-}-{-# LANGUAGE TypeOperators #-}-{-# LANGUAGE UndecidableInstances #-}--#if MIN_VERSION_base(4,9,0)-{-# OPTIONS_GHC -Wno-redundant-constraints #-}-#endif---- | This is an internal module. Import "Money" instead.-module Money.Internal- ( -- * Dense monetary values- Dense- , dense- -- * Discrete monetary values- , Discrete- , Discrete'- , fromDiscrete- , round- , ceiling- , floor- , truncate- -- * Currency scales- , Scale- , GoodScale- , ErrScaleNonCanonical- , scale- -- * Currency exchange- , ExchangeRate- , exchangeRate- , fromExchangeRate- , flipExchangeRate- , exchange- -- * Serializable representations- , DenseRep- , toDenseRep- , mkDenseRep- , fromDenseRep- , withDenseRep- , denseRepCurrency- , denseRepAmount- , denseRepAmountNumerator- , denseRepAmountDenominator- , DiscreteRep- , toDiscreteRep- , mkDiscreteRep- , fromDiscreteRep- , withDiscreteRep- , discreteRepCurrency- , discreteRepScale- , discreteRepScaleNumerator- , discreteRepScaleDenominator- , discreteRepAmount- , ExchangeRateRep- , toExchangeRateRep- , mkExchangeRateRep- , fromExchangeRateRep- , withExchangeRateRep- , exchangeRateRepSrcCurrency- , exchangeRateRepDstCurrency- , exchangeRateRepRate- , exchangeRateRepRateNumerator- , exchangeRateRepRateDenominator- ) where--import Control.Applicative (empty)-import Control.Monad ((<=<))-import Data.Constraint (Dict(Dict))-import Data.Proxy (Proxy(..))-import Data.Ratio ((%), numerator, denominator)-import qualified GHC.Generics as GHC-import GHC.Real (infinity, notANumber)-import GHC.TypeLits- (Symbol, SomeSymbol(..), Nat, SomeNat(..), CmpNat, KnownSymbol, KnownNat,- natVal, someNatVal, symbolVal, someSymbolVal)-import Prelude hiding (round, ceiling, floor, truncate)-import qualified Prelude-import qualified Text.ParserCombinators.ReadPrec as ReadPrec-import qualified Text.ParserCombinators.ReadP as ReadP-import Text.Read (readPrec)-import Unsafe.Coerce (unsafeCoerce)--#ifdef VERSION_aeson-import qualified Data.Aeson as Ae-#endif--#ifdef VERSION_binary-import qualified Data.Binary as Binary-#endif--#ifdef VERSION_cereal-import qualified Data.Serialize as Cereal-#endif--#ifdef VERSION_deepseq-import Control.DeepSeq (NFData)-#endif--#ifdef VERSION_hashable-import Data.Hashable (Hashable)-#endif--#ifdef VERSION_store-import qualified Data.Store as Store-#endif--#if MIN_VERSION_base(4,9,0)-import qualified GHC.TypeLits as GHC-#endif------------------------------------------------------------------------------------- | 'Dense' represents a dense monetary value for @currency@ (usually a--- ISO-4217 currency code, but not necessarily) as a rational number.------ While monetary values associated with a particular currency are discrete, you--- can still treat monetary values as dense while operating on them. For--- example, the half 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 eventually multiply @USD 1.705@--- by @4@, for example, 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--- 'Discrete' monetary values as much as posible. Once you are ready to--- aproximate a 'Dense' value to a 'Discrete' value you can use one of--- 'round', 'floor', 'ceiling' or 'truncate'. Otherwise, using 'toRational' you--- can obtain a precise 'Rational' representation.------ Construct 'Dense' monetary values using 'dense', or--- 'fromInteger' / 'fromIntegral' if that suffices.------ /WARNING/ if you want to treat a dense monetary value as a /Real/ number (for--- example, to take the square root of that monetary value), then you are on--- your own. We can only guarantee lossless manipulation of rational values, so--- you will need to convert back and forth betwen the 'Rational' representation--- for 'Dense' and your (likely lossy) representation for /Real/ numbers.-newtype Dense (currency :: Symbol) = Dense Rational- deriving (Eq, Ord, Num, Real, Fractional, GHC.Generic)--instance forall currency. KnownSymbol currency => Show (Dense currency) where- show = \(Dense r0) ->- let c = symbolVal (Proxy :: Proxy currency)- in concat [ "Dense ", show c, " (", show r0, ")" ]--instance forall currency. KnownSymbol currency => Read (Dense currency) where- readPrec = do- let c = symbolVal (Proxy :: Proxy currency)- _ <- ReadPrec.lift (ReadP.string ("Dense " ++ show c ++ " "))- maybe empty pure =<< fmap dense readPrec---- | Build a 'Dense' monetary value from a 'Rational' value.------ For example, if you want to represent @USD 12.52316@, then you can use:------ @--- 'dense' (125316 % 10000)--- @------ This function returns 'Nothing' in case the given 'Rational' is 'infinity' or--- 'notANumber'.-dense :: Rational -> Maybe (Dense currency)-dense = \r0 ->- if (infinity == r0 || notANumber == r0)- then Nothing else Just (Dense r0)-{-# INLINABLE dense #-}---- | 'Discrete' represents a discrete monetary value for a @currency@ expresed--- as an integer amount of a particular @unit@. For example, with @currency ~--- \"USD\"@ and @unit ~ \"cent\"@ you can represent United States Dollars to--- their full extent.------ @currency@ is usually a ISO-4217 currency code, but not necessarily.------ Construct 'Discrete' values using 'fromInteger'.------ For example, if you want to represent @GBP 21.05@, where the smallest--- represetable unit for a GBP (United Kingdom Pound) is the /penny/, and 100--- /pennies/ equal 1 GBP (i.e., @'Scale' \"GBP\" ~ '(100, 1)@), then you can--- use:------ @--- 'fromInteger' 2105 :: Discrete \"GBP\" \"penny\"--- @------ Because @2015 / 100 == 20.15@.-type Discrete (currency :: Symbol) (unit :: Symbol)- = Discrete' currency (Scale currency unit)---- | 'Discrete'' represents a discrete monetary value for a @currency@ expresed--- as an amount of @scale@, which is a rational number expressed as @(numerator,--- denominator)@.------ You'll be using 'Discrete' instead of 'Discrete'' most of the time, which--- mentions the unit name (such as /cent/ or /centavo/) instead of explicitely--- mentioning the unit scale.-newtype Discrete' (currency :: Symbol) (scale :: (Nat, Nat))- = Discrete Integer--deriving instance GoodScale scale => Eq (Discrete' currency scale)-deriving instance GoodScale scale => Ord (Discrete' currency scale)-deriving instance GoodScale scale => Enum (Discrete' currency scale)-deriving instance GoodScale scale => Num (Discrete' currency scale)-deriving instance GoodScale scale => Real (Discrete' currency scale)-deriving instance GoodScale scale => Integral (Discrete' currency scale)-deriving instance GoodScale scale => GHC.Generic (Discrete' currency scale)--instance forall currency scale.- ( KnownSymbol currency, GoodScale scale- ) => Show (Discrete' currency scale) where- show = \d0@(Discrete i0) ->- let c = symbolVal (Proxy :: Proxy currency)- in concat [ "Discrete ", show c, " (", show (scale d0), ") ", show i0 ]--instance forall currency scale.- ( KnownSymbol currency, GoodScale scale- ) => Read (Discrete' currency scale) where- readPrec = do- let c = symbolVal (Proxy :: Proxy currency)- s = scale (Proxy :: Proxy scale)- _ <- ReadPrec.lift (ReadP.string- ("Discrete " ++ show c ++ " (" ++ show s ++ ") ") )- Discrete <$> readPrec--#if MIN_VERSION_base(4,9,0)-instance- ( GHC.TypeError- (('GHC.Text "The ") 'GHC.:<>:- ('GHC.ShowType Discrete') 'GHC.:<>:- ('GHC.Text " type is deliberately not a ") 'GHC.:<>:- ('GHC.ShowType Fractional) 'GHC.:$$:- ('GHC.Text "instance. Convert the ") 'GHC.:<>:- ('GHC.ShowType Discrete') 'GHC.:<>:- ('GHC.Text " value to a ") 'GHC.:<>:- ('GHC.ShowType Dense) 'GHC.:$$:- ('GHC.Text "value and use the ") 'GHC.:<>:- ('GHC.ShowType Fractional) 'GHC.:<>:- ('GHC.Text " features on it instead."))- , GoodScale scale- ) => Fractional (Discrete' currency scale) where- fromRational = undefined- recip = undefined-#endif---- | Convert currency 'Discrete' monetary value into a 'Dense' monetary--- value.-fromDiscrete- :: GoodScale scale- => Discrete' currency scale- -> Dense currency -- ^-fromDiscrete = \c@(Discrete i) -> Dense (fromInteger i / scale c)-{-# INLINABLE fromDiscrete #-}---- | Internal. Used to implement 'round', 'ceiling', 'floor' and 'truncate'.-roundf- :: forall currency scale- . GoodScale scale- => (Rational -> Integer) -- ^ 'Prelude.round', 'Prelude.ceiling' or similar.- -> Dense currency- -> (Discrete' currency scale, Maybe (Dense currency))-roundf f = \c0 ->- let !r0 = toRational c0 :: Rational- !r1 = scale (Proxy :: Proxy scale)- !i2 = f (r0 * r1) :: Integer- !r2 = fromInteger i2 / r1 :: Rational- !ycrest | r0 == r2 = Nothing- | otherwise = Just (Dense (r0 - r2))- !d2 = Discrete i2- in (d2, ycrest)-{-# INLINABLE roundf #-}---- | Round a 'Dense' value @x@ to the nearest value fully representable in--- its @currency@'s @unit@ 'Scale', which might be @x@ itself.------ If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',--- then the following holds:------ @--- 'round' x == (x, 'Nothing')--- @------ Otherwise, if the nearest value to @x@ that is fully representable in its--- @currency@'s @unit@ 'Scale' is greater than @x@, then the following holds:------ @--- 'round' == 'ceiling'--- @------ Otherwise, the nearest value to @x@ that is fully representable in its--- @currency@'s @unit@ 'Scale' is smaller than @x@, and the following holds:------ @--- 'round' == 'floor'--- @------ Proof that 'round' doesn't lose money:------ @--- x == case 'round' x of--- (y, 'Nothing') -> y--- (y, 'Just' z) -> y + z--- @-round- :: GoodScale scale- => Dense currency- -> (Discrete' currency scale, Maybe (Dense currency)) -- ^-round = roundf Prelude.round-{-# INLINABLE round #-}---- | Round a 'Dense' value @x@ to the nearest value fully representable in--- its @currency@'s @unit@ 'Scale' which is greater than @x@ or equal to @x@.--------- If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',--- then the following holds:------ @--- 'ceiling' x == (x, 'Nothing')--- @------ Otherwise, if @x@ is not representable in its @currency@'s @unit@ 'Scale',--- then the following holds:------ @--- 'ceiling' x == (y, 'Just' z)--- @------ @--- x /= y--- @------ @--- z < 'zero'--- @------ Proof that 'ceiling' doesn't lose money:------ @--- x == case 'ceiling' x of--- (y, 'Nothing') -> y--- (y, 'Just' z) -> y + z--- @-ceiling- :: GoodScale scale- => Dense currency- -> (Discrete' currency scale, Maybe (Dense currency)) -- ^-ceiling = roundf Prelude.ceiling-{-# INLINABLE ceiling #-}---- | Round a 'Dense' value @x@ to the nearest value fully representable in--- its @currency@'s @unit@ 'Scale' which is smaller than @x@ or equal to @x@.--------- If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',--- then the following holds:------ @--- 'floor' x == (x, 'Nothing')--- @------ Otherwise, if @x@ is not representable in its @currency@'s @unit@ 'Scale',--- then the following holds:------ @--- 'floor' x == (y, 'Just' z)--- @------ @--- x /= y--- @------ @--- z > 'zero'--- @------ Proof that 'floor' doesn't lose money:------ @--- x == case 'floor' x of--- (y, 'Nothing') -> y--- (y, 'Just' z) -> y + z--- @-floor- :: GoodScale scale- => Dense currency- -> (Discrete' currency scale, Maybe (Dense currency)) -- ^-floor = roundf Prelude.floor-{-# INLINABLE floor #-}---- | Round a 'Dense' value @x@ to the nearest value between zero and--- @x@ (inclusive) which is fully representable in its @currency@'s @unit@--- 'Scale'.------ If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',--- then the following holds:------ @--- 'truncate' x == (x, 'Nothing')--- @------ Otherwise, if @x@ is positive, then the following holds:------ @--- 'truncate' == 'floor'--- @------ Otherwise, if @x@ is negative, the following holds:------ @--- 'truncate' == 'ceiling'--- @------ Proof that 'truncate' doesn't lose money:------ @--- x == case 'truncate' x of--- (y, 'Nothing') -> y--- (y, 'Just' z) -> y + z--- @-truncate- :: GoodScale scale- => Dense currency- -> (Discrete' currency scale, Maybe (Dense currency)) -- ^-truncate = roundf Prelude.truncate-{-# INLINABLE truncate #-}-------------------------------------------------------------------------------------- | @'Scale' currency unit@ is a rational number (expressed as @'(numerator,--- denominator)@) indicating how many pieces of @unit@ fit in @currency@.------ @currency@ is usually a ISO-4217 currency code, but not necessarily.------ The 'Scale' will determine how to convert a 'Dense' value into a--- 'Discrete' value and vice-versa.------ For example, there are 100 USD cents in 1 USD, so the scale for this--- relationship is:------ @--- type instance 'Scale' \"USD\" \"cent\" = '(100, 1)--- @------ As another example, there is 1 dollar in USD, so the scale for this--- relationship is:------ @--- type instance 'Scale' \"USD\" \"dollar\" = '(1, 1)--- @------ When using 'Discrete' values to represent money, it will be impossible to--- represent an amount of @currency@ smaller than @unit@. So, if you decide to--- use @Scale \"USD\" \"dollar\"@ as your scale, you will not be able to--- represent values such as USD 3.50 or USD 21.87, since they are not exact--- multiples of a dollar.------ If there exists a canonical smallest @unit@ that can fully represent the--- currency, then an instance @'Scale' currency currency@ exists.------ @--- type instance 'Scale' \"USD\" \"USD\" = Scale \"USD\" \"cent\"--- @------ For some monetary values, such as precious metals, the smallest representable--- unit is not obvious, since you can continue to split the precious metal many--- times before it stops being a precious metal. Still, for practical purposes--- we can make a sane arbitrary choice of smallest unit. For example, the base--- unit for XAU (Gold) is the /troy ounce/, which is too big to be considered--- the smallest unit, but we can arbitrarily choose the /milligrain/ as our--- smallest unit, which is about as heavy as a single grain of table salt and--- should be sufficiently precise for all monetary practical purposes. A /troy--- ounce/ equals 480000 /milligrains/.------ @--- type instance 'Scale' \"XAG\" \"milligrain\" = '(480000, 1)--- @------ You can use other units such as /milligrams/ for measuring XAU, for example.--- However, since the amount of /milligrams/ in a /troy ounce/ (31103.477) is--- not integral, we need to use rational number to express it.------ @--- type instance 'Scale' \"XAU\" \"milligram\" = '(31103477, 1000)--- @------ If you try to obtain the 'Scale of a @currency@ without an obvious smallest--- representable @unit@, like XAU, you will get a compile error.-type family Scale (currency :: Symbol) (unit :: Symbol) :: (Nat, Nat)--#if MIN_VERSION_base(4,9,0)--- | A friendly 'GHC.TypeError' to use for a @currency@ that doesn't have a--- canonical small unit.-type family ErrScaleNonCanonical (currency :: Symbol) :: k where- ErrScaleNonCanonical c = GHC.TypeError- ( 'GHC.Text c 'GHC.:<>:- 'GHC.Text " is not a currency with a canonical smallest unit," 'GHC.:$$:- 'GHC.Text "be explicit about the currency unit you want to use." )-#else--- | Forbid a @currency@ that doesn't have a canonical small unit.------ In GHC versions before 8.0 we can't provide a nice error message here, so we--- simply set this to a value that will fail to satisfy 'GoodScale'. As a--- consequence, trying to use this 'Scale' will result in a cryptic error saying--- /«@Couldn't match type ‘'EQ’ with ‘'LT’@»/.-type ErrScaleNonCanonical (currency :: Symbol) = '(0, 0)-#endif---- | Constraints to a scale (like the one returned by @'Scale' currency unit@)--- expected to always be satisfied. In particular, the scale is always--- guaranteed to be a positive rational number ('infinity' and 'notANumber' are--- forbidden by 'GoodScale').-type GoodScale (scale :: (Nat, Nat))- = ( CmpNat 0 (Fst scale) ~ 'LT- , CmpNat 0 (Snd scale) ~ 'LT- , KnownNat (Fst scale)- , KnownNat (Snd scale)- )---- | If the specified @num@ and @den@ satisfy the expectations of 'GoodScale' at--- the type level, then construct a proof for 'GoodScale'.-mkGoodScale- :: forall num den- . (KnownNat num, KnownNat den)- => Maybe (Dict (GoodScale '(num, den)))-mkGoodScale =- let n = natVal (Proxy :: Proxy num)- d = natVal (Proxy :: Proxy den)- in if (n > 0) && (d > 0)- then Just (unsafeCoerce (Dict :: Dict ('LT ~ 'LT, 'LT ~ 'LT,- KnownNat num, KnownNat den)))- else Nothing-{-# INLINABLE mkGoodScale #-}---- | Term-level representation for the @currency@'s @unit@ 'Scale'.------ For example, the 'Scale' for @\"USD\"@ in @\"cent\"@s is @100/1@.------ The returned 'Rational' is statically guaranteed to be a positive number, and--- to be different from both 'notANumber' and 'infinity'.-scale :: forall proxy scale. GoodScale scale => proxy scale -> Rational -- ^-scale = \_ ->- natVal (Proxy :: Proxy (Fst scale)) %- natVal (Proxy :: Proxy (Snd scale))-{-# INLINABLE scale #-}-------------------------------------------------------------------------------------- | Exchange rate for converting monetary values of currency @src@ into--- monetary values of currency @dst@ by multiplying for it.------ For example, if in order to convert USD to GBP we have to multiply by 1.2345,--- then we can represent this situaion using:------ @--- 'exchangeRate' (12345 % 10000) :: 'Maybe' ('ExchangeRate' \"USD\" \"GBP\")--- @-newtype ExchangeRate (src :: Symbol) (dst :: Symbol) = ExchangeRate Rational- deriving (Eq, Ord, GHC.Generic)--instance forall src dst.- ( KnownSymbol src, KnownSymbol dst- ) => Show (ExchangeRate src dst) where- show = \(ExchangeRate r0) ->- let s = symbolVal (Proxy :: Proxy src)- d = symbolVal (Proxy :: Proxy dst)- in concat [ "ExchangeRate ", show s, " ", show d, " (", show r0, ")" ]--instance forall src dst.- ( KnownSymbol src, KnownSymbol dst- ) => Read (ExchangeRate (src :: Symbol) (dst :: Symbol)) where- readPrec = do- let s = symbolVal (Proxy :: Proxy src)- d = symbolVal (Proxy :: Proxy dst)- _ <- ReadPrec.lift (ReadP.string- ("ExchangeRate " ++ show s ++ " " ++ show d ++ " "))- maybe empty pure =<< fmap exchangeRate readPrec---- | Obtain a 'Rational' representation of the 'ExchangeRate'.------ This 'Rational' is statically guaranteed to be greater than 0, different--- from 'infinity' and different from 'notANumber'.-fromExchangeRate :: ExchangeRate src dst -> Rational-fromExchangeRate = \(ExchangeRate r0) -> r0-{-# INLINABLE fromExchangeRate #-}---- | Safely construct an 'ExchangeRate' from a 'Rational' number.------ For construction to succeed, this 'Rational' must be greater than 0,--- different from 'infinity' and different from 'notANumber'.-exchangeRate :: Rational -> Maybe (ExchangeRate src dst)-exchangeRate = \r0 ->- if (r0 <= 0 || infinity == r0 || notANumber == r0)- then Nothing else Just (ExchangeRate r0)-{-# INLINABLE exchangeRate #-}---- | Flip the direction of an 'ExchangeRate'.------ Identity law:------ @--- 'flipExchangeRate' . 'flipExchangeRate' == 'id'--- @-flipExchangeRate :: ExchangeRate a b -> ExchangeRate b a-flipExchangeRate = \(ExchangeRate x) -> ExchangeRate (1 / x)-{-# INLINABLE flipExchangeRate #-}---- | Apply the 'ExchangeRate' to the given @'Dense' src@ monetary value.------ Identity law:------ @--- 'exchange' ('flipExchangeRate' x) . 'exchange' x == 'id'--- @------ Use the /Identity law/ for reasoning about going back and forth between @src@--- and @dst@ in order to manage any leftovers that might not be representable as--- a 'Discrete' monetary value of @src@.-exchange :: ExchangeRate src dst -> Dense src -> Dense dst-exchange = \(ExchangeRate r) -> \(Dense s) -> Dense (r * s)-{-# INLINABLE exchange #-}------------------------------------------------------------------------------------- DenseRep---- | A monomorphic representation of 'Dense' that is easier to serialize and--- deserialize than 'Dense' in case you don't know the type indexes involved.------ If you are trying to construct a value of this type from some raw input, then--- you will need to use the 'mkDenseRep' function.------ In order to be able to effectively serialize a 'DenseRep' value, you--- need to serialize the following three values (which are the eventual--- arguments to 'mkDenseRep'):------ * 'denseRepCurrency'--- * 'denseRepAmountNumerator'--- * 'denseRepAmountDenominator'-data DenseRep = DenseRep- { _denseRepCurrency :: !String- , _denseRepAmountNumerator :: !Integer- , _denseRepAmountDenominator :: !Integer -- ^ Positive, non-zero.- } deriving (Eq, Show, GHC.Generic)---- | WARNING: This instance does not compare monetary amounts, it just helps you--- sort 'DenseRep' values in case you need to put them in a 'Data.Set.Set' or--- similar.-deriving instance Ord DenseRep---- | Currency name.-denseRepCurrency :: DenseRep -> String-denseRepCurrency = _denseRepCurrency-{-# INLINABLE denseRepCurrency #-}---- | Currency unit amount.-denseRepAmount :: DenseRep -> Rational-denseRepAmount = \dr ->- denseRepAmountNumerator dr % denseRepAmountDenominator dr-{-# INLINABLE denseRepAmount #-}---- | Currency unit amount numerator.-denseRepAmountNumerator :: DenseRep -> Integer-denseRepAmountNumerator = _denseRepAmountNumerator-{-# INLINABLE denseRepAmountNumerator #-}---- | Currency unit amount denominator. Positive, non-zero.-denseRepAmountDenominator :: DenseRep -> Integer-denseRepAmountDenominator = _denseRepAmountDenominator-{-# INLINABLE denseRepAmountDenominator #-}---- | Build a 'DenseRep' from raw values.------ This function is intended for deserialization purposes. You need to convert--- this 'DenseRep' value to a 'Dense' value in order to do any arithmetic--- operation on the monetary value.-mkDenseRep- :: String -- ^ Currency. ('denseRepCurrency')- -> Integer -- ^ Scale numerator. ('denseRepAmountNumerator')- -> Integer -- ^ Scale denominator (positive, non zero). ('denseRepAmountDenominator')- -> Maybe DenseRep-mkDenseRep = \c n d -> case d > 0 of- False -> Nothing- True -> Just (DenseRep c n d)-{-# INLINABLE mkDenseRep #-}---- | Convert a 'Dense' to a 'DenseRep' for ease of serialization.-toDenseRep :: KnownSymbol currency => Dense currency -> DenseRep-toDenseRep = \(Dense r0 :: Dense currency) ->- let c = symbolVal (Proxy :: Proxy currency)- in DenseRep c (numerator r0) (denominator r0)-{-# INLINABLE toDenseRep #-}---- | Attempt to convert a 'DenseRep' to a 'Dense', provided you know the target--- @currency@.-fromDenseRep- :: forall currency- . KnownSymbol currency- => DenseRep- -> Maybe (Dense currency) -- ^-fromDenseRep = \dr ->- case denseRepCurrency dr == symbolVal (Proxy :: Proxy currency) of- False -> Nothing- True -> Just (Dense (denseRepAmount dr))-{-# INLINABLE fromDenseRep #-}---- | Convert a 'DenseRep' to a 'Dense' without knowing the target @currency@.------ Notice that @currency@ here can't leave its intended scope unless you can--- prove equality with some other type at the outer scope, but in that case you--- would be better off using 'fromDenseRep' directly.-withDenseRep- :: DenseRep- -> (forall currency. KnownSymbol currency => Dense currency -> r)- -> r -- ^-withDenseRep dr = \f ->- case someSymbolVal (denseRepCurrency dr) of- SomeSymbol (Proxy :: Proxy currency) ->- f (Dense (denseRepAmount dr) :: Dense currency)-{-# INLINABLE withDenseRep #-}------------------------------------------------------------------------------------- DiscreteRep---- | A monomorphic representation of 'Discrete' that is easier to serialize and--- deserialize than 'Discrete' in case you don't know the type indexes involved.------ If you are trying to construct a value of this type from some raw input, then--- you will need to use the 'mkDiscreteRep' function.------ In order to be able to effectively serialize a 'DiscreteRep' value, you need--- to serialize the following four values (which are the eventual arguments to--- 'mkDiscreteRep'):------ * 'discreteRepCurrency'--- * 'discreteRepScaleNumerator'--- * 'discreteRepScaleDenominator'--- * 'discreteRepAmount'-data DiscreteRep = DiscreteRep- { _discreteRepCurrency :: !String -- ^ Currency name.- , _discreteRepScaleNumerator :: !Integer -- ^ Positive, non-zero.- , _discreteRepScaleDenominator :: !Integer -- ^ Positive, non-zero.- , _discreteRepAmount :: !Integer -- ^ Amount of unit.- } deriving (Eq, Show, GHC.Generic)---- | WARNING: This instance does not compare monetary amounts, it just helps you--- sort 'DiscreteRep' values in case you need to put them in a 'Data.Set.Set' or--- similar.-deriving instance Ord DiscreteRep---- | Currency name.-discreteRepCurrency :: DiscreteRep -> String-discreteRepCurrency = _discreteRepCurrency-{-# INLINABLE discreteRepCurrency #-}---- | Positive, non-zero.-discreteRepScaleNumerator :: DiscreteRep -> Integer-discreteRepScaleNumerator = _discreteRepScaleNumerator-{-# INLINABLE discreteRepScaleNumerator #-}---- | Positive, non-zero.-discreteRepScaleDenominator :: DiscreteRep -> Integer-discreteRepScaleDenominator = _discreteRepScaleDenominator-{-# INLINABLE discreteRepScaleDenominator #-}---- | Amount of currency unit.-discreteRepAmount :: DiscreteRep -> Integer-discreteRepAmount = _discreteRepAmount-{-# INLINABLE discreteRepAmount #-}---- | Positive, non-zero.-discreteRepScale :: DiscreteRep -> Rational-discreteRepScale = \dr ->- discreteRepScaleNumerator dr % discreteRepScaleDenominator dr-{-# INLINABLE discreteRepScale #-}----- | Internal. Build a 'DiscreteRep' from raw values.------ This function is intended for deserialization purposes. You need to convert--- this 'DiscreteRep' value to a 'Discrete' vallue in order to do any arithmetic--- operation on the monetary value.-mkDiscreteRep- :: String -- ^ Currency name. ('discreteRepCurrency')- -> Integer -- ^ Scale numerator. Positive, non-zero. ('discreteRepScaleNumerator')- -> Integer -- ^ Scale denominator. Positive, non-zero. ('discreteRepScaleDenominator')- -> Integer -- ^ Amount of unit. ('discreteRepAmount')- -> Maybe DiscreteRep-mkDiscreteRep = \c n d a -> case (n > 0) && (d > 0) of- False -> Nothing- True -> Just (DiscreteRep c n d a)-{-# INLINABLE mkDiscreteRep #-}---- | Convert a 'Discrete' to a 'DiscreteRep' for ease of serialization.-toDiscreteRep- :: (KnownSymbol currency, GoodScale scale)- => Discrete' currency scale- -> DiscreteRep -- ^-toDiscreteRep = \(Discrete i0 :: Discrete' currency scale) ->- let c = symbolVal (Proxy :: Proxy currency)- n = natVal (Proxy :: Proxy (Fst scale))- d = natVal (Proxy :: Proxy (Snd scale))- in DiscreteRep c n d i0-{-# INLINABLE toDiscreteRep #-}---- | Attempt to convert a 'DiscreteRep' to a 'Discrete', provided you know the--- target @currency@ and @unit@.-fromDiscreteRep- :: forall currency scale- . (KnownSymbol currency, GoodScale scale)- => DiscreteRep- -> Maybe (Discrete' currency scale) -- ^-fromDiscreteRep = \dr ->- if (discreteRepCurrency dr == symbolVal (Proxy :: Proxy currency)) &&- (discreteRepScaleNumerator dr == natVal (Proxy :: Proxy (Fst scale))) &&- (discreteRepScaleDenominator dr == natVal (Proxy :: Proxy (Snd scale)))- then Just (Discrete (discreteRepAmount dr))- else Nothing-{-# INLINABLE fromDiscreteRep #-}---- | Convert a 'DiscreteRep' to a 'Discrete' without knowing the target--- @currency@ and @unit@.------ Notice that @currency@ and @unit@ here can't leave its intended scope unless--- you can prove equality with some other type at the outer scope, but in that--- case you would be better off using 'fromDiscreteRep' directly.------ Notice that you may need to add an explicit type to the result of this--- function in order to keep the compiler happy.-withDiscreteRep- :: forall r- . DiscreteRep- -> ( forall currency scale.- ( KnownSymbol currency- , GoodScale scale- ) => Discrete' currency scale- -> r )- -> r -- ^-withDiscreteRep dr = \f ->- case someSymbolVal (discreteRepCurrency dr) of- SomeSymbol (Proxy :: Proxy currency) ->- case someNatVal (discreteRepScaleNumerator dr) of- Nothing -> error "withDiscreteRep: impossible: numerator < 0"- Just (SomeNat (Proxy :: Proxy num)) ->- case someNatVal (discreteRepScaleDenominator dr) of- Nothing -> error "withDiscreteRep: impossible: denominator < 0"- Just (SomeNat (Proxy :: Proxy den)) ->- case mkGoodScale of- Nothing -> error "withDiscreteRep: impossible: mkGoodScale"- Just (Dict :: Dict (GoodScale '(num, den))) ->- f (Discrete (discreteRepAmount dr)- :: Discrete' currency '(num, den))-{-# INLINABLE withDiscreteRep #-}------------------------------------------------------------------------------------- ExchangeRateRep---- | A monomorphic representation of 'ExchangeRate' that is easier to serialize--- and deserialize than 'ExchangeRate' in case you don't know the type indexes--- involved.------ If you are trying to construct a value of this type from some raw input, then--- you will need to use the 'mkExchangeRateRep' function.------ In order to be able to effectively serialize an 'ExchangeRateRep' value, you--- need to serialize the following four values (which are the eventual arguments--- to 'mkExchangeRateRep'):------ * 'exchangeRateRepSrcCurrency'--- * 'exchangeRateRepDstCurrency'--- * 'exchangeRateRepRateNumerator'--- * 'exchangeRateRepRateDenominator'-data ExchangeRateRep = ExchangeRateRep- { _exchangeRateRepSrcCurrency :: !String- , _exchangeRateRepDstCurrency :: !String- , _exchangeRateRepRateNumerator :: !Integer -- ^ Positive, non-zero.- , _exchangeRateRepRateDenominator :: !Integer -- ^ Positive, non-zero.- } deriving (Eq, Show, GHC.Generic)---- | WARNING: This instance does not compare monetary amounts, it just helps you--- sort 'ExchangeRateRep' values in case you need to put them in a--- 'Data.Set.Set' or similar.-deriving instance Ord ExchangeRateRep---- | Source currency name.-exchangeRateRepSrcCurrency :: ExchangeRateRep -> String-exchangeRateRepSrcCurrency = _exchangeRateRepSrcCurrency-{-# INLINABLE exchangeRateRepSrcCurrency #-}---- | Destination currency name.-exchangeRateRepDstCurrency :: ExchangeRateRep -> String-exchangeRateRepDstCurrency = _exchangeRateRepDstCurrency-{-# INLINABLE exchangeRateRepDstCurrency #-}---- | Exchange rate. Positive, non-zero.-exchangeRateRepRate :: ExchangeRateRep -> Rational-exchangeRateRepRate = \x ->- exchangeRateRepRateNumerator x % _exchangeRateRepRateDenominator x-{-# INLINABLE exchangeRateRepRate #-}---- | Exchange rate numerator. Positive, non-zero.-exchangeRateRepRateNumerator :: ExchangeRateRep -> Integer-exchangeRateRepRateNumerator = _exchangeRateRepRateNumerator-{-# INLINABLE exchangeRateRepRateNumerator #-}---- | Exchange rate denominator. Positive, non-zero.-exchangeRateRepRateDenominator :: ExchangeRateRep -> Integer-exchangeRateRepRateDenominator = _exchangeRateRepRateDenominator-{-# INLINABLE exchangeRateRepRateDenominator #-}---- | Internal. Build a 'ExchangeRateRep' from raw values.------ This function is intended for deserialization purposes. You need to convert--- this 'ExchangeRateRep' value to a 'ExchangeRate' value in order to do any--- arithmetic operation with the exchange rate.-mkExchangeRateRep- :: String -- ^ Source currency name. ('exchangeRateRepSrcCurrency')- -> String -- ^ Destination currency name. ('exchangeRateRepDstCurrency')- -> Integer -- ^ Exchange rate numerator. Positive, non-zero. ('exchangeRateRepRateNumerator')- -> Integer -- ^ Exchange rate denominator. Positive, non-zero. ('exchangeRateRepRateDenominator')- -> Maybe ExchangeRateRep-mkExchangeRateRep = \src dst n d -> case (n > 0) && (d > 0) of- False -> Nothing- True -> Just (ExchangeRateRep src dst n d)-{-# INLINABLE mkExchangeRateRep #-}---- | Convert a 'ExchangeRate' to a 'DiscreteRep' for ease of serialization.-toExchangeRateRep- :: (KnownSymbol src, KnownSymbol dst)- => ExchangeRate src dst- -> ExchangeRateRep -- ^-toExchangeRateRep = \(ExchangeRate r0 :: ExchangeRate src dst) ->- let src = symbolVal (Proxy :: Proxy src)- dst = symbolVal (Proxy :: Proxy dst)- in ExchangeRateRep src dst (numerator r0) (denominator r0)-{-# INLINABLE toExchangeRateRep #-}---- | Attempt to convert a 'ExchangeRateRep' to a 'ExchangeRate', provided you--- know the target @src@ and @dst@ types.-fromExchangeRateRep- :: forall src dst- . (KnownSymbol src, KnownSymbol dst)- => ExchangeRateRep- -> Maybe (ExchangeRate src dst) -- ^-fromExchangeRateRep = \x ->- if (exchangeRateRepSrcCurrency x == symbolVal (Proxy :: Proxy src)) &&- (exchangeRateRepDstCurrency x == symbolVal (Proxy :: Proxy dst))- then Just (ExchangeRate (exchangeRateRepRate x))- else Nothing-{-# INLINABLE fromExchangeRateRep #-}---- | Convert a 'ExchangeRateRep' to a 'ExchangeRate' without knowing the target--- @currency@ and @unit@.------ Notice that @src@ and @dst@ here can't leave its intended scope unless--- you can prove equality with some other type at the outer scope, but in that--- case you would be better off using 'fromExchangeRateRep' directly.-withExchangeRateRep- :: ExchangeRateRep- -> ( forall src dst.- ( KnownSymbol src- , KnownSymbol dst- ) => ExchangeRate src dst- -> r )- -> r -- ^-withExchangeRateRep x = \f ->- case someSymbolVal (exchangeRateRepSrcCurrency x) of- SomeSymbol (Proxy :: Proxy src) ->- case someSymbolVal (exchangeRateRepDstCurrency x) of- SomeSymbol (Proxy :: Proxy dst) ->- f (ExchangeRate (exchangeRateRepRate x) :: ExchangeRate src dst)-{-# INLINABLE withExchangeRateRep #-}------------------------------------------------------------------------------------- Miscellaneous--type family Fst (ab :: (ka, kb)) :: ka where Fst '(a,b) = a-type family Snd (ab :: (ka, kb)) :: ka where Snd '(a,b) = b------------------------------------------------------------------------------------- Extra instances: hashable-#ifdef VERSION_hashable-instance Hashable (Dense currency)-instance Hashable DenseRep-instance GoodScale scale => Hashable (Discrete' currency scale)-instance Hashable DiscreteRep-instance Hashable (ExchangeRate src dst)-instance Hashable ExchangeRateRep-#endif------------------------------------------------------------------------------------- Extra instances: deepseq-#ifdef VERSION_deepseq-instance NFData (Dense currency)-instance NFData DenseRep-instance GoodScale scale => NFData (Discrete' currency scale)-instance NFData DiscreteRep-instance NFData (ExchangeRate src dst)-instance NFData ExchangeRateRep-#endif------------------------------------------------------------------------------------- Extra instances: cereal-#ifdef VERSION_cereal--- | Compatible with 'DenseRep'.-instance (KnownSymbol currency) => Cereal.Serialize (Dense currency) where- put = Cereal.put . toDenseRep- get = maybe empty pure =<< fmap fromDenseRep Cereal.get--- | Compatible with 'DiscreteRep'.-instance- ( KnownSymbol currency, GoodScale scale- ) => Cereal.Serialize (Discrete' currency scale) where- put = Cereal.put . toDiscreteRep- get = maybe empty pure =<< fmap fromDiscreteRep Cereal.get--- | Compatible with 'ExchangeRateRep'.-instance- ( KnownSymbol src, KnownSymbol dst- ) => Cereal.Serialize (ExchangeRate src dst) where- put = Cereal.put . toExchangeRateRep- get = maybe empty pure =<< fmap fromExchangeRateRep Cereal.get--- | Compatible with 'Dense'.-instance Cereal.Serialize DenseRep where- put = \(DenseRep c n d) -> Cereal.put c >> Cereal.put n >> Cereal.put d- get = maybe empty pure =<< mkDenseRep- <$> Cereal.get <*> Cereal.get <*> Cereal.get--- | Compatible with 'Discrete'.-instance Cereal.Serialize DiscreteRep where- put = \(DiscreteRep c n d a) ->- Cereal.put c >> Cereal.put n >> Cereal.put d >> Cereal.put a- get = maybe empty pure =<< mkDiscreteRep- <$> Cereal.get <*> Cereal.get <*> Cereal.get <*> Cereal.get--- | Compatible with 'ExchangeRate'.-instance Cereal.Serialize ExchangeRateRep where- put = \(ExchangeRateRep src dst n d) ->- Cereal.put src >> Cereal.put dst >> Cereal.put n >> Cereal.put d- get = maybe empty pure =<< mkExchangeRateRep- <$> Cereal.get <*> Cereal.get <*> Cereal.get <*> Cereal.get-#endif------------------------------------------------------------------------------------- Extra instances: binary-#ifdef VERSION_binary--- | Compatible with 'DenseRep'.-instance (KnownSymbol currency) => Binary.Binary (Dense currency) where- put = Binary.put . toDenseRep- get = maybe empty pure =<< fmap fromDenseRep Binary.get--- | Compatible with 'DiscreteRep'.-instance- ( KnownSymbol currency, GoodScale scale- ) => Binary.Binary (Discrete' currency scale) where- put = Binary.put . toDiscreteRep- get = maybe empty pure =<< fmap fromDiscreteRep Binary.get--- | Compatible with 'ExchangeRateRep'.-instance- ( KnownSymbol src, KnownSymbol dst- ) => Binary.Binary (ExchangeRate src dst) where- put = Binary.put . toExchangeRateRep- get = maybe empty pure =<< fmap fromExchangeRateRep Binary.get--- | Compatible with 'Dense'.-instance Binary.Binary DenseRep where- put = \(DenseRep c n d) -> Binary.put c >> Binary.put n >> Binary.put d- get = maybe empty pure =<< mkDenseRep- <$> Binary.get <*> Binary.get <*> Binary.get--- | Compatible with 'Discrete'.-instance Binary.Binary DiscreteRep where- put = \(DiscreteRep c n d a) ->- Binary.put c >> Binary.put n >> Binary.put d >> Binary.put a- get = maybe empty pure =<< mkDiscreteRep- <$> Binary.get <*> Binary.get <*> Binary.get <*> Binary.get--- | Compatible with 'ExchangeRate'.-instance Binary.Binary ExchangeRateRep where- put = \(ExchangeRateRep src dst n d) ->- Binary.put src >> Binary.put dst >> Binary.put n >> Binary.put d- get = maybe empty pure =<< mkExchangeRateRep- <$> Binary.get <*> Binary.get <*> Binary.get <*> Binary.get-#endif------------------------------------------------------------------------------------- Extra instances: aeson-#ifdef VERSION_aeson--- | Compatible with 'DenseRep'-instance KnownSymbol currency => Ae.ToJSON (Dense currency) where- toJSON = Ae.toJSON . toDenseRep--- | Compatible with 'DenseRep'-instance KnownSymbol currency => Ae.FromJSON (Dense currency) where- parseJSON = maybe empty pure <=< fmap fromDenseRep . Ae.parseJSON--- | Compatible with 'Dense'-instance Ae.ToJSON DenseRep where- toJSON = \(DenseRep c n d) -> Ae.toJSON ("Dense", c, n, d)--- | Compatible with 'Dense'-instance Ae.FromJSON DenseRep where- parseJSON = \v -> do- ("Dense", c, n, d) <- Ae.parseJSON v- maybe empty pure (mkDenseRep c n d)--- | Compatible with 'DiscreteRep'-instance- ( KnownSymbol currency, GoodScale scale- ) => Ae.ToJSON (Discrete' currency scale) where- toJSON = Ae.toJSON . toDiscreteRep--- | Compatible with 'DiscreteRep'-instance- ( KnownSymbol currency, GoodScale scale- ) => Ae.FromJSON (Discrete' currency scale) where- parseJSON = maybe empty pure <=< fmap fromDiscreteRep . Ae.parseJSON--- | Compatible with 'Discrete''-instance Ae.ToJSON DiscreteRep where- toJSON = \(DiscreteRep c n d a) -> Ae.toJSON ("Discrete", c, n, d, a)--- | Compatible with 'Discrete''-instance Ae.FromJSON DiscreteRep where- parseJSON = \v -> do- ("Discrete", c, n, d, a) <- Ae.parseJSON v- maybe empty pure (mkDiscreteRep c n d a)--- | Compatible with 'ExchangeRateRep'-instance- ( KnownSymbol src, KnownSymbol dst- ) => Ae.ToJSON (ExchangeRate src dst) where- toJSON = Ae.toJSON . toExchangeRateRep--- | Compatible with 'ExchangeRateRep'-instance- ( KnownSymbol src, KnownSymbol dst- ) => Ae.FromJSON (ExchangeRate src dst) where- parseJSON = maybe empty pure <=< fmap fromExchangeRateRep . Ae.parseJSON--- | Compatible with 'ExchangeRate'-instance Ae.ToJSON ExchangeRateRep where- toJSON = \(ExchangeRateRep src dst n d) ->- Ae.toJSON ("ExchangeRate", src, dst, n, d)--- | Compatible with 'ExchangeRate'-instance Ae.FromJSON ExchangeRateRep where- parseJSON = \v -> do- ("ExchangeRate", src, dst, n, d) <- Ae.parseJSON v- maybe empty pure (mkExchangeRateRep src dst n d)-#endif------------------------------------------------------------------------------------- Extra instances: store-#ifdef VERSION_store--- | Compatible with 'DenseRep'.-instance (KnownSymbol currency) => Store.Store (Dense currency) where- size = storeContramapSize toDenseRep Store.size- poke = Store.poke . toDenseRep- peek = maybe (fail "peek") pure =<< fmap fromDenseRep Store.peek--- | Compatible with 'Dense'.-instance Store.Store DenseRep where- poke = \(DenseRep c n d) -> Store.poke c >> Store.poke n >> Store.poke d- peek = maybe (fail "peek") pure =<< do- mkDenseRep <$> Store.peek <*> Store.peek <*> Store.peek---- | Compatible with 'DiscreteRep'.-instance- ( KnownSymbol currency, GoodScale scale- ) => Store.Store (Discrete' currency scale) where- size = storeContramapSize toDiscreteRep Store.size- poke = Store.poke . toDiscreteRep- peek = maybe (fail "peek") pure =<< fmap fromDiscreteRep Store.peek--- | Compatible with 'Discrete''.-instance Store.Store DiscreteRep where- poke = \(DiscreteRep c n d a) ->- Store.poke c >> Store.poke n >> Store.poke d >> Store.poke a- peek = maybe (fail "peek") pure =<< do- mkDiscreteRep <$> Store.peek <*> Store.peek <*> Store.peek <*> Store.peek--- | Compatible with 'ExchangeRateRep'.-instance- ( KnownSymbol src, KnownSymbol dst- ) => Store.Store (ExchangeRate src dst) where- size = storeContramapSize toExchangeRateRep Store.size- poke = Store.poke . toExchangeRateRep- peek = maybe (fail "peek") pure =<< fmap fromExchangeRateRep Store.peek--- | Compatible with 'ExchangeRate'.-instance Store.Store ExchangeRateRep where- poke = \(ExchangeRateRep src dst n d) ->- Store.poke src >> Store.poke dst >> Store.poke n >> Store.poke d- peek = maybe (fail "peek") pure =<< mkExchangeRateRep- <$> Store.peek <*> Store.peek <*> Store.peek <*> Store.peek--storeContramapSize :: (a -> b) -> Store.Size b -> Store.Size a-storeContramapSize f = \case- Store.VarSize g -> Store.VarSize (g . f)- Store.ConstSize x -> Store.ConstSize x-{-# INLINABLE storeContramapSize #-}-#endif+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}++#if MIN_VERSION_base(4,9,0)+{-# OPTIONS_GHC -Wno-redundant-constraints #-}+#endif++-- | This is an internal module. Import "Money" instead.+module Money.Internal+ ( -- * Dense monetary values+ Dense+ , dense+ -- * Discrete monetary values+ , Discrete+ , Discrete'+ , fromDiscrete+ , round+ , ceiling+ , floor+ , truncate+ -- * Currency scales+ , Scale+ , GoodScale+ , ErrScaleNonCanonical+ , scale+ -- * Currency exchange+ , ExchangeRate+ , exchangeRate+ , fromExchangeRate+ , flipExchangeRate+ , exchange+ -- * Serializable representations+ , SomeDense+ , toSomeDense+ , mkSomeDense+ , fromSomeDense+ , withSomeDense+ , someDenseCurrency+ , someDenseAmount+ , SomeDiscrete+ , toSomeDiscrete+ , mkSomeDiscrete+ , fromSomeDiscrete+ , withSomeDiscrete+ , someDiscreteCurrency+ , someDiscreteScale+ , someDiscreteAmount+ , SomeExchangeRate+ , toSomeExchangeRate+ , mkSomeExchangeRate+ , fromSomeExchangeRate+ , withSomeExchangeRate+ , someExchangeRateSrcCurrency+ , someExchangeRateDstCurrency+ , someExchangeRateRate+ ) where++import Control.Applicative ((<|>), empty)+import Control.Monad ((<=<), guard, when)+import Data.Constraint (Dict(Dict))+import Data.Monoid ((<>))+import Data.Proxy (Proxy(..))+import Data.Ratio ((%), numerator, denominator)+import GHC.Exts (fromList)+import qualified GHC.Generics as GHC+import GHC.Real (infinity, notANumber)+import GHC.TypeLits+ (Symbol, SomeSymbol(..), Nat, SomeNat(..), CmpNat, KnownSymbol, KnownNat,+ natVal, someNatVal, symbolVal, someSymbolVal)+import Prelude hiding (round, ceiling, floor, truncate)+import qualified Prelude+import qualified Text.ParserCombinators.ReadPrec as ReadPrec+import qualified Text.ParserCombinators.ReadP as ReadP+import qualified Text.Read as Read+import Unsafe.Coerce (unsafeCoerce)++#ifdef HAS_aeson+import qualified Data.Aeson as Ae+#endif++#ifdef HAS_binary+import qualified Data.Binary as Binary+#endif++#ifdef HAS_cereal+import qualified Data.Serialize as Cereal+#endif++#ifdef HAS_deepseq+import Control.DeepSeq (NFData)+#endif++#ifdef HAS_hashable+import Data.Hashable (Hashable)+#endif++#ifdef HAS_serialise+import qualified Codec.Serialise as Ser+#endif++#ifdef HAS_store+import qualified Data.Store as Store+#endif++#ifdef HAS_xmlbf+import qualified Xmlbf+import qualified Data.Text as Text+#endif++#if MIN_VERSION_base(4,9,0)+import qualified GHC.TypeLits as GHC+#endif++--------------------------------------------------------------------------------+-- | 'Dense' represents a dense monetary value for @currency@ (usually a+-- ISO-4217 currency code, but not necessarily) as a rational number.+--+-- While monetary values associated with a particular currency are discrete, you+-- can still treat monetary values as dense while operating on them. For+-- example, the half 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 eventually multiply @USD 1.705@+-- by @4@, for example, 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+-- 'Discrete' monetary values as much as posible. Once you are ready to+-- aproximate a 'Dense' value to a 'Discrete' value you can use one of+-- 'round', 'floor', 'ceiling' or 'truncate'. Otherwise, using 'toRational' you+-- can obtain a precise 'Rational' representation.+--+-- Construct 'Dense' monetary values using 'dense', 'fromRational',+-- 'fromInteger' or 'fromIntegral'.+--+-- /WARNING/ if you want to treat a dense monetary value as a /Real/ number (for+-- example, to take the square root of that monetary value), then you are on+-- your own. We can only guarantee lossless manipulation of rational values, so+-- you will need to convert back and forth betwen the 'Rational' representation+-- for 'Dense' and your (likely lossy) representation for /Real/ numbers.+newtype Dense (currency :: Symbol) = Dense Rational+ deriving (Eq, Ord, Num, Real, GHC.Generic)++-- | /WARNING/ if there exists the possibility that the given 'Rational' has a+-- zero as a denominator, which although unlikely, is possible if the 'Rational'+-- is constructed unsafely using 'GHC.Real.infinity' or 'GHC.Real.notANumber'+-- for example, then use 'dense' instead of 'fromRational'.+deriving instance Fractional (Dense (currency :: Symbol))++instance forall currency. KnownSymbol currency => Show (Dense currency) where+ showsPrec n = \(Dense r0) ->+ let c = symbolVal (Proxy :: Proxy currency)+ in showParen (n > 10) $+ showString "Dense " . showsPrec 0 c . showChar ' ' .+ showsPrec 0 (numerator r0) . showChar '%' .+ showsPrec 0 (denominator r0)++instance forall currency. KnownSymbol currency => Read (Dense currency) where+ readPrec = Read.parens $ do+ let c = symbolVal (Proxy :: Proxy currency)+ _ <- ReadPrec.lift (ReadP.string ("Dense " ++ show c ++ " "))+ maybe empty pure =<< fmap dense Read.readPrec++-- | Build a 'Dense' monetary value from a 'Rational' value.+--+-- For example, if you want to represent @USD 12.52316@, then you can use:+--+-- @+-- 'dense' (125316 % 10000)+-- @+--+-- Returns 'Nothing' in case the denominator of the given 'Rational' is zero,+-- which although unlikely, is possible if the 'Rational' is constructed+-- unsafely using 'GHC.Real.infinity' or 'GHC.Real.notANumber', for example.+-- If you don't care about that scenario, you can use `fromRational` to build+-- the `Dense` value.+dense :: Rational -> Maybe (Dense currency)+dense = \r0 -> if (denominator r0 == 0) then Nothing else Just (Dense r0)+{-# INLINABLE dense #-}++-- | 'Discrete' represents a discrete monetary value for a @currency@ expresed+-- as an integer amount of a particular @unit@. For example, with @currency ~+-- \"USD\"@ and @unit ~ \"cent\"@ you can represent United States Dollars to+-- their full extent.+--+-- @currency@ is usually a ISO-4217 currency code, but not necessarily.+--+-- Construct 'Discrete' values using 'fromInteger'.+--+-- For example, if you want to represent @GBP 21.05@, where the smallest+-- represetable unit for a GBP (United Kingdom Pound) is the /penny/, and 100+-- /pennies/ equal 1 GBP (i.e., @'Scale' \"GBP\" ~ '(100, 1)@), then you can+-- use:+--+-- @+-- 'fromInteger' 2105 :: Discrete \"GBP\" \"penny\"+-- @+--+-- Because @2015 / 100 == 20.15@.+type Discrete (currency :: Symbol) (unit :: Symbol)+ = Discrete' currency (Scale currency unit)++-- | 'Discrete'' represents a discrete monetary value for a @currency@ expresed+-- as an amount of @scale@, which is a rational number expressed as @(numerator,+-- denominator)@.+--+-- You'll be using 'Discrete' instead of 'Discrete'' most of the time, which+-- mentions the unit name (such as /cent/ or /centavo/) instead of explicitely+-- mentioning the unit scale.+newtype Discrete' (currency :: Symbol) (scale :: (Nat, Nat))+ = Discrete Integer++deriving instance GoodScale scale => Eq (Discrete' currency scale)+deriving instance GoodScale scale => Ord (Discrete' currency scale)+deriving instance GoodScale scale => Enum (Discrete' currency scale)+deriving instance GoodScale scale => Num (Discrete' currency scale)+deriving instance GoodScale scale => Real (Discrete' currency scale)+deriving instance GoodScale scale => Integral (Discrete' currency scale)+deriving instance GoodScale scale => GHC.Generic (Discrete' currency scale)++instance forall currency scale.+ ( KnownSymbol currency, GoodScale scale+ ) => Show (Discrete' currency scale) where+ showsPrec n = \d0@(Discrete i0) ->+ let c = symbolVal (Proxy :: Proxy currency)+ s = scale d0+ in showParen (n > 10) $+ showString "Discrete " . showsPrec 0 c . showChar ' ' .+ showsPrec 0 (numerator s) . showChar '%' .+ showsPrec 0 (denominator s) . showChar ' ' .+ showsPrec 0 i0++instance forall currency scale.+ ( KnownSymbol currency, GoodScale scale+ ) => Read (Discrete' currency scale) where+ readPrec = Read.parens $ do+ let c = symbolVal (Proxy :: Proxy currency)+ s = scale (Proxy :: Proxy scale)+ _ <- ReadPrec.lift (ReadP.string (concat+ [ "Discrete ", show c, " "+ , show (numerator s), "%"+ , show (denominator s), " "+ ]))+ Discrete <$> Read.readPrec++#if MIN_VERSION_base(4,9,0)+instance+ ( GHC.TypeError+ (('GHC.Text "The ") 'GHC.:<>:+ ('GHC.ShowType Discrete') 'GHC.:<>:+ ('GHC.Text " type is deliberately not a ") 'GHC.:<>:+ ('GHC.ShowType Fractional) 'GHC.:$$:+ ('GHC.Text "instance. Convert the ") 'GHC.:<>:+ ('GHC.ShowType Discrete') 'GHC.:<>:+ ('GHC.Text " value to a ") 'GHC.:<>:+ ('GHC.ShowType Dense) 'GHC.:$$:+ ('GHC.Text "value and use the ") 'GHC.:<>:+ ('GHC.ShowType Fractional) 'GHC.:<>:+ ('GHC.Text " features on it instead."))+ , GoodScale scale+ ) => Fractional (Discrete' currency scale) where+ fromRational = undefined+ recip = undefined+#endif++-- | Convert currency 'Discrete' monetary value into a 'Dense' monetary+-- value.+fromDiscrete+ :: GoodScale scale+ => Discrete' currency scale+ -> Dense currency -- ^+fromDiscrete = \c@(Discrete i) -> Dense (fromInteger i / scale c)+{-# INLINABLE fromDiscrete #-}++-- | Internal. Used to implement 'round', 'ceiling', 'floor' and 'truncate'.+roundf+ :: forall currency scale+ . GoodScale scale+ => (Rational -> Integer) -- ^ 'Prelude.round', 'Prelude.ceiling' or similar.+ -> Dense currency+ -> (Discrete' currency scale, Dense currency)+roundf f = \c0 ->+ let !r0 = toRational c0 :: Rational+ !r1 = scale (Proxy :: Proxy scale)+ !i2 = f (r0 * r1) :: Integer+ !r2 = fromInteger i2 / r1 :: Rational+ !d2 = Discrete i2+ !rest = Dense (r0 - r2)+ in (d2, rest)+{-# INLINABLE roundf #-}++-- | Round a 'Dense' value @x@ to the nearest value fully representable in+-- its @currency@'s @unit@ 'Scale', which might be @x@ itself.+--+-- If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',+-- then the following holds:+--+-- @+-- 'round' x == ('fromDiscrete' x, 0)+-- @+--+-- Otherwise, if the nearest value to @x@ that is fully representable in its+-- @currency@'s @unit@ 'Scale' is greater than @x@, then the following holds:+--+-- @+-- 'round' == 'ceiling'+-- @+--+-- Otherwise, the nearest value to @x@ that is fully representable in its+-- @currency@'s @unit@ 'Scale' is smaller than @x@, and the following holds:+--+-- @+-- 'round' == 'floor'+-- @+--+-- Proof that 'round' doesn't lose money:+--+-- @+-- x == case 'round' x of (y, z) -> 'fromDiscrete' y + z+-- @+round+ :: GoodScale scale+ => Dense currency+ -> (Discrete' currency scale, Dense currency) -- ^+round = roundf Prelude.round+{-# INLINABLE round #-}++-- | Round a 'Dense' value @x@ to the nearest value fully representable in+-- its @currency@'s @unit@ 'Scale' which is greater than @x@ or equal to @x@.+--+--+-- If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',+-- then the following holds:+--+-- @+-- 'ceiling' x == ('fromDiscrete' x, 0)+-- @+--+-- Otherwise, if @x@ is not representable in its @currency@'s @unit@ 'Scale',+-- then the following holds:+--+-- @+-- 'ceiling' x == (y, 'Just' z)+-- @+--+-- @+-- x /= y+-- @+--+-- @+-- z < 'zero'+-- @+--+-- Proof that 'ceiling' doesn't lose money:+--+-- @+-- x == case 'ceiling' x of (y, z) -> 'fromDiscrete' y + z+-- @+ceiling+ :: GoodScale scale+ => Dense currency+ -> (Discrete' currency scale, Dense currency) -- ^+ceiling = roundf Prelude.ceiling+{-# INLINABLE ceiling #-}++-- | Round a 'Dense' value @x@ to the nearest value fully representable in+-- its @currency@'s @unit@ 'Scale' which is smaller than @x@ or equal to @x@.+--+--+-- If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',+-- then the following holds:+--+-- @+-- 'floor' x == ('fromDiscrete' x, 0)+-- @+--+-- Otherwise, if @x@ is not representable in its @currency@'s @unit@ 'Scale',+-- then the following holds:+--+-- @+-- 'floor' x == (y, 'Just' z)+-- @+--+-- @+-- x /= y+-- @+--+-- @+-- z > 'zero'+-- @+--+-- Proof that 'floor' doesn't lose money:+--+-- @+-- x == case 'floor' x of (y, z) -> 'fromDiscrete' y + z+-- @+floor+ :: GoodScale scale+ => Dense currency+ -> (Discrete' currency scale, Dense currency) -- ^+floor = roundf Prelude.floor+{-# INLINABLE floor #-}++-- | Round a 'Dense' value @x@ to the nearest value between zero and+-- @x@ (inclusive) which is fully representable in its @currency@'s @unit@+-- 'Scale'.+--+-- If @x@ is already fully representable in its @currency@'s @unit@ 'Scale',+-- then the following holds:+--+-- @+-- 'truncate' x == ('fromDiscrete' x, 0)+-- @+--+-- Otherwise, if @x@ is positive, then the following holds:+--+-- @+-- 'truncate' == 'floor'+-- @+--+-- Otherwise, if @x@ is negative, the following holds:+--+-- @+-- 'truncate' == 'ceiling'+-- @+--+-- Proof that 'truncate' doesn't lose money:+--+-- @+-- x == case 'truncate' x of (y, z) -> 'fromDiscrete' y + z+-- @+truncate+ :: GoodScale scale+ => Dense currency+ -> (Discrete' currency scale, Dense currency) -- ^+truncate = roundf Prelude.truncate+{-# INLINABLE truncate #-}++--------------------------------------------------------------------------------++-- | @'Scale' currency unit@ is a rational number (expressed as @'(numerator,+-- denominator)@) indicating how many pieces of @unit@ fit in @currency@.+--+-- @currency@ is usually a ISO-4217 currency code, but not necessarily.+--+-- The 'Scale' will determine how to convert a 'Dense' value into a+-- 'Discrete' value and vice-versa.+--+-- For example, there are 100 USD cents in 1 USD, so the scale for this+-- relationship is:+--+-- @+-- type instance 'Scale' \"USD\" \"cent\" = '(100, 1)+-- @+--+-- As another example, there is 1 dollar in USD, so the scale for this+-- relationship is:+--+-- @+-- type instance 'Scale' \"USD\" \"dollar\" = '(1, 1)+-- @+--+-- When using 'Discrete' values to represent money, it will be impossible to+-- represent an amount of @currency@ smaller than @unit@. So, if you decide to+-- use @Scale \"USD\" \"dollar\"@ as your scale, you will not be able to+-- represent values such as USD 3.50 or USD 21.87, since they are not exact+-- multiples of a dollar.+--+-- If there exists a canonical smallest @unit@ that can fully represent the+-- currency, then an instance @'Scale' currency currency@ exists.+--+-- @+-- type instance 'Scale' \"USD\" \"USD\" = Scale \"USD\" \"cent\"+-- @+--+-- For some monetary values, such as precious metals, the smallest representable+-- unit is not obvious, since you can continue to split the precious metal many+-- times before it stops being a precious metal. Still, for practical purposes+-- we can make a sane arbitrary choice of smallest unit. For example, the base+-- unit for XAU (Gold) is the /troy ounce/, which is too big to be considered+-- the smallest unit, but we can arbitrarily choose the /milligrain/ as our+-- smallest unit, which is about as heavy as a single grain of table salt and+-- should be sufficiently precise for all monetary practical purposes. A /troy+-- ounce/ equals 480000 /milligrains/.+--+-- @+-- type instance 'Scale' \"XAG\" \"milligrain\" = '(480000, 1)+-- @+--+-- You can use other units such as /milligrams/ for measuring XAU, for example.+-- However, since the amount of /milligrams/ in a /troy ounce/ (31103.477) is+-- not integral, we need to use rational number to express it.+--+-- @+-- type instance 'Scale' \"XAU\" \"milligram\" = '(31103477, 1000)+-- @+--+-- If you try to obtain the 'Scale of a @currency@ without an obvious smallest+-- representable @unit@, like XAU, you will get a compile error.+type family Scale (currency :: Symbol) (unit :: Symbol) :: (Nat, Nat)++#if MIN_VERSION_base(4,9,0)+-- | A friendly 'GHC.TypeError' to use for a @currency@ that doesn't have a+-- canonical small unit.+type family ErrScaleNonCanonical (currency :: Symbol) :: k where+ ErrScaleNonCanonical c = GHC.TypeError+ ( 'GHC.Text c 'GHC.:<>:+ 'GHC.Text " is not a currency with a canonical smallest unit," 'GHC.:$$:+ 'GHC.Text "be explicit about the currency unit you want to use." )+#else+-- | Forbid a @currency@ that doesn't have a canonical small unit.+--+-- In GHC versions before 8.0 we can't provide a nice error message here, so we+-- simply set this to a value that will fail to satisfy 'GoodScale'. As a+-- consequence, trying to use this 'Scale' will result in a cryptic error saying+-- /«@Couldn't match type ‘'EQ’ with ‘'LT’@»/.+type ErrScaleNonCanonical (currency :: Symbol) = '(0, 0)+#endif++-- | Constraints to a scale (like the one returned by @'Scale' currency unit@)+-- expected to always be satisfied. In particular, the scale is always+-- guaranteed to be a positive rational number ('infinity' and 'notANumber' are+-- forbidden by 'GoodScale').+type GoodScale (scale :: (Nat, Nat))+ = ( CmpNat 0 (Fst scale) ~ 'LT+ , CmpNat 0 (Snd scale) ~ 'LT+ , KnownNat (Fst scale)+ , KnownNat (Snd scale)+ )++-- | If the specified @num@ and @den@ satisfy the expectations of 'GoodScale' at+-- the type level, then construct a proof for 'GoodScale'.+mkGoodScale+ :: forall num den+ . (KnownNat num, KnownNat den)+ => Maybe (Dict (GoodScale '(num, den)))+mkGoodScale =+ let n = natVal (Proxy :: Proxy num)+ d = natVal (Proxy :: Proxy den)+ in if (n > 0) && (d > 0)+ then Just (unsafeCoerce (Dict :: Dict ('LT ~ 'LT, 'LT ~ 'LT,+ KnownNat num, KnownNat den)))+ else Nothing+{-# INLINABLE mkGoodScale #-}++-- | Term-level representation for the @currency@'s @unit@ 'Scale'.+--+-- For example, the 'Scale' for @\"USD\"@ in @\"cent\"@s is @100/1@.+--+-- The returned 'Rational' is statically guaranteed to be a positive number, and+-- to be different from both 'notANumber' and 'infinity'.+scale :: forall proxy scale. GoodScale scale => proxy scale -> Rational -- ^+scale = \_ ->+ natVal (Proxy :: Proxy (Fst scale)) %+ natVal (Proxy :: Proxy (Snd scale))+{-# INLINABLE scale #-}++--------------------------------------------------------------------------------++-- | Exchange rate for converting monetary values of currency @src@ into+-- monetary values of currency @dst@ by multiplying for it.+--+-- For example, if in order to convert USD to GBP we have to multiply by 1.2345,+-- then we can represent this situaion using:+--+-- @+-- 'exchangeRate' (12345 % 10000) :: 'Maybe' ('ExchangeRate' \"USD\" \"GBP\")+-- @+newtype ExchangeRate (src :: Symbol) (dst :: Symbol) = ExchangeRate Rational+ deriving (Eq, Ord, GHC.Generic)++instance forall src dst.+ ( KnownSymbol src, KnownSymbol dst+ ) => Show (ExchangeRate src dst) where+ showsPrec n = \(ExchangeRate r0) ->+ let s = symbolVal (Proxy :: Proxy src)+ d = symbolVal (Proxy :: Proxy dst)+ in showParen (n > 10) $+ showString "ExchangeRate " . showsPrec 0 s . showChar ' ' .+ showsPrec 0 d . showChar ' ' .+ showsPrec 0 (numerator r0) . showChar '%' .+ showsPrec 0 (denominator r0)++instance forall src dst.+ ( KnownSymbol src, KnownSymbol dst+ ) => Read (ExchangeRate (src :: Symbol) (dst :: Symbol)) where+ readPrec = Read.parens $ do+ let s = symbolVal (Proxy :: Proxy src)+ d = symbolVal (Proxy :: Proxy dst)+ _ <- ReadPrec.lift (ReadP.string+ ("ExchangeRate " ++ show s ++ " " ++ show d ++ " "))+ maybe empty pure =<< fmap exchangeRate Read.readPrec++-- | Obtain a 'Rational' representation of the 'ExchangeRate'.+--+-- This 'Rational' is statically guaranteed to be greater than 0, different+-- from 'infinity' and different from 'notANumber'.+fromExchangeRate :: ExchangeRate src dst -> Rational+fromExchangeRate = \(ExchangeRate r0) -> r0+{-# INLINABLE fromExchangeRate #-}++-- | Safely construct an 'ExchangeRate' from a 'Rational' number.+--+-- For construction to succeed, this 'Rational' must be greater than 0,+-- different from 'infinity' and different from 'notANumber'.+exchangeRate :: Rational -> Maybe (ExchangeRate src dst)+exchangeRate = \r0 ->+ if (r0 <= 0 || infinity == r0 || notANumber == r0)+ then Nothing else Just (ExchangeRate r0)+{-# INLINABLE exchangeRate #-}++-- | Flip the direction of an 'ExchangeRate'.+--+-- Identity law:+--+-- @+-- 'flipExchangeRate' . 'flipExchangeRate' == 'id'+-- @+flipExchangeRate :: ExchangeRate a b -> ExchangeRate b a+flipExchangeRate = \(ExchangeRate x) -> ExchangeRate (1 / x)+{-# INLINABLE flipExchangeRate #-}++-- | Apply the 'ExchangeRate' to the given @'Dense' src@ monetary value.+--+-- Identity law:+--+-- @+-- 'exchange' ('flipExchangeRate' x) . 'exchange' x == 'id'+-- @+--+-- Use the /Identity law/ for reasoning about going back and forth between @src@+-- and @dst@ in order to manage any leftovers that might not be representable as+-- a 'Discrete' monetary value of @src@.+exchange :: ExchangeRate src dst -> Dense src -> Dense dst+exchange = \(ExchangeRate r) -> \(Dense s) -> Dense (r * s)+{-# INLINABLE exchange #-}++--------------------------------------------------------------------------------+-- SomeDense++-- | A monomorphic representation of 'Dense' that is easier to serialize and+-- deserialize than 'Dense' in case you don't know the type indexes involved.+--+-- If you are trying to construct a value of this type from some raw input, then+-- you will need to use the 'mkSomeDense' function.+--+-- In order to be able to effectively serialize a 'SomeDense' value, you+-- need to serialize the following three values (which are the eventual+-- arguments to 'mkSomeDense'):+--+-- * 'someDenseCurrency'+-- * 'someDenseAmount'+data SomeDense = SomeDense+ { _someDenseCurrency :: !String+ , _someDenseAmount :: !Rational+ } deriving (Eq, Show, GHC.Generic)++-- | WARNING: This instance does not compare monetary amounts, it just helps you+-- sort 'SomeDense' values in case you need to put them in a 'Data.Set.Set' or+-- similar.+deriving instance Ord SomeDense++-- | Currency name.+someDenseCurrency :: SomeDense -> String+someDenseCurrency = _someDenseCurrency+{-# INLINABLE someDenseCurrency #-}++-- | Currency unit amount.+someDenseAmount :: SomeDense -> Rational+someDenseAmount = _someDenseAmount+{-# INLINABLE someDenseAmount #-}++-- | Build a 'SomeDense' from raw values.+--+-- This function is intended for deserialization purposes. You need to convert+-- this 'SomeDense' value to a 'Dense' value in order to do any arithmetic+-- operation on the monetary value.+mkSomeDense+ :: String -- ^ Currency. ('someDenseCurrency')+ -> Rational -- ^ Scale. ('someDenseAmount')+ -> Maybe SomeDense+mkSomeDense = \c r -> do+ guard (denominator r /= 0)+ Just (SomeDense c r)+{-# INLINABLE mkSomeDense #-}++-- | Convert a 'Dense' to a 'SomeDense' for ease of serialization.+toSomeDense :: KnownSymbol currency => Dense currency -> SomeDense+toSomeDense = \(Dense r0 :: Dense currency) ->+ let c = symbolVal (Proxy :: Proxy currency)+ in SomeDense c r0+{-# INLINABLE toSomeDense #-}++-- | Attempt to convert a 'SomeDense' to a 'Dense', provided you know the target+-- @currency@.+fromSomeDense+ :: forall currency+ . KnownSymbol currency+ => SomeDense+ -> Maybe (Dense currency) -- ^+fromSomeDense = \dr -> do+ guard (someDenseCurrency dr == symbolVal (Proxy :: Proxy currency))+ Just (Dense (someDenseAmount dr))+{-# INLINABLE fromSomeDense #-}++-- | Convert a 'SomeDense' to a 'Dense' without knowing the target @currency@.+--+-- Notice that @currency@ here can't leave its intended scope unless you can+-- prove equality with some other type at the outer scope, but in that case you+-- would be better off using 'fromSomeDense' directly.+withSomeDense+ :: SomeDense+ -> (forall currency. KnownSymbol currency => Dense currency -> r)+ -> r -- ^+withSomeDense dr = \f ->+ case someSymbolVal (someDenseCurrency dr) of+ SomeSymbol (Proxy :: Proxy currency) ->+ f (Dense (someDenseAmount dr) :: Dense currency)+{-# INLINABLE withSomeDense #-}++--------------------------------------------------------------------------------+-- SomeDiscrete++-- | A monomorphic representation of 'Discrete' that is easier to serialize and+-- deserialize than 'Discrete' in case you don't know the type indexes involved.+--+-- If you are trying to construct a value of this type from some raw input, then+-- you will need to use the 'mkSomeDiscrete' function.+--+-- In order to be able to effectively serialize a 'SomeDiscrete' value, you need+-- to serialize the following four values (which are the eventual arguments to+-- 'mkSomeDiscrete'):+--+-- * 'someDiscreteCurrency'+-- * 'someDiscreteScale'+-- * 'someDiscreteAmount'+data SomeDiscrete = SomeDiscrete+ { _someDiscreteCurrency :: !String -- ^ Currency name.+ , _someDiscreteScale :: !Rational -- ^ Positive, non-zero.+ , _someDiscreteAmount :: !Integer -- ^ Amount of unit.+ } deriving (Eq, Show, GHC.Generic)++-- | WARNING: This instance does not compare monetary amounts, it just helps you+-- sort 'SomeDiscrete' values in case you need to put them in a 'Data.Set.Set' or+-- similar.+deriving instance Ord SomeDiscrete++-- | Currency name.+someDiscreteCurrency :: SomeDiscrete -> String+someDiscreteCurrency = _someDiscreteCurrency+{-# INLINABLE someDiscreteCurrency #-}++-- | Positive, non-zero.+someDiscreteScale :: SomeDiscrete -> Rational+someDiscreteScale = _someDiscreteScale+{-# INLINABLE someDiscreteScale #-}++-- | Amount of currency unit.+someDiscreteAmount :: SomeDiscrete -> Integer+someDiscreteAmount = _someDiscreteAmount+{-# INLINABLE someDiscreteAmount #-}++-- | Internal. 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 arithmetic+-- operation on the monetary value.+mkSomeDiscrete+ :: String -- ^ Currency name. ('someDiscreteCurrency')+ -> Rational -- ^ Scale. Positive, non-zero. ('someDiscreteScale')+ -> Integer -- ^ Amount of unit. ('someDiscreteAmount')+ -> Maybe SomeDiscrete+mkSomeDiscrete = \c r a -> do+ guard (denominator r /= 0)+ guard (r > 0)+ Just (SomeDiscrete c r a)+{-# INLINABLE mkSomeDiscrete #-}++-- | Convert a 'Discrete' to a 'SomeDiscrete' for ease of serialization.+toSomeDiscrete+ :: (KnownSymbol currency, GoodScale scale)+ => Discrete' currency scale+ -> SomeDiscrete -- ^+toSomeDiscrete = \(Discrete i0 :: Discrete' currency scale) ->+ let c = symbolVal (Proxy :: Proxy currency)+ n = natVal (Proxy :: Proxy (Fst scale))+ d = natVal (Proxy :: Proxy (Snd scale))+ in SomeDiscrete c (n % d) i0+{-# INLINABLE toSomeDiscrete #-}++-- | Attempt to convert a 'SomeDiscrete' to a 'Discrete', provided you know the+-- target @currency@ and @unit@.+fromSomeDiscrete+ :: forall currency scale+ . (KnownSymbol currency, GoodScale scale)+ => SomeDiscrete+ -> Maybe (Discrete' currency scale) -- ^+fromSomeDiscrete = \dr ->+ if (someDiscreteCurrency dr == symbolVal (Proxy :: Proxy currency)) &&+ (someDiscreteScale dr == scale (Proxy :: Proxy scale))+ then Just (Discrete (someDiscreteAmount dr))+ else Nothing+{-# INLINABLE fromSomeDiscrete #-}++-- | Convert a 'SomeDiscrete' to a 'Discrete' without knowing the target+-- @currency@ and @unit@.+--+-- Notice that @currency@ and @unit@ here can't leave its intended scope unless+-- you can prove equality with some other type at the outer scope, but in that+-- case you would be better off using 'fromSomeDiscrete' directly.+--+-- Notice that you may need to add an explicit type to the result of this+-- function in order to keep the compiler happy.+withSomeDiscrete+ :: forall r+ . SomeDiscrete+ -> ( forall currency scale.+ ( KnownSymbol currency+ , GoodScale scale+ ) => Discrete' currency scale+ -> r )+ -> r -- ^+withSomeDiscrete dr = \f ->+ case someSymbolVal (someDiscreteCurrency dr) of+ SomeSymbol (Proxy :: Proxy currency) ->+ case someNatVal (numerator (someDiscreteScale dr)) of+ Nothing -> error "withSomeDiscrete: impossible: numerator < 0"+ Just (SomeNat (Proxy :: Proxy num)) ->+ case someNatVal (denominator (someDiscreteScale dr)) of+ Nothing -> error "withSomeDiscrete: impossible: denominator < 0"+ Just (SomeNat (Proxy :: Proxy den)) ->+ case mkGoodScale of+ Nothing -> error "withSomeDiscrete: impossible: mkGoodScale"+ Just (Dict :: Dict (GoodScale '(num, den))) ->+ f (Discrete (someDiscreteAmount dr)+ :: Discrete' currency '(num, den))+{-# INLINABLE withSomeDiscrete #-}++--------------------------------------------------------------------------------+-- SomeExchangeRate++-- | A monomorphic representation of 'ExchangeRate' that is easier to serialize+-- and deserialize than 'ExchangeRate' in case you don't know the type indexes+-- involved.+--+-- If you are trying to construct a value of this type from some raw input, then+-- you will need to use the 'mkSomeExchangeRate' function.+--+-- In order to be able to effectively serialize an 'SomeExchangeRate' value, you+-- need to serialize the following four values (which are the eventual arguments+-- to 'mkSomeExchangeRate'):+--+-- * 'someExchangeRateSrcCurrency'+-- * 'someExchangeRateDstCurrency'+-- * 'someExchangeRateRate'+data SomeExchangeRate = SomeExchangeRate+ { _someExchangeRateSrcCurrency :: !String+ , _someExchangeRateDstCurrency :: !String+ , _someExchangeRateRate :: !Rational -- ^ Positive, non-zero.+ } deriving (Eq, Show, GHC.Generic)++-- | WARNING: This instance does not compare monetary amounts, it just helps you+-- sort 'SomeExchangeRate' values in case you need to put them in a+-- 'Data.Set.Set' or similar.+deriving instance Ord SomeExchangeRate++-- | Source currency name.+someExchangeRateSrcCurrency :: SomeExchangeRate -> String+someExchangeRateSrcCurrency = _someExchangeRateSrcCurrency+{-# INLINABLE someExchangeRateSrcCurrency #-}++-- | Destination currency name.+someExchangeRateDstCurrency :: SomeExchangeRate -> String+someExchangeRateDstCurrency = _someExchangeRateDstCurrency+{-# INLINABLE someExchangeRateDstCurrency #-}++-- | Exchange rate. Positive, non-zero.+someExchangeRateRate :: SomeExchangeRate -> Rational+someExchangeRateRate = _someExchangeRateRate+{-# INLINABLE someExchangeRateRate #-}++-- | Internal. 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+-- arithmetic operation with the exchange rate.+mkSomeExchangeRate+ :: String -- ^ Source currency name. ('someExchangeRateSrcCurrency')+ -> String -- ^ Destination currency name. ('someExchangeRateDstCurrency')+ -> Rational -- ^ Exchange rate . Positive, non-zero. ('someExchangeRateRate')+ -> Maybe SomeExchangeRate+mkSomeExchangeRate = \src dst r -> do+ guard (denominator r /= 0)+ guard (r > 0)+ Just (SomeExchangeRate src dst r)+{-# INLINABLE mkSomeExchangeRate #-}++-- | Convert a 'ExchangeRate' to a 'SomeDiscrete' for ease of serialization.+toSomeExchangeRate+ :: (KnownSymbol src, KnownSymbol dst)+ => ExchangeRate src dst+ -> SomeExchangeRate -- ^+toSomeExchangeRate = \(ExchangeRate r0 :: ExchangeRate src dst) ->+ let src = symbolVal (Proxy :: Proxy src)+ dst = symbolVal (Proxy :: Proxy dst)+ in SomeExchangeRate src dst r0+{-# INLINABLE toSomeExchangeRate #-}++-- | Attempt to convert a 'SomeExchangeRate' to a 'ExchangeRate', provided you+-- know the target @src@ and @dst@ types.+fromSomeExchangeRate+ :: forall src dst+ . (KnownSymbol src, KnownSymbol dst)+ => SomeExchangeRate+ -> Maybe (ExchangeRate src dst) -- ^+fromSomeExchangeRate = \x ->+ if (someExchangeRateSrcCurrency x == symbolVal (Proxy :: Proxy src)) &&+ (someExchangeRateDstCurrency x == symbolVal (Proxy :: Proxy dst))+ then Just (ExchangeRate (someExchangeRateRate x))+ else Nothing+{-# INLINABLE fromSomeExchangeRate #-}++-- | Convert a 'SomeExchangeRate' to a 'ExchangeRate' without knowing the target+-- @currency@ and @unit@.+--+-- Notice that @src@ and @dst@ here can't leave its intended scope unless+-- you can prove equality with some other type at the outer scope, but in that+-- case you would be better off using 'fromSomeExchangeRate' directly.+withSomeExchangeRate+ :: SomeExchangeRate+ -> ( forall src dst.+ ( KnownSymbol src+ , KnownSymbol dst+ ) => ExchangeRate src dst+ -> r )+ -> r -- ^+withSomeExchangeRate x = \f ->+ case someSymbolVal (someExchangeRateSrcCurrency x) of+ SomeSymbol (Proxy :: Proxy src) ->+ case someSymbolVal (someExchangeRateDstCurrency x) of+ SomeSymbol (Proxy :: Proxy dst) ->+ f (ExchangeRate (someExchangeRateRate x) :: ExchangeRate src dst)+{-# INLINABLE withSomeExchangeRate #-}++--------------------------------------------------------------------------------+-- Miscellaneous++type family Fst (ab :: (ka, kb)) :: ka where Fst '(a,b) = a+type family Snd (ab :: (ka, kb)) :: ka where Snd '(a,b) = b++--------------------------------------------------------------------------------+-- Extra instances: hashable+#ifdef HAS_hashable+instance Hashable (Dense currency)+instance Hashable SomeDense+instance GoodScale scale => Hashable (Discrete' currency scale)+instance Hashable SomeDiscrete+instance Hashable (ExchangeRate src dst)+instance Hashable SomeExchangeRate+#endif++--------------------------------------------------------------------------------+-- Extra instances: deepseq+#ifdef HAS_deepseq+instance NFData (Dense currency)+instance NFData SomeDense+instance GoodScale scale => NFData (Discrete' currency scale)+instance NFData SomeDiscrete+instance NFData (ExchangeRate src dst)+instance NFData SomeExchangeRate+#endif++--------------------------------------------------------------------------------+-- Extra instances: cereal+#ifdef HAS_cereal+-- | Compatible with 'SomeDense'.+instance (KnownSymbol currency) => Cereal.Serialize (Dense currency) where+ put = Cereal.put . toSomeDense+ get = maybe empty pure =<< fmap fromSomeDense Cereal.get++-- | Compatible with 'SomeDiscrete'.+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Cereal.Serialize (Discrete' currency scale) where+ put = Cereal.put . toSomeDiscrete+ get = maybe empty pure =<< fmap fromSomeDiscrete Cereal.get++-- | Compatible with 'SomeExchangeRate'.+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Cereal.Serialize (ExchangeRate src dst) where+ put = Cereal.put . toSomeExchangeRate+ get = maybe empty pure =<< fmap fromSomeExchangeRate Cereal.get++-- | Compatible with 'Dense'.+instance Cereal.Serialize SomeDense where+ put = \(SomeDense c r) -> do+ Cereal.put c+ Cereal.put (numerator r)+ Cereal.put (denominator r)+ get = maybe empty pure =<< do+ c :: String <- Cereal.get+ n :: Integer <- Cereal.get+ d :: Integer <- Cereal.get+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeDense c (n % d))++-- | Compatible with 'Discrete'.+instance Cereal.Serialize SomeDiscrete where+ put = \(SomeDiscrete c r a) -> do+ Cereal.put c+ Cereal.put (numerator r)+ Cereal.put (denominator r)+ Cereal.put a+ get = maybe empty pure =<< do+ c :: String <- Cereal.get+ n :: Integer <- Cereal.get+ d :: Integer <- Cereal.get+ when (d == 0) (fail "denominator is zero")+ a :: Integer <- Cereal.get+ pure (mkSomeDiscrete c (n % d) a)++-- | Compatible with 'ExchangeRate'.+instance Cereal.Serialize SomeExchangeRate where+ put = \(SomeExchangeRate src dst r) -> do+ Cereal.put src+ Cereal.put dst+ Cereal.put (numerator r)+ Cereal.put (denominator r)+ get = maybe empty pure =<< do+ src :: String <- Cereal.get+ dst :: String <- Cereal.get+ n :: Integer <- Cereal.get+ d :: Integer <- Cereal.get+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeExchangeRate src dst (n % d))+#endif++------------------------------------------------------------------------------+-- Extra instances: binary+#ifdef HAS_binary+-- | Compatible with 'SomeDense'.+instance (KnownSymbol currency) => Binary.Binary (Dense currency) where+ put = Binary.put . toSomeDense+ get = maybe empty pure =<< fmap fromSomeDense Binary.get++-- | Compatible with 'SomeDiscrete'.+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Binary.Binary (Discrete' currency scale) where+ put = Binary.put . toSomeDiscrete+ get = maybe empty pure =<< fmap fromSomeDiscrete Binary.get++-- | Compatible with 'SomeExchangeRate'.+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Binary.Binary (ExchangeRate src dst) where+ put = Binary.put . toSomeExchangeRate+ get = maybe empty pure =<< fmap fromSomeExchangeRate Binary.get++-- | Compatible with 'Dense'.+instance Binary.Binary SomeDense where+ put = \(SomeDense c r) ->+ Binary.put c >> Binary.put (numerator r) >> Binary.put (denominator r)+ get = maybe empty pure =<< do+ c :: String <- Binary.get+ n :: Integer <- Binary.get+ d :: Integer <- Binary.get+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeDense c (n % d))++-- | Compatible with 'Discrete'.+instance Binary.Binary SomeDiscrete where+ put = \(SomeDiscrete c r a) ->+ Binary.put c <>+ Binary.put (numerator r) <>+ Binary.put (denominator r) <>+ Binary.put a+ get = maybe empty pure =<< do+ c :: String <- Binary.get+ n :: Integer <- Binary.get+ d :: Integer <- Binary.get+ when (d == 0) (fail "denominator is zero")+ a :: Integer <- Binary.get+ pure (mkSomeDiscrete c (n % d) a)++-- | Compatible with 'ExchangeRate'.+instance Binary.Binary SomeExchangeRate where+ put = \(SomeExchangeRate src dst r) -> do+ Binary.put src+ Binary.put dst+ Binary.put (numerator r)+ Binary.put (denominator r)+ get = maybe empty pure =<< do+ src :: String <- Binary.get+ dst :: String <- Binary.get+ n :: Integer <- Binary.get+ d :: Integer <- Binary.get+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeExchangeRate src dst (n % d))+#endif++--------------------------------------------------------------------------------+-- Extra instances: serialise+#ifdef HAS_serialise+-- | Compatible with 'SomeDense'.+instance (KnownSymbol currency) => Ser.Serialise (Dense currency) where+ encode = Ser.encode . toSomeDense+ decode = maybe (fail "Dense") pure =<< fmap fromSomeDense Ser.decode++-- | Compatible with 'SomeDiscrete'.+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Ser.Serialise (Discrete' currency scale) where+ encode = Ser.encode . toSomeDiscrete+ decode = maybe (fail "Discrete'") pure =<< fmap fromSomeDiscrete Ser.decode++-- | Compatible with 'SomeExchangeRate'.+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Ser.Serialise (ExchangeRate src dst) where+ encode = Ser.encode . toSomeExchangeRate+ decode = maybe (fail "ExchangeRate") pure+ =<< fmap fromSomeExchangeRate Ser.decode++-- | Compatible with 'Dense'.+instance Ser.Serialise SomeDense where+ encode = \(SomeDense c r) ->+ Ser.encode c <> Ser.encode (numerator r) <> Ser.encode (denominator r)+ decode = maybe (fail "SomeDense") pure =<< do+ c :: String <- Ser.decode+ n :: Integer <- Ser.decode+ d :: Integer <- Ser.decode+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeDense c (n % d))++-- | Compatible with 'Discrete'.+instance Ser.Serialise SomeDiscrete where+ encode = \(SomeDiscrete c r a) ->+ Ser.encode c <>+ Ser.encode (numerator r) <>+ Ser.encode (denominator r) <>+ Ser.encode a+ decode = maybe (fail "SomeDiscrete") pure =<< do+ c :: String <- Ser.decode+ n :: Integer <- Ser.decode+ d :: Integer <- Ser.decode+ when (d == 0) (fail "denominator is zero")+ a :: Integer <- Ser.decode+ pure (mkSomeDiscrete c (n % d) a)++-- | Compatible with 'ExchangeRate'.+instance Ser.Serialise SomeExchangeRate where+ encode = \(SomeExchangeRate src dst r) ->+ Ser.encode src <>+ Ser.encode dst <>+ Ser.encode (numerator r) <>+ Ser.encode (denominator r)+ decode = maybe (fail "SomeExchangeRate") pure =<< do+ src :: String <- Ser.decode+ dst :: String <- Ser.decode+ n :: Integer <- Ser.decode+ d :: Integer <- Ser.decode+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeExchangeRate src dst (n % d))+#endif++--------------------------------------------------------------------------------+-- Extra instances: aeson+#ifdef HAS_aeson+-- | Compatible with 'SomeDense'+--+-- Example rendering @'fromRational' (2 % 3) :: 'Dense' \"BTC\"@:+--+-- @+-- [\"BTC\", 2, 3]+-- @+--+-- Note: The JSON serialization changed in version 0.4 (the leading @"Dense"@+-- string was dropped from the rendered 'Ae.Array').+instance KnownSymbol currency => Ae.ToJSON (Dense currency) where+ toJSON = Ae.toJSON . toSomeDense++-- | Compatible with 'SomeDense'+--+-- Note: The JSON serialization changed in version 0.4. However, this instance+-- is still able to cope with the previous format.+instance KnownSymbol currency => Ae.FromJSON (Dense currency) where+ parseJSON = maybe empty pure <=< fmap fromSomeDense . Ae.parseJSON++-- | Compatible with 'Dense'+--+-- Note: The JSON serialization changed in version 0.4 (the leading @"Dense"@+-- string was dropped from the rendered 'Ae.Array').+instance Ae.ToJSON SomeDense where+ toJSON = \(SomeDense c r) ->+ Ae.toJSON (c, numerator r, denominator r)++-- | Compatible with 'Dense'.+--+-- Note: The JSON serialization changed in version 0.4. However, this instance+-- is still able to cope with the previous format.+instance Ae.FromJSON SomeDense where+ parseJSON = \v -> do+ (c, n, d) <- Ae.parseJSON v <|> do+ -- Pre 0.4 format.+ ("Dense" :: String, c, n, d) <- Ae.parseJSON v+ pure (c, n, d)+ when (d == 0) (fail "denominator is zero")+ maybe empty pure (mkSomeDense c (n % d))++-- | Compatible with 'SomeDiscrete'+--+-- Example rendering @43 :: 'Discrete' \"BTC\" \"satoshi\"@:+--+-- @+-- [\"BTC\", 100000000, 1, 43]+-- @+--+-- Note: The JSON serialization changed in version 0.4 (the leading @"Discrete"@+-- string was dropped from the rendered 'Ae.Array').+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Ae.ToJSON (Discrete' currency scale) where+ toJSON = Ae.toJSON . toSomeDiscrete++-- | Compatible with 'SomeDiscrete'+--+-- Note: The JSON serialization changed in version 0.4. However, this instance+-- is still able to cope with the previous format.+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Ae.FromJSON (Discrete' currency scale) where+ parseJSON = maybe empty pure <=< fmap fromSomeDiscrete . Ae.parseJSON++-- | Compatible with 'Discrete''+--+-- Note: The JSON serialization changed in version 0.4 (the leading @"Discrete"@+-- string was dropped from the rendered 'Ae.Array').+instance Ae.ToJSON SomeDiscrete where+ toJSON = \(SomeDiscrete c r a) ->+ Ae.toJSON (c, numerator r, denominator r, a)++-- | Compatible with 'Discrete''+--+-- Note: The JSON serialization changed in version 0.4. However, this instance+-- is still able to cope with the previous format.+instance Ae.FromJSON SomeDiscrete where+ parseJSON = \v -> do+ (c, n, d, a) <- Ae.parseJSON v <|> do+ -- Pre 0.4 format.+ ("Discrete" :: String, c, n, d, a) <- Ae.parseJSON v+ pure (c, n, d, a)+ when (d == 0) (fail "denominator is zero")+ maybe empty pure (mkSomeDiscrete c (n % d) a)++-- | Compatible with 'SomeExchangeRate'+--+-- Example rendering an 'ExchangeRate' constructed with+-- @'exchangeRate' (5 % 7) :: 'Maybe' ('ExchangeRate' \"USD\" \"JPY\")@+--+-- @+-- [\"USD\", \"JPY\", 5, 7]+-- @+--+-- Note: The JSON serialization changed in version 0.4 (the leading+-- @"ExchangeRate"@ string was dropped from the rendered 'Ae.Array').+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Ae.ToJSON (ExchangeRate src dst) where+ toJSON = Ae.toJSON . toSomeExchangeRate++-- | Compatible with 'SomeExchangeRate'+--+-- Note: The JSON serialization changed in version 0.4. However, this instance+-- is still able to cope with the previous format.+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Ae.FromJSON (ExchangeRate src dst) where+ parseJSON = maybe empty pure <=< fmap fromSomeExchangeRate . Ae.parseJSON++-- | Compatible with 'ExchangeRate'+--+-- Note: The JSON serialization changed in version 0.4 (the leading+-- @"ExchangeRate"@ string was dropped from the rendered 'Ae.Array').+instance Ae.ToJSON SomeExchangeRate where+ toJSON = \(SomeExchangeRate src dst r) ->+ Ae.toJSON (src, dst, numerator r, denominator r)++-- | Compatible with 'ExchangeRate'+--+-- Note: The JSON serialization changed in version 0.4. However, this instance+-- is still able to cope with the previous format.+instance Ae.FromJSON SomeExchangeRate where+ parseJSON = \v -> do+ (src, dst, n, d) <- Ae.parseJSON v <|> do+ -- Pre 0.4 format.+ ("ExchangeRate" :: String, src, dst, n, d) <- Ae.parseJSON v+ pure (src, dst, n, d)+ when (d == 0) (fail "denominator is zero")+ maybe empty pure (mkSomeExchangeRate src dst (n % d))+#endif++--------------------------------------------------------------------------------+-- Extra instances: xmlbf+#ifdef HAS_xmlbf++-- | Compatible with 'SomeDense'+--+-- Example rendering @'fromRational' (2 % 3) :: 'Dense' \"BTC\"@:+--+-- @+-- \<money-dense c=\"BTC\" n=\"2\" d=\"3\"/>+-- @+instance KnownSymbol currency => Xmlbf.ToXml (Dense currency) where+ toXml = Xmlbf.toXml . toSomeDense++-- | Compatible with 'SomeDense'+instance KnownSymbol currency => Xmlbf.FromXml (Dense currency) where+ fromXml = maybe empty pure =<< fmap fromSomeDense Xmlbf.fromXml++-- | Compatible with 'Dense'+instance Xmlbf.ToXml SomeDense where+ toXml = \(SomeDense c r) ->+ let as = [ (Text.pack "c", Text.pack c)+ , (Text.pack "n", Text.pack (show (numerator r)))+ , (Text.pack "d", Text.pack (show (denominator r))) ]+ Right e = Xmlbf.element (Text.pack "money-dense") (fromList as) []+ in [e]++-- | Compatible with 'Dense'.+instance Xmlbf.FromXml SomeDense where+ fromXml = Xmlbf.pElement (Text.pack "money-dense") $ do+ c <- Text.unpack <$> Xmlbf.pAttr "c"+ n <- Xmlbf.pRead =<< Xmlbf.pAttr "n"+ d <- Xmlbf.pRead =<< Xmlbf.pAttr "d"+ when (d == 0) (fail "denominator is zero")+ maybe empty pure (mkSomeDense c (n % d))++-- | Compatible with 'SomeDiscrete'+--+-- Example rendering @43 :: 'Discrete' \"BTC\" \"satoshi\"@:+--+-- @+-- \<money-discrete c=\"BTC\" n=\"100000000\" d=\"1\" a=\"43\"/>+-- @+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Xmlbf.ToXml (Discrete' currency scale) where+ toXml = Xmlbf.toXml . toSomeDiscrete++-- | Compatible with 'SomeDiscrete'+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Xmlbf.FromXml (Discrete' currency scale) where+ fromXml = maybe empty pure =<< fmap fromSomeDiscrete Xmlbf.fromXml++-- | Compatible with 'Discrete''+instance Xmlbf.ToXml SomeDiscrete where+ toXml = \(SomeDiscrete c r a) ->+ let as = [ (Text.pack "c", Text.pack c)+ , (Text.pack "n", Text.pack (show (numerator r)))+ , (Text.pack "d", Text.pack (show (denominator r)))+ , (Text.pack "a", Text.pack (show a)) ]+ Right e = Xmlbf.element (Text.pack "money-discrete") (fromList as) []+ in [e]++-- | Compatible with 'Discrete''+instance Xmlbf.FromXml SomeDiscrete where+ fromXml = Xmlbf.pElement (Text.pack "money-discrete") $ do+ c <- Text.unpack <$> Xmlbf.pAttr "c"+ n <- Xmlbf.pRead =<< Xmlbf.pAttr "n"+ d <- Xmlbf.pRead =<< Xmlbf.pAttr "d"+ when (d == 0) (fail "denominator is zero")+ a <- Xmlbf.pRead =<< Xmlbf.pAttr "a"+ maybe empty pure (mkSomeDiscrete c (n % d) a)++-- | Compatible with 'SomeExchangeRate'+--+-- Example rendering an 'ExchangeRate' constructed with+-- @'exchangeRate' (5 % 7) :: 'Maybe' ('ExchangeRate' \"USD\" \"JPY\")@+--+-- @+-- \<exchange-rate src=\"USD\" dst=\"JPY\" n=\"5\" d=\"7\"/>+-- @+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Xmlbf.ToXml (ExchangeRate src dst) where+ toXml = Xmlbf.toXml . toSomeExchangeRate++-- | Compatible with 'SomeExchangeRate'+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Xmlbf.FromXml (ExchangeRate src dst) where+ fromXml = maybe empty pure =<< fmap fromSomeExchangeRate Xmlbf.fromXml++-- | Compatible with 'ExchangeRate'+instance Xmlbf.ToXml SomeExchangeRate where+ toXml = \(SomeExchangeRate src dst r) ->+ let as = [ (Text.pack "src", Text.pack src)+ , (Text.pack "dst", Text.pack dst)+ , (Text.pack "n", Text.pack (show (numerator r)))+ , (Text.pack "d", Text.pack (show (denominator r))) ]+ Right e = Xmlbf.element (Text.pack "exchange-rate") (fromList as) []+ in [e]++-- | Compatible with 'ExchangeRate'+instance Xmlbf.FromXml SomeExchangeRate where+ fromXml = Xmlbf.pElement (Text.pack "exchange-rate") $ do+ src <- Text.unpack <$> Xmlbf.pAttr "src"+ dst <- Text.unpack <$> Xmlbf.pAttr "dst"+ n <- Xmlbf.pRead =<< Xmlbf.pAttr "n"+ d <- Xmlbf.pRead =<< Xmlbf.pAttr "d"+ when (d == 0) (fail "denominator is zero")+ maybe empty pure (mkSomeExchangeRate src dst (n % d))+#endif++--------------------------------------------------------------------------------+-- Extra instances: store+#ifdef HAS_store+-- | Compatible with 'SomeDense'.+instance (KnownSymbol currency) => Store.Store (Dense currency) where+ size = storeContramapSize toSomeDense Store.size+ poke = Store.poke . toSomeDense+ peek = maybe (fail "peek") pure =<< fmap fromSomeDense Store.peek+-- | Compatible with 'Dense'.+instance Store.Store SomeDense where+ poke = \(SomeDense c r) -> do+ Store.poke c+ Store.poke (numerator r)+ Store.poke (denominator r)+ peek = maybe (fail "peek") pure =<< do+ c :: String <- Store.peek+ n :: Integer <- Store.peek+ d :: Integer <- Store.peek+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeDense c (n % d))++-- | Compatible with 'SomeDiscrete'.+instance+ ( KnownSymbol currency, GoodScale scale+ ) => Store.Store (Discrete' currency scale) where+ size = storeContramapSize toSomeDiscrete Store.size+ poke = Store.poke . toSomeDiscrete+ peek = maybe (fail "peek") pure =<< fmap fromSomeDiscrete Store.peek+-- | Compatible with 'Discrete''.+instance Store.Store SomeDiscrete where+ poke = \(SomeDiscrete c r a) ->do+ Store.poke c+ Store.poke (numerator r)+ Store.poke (denominator r)+ Store.poke a+ peek = maybe (fail "peek") pure =<< do+ c :: String <- Store.peek+ n :: Integer <- Store.peek+ d :: Integer <- Store.peek+ when (d == 0) (fail "denominator is zero")+ a :: Integer <- Store.peek+ pure (mkSomeDiscrete c (n % d) a)+-- | Compatible with 'SomeExchangeRate'.+instance+ ( KnownSymbol src, KnownSymbol dst+ ) => Store.Store (ExchangeRate src dst) where+ size = storeContramapSize toSomeExchangeRate Store.size+ poke = Store.poke . toSomeExchangeRate+ peek = maybe (fail "peek") pure =<< fmap fromSomeExchangeRate Store.peek+-- | Compatible with 'ExchangeRate'.+instance Store.Store SomeExchangeRate where+ poke = \(SomeExchangeRate src dst r) -> do+ Store.poke src+ Store.poke dst+ Store.poke (numerator r)+ Store.poke (denominator r)+ peek = maybe (fail "peek") pure =<< do+ src <- Store.peek+ dst <- Store.peek+ n <- Store.peek+ d <- Store.peek+ when (d == 0) (fail "denominator is zero")+ pure (mkSomeExchangeRate src dst (n % d))++storeContramapSize :: (a -> b) -> Store.Size b -> Store.Size a+storeContramapSize f = \case+ Store.VarSize g -> Store.VarSize (g . f)+ Store.ConstSize x -> Store.ConstSize x+{-# INLINABLE storeContramapSize #-}+#endif+
test/Main.hs view
@@ -8,23 +8,41 @@ module Main where +import qualified Data.ByteString.Lazy as BSL+import Data.Maybe (catMaybes, isJust, isNothing)+import Data.Proxy (Proxy(Proxy))+import Data.Ratio (numerator, denominator)+import GHC.TypeLits (Nat, Symbol, KnownSymbol, symbolVal) import qualified Test.Tasty as Tasty import qualified Test.Tasty.Runners as Tasty-import Test.Tasty.QuickCheck ((===), (==>))+import Test.Tasty.QuickCheck ((===), (==>), (.&&.)) import qualified Test.Tasty.QuickCheck as QC +#ifdef HAS_aeson import qualified Data.Aeson as Ae+#endif++#ifdef HAS_binary import qualified Data.Binary as Binary-import qualified Data.ByteString.Lazy as BSL-import Data.Maybe (catMaybes, isJust, isNothing)-import Data.Proxy (Proxy(Proxy))+#endif++#ifdef HAS_cereal import qualified Data.Serialize as Cereal-import GHC.TypeLits (Nat, Symbol, KnownSymbol, symbolVal)+#endif -#ifdef VERSION_store+#ifdef HAS_serialise+import qualified Codec.Serialise as Ser+#endif++#ifdef HAS_store import qualified Data.Store as Store #endif +#ifdef HAS_xmlbf+import qualified Xmlbf+import qualified Data.Text as Text+#endif+ import qualified Money --------------------------------------------------------------------------------@@ -35,13 +53,13 @@ arbitrary = fmap fromInteger QC.arbitrary shrink = fmap fromInteger . QC.shrink . toInteger -instance QC.Arbitrary Money.DiscreteRep where+instance QC.Arbitrary Money.SomeDiscrete where arbitrary = do- let md = Money.mkDiscreteRep <$> QC.arbitrary <*> QC.arbitrary- <*> QC.arbitrary <*> QC.arbitrary+ let md = Money.mkSomeDiscrete+ <$> QC.arbitrary <*> QC.arbitrary <*> QC.arbitrary Just x <- QC.suchThat md isJust pure x- shrink = \x -> Money.withDiscreteRep x (map Money.toDiscreteRep . QC.shrink)+ shrink = \x -> Money.withSomeDiscrete x (map Money.toSomeDiscrete . QC.shrink) instance QC.Arbitrary (Money.Dense currency) where arbitrary = do@@ -49,12 +67,12 @@ pure x shrink = catMaybes . fmap Money.dense . QC.shrink . toRational -instance QC.Arbitrary Money.DenseRep where+instance QC.Arbitrary Money.SomeDense where arbitrary = do- let md = Money.mkDenseRep <$> QC.arbitrary <*> QC.arbitrary <*> QC.arbitrary+ let md = Money.mkSomeDense <$> QC.arbitrary <*> QC.arbitrary Just x <- QC.suchThat md isJust pure x- shrink = \x -> Money.withDenseRep x (map Money.toDenseRep . QC.shrink)+ shrink = \x -> Money.withSomeDense x (map Money.toSomeDense . QC.shrink) instance QC.Arbitrary (Money.ExchangeRate src dst) where arbitrary = do@@ -63,14 +81,14 @@ shrink = catMaybes . fmap Money.exchangeRate . QC.shrink . Money.fromExchangeRate -instance QC.Arbitrary Money.ExchangeRateRep where+instance QC.Arbitrary Money.SomeExchangeRate where arbitrary = do- let md = Money.mkExchangeRateRep <$> QC.arbitrary <*> QC.arbitrary- <*> QC.arbitrary <*> QC.arbitrary+ let md = Money.mkSomeExchangeRate+ <$> QC.arbitrary <*> QC.arbitrary <*> QC.arbitrary Just x <- QC.suchThat md isJust pure x shrink = \x ->- Money.withExchangeRateRep x (map Money.toExchangeRateRep . QC.shrink)+ Money.withSomeExchangeRate x (map Money.toSomeExchangeRate . QC.shrink) -------------------------------------------------------------------------------- @@ -122,83 +140,132 @@ [ QC.testProperty "read . show == id" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> x === read (show x)- , QC.testProperty "fromDenseRep . denseRep == Just" $+ , QC.testProperty "read . show . Just == Just " $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- Just x === Money.fromDenseRep (Money.toDenseRep x)- , QC.testProperty "fromDenseRep works only for same currency" $- QC.forAll QC.arbitrary $ \(dr :: Money.DenseRep) ->- (Money.denseRepCurrency dr /= symbolVal pc)- ==> isNothing (Money.fromDenseRep dr :: Maybe (Money.Dense currency))- , QC.testProperty "withDenseRep" $+ Just x === read (show (Just x))+ , QC.testProperty "fromSomeDense . someDense == Just" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- let dr = Money.toDenseRep x- in Money.withDenseRep dr $ \x' ->- (show x, dr, Money.toDenseRep (x + 1))- === (show x', Money.toDenseRep x', Money.toDenseRep (x' + 1))+ Just x === Money.fromSomeDense (Money.toSomeDense x)+ , QC.testProperty "fromSomeDense works only for same currency" $+ QC.forAll QC.arbitrary $ \(dr :: Money.SomeDense) ->+ (Money.someDenseCurrency dr /= symbolVal pc)+ ==> isNothing (Money.fromSomeDense dr :: Maybe (Money.Dense currency))+ , QC.testProperty "withSomeDense" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ let dr = Money.toSomeDense x+ in Money.withSomeDense dr $ \x' ->+ (show x, dr, Money.toSomeDense (x + 1))+ === (show x', Money.toSomeDense x', Money.toSomeDense (x' + 1)) +#ifdef HAS_aeson , QC.testProperty "Aeson encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> Just x === Ae.decode (Ae.encode x)- , QC.testProperty "Aeson encoding roundtrip (DenseRep)" $+ , QC.testProperty "Aeson encoding roundtrip (SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- let x' = Money.toDenseRep x+ let x' = Money.toSomeDense x in Just x' === Ae.decode (Ae.encode x')- , QC.testProperty "Aeson encoding roundtrip (Dense through DenseRep)" $+ , QC.testProperty "Aeson encoding roundtrip (Dense through SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- Just x === Ae.decode (Ae.encode (Money.toDenseRep x))- , QC.testProperty "Aeson encoding roundtrip (DenseRep through Dense)" $+ Just x === Ae.decode (Ae.encode (Money.toSomeDense x))+ , QC.testProperty "Aeson encoding roundtrip (SomeDense through Dense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- Just (Money.toDenseRep x) === Ae.decode (Ae.encode x)+ Just (Money.toSomeDense x) === Ae.decode (Ae.encode x)+ , QC.testProperty "Aeson decoding of pre-0.4 format (Dense, SomeDense)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ let sx = Money.toSomeDense x+ c = Money.someDenseCurrency sx+ r = Money.someDenseAmount sx+ bs = Ae.encode ("Dense", c, numerator r, denominator r)+ in (Just x === Ae.decode bs) .&&.+ (Just sx === Ae.decode bs)+#endif +#ifdef HAS_binary , QC.testProperty "Binary encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> let Right (_,_,y) = Binary.decodeOrFail (Binary.encode x) in x === y- , QC.testProperty "Binary encoding roundtrip (DenseRep)" $+ , QC.testProperty "Binary encoding roundtrip (SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- let x' = Money.toDenseRep x+ let x' = Money.toSomeDense x bs = Binary.encode x' in Right (mempty, BSL.length bs, x') === Binary.decodeOrFail bs- , QC.testProperty "Binary encoding roundtrip (Dense through DenseRep)" $+ , QC.testProperty "Binary encoding roundtrip (Dense through SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- let x' = Money.toDenseRep x+ let x' = Money.toSomeDense x bs = Binary.encode x' in Right (mempty, BSL.length bs, x) === Binary.decodeOrFail bs- , QC.testProperty "Binary encoding roundtrip (DenseRep through Dense)" $+ , QC.testProperty "Binary encoding roundtrip (SomeDense through Dense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- let x' = Money.toDenseRep x+ let x' = Money.toSomeDense x bs = Binary.encode x in Right (mempty, BSL.length bs, x') === Binary.decodeOrFail bs+#endif +#ifdef HAS_cereal , QC.testProperty "Cereal encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> Right x === Cereal.decode (Cereal.encode x)- , QC.testProperty "Cereal encoding roundtrip (DenseRep)" $+ , QC.testProperty "Cereal encoding roundtrip (SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- let x' = Money.toDenseRep x+ let x' = Money.toSomeDense x in Right x' === Cereal.decode (Cereal.encode x')- , QC.testProperty "Cereal encoding roundtrip (Dense through DenseRep)" $+ , QC.testProperty "Cereal encoding roundtrip (Dense through SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- Right x === Cereal.decode (Cereal.encode (Money.toDenseRep x))- , QC.testProperty "Cereal encoding roundtrip (DenseRep through Dense)" $+ Right x === Cereal.decode (Cereal.encode (Money.toSomeDense x))+ , QC.testProperty "Cereal encoding roundtrip (SomeDense through Dense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- Right (Money.toDenseRep x) === Cereal.decode (Cereal.encode x)+ Right (Money.toSomeDense x) === Cereal.decode (Cereal.encode x)+#endif -#ifdef VERSION_store+#ifdef HAS_serialise+ , QC.testProperty "Serialise encoding roundtrip" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ Just x === hush (Ser.deserialiseOrFail (Ser.serialise x))+ , QC.testProperty "Serialise encoding roundtrip (SomeDense)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ let x' = Money.toSomeDense x+ in Just x' === hush (Ser.deserialiseOrFail (Ser.serialise x'))+ , QC.testProperty "Serialise encoding roundtrip (Dense through SomeDense)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ Just x === hush (Ser.deserialiseOrFail (Ser.serialise (Money.toSomeDense x)))+ , QC.testProperty "Serialise encoding roundtrip (SomeDense through Dense)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ Just (Money.toSomeDense x) === hush (Ser.deserialiseOrFail (Ser.serialise x))+#endif++#ifdef HAS_store , QC.testProperty "Store encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> Right x === Store.decode (Store.encode x)- , QC.testProperty "Store encoding roundtrip (DenseRep)" $+ , QC.testProperty "Store encoding roundtrip (SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- let x' = Money.toDenseRep x+ let x' = Money.toSomeDense x in Right x' === Store.decode (Store.encode x')- , QC.testProperty "Store encoding roundtrip (Dense through DenseRep)" $+ , QC.testProperty "Store encoding roundtrip (Dense through SomeDense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- Right x === Store.decode (Store.encode (Money.toDenseRep x))- , QC.testProperty "Store encoding roundtrip (DenseRep through Dense)" $+ Right x === Store.decode (Store.encode (Money.toSomeDense x))+ , QC.testProperty "Store encoding roundtrip (SomeDense through Dense)" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->- Right (Money.toDenseRep x) === Store.decode (Store.encode x)+ Right (Money.toSomeDense x) === Store.decode (Store.encode x) #endif++#ifdef HAS_xmlbf+ , QC.testProperty "Xmlbf encoding roundtrip" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ Right x === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x)+ , QC.testProperty "Xmlbf encoding roundtrip (SomeDense)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ let x' = Money.toSomeDense x+ in Right x' === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x')+ , QC.testProperty "Xmlbf encoding roundtrip (Dense through SomeDense)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ Right x === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml (Money.toSomeDense x))+ , QC.testProperty "Xmlbf encoding roundtrip (SomeDense through Dense)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ Right (Money.toSomeDense x) === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x)+#endif ] testExchange :: Tasty.TestTree@@ -237,87 +304,137 @@ , QC.testProperty "read . show == id" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) -> x === read (show x)- , QC.testProperty "fromDiscreteRep . discreteRep == Just" $+ , QC.testProperty "read . show . Just == Just" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- Just x === Money.fromDiscreteRep (Money.toDiscreteRep x)- , QC.testProperty "fromDiscreteRep works only for same currency and scale" $- QC.forAll QC.arbitrary $ \(dr :: Money.DiscreteRep) ->- ((Money.discreteRepCurrency dr /= symbolVal pc) &&- (Money.discreteRepScale dr /=+ Just x === read (show (Just x))+ , QC.testProperty "fromSomeDiscrete . someDiscrete == Just" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Just x === Money.fromSomeDiscrete (Money.toSomeDiscrete x)+ , QC.testProperty "fromSomeDiscrete works only for same currency and scale" $+ QC.forAll QC.arbitrary $ \(dr :: Money.SomeDiscrete) ->+ ((Money.someDiscreteCurrency dr /= symbolVal pc) &&+ (Money.someDiscreteScale dr /= Money.scale (Proxy :: Proxy (Money.Scale currency unit)))- ) ==> isNothing (Money.fromDiscreteRep dr+ ) ==> isNothing (Money.fromSomeDiscrete dr :: Maybe (Money.Discrete currency unit))- , QC.testProperty "withDiscreteRep" $+ , QC.testProperty "withSomeDiscrete" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- let dr = Money.toDiscreteRep x- in ( Money.withDiscreteRep dr $ \x' ->- (show x, dr, Money.toDiscreteRep (x + 1))- === (show x', Money.toDiscreteRep x', Money.toDiscreteRep (x' + 1))+ let dr = Money.toSomeDiscrete x+ in ( Money.withSomeDiscrete dr $ \x' ->+ (show x, dr, Money.toSomeDiscrete (x + 1))+ === (show x', Money.toSomeDiscrete x', Money.toSomeDiscrete (x' + 1)) ) :: QC.Property +#ifdef HAS_aeson , QC.testProperty "Aeson encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) -> Just x === Ae.decode (Ae.encode x)- , QC.testProperty "Aeson encoding roundtrip (DiscreteRep)" $+ , QC.testProperty "Aeson encoding roundtrip (SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- let x' = Money.toDiscreteRep x+ let x' = Money.toSomeDiscrete x in Just x' === Ae.decode (Ae.encode x')- , QC.testProperty "Aeson encoding roundtrip (Discrete through DiscreteRep)" $+ , QC.testProperty "Aeson encoding roundtrip (Discrete through SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- Just x === Ae.decode (Ae.encode (Money.toDiscreteRep x))- , QC.testProperty "Aeson encoding roundtrip (DiscreteRep through Discrete)" $+ Just x === Ae.decode (Ae.encode (Money.toSomeDiscrete x))+ , QC.testProperty "Aeson encoding roundtrip (SomeDiscrete through Discrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- Just (Money.toDiscreteRep x) === Ae.decode (Ae.encode x)+ Just (Money.toSomeDiscrete x) === Ae.decode (Ae.encode x)+ , QC.testProperty "Aeson decoding of pre-0.4 format (Discrete, SomeDiscrete)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ let sx = Money.toSomeDiscrete x+ c = Money.someDiscreteCurrency sx+ r = Money.someDiscreteScale sx+ a = Money.someDiscreteAmount sx+ bs = Ae.encode ("Discrete", c, numerator r, denominator r, a)+ in (Just x === Ae.decode bs) .&&.+ (Just sx === Ae.decode bs)+#endif +#ifdef HAS_binary , QC.testProperty "Binary encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) -> let Right (_,_,y) = Binary.decodeOrFail (Binary.encode x) in x === y- , QC.testProperty "Binary encoding roundtrip (DiscreteRep)" $+ , QC.testProperty "Binary encoding roundtrip (SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- let x' = Money.toDiscreteRep x+ let x' = Money.toSomeDiscrete x bs = Binary.encode x' in Right (mempty, BSL.length bs, x') === Binary.decodeOrFail bs- , QC.testProperty "Binary encoding roundtrip (Discrete through DiscreteRep)" $+ , QC.testProperty "Binary encoding roundtrip (Discrete through SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- let x' = Money.toDiscreteRep x+ let x' = Money.toSomeDiscrete x bs = Binary.encode x' in Right (mempty, BSL.length bs, x) === Binary.decodeOrFail bs- , QC.testProperty "Binary encoding roundtrip (DiscreteRep through Discrete)" $+ , QC.testProperty "Binary encoding roundtrip (SomeDiscrete through Discrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- let x' = Money.toDiscreteRep x+ let x' = Money.toSomeDiscrete x bs = Binary.encode x in Right (mempty, BSL.length bs, x') === Binary.decodeOrFail bs+#endif +#ifdef HAS_cereal , QC.testProperty "Cereal encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) -> Right x === Cereal.decode (Cereal.encode x)- , QC.testProperty "Cereal encoding roundtrip (DiscreteRep)" $+ , QC.testProperty "Cereal encoding roundtrip (SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- let x' = Money.toDiscreteRep x+ let x' = Money.toSomeDiscrete x in Right x' === Cereal.decode (Cereal.encode x')- , QC.testProperty "Cereal encoding roundtrip (Discrete through DiscreteRep)" $+ , QC.testProperty "Cereal encoding roundtrip (Discrete through SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- Right x === Cereal.decode (Cereal.encode (Money.toDiscreteRep x))- , QC.testProperty "Cereal encoding roundtrip (DiscreteRep through Discrete)" $+ Right x === Cereal.decode (Cereal.encode (Money.toSomeDiscrete x))+ , QC.testProperty "Cereal encoding roundtrip (SomeDiscrete through Discrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- Right (Money.toDiscreteRep x) === Cereal.decode (Cereal.encode x)+ Right (Money.toSomeDiscrete x) === Cereal.decode (Cereal.encode x)+#endif -#ifdef VERSION_store+#ifdef HAS_serialise+ , QC.testProperty "Serialise encoding roundtrip" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Just x === hush (Ser.deserialiseOrFail (Ser.serialise x))+ , QC.testProperty "Serialise encoding roundtrip (SomeDiscrete)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ let x' = Money.toSomeDiscrete x+ in Just x' === hush (Ser.deserialiseOrFail (Ser.serialise x'))+ , QC.testProperty "Serialise encoding roundtrip (Discrete through SomeDiscrete)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Just x === hush (Ser.deserialiseOrFail (Ser.serialise (Money.toSomeDiscrete x)))+ , QC.testProperty "Serialise encoding roundtrip (SomeDiscrete through Discrete)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Just (Money.toSomeDiscrete x) === hush (Ser.deserialiseOrFail (Ser.serialise x))+#endif++#ifdef HAS_store , QC.testProperty "Store encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) -> Right x === Store.decode (Store.encode x)- , QC.testProperty "Store encoding roundtrip (DiscreteRep)" $+ , QC.testProperty "Store encoding roundtrip (SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- let x' = Money.toDiscreteRep x+ let x' = Money.toSomeDiscrete x in Right x' === Store.decode (Store.encode x')- , QC.testProperty "Store encoding roundtrip (Discrete through DiscreteRep)" $+ , QC.testProperty "Store encoding roundtrip (Discrete through SomeDiscrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- Right x === Store.decode (Store.encode (Money.toDiscreteRep x))- , QC.testProperty "Store encoding roundtrip (DiscreteRep through Discrete)" $+ Right x === Store.decode (Store.encode (Money.toSomeDiscrete x))+ , QC.testProperty "Store encoding roundtrip (SomeDiscrete through Discrete)" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->- Right (Money.toDiscreteRep x) === Store.decode (Store.encode x)+ Right (Money.toSomeDiscrete x) === Store.decode (Store.encode x) #endif++#ifdef HAS_xmlbf+ , QC.testProperty "Xmlbf encoding roundtrip" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Right x === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x)+ , QC.testProperty "Xmlbf encoding roundtrip (SomeDiscrete)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ let x' = Money.toSomeDiscrete x+ in Right x' === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x')+ , QC.testProperty "Xmlbf encoding roundtrip (Discrete through SomeDiscrete)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Right x === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml (Money.toSomeDiscrete x))+ , QC.testProperty "Xmlbf encoding roundtrip (SomeDiscrete through Discrete)" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Right (Money.toSomeDiscrete x) === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x)+#endif ] testExchangeRate@@ -332,6 +449,9 @@ [ QC.testProperty "read . show == id" $ QC.forAll QC.arbitrary $ \(xr :: Money.ExchangeRate src dst) -> xr === read (show xr)+ , QC.testProperty "read . show . Just == Just" $+ QC.forAll QC.arbitrary $ \(xr :: Money.ExchangeRate src dst) ->+ Just xr === read (show (Just xr)) , QC.testProperty "flipExchangeRate . flipExchangeRate == id" $ QC.forAll QC.arbitrary $ \(xr :: Money.ExchangeRate src dst) -> let xr' = Money.flipExchangeRate xr@@ -354,84 +474,131 @@ , xr :: Money.ExchangeRate src dst ) -> (Money.fromExchangeRate xr /= 1) ==> (toRational c0 /= toRational (Money.exchange xr c0))- , QC.testProperty "fromExchangeRateRep . exchangeRateRep == Just" $+ , QC.testProperty "fromSomeExchangeRate . someExchangeRate == Just" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- Just x === Money.fromExchangeRateRep (Money.toExchangeRateRep x)- , QC.testProperty "fromExchangeRateRep works only for same currencies" $- QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRateRep) ->- ((Money.exchangeRateRepSrcCurrency x /= symbolVal ps) &&- (Money.exchangeRateRepDstCurrency x /= symbolVal pd))- ==> isNothing (Money.fromExchangeRateRep x+ Just x === Money.fromSomeExchangeRate (Money.toSomeExchangeRate x)+ , QC.testProperty "fromSomeExchangeRate works only for same currencies" $+ QC.forAll QC.arbitrary $ \(x :: Money.SomeExchangeRate) ->+ ((Money.someExchangeRateSrcCurrency x /= symbolVal ps) &&+ (Money.someExchangeRateDstCurrency x /= symbolVal pd))+ ==> isNothing (Money.fromSomeExchangeRate x :: Maybe (Money.ExchangeRate src dst))- , QC.testProperty "withExchangeRateRep" $+ , QC.testProperty "withSomeExchangeRate" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- let dr = Money.toExchangeRateRep x- in Money.withExchangeRateRep dr $ \x' ->- (show x, dr) === (show x', Money.toExchangeRateRep x')+ let dr = Money.toSomeExchangeRate x+ in Money.withSomeExchangeRate dr $ \x' ->+ (show x, dr) === (show x', Money.toSomeExchangeRate x') +#ifdef HAS_aeson , QC.testProperty "Aeson encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) -> Just x === Ae.decode (Ae.encode x)- , QC.testProperty "Aeson encoding roundtrip (ExchangeRateRep)" $+ , QC.testProperty "Aeson encoding roundtrip (SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- let x' = Money.toExchangeRateRep x+ let x' = Money.toSomeExchangeRate x in Just x' === Ae.decode (Ae.encode x')- , QC.testProperty "Aeson encoding roundtrip (ExchangeRate through ExchangeRateRep)" $+ , QC.testProperty "Aeson encoding roundtrip (ExchangeRate through SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- Just x === Ae.decode (Ae.encode (Money.toExchangeRateRep x))- , QC.testProperty "Aeson encoding roundtrip (ExchangeRateRep through ExchangeRate)" $+ Just x === Ae.decode (Ae.encode (Money.toSomeExchangeRate x))+ , QC.testProperty "Aeson encoding roundtrip (SomeExchangeRate through ExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- Just (Money.toExchangeRateRep x) === Ae.decode (Ae.encode x)+ Just (Money.toSomeExchangeRate x) === Ae.decode (Ae.encode x)+ , QC.testProperty "Aeson decoding of pre-0.4 format (ExchangeRate, SomeExchangeRate)" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ let sx = Money.toSomeExchangeRate x+ src = Money.someExchangeRateSrcCurrency sx+ dst = Money.someExchangeRateDstCurrency sx+ r = Money.someExchangeRateRate sx+ bs = Ae.encode ("ExchangeRate", src, dst, numerator r, denominator r)+ in (Just x === Ae.decode bs) .&&.+ (Just sx === Ae.decode bs)+#endif +#ifdef HAS_binary , QC.testProperty "Binary encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) -> let Right (_,_,y) = Binary.decodeOrFail (Binary.encode x) in x === y- , QC.testProperty "Binary encoding roundtrip (ExchangeRateRep)" $+ , QC.testProperty "Binary encoding roundtrip (SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- let x' = Money.toExchangeRateRep x+ let x' = Money.toSomeExchangeRate x bs = Binary.encode x' in Right (mempty, BSL.length bs, x') === Binary.decodeOrFail bs- , QC.testProperty "Binary encoding roundtrip (ExchangeRate through ExchangeRateRep)" $+ , QC.testProperty "Binary encoding roundtrip (ExchangeRate through SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- let x' = Money.toExchangeRateRep x+ let x' = Money.toSomeExchangeRate x bs = Binary.encode x' in Right (mempty, BSL.length bs, x) === Binary.decodeOrFail bs- , QC.testProperty "Binary encoding roundtrip (ExchangeRateRep through ExchangeRate)" $+ , QC.testProperty "Binary encoding roundtrip (SomeExchangeRate through ExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- let x' = Money.toExchangeRateRep x+ let x' = Money.toSomeExchangeRate x bs = Binary.encode x in Right (mempty, BSL.length bs, x') === Binary.decodeOrFail bs+#endif +#ifdef HAS_cereal , QC.testProperty "Cereal encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) -> Right x === Cereal.decode (Cereal.encode x)- , QC.testProperty "Cereal encoding roundtrip (ExchangeRateRep)" $+ , QC.testProperty "Cereal encoding roundtrip (SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- let x' = Money.toExchangeRateRep x+ let x' = Money.toSomeExchangeRate x in Right x' === Cereal.decode (Cereal.encode x')- , QC.testProperty "Cereal encoding roundtrip (ExchangeRate through ExchangeRateRep)" $+ , QC.testProperty "Cereal encoding roundtrip (ExchangeRate through SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- Right x === Cereal.decode (Cereal.encode (Money.toExchangeRateRep x))- , QC.testProperty "Cereal encoding roundtrip (ExchangeRateRep through ExchangeRate)" $+ Right x === Cereal.decode (Cereal.encode (Money.toSomeExchangeRate x))+ , QC.testProperty "Cereal encoding roundtrip (SomeExchangeRate through ExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- Right (Money.toExchangeRateRep x) === Cereal.decode (Cereal.encode x)+ Right (Money.toSomeExchangeRate x) === Cereal.decode (Cereal.encode x)+#endif -#ifdef VERSION_store+#ifdef HAS_serialise+ , QC.testProperty "Serialise encoding roundtrip" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ Just x === hush (Ser.deserialiseOrFail (Ser.serialise x))+ , QC.testProperty "Serialise encoding roundtrip (SomeExchangeRate)" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ let x' = Money.toSomeExchangeRate x+ in Just x' === hush (Ser.deserialiseOrFail (Ser.serialise x'))+ , QC.testProperty "Serialise encoding roundtrip (ExchangeRate through SomeExchangeRate)" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ Just x === hush (Ser.deserialiseOrFail (Ser.serialise (Money.toSomeExchangeRate x)))+ , QC.testProperty "Serialise encoding roundtrip (SomeExchangeRate through ExchangeRate)" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ Just (Money.toSomeExchangeRate x) === hush (Ser.deserialiseOrFail (Ser.serialise x))+#endif++#ifdef HAS_store , QC.testProperty "Store encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) -> Right x === Store.decode (Store.encode x)- , QC.testProperty "Store encoding roundtrip (ExchangeRateRep)" $+ , QC.testProperty "Store encoding roundtrip (SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- let x' = Money.toExchangeRateRep x+ let x' = Money.toSomeExchangeRate x in Right x' === Store.decode (Store.encode x')- , QC.testProperty "Store encoding roundtrip (ExchangeRate through ExchangeRateRep)" $+ , QC.testProperty "Store encoding roundtrip (ExchangeRate through SomeExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- Right x === Store.decode (Store.encode (Money.toExchangeRateRep x))- , QC.testProperty "Store encoding roundtrip (ExchangeRateRep through ExchangeRate)" $+ Right x === Store.decode (Store.encode (Money.toSomeExchangeRate x))+ , QC.testProperty "Store encoding roundtrip (SomeExchangeRate through ExchangeRate)" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->- Right (Money.toExchangeRateRep x) === Store.decode (Store.encode x)+ Right (Money.toSomeExchangeRate x) === Store.decode (Store.encode x) #endif++#ifdef HAS_xmlbf+ , QC.testProperty "Xmlbf encoding roundtrip" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ Right x === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x)+ , QC.testProperty "Xmlbf encoding roundtrip (SomeExchangeRate)" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ let x' = Money.toSomeExchangeRate x+ in Right x' === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x')+ , QC.testProperty "Xmlbf encoding roundtrip (ExchangeRate through SomeExchangeRate)" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ Right x === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml (Money.toSomeExchangeRate x))+ , QC.testProperty "Xmlbf encoding roundtrip (SomeExchangeRate through ExchangeRate)" $+ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->+ Right (Money.toSomeExchangeRate x) === Xmlbf.runParser Xmlbf.fromXml (Xmlbf.toXml x)+#endif ] testRounding@@ -446,8 +613,25 @@ , QC.testProperty "ceiling" $ QC.forAll QC.arbitrary (g Money.ceiling) , QC.testProperty "round" $ QC.forAll QC.arbitrary (g Money.round) , QC.testProperty "truncate" $ QC.forAll QC.arbitrary (g Money.truncate)+ , QC.testProperty "floor no reminder" $ QC.forAll QC.arbitrary (h Money.floor)+ , QC.testProperty "ceiling no reminder" $ QC.forAll QC.arbitrary (h Money.ceiling)+ , QC.testProperty "round no reminder" $ QC.forAll QC.arbitrary (h Money.round)+ , QC.testProperty "truncate no reminder" $ QC.forAll QC.arbitrary (h Money.truncate) ] where- g f = \(x :: Money.Dense currency) -> x === case f x of- (y, Nothing) -> Money.fromDiscrete (y :: Money.Discrete currency unit)- (y, Just z) -> Money.fromDiscrete y + z+ g :: (Money.Dense currency -> (Money.Discrete' currency (Money.Scale currency unit), Money.Dense currency))+ -> Money.Dense currency+ -> QC.Property+ g f = \x -> x === case f x of (y, z) -> Money.fromDiscrete y + z++ h :: (Money.Dense currency -> (Money.Discrete' currency (Money.Scale currency unit), Money.Dense currency))+ -> Money.Discrete currency unit+ -> QC.Property+ h f = \x -> (Money.fromDiscrete x) === case f (Money.fromDiscrete x) of+ (y, 0) -> Money.fromDiscrete y+ (_, _) -> error "testRounding.h: unexpected"++hush :: Either a b -> Maybe b+hush (Left _ ) = Nothing+hush (Right b) = Just b+