-- | -- Module : Numeric.Literals.Decimal -- Copyright : (c) Justus Sagemüller 2017 -- License : GPL v3 -- -- Maintainer : (@) jsagemue $ uni-koeln.de -- Stability : experimental -- Portability : portable -- {-# LANGUAGE PatternSynonyms, ViewPatterns #-} module Numeric.Literals.Decimal ( FractionalLit , pattern (:%) , pattern Scientific -- * Auxiliary , B₁₀Digit ) where import Data.Ratio -- | A type whose 'Fractional' instance gives a somewhat reliable indication whether -- a value was actually defined as an integer or a ratio, or as a decimal-fraction -- literal. This is useful to know for a type that supports both exact fraction -- values and more floating-point-like / physical values; it allows avoiding -- issues like @0.524@ showing up as @589971551185535/1125899906842624@, or conversely -- @7/23@ as @0.30434782608695654@. Both of these scenarios are quite awkward. data FractionalLit = ExactRatio Rational | DecimalFraction { decimalMantissa :: Integer , decimalExponent :: Int } asFraction :: FractionalLit -> Maybe (Integer, Integer) asFraction (ExactRatio r) = Just (numerator r, denominator r) asFraction (DecimalFraction _ _) = Nothing -- | Construct an exact fraction. The values behave like 'Rational', until combined -- – e.g. added – with a 'Scientific' value (which has an implicit -- measurement-uncertainty, and that carries over to the result). pattern (:%) :: Integer -> Integer -> FractionalLit pattern n:%d <- (asFraction -> Just (n,d)) where n:%d = ExactRatio $ fromInteger n % fromInteger d asScientific :: FractionalLit -> Maybe ((Int, [B₁₀Digit]), Int) asScientific (ExactRatio _) = Nothing asScientific n = case break (=='e') $ show n of (m, 'e':e) -> Just (splitMantissa m, read e) (m, []) -> Just (splitMantissa m, 0) where splitMantissa m = case break (=='.') m of (pm,'.':am) -> (read pm, parseB₁₀<$>am) (pm,[]) -> (read pm, []) parseB₁₀ c = case fromEnum c - fromEnum '0' of i | i>=0 && i<10 -> toEnum i | otherwise -> error $ "Impossible digit "++[c]++" in number "++show n -- | Construct a scientific number of the form @m.n * 10^e@, where @m@ and @e@ are -- integers and @n@ is a list of digits after the decimal point. The result -- is considered to be only exact up to the precision indicated by the number -- of digits. I.e. @Scientific 2 [4,8,3]\ (-4)@ basically means @2.483×10⁻⁴ ± 10⁻⁷@, -- -- The 'Fractional' instance allows these values to be written in the standard -- @2.483e-4@ notation. Note that this cannot completely reconstruct the written -- form, e.g. @12.483e-4@ will actually show up as @Scientific 1 [2,4,8,3]\ (-3)@. -- Leading and trailing zeroes are always ignored. pattern Scientific :: Int -- ^ Integral part of the mantissa -> [B₁₀Digit] -- ^ Digits after the point of the mantissa -> Int -- ^ Base-10 exponent of the number in scientific form -> FractionalLit pattern Scientific pc ac ex <- (asScientific -> Just ((pc,ac),ex)) where Scientific pc ac ex = DecimalFraction (nqr (fromIntegral pc) ac) $ ex - length ac where nqr n [] = n nqr n (d:ds) = nqr (n*10 + fromIntegral (fromEnum d)) ds -- | A number between @0@ and @9@. data B₁₀Digit = D0 | D1 | D2 | D3 | D4 | D5 | D6 | D7 | D8 | D9 deriving (Enum, Eq, Ord) instance Show B₁₀Digit where show = show . fromEnum instance Num B₁₀Digit where fromInteger = toEnum . (`mod`10) . fromInteger d + e = toEnum $ (fromEnum d + fromEnum e)`mod`10 d * e = toEnum $ (fromEnum d * fromEnum e)`mod`10 d - e = toEnum $ (fromEnum d - fromEnum e)`mod`10 abs = id signum D0 = 0 signum _ = 1 instance Eq FractionalLit where ExactRatio r₀ == ExactRatio r₁ = r₀==r₁ DecimalFraction m₀ e₀ == DecimalFraction m₁ e₁ = m₀==m₁ && e₀==e₁ ExactRatio r == DecimalFraction m e | e>0 = ExactRatio r == DecimalFraction (m*10^e) 0 | (ers,0) <- (10^(-e)) `divMod` denominator r = m == numerator r*ers | otherwise = False n == m = m == n instance Show FractionalLit where showsPrec p (ExactRatio r) | denominator r > 1 = showParen (p>6) $ showsPrec 7 (numerator r) . ('/':) . showsPrec 7 (denominator r) | otherwise = shows $ numerator r showsPrec p (DecimalFraction m e) | m < 0 = showParen (p>5) $ ('-':) . shows (DecimalFraction (-m) e) | e > 4 || e < -2 = case show m of lsh | e<0, length lsh + 1 > -e -> case splitAt (-e) $ reverse lsh of (acs, []) -> ("0."++) . (replicate (-e-length acs) '0'++) . (reverse acs++) (acs, pcs) -> (reverse pcs++) . ('.':) . (reverse acs++) (hd:[]) -> (hd:) . ("e"++) . shows e (hd:lds) -> (hd:) . ('.':) . (lds++) . ("e"++) . shows (e + length lds) | e < 0 = case splitAt (-e) . reverse $ show m of (acs, []) -> ("0."++) . (replicate (-e-length acs) '0'++) . (reverse acs++) (acs, pcs) -> (reverse pcs++) . ('.':) . (reverse acs++) | otherwise = shows m . (replicate e '0'++) infixl 7 `unbiasedDiv` unbiasedDiv :: Integral a => a -> a -> a unbiasedDiv x y = (x + y`quot`2)`div`y instance Num FractionalLit where fromInteger = ExactRatio . fromInteger ExactRatio r₀ + ExactRatio r₁ = ExactRatio $ r₀ + r₁ DecimalFraction m e + ExactRatio r = DecimalFraction (m + round (r * 10^^(-e))) e DecimalFraction m₀ e₀ + DecimalFraction m₁ e₁ | e₀ <= e₁ = DecimalFraction (m₀`unbiasedDiv`10^(e₁ - e₀) + m₁) e₁ n + m = m + n ExactRatio r₀ * ExactRatio r₁ = ExactRatio $ r₀ * r₁ DecimalFraction m e * ExactRatio r = DecimalFraction ((m * numerator r)`unbiasedDiv`denominator r) e DecimalFraction m₀ e₀ * DecimalFraction m₁ e₁ = DecimalFraction (m₀*m₁) (e₀+e₁) n * m = m * n negate (ExactRatio r) = ExactRatio $ -r negate (DecimalFraction m e) = DecimalFraction (-m) e abs (ExactRatio r) = ExactRatio $ abs r abs (DecimalFraction m e) = DecimalFraction (abs m) e signum (ExactRatio r) = ExactRatio $ signum r signum (DecimalFraction m e) = ExactRatio . fromIntegral $ signum m -- | Despite the name, 'fromRational' should /not/ be used to promote a 'Rational' -- value to 'FractionalLit', because that method contains the heuristic which interprets -- decimal\/scientific literals (which in Haskell are, perhaps unfortunately, always -- desugared through 'fromRational'). Use '/' or ':%' instead, to define exact-ratio -- values. instance Fractional FractionalLit where fromRational r | r < 0 = negate $ fromRational (-r) | denominator r == 1 = goI 0 0 $ numerator r | otherwise = goF 0 0 $ denominator r where goI n2 n5 u | (u', 0) <- u`divMod`2 = goI (n2+1) n5 u' | (u', 0) <- u`divMod`5 = goI n2 (n5+1) u' | n2 > 3 && n5 > n2 = DecimalFraction (u*5^(n5-n2)) n2 | n5 > 3 && n2 >= n5 = DecimalFraction (u*2^(n2-n5)) n5 | otherwise = ExactRatio r goF 0 0 1 = ExactRatio r goF n2 n5 1 | n2>n5 = DecimalFraction (numerator r*5^(n2-n5)) (-n2) | otherwise = DecimalFraction (numerator r*2^(n5-n2)) (-n5) goF n2 n5 d | (d', 0) <- d`divMod`2 = goF (n2+1) n5 d' | (d', 0) <- d`divMod`5 = goF n2 (n5+1) d' | otherwise = ExactRatio r ExactRatio r₀ / ExactRatio r₁ = ExactRatio $ r₀ / r₁ DecimalFraction m e / ExactRatio r = DecimalFraction ((m * denominator r)`unbiasedDiv`numerator r) e ExactRatio r / DecimalFraction m e = DecimalFraction (round $ r * 10^dp / fromIntegral m) (-dp-e) where dp = ceiling . logBase 10 $ fromIntegral m DecimalFraction m₀ e₀ / DecimalFraction m₁ e₁ = DecimalFraction ((m₀*10^dp)`unbiasedDiv`m₁) (e₀-e₁-dp) where dp = ceiling . logBase 10 $ fromIntegral m₁