Decimal 0.3.1 → 0.4.1
raw patch · 4 files changed
+125/−56 lines, 4 files
Files
- Decimal.cabal +1/−1
- README.txt +12/−2
- src/Data/Decimal.hs +101/−47
- tests/Main.hs +11/−6
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) ] ]