Decimal 0.2.3 → 0.3.1
raw patch · 4 files changed
+240/−144 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- Data.Decimal: instance (Integral i, Arbitrary i) => Arbitrary (DecimalRaw i)
- Data.Decimal: instance (Integral i, Arbitrary i) => CoArbitrary (DecimalRaw i)
- Data.Decimal: prop_abs :: Decimal -> Bool
- Data.Decimal: prop_allocateParts :: Decimal -> [Integer] -> Property
- Data.Decimal: prop_allocateUnits :: Decimal -> [Integer] -> Property
- Data.Decimal: prop_decreaseDecimals :: Decimal -> Decimal -> Bool
- Data.Decimal: prop_divisionParts :: Decimal -> Positive Int -> Property
- Data.Decimal: prop_divisionUnits :: Decimal -> Positive Int -> Bool
- Data.Decimal: prop_fromIntegerZero :: Integer -> Bool
- Data.Decimal: prop_increaseDecimals :: Decimal -> Property
- Data.Decimal: prop_inverseAdd :: Decimal -> Decimal -> Bool
- Data.Decimal: prop_readShow :: Decimal -> Bool
- Data.Decimal: prop_readShowPrecision :: Decimal -> Bool
- Data.Decimal: prop_repeatedAdd :: Decimal -> Word8 -> Bool
- Data.Decimal: prop_signum :: Decimal -> Bool
+ Data.Decimal: eitherFromRational :: Integral i => Rational -> Either String (DecimalRaw i)
+ Data.Decimal: instance Integral i => Fractional (DecimalRaw i)
+ Data.Decimal: instance Integral i => RealFrac (DecimalRaw i)
+ Data.Decimal: instance Typeable1 DecimalRaw
+ Data.Decimal: normalizeDecimal :: Integral i => (DecimalRaw i) -> (DecimalRaw i)
Files
- Decimal.cabal +5/−5
- README.txt +11/−2
- src/Data/Decimal.hs +54/−134
- tests/Main.hs +170/−3
Decimal.cabal view
@@ -1,8 +1,8 @@ Name: Decimal-Version: 0.2.3+Version: 0.3.1 License: BSD3 License-file: LICENSE.txt-Copyright: Paul Johnson, 2011+Copyright: Paul Johnson, 2013 Author: Paul Johnson Maintainer: paul@cogito.org.uk Stability: beta@@ -14,13 +14,13 @@ exponent. The exponent can be interpreted as the number of decimal places in the value. Extra-source-files: README.txt-tested-with: GHC==7.0.4+tested-with: GHC==7.4.2+homepage: https://github.com/PaulJohnson/Haskell-Decimal library build-depends: base >= 4 && < 5,- deepseq,- QuickCheck >= 2.4+ deepseq hs-source-dirs: src if impl(ghc >= 7.0.0) default-language: Haskell2010
README.txt view
@@ -21,8 +21,7 @@ ------------------------ Data.Decimal includes a set of QuickCheck properties which act as both-tests and a formal specification (hence their inclusion in the Haddock-documentation). To run the tests do:+tests and a formal specification. To run the tests do: cabal configure --enable-tests cabal build@@ -48,3 +47,13 @@ Added instance of NFData from Control.DeepSeq, and hence a dependency on the deepseq package, thanks to Jeff Shaw (shawjef3 at msu.edu).++Version 0.3.1+-------------++Added Typeable, Fractional and RealFrac instances.+Multiplication now returns an exact result, increasing precision if necessary.++These changes alter the API. Hence the increment to the major version number.++Thanks to Alexey Uimanov (s9gf4ult at gmail.com).
src/Data/Decimal.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE DeriveDataTypeable #-}+ -- | Decimal numbers are represented as @m*10^(-e)@ where -- @m@ and @e@ are integers. The exponent @e@ is an unsigned Word8. Hence -- the smallest value that can be represented is @10^-255@.@@ -27,27 +29,16 @@ (*.), divide, allocate,- -- ** QuickCheck Properties- prop_readShow,- prop_readShowPrecision,- prop_fromIntegerZero,- prop_increaseDecimals,- prop_decreaseDecimals,- prop_inverseAdd,- prop_repeatedAdd,- prop_divisionParts,- prop_divisionUnits,- prop_allocateParts,- prop_allocateUnits,- prop_abs,- prop_signum+ eitherFromRational,+ normalizeDecimal, ) where +import Control.Monad.Instances () import Control.DeepSeq import Data.Char import Data.Ratio import Data.Word-import Test.QuickCheck+import Data.Typeable import Text.ParserCombinators.ReadP -- | Raw decimal arithmetic type constructor. A decimal value consists of an@@ -69,6 +60,7 @@ data (Integral i) => DecimalRaw i = Decimal { decimalPlaces :: ! Word8, decimalMantissa :: ! i}+ deriving (Typeable) -- | Arbitrary precision decimal type. As a rule programs should do decimal@@ -159,9 +151,8 @@ where (e, n1, n2) = roundMax d1 d2 d1 - d2 = Decimal e $ fromIntegral (n1 - n2) where (e, n1, n2) = roundMax d1 d2- d1 * d2 = Decimal e $ fromIntegral $ - (n1 * n2) `divRound` (10 ^ e)- where (e, n1, n2) = roundMax d1 d2+ d1 * d2 = normalizeDecimal $ realFracToDecimal maxBound $ (toRational d1) * (toRational d2)+ abs (Decimal e n) = Decimal e $ abs n signum (Decimal _ n) = fromIntegral $ signum n fromInteger n = Decimal 0 $ fromIntegral n@@ -169,16 +160,16 @@ instance (Integral i) => Real (DecimalRaw i) where toRational (Decimal e n) = fromIntegral n % (10 ^ e) -instance (Integral i, Arbitrary i) => Arbitrary (DecimalRaw i) where- arbitrary = do- e <- sized (\n -> resize (n `div` 10) arbitrary) :: Gen Int- m <- sized (\n -> resize (n * 10) arbitrary)- return $ Decimal (fromIntegral $ abs e) m- -instance (Integral i, Arbitrary i) => CoArbitrary (DecimalRaw i) where- coarbitrary (Decimal e m) gen = variant (v:: Integer) gen- where v = fromIntegral e + fromIntegral m+instance (Integral i) => Fractional (DecimalRaw i) where+ fromRational r = normalizeDecimal $ realFracToDecimal maxBound r+ a / b = fromRational $ (toRational a) / (toRational b) +instance (Integral i) => RealFrac (DecimalRaw i) where+ properFraction a = (rnd, fromRational rep)+ where+ (rnd, rep) = properFraction $ toRational a+ + -- | Divide a @DecimalRaw@ value into one or more portions. The portions -- will be approximately equal, and the sum of the portions is guaranteed to@@ -226,112 +217,41 @@ (*.) :: (Integral i, RealFrac r) => DecimalRaw i -> r -> DecimalRaw i (Decimal e m) *. d = Decimal e $ round $ fromIntegral m * d ---- | "read" is the inverse of "show".--- --- > read (show n) == n-prop_readShow :: Decimal -> Bool-prop_readShow d = read (show d) == d---- | Read and show preserve decimal places.--- --- > decimalPlaces (read (show n)) == decimalPlaces n-prop_readShowPrecision :: Decimal -> Bool-prop_readShowPrecision d = decimalPlaces (read (show d) :: Decimal) - == decimalPlaces d----- | "fromInteger" definition.--- --- > decimalPlaces (fromInteger n) == 0 &&--- > decimalMantissa (fromInteger n) == n-prop_fromIntegerZero :: Integer -> Bool-prop_fromIntegerZero n = decimalPlaces (fromInteger n :: Decimal) == 0 &&- decimalMantissa (fromInteger n :: Decimal) == n----- | Increased precision does not affect equality.--- --- > decimalPlaces d < maxBound ==> roundTo (decimalPlaces d + 1) d == d-prop_increaseDecimals :: Decimal -> Property-prop_increaseDecimals d = - decimalPlaces d < maxBound ==> roundTo (decimalPlaces d + 1) d == d----- | Decreased precision can make two decimals equal, but it can never change--- their order.--- --- > forAll d1, d2 :: Decimal -> legal beforeRound afterRound--- > where--- > beforeRound = compare d1 d2--- > afterRound = compare (roundTo 0 d1) (roundTo 0 d2)--- > legal GT x = x `elem` [GT, EQ]--- > legal EQ x = x `elem` [EQ]--- > legal LT x = x `elem` [LT, EQ]-prop_decreaseDecimals :: Decimal -> Decimal -> Bool-prop_decreaseDecimals d1 d2 = legal beforeRound afterRound- where- beforeRound = compare d1 d2- afterRound = compare (roundTo 0 d1) (roundTo 0 d2)- legal GT x = x `elem` [GT, EQ]- legal EQ x = x `elem` [EQ]- legal LT x = x `elem` [LT, EQ]----- | > (x + y) - y == x-prop_inverseAdd :: Decimal -> Decimal -> Bool-prop_inverseAdd x y = (x + y) - y == x----- | Multiplication is repeated addition.--- --- > forall d, NonNegative i : (sum $ replicate i d) == d * fromIntegral (max i 0)-prop_repeatedAdd :: Decimal -> Word8 -> Bool-prop_repeatedAdd d i = (sum $ replicate (fromIntegral i) d) == d * fromIntegral (max i 0)----- | Division produces the right number of parts.--- --- > forall d, Positive i : (sum $ map fst $ divide d i) == i-prop_divisionParts :: Decimal -> Positive Int -> Property-prop_divisionParts d (Positive i) = i > 0 ==> (sum $ map fst $ divide d i) == i----- | Division doesn't drop any units.--- --- > forall d, Positive i : (sum $ map (\(n,d1) -> fromIntegral n * d1) $ divide d i) == d-prop_divisionUnits :: Decimal -> Positive Int -> Bool-prop_divisionUnits d (Positive i) = - (sum $ map (\(n,d1) -> fromIntegral n * d1) $ divide d i) == d----- | Allocate produces the right number of parts.--- --- > sum ps /= 0 ==> length ps == length (allocate d ps)-prop_allocateParts :: Decimal -> [Integer] -> Property-prop_allocateParts d ps = - sum ps /= 0 ==> length ps == length (allocate d ps)----- | Allocate doesn't drop any units.--- --- > sum ps /= 0 ==> sum (allocate d ps) == d-prop_allocateUnits :: Decimal -> [Integer] -> Property-prop_allocateUnits d ps =- sum ps /= 0 ==> sum (allocate d ps) == d+-- | Count the divisors, i.e. the count of 2 divisors in 18 is 1 because 18 = 2 * 3 * 3+factorN :: (Integral a)+ => a -- ^ Denominator base+ -> a -- ^ dividing value+ -> (a, a) -- ^ The count of divisors and the result of division+factorN d val = factorN' val 0+ where+ factorN' 1 acc = (acc, 1)+ factorN' v acc = if md == 0+ then factorN' vd (acc + 1)+ else (acc, v)+ where+ (vd, md) = v `divMod` d --- | Absolute value definition--- --- > decimalPlaces a == decimalPlaces d && --- > decimalMantissa a == abs (decimalMantissa d)--- > where a = abs d-prop_abs :: Decimal -> Bool-prop_abs d = decimalPlaces a == decimalPlaces d && - decimalMantissa a == abs (decimalMantissa d)- where a = abs d+-- | Try to convert Rational to Decimal with absolute precision+-- return string with fail description if not converted+eitherFromRational :: (Integral i) => Rational -> Either String (DecimalRaw i)+eitherFromRational r = if done == 1+ then do+ wres <- we+ return $ Decimal wres (fromIntegral m)+ else Left $ show r ++ " has no decimal denominator"+ where+ den = denominator r+ num = numerator r+ (f2, rest) = factorN 2 den+ (f5, done) = factorN 5 rest+ e = max f2 f5+ m = num * ((10^e) `div` den)+ we = if e > (fromIntegral (maxBound :: Word8)) -- FIXME: will fail if DecimalRaw changed+ then Left $ show e ++ " is too big ten power to represent as Decimal"+ else Right $ fromIntegral e --- | Sign number defintion--- --- > signum d == (fromInteger $ signum $ decimalMantissa d)-prop_signum :: Decimal -> Bool-prop_signum d = signum d == (fromInteger $ signum $ decimalMantissa d)+-- | Reduce the exponent of the decimal numer to the minimal posible value+normalizeDecimal :: (Integral i) => (DecimalRaw i) -> (DecimalRaw i)+normalizeDecimal r = case eitherFromRational $ toRational r of+ Right x -> x+ Left e -> error $ "Imposible happened: " ++ e
tests/Main.hs view
@@ -4,15 +4,175 @@ import Data.Ratio import Data.Word import Test.HUnit--+import Control.Applicative +import Test.QuickCheck+import qualified Test.QuickCheck.Property as P import Test.Framework as TF (defaultMain, testGroup, Test) import Test.Framework.Providers.HUnit import Test.Framework.Providers.QuickCheck2 (testProperty) +instance (Integral i, Arbitrary i) => Arbitrary (DecimalRaw i) where+ arbitrary = Decimal <$> arbitrary <*> arbitrary+ -- arbitrary = do + -- e <- sized (\n -> resize (n `div` 10) arbitrary) :: Gen Int+ -- m <- sized (\n -> resize (n * 10) arbitrary)+ -- return $ Decimal (fromIntegral $ abs e) m+ +instance (Integral i, Arbitrary i) => CoArbitrary (DecimalRaw i) where+ coarbitrary (Decimal e m) gen = variant (v:: Integer) gen+ where v = fromIntegral e + fromIntegral m+ +-- | "read" is the inverse of "show".+-- +-- > read (show n) == n+prop_readShow :: Decimal -> Bool+prop_readShow d = (read (show d)) == d +-- | Read and show preserve decimal places.+-- +-- > decimalPlaces (read (show n)) == decimalPlaces n+prop_readShowPrecision :: Decimal -> Bool+prop_readShowPrecision d = decimalPlaces (read (show d) :: Decimal) + == decimalPlaces d+++-- | "fromInteger" definition.+-- +-- > decimalPlaces (fromInteger n) == 0 &&+-- > decimalMantissa (fromInteger n) == n+prop_fromIntegerZero :: Integer -> Bool+prop_fromIntegerZero n = decimalPlaces (fromInteger n :: Decimal) == 0 &&+ decimalMantissa (fromInteger n :: Decimal) == n+++-- | Increased precision does not affect equality.+-- +-- > decimalPlaces d < maxBound ==> roundTo (decimalPlaces d + 1) d == d+prop_increaseDecimals :: Decimal -> Property+prop_increaseDecimals d = + decimalPlaces d < maxBound ==> roundTo (decimalPlaces d + 1) d == d+++-- | Decreased precision can make two decimals equal, but it can never change+-- their order.+-- +-- > forAll d1, d2 :: Decimal -> legal beforeRound afterRound+-- > where+-- > beforeRound = compare d1 d2+-- > afterRound = compare (roundTo 0 d1) (roundTo 0 d2)+-- > legal GT x = x `elem` [GT, EQ]+-- > legal EQ x = x `elem` [EQ]+-- > legal LT x = x `elem` [LT, EQ]+prop_decreaseDecimals :: Decimal -> Decimal -> Bool+prop_decreaseDecimals d1 d2 = legal beforeRound afterRound+ where+ beforeRound = compare d1 d2+ afterRound = compare (roundTo 0 d1) (roundTo 0 d2)+ legal GT x = x `elem` [GT, EQ]+ legal EQ x = x `elem` [EQ]+ legal LT x = x `elem` [LT, EQ]+++-- | > (x + y) - y == x+prop_inverseAdd :: Decimal -> Decimal -> Bool+prop_inverseAdd x y = (x + y) - y == x+++-- | Multiplication is repeated addition.+-- +-- > forall d, NonNegative i : (sum $ replicate i d) == d * fromIntegral (max i 0)+prop_repeatedAdd :: Decimal -> Word8 -> Bool+prop_repeatedAdd d i = (sum $ replicate (fromIntegral i) d) == d * fromIntegral (max i 0)+++-- | Division produces the right number of parts.+-- +-- > forall d, Positive i : (sum $ map fst $ divide d i) == i+prop_divisionParts :: Decimal -> Positive Int -> Property+prop_divisionParts d (Positive i) = i > 0 ==> (sum $ map fst $ divide d i) == i+++-- | Division doesn't drop any units.+-- +-- > forall d, Positive i : (sum $ map (\(n,d1) -> fromIntegral n * d1) $ divide d i) == d+prop_divisionUnits :: Decimal -> Positive Int -> Bool+prop_divisionUnits d (Positive i) = + (sum $ map (\(n,d1) -> fromIntegral n * d1) $ divide d i) == d+++-- | Allocate produces the right number of parts.+-- +-- > sum ps /= 0 ==> length ps == length (allocate d ps)+prop_allocateParts :: Decimal -> [Integer] -> Property+prop_allocateParts d ps = + sum ps /= 0 ==> length ps == length (allocate d ps)+++-- | Allocate doesn't drop any units.+-- +-- > sum ps /= 0 ==> sum (allocate d ps) == d+prop_allocateUnits :: Decimal -> [Integer] -> Property+prop_allocateUnits d ps =+ sum ps /= 0 ==> sum (allocate d ps) == d++-- | Absolute value definition+-- +-- > decimalPlaces a == decimalPlaces d && +-- > decimalMantissa a == abs (decimalMantissa d)+-- > where a = abs d+prop_abs :: Decimal -> Bool+prop_abs d = decimalPlaces a == decimalPlaces d && + decimalMantissa a == abs (decimalMantissa d)+ where a = abs d++-- | Sign number defintion+-- +-- > signum d == (fromInteger $ signum $ decimalMantissa d)+prop_signum :: Decimal -> Bool+prop_signum d = signum d == (fromInteger $ signum $ decimalMantissa d)++-- | The addition is valid+ +prop_sumValid :: Decimal -> Decimal -> Property+prop_sumValid a b = (decimalPlaces a < maxBound && decimalPlaces b < maxBound) ==>+ (toRational (a + b) == (toRational a) + (toRational b))++prop_mulValid :: Decimal -> Decimal -> Property+prop_mulValid a b = ((ad + bd) < fromIntegral (maxBound :: Word8)) ==>+ (toRational (a * b) == (toRational a) * (toRational b))+ where+ ad :: Integer+ ad = fromIntegral $ decimalPlaces a+ bd = fromIntegral $ decimalPlaces b++prop_eitherFromRational :: Decimal -> Bool+prop_eitherFromRational d = (Right d) == (eitherFromRational $ toRational d)++prop_normalizeDecimal :: Decimal -> Bool+prop_normalizeDecimal d = d == (normalizeDecimal d)+++-- | Division is the inverted multiplication+prop_divisionMultiplication :: Decimal -> Decimal -> Property+prop_divisionMultiplication a b = ((ad + bd) < fromIntegral (maxBound :: Word8) && a /= 0 && b /= 0) ==>+ (c / a == b) .&&. (c / b == a)+ where+ ad :: Integer+ ad = fromIntegral $ decimalPlaces a+ bd = fromIntegral $ decimalPlaces b+ c = a * b++prop_fromRational :: Decimal -> Bool+prop_fromRational a = a == (fromRational $ toRational a)++prop_properFraction :: Decimal -> Bool+prop_properFraction a = a == (fromIntegral b + d)+ where+ b :: Integer+ (b, d) = properFraction a+ main :: IO () main = defaultMain tests @@ -43,7 +203,14 @@ testProperty "allocateParts" prop_allocateParts, testProperty "allocateUnits" prop_allocateUnits, testProperty "abs" prop_abs,- testProperty "signum" prop_signum+ testProperty "signum" prop_signum,+ testProperty "sumvalid" prop_sumValid,+ testProperty "mulValid" prop_mulValid,+ testProperty "eitherFromRational" prop_eitherFromRational,+ testProperty "normalizeDecimal" prop_normalizeDecimal,+ testProperty "divisionMultiplication" prop_divisionMultiplication,+ testProperty "fromRational" prop_fromRational,+ testProperty "properFraction" prop_properFraction ], testGroup "Point tests Data.Decimal" [ testCase "pi to 3dp" (dec 3 3142 @=? realFracToDecimal 3 piD),