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