packages feed

HasBigDecimal-0.1.1: test/Data/BigDecimalSpec.hs

module Data.BigDecimalSpec
  (main, spec)
where

import           Control.Exception     (evaluate)
import           Data.BigDecimal
import           GHC.Real              (Ratio ((:%)))
import           Test.Hspec            hiding (it)
import           Data.TestUtils        (it)        -- I'm redefining it to use 1000 examples
import           Test.Hspec.QuickCheck (modifyMaxSize, modifyMaxSuccess)
import           Test.QuickCheck

-- `main` is here so that this module can be run from GHCi on its own.  It is
-- not needed for automatic spec discovery.
main :: IO ()
main = hspec spec

spec :: Spec
spec = do
  describe "toBD" $ do
    it "reads BigDecimals from strings" $
      fromString "-145.123" `shouldBe` BigDecimal (-145123) 3
    it "is inverse of toString" $
      property $ \bd -> (fromString . toString) bd === (bd :: BigDecimal)

  describe "toString" $ do
    it "converts BigDecimals to string" $
      toString (BigDecimal (-145123) 3) `shouldBe` "-145.123"
    it "adds leading 0s if required" $
      toString (BigDecimal (-14) 10) `shouldBe` "-0.0000000014"
    it "can handle integer values" $
      toString 10 `shouldBe` "10"
    it "is inverse of toBD" $
      property $ \bd -> (toString . fromString . toString) bd === toString (bd :: BigDecimal)

  describe "read" $ do
    it "reads BigDecimals from strings in constructor notation" $
      read "BigDecimal 76878 5" `shouldBe` BigDecimal 76878 5
    it "is inverse of show" $
      property $ \bd -> (read . show) bd === (bd :: BigDecimal)

  describe "show" $ do
    it "converts BigDecimals to strings in constructor notation" $
      show (BigDecimal 76878 5) `shouldBe` "BigDecimal 76878 5"
    it "is inverse of read" $
      property $ \bd -> (read . show) bd === (bd :: BigDecimal)

  describe "(+)" $ do
    it "adds two BigDecimals" $
      BigDecimal 73 1 + BigDecimal 270 2 `shouldBe` BigDecimal 1000 2
    modifyMaxSuccess (const 1000) $ it "has 0 as neutral element" $
      property $ \bd -> bd + 0 === (bd :: BigDecimal)
    it "adds x to (-x) yielding 0" $
      property $ \bd -> bd + (-bd) === (0 :: BigDecimal)
    it "uses the max scale of the summands" $
      property $ \ai as bi bs -> max as bs === getScale (BigDecimal ai as + BigDecimal bi bs)
    it "uses Integer addition when summands have same scale" $
      property $ \ai bi scale -> ai + bi === getValue (BigDecimal ai scale + BigDecimal bi scale)
    it "matches values when scaling" $
      property $ \ai bi scale -> getValue (BigDecimal ai scale + BigDecimal bi (scale+1)) === 10*ai + bi

  describe "(*)" $ do
    it "multiplies BigDecimals" $
      BigDecimal 12 1 * BigDecimal 12 2 `shouldBe` BigDecimal 144 3
    it "has 1 as neutral element" $
      property $ \bd -> bd * 1 === (bd :: BigDecimal)
    it "has 0 as zero element" $
      property $ \bd -> bd * 0 === (0 :: BigDecimal)
    it "Uses Integer multiplication" $
      property $ \ai as bi -> BigDecimal ai as * BigDecimal bi 0 === BigDecimal (ai*bi) as
    it "adds the scales of the multiplicands" $
      property $ \ai as bi bs -> BigDecimal ai as * BigDecimal bi bs === BigDecimal (ai*bi) (as+bs)

  describe "abs" $ do
    it "determines the absolute value of a BigDecimal" $
      abs (BigDecimal (-12) 4)  `shouldBe` BigDecimal 12 4
    it "is idempotent" $
      property $ \bd -> (abs . abs) bd === (abs bd :: BigDecimal)
    it "is based on abs for Integers" $
      property $ \ai as -> abs (BigDecimal ai as) === BigDecimal (abs ai) as
    it "negates for input < 0" $
      property $ \bd -> abs bd === if getValue bd < 0 then negate bd else bd

  describe "signum" $ do
    it "determines the signature a BigDecimal" $
      signum (BigDecimal (-12) 4)  `shouldBe` -1
    it "returns 1 if input > 0, zero if input == 0 and -1 if input < 0" $
      property $ \ai as -> signum (BigDecimal ai as) === if ai > 0 then 1 else if ai == 0 then 0 else -1
    it "is based on signum for Integers" $
      property $ \ai as -> signum (BigDecimal ai as) === BigDecimal (signum ai) 0

  describe "fromInteger" $ do
    it "constructs a BigDecimal from an Integer" $
      1234  `shouldBe` BigDecimal 1234 0
    it "works for any Integer" $
      property $ \i -> fromInteger i === BigDecimal i 0

  describe "negate" $ do
    it "negates a BigDecimal" $
      negate (BigDecimal 1234 1)  `shouldBe` -BigDecimal 1234 1
    it "works for any BigDecimal" $
      property $ \bd -> negate bd === (-bd :: BigDecimal)
    it "is the same as *(-1)" $
      property $ \bd -> negate bd === (-1 * bd :: BigDecimal)
    it "is its own inverse" $
      property $ \bd -> negate (negate bd) === (bd :: BigDecimal)

  describe "(/)" $ do
    it "divides two BigDecimals" $
      BigDecimal 16 1 / BigDecimal 4 1 `shouldBe` BigDecimal 4 0
    it "yields x for x/1 for any x" $
      property $ \x -> x/1 === (x :: BigDecimal)
    it "yields 1 for x/x any non-zero x" $
      property $ \x -> if x /= (0 :: BigDecimal) then x / x === 1 else 1===1
    it "throws an Arithmetic exception when dividing by 0" $
      property $ \bd -> evaluate (bd / 0 :: BigDecimal) `shouldThrow` anyArithException
    it "yields y for (x*y)/x for any nonzero x" $
      property $ \x y -> y === if x == (0 :: BigDecimal) then y else (x*y)/x
    it "rounds up if next decimal would be > 5" $
      6 / 9 `shouldBe` fromString "0.6667"
    it "rounds up if next decimal would be = 5" $
      5 / 9 `shouldBe` fromString "0.5556"
    it "rounds down if next decimal would be < 5" $
      4 / 9 `shouldBe` fromString "0.4444"

  describe "fromRational" $ do
    it "constructs a BigDecimal from a Ratio" $
      fromRational (1 :% 32) `shouldBe` 1 / BigDecimal 32 0
    it "works for any non-zero divisors" $
      property $ \x y -> if y == 0 then 1 ===1 else fromRational (x :% y) === BigDecimal x 0 / BigDecimal y 0

  describe "toRational" $ do
    it "converts a BigDecimal to a Ratio" $
      toRational (1 / BigDecimal 32 0) `shouldBe` (1 :% 32)
    it "is inverse to fromRational" $
      property $ \x -> (x::BigDecimal) === fromRational (toRational x)

  describe "divide +/+" $ do
    -- checking expresions >= 0
    it "divides BigDecimals applying RoundingMode and precision" $
      divide (2, 3) (HALF_UP, Just 9) `shouldBe` fromString "0.666666667"
    it "always rounds down when using DOWN" $
      divide (2, 3) (DOWN, Just 9) `shouldBe` fromString "0.666666666"
    it "always rounds down when using FLOOR" $
      divide (2, 3) (FLOOR, Just 9) `shouldBe` fromString "0.666666666"
    it "rounds up when using UP when there is a remainder" $
      divide (1, 9) (UP, Just 3) `shouldBe` fromString "0.112"
    it "does not round when there is no remainder when using UP" $
      divide (14, 100) (UP, Just 2) `shouldBe` fromString "0.14"
    it "rounds up when using UP when there is a remainder" $
      divide (1, 9) (CEILING, Just 3) `shouldBe` fromString "0.112"
    it "does not round when there is no remainder when using UP" $
      divide (14, 100) (CEILING, Just 2) `shouldBe` fromString "0.14"
    it "rounds down if next decimal would be <= 5 when using HALF_DOWN" $
      divide (5, 9) (HALF_DOWN, Just 4) `shouldBe` fromString "0.5555"
    it "rounds up if next decimal would be > 5 when using HALF_DOWN" $
      divide (2, 3) (HALF_DOWN, Just 4) `shouldBe` fromString "0.6667"
    it "rounds up if next decimal would be >= 5 when using HALF_UP" $
      divide (5, 9) (HALF_UP, Just 4) `shouldBe` fromString "0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (5, 9) (HALF_EVEN, Just 4) `shouldBe` fromString "0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (1, 8) (HALF_EVEN, Just 2) `shouldBe` fromString "0.12"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (15, 100) (HALF_EVEN, Just 1) `shouldBe` fromString "0.2"
    it "rounds up if next decimal would be > 5 when using HALF_EVEN" $
      divide (2, 3) (HALF_EVEN, Just 4) `shouldBe` fromString "0.6667"
    it "throws an exception when PRECISE is used and a non-terminating decimal expansion is detected" $
      evaluate (divide (5, 9) (PRECISE, Nothing)) `shouldThrow` anyException
    it "gives a precise value when using PRECISE and no max precision" $
      divide (1, 32) (PRECISE, Nothing) `shouldBe` fromString "0.03125"
    it "gives a precise value when using PRECISE and a sufficient precision" $
      divide (1, 32) (PRECISE, Just 5) `shouldBe` fromString "0.03125"
    it "gives a precise value when using PRECISE and a to small precision" $
      evaluate (divide (1, 32) (PRECISE, Just 4)) `shouldThrow` anyException

  describe "divide -/+" $ do
    -- checking dividend < 0
    it "divides BigDecimals applying RoundingMode and precision" $
      divide (-2, 3) (HALF_UP, Just 9) `shouldBe` fromString "-0.666666667"
    it "always rounds down when using DOWN" $
      divide (-2, 3) (DOWN, Just 9) `shouldBe` fromString "-0.666666666"
    it "always rounds towards -INF when using FLOOR" $
      divide (-2, 3) (FLOOR, Just 9) `shouldBe` fromString "-0.666666667"
    it "rounds up when using UP when there is a remainder" $
      divide (-1, 9) (UP, Just 3) `shouldBe` fromString "-0.112"
    it "does not round when there is no remainder when using UP" $
      divide (-14, 100) (UP, Just 2) `shouldBe` fromString "-0.14"
    it "rounds towards +INF when using CEILING when there is a remainder" $
      divide (-1, 9) (CEILING, Just 3) `shouldBe` fromString "-0.111"
    it "does not round when there is no remainder when using UP" $
      divide (-14, 100) (CEILING, Just 2) `shouldBe` fromString "-0.14"
    it "rounds down if next decimal would be <= 5 when using HALF_DOWN" $
      divide (-5, 9) (HALF_DOWN, Just 4) `shouldBe` fromString "-0.5555"
    it "rounds up if next decimal would be > 5 when using HALF_DOWN" $
      divide (-2, 3) (HALF_DOWN, Just 4) `shouldBe` fromString "-0.6667"
    it "rounds up if next decimal would be >= 5 when using HALF_UP" $
      divide (-5, 9) (HALF_UP, Just 4) `shouldBe` fromString "-0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (-5, 9) (HALF_EVEN, Just 4) `shouldBe` fromString "-0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (-1, 8) (HALF_EVEN, Just 2) `shouldBe` fromString "-0.12"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (-15, 100) (HALF_EVEN, Just 1) `shouldBe` fromString "-0.2"
    it "rounds up if next decimal would be > 5 when using HALF_EVEN" $
      divide (-2, 3) (HALF_EVEN, Just 4) `shouldBe` fromString "-0.6667"
    it "throws an exception when PRECISE is used and a non-terminating decimal expansion is detected" $
      evaluate (divide (-5, 9) (PRECISE, Nothing)) `shouldThrow` anyException
    it "gives a precise value when using PRECISE and no max precision" $
      divide (-1, 32) (PRECISE, Nothing) `shouldBe` fromString "-0.03125"
    it "gives a precise value when using PRECISE and a sufficient precision" $
      divide (-1, 32) (PRECISE, Just 5) `shouldBe` fromString "-0.03125"
    it "gives a precise value when using PRECISE and a to small precision" $
      evaluate (divide (-1, 32) (PRECISE, Just 4)) `shouldThrow` anyException

  describe "divide +/-" $ do
    -- checking divisor < 0
    it "divides BigDecimals applying RoundingMode and precision" $
      divide (2, -3) (HALF_UP, Just 9) `shouldBe` fromString "-0.666666667"
    it "always rounds down when using DOWN" $
      divide (2, -3) (DOWN, Just 9) `shouldBe` fromString "-0.666666666"
    it "always rounds towards -INF when using FLOOR" $
      divide (2, -3) (FLOOR, Just 9) `shouldBe` fromString "-0.666666667"
    it "rounds up when using UP when there is a remainder" $
      divide (1, -9) (UP, Just 3) `shouldBe` fromString "-0.112"
    it "does not round when there is no remainder when using UP" $
      divide (14, -100) (UP, Just 2) `shouldBe` fromString "-0.14"
    it "rounds towards +INF when using CEILING when there is a remainder" $
      divide (1, -9) (CEILING, Just 3) `shouldBe` fromString "-0.111"
    it "does not round when there is no remainder when using UP" $
      divide (14, -100) (CEILING, Just 2) `shouldBe` fromString "-0.14"
    it "rounds down if next decimal would be <= 5 when using HALF_DOWN" $
      divide (5, -9) (HALF_DOWN, Just 4) `shouldBe` fromString "-0.5555"
    it "rounds up if next decimal would be > 5 when using HALF_DOWN" $
      divide (2, -3) (HALF_DOWN, Just 4) `shouldBe` fromString "-0.6667"
    it "rounds up if next decimal would be >= 5 when using HALF_UP" $
      divide (5, -9) (HALF_UP, Just 4) `shouldBe` fromString "-0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (5, -9) (HALF_EVEN, Just 4) `shouldBe` fromString "-0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (1, -8) (HALF_EVEN, Just 2) `shouldBe` fromString "-0.12"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (15, -100) (HALF_EVEN, Just 1) `shouldBe` fromString "-0.2"
    it "rounds up if next decimal would be > 5 when using HALF_EVEN" $
      divide (2, -3) (HALF_EVEN, Just 4) `shouldBe` fromString "-0.6667"
    it "throws an exception when PRECISE is used and a non-terminating decimal expansion is detected" $
      evaluate (divide (5, -9) (PRECISE, Nothing)) `shouldThrow` anyException
    it "gives a precise value when using PRECISE and no max precision" $
      divide (1, -32) (PRECISE, Nothing) `shouldBe` fromString "-0.03125"
    it "gives a precise value when using PRECISE and a sufficient precision" $
      divide (1, -32) (PRECISE, Just 5) `shouldBe` fromString "-0.03125"
    it "gives a precise value when using PRECISE and a to small precision" $
      evaluate (divide (1, -32) (PRECISE, Just 4)) `shouldThrow` anyException

  describe "divide -/-" $ do
    -- checking dividend and divisor < 0
    it "divides BigDecimals applying RoundingMode and precision" $
      divide (-2, -3) (HALF_UP, Just 9) `shouldBe` fromString "0.666666667"
    it "always rounds down when using DOWN" $
      divide (-2, -3) (DOWN, Just 9) `shouldBe` fromString "0.666666666"
    it "always rounds towards -INF when using FLOOR" $
      divide (-2, -3) (FLOOR, Just 9) `shouldBe` fromString "0.666666666"
    it "rounds up when using UP when there is a remainder" $
      divide (-1, -9) (UP, Just 3) `shouldBe` fromString "0.112"
    it "does not round when there is no remainder when using UP" $
      divide (-14, -100) (UP, Just 2) `shouldBe` fromString "0.14"
    it "rounds towards +INF when using CEILING when there is a remainder" $
      divide (-1, -9) (CEILING, Just 3) `shouldBe` fromString "0.112"
    it "does not round when there is no remainder when using UP" $
      divide (-14, -100) (CEILING, Just 2) `shouldBe` fromString "0.14"
    it "rounds down if next decimal would be <= 5 when using HALF_DOWN" $
      divide (-5, -9) (HALF_DOWN, Just 4) `shouldBe` fromString "0.5555"
    it "rounds up if next decimal would be > 5 when using HALF_DOWN" $
      divide (-2, -3) (HALF_DOWN, Just 4) `shouldBe` fromString "0.6667"
    it "rounds up if next decimal would be >= 5 when using HALF_UP" $
      divide (-5, -9) (HALF_UP, Just 4) `shouldBe` fromString "0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (-5, -9) (HALF_EVEN, Just 4) `shouldBe` fromString "0.5556"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (-1, -8) (HALF_EVEN, Just 2) `shouldBe` fromString "0.12"
    it "rounds to next even number if next decimal would be == 5 when using HALF_EVEN" $
      divide (-15, -100) (HALF_EVEN, Just 1) `shouldBe` fromString "0.2"
    it "rounds up if next decimal would be > 5 when using HALF_EVEN" $
      divide (-2, -3) (HALF_EVEN, Just 4) `shouldBe` fromString "0.6667"
    it "throws an exception when PRECISE is used and a non-terminating decimal expansion is detected" $
      evaluate (divide (-5, -9) (PRECISE, Nothing)) `shouldThrow` anyException
    it "gives a precise value when using PRECISE and no max precision" $
      divide (-1, -32) (PRECISE, Nothing) `shouldBe` fromString "0.03125"
    it "gives a precise value when using PRECISE and a sufficient precision" $
      divide (-1, -32) (PRECISE, Just 5) `shouldBe` fromString "0.03125"
    it "gives a precise value when using PRECISE and a to small precision" $
      evaluate (divide (-1, -32) (PRECISE, Just 4)) `shouldThrow` anyException


  describe "shrink" $ do
    it "removes trailing zeros while taking care of the scale" $
      nf (BigDecimal 1000 3) `shouldBe` BigDecimal 1 0
    it "does not eliminate more 0s than requested" $
      trim 2 (BigDecimal 1000 3) `shouldBe` BigDecimal 100 2
    it "does not eliminate more 0s than possible" $
      nf (BigDecimal 1230 3) `shouldBe` BigDecimal 123 2
    it "does not change the value of a BigDecimal" $
      property $ \bd n -> trim n bd === bd

  describe "matchScales" $
    it "adjusts a pair of BigDecimals to use the same scale" $
      property $ \x y -> let (x', y') = matchScales (x,y) in getScale x' === getScale y'

  describe "roundBD" $ do
    it "rounds a BigDecimal " $
      roundBD (BigDecimal 123456 3) (halfUp 2) `shouldBe` BigDecimal 12346 2
    it "ignores negative scales in MathContext" $
      roundBD (BigDecimal 123456 3) (halfUp (-2)) `shouldBe` BigDecimal 123456 3
    it "ignores MathContext with scale higher than in input value" $
      roundBD (BigDecimal 123456 3) (halfUp 10) `shouldBe` BigDecimal 123456 3