{-# LANGUAGE DataKinds, PolyKinds, KindSignatures, ScopedTypeVariables, RankNTypes, ImplicitParams, ExistentialQuantification, OverloadedStrings #-} {-| Module : Data.Currency.Convert Description : Typesafe currency coversion using exchange rates from various sources. Copyright : (c) Tuomas Laakkonen 2016 License : BSD3 Maintainer : pigworts2@gmail.com Stability : Experimental Portability : Non-portable (GHC extensions) This module allows values of currency to be converted from one currency to another using exchange rates from various sources. The basic operation for this module is as follows: First, get a conversion function (with @'getDefaultConverter'@ or @'getConverter'@): >>> Converter convert <- getDefaultConverter Then, construct some value of currency using the convenience functions, and convert it using the convert function you just got: >>> convert (usd 100) :: EUR 90.0 eur The result type of @convert@ defines what currency the value will be converted into. -} module Data.Currency.Convert ( Converter(..), getConverter, getDefaultConverter, unsafeCoerceCurrency, Currency(), RateProvider(..), RateDict(..), defaultProvider, backupProvider, dictProvider, localProvider, fixerIOProvider, (<|-|>), aud, bgn, brl, cad, chf, cny, czk, dkk, gbp, hkd, hrk, huf, idr, ils, inr, jpy, krw, mxn, myr, nok, nzd, php, pln, ron, rub, sek, sgd, thb, try, usd, zar, eur, AUD, BGN, BRL, CAD, CHF, CNY, CZK, DKK, GBP, HKD, HRK, HUF, IDR, ILS, INR, JPY, KRW, MXN, MYR, NOK, NZD, PHP, PLN, RON, RUB, SEK, SGD, THB, TRY, USD, ZAR, EUR ) where import GHC.TypeLits import Network.HTTP.Simple hiding (Proxy(..)) import qualified Data.HashMap.Strict as HS import qualified Data.Text as T import Data.Aeson import Data.Proxy import Control.Applicative import qualified Control.Exception as E {-| @'Currency'@ is a wrapper around @Double@ that has a phantom symbol type, allowing different currencies to be distinguished using type level literals. To extract the value from a @'Currency'@ value, use @toRational@ from @Real@. -} data Currency (s :: Symbol) = Currency Double deriving (Eq, Ord) instance KnownSymbol s => Show (Currency s) where show (Currency val) = symbolVal (Proxy :: Proxy s) ++ " " ++ show val instance Read (Currency a) where readsPrec _ s = let (currency : value : rest) = words s in let n :: Proxy s n = case someSymbolVal currency of SomeSymbol n' -> n in [((Currency (read value) :: Currency s), unwords rest)] instance Num (Currency a) where Currency a + Currency b = Currency $ a + b Currency a * Currency b = Currency $ a * b negate (Currency a) = Currency $ negate a signum (Currency a) = Currency $ signum a fromInteger i = Currency $ fromInteger i abs (Currency a) = Currency $ abs a instance Fractional (Currency a) where Currency a / Currency b = Currency $ a / b fromRational r = Currency $ fromRational r instance Real (Currency a) where toRational (Currency a) = toRational a instance RealFrac (Currency a) where properFraction (Currency a) = let (a', b') = properFraction a in (a', Currency b') -- | The Australian Dollar type AUD = Currency "aud" -- | The Australian Dollar aud :: Double -> AUD aud = Currency -- | The Bulgarian Lev type BGN = Currency "bgn" -- | The Bulgarian Lev bgn :: Double -> BGN bgn = Currency -- | The Brazillian Real type BRL = Currency "brl" -- | The Brazillian Real brl :: Double -> BRL brl = Currency -- | The Canadian Dollar type CAD = Currency "cad" -- | The Canadian Dollar cad :: Double -> CAD cad = Currency -- | The Swiss Franc type CHF = Currency "chf" -- | The Swiss Franc chf :: Double -> CHF chf = Currency -- | The Yuan type CNY = Currency "cny" -- | The Yuan cny :: Double -> CNY cny = Currency -- | The Czech Karuna type CZK = Currency "czk" -- | The Czech Karuna czk :: Double -> CZK czk = Currency -- | The Danish Krone type DKK = Currency "dkk" -- | The Danish Krone dkk :: Double -> DKK dkk = Currency -- | The Pound Sterling type GBP = Currency "gbp" -- | The Pound Sterling gbp :: Double -> GBP gbp = Currency -- | The Hong Kong Dollar type HKD = Currency "hkd" -- | The Hong Kong Dollar hkd :: Double -> HKD hkd = Currency -- | The Croatian Kuna type HRK = Currency "hrk" -- | The Croatian Kuna hrk :: Double -> HRK hrk = Currency -- | The Forint type HUF = Currency "huf" -- | The Forint huf :: Double -> HUF huf = Currency -- | The Rupiah type IDR = Currency "idr" -- | The Rupiah idr :: Double -> IDR idr = Currency -- | The New Israeli Sheqel type ILS = Currency "ils" -- | The New Israeli Sheqel ils :: Double -> ILS ils = Currency -- | The Indian Rupee type INR = Currency "inr" -- | The Indian Rupee inr :: Double -> INR inr = Currency -- | The Yen type JPY = Currency "jpy" -- | The Yen jpy :: Double -> JPY jpy = Currency -- | The Won type KRW = Currency "krw" -- | The Won krw :: Double -> KRW krw = Currency -- | The Mexican Peso type MXN = Currency "mxn" -- | The Mexican Peso mxn :: Double -> MXN mxn = Currency -- | The Malaysian Ringgit type MYR = Currency "myr" -- | The Malaysian Ringgit myr :: Double -> MYR myr = Currency -- | The Norwegian Krone type NOK = Currency "nok" -- | The Norwegian Krone nok :: Double -> NOK nok = Currency -- | The New Zealand Dollar type NZD = Currency "nzd" -- | The New Zealand Dollar nzd :: Double -> NZD nzd = Currency -- | The Phillipine Peso type PHP = Currency "php" -- | The Phillipine Peso php :: Double -> PHP php = Currency -- | The Zloty type PLN = Currency "pln" -- | The Zloty pln :: Double -> PLN pln = Currency -- | The Romanian Leu type RON = Currency "ron" -- | The Romanian Leu ron :: Double -> RON ron = Currency -- | The Russian Ruble type RUB = Currency "rub" -- | The Russian Ruble rub :: Double -> RUB rub = Currency -- | The Swedish Krona type SEK = Currency "sek" -- | The Swedish Krona sek :: Double -> SEK sek = Currency -- | The Singapore Dollar type SGD = Currency "sgd" -- | The Singapore Dollar sgd :: Double -> SGD sgd = Currency -- | The Thai Baht type THB = Currency "thb" -- | The Thai Baht thb :: Double -> THB thb = Currency -- | The Turkish Lira type TRY = Currency "try" -- | The Turkish Lira try :: Double -> TRY try = Currency -- | The United States of America Dollar type USD = Currency "usd" -- | The United States of America Dollar usd :: Double -> USD usd = Currency -- | The Rand type ZAR = Currency "zar" -- | The Rand zar :: Double -> ZAR zar = Currency -- | The Euro type EUR = Currency "eur" -- | The Euro eur :: Double -> EUR eur = Currency {-| @'RateProvider'@ is used by @'getConverter'@ to provide a conversion rate dictionary to the resulting conversion function. @'RateProvider'@ has two fields, the name (@:: String@), and the action (@:: IO RateDict@). The name is used in error reporting, and the action is used to provide conversion rates when needed. There are several prebuilt @'RateProvider'@s - @'fixerIOProvider'@, @'backupProvider'@, @'localProvider'@, @'dictProvider'@ and @'defaultProvider'@. -} data RateProvider = RateProvider String (IO RateDict) {-| @'RateDict'@ is the type provided by a @'RateProvider'@. It is a mapping from currency codes to currency-to-euro conversion rates. -} newtype RateDict = RateDict [(String, Double)] {-| @'(\<|-|\>)'@ combines two @'RateProvider'@s to create a third. This provider tries to return the first providers result, but if an exception is raised, it returns the second providers result. -} (<|-|>) :: RateProvider -> RateProvider -> RateProvider (RateProvider sa a) <|-|> (RateProvider sb b) = RateProvider (sa ++ " | " ++ sb) $ a `E.catch` (const b :: E.SomeException -> IO RateDict) infixr <|-|> newtype FixerIOResponse = FixerIOResponse [(String, Double)] instance FromJSON FixerIOResponse where parseJSON = withObject "response" $ \r -> do o <- r .: "rates" let os = HS.toList o os' <- flip mapM os $ \(a, b) -> do b' <- parseJSON b return (T.unpack $ T.toLower a, b') return $ FixerIOResponse $ os' ++ [("eur", 1)] {-| This @'RateProvider'@ uses the API to return exchange rates updated daily by the European Central Bank. -} fixerIOProvider :: RateProvider fixerIOProvider = RateProvider "fixer.io" $ do resp <- httpJSON request let FixerIOResponse dict = getResponseBody resp return $ RateDict dict where request = setRequestQueryString [("base", Just "eur")] "http://api.fixer.io/latest" {-| This @'RateProvider'@ takes a file path and returns a dictionary of rates from the file. The file must be in the form @ \ \ \ \ ... @ -} localProvider :: FilePath -> RateProvider localProvider path = RateProvider ("file " ++ path) $ do text <- readFile path let ls = map (\line -> let [a, b] = words line in (a, read b)) $ lines text return $ RateDict ls {-| This @'RateProvider'@ allows a dictionary to be provided. -} dictProvider :: String -> [(String, Double)] -> RateProvider dictProvider s = RateProvider (s ++ " dictionary") . return . RateDict {-| This @'RateProvider'@ has a hardcoded dictionary. -} backupProvider :: RateProvider backupProvider = dictProvider "backup" [("usd", 1.11), ("gbp", 0.83), ("eur", 1)] {-| This @'RateProvider'@ is used by @'getDefaultConverter'@ it first tries and then resorts to the backup dictionary. -} defaultProvider :: RateProvider defaultProvider = fixerIOProvider <|-|> backupProvider convert :: forall a b. (KnownSymbol a, KnownSymbol b, ?dict :: [(String, Double)], ?name :: String) => Currency a -> Currency b convert (Currency a) = let as = symbolVal (Proxy :: Proxy a) bs = symbolVal (Proxy :: Proxy b) in case lookup as ?dict of Just aRate -> let eurVal = a / aRate in case lookup bs ?dict of Just bRate -> Currency $ eurVal * bRate Nothing -> error $ "Unknown currency `" ++ bs ++ "` for provider `" ++ ?name ++ "`" Nothing -> error $ "Unknown currency `" ++ as ++ "` for provider `" ++ ?name ++ "`" {-| @'unsafeCoerceCurrency'@ can be used to convert between currencies directly without using exchange rates. Only use this if you know the resulting currency is supported by your @'RateProvider'@ -} unsafeCoerceCurrency :: Currency a -> Currency b unsafeCoerceCurrency (Currency a) = Currency a {-| @'Converter'@ is a newtype wrapper around the type of conversion functions to avoid @ImpredicativeTypes@ in @'getConverter'@ -} newtype Converter = Converter (forall a b. (KnownSymbol a, KnownSymbol b) => Currency a -> Currency b) {-| @'getConverter'@ takes a rate provider and returns a @'Converter'@ in the @IO@ monad. It is used like this: >>> Converter convert <- getConverter provider @convert@ can then be used to convert between currencies: >>> convert (usd 100) :: GBP 75.0 gbp -} getConverter :: RateProvider -> IO Converter getConverter (RateProvider name' prov) = do RateDict dict' <- prov return $ let ?dict = dict' ?name = name' in Converter $ convert {-| @'getDefaultConverter'@ is like @'getConverter'@ except no provider needs to be specified (@'defaultProvider'@ is used). -} getDefaultConverter :: IO Converter getDefaultConverter = getConverter defaultProvider