packages feed

HasBigDecimal-0.2.0.0: src/Data/BigDecimal.hs

{-# OPTIONS_GHC -fno-warn-type-defaults #-} -- avoids  warnings for things like x^2
-- -- | This module defines the type 'BigDecimal' which provides a representation of arbitrary precision decimal numbers.
--     'BigDecimal' is a native Haskell implementation based on arbitrary sized 'Integer' values.
--     The implementation was inspired by Java BigDecimals.
--
--      BigDecimal instantiates the typeclasses 'Num', 'Fractional' and 'Real'. It is thus possible to use all common
--          operators like '+', '-', '*', '/', '^' on them.
--
--       Here are a few examples from an interactive GHCI session:
--
--      >  λ> a = BigDecimal 144 2
--      >  λ> toString a
--      >  1.44
--      >  λ> b = sqrt a
--      >  λ> b
--      >  1.2
--      >  λ> b * b
--      >  1.44
--      >  λ> b * b * b
--      >  1.728
--      >  λ> b^2
--      >  1.44
--      >  λ> c = read "123.4567890" :: BigDecimal
--      >  λ> c
--      >  123.4567890
--      >  λ> a / c
--      >  0.01166400010614240096589584878965222398584
--      >  λ> roundBD it (halfUp 10)
--      >  0.0116640001
--      >  λ> divide (a, c) $ halfUp 20
--      >  0.01166400010614240097
module Data.BigDecimal
  ( BigDecimal (..),
    RoundingMode (..),
    RoundingAdvice,
    precision,
    trim,
    nf,
    divide,
    roundBD,
    fromRatio,
    halfUp,
    fromString,
    fromStringMaybe,
    fromNatural,
    matchScales,
  )
where

import           Data.List   (elemIndex)
import           Data.Maybe  (fromJust, fromMaybe)
import           GHC.Natural (Natural)
import           GHC.Real    (Ratio ((:%)))
import           Text.Read   (readMaybe)

-- | BigDecimal is represented by an unscaled Integer value and a Natural that defines the scale
--   E.g.: (BigDecimal 1234 2) represents the decimal value 12.34.
data BigDecimal = BigDecimal
  { -- | the unscaled Integer value
    value :: Integer,
    -- | the scale (i.e. the number of digits after the decimal point)
    scale :: Natural
  }

-- | A RoundingAdvice is interpreted by divisions and rounding operations to specify the expected loss of precision and the rounding behaviour.
--   RoundingAdvice is a pair of a 'RoundingMode' and a target precision of type 'Maybe' 'Natural'. The precision defines the number of digits after the decimal point.
--   If 'Nothing' is given as precision all decimal digits are to be preserved, that is precision is not limited.
type RoundingAdvice = (RoundingMode, Maybe Natural)

-- | RoundingMode defines how to handle loss of precision in divisions or explicit rounding.
data RoundingMode
  = -- | Rounding mode to round away from zero.
    UP
  | -- | Rounding mode to round towards zero.
    DOWN
  | -- | Rounding mode to round towards positive infinity.
    CEILING
  | -- | Rounding mode to round towards negative infinity.
    FLOOR
  | -- | Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round up.
    HALF_UP
  | -- | Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round down.
    HALF_DOWN
  | -- | Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case, round towards the even neighbor.
    HALF_EVEN
  | -- | Rounding mode to assert that the requested operation has an exact result, hence no rounding is applied.
    PRECISE

instance Show BigDecimal where
  show = toString

instance Read BigDecimal where
  readsPrec _ str =
    case fromStringMaybe str of
      Nothing   -> []
      (Just bd) -> [(bd, "")]

instance Num BigDecimal where
  a + b = plus (a, b)
  a * b = mul (a, b)
  abs (BigDecimal v s) = BigDecimal (abs v) s
  signum (BigDecimal v _) = BigDecimal (signum v) 0
  fromInteger i = BigDecimal i 0
  negate (BigDecimal v s) = BigDecimal (- v) s

instance Eq BigDecimal where
  a == b =
    let (BigDecimal valA _, BigDecimal valB _) = matchScales (a, b)
     in valA == valB

instance Fractional BigDecimal where
  -- default division rounds up and does not limit precision
  a / b = nf $ divide (matchScales (a, b)) (HALF_UP, Nothing)
  fromRational ratio = fromRatio ratio (HALF_UP, Nothing)

-- | creates a BigDecimal from a 'Rational' value. 'RoundingAdvice' defines precision and rounding mode.
fromRatio :: Rational -> RoundingAdvice -> BigDecimal
fromRatio (x :% y) = nf . divide (fromInteger x, fromInteger y)

instance Real BigDecimal where
  toRational (BigDecimal val scl) = toRational val * 10 ^^ (- fromNatural scl)

instance Ord BigDecimal where
  compare a b =
    let (BigDecimal valA _, BigDecimal valB _) = matchScales (a, b)
     in compare valA valB

-- | add two BigDecimals
plus :: (BigDecimal, BigDecimal) -> BigDecimal
plus (a@(BigDecimal valA scaleA), b@(BigDecimal valB scaleB))
  | scaleA == scaleB = BigDecimal (valA + valB) scaleA
  | otherwise = plus $ matchScales (a, b)

-- | multiply two BigDecimals
mul :: (BigDecimal, BigDecimal) -> BigDecimal
mul (BigDecimal valA scaleA, BigDecimal valB scaleB) = BigDecimal (valA * valB) (scaleA + scaleB)

-- | divide two BigDecimals and applies the 'RoundingAdvice' (i.e. a tuple of 'RoundingMode' and the specified precision) for rounding.
divide ::
  -- |  the tuple of dividend and divisor. I.e. (dividend, divisor)
  (BigDecimal, BigDecimal) ->
  -- | 'RoundingAdvice' (i.e. a tuple of 'RoundingMode' and the specified precision) defines the rounding behaviour.
  --   if 'Nothing' if given as precision the maximum possible precision is used.
  RoundingAdvice ->
  -- | the resulting BigDecimal
  BigDecimal
divide (a, b) (rMode, prefScale) =
  let (BigDecimal numA _, BigDecimal numB _) = matchScales (a, b)
      maxPrecision = fromMaybe (precision a + round (fromIntegral (precision b) * 10 / 3)) prefScale :: Natural
   in trim maxPrecision (BigDecimal (divUsing rMode (numA * (10 :: Integer) ^ maxPrecision) numB) maxPrecision)

-- | divide two correctly scaled Integers and apply the RoundingMode
divUsing :: RoundingMode -> Integer -> Integer -> Integer
divUsing rounding a b =
  let (quotient, remainder) = quotRem a b
      delta = (10 * abs remainder `div` abs b) - 5
   in case rounding of
        PRECISE -> if remainder == 0 then quotient else error "non-terminating decimal expansion"
        UP -> if abs remainder > 0 then quotient + signum quotient else quotient
        CEILING -> if abs remainder > 0 && quotient >= 0 then quotient + 1 else quotient
        HALF_UP -> if delta >= 0 then quotient + signum quotient else quotient
        HALF_DOWN -> if delta <= 0 then quotient else quotient + signum quotient
        DOWN -> quotient
        FLOOR -> if quotient >= 0 then quotient else quotient - 1
        HALF_EVEN
          | delta > 0 -> quotient + signum quotient
          | delta == 0 && odd quotient -> quotient + signum quotient
          | otherwise -> quotient

-- | round a BigDecimal according to a 'RoundingAdvice' to 'n' digits applying the 'RoundingMode' 'rMode'
roundBD :: BigDecimal -> RoundingAdvice -> BigDecimal
roundBD bd@(BigDecimal val scl) (rMode, Just n)
  | n < 0 || n >= scl = bd
  | otherwise = BigDecimal (divUsing rMode val (10 ^ (scl - n))) n
roundBD bd _ = bd

-- | match the scales of a tuple of BigDecimals
matchScales :: (BigDecimal, BigDecimal) -> (BigDecimal, BigDecimal)
matchScales (a@(BigDecimal integerA scaleA), b@(BigDecimal integerB scaleB))
  | scaleA < scaleB = (BigDecimal (integerA * 10 ^ (scaleB - scaleA)) scaleB, b)
  | scaleA > scaleB = (a, BigDecimal (integerB * 10 ^ (scaleA - scaleB)) scaleA)
  | otherwise = (a, b)

-- | returns the number of digits of a BigDecimal.
precision :: BigDecimal -> Natural
-- see benchmark/Main.hs
precision = fromInteger . toInteger . length . show . abs . value

-- | removes trailing zeros from a BigDecimals intValue by decreasing the scale
trim :: Natural -> BigDecimal -> BigDecimal
trim prefScale bd@(BigDecimal val scl) =
  let (v, r) = quotRem val 10
   in if r == 0 && 0 <= prefScale && prefScale < scl
        then trim prefScale $ BigDecimal v (scl - 1)
        else bd

-- | computes the normal form of a BigDecimal
nf :: BigDecimal -> BigDecimal
nf = trim 0

-- | read a BigDecimal from a human readable decimal notation.
--   e.g. @ fromString "3.14" @ yields 'BigDecimal 314 2'
fromString :: String -> BigDecimal
fromString = fromJust . fromStringMaybe

-- | read a BigDecimal from a human readable decimal notation.
--   e.g. @ fromString "3.14" @ yields 'BigDecimal 314 2'
fromStringMaybe :: String -> Maybe BigDecimal
fromStringMaybe s =
  let maybeIndex = elemIndex '.' s
      maybeIntValue = readMaybe (filter (/= '.') s)
   in do
        intValue <- maybeIntValue
        case maybeIndex of
          Nothing -> pure $ BigDecimal intValue 0
          Just i -> pure $ BigDecimal intValue (fromIntegral (length s - i - 1))

-- | returns a readable String representation of a BigDecimal
--   e.g. @ toString (BigDecimal 314 2) @ yields "3.14"
toString :: BigDecimal -> String
toString (BigDecimal intValue scl) =
  let s = show $ abs intValue
      filled =
        if fromNatural scl >= length s
          then replicate (1 + fromNatural scl - length s) '0' ++ s
          else s
      splitPos = length filled - fromNatural scl
      (ints, decimals) = splitAt splitPos filled
      sign = if intValue < 0 then "-" else ""
   in sign ++ if not (null decimals) then ints ++ "." ++ decimals else ints

-- | construct a 'RoundingAdvice' for rounding 'HALF_UP' with 'scl' decimal digits
halfUp :: Natural -> RoundingAdvice
halfUp scl = (HALF_UP, Just scl)

-- | convert a Natural to any numeric type a
fromNatural :: Num a => Natural -> a
fromNatural = fromInteger . toInteger