scientific 0.3.2.2 → 0.3.3.0
raw patch · 4 files changed
+139/−32 lines, 4 filesdep +tasty-hunit
Dependencies added: tasty-hunit
Files
- changelog +7/−0
- scientific.cabal +2/−1
- src/Data/Scientific.hs +98/−26
- test/test.hs +32/−5
changelog view
@@ -1,6 +1,13 @@+0.3.3.0+ * Add the isFloating or isInteger predicates.+ Courtesy of Zejun Wu (@watashi).+ * Add the toRealFloat' and toBoundedInteger functions.+ Courtesy of Fujimura Daisuke (@fujimura).+ 0.3.2.2 * Enable package to link with integer-simple instead of integer-gmp using the -finteger-simple cabal flag.+ Courtesy of @k0ral. 0.3.2.1
scientific.cabal view
@@ -1,5 +1,5 @@ name: scientific-version: 0.3.2.2+version: 0.3.3.0 synopsis: Numbers represented using scientific notation description: @Data.Scientific@ provides a space efficient and arbitrary precision@@ -95,6 +95,7 @@ , base >= 4.3 && < 4.8 , tasty >= 0.5 && < 0.9 , tasty-ant-xml >= 1.0 && < 1.1+ , tasty-hunit >= 0.8 && < 0.9 , tasty-smallcheck >= 0.2 && < 0.9 , tasty-quickcheck >= 0.8 && < 0.9 , smallcheck >= 1.0 && < 1.2
src/Data/Scientific.hs view
@@ -38,6 +38,12 @@ -- to @'Integral's@ (like: 'Int') or @'RealFloat's@ (like: 'Double' or 'Float') -- will always be bounded by the target type. --+-- /WARNING:/ Although @Scientific@ is an instance of 'Fractional', the methods+-- are only partially defined! Specifically 'recip' and '/' will diverge+-- (i.e. loop and consume all space) when their outputs have an infinite decimal+-- expansion. 'fromRational' will diverge when the input 'Rational' has an+-- infinite decimal expansion.+-- -- This module is designed to be imported qualified: -- -- @import Data.Scientific as Scientific@@@ -51,10 +57,16 @@ , coefficient , base10Exponent + -- * Predicates+ , isFloating+ , isInteger+ -- * Conversions- , fromFloatDigits- , toRealFloat , floatingOrInteger+ , toRealFloat+ , toBoundedRealFloat+ , toBoundedInteger+ , fromFloatDigits -- * Pretty printing , formatScientific@@ -216,9 +228,9 @@ "realToFrac_toRealFloat_Float" realToFrac = toRealFloat :: Scientific -> Float #-} --- | /WARNING:/ 'recip' and '/' will diverge when their outputs have--- an infinite decimal expansion. 'fromRational' will diverge when the--- input 'Rational' has an infinite decimal expansion.+-- | /WARNING:/ 'recip' and '/' will diverge (i.e. loop and consume all space)+-- when their outputs have an infinite decimal expansion. 'fromRational' will+-- diverge when the input 'Rational' has an infinite decimal expansion. instance Fractional Scientific where recip = fromRational . recip . toRational {-# INLINE recip #-}@@ -326,7 +338,7 @@ -- space usage is bounded by the target type. -- -- For large negative exponents we check if the exponent is smaller--- than some limit (currently -1100). In that case we know that the+-- than some limit (currently -324). In that case we know that the -- scientific number is really small (unless the coefficient has many -- digits) so we can immediately return -1 for negative scientific -- numbers or 0 for positive numbers.@@ -422,21 +434,29 @@ go [] !c !n = Scientific c (e - n) go (d:ds) !c !n = go ds (c * 10 + fromIntegral d) (n + 1) --- | Convert a 'Scientific' number into a 'RealFloat' (like a 'Double'--- or a 'Float').+-- | Safely convert a 'Scientific' number into a 'RealFloat' (like a 'Double' or a+-- 'Float'). ----- Note that this function uses 'realToFrac'--- (@'fromRational' . 'toRational'@) internally but it guards against--- computing huge Integer magnitudes (@10^e@) that could fill up all--- space and crash your program.+-- Note that this function uses 'realToFrac' (@'fromRational' . 'toRational'@)+-- internally but it guards against computing huge Integer magnitudes (@10^e@)+-- that could fill up all space and crash your program. If the 'base10Exponent'+-- of the given 'Scientific' is too big or too small to be represented in the+-- target type, Infinity or 0 will be returned respectively. Use+-- 'toBoundedRealFloat' which explicitly handles this case by returning 'Left'. ----- Always prefer 'toRealFloat' over 'realToFrac' when converting from--- scientific numbers coming from an untrusted source.-toRealFloat :: forall a. (RealFloat a) => Scientific -> a-toRealFloat s@(Scientific c e)- | e > limit && e > hiLimit = sign (1/0) -- Infinity- | e < -limit && e < loLimit && e + d < loLimit = sign 0- | otherwise = realToFrac s+-- Always prefer 'toRealFloat' over 'realToFrac' when converting from scientific+-- numbers coming from an untrusted source.+toRealFloat :: (RealFloat a) => Scientific -> a+toRealFloat = either id id . toBoundedRealFloat++-- | Preciser version of `toRealFloat`. If the 'base10Exponent' of the given+-- 'Scientific' is too big or too small to be represented in the target type,+-- Infinity or 0 will be returned as 'Left'.+toBoundedRealFloat :: forall a. (RealFloat a) => Scientific -> Either a a+toBoundedRealFloat s@(Scientific c e)+ | e > limit && e > hiLimit = Left $ sign (1/0) -- Infinity+ | e < -limit && e < loLimit && e + d < loLimit = Left $ sign 0+ | otherwise = Right $ realToFrac s where (loLimit, hiLimit) = exponentLimits (undefined :: a) @@ -459,9 +479,44 @@ digits = floatDigits (undefined :: a) (lo, hi) = floatRange (undefined :: a) +-- | Convert a `Scientific` to a bounded integer.+--+-- If the given `Scientific` doesn't fit in the target representation, it will+-- return `Nothing`.+--+-- This function also guards against computing huge Integer magnitudes (@10^e@)+-- that could fill up all space and crash your program.+toBoundedInteger :: forall i. (Integral i, Bounded i) => Scientific -> Maybe i+toBoundedInteger s+ | integral = if dangerouslyBig+ then Nothing+ else if n < toInteger (minBound :: i) ||+ n > toInteger (maxBound :: i)+ then Nothing+ else Just $ fromInteger n+ | otherwise = Nothing+ where+ integral = e >= 0 || e' >= 0++ e = base10Exponent s+ e' = base10Exponent s'++ s' = normalize s++ dangerouslyBig = e > limit &&+ e > integerLog10' (max (abs $ toInteger (minBound :: i))+ (abs $ toInteger (maxBound :: i)))++ -- This should not be evaluated if the given Scientific is dangerouslyBig+ -- since it could consume all space and crash the process:+ n :: Integer+ n = toIntegral s'+ -- | @floatingOrInteger@ determines if the scientific is floating point -- or integer. In case it's floating-point the scientific is converted -- to the desired 'RealFloat' using 'toRealFloat'.+--+-- Also see: 'isFloating' or 'isInteger'. floatingOrInteger :: (RealFloat r, Integral i) => Scientific -> Either r i floatingOrInteger s | base10Exponent s >= 0 = Right (toIntegral s)@@ -472,6 +527,26 @@ ----------------------------------------------------------------------+-- Predicates+----------------------------------------------------------------------++-- | Return 'True' if the scientific is a floating point, 'False' otherwise.+--+-- Also see: 'floatingOrInteger'.+isFloating :: Scientific -> Bool+isFloating = not . isInteger++-- | Return 'True' if the scientific is an integer, 'False' otherwise.+--+-- Also see: 'floatingOrInteger'.+isInteger :: Scientific -> Bool+isInteger s = base10Exponent s >= 0 ||+ base10Exponent s' >= 0+ where+ s' = normalize s+++---------------------------------------------------------------------- -- Parsing ---------------------------------------------------------------------- @@ -640,13 +715,10 @@ -- -- then ----- (1) @n >= 1@------ (2) @x = 0.d1d2...dn * (10^^e)@------ (3) @0 <= di <= 9@------ (4) @null $ takeWhile (==0) $ reverse [d1,d2,...,dn]@+-- 1. @n >= 1@+-- 2. @x = 0.d1d2...dn * (10^^e)@+-- 3. @0 <= di <= 9@+-- 4. @null $ takeWhile (==0) $ reverse [d1,d2,...,dn]@ -- -- The last property means that the coefficient will be normalized, i.e. doesn't -- contain trailing zeros.
test/test.hs view
@@ -11,9 +11,11 @@ import Control.Applicative import Control.Monad+import Data.Int import Data.Scientific as Scientific import Test.Tasty import Test.Tasty.Runners.AntXML+import Test.Tasty.HUnit import qualified Test.SmallCheck as SC import qualified Test.SmallCheck.Series as SC import qualified Test.Tasty.SmallCheck as SC (testProperty)@@ -142,20 +144,45 @@ , testProperty "Integer == Right" $ \(i::Integer) -> (floatingOrInteger (fromInteger i) :: Either Double Integer) == Right i , smallQuick "Double == Left"- (\(d::Double) -> isFloating d SC.==>+ (\(d::Double) -> genericIsFloating d SC.==> (floatingOrInteger (realToFrac d) :: Either Double Integer) == Left d)- (\(d::Double) -> isFloating d QC.==>+ (\(d::Double) -> genericIsFloating d QC.==> (floatingOrInteger (realToFrac d) :: Either Double Integer) == Left d) ]+ , testGroup "toBoundedInteger"+ [ testProperty "correct conversion" $ \s ->+ case toBoundedInteger s :: Maybe Int64 of+ Just i -> i == (fromIntegral $ (coefficient s') * 10^(base10Exponent s'))+ where+ s' = normalize s+ Nothing -> isFloating s ||+ s < fromIntegral (minBound :: Int64) ||+ s > fromIntegral (maxBound :: Int64)+ ] ]+ , testGroup "toBoundedInteger"+ [ testGroup "to Int64" $+ [ testCase "succ of maxBound" $+ let i = succ . fromIntegral $ (maxBound :: Int64)+ s = scientific i 0+ in (toBoundedInteger s :: Maybe Int64) @?= Nothing+ , testCase "pred of minBound" $+ let i = pred . fromIntegral $ (minBound :: Int64)+ s = scientific i 0+ in (toBoundedInteger s :: Maybe Int64) @?= Nothing+ ]+ ]+ , testGroup "Predicates"+ [ testProperty "isFloating" $ \s -> isFloating s == genericIsFloating s+ , testProperty "isInteger" $ \s -> isInteger s == not (genericIsFloating s)+ ] ] testMain :: TestTree -> IO () testMain = defaultMainWithIngredients (antXMLRunner:defaultIngredients) -isFloating :: Double -> Bool-isFloating 0 = False-isFloating d = fromInteger (floor d :: Integer) /= d+genericIsFloating :: RealFrac a => a -> Bool+genericIsFloating a = fromInteger (floor a :: Integer) /= a conversionsProperties :: forall realFloat. ( RealFloat realFloat