{-# LANGUAGE Safe, DeriveDataTypeable #-}

-- | Simple decimal arithmetic.
--
-- 'Deka' provides a decimal arithmetic type. Using this module, the
-- results are never inexact.  Computations will throw exceptions
-- rather than returning an inexact result.  That way, you know that
-- any result you have is exactly correct.
--
-- On 64-bit platforms, you are limited to:
--
-- * a coefficient of ((2 * 10 ^ 17) - 1) digits long
--
-- * a maximum exponent of ((1 * 10 ^ 18) - 1)
--
-- * a minimum exponent of -((1 * 10 ^ 18) + 1)
--
-- On 32-bit platforms, you are limited to:
--
-- * a coefficient of 8.5 * 10 ^ 8 digits long
--
-- * a maximum exponent of 4.25 * 10 ^ 8
--
-- * a minimum exponent of -4.25 * 10 ^ 8
--
-- If you exceed these limits, your computation will throw an
-- exception.
--
-- 'Deka' represents only finite values.  There are no infinities or
-- not-a-number values allowed.
--
-- For more control over your arithmetic, see "Deka.Dec", but
-- for many routine uses this module is sufficient and is more
-- succinct because, unlike 'Dec', 'Deka' is a member of the 'Num'
-- typeclass.

module Deka
  ( Deka
  , unDeka
  , DekaT(..)
  , integralToDeka
  , strToDeka
  , quadToDeka
  , DekaError(..)
  ) where

import Control.Exception
import Data.Typeable
import Deka.Dec hiding (compare)
import qualified Deka.Dec as D
import qualified Data.ByteString.Char8 as BS8

-- | Thrown by arithmetic functions in the Num class, as this is the
-- only way to indicate errors.
data DekaError
  = Flagged Flags
  -- ^ A computation set flags.  This will happen if, for example,
  -- you calculate a result that is out of range.
  deriving (Show, Typeable)

instance Exception DekaError

-- | Deka wraps a 'Dec'.  Only finite 'Dec' may become a 'Deka';
-- no infinities or NaN values are allowed.
--
-- 'Deka' is a member of 'Num', making it easy to use for
-- elementary arithmetic.  Any time you perform arithmetic, the
-- results are always exact.  The arithmetic functions will throw
-- exceptions rather than give you an inexact result.
--
-- 'Deka' is not a member 'Fractional' because it is generally
-- impossible to perform division without getting inexact results,
-- and 'Deka' never holds inexact results.
newtype Deka = Deka { unDeka :: Dec }
  deriving Show

eval :: Ctx a -> a
eval c
  | fl == emptyFlags = r
  | otherwise = throw . Flagged $ fl
  where
    (r, fl) = runCtxStatus c

-- | Eq compares by value.  For instance, @3.5 == 3.500@.
instance Eq Deka where
  Deka x == Deka y = case eval k of
    EQ -> True
    _ -> False
    where
      k = do
        d <- D.compare x y
        let f | isZero d = EQ
              | isPositive d = GT
              | otherwise = LT
        return f

-- | Ord compares by value.  For instance, @compare 3.5 3.500 ==
-- EQ@.
instance Ord Deka where
  compare (Deka x) (Deka y) = eval $ do
    d <- D.compare x y
    let f | isZero d = EQ
          | isPositive d = GT
          | otherwise = LT
    return f

-- | Many of the 'Num' functions will throw 'DekaError' if their
-- arguments are out of range or if they produce results that are
-- out of range or inexact.  For functions that don't throw, you can
-- use 'integralToDeka' rather than 'fromInteger', or you can use
-- "Deka.Dec" instead of 'Deka'.
instance Num Deka where
  Deka x + Deka y = Deka . eval $ D.add x y
  Deka x - Deka y = Deka . eval $ D.subtract x y
  Deka x * Deka y = Deka . eval $ D.multiply x y
  negate = Deka . eval . D.minus . unDeka
  abs = Deka . eval . D.abs . unDeka
  signum (Deka x)
    | f isZero = fromInteger 0
    | f isNegative = fromInteger (-1)
    | otherwise = fromInteger 1
    where
      f g = g x
  fromInteger = Deka . eval . fromByteString . BS8.pack . show

-- | Decimals with a total ordering.
newtype DekaT = DekaT { unDekaT :: Deka }
  deriving Show

-- | Eq compares by a total ordering.
instance Eq DekaT where
  DekaT (Deka x) == DekaT (Deka y)
    | r == EQ = True
    | otherwise = False
    where
      r = compareTotal x y

-- | Ord compares by a total ordering.
instance Ord DekaT where
  compare (DekaT (Deka x)) (DekaT (Deka y)) = compareTotal x y


-- | Convert any integral to a 'Deka'.  Returns 'Nothing' if the
-- integer is too big to fit into a 'Deka'.
integralToDeka :: (Integral a, Show a) => a -> Maybe Deka
integralToDeka i
  | fl == emptyFlags = Just . Deka $ d
  | otherwise = Nothing
  where
    (d, fl) = runCtxStatus . fromByteString . BS8.pack . show $ i

-- | Convert a string to a Deka.  You can use ordinary numeric
-- strings, such as @3.25@, or exponential notation, like @325E-2@.
-- More information on your choices is at:
--
-- <http://speleotrove.com/decimal/daconvs.html#reftonum>
--
-- You cannot use strings that represent an NaN or an infinity.  If
-- you do that, or use an otherwise invalid string, this function
-- returns 'Nothing'.
strToDeka :: String -> Maybe Deka
strToDeka s
  | not (emptyFlags == fl) = Nothing
  | not (isFinite r) = Nothing
  | otherwise = Just (Deka r)
  where
    (r, fl) = runCtxStatus . fromByteString . BS8.pack $ s

-- | Change a 'Dec' to a 'Deka'.  Only succeeds for finite 'Dec'.
quadToDeka :: Dec -> Maybe Deka
quadToDeka d
  | isFinite d = Just $ Deka d
  | otherwise = Nothing