safe-money 0.4.1 → 0.5
raw patch · 7 files changed
+2489/−1607 lines, 7 filesdep +vector-spacePVP ok
version bump matches the API change (PVP)
Dependencies added: vector-space
API changes (from Hackage documentation)
- Money: ceiling :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
- Money: flipExchangeRate :: ExchangeRate a b -> ExchangeRate b a
- Money: floor :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
- Money: fromDiscrete :: GoodScale scale => Discrete' currency scale -> Dense currency
- Money: fromExchangeRate :: ExchangeRate src dst -> Rational
- Money: round :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
- Money: truncate :: GoodScale scale => Dense currency -> (Discrete' currency scale, Dense currency)
+ Money: Ceiling :: Approximation
+ Money: Floor :: Approximation
+ Money: Round :: Approximation
+ Money: Truncate :: Approximation
+ Money: data Approximation
+ Money: dense' :: Rational -> Dense currency
+ Money: denseCurrency :: KnownSymbol currency => Dense currency -> String
+ Money: denseFromDecimal :: Maybe Char -> Char -> String -> Maybe (Dense currency)
+ Money: denseFromDiscrete :: GoodScale scale => Discrete' currency scale -> Dense currency
+ Money: denseToDecimal :: GoodScale scale => Approximation -> Bool -> Maybe Char -> Char -> Word8 -> Proxy scale -> Dense currency -> Maybe String
+ Money: discrete :: GoodScale scale => Integer -> Discrete' currency scale
+ Money: discreteCurrency :: forall currency scale. (KnownSymbol currency, GoodScale scale) => Discrete' currency scale -> String
+ Money: discreteFromDecimal :: GoodScale scale => Maybe Char -> Char -> String -> Maybe (Discrete' currency scale)
+ Money: discreteFromDense :: forall currency scale. GoodScale scale => Approximation -> Dense currency -> (Discrete' currency scale, Dense currency)
+ Money: exchangeRateFromDecimal :: Maybe Char -> Char -> String -> Maybe (ExchangeRate src dst)
+ Money: exchangeRateRecip :: ExchangeRate a b -> ExchangeRate b a
+ Money: exchangeRateToDecimal :: Approximation -> Maybe Char -> Char -> Word8 -> ExchangeRate src dst -> Maybe String
+ Money: exchangeRateToRational :: ExchangeRate src dst -> Rational
- Money: type GoodScale (scale :: (Nat, Nat)) = (CmpNat 0 (Fst scale) ~ LT, CmpNat 0 (Snd scale) ~ LT, KnownNat (Fst scale), KnownNat (Snd scale))
+ Money: type GoodScale (scale :: (Nat, Nat)) = (CmpNat 0 (Fst scale) ~ 'LT, CmpNat 0 (Snd scale) ~ 'LT, KnownNat (Fst scale), KnownNat (Snd scale))
Files
- LICENSE +1/−1
- README.md +2/−2
- changelog.md +66/−19
- safe-money.cabal +15/−4
- src/Money.hs +21/−8
- src/Money/Internal.hs +1902/−1532
- test/Main.hs +482/−41
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2016-2017, Renzo Carbonara+Copyright (c) 2016-2018, Renzo Carbonara All rights reserved.
README.md view
@@ -2,5 +2,5 @@ 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`).+You can enter a development shell with `nix-shell`, or build with `nix build+-f release.nix ghc822.safe-money` (or similar, check `release.nix`).
changelog.md view
@@ -1,3 +1,47 @@+# Version 0.5++* _COMPILER ASSISTED BREAKING CHANGE_. The `round`, `floor`, `ceiling` and+ `truncate` functions were replaced by a single `discreteFromDense` function+ taking an argument of type `Approximation` (`Round`, `Floor`, `Ceiling` or+ `Truncate`) as an argument.++* _COMPILER ASSISTED BREAKING CHANGE_. The `fromDiscrete` function was+ renamed to `denseFromDiscrete`.++* _COMPILER ASSISTED BREAKING CHANGE_. The `fromExchangeRate` function was+ renamed to `exchangeRateToRational`.++* _COMPILER ASSISTED BREAKING CHANGE_. The `flipExchangeRate` function was+ renamed to `exchangeRateRecip`.++* _COMPILER ASSISTED BREAKING CHANGE_. The `Dense` is not an instance of+ `Fractional` anymore because `recip` and `/` could potentially crash.++* Introduced new functions for rendering and parsing decimal reperesentations+ of monetary amounts: `denseCurrency`, `discreteCurrency`, `denseFromDecimal`,+ `denseToDecimal`, `discreteFromDecimal`, `exchangeRateFromDecimal`,+ `exchangeRateToDecimal`.++* Introduced optional `AdditiveGroup` and `VectorSpace` group instances for+ `Dense` and `Discrete`. These type-classes come from the `vector-space`+ library and they can be enabled or disabled via the `vector-space` Cabal flag,+ which is enabled by default.++* Introduced `discrete` constructor which behaves just like `fromInteger`.++* Introduced the `dense'` constructor as an unsafe but convenient version of+ `dense`.++* The `ErrScaleNonCanonical` type is now exported.++* The `Money.Internal` module is now exposed, but hidden from the Haddock+ documentation.++* New scale: `"BTC" "millibitcoin"`.++* Added many tests.++ # Version 0.4.1 * `ExchangeRate` is now a `Category`.@@ -5,26 +49,25 @@ # 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 REQUIRING HUMAN INTERVENTION**. 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`+* _COMPILER ASSISTED BREAKING CHANGE_. Changed the `Rep` suffix for a `Some` prefix Everywhere. For example, `DenseRep` was renamed to `SomeDense`. -* BREAKING CHANGE. COMPILER WILL COMPLAIN: Replaced the+* _COMPILER ASSISTED BREAKING CHANGE_. 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+* _COMPILER ASSISTED BREAKING CHANGE_. The `truncate`, `floor`, `celing` and `round` functions now return just `0` as a reminder if there's no significant reminder, instead of `Nothing`. @@ -40,18 +83,22 @@ # Version 0.3 -* BREAKING CHANGE: The `Data.Money` module was renamed to `Money`.+* _COMPILER ASSISTED BREAKING CHANGE_. The `Data.Money` module was renamed to+ `Money`. -* BREAKING CHANGE: The `Data.Money.Internal` module is not exposed anymore. All- of its contents are now exported from the `Money` module.+* _COMPILER ASSISTED BREAKING CHANGE_. The `Data.Money.Internal` module is not+ exposed anymore. All of its contents are now exported from the `Money` module. -* BREAKING CHANGE: Renamed `discreteRep` to `toDiscreteRep`.+* _COMPILER ASSISTED BREAKING CHANGE_. Renamed `discreteRep` to+ `toDiscreteRep`. -* BREAKING CHANGE: Renamed `denseRep` to `toDenseRep`.+* _COMPILER ASSISTED BREAKING CHANGE_. Renamed `denseRep` to `toDenseRep`. -* BREAKING CHANGE: Renamed `exchangeRateRep` to `toExchangeRateRep`.+* _COMPILER ASSISTED BREAKING CHANGE_. Renamed `exchangeRateRep` to+ `toExchangeRateRep`. -* BREAKING CHANGE: Renamed Iceleandic currency `"eyir"` to `"eyrir"`+* _COMPILER ASSISTED BREAKING CHANGE_. Renamed Iceleandic currency `"eyir"` to+ `"eyrir"` * Remove upper bound constraints from all dependencies except `base`.
safe-money.cabal view
@@ -1,8 +1,8 @@ name: safe-money-version: 0.4.1+version: 0.5 license: BSD3 license-file: LICENSE-copyright: Copyright (c) Renzo Carbonara 2016-2017+copyright: Copyright (c) Renzo Carbonara 2016-2018 author: Renzo Carbonara maintainer: renλren!zone stability: Experimental@@ -33,9 +33,10 @@ default-language: Haskell2010 hs-source-dirs: src ghc-options: -Wall -O2- exposed-modules: Money- other-modules: Money.Internal build-depends: base (>=4.8 && <5.0), constraints+ exposed-modules:+ Money+ Money.Internal if flag(aeson) build-depends: aeson (>=0.9)@@ -58,6 +59,9 @@ if (flag(store) && !impl(ghcjs)) build-depends: store (>=0.2) cpp-options: -DHAS_store+ if flag(vector-space)+ build-depends: vector-space (>=0.12)+ cpp-options: -DHAS_vector_space if flag(xmlbf) build-depends: xmlbf (>=0.2), text cpp-options: -DHAS_xmlbf@@ -97,6 +101,9 @@ if (flag(store) && !impl(ghcjs)) build-depends: store cpp-options: -DHAS_store+ if flag(vector-space)+ build-depends: vector-space+ cpp-options: -DHAS_vector_space if flag(xmlbf) build-depends: xmlbf, text cpp-options: -DHAS_xmlbf@@ -127,6 +134,10 @@ manual: True flag serialise description: Provide instances for @serialise@+ default: True+ manual: True+flag vector-space+ description: Provide instances for @vector-space@ default: True manual: True flag xmlbf
src/Money.hs view
@@ -17,29 +17,39 @@ -- -- This module offers plenty of documentation, but for a deep explanation of -- how all of the pieces fit together, please read--- https://ren.zone/articles/safe-money+-- <https://ren.zone/articles/safe-money>. Notice, however, that this library+-- has changed a bit since that article was written. You can always see the+-- [change log](https://github.com/k0001/safe-money/blob/master/changelog.md) to+-- understand what has changed. module Money ( -- * Dense monetary values I.Dense+ , I.denseCurrency , I.dense+ , I.dense'+ , I.denseFromDiscrete+ , I.denseFromDecimal+ , I.denseToDecimal -- * Discrete monetary values , I.Discrete , I.Discrete'- , I.fromDiscrete- , I.round- , I.ceiling- , I.floor- , I.truncate+ , I.discrete+ , I.discreteCurrency+ , I.discreteFromDense+ , I.discreteFromDecimal -- * Currency scales , I.Scale , I.GoodScale+ , I.ErrScaleNonCanonical , I.scale -- * Currency exchange , I.ExchangeRate , I.exchangeRate- , I.fromExchangeRate- , I.flipExchangeRate , I.exchange+ , I.exchangeRateRecip+ , I.exchangeRateFromDecimal+ , I.exchangeRateToDecimal+ , I.exchangeRateToRational -- * Serializable representations , I.SomeDense , I.toSomeDense@@ -64,6 +74,8 @@ , I.someExchangeRateSrcCurrency , I.someExchangeRateDstCurrency , I.someExchangeRateRate+ -- * Misc+ , I.Approximation(Round, Floor, Ceiling, Truncate) ) where import qualified Money.Internal as I@@ -768,6 +780,7 @@ -- | Bitcoin type instance I.Scale "BTC" "BTC" = '(100000000, 1) type instance I.Scale "BTC" "bitcoin" = '(1, 1)+type instance I.Scale "BTC" "millibitcoin" = '(1000, 1) type instance I.Scale "BTC" "satoshi" = '(100000000, 1) -- | Bitcoin
src/Money/Internal.hs view
@@ -4,1536 +4,1906 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE KindSignatures #-}-{-# LANGUAGE LambdaCase #-}-{-# 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.Category (Category((.), id))-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, z)--- @------ @--- x /= y--- @------ @--- z < 0--- @------ 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, z)--- @------ @--- x /= y--- @------ @--- z > 0--- @------ 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)----- | Composition of 'ExchangeRate's multiplies exchange rates together:------ @--- 'fromExchangeRate' x * 'fromExchangeRate' y == 'fromExchangeRate' (x . y)--- @------ Identity:------ @--- x == x . id == id . x--- @------ Associativity:------ @--- x . y . z == x . (y . z) == (x . y) . z--- @------ Conmutativity (provided the types allow for composition):------ @--- x . y == y . x--- @------ Multiplicative inverse:------ @--- 1 == 'fromExchangeRate' (x . 'flipExchangeRate' x)--- @-instance Category ExchangeRate where- id = ExchangeRate 1- {-# INLINE id #-}- ExchangeRate a . ExchangeRate b = ExchangeRate (a * b)- {-# INLINE (.) #-}--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'.------ This function retuns the multiplicative inverse of the given 'ExchangeRate',--- leading to the following 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+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE KindSignatures #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE PolyKinds #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeOperators #-}+{-# LANGUAGE UndecidableInstances #-}+{-# OPTIONS_HADDOCK hide #-}+{-# OPTIONS_GHC -Wno-redundant-constraints #-}++-- | This is an internal module. You may use stuff exported from here, but we+-- can't garantee their stability.+module Money.Internal+ ( -- * Dense monetary values+ Dense+ , denseCurrency+ , dense+ , dense'+ , denseFromDiscrete+ , denseFromDecimal+ , denseToDecimal+ -- * Discrete monetary values+ , Discrete+ , Discrete'+ , discrete+ , discreteCurrency+ , discreteFromDense+ , discreteFromDecimal+ -- * Currency scales+ , Scale+ , GoodScale+ , ErrScaleNonCanonical+ , scale+ -- * Currency exchange+ , ExchangeRate+ , exchangeRate+ , exchange+ , exchangeRateFromDecimal+ , exchangeRateToDecimal+ , exchangeRateToRational+ , exchangeRateRecip+ -- * 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+ -- * Misc+ , Approximation(Round, Floor, Ceiling, Truncate)+ , rationalToDecimal+ , rationalFromDecimal+ ) where++import Control.Applicative ((<|>), empty)+import Control.Category (Category((.), id))+import Control.Monad ((<=<), guard, when)+import qualified Data.Char as Char+import Data.Constraint (Dict(Dict))+import Data.Functor (($>))+import Data.Foldable (for_)+import qualified Data.List as List+import Data.Monoid ((<>))+import Data.Proxy (Proxy(..))+import Data.Ratio ((%), numerator, denominator)+import Data.Word (Word8)+import GHC.Exts (Constraint, fromList)+import qualified GHC.Generics as GHC+import GHC.TypeLits+ (Symbol, SomeSymbol(..), Nat, SomeNat(..), CmpNat, KnownSymbol, KnownNat,+ natVal, someNatVal, symbolVal, someSymbolVal)+import qualified GHC.TypeLits as GHC+import Numeric.Natural (Natural)+import Prelude hiding ((.), id)+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+import qualified Data.Text as T+#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_vector_space+import qualified Data.AdditiveGroup as AG+import qualified Data.VectorSpace as VS+#endif++#ifdef HAS_xmlbf+import qualified Xmlbf+#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 (e.g., an exact number of coins and bills), 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 do manage to represent @USD+-- 1.709@ somehow, and you eventually multiply @USD 1.705@ by @4@ for+-- example, then you end up with @USD 6.82@, which is again a value+-- representable as USD cents. In other words, 'Dense' monetary values+-- allow us to perform precise calculations deferring the conversion to a+-- 'Discrete' monetary values as much as posible. Once you are ready to+-- approximate a 'Dense' value to a 'Discrete' value you can use one+-- 'discreteFromDense'. Otherwise, using 'toRational' you can obtain a+-- precise 'Rational' representation.++-- Construct 'Dense' monetary values using 'dense', 'dense'',+-- 'denseFromDiscrete', 'denseFromDecimal'.+--+-- /WARNING/ if you want to treat a dense monetary value as a /Real/ number+-- like 'Float' or 'Double', 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, Real, GHC.Generic)++-- | Notice that multiplication of 'Dense' values doesn't make sense:+--+-- @+-- ('*') :: 'Dense' currency -> 'Dense' currency -> 'Dense' currency+-- @+--+-- How is '*' implemented, then? It behaves as the /scalar multiplication/ of a+-- 'Dense' amount by a 'Rational' scalar. That is, you can think of '*' as+-- having one of the the following types:+--+-- @+-- ('*') :: 'Rational' -> 'Dense' currency -> 'Dense' currency+-- @+--+-- @+-- ('*') :: 'Dense' currency -> 'Rational' -> 'Dense' currency@+-- @+--+-- That is:+--+-- @+-- 'dense'' (1 '%' 4) '*' 'dense'' (1 '%' 2) == 'dense'' (1 '%' 8)+-- @+--+-- In fact, if you compiled this library with support for+-- 'Data.VectorSpace.VectorSpace', then '*' functions exactly as+-- 'Data.VectorSpace.*^'.+--+-- @+-- ('*') == ('Data.VectorSpace.*^')+-- @+--+-- @+-- ('*') == 'flip' ('Data.VectorSpace.*^')+-- @+deriving instance Num (Dense currency)++type family ErrFractionalDense :: Constraint where+ ErrFractionalDense+ = GHC.TypeError+ (('GHC.Text "The ") 'GHC.:<>:+ ('GHC.ShowType Dense) 'GHC.:<>:+ ('GHC.Text " type is deliberately not an instance of ") 'GHC.:<>:+ ('GHC.ShowType Fractional) 'GHC.:$$:+ ('GHC.Text "because functions like 'recip' and '/' can diverge.") 'GHC.:$$:+ ('GHC.Text "Temporarily convert the ") 'GHC.:<>:+ ('GHC.ShowType Dense) 'GHC.:<>:+ ('GHC.Text " value to a ") 'GHC.:<>:+ ('GHC.ShowType Rational) 'GHC.:$$:+ ('GHC.Text " if you know what you are doing."))++instance ErrFractionalDense => Fractional (Dense currency) where+ fromRational = undefined+ recip = undefined++-- |+-- @+-- > 'show' ('dense'' (1 '%' 3) :: 'Dense' \"USD\")+-- \"Dense \\\"USD\\\" 1%3\"+-- @+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)+-- @+--+-- Notice that 'dense' returns 'Nothing' in case the given 'Rational''s+-- denominator is zero, which although unlikely, it is possible if the+-- 'Rational' was unsafely constructed. When dealing with hardcoded or trusted+-- 'Rational' values, you can use 'dense'' instead of 'dense' which unsafely+-- constructs a 'Dense'.+dense :: Rational -> Maybe (Dense currency)+dense = \r ->+ if denominator r /= 0+ then Just (Dense r)+ else Nothing+{-# INLINE dense #-}++-- | Unsafely build a 'Dense' monetary value from a 'Rational' value. Contrary+-- to 'dense', this function *crashes* if the given 'Rational' has zero as a+-- denominator, which is something very unlikely to happen unless the given+-- 'Rational' was itself unsafely constructed. Other than that, 'dense' and+-- 'dense'' behave the same.+--+-- Prefer to use 'dense' when dealing with 'Rational' inputs from untrusted+-- sources.+--+-- @+-- 'denominator' x /= 0+-- ⇒ 'dense' x == 'Just' ('dense'' x)+-- @+--+-- @+-- 'denominator' x == 0+-- ⇒ 'undefined' == 'dense'' x+-- @+dense' :: Rational -> Dense currency+dense' = \r ->+ if denominator r /= 0+ then Dense r+ else error "dense': malformed Rational given (denominator is zero)."+{-# INLINABLE dense' #-}++-- | 'Dense' currency identifier.+--+-- @+-- > 'denseCurrency' ('dense'' 4 :: 'Dense' \"USD\")+-- \"USD\"+-- @+denseCurrency :: KnownSymbol currency => Dense currency -> String+denseCurrency = symbolVal+{-# INLINE denseCurrency #-}++-- | '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 'discrete', 'fromIntegral', 'fromInteger',+-- 'discreteFromDense', 'discreteFromDecimal'.+--+-- 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:+--+-- @+-- 'discrete' 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 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 => Real (Discrete' currency scale)+deriving instance GoodScale scale => Integral (Discrete' currency scale)+deriving instance GoodScale scale => GHC.Generic (Discrete' currency scale)++-- | Notice that multiplication of 'Discrete'' values doesn't make sense:+--+-- @+-- ('*') :: 'Discrete'' currency scale -> 'Discrete'' currency scale -> 'Discrete'' currency scale+-- @+--+-- How is '*' implemented, then? It behaves as the /scalar multiplication/ of a+-- 'Discrete'' amount by an 'Integer' scalar. That is, you can think of '*' as+-- having one of the the following types:+--+-- @+-- ('*') :: 'Integer' -> 'Discrete'' currency scale -> 'Discrete'' currency scale+-- @+--+-- @+-- ('*') :: 'Discrete'' currency scale -> 'Integer' -> 'Discrete'' currency scale@+-- @+--+-- That is:+--+-- @+-- 'discrete' 2 '*' 'discrete' 4 == 'discrete' 8+-- @+--+-- In fact, if you compiled this library with support for+-- 'Data.VectorSpace.VectorSpace', then '*' functions exactly as+-- 'Data.VectorSpace.*^'.+--+-- @+-- ('*') == ('Data.VectorSpace.*^')+-- @+--+-- @+-- ('*') == 'flip' ('Data.VectorSpace.*^')+-- @+deriving instance GoodScale scale => Num (Discrete' currency scale)++-- |+-- @+-- > 'show' ('discrete' 123 :: 'Discrete' \"USD\" \"cent\")+-- \"Discrete \\\"USD\\\" 100%1 123\"+-- @+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), " "+ ]))+ fmap Discrete Read.readPrec++type family ErrFractionalDiscrete :: Constraint where+ ErrFractionalDiscrete+ = 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."))++instance+ ( ErrFractionalDiscrete+ , GoodScale scale+ ) => Fractional (Discrete' currency scale) where+ fromRational = undefined+ recip = undefined++-- | Construct a 'Discrete' value.+discrete :: GoodScale scale => Integer -> Discrete' currency scale+discrete = Discrete+{-# INLINE discrete #-}+++-- | Convert currency 'Discrete' monetary value into a 'Dense' monetary+-- value.+denseFromDiscrete+ :: GoodScale scale+ => Discrete' currency scale+ -> Dense currency -- ^+denseFromDiscrete = \c@(Discrete i) -> Dense (fromInteger i / scale c)+{-# INLINE denseFromDiscrete #-}++-- | 'Discrete' currency identifier.+--+-- @+-- > 'discreteCurrency' ('discrete' 4 :: 'Discrete' \"USD\" \"cent\")+-- \"USD\"+-- @+discreteCurrency+ :: forall currency scale+ . (KnownSymbol currency, GoodScale scale)+ => Discrete' currency scale+ -> String -- ^+discreteCurrency = \_ -> symbolVal (Proxy :: Proxy currency)+{-# INLINE discreteCurrency #-}++-- | Method for approximating a fractional number to an integer number.+data Approximation+ = Round+ -- ^ Approximate @x@ to the nearest integer, or to the nearest even integer if+ -- @x@ is equidistant between two integers.+ | Floor+ -- ^ Approximate @x@ to the nearest integer less than or equal to @x@.+ | Ceiling+ -- ^ Approximate @x@ to the nearest integer greater than or equal to @x@.+ | Truncate+ -- ^ Approximate @x@ to the nearest integer betwen @0@ and @x@, inclusive.+ deriving (Eq, Ord, Show, Read, GHC.Generic)+++approximate :: Approximation -> Rational -> Integer+{-# INLINE approximate #-}+approximate = \case+ Round -> round+ Floor -> floor+ Ceiling -> ceiling+ Truncate -> truncate++-- | Approximate a 'Dense' value @x@ to the nearest value fully representable a+-- given @scale@.+--+-- If the given 'Dense' doesn't fit entirely in the @scale@, then a non-zero+-- 'Dense' reminder is returned alongside the 'Discrete' approximation.+--+-- Proof that 'discreteFromDense' doesn't lose money:+--+-- @+-- x == case 'discreteFromDense' a x of+-- (y, z) -> 'denseFromDiscrete' y + z+-- @+discreteFromDense+ :: forall currency scale+ . GoodScale scale+ => Approximation+ -- ^ Approximation to use if necesary in order to fit the 'Dense' amount in+ -- the requested @scale@.+ -> Dense currency+ -> (Discrete' currency scale, Dense currency)+discreteFromDense a = \c0 ->+ let !r0 = toRational c0 :: Rational+ !r1 = scale (Proxy :: Proxy scale)+ !i2 = approximate a (r0 * r1) :: Integer+ !r2 = fromInteger i2 / r1 :: Rational+ !d2 = Discrete i2+ !rest = Dense (r0 - r2)+ in (d2, rest)+{-# INLINABLE discreteFromDense #-}++--------------------------------------------------------------------------------++-- | @'Scale' currency unit@ is an 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 becacuse they are not exact+-- multiples of a dollar.+--+-- If there exists a canonical smallest @unit@ that can fully represent the+-- currency in all its denominations, then an instance @'Scale' currency+-- currency@ exists.+--+-- @+-- type instance 'Scale' \"USD\" \"USD\" = 'Scale' \"USD\" \"cent\"+-- @+--+-- For some monetary values, such as precious metals, there is no smallest+-- representable unit, since you can repeatedly split the precious metal many+-- times before it stops being a precious metal. Nevertheless, 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' \"XAU\" \"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 with a denominator different than 1 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)++-- | 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." )++-- | 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 ('GHC.Real.infinity' and+-- 'GHC.Real.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 of a currrency @scale@.+--+-- For example, the 'Scale' for @\"USD\"@ in @\"cent\"@s is @100/1@.+--+-- @+-- > 'scale' ('Proxy' :: 'Proxy' ('Scale' \"USD\" \"cent\"))+-- 100 '%' 1+-- @+--+-- @+-- > 'scale' (x :: 'Discrete' \"USD\" \"cent\")+-- 100 '%' 1+-- @+--+-- The returned 'Rational' is statically guaranteed to be a positive number.+scale :: forall proxy scale. GoodScale scale => proxy scale -> Rational -- ^+scale = \_ -> natVal (Proxy :: Proxy (Fst scale)) %+ natVal (Proxy :: Proxy (Snd scale))+{-# INLINE 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)+++-- | Composition of 'ExchangeRate's multiplies exchange rates together:+--+-- @+-- 'exchangeRateToRational' x * 'exchangeRateToRational' y == 'exchangeRateToRational' (x . y)+-- @+--+-- Identity:+--+-- @+-- x == x . id == id . x+-- @+--+-- Associativity:+--+-- @+-- x . y . z == x . (y . z) == (x . y) . z+-- @+--+-- Conmutativity (provided the types allow for composition):+--+-- @+-- x . y == y . x+-- @+--+-- Reciprocal:+--+-- @+-- 1 == 'exchangeRateToRational' (x . 'exchangeRateRecip' x)+-- @+instance Category ExchangeRate where+ id = ExchangeRate 1+ {-# INLINE id #-}+ ExchangeRate a . ExchangeRate b = ExchangeRate (a * b)+ {-# INLINE (.) #-}++-- |+-- @+-- > 'show' ('exchangeRate' (5 '%' 7) :: 'Maybe' ('ExchangeRate' \"USD\" \"JPY\"))@+-- Just \"ExchangeRate \\\"USD\\\" \\\"JPY\\\" 5%7\"+-- @+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 guaranteed to be a positive number.+exchangeRateToRational :: ExchangeRate src dst -> Rational+exchangeRateToRational = \(ExchangeRate r0) -> r0+{-# INLINE exchangeRateToRational #-}++-- | Safely construct an 'ExchangeRate' from a *positive* 'Rational' number.+exchangeRate :: Rational -> Maybe (ExchangeRate src dst)+exchangeRate = \r ->+ if denominator r /= 0 && r > 0+ then Just (ExchangeRate r)+ else Nothing+{-# INLINE exchangeRate #-}++-- | Reciprocal 'ExchangeRate'.+--+-- This function retuns the reciprocal or multiplicative inverse of the given+-- 'ExchangeRate', leading to the following identity law:+--+-- @+-- 'exchangeRateRecip' . 'exchangeRateRecip' == 'id'+-- @+--+-- Note: If 'ExchangeRate' had a 'Fractional' instance, then 'exchangeRateRecip'+-- would be the implementation of 'recip'.+exchangeRateRecip :: ExchangeRate a b -> ExchangeRate b a+exchangeRateRecip = \(ExchangeRate x) ->+ ExchangeRate (1 / x) -- 'exchangeRate' guarantees that @x@ isn't zero.+{-# INLINE exchangeRateRecip #-}++-- | Apply the 'ExchangeRate' to the given @'Dense' src@ monetary value.+--+-- Identity law:+--+-- @+-- 'exchange' ('exchangeRateRecip' 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)+{-# INLINE 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+{-# INLINE someDenseCurrency #-}++-- | Currency unit amount.+someDenseAmount :: SomeDense -> Rational+someDenseAmount = _someDenseAmount+{-# INLINE 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 ->+ if (denominator r /= 0)+ then Just (SomeDense c r)+ else Nothing+{-# 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+{-# INLINE 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 ->+ if (someDenseCurrency dr == symbolVal (Proxy :: Proxy currency))+ then Just (Dense (someDenseAmount dr))+ else Nothing+{-# 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+{-# INLINE someDiscreteCurrency #-}++-- | Positive, non-zero.+someDiscreteScale :: SomeDiscrete -> Rational+someDiscreteScale = _someDiscreteScale+{-# INLINE someDiscreteScale #-}++-- | Amount of currency unit.+someDiscreteAmount :: SomeDiscrete -> Integer+someDiscreteAmount = _someDiscreteAmount+{-# INLINE 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 ->+ if (denominator r /= 0) && (r > 0)+ then Just (SomeDiscrete c r a)+ else Nothing+{-# 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+{-# INLINE someExchangeRateSrcCurrency #-}++-- | Destination currency name.+someExchangeRateDstCurrency :: SomeExchangeRate -> String+someExchangeRateDstCurrency = _someExchangeRateDstCurrency+{-# INLINE someExchangeRateDstCurrency #-}++-- | Exchange rate. Positive, non-zero.+someExchangeRateRate :: SomeExchangeRate -> Rational+someExchangeRateRate = _someExchangeRateRate+{-# INLINE 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 ->+ if (denominator r /= 0) && (r > 0)+ then Just (SomeExchangeRate src dst r)+ else Nothing+{-# 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++--------------------------------------------------------------------------------+-- vector-space instances++#ifdef HAS_vector_space+instance AG.AdditiveGroup (Dense currency) where+ zeroV = Dense AG.zeroV+ {-# INLINE zeroV #-}+ Dense a ^+^ Dense b = Dense $! (a AG.^+^ b)+ {-# INLINE (^+^) #-}+ negateV (Dense a) = Dense $! (AG.negateV a)+ {-# INLINE negateV #-}+ Dense a ^-^ Dense b = Dense $! (a AG.^-^ b)+ {-# INLINE (^-^) #-}++-- | __WARNING__ a scalar with a zero denominator will cause 'VS.*^' to crash.+instance VS.VectorSpace (Dense currency) where+ type Scalar (Dense currency) = Rational+ s *^ Dense a =+ if denominator s /= 0+ then Dense $! s VS.*^ a+ else error "(*^)': malformed Rational given (denominator is zero)."+ {-# INLINE (*^) #-}++instance GoodScale scale => AG.AdditiveGroup (Discrete' currency scale) where+ zeroV = Discrete AG.zeroV+ {-# INLINE zeroV #-}+ Discrete a ^+^ Discrete b = Discrete $! (a AG.^+^ b)+ {-# INLINE (^+^) #-}+ negateV (Discrete a) = Discrete $! (AG.negateV a)+ {-# INLINE negateV #-}+ Discrete a ^-^ Discrete b = Discrete $! (a AG.^-^ b)+ {-# INLINE (^-^) #-}++instance GoodScale scale => VS.VectorSpace (Discrete' currency scale) where+ type Scalar (Discrete' currency scale) = Integer+ s *^ Discrete a = Discrete $! (s VS.*^ a)+ {-# INLINE (*^) #-}+#endif++--------------------------------------------------------------------------------+-- Extra instances: hashable+#ifdef HAS_hashable+instance Hashable Approximation+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 Approximation+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 @'dense'' (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 @'discrete' 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) :: '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 @'dense' (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 = [ (T.pack "c", T.pack c)+ , (T.pack "n", T.pack (show (numerator r)))+ , (T.pack "d", T.pack (show (denominator r))) ]+ Right e = Xmlbf.element (T.pack "money-dense") (fromList as) []+ in [e]++-- | Compatible with 'Dense'.+instance Xmlbf.FromXml SomeDense where+ fromXml = Xmlbf.pElement (T.pack "money-dense") $ do+ c <- T.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 @'discrete' 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 = [ (T.pack "c", T.pack c)+ , (T.pack "n", T.pack (show (numerator r)))+ , (T.pack "d", T.pack (show (denominator r)))+ , (T.pack "a", T.pack (show a)) ]+ Right e = Xmlbf.element (T.pack "money-discrete") (fromList as) []+ in [e]++-- | Compatible with 'Discrete''+instance Xmlbf.FromXml SomeDiscrete where+ fromXml = Xmlbf.pElement (T.pack "money-discrete") $ do+ c <- T.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) :: '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 = [ (T.pack "src", T.pack src)+ , (T.pack "dst", T.pack dst)+ , (T.pack "n", T.pack (show (numerator r)))+ , (T.pack "d", T.pack (show (denominator r))) ]+ Right e = Xmlbf.element (T.pack "exchange-rate") (fromList as) []+ in [e]++-- | Compatible with 'ExchangeRate'+instance Xmlbf.FromXml SomeExchangeRate where+ fromXml = Xmlbf.pElement (T.pack "exchange-rate") $ do+ src <- T.unpack <$> Xmlbf.pAttr "src"+ dst <- T.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++--------------------------------------------------------------------------------+-- Decimal rendering++-- | Render a 'Dense' monetary amount as a decimal number in a potentially lossy+-- manner.+--+-- @+-- > 'denseToDecimal' 'Round' 'True' ('Just' \',\') \'.\' 2+-- ('Proxy' :: 'Proxy' ('Scale' \"USD\" \"dollar\"))+-- ('dense'' (123456 '%' 100) :: 'Dense' \"USD\")+-- Just \"+1,234.56\"+-- @+--+-- @+-- > 'denseToDecimal' 'Round' 'True' ('Just' \',\') \'.\' 2+-- ('Proxy' :: 'Proxy' ('Scale' \"USD\" \"cent\"))+-- ('dense'' (123456 '%' 100) :: 'Dense' \"USD\")+-- Just \"+123,456.00\"+-- @+--+-- This function returns 'Nothing' if it is not possible to reliably render the+-- decimal string due to a bad choice of separators. That is, if the separators+-- are digits or equal among themselves, this function returns 'Nothing'.+denseToDecimal+ :: GoodScale scale+ => Approximation+ -- ^ Approximation to use if necesary in order to fit the 'Dense' amount in+ -- as many decimal numbers as requested.+ -> Bool+ -- ^ Whether to render a leading @\'+\'@ sign in case the amount is positive.+ -> Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @1,234.56789@).+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @1,234.56789@)+ -> Word8+ -- ^ Number of decimal numbers to render, if any.+ -> Proxy scale+ -- ^ Scale used by the integer part of the decimal number. For example, a when+ -- rendering render @'dense'' (123 '%' 100) :: 'Dense' "USD"@ as a decimal+ -- number with three decimal places, a scale of @1@ (i.e. @'Scale' \"USD\"+ -- \"dollar\"@) would render @1@ as the integer part and @230@ as the+ -- fractional part, whereas a scale of @100@ (i.e., @'Scale' \"USD\"+ -- \"cent\"@) would render @123@ as the integer part and @000@ as the+ -- fractional part.+ -> Dense currency+ -- ^ The dense monetary amount to render.+ -> Maybe String+ -- ^ Returns 'Nothing' is the given separators are not acceptable (i.e., they+ -- are digits, or they are equal).+{-# INLINABLE denseToDecimal #-}+denseToDecimal a plus ytsep dsep fdigs0 ps = \(Dense r0) ->+ rationalToDecimal a plus ytsep dsep fdigs0 (scale ps * r0)++-- | Render a 'ExchangeRate' as a decimal number in a potentially lossy manner.+--+-- @+-- > 'exchangeRateToDecimal' 'Round' 'True' ('Just' \',\') \'.\' 2+-- '=<<' ('exchangeRate' (123456 '%' 100) :: 'Maybe' ('ExchangeRate' \"USD\" \"EUR\"))+-- Just \"1,234.56\"+-- @+--+-- This function returns 'Nothing' if it is not possible to reliably render the+-- decimal string due to a bad choice of separators. That is, if the separators+-- are digits or equal among themselves, this function returns 'Nothing'.+exchangeRateToDecimal+ :: Approximation+ -- ^ Approximation to use if necesary in order to fit the 'Dense' amount in+ -- as many decimal numbers as requested.+ -> Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @1,234.56789@).+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @1,234.56789@)+ -> Word8+ -- ^ Number of decimal numbers to render, if any.+ -> ExchangeRate src dst+ -- ^ The 'ExchangeRate' to render.+ -> Maybe String+ -- ^ Returns 'Nothing' is the given separators are not acceptable (i.e., they+ -- are digits, or they are equal).+{-# INLINABLE exchangeRateToDecimal #-}+exchangeRateToDecimal a ytsep dsep fdigs0 = \(ExchangeRate r0) ->+ rationalToDecimal a False ytsep dsep fdigs0 r0++-- | Render a 'Rational' number as a decimal approximation.+--+-- This function returns 'Nothing' if it is not possible to reliably render the+-- decimal string due to a bad choice of separators. That is, if the separators+-- are digits or equal among themselves, this function returns 'Nothing'.+rationalToDecimal+ :: Approximation+ -- ^ Approximation to use if necesary in order to fit the 'Dense' amount in+ -- as many decimal numbers as requested.+ -> Bool+ -- ^ Whether to render a leading @\'+\'@ sign in case the amount is positive.+ -> Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @1,234.56789@).+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @1,234.56789@)+ -> Word8+ -- ^ Number of decimal numbers to render, if any.+ -> Rational+ -- ^ The dense monetary amount to render.+ -> Maybe String+ -- ^ Returns 'Nothing' is the given separators are not acceptable (i.e., they+ -- are digits, or they are equal).+{-# INLINABLE rationalToDecimal #-}+rationalToDecimal a plus ytsep dsep fdigs0 = \r0 -> do+ for_ ytsep $ \tsep ->+ guard (tsep /= dsep && not (Char.isDigit tsep))+ guard (not (Char.isDigit dsep))+ -- this string-fu is not particularly efficient.+ let parts = approximate a (r0 * (10 ^ fdigs0)) :: Integer+ ipart = fromInteger (abs parts) `div` (10 ^ fdigs0) :: Natural+ ftext | ipart == 0 = show (abs parts) :: String+ | otherwise = drop (length (show ipart)) (show (abs parts))+ itext = maybe (show ipart) (renderThousands ipart) ytsep :: String+ fpad0 = List.replicate (fromIntegral fdigs0 - length ftext) '0' :: String+ Just $ mconcat+ [ if | parts < 0 -> "-"+ | plus && parts > 0 -> "+"+ | otherwise -> ""+ , itext+ , if | fdigs0 > 0 -> dsep : ftext <> fpad0+ | otherwise -> ""+ ]+++-- | Render a 'Natural' number with thousand markers.+--+-- @+-- > 'renderThousands' 12045 \',\'+-- \"12,045\"+-- @+renderThousands :: Natural -> Char -> String+{-# INLINABLE renderThousands #-}+renderThousands n0+ | n0 < 1000 = \_ -> show n0+ | otherwise = \c -> List.foldl' (flip mappend) mempty (List.unfoldr (f c) n0)+ where f :: Char -> Natural -> Maybe (String, Natural)+ f c = \x -> case divMod x 1000 of+ (0, 0) -> Nothing+ (0, z) -> Just (show z, 0)+ (y, z) | z < 10 -> Just (c:'0':'0':show z, y)+ | z < 100 -> Just (c:'0':show z, y)+ | otherwise -> Just (c:show z, y)++--------------------------------------------------------------------------------+-- Decimal parsing++-- | Parses a decimal representation of a 'Dense'.+--+-- Leading @\'-\'@ and @\'+\'@ characters are considered.+denseFromDecimal+ :: Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @-1,234.56789@).+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @-1,234.56789@)+ -> String+ -- ^ The raw string containing the decimal representation (e.g.,+ -- @"-1,234.56789"@).+ -> Maybe (Dense currency)+denseFromDecimal yst sf = fmap Dense . rationalFromDecimal yst sf++-- | Parses a decimal representation of a 'Discrete'.+--+-- Leading @\'-\'@ and @\'+\'@ characters are considered.+--+-- Notice that parsing will fail unless the entire precision of the decimal+-- number can be represented in the desired @scale@.+discreteFromDecimal+ :: GoodScale scale+ => Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @-1,234.56789@).+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @-1,234.56789@)+ -> String+ -- ^ The raw string containing the decimal representation (e.g.,+ -- @"-1,234.56789"@).+ -> Maybe (Discrete' currency scale)+discreteFromDecimal yst sf = \s -> do+ dns <- denseFromDecimal yst sf s+ case discreteFromDense Truncate dns of+ (x, 0) -> Just x+ _ -> Nothing -- We fail for decimals that don't fit exactly in our scale.++-- | Parses a decimal representation of an 'ExchangeRate'.+exchangeRateFromDecimal+ :: Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @1,234.56789@).+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @1,234.56789@)+ -> String+ -- ^ The raw string containing the decimal representation (e.g.,+ -- @"1,234.56789"@).+ -> Maybe (ExchangeRate src dst)+exchangeRateFromDecimal yst sf = \case+ ('-':_) -> Nothing+ str -> exchangeRate =<< rationalFromDecimal yst sf str++rationalFromDecimal+ :: Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @-1,234.56789@).+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @-1,234.56789@)+ -> String+ -- ^ The raw string containing the decimal representation (e.g.,+ -- @"-1,234.56789"@).+ -> Maybe Rational+rationalFromDecimal yst sf = \s ->+ case ReadP.readP_to_S (rationalFromDecimalP yst sf) s of+ [(x,"")] -> Just x+ _ -> Nothing++-- TODO limit number of digits parsed to prevent DoS+rationalFromDecimalP+ :: Maybe Char+ -- ^ Thousands separator for the integer part, if any (i.e., the @\',\'@ in+ -- @-1,234.56789@).+ --+ -- The separator can't be a digit or control character. If it is, then parsing+ -- will always fail.+ -> Char+ -- ^ Decimal separator (i.e., the @\'.\'@ in @-1,234.56789@).+ --+ -- The separator can't be a digit or control character. If it is, then parsing+ -- will always fail.+ -> ReadP.ReadP Rational+rationalFromDecimalP ytsep dsep = do+ for_ ytsep $ \tsep ->+ guard (tsep /= dsep && not (Char.isDigit tsep))+ guard (not (Char.isDigit dsep))+ sig :: Rational -> Rational <-+ (ReadP.char '-' $> negate) <|>+ (ReadP.char '+' $> id) <|>+ (pure id)+ ipart :: String <- case ytsep of+ Nothing -> ReadP.munch1 Char.isDigit+ Just tsep -> mappend+ <$> (ReadP.count 3 (ReadP.satisfy Char.isDigit) <|>+ ReadP.count 2 (ReadP.satisfy Char.isDigit) <|>+ ReadP.count 1 (ReadP.satisfy Char.isDigit))+ <*> (fmap concat $ ReadP.many+ (ReadP.char tsep *> ReadP.count 3 (ReadP.satisfy Char.isDigit)))+ yfpart :: Maybe String <-+ (ReadP.char dsep *> fmap Just (ReadP.munch1 Char.isDigit) <* ReadP.eof) <|>+ (ReadP.eof $> Nothing)+ pure $! sig $ case yfpart of+ Nothing -> fromInteger (read ipart)+ Just fpart -> read (ipart <> fpart) % (10 ^ length fpart)
test/Main.hs view
@@ -5,17 +5,22 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE TypeApplications #-} module Main where import Control.Category (Category((.), id)) import qualified Data.ByteString.Lazy as BSL-import Data.Maybe (catMaybes, isJust, isNothing)+import qualified Data.Char as Char+import Data.Maybe (catMaybes, isJust, isNothing, fromJust) import Data.Proxy (Proxy(Proxy))-import Data.Ratio (numerator, denominator)+import Data.Ratio ((%), numerator, denominator)+import Data.Word (Word8) import GHC.TypeLits (Nat, Symbol, KnownSymbol, symbolVal) import Prelude hiding ((.), id) import qualified Test.Tasty as Tasty+import Test.Tasty.HUnit ((@?=))+import qualified Test.Tasty.HUnit as HU import qualified Test.Tasty.Runners as Tasty import Test.Tasty.QuickCheck ((===), (==>), (.&&.)) import qualified Test.Tasty.QuickCheck as QC@@ -40,12 +45,19 @@ import qualified Data.Store as Store #endif +#ifdef HAS_vector_space+import qualified Data.AdditiveGroup as AG+import qualified Data.VectorSpace as VS+#endif+ #ifdef HAS_xmlbf import qualified Xmlbf import qualified Data.Text as Text #endif import qualified Money+import qualified Money.Internal as Money+ (rationalFromDecimal, rationalToDecimal) -------------------------------------------------------------------------------- @@ -59,46 +71,73 @@ arbitrary = do let md = Money.mkSomeDiscrete <$> QC.arbitrary <*> QC.arbitrary <*> QC.arbitrary- Just x <- QC.suchThat md isJust- pure x+ fromJust <$> QC.suchThat md isJust shrink = \x -> Money.withSomeDiscrete x (map Money.toSomeDiscrete . QC.shrink) instance QC.Arbitrary (Money.Dense currency) where arbitrary = do- Just x <- QC.suchThat (Money.dense <$> QC.arbitrary) isJust- pure x- shrink = catMaybes . fmap Money.dense . QC.shrink . toRational+ let myd = fmap Money.dense QC.arbitrary+ fromJust <$> QC.suchThat myd isJust+ shrink = catMaybes . map Money.dense . QC.shrink . toRational instance QC.Arbitrary Money.SomeDense where arbitrary = do let md = Money.mkSomeDense <$> QC.arbitrary <*> QC.arbitrary- Just x <- QC.suchThat md isJust- pure x+ fromJust <$> QC.suchThat md isJust shrink = \x -> Money.withSomeDense x (map Money.toSomeDense . QC.shrink) instance QC.Arbitrary (Money.ExchangeRate src dst) where arbitrary = do- Just x <- QC.suchThat (fmap Money.exchangeRate QC.arbitrary) isJust- pure x- shrink =- catMaybes . fmap Money.exchangeRate . QC.shrink . Money.fromExchangeRate+ let myxr = fmap Money.exchangeRate QC.arbitrary+ fromJust <$> QC.suchThat myxr isJust+ shrink = catMaybes . map Money.exchangeRate+ . QC.shrink . Money.exchangeRateToRational instance QC.Arbitrary Money.SomeExchangeRate where arbitrary = do let md = Money.mkSomeExchangeRate <$> QC.arbitrary <*> QC.arbitrary <*> QC.arbitrary- Just x <- QC.suchThat md isJust- pure x+ fromJust <$> QC.suchThat md isJust shrink = \x -> Money.withSomeExchangeRate x (map Money.toSomeExchangeRate . QC.shrink) +instance QC.Arbitrary Money.Approximation where+ arbitrary = QC.oneof [ pure Money.Round+ , pure Money.Floor+ , pure Money.Ceiling+ , pure Money.Truncate ]++-- | Generates a valid 'Money.rationalToDecimal' result. Returns the thousand+-- and decimal separators as welland decimal separators as well.+genDecimal :: QC.Gen (String, Maybe Char, Char)+genDecimal = do+ aprox :: Money.Approximation <- QC.arbitrary+ plus :: Bool <- QC.arbitrary+ digs :: Word8 <- QC.arbitrary+ r :: Rational <- (%) <$> QC.arbitrary <*> QC.suchThat QC.arbitrary (/= 0)+ (yts, ds) <- genDecimalSeps+ Just dec <- pure (Money.rationalToDecimal aprox plus yts ds digs r)+ pure (dec, yts, ds)++-- | Generates valid separators for decimal representations (see genDecimal).+genDecimalSeps :: QC.Gen (Maybe Char, Char)+genDecimalSeps = do+ let msep = QC.suchThat QC.arbitrary (not . Char.isDigit)+ ds :: Char <- msep+ yts :: Maybe Char <- genMaybe (QC.suchThat msep (/= ds))+ pure (yts, ds)+++genMaybe :: QC.Gen a -> QC.Gen (Maybe a)+genMaybe m = QC.oneof [pure Nothing, fmap Just m]+ -------------------------------------------------------------------------------- main :: IO () main = Tasty.defaultMainWithIngredients [ Tasty.consoleTestReporter , Tasty.listingTests- ] tests+ ] (Tasty.localOption (QC.QuickCheckTests 500) tests) tests :: Tasty.TestTree tests =@@ -106,6 +145,9 @@ [ testCurrencies , testCurrencyUnits , testExchange+ , testRationalToDecimal+ , testRationalFromDecimal+ , testDiscreteFromDecimal ] testCurrencies :: Tasty.TestTree@@ -127,11 +169,274 @@ , testDiscrete (Proxy :: Proxy "USD") (Proxy :: Proxy "cent") , testDiscrete (Proxy :: Proxy "USD") (Proxy :: Proxy "dollar") , testDiscrete (Proxy :: Proxy "VUV") (Proxy :: Proxy "vatu")- , testDiscrete (Proxy :: Proxy "XAU") (Proxy :: Proxy "micrograin")- , testDiscrete (Proxy :: Proxy "XAU") (Proxy :: Proxy "milligrain")+ , testDiscrete (Proxy :: Proxy "XAU") (Proxy :: Proxy "gram") , testDiscrete (Proxy :: Proxy "XAU") (Proxy :: Proxy "grain") ] ++testRationalToDecimal :: Tasty.TestTree+testRationalToDecimal =+ Tasty.testGroup "rationalToDecimal"+ [ HU.testCase "Round: r1" $ do+ render Money.Round r1 @?=+ [ "1023004567.90" -- 0+ , "1,023,004,567.90" -- 1+ , "+1023004567.90" -- 2+ , "+1,023,004,567.90" -- 3+ , "1023004568" -- 8+ , "1,023,004,568" -- 9+ , "+1023004568" -- 10+ , "+1,023,004,568" -- 11+ ]+ , HU.testCase "Round: negate r1" $ do+ render Money.Round (negate r1) @?=+ [ "-1023004567.90" -- 0+ , "-1,023,004,567.90" -- 1+ , "-1023004567.90" -- 2+ , "-1,023,004,567.90" -- 3+ , "-1023004568" -- 8+ , "-1,023,004,568" -- 9+ , "-1023004568" -- 10+ , "-1,023,004,568" -- 11+ ]+ , HU.testCase "Round: r2" $ do+ render Money.Round r2 @?=+ [ "1.23" -- 0+ , "1.23" -- 1+ , "+1.23" -- 2+ , "+1.23" -- 3+ , "1" -- 8+ , "1" -- 9+ , "+1" -- 10+ , "+1" -- 11+ ]+ , HU.testCase "Round: negate r2" $ do+ render Money.Round (negate r2) @?=+ [ "-1.23" -- 0+ , "-1.23" -- 1+ , "-1.23" -- 2+ , "-1.23" -- 3+ , "-1" -- 8+ , "-1" -- 9+ , "-1" -- 10+ , "-1" -- 11+ ]+ , HU.testCase "Round: r3" $ do+ render Money.Round r3 @?=+ [ "0.34" -- 0+ , "0.34" -- 1+ , "+0.34" -- 2+ , "+0.34" -- 3+ , "0" -- 8+ , "0" -- 9+ , "0" -- 10+ , "0" -- 11+ ]+ , HU.testCase "Round: negate r3" $ do+ render Money.Round (negate r3) @?=+ [ "-0.34" -- 0+ , "-0.34" -- 1+ , "-0.34" -- 2+ , "-0.34" -- 3+ , "0" -- 8+ , "0" -- 9+ , "0" -- 10+ , "0" -- 11+ ]+ , HU.testCase "Floor: r1" $ do+ render Money.Floor r1 @?=+ [ "1023004567.89" -- 0+ , "1,023,004,567.89" -- 1+ , "+1023004567.89" -- 2+ , "+1,023,004,567.89" -- 3+ , "1023004567" -- 8+ , "1,023,004,567" -- 9+ , "+1023004567" -- 10+ , "+1,023,004,567" -- 11+ ]+ , HU.testCase "Floor: negate r1" $ do+ render Money.Floor (negate r1) @?=+ [ "-1023004567.90" -- 0+ , "-1,023,004,567.90" -- 1+ , "-1023004567.90" -- 2+ , "-1,023,004,567.90" -- 3+ , "-1023004568" -- 8+ , "-1,023,004,568" -- 9+ , "-1023004568" -- 10+ , "-1,023,004,568" -- 11+ ]+ , HU.testCase "Floor: r2" $ do+ render Money.Floor r2 @?=+ [ "1.23" -- 0+ , "1.23" -- 1+ , "+1.23" -- 2+ , "+1.23" -- 3+ , "1" -- 8+ , "1" -- 9+ , "+1" -- 10+ , "+1" -- 11+ ]+ , HU.testCase "Floor: negate r2" $ do+ render Money.Floor (negate r2) @?=+ [ "-1.23" -- 0+ , "-1.23" -- 1+ , "-1.23" -- 2+ , "-1.23" -- 3+ , "-2" -- 8+ , "-2" -- 9+ , "-2" -- 10+ , "-2" -- 11+ ]+ , HU.testCase "Floor: r3" $ do+ render Money.Floor r3 @?=+ [ "0.34" -- 0+ , "0.34" -- 1+ , "+0.34" -- 2+ , "+0.34" -- 3+ , "0" -- 8+ , "0" -- 9+ , "0" -- 10+ , "0" -- 11+ ]+ , HU.testCase "Floor: negate r3" $ do+ render Money.Floor (negate r3) @?=+ [ "-0.35" -- 0+ , "-0.35" -- 1+ , "-0.35" -- 2+ , "-0.35" -- 3+ , "-1" -- 8+ , "-1" -- 9+ , "-1" -- 10+ , "-1" -- 11+ ]+ , HU.testCase "Ceiling: r1" $ do+ render Money.Ceiling r1 @?=+ [ "1023004567.90" -- 0+ , "1,023,004,567.90" -- 1+ , "+1023004567.90" -- 2+ , "+1,023,004,567.90" -- 3+ , "1023004568" -- 8+ , "1,023,004,568" -- 9+ , "+1023004568" -- 10+ , "+1,023,004,568" -- 11+ ]+ , HU.testCase "Ceiling: negate r1" $ do+ render Money.Ceiling (negate r1) @?=+ [ "-1023004567.89" -- 0+ , "-1,023,004,567.89" -- 1+ , "-1023004567.89" -- 2+ , "-1,023,004,567.89" -- 3+ , "-1023004567" -- 8+ , "-1,023,004,567" -- 9+ , "-1023004567" -- 10+ , "-1,023,004,567" -- 11+ ]+ , HU.testCase "Ceiling: r2" $ do+ render Money.Ceiling r2 @?=+ [ "1.23" -- 0+ , "1.23" -- 1+ , "+1.23" -- 2+ , "+1.23" -- 3+ , "2" -- 8+ , "2" -- 9+ , "+2" -- 10+ , "+2" -- 11+ ]+ , HU.testCase "Ceiling: negate r2" $ do+ render Money.Ceiling (negate r2) @?=+ [ "-1.23" -- 0+ , "-1.23" -- 1+ , "-1.23" -- 2+ , "-1.23" -- 3+ , "-1" -- 8+ , "-1" -- 9+ , "-1" -- 10+ , "-1" -- 11+ ]+ , HU.testCase "Ceiling: r3" $ do+ render Money.Ceiling r3 @?=+ [ "0.35" -- 0+ , "0.35" -- 1+ , "+0.35" -- 2+ , "+0.35" -- 3+ , "1" -- 8+ , "1" -- 9+ , "+1" -- 10+ , "+1" -- 11+ ]+ , HU.testCase "Ceiling: negate r3" $ do+ render Money.Ceiling (negate r3) @?=+ [ "-0.34" -- 0+ , "-0.34" -- 1+ , "-0.34" -- 2+ , "-0.34" -- 3+ , "0" -- 8+ , "0" -- 9+ , "0" -- 10+ , "0" -- 11+ ]++ , HU.testCase "Truncate: r1" $ do+ render Money.Truncate r1 @?= render Money.Floor r1++ , HU.testCase "Truncate: negate r1" $ do+ render Money.Truncate (negate r1) @?= render Money.Ceiling (negate r1)++ , HU.testCase "Truncate: r2" $ do+ render Money.Truncate r2 @?= render Money.Floor r2++ , HU.testCase "Truncate: negate r2" $ do+ render Money.Truncate (negate r2) @?= render Money.Ceiling (negate r2)++ , HU.testCase "Truncate: r3" $ do+ render Money.Truncate r3 @?= render Money.Floor r3++ , HU.testCase "Truncate: negate r3" $ do+ render Money.Truncate (negate r3) @?= render Money.Ceiling (negate r3)+ ]+ where+ r1 :: Rational = 1023004567895 % 1000+ r2 :: Rational = 123 % 100+ r3 :: Rational = 345 % 1000++ render :: Money.Approximation -> Rational -> [String]+ render a r =+ [ fromJust $ Money.rationalToDecimal a False Nothing '.' 2 r -- 0+ , fromJust $ Money.rationalToDecimal a False (Just ',') '.' 2 r -- 1+ , fromJust $ Money.rationalToDecimal a True Nothing '.' 2 r -- 2+ , fromJust $ Money.rationalToDecimal a True (Just ',') '.' 2 r -- 3+ , fromJust $ Money.rationalToDecimal a False Nothing '.' 0 r -- 8+ , fromJust $ Money.rationalToDecimal a False (Just ',') '.' 0 r -- 9+ , fromJust $ Money.rationalToDecimal a True Nothing '.' 0 r -- 10+ , fromJust $ Money.rationalToDecimal a True (Just ',') '.' 0 r -- 11+ ]++testRationalFromDecimal :: Tasty.TestTree+testRationalFromDecimal =+ Tasty.testGroup "rationalFromDecimal"+ [ QC.testProperty "Unsupported separators" $+ let mbadsep :: QC.Gen Char = QC.suchThat QC.arbitrary Char.isDigit+ mgoodsep :: QC.Gen Char = QC.suchThat QC.arbitrary (not . Char.isDigit)+ in QC.forAll ((,,) <$> mbadsep <*> mbadsep <*> mgoodsep) $+ \(s1 :: Char, s2 :: Char, s3 :: Char) ->+ let f = Money.rationalFromDecimal+ in (f Nothing s1 (error "untouched") === Nothing) .&&.+ (f (Just s1) s2 (error "untouched") === Nothing) .&&.+ (f (Just s1) s1 (error "untouched") === Nothing) .&&.+ (f (Just s3) s3 (error "untouched") === Nothing)++ , QC.testProperty "Lossy roundtrip" $+ -- We check that the roundtrip results in a close amount with a fractional+ -- difference of up to one.+ let gen = (,) <$> genDecimalSeps <*> QC.arbitrary+ in QC.forAll gen $ \( (yst :: Maybe Char, sd :: Char)+ , (r :: Rational, plus :: Bool, digs :: Word8,+ aprox :: Money.Approximation) ) ->+ let Just dec = Money.rationalToDecimal aprox plus yst sd digs r+ Just r' = Money.rationalFromDecimal yst sd dec+ in 1 > abs (abs r - abs r')+ ]+ testDense :: forall currency . KnownSymbol currency@@ -142,16 +447,20 @@ [ QC.testProperty "read . show == id" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> x === read (show x)+ , QC.testProperty "read . show . Just == Just " $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> Just x === read (show (Just x))+ , QC.testProperty "fromSomeDense . someDense == Just" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) -> 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@@ -159,6 +468,45 @@ (show x, dr, Money.toSomeDense (x + 1)) === (show x', Money.toSomeDense x', Money.toSomeDense (x' + 1)) + , QC.testProperty "denseCurrency" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ Money.denseCurrency x === symbolVal pc++ , QC.testProperty "denseToDecimal: Same as rationalToDecimal" $+ let gen = (,) <$> genDecimalSeps <*> QC.arbitrary+ in QC.forAll gen $ \( (yst :: Maybe Char, sd :: Char)+ , (dns :: Money.Dense currency, plus :: Bool,+ digs :: Word8, aprox :: Money.Approximation) ) ->+ let ydnsd1 = Money.denseToDecimal aprox plus yst sd digs (Proxy :: Proxy '(1,1)) dns+ ydnsd100 = Money.denseToDecimal aprox plus yst sd digs (Proxy :: Proxy '(100,1)) dns+ yrd1 = Money.rationalToDecimal aprox plus yst sd digs (toRational dns)+ yrd100 = Money.rationalToDecimal aprox plus yst sd digs (toRational dns * 100)+ in (ydnsd1 === yrd1) .&&. (ydnsd100 === yrd100)++ , QC.testProperty "denseFromDecimal: Same as rationalFromDecimal" $+ QC.forAll genDecimal $ \(dec :: String, yts :: Maybe Char, ds :: Char) ->+ let Just r = Money.rationalFromDecimal yts ds dec+ Just dns = Money.denseFromDecimal yts ds dec+ in r === toRational (dns :: Money.Dense currency)++#ifdef HAS_vector_space+ , HU.testCase "AdditiveGroup: zeroV" $+ (AG.zeroV :: Money.Dense currency) @?= Money.dense' (0%1)+ , QC.testProperty "AdditiveGroup: negateV" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->+ AG.negateV x === negate x+ , QC.testProperty "AdditiveGroup: ^+^" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency, y) ->+ x AG.^+^ y === x + y+ , QC.testProperty "AdditiveGroup: ^-^" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency, y) ->+ x AG.^-^ y === x - y+ , QC.testProperty "VectorSpace: *^" $+ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency, y) ->+ (toRational x VS.*^ y === x * y) .&&.+ (toRational y VS.*^ x === x * y)+#endif+ #ifdef HAS_aeson , QC.testProperty "Aeson encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Dense currency) ->@@ -291,6 +639,7 @@ , testExchangeRate (Proxy :: Proxy "XAU") (Proxy :: Proxy "XAU") ] + testDiscrete :: forall (currency :: Symbol) (unit :: Symbol) . ( Money.GoodScale (Money.Scale currency unit)@@ -327,6 +676,28 @@ === (show x', Money.toSomeDiscrete x', Money.toSomeDiscrete (x' + 1)) ) :: QC.Property + , QC.testProperty "discreteCurrency" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ Money.discreteCurrency x === symbolVal pc++#ifdef HAS_vector_space+ , HU.testCase "AdditiveGroup: zeroV" $+ (AG.zeroV :: Money.Discrete currency unit) @?= Money.discrete 0+ , QC.testProperty "AdditiveGroup: negateV" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->+ AG.negateV x === negate x+ , QC.testProperty "AdditiveGroup: ^+^" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit, y) ->+ x AG.^+^ y === x + y+ , QC.testProperty "AdditiveGroup: ^-^" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit, y) ->+ x AG.^-^ y === x - y+ , QC.testProperty "VectorSpace: *^" $+ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit, y) ->+ (toInteger x VS.*^ y === x * y) .&&.+ (toInteger y VS.*^ x === x * y)+#endif+ #ifdef HAS_aeson , QC.testProperty "Aeson encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.Discrete currency unit) ->@@ -456,14 +827,14 @@ xr === xr . id , QC.testProperty "Category: composition with inverse" $ QC.forAll QC.arbitrary $ \(xr1 :: Money.ExchangeRate src dst) ->- (1 === Money.fromExchangeRate (xr1 . Money.flipExchangeRate xr1)) .&&.- (1 === Money.fromExchangeRate (Money.flipExchangeRate xr1 . xr1))+ (1 === Money.exchangeRateToRational (xr1 . Money.exchangeRateRecip xr1)) .&&.+ (1 === Money.exchangeRateToRational (Money.exchangeRateRecip xr1 . xr1)) , QC.testProperty "Category: composition with other" $ QC.forAll QC.arbitrary $ \(xr1 :: Money.ExchangeRate src dst, xr2 :: Money.ExchangeRate dst src) ->- let a = Money.fromExchangeRate xr1 * Money.fromExchangeRate xr2- in (a === Money.fromExchangeRate (xr1 . xr2)) .&&.- (a === Money.fromExchangeRate (xr2 . xr1))+ let a = Money.exchangeRateToRational xr1 * Money.exchangeRateToRational xr2+ in (a === Money.exchangeRateToRational (xr1 . xr2)) .&&.+ (a === Money.exchangeRateToRational (xr2 . xr1)) , QC.testProperty "read . show == id" $ QC.forAll QC.arbitrary $ \(xr :: Money.ExchangeRate src dst) ->@@ -473,25 +844,25 @@ 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- in (Money.fromExchangeRate xr /= Money.fromExchangeRate xr')- ==> (xr === Money.flipExchangeRate xr')+ let xr' = Money.exchangeRateRecip xr+ in (Money.exchangeRateToRational xr /= Money.exchangeRateToRational xr')+ ==> (xr === Money.exchangeRateRecip xr') , QC.testProperty "exchange (flipExchangeRate x) . exchange x == id" $ QC.forAll QC.arbitrary $ \( c0 :: Money.Dense src , xr :: Money.ExchangeRate src dst- ) -> c0 === Money.exchange (Money.flipExchangeRate xr)+ ) -> c0 === Money.exchange (Money.exchangeRateRecip xr) (Money.exchange xr c0)- , QC.testProperty "x == 1 ===> exchange x == id" $+ , QC.testProperty "x == 1 ==> exchange x == id" $ QC.forAll QC.arbitrary $ \( c0 :: Money.Dense src ) -> let Just xr = Money.exchangeRate 1 in toRational c0 === toRational (Money.exchange xr c0)- , QC.testProperty "x /= 1 ===> exchange x /= id" $+ , QC.testProperty "x /= 1 ==> exchange x /= id" $ QC.forAll QC.arbitrary $ \( c0 :: Money.Dense src , xr :: Money.ExchangeRate src dst- ) -> (Money.fromExchangeRate xr /= 1)+ ) -> (Money.exchangeRateToRational xr /= 1 && toRational c0 /= 0) ==> (toRational c0 /= toRational (Money.exchange xr c0)) , QC.testProperty "fromSomeExchangeRate . someExchangeRate == Just" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->@@ -508,6 +879,23 @@ in Money.withSomeExchangeRate dr $ \x' -> (show x, dr) === (show x', Money.toSomeExchangeRate x') + , QC.testProperty "exchangeRateToDecimal: Same as rationalToDecimal" $+ let gen = (,) <$> genDecimalSeps <*> QC.arbitrary+ in QC.forAll gen $ \( (yst :: Maybe Char, sd :: Char)+ , (xr :: Money.ExchangeRate src dst, digs :: Word8,+ aprox :: Money.Approximation ) ) ->+ let xrd = Money.exchangeRateToDecimal aprox yst sd digs xr+ rd = Money.rationalToDecimal aprox False yst sd digs+ (Money.exchangeRateToRational xr)+ in xrd === rd++ , QC.testProperty "exchangeRateFromDecimal: Same as rationalFromDecimal" $+ QC.forAll genDecimal $ \(dec :: String, yts :: Maybe Char, ds :: Char) ->+ let Just r = Money.rationalFromDecimal yts ds dec+ yxr = Money.exchangeRateFromDecimal yts ds dec+ :: Maybe (Money.ExchangeRate src dst)+ in (r > 0) ==> (Just r === fmap Money.exchangeRateToRational yxr)+ #ifdef HAS_aeson , QC.testProperty "Aeson encoding roundtrip" $ QC.forAll QC.arbitrary $ \(x :: Money.ExchangeRate src dst) ->@@ -620,6 +1008,59 @@ #endif ] ++testDiscreteFromDecimal :: Tasty.TestTree+testDiscreteFromDecimal =+ Tasty.testGroup "discreteFromDecimal"+ [ HU.testCase "Too large" $ do+ Money.discreteFromDecimal Nothing '.' "0.053"+ @?= (Nothing :: Maybe (Money.Discrete "USD" "cent"))+ Money.discreteFromDecimal (Just ',') '.' "0.253"+ @?= (Nothing :: Maybe (Money.Discrete "USD" "cent"))++ , HU.testCase "USD cent, small, zero" $ do+ let dis = 0 :: Money.Discrete "USD" "cent"+ f = Money.discreteFromDecimal+ f Nothing '.' "0" @?= Just dis+ f Nothing '.' "+0" @?= Just dis+ f Nothing '.' "-0" @?= Just dis+ f (Just ',') '.' "0" @?= Just dis+ f (Just ',') '.' "+0" @?= Just dis+ f (Just ',') '.' "-0" @?= Just dis++ , HU.testCase "USD cent, small, positive" $ do+ let dis = 25 :: Money.Discrete "USD" "cent"+ f = Money.discreteFromDecimal+ f Nothing '.' "0.25" @?= Just dis+ f Nothing '.' "+0.25" @?= Just dis+ f (Just ',') '.' "0.25" @?= Just dis+ f (Just ',') '.' "+0.25" @?= Just dis++ , HU.testCase "USD cent, small, negative" $ do+ let dis = -25 :: Money.Discrete "USD" "cent"+ f = Money.discreteFromDecimal+ f Nothing '.' "-0.25" @?= Just dis+ f Nothing '.' "-0.25" @?= Just dis+ f (Just ',') '.' "-0.25" @?= Just dis+ f (Just ',') '.' "-0.25" @?= Just dis++ , HU.testCase "USD cent, big, positive" $ do+ let dis = 102300456789 :: Money.Discrete "USD" "cent"+ f = Money.discreteFromDecimal+ f Nothing '.' "1023004567.89" @?= Just dis+ f Nothing '.' "+1023004567.89" @?= Just dis+ f (Just ',') '.' "1,023,004,567.89" @?= Just dis+ f (Just ',') '.' "+1,023,004,567.89" @?= Just dis++ , HU.testCase "USD cent, big, negative" $ do+ let dis = -102300456789 :: Money.Discrete "USD" "cent"+ f = Money.discreteFromDecimal+ f Nothing '.' "-1023004567.89" @?= Just dis+ f Nothing '.' "-1023004567.89" @?= Just dis+ f (Just ',') '.' "-1,023,004,567.89" @?= Just dis+ f (Just ',') '.' "-1,023,004,567.89" @?= Just dis+ ]+ testRounding :: forall (currency :: Symbol) (unit :: Symbol) . (Money.GoodScale (Money.Scale currency unit), KnownSymbol currency)@@ -628,26 +1069,26 @@ -> Tasty.TestTree testRounding _ _ = Tasty.testGroup "Rounding"- [ QC.testProperty "floor" $ QC.forAll QC.arbitrary (g Money.floor)- , 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)+ [ QC.testProperty "floor" $ QC.forAll QC.arbitrary (g (Money.discreteFromDense Money.Floor))+ , QC.testProperty "ceiling" $ QC.forAll QC.arbitrary (g (Money.discreteFromDense Money.Ceiling))+ , QC.testProperty "round" $ QC.forAll QC.arbitrary (g (Money.discreteFromDense Money.Round))+ , QC.testProperty "truncate" $ QC.forAll QC.arbitrary (g (Money.discreteFromDense Money.Truncate))+ , QC.testProperty "floor no reminder" $ QC.forAll QC.arbitrary (h (Money.discreteFromDense Money.Floor))+ , QC.testProperty "ceiling no reminder" $ QC.forAll QC.arbitrary (h (Money.discreteFromDense Money.Ceiling))+ , QC.testProperty "round no reminder" $ QC.forAll QC.arbitrary (h (Money.discreteFromDense Money.Round))+ , QC.testProperty "truncate no reminder" $ QC.forAll QC.arbitrary (h (Money.discreteFromDense Money.Truncate)) ] where 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+ g f = \x -> x === case f x of (y, z) -> Money.denseFromDiscrete 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+ h f = \x -> (Money.denseFromDiscrete x) === case f (Money.denseFromDiscrete x) of+ (y, 0) -> Money.denseFromDiscrete y (_, _) -> error "testRounding.h: unexpected" hush :: Either a b -> Maybe b