packages feed

Decimal 0.3.1 → 0.4.1

raw patch · 4 files changed

+125/−56 lines, 4 files

Files

Decimal.cabal view
@@ -1,5 +1,5 @@ Name:                Decimal-Version:             0.3.1+Version:             0.4.1 License:             BSD3 License-file:        LICENSE.txt Copyright:           Paul Johnson, 2013
README.txt view
@@ -27,8 +27,6 @@    cabal build    cabal test -Data.Decimal is an instance of Arbitrary, for your convenience in-writing your own tests.   Version 0.2.1@@ -57,3 +55,15 @@ These changes alter the API. Hence the increment to the major version number.  Thanks to Alexey Uimanov (s9gf4ult at gmail.com).++Version 0.4.1+-------------++Improved "Read" instance. Now handles "1.2e3" and "reads" only returns a +   single parse.+Corrected documentation.+Added "Enum" instance.+"decimalConvert" now returns a Maybe value. The old version has been renamed+   to "unsafeDecimalConvert.++   
src/Data/Decimal.hs view
@@ -4,35 +4,44 @@ -- @m@ and @e@ are integers.  The exponent @e@ is an unsigned Word8.  Hence -- the smallest value that can be represented is @10^-255@. -- --- Unary arithmetic results have the exponent of the argument.  Binary--- arithmetic results have an exponent equal to the maximum of the exponents--- of the arguments.+-- Unary arithmetic results have the exponent of the argument.  +-- Addition and subtraction results have an exponent equal to the +-- maximum of the exponents of the arguments. Other operators have+-- exponents sufficient to show the exact result, up to a limit of+-- 255: -- --- Decimal numbers are defined as instances of @Real@.  This means that--- conventional division is not defined.  Instead the functions @divide@ and --- @allocate@ will split a decimal amount into lists of results.  These --- results are guaranteed to sum to the original number.  This is a useful--- property when doing financial arithmetic.+-- > 0.15 * 0.15 :: Decimal    = 0.0225+-- > (1/3) :: Decimal          = 0.33333333333333...+-- > decimalPlaces (1/3)       = 255 -- +-- While @(/)@ is defined, you don't normally want to use it. Instead+-- The functions "divide" and "allocate" will split a decimal amount +-- into lists of results which are guaranteed to sum to the original +-- number.  This is a useful property when doing financial arithmetic.+--  -- The arithmetic on mantissas is always done using @Integer@, regardless of--- the type of @DecimalRaw@ being manipulated.  In practice it is recommended--- that @Decimal@ be used, with other types being used only where necessary--- (e.g. to conform to a network protocol).+-- the type of @DecimalRaw@ being manipulated.  In practice it is strongly+-- recommended that @Decimal@ be used, with other types being used only where +-- necessary (e.g. to conform to a network protocol). For instance +-- @(1/3) :: DecimalRaw Int@ does not give the right answer. + module Data.Decimal (    -- ** Decimal Values    DecimalRaw (..),    Decimal,    realFracToDecimal,    decimalConvert,+   unsafeDecimalConvert,    roundTo,    (*.),    divide,    allocate,    eitherFromRational,-   normalizeDecimal,+   normalizeDecimal ) where + import Control.Monad.Instances () import Control.DeepSeq import Data.Char@@ -51,8 +60,6 @@ -- will return \"1.500\".  Conversely the "Read" instance will use the decimal -- places to determine the precision. -- --- Arithmetic and comparision operators convert their arguments to the --- greater of the two precisions, and return a result of that precision.   -- Regardless of the type of the arguments, all mantissa arithmetic is done -- using @Integer@ types, so application developers do not need to worry about -- overflow in the internal algorithms.  However the result of each operator@@ -63,7 +70,7 @@                                   deriving (Typeable)  --- | Arbitrary precision decimal type.  As a rule programs should do decimal+-- | Arbitrary precision decimal type.  Programs should do decimal -- arithmetic with this type and only convert to other instances of  -- "DecimalRaw" where required by an external interface. -- @@ -73,6 +80,17 @@  instance (Integral i, NFData i) => NFData (DecimalRaw i) where     rnf (Decimal _ i) = rnf i+    +instance (Integral i) => Enum (DecimalRaw i) where+   succ x = x + 1+   pred x = x - 1+   toEnum = fromIntegral+   fromEnum = fromIntegral . decimalMantissa . roundTo 0+   enumFrom = iterate (+1)+   enumFromThen x1 x2 = let dx = x2 - x1 in iterate (+dx) x1+   enumFromTo x1 x2 = takeWhile (<= x2) $ iterate (+1) x1+   enumFromThenTo x1 x2 x3 = takeWhile (<= x3) $ enumFromThen x1 x2+     -- | Convert a real fractional value into a Decimal of the appropriate  -- precision.@@ -80,20 +98,36 @@ realFracToDecimal e r = Decimal e $ round (r * (10^e))  --- Internal function to divide and return the nearest integer.+-- Internal function to divide and return the nearest integer. Rounds 0.5 away from zero. divRound :: (Integral a) => a -> a -> a-divRound n1 n2 = if abs r > abs (n2 `quot` 2) then n + signum n else n+divRound n1 n2 = if abs r * 2 >= abs n2 then n + signum n1 else n     where (n, r) = n1 `quotRem` n2   -- | Convert a @DecimalRaw@ from one base representation to another.  Does--- not check for overflow in the new representation.-decimalConvert :: (Integral a, Integral b) => DecimalRaw a -> DecimalRaw b-decimalConvert (Decimal e n) = Decimal e $ fromIntegral n+-- not check for overflow in the new representation. Only use after+-- using "roundTo" to put an upper value on the exponent, or to convert+-- to a larger representation.+unsafeDecimalConvert :: (Integral a, Integral b) => DecimalRaw a -> DecimalRaw b+unsafeDecimalConvert (Decimal e n) = Decimal e $ fromIntegral n  --- | Round a @DecimalRaw@ to a specified number of decimal places.-roundTo :: (Integral i) => Word8 -> DecimalRaw i -> DecimalRaw Integer+-- | Convert a @DecimalRaw@ from one base to another. Returns @Nothing@ if+-- this would cause arithmetic overflow.+decimalConvert :: (Integral a, Integral b, Bounded b) =>+   DecimalRaw a -> Maybe (DecimalRaw b)+decimalConvert (Decimal e n) = +   let n1 :: Integer+       n1 = fromIntegral n+       n2 = fromIntegral n   -- Of type b.+       ub = fromIntegral $ max maxBound n2  -- Can't say "maxBound :: b", so do this instead.+       lb = fromIntegral $ min minBound n2+   in if lb <= n1 && n1 <= ub then Just $ Decimal e n2 else Nothing+++-- | Round a @DecimalRaw@ to a specified number of decimal places. +-- If the value ends in @5@ then it is rounded away from zero. +roundTo :: (Integral i) => Word8 -> DecimalRaw i -> DecimalRaw i roundTo d (Decimal e n) = Decimal d $ fromIntegral n1     where       n1 = case compare d e of@@ -105,8 +139,7 @@   -- Round the two DecimalRaw values to the largest exponent.-roundMax :: (Integral i) => -            DecimalRaw i -> DecimalRaw i -> (Word8, Integer, Integer)+roundMax :: (Integral i) => DecimalRaw i -> DecimalRaw i -> (Word8, i, i) roundMax d1@(Decimal e1 _) d2@(Decimal e2 _) = (e, n1, n2)     where       e = max e1 e2@@ -116,7 +149,7 @@  instance (Integral i, Show i) => Show (DecimalRaw i) where    showsPrec _ (Decimal e n)-       | e == 0     = (concat [signStr, strN] ++)+       | e == 0     = ((signStr ++ strN) ++)        | otherwise  = (concat [signStr, intPart, ".", fracPart] ++)        where          strN = show $ abs n@@ -126,16 +159,34 @@          (intPart, fracPart) = splitAt (max 1 (len - fromIntegral e)) padded  instance (Integral i, Read i) => Read (DecimalRaw i) where-    readsPrec _ = -        readP_to_S $ do-          (intPart, _) <- gather $ do-                            optional $ char '-'-                            munch1 isDigit-          fractPart    <- option "" $ do+    readsPrec _ = readP_to_S readDecimalP+        ++-- | Parse a Decimal value. Used for the Read instance.+readDecimalP :: (Integral i, Read i) => ReadP (DecimalRaw i)+readDecimalP = do+          s1           <- myOpt '+' $ char '-' +++ char '+'+          intPart      <- munch1 isDigit+          fractPart    <- myOpt "" $ do                             _ <- char '.'                             munch1 isDigit-          return $ Decimal (fromIntegral $ length fractPart) $ read $ -                 intPart ++ fractPart+          expPart <- myOpt 0 $ do+                            _  <- char 'e' +++ char 'E'+                            s2 <- myOpt '+' $ char '-' +++ char '+'+                            fmap (applySign s2 . strToInt) $ munch1 isDigit+          let n = applySign s1 $ strToInt $ intPart ++ fractPart+              e = length fractPart - expPart+          if e < 0+             then return $ Decimal 0 $ n * 10 ^ negate e+             else if e < 256+                then return $ Decimal (fromIntegral e) n+                else pfail+    where+       strToInt :: (Integral n) => String -> n+       strToInt = foldl (\t v -> 10 * t + v) 0 . map (fromIntegral . subtract (ord '0') . ord)+       applySign '-' v = negate v+       applySign _   v = v+       myOpt d p = p <++ return d   instance (Integral i) => Eq (DecimalRaw i) where@@ -151,7 +202,7 @@         where (e, n1, n2) = roundMax d1 d2     d1 - d2 = Decimal e $ fromIntegral (n1 - n2)         where (e, n1, n2) = roundMax d1 d2-    d1 * d2 = normalizeDecimal $ realFracToDecimal maxBound $ (toRational d1) * (toRational d2)+    d1 * d2 = normalizeDecimal $ realFracToDecimal maxBound $ toRational d1 * toRational d2      abs (Decimal e n) = Decimal e $ abs n     signum (Decimal _ n) = fromIntegral $ signum n@@ -161,8 +212,12 @@     toRational (Decimal e n) = fromIntegral n % (10 ^ e)  instance (Integral i) => Fractional (DecimalRaw i) where-  fromRational r = normalizeDecimal $ realFracToDecimal maxBound r-  a / b = fromRational $ (toRational a) / (toRational b)+  fromRational r = +     let+        v :: Decimal+        v = normalizeDecimal $ realFracToDecimal maxBound r+     in unsafeDecimalConvert v +  a / b = fromRational $ toRational a / toRational b  instance (Integral i) => RealFrac (DecimalRaw i) where   properFraction a = (rnd, fromRational rep)@@ -178,12 +233,12 @@ -- The portions are represented as a list of pairs.  The first part of each -- pair is the number of portions, and the second part is the portion value. -- Hence 10 dollars divided 3 ways will produce @[(2, 3.33), (1, 3.34)]@.-divide :: (Integral i) => DecimalRaw i -> Int -> [(Int, DecimalRaw i)]+divide :: Decimal -> Int -> [(Int, Decimal)] divide (Decimal e n) d      | d > 0 =          case n `divMod` fromIntegral d of-          (result, 0) -> [(fromIntegral d, Decimal e result)]-          (result, r) -> [(fromIntegral d - fromIntegral r,+          (result, 0) -> [(d, Decimal e result)]+          (result, r) -> [(d - fromIntegral r,                            Decimal e result),                            (fromIntegral r, Decimal e (result+1))]     | otherwise = error "Data.Decimal.divide: Divisor must be > 0."@@ -199,18 +254,17 @@ --  -- > let result = allocate d parts -- > in all (== d / sum parts) $ zipWith (/) result parts-allocate :: (Integral i) => DecimalRaw i -> [Integer] -> [DecimalRaw i]+allocate :: Decimal -> [Integer] -> [Decimal] allocate (Decimal e n) ps     | total == 0  =          error "Data.Decimal.allocate: allocation list must not sum to zero."     | otherwise   = map (Decimal e) $ zipWith (-) ts (tail ts)     where       ts = map fst $ scanl nxt (n, total) ps-      nxt (n1, t1) p1 = (n1 - (n1 * fromIntegral p1) `zdiv` t1, -                         t1 - fromIntegral p1)+      nxt (n1, t1) p1 = (n1 - (n1 * p1) `zdiv` t1, t1 - p1)       zdiv 0 0 = 0       zdiv x y = x `divRound` y-      total = fromIntegral $ sum ps+      total = sum ps   -- | Multiply a @DecimalRaw@ by a @RealFrac@ value.@@ -246,12 +300,12 @@     (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+    we = if e > fromIntegral (maxBound :: Word8)          then Left $ show e ++ " is too big ten power to represent as Decimal"          else Right $ fromIntegral e --- | Reduce the exponent of the decimal numer to the minimal posible value-normalizeDecimal :: (Integral i) => (DecimalRaw i) -> (DecimalRaw i)+-- | Reduce the exponent of the decimal number to the minimal possible value+normalizeDecimal :: (Integral i) => DecimalRaw i -> DecimalRaw i normalizeDecimal r = case eitherFromRational $ toRational r of   Right x -> x-  Left e -> error $ "Imposible happened: " ++ e+  Left e -> error $ "Impossible happened: " ++ e
tests/Main.hs view
@@ -7,7 +7,6 @@ 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)@@ -21,14 +20,14 @@   --   return $ Decimal (fromIntegral $ abs e) m        instance (Integral i, Arbitrary i) => CoArbitrary (DecimalRaw i) where-    coarbitrary (Decimal e m) gen = variant (v:: Integer) gen+    coarbitrary (Decimal e m) = variant (v:: Integer)        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+prop_readShow d =  read (show d) == d  -- | Read and show preserve decimal places. -- @@ -143,7 +142,7 @@ prop_mulValid a b = ((ad + bd) < fromIntegral (maxBound :: Word8)) ==>                     (toRational (a * b) == (toRational a) * (toRational b))   where-    ad :: Integer+    ad, bd :: Integer     ad = fromIntegral $ decimalPlaces a     bd = fromIntegral $ decimalPlaces b @@ -219,7 +218,13 @@                 testCase "1.0 * pi"      (dec 1 31    @=? dec 1 10 *. piD),                 testCase "1.23 * pi"     (dec 2 386   @=? dec 2 123 *. piD),                 testCase "Decimal to DecimalRaw Int" -                                         (decimalConvert (dec 2 123) @=? dec1 2 123),-                testCase "1.234 to rational" (1234 % 1000 @=? (toRational (dec 3 1234)))+                                         (decimalConvert (dec 2 123) @=? Just (dec1 2 123)),+                testCase "decimalConvert overflow prevention"+                                         (decimalConvert (1/3) @=? (Nothing :: Maybe (DecimalRaw Int))),+                testCase "1.234 to rational" (1234 % 1000 @=? toRational (dec 3 1234)),+                testCase "fromRational (1%10) for DecimalRaw Int"  -- Fixed bug #3+                                         (let v :: DecimalRaw Int+                                              v = fromRational (1%10)+                                          in toRational v @=? 1%10)                 ]        ]