module Data.Decimal (
DecimalRaw (..),
Decimal,
realFracToDecimal,
decimalConvert,
unsafeDecimalConvert,
roundTo,
(*.),
divide,
allocate,
eitherFromRational,
normalizeDecimal
) where
import Control.Monad.Instances ()
import Control.DeepSeq
import Data.Char
import Data.Ratio
import Data.Word
import Data.Typeable
import Text.ParserCombinators.ReadP
data (Integral i) => DecimalRaw i = Decimal {
decimalPlaces :: ! Word8,
decimalMantissa :: ! i}
deriving (Typeable)
type Decimal = DecimalRaw Integer
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
realFracToDecimal :: (Integral i, RealFrac r) => Word8 -> r -> DecimalRaw i
realFracToDecimal e r = Decimal e $ round (r * (10^e))
divRound :: (Integral a) => a -> a -> a
divRound n1 n2 = if abs r * 2 >= abs n2 then n + signum n1 else n
where (n, r) = n1 `quotRem` n2
unsafeDecimalConvert :: (Integral a, Integral b) => DecimalRaw a -> DecimalRaw b
unsafeDecimalConvert (Decimal e n) = Decimal e $ fromIntegral n
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
ub = fromIntegral $ max maxBound n2
lb = fromIntegral $ min minBound n2
in if lb <= n1 && n1 <= ub then Just $ Decimal e n2 else Nothing
roundTo :: (Integral i) => Word8 -> DecimalRaw i -> DecimalRaw i
roundTo d (Decimal e n) = Decimal d $ fromIntegral n1
where
n1 = case compare d e of
LT -> n `divRound` divisor
EQ -> n
GT -> n * multiplier
divisor = 10 ^ (ed)
multiplier = 10 ^ (de)
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
(Decimal _ n1) = roundTo e d1
(Decimal _ n2) = roundTo e d2
instance (Integral i, Show i) => Show (DecimalRaw i) where
showsPrec _ (Decimal e n)
| e == 0 = ((signStr ++ strN) ++)
| otherwise = (concat [signStr, intPart, ".", fracPart] ++)
where
strN = show $ abs n
signStr = if n < 0 then "-" else ""
len = length strN
padded = replicate (fromIntegral e + 1 len) '0' ++ strN
(intPart, fracPart) = splitAt (max 1 (len fromIntegral e)) padded
instance (Integral i, Read i) => Read (DecimalRaw i) where
readsPrec _ = readP_to_S readDecimalP
readDecimalP :: (Integral i, Read i) => ReadP (DecimalRaw i)
readDecimalP = do
s1 <- myOpt '+' $ char '-' +++ char '+'
intPart <- munch1 isDigit
fractPart <- myOpt "" $ do
_ <- char '.'
munch1 isDigit
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
d1 == d2 = n1 == n2 where (_, n1, n2) = roundMax d1 d2
instance (Integral i) => Ord (DecimalRaw i) where
compare d1 d2 = compare n1 n2 where (_, n1, n2) = roundMax d1 d2
instance (Integral i) => Num (DecimalRaw i) where
d1 + d2 = Decimal e $ fromIntegral (n1 + n2)
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
abs (Decimal e n) = Decimal e $ abs n
signum (Decimal _ n) = fromIntegral $ signum n
fromInteger n = Decimal 0 $ fromIntegral n
instance (Integral i) => Real (DecimalRaw i) where
toRational (Decimal e n) = fromIntegral n % (10 ^ e)
instance (Integral i) => Fractional (DecimalRaw i) where
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)
where
(rnd, rep) = properFraction $ toRational a
divide :: Decimal -> Int -> [(Int, Decimal)]
divide (Decimal e n) d
| d > 0 =
case n `divMod` fromIntegral d of
(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."
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 * p1) `zdiv` t1, t1 p1)
zdiv 0 0 = 0
zdiv x y = x `divRound` y
total = sum ps
(*.) :: (Integral i, RealFrac r) => DecimalRaw i -> r -> DecimalRaw i
(Decimal e m) *. d = Decimal e $ round $ fromIntegral m * d
factorN :: (Integral a)
=> a
-> a
-> (a, a)
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
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)
then Left $ show e ++ " is too big ten power to represent as Decimal"
else Right $ fromIntegral e
normalizeDecimal :: (Integral i) => DecimalRaw i -> DecimalRaw i
normalizeDecimal r = case eitherFromRational $ toRational r of
Right x -> x
Left e -> error $ "Impossible happened: " ++ e