Add a faster, custom impl of double parser

This commit is contained in:
Harendra Kumar 2023-07-26 23:12:31 +05:30
parent 2e489ea194
commit b085507aec
3 changed files with 309 additions and 102 deletions

View File

@ -10,6 +10,11 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-orphans #-}
#undef FUSION_CHECK
#ifdef FUSION_CHECK
{-# OPTIONS_GHC -ddump-simpl -ddump-to-file -dsuppress-all #-}
#endif
module Main
(
main
@ -22,28 +27,18 @@ import Streamly.Internal.Data.Stream (Stream)
import Prelude hiding
(any, all, take, sequence, sequence_, sequenceA, takeWhile, dropWhile)
import qualified Streamly.Internal.Data.Fold as Fold
import qualified Streamly.Data.Array as Array
import qualified Streamly.Data.Stream as Stream
import qualified Streamly.Data.Unfold as Unfold
import qualified Streamly.Internal.Unicode.Parser as PRU
import Gauge hiding (env)
import Streamly.Benchmark.Common
import Streamly.Benchmark.Common.Handle
{-# INLINE sourceUnfoldrM #-}
sourceUnfoldrM :: Monad m => Int -> Int -> Stream m Int
sourceUnfoldrM value n = Stream.unfoldrM step n
where
step cnt =
if cnt > n + value
then return Nothing
else return (Just (cnt, cnt + 1))
runParser :: Int -> (Stream IO Char -> IO a) -> IO ()
runParser count p = do
let v = "+123456789.123456789e-123"
let s = Stream.unfold Unfold.fromList v
let !(arr :: Array.Array Char) = Array.fromListN (length v) v
let s = Stream.unfold Array.reader arr
replicateM_ count (p s)
-- | Takes a fold method, and uses it with a default source.
@ -51,10 +46,23 @@ runParser count p = do
benchIOSink :: Int -> String -> (Stream IO Char -> IO b) -> Benchmark
benchIOSink value name f = bench name $ nfIO $ runParser value f
{-# INLINE doubleParser #-}
doubleParser :: Monad m => Stream m Char -> m (Either ParseError (Int, Int))
doubleParser = Stream.parse PRU.doubleParser
{-# INLINE number #-}
number :: Monad m => Stream m Char -> m (Either ParseError (Integer, Int))
number = Stream.parse PRU.number
{-# INLINE double #-}
double :: Monad m => Stream m Char -> m (Either ParseError Double)
double = Stream.parse PRU.double
{-# INLINE numberDouble #-}
numberDouble :: Monad m => Stream m Char -> m (Either ParseError Double)
numberDouble = Stream.parse p
where p = uncurry PRU.mkDouble <$> PRU.number
-------------------------------------------------------------------------------
-- Benchmarks
-------------------------------------------------------------------------------
@ -69,7 +77,10 @@ instance NFData ParseError where
o_n_heap_serial :: Int -> [Benchmark]
o_n_heap_serial value =
[
benchIOSink value "double" double
benchIOSink value "doubleParser" doubleParser
, benchIOSink value "number" number
, benchIOSink value "double (doubleParser)" double
, benchIOSink value "double (number)" numberDouble
]
-------------------------------------------------------------------------------
@ -78,13 +89,19 @@ o_n_heap_serial value =
main :: IO ()
main = do
env <- mkHandleBenchEnv
runWithCLIOptsEnv defaultStreamSize alloc (allBenchmarks env)
#ifndef FUSION_CHECK
runWithCLIOpts defaultStreamSize allBenchmarks
where
alloc value = Stream.fold Fold.toList $ Stream.chunksOf 100 $ sourceUnfoldrM value 0
allBenchmarks _ _ value =
allBenchmarks value =
[ bgroup (o_n_heap_prefix moduleName) (o_n_heap_serial value)
]
#else
-- Enable FUSION_CHECK macro at the beginning of the file
-- Enable one benchmark below, and run the benchmark
-- Check the .dump-simpl output
let value = 100000
runParser value double
return ()
#endif

View File

@ -48,6 +48,7 @@ module Streamly.Internal.Unicode.Parser
-- * Numeric
, signed
, number
, doubleParser
, double
, decimal
, hexadecimal
@ -58,7 +59,7 @@ module Streamly.Internal.Unicode.Parser
where
import Control.Applicative (Alternative(..))
import Data.Bits (Bits, (.|.), shiftL)
import Data.Bits (Bits, (.|.), shiftL, (.&.))
import Data.Char (ord)
import Data.Ratio ((%))
import Fusion.Plugin.Types (Fuse(..))
@ -269,6 +270,7 @@ hexadecimal = Parser.takeWhile1 isHexDigit (Fold.foldl' step 0)
signed :: (Num a, Monad m) => Parser Char m a -> Parser Char m a
signed p = (negate <$> (char '-' *> p)) <|> (char '+' *> p) <|> p
-- XXX Change Multiplier to Sign
type Multiplier = Int
-- XXX We can use Int instead of Integer to make it twice as fast. But then we
@ -376,12 +378,12 @@ number = Parser (\s a -> return $ step s a) initial (return . extract)
let num = ord val - 48
if num >= 0 && num <= 9
then Partial 0 $ SPAfterExponent multiplier buf decimalPlaces 1 num
else Error $ exitSPInitial $ show val
else Done 2 $ exitSPAfterDot multiplier buf decimalPlaces
step (SPExponentWithSign mult buf decimalPlaces powerMult) val =
let num = ord val - 48
in if num >= 0 && num <= 9
then Partial 0 $ SPAfterExponent mult buf decimalPlaces powerMult num
else Error $ exitSPSign $ show val
else Done 3 $ exitSPAfterDot mult buf decimalPlaces
step (SPAfterExponent mult num decimalPlaces powerMult buf) val =
let n = ord val - 48
in if n >= 0 && n <= 9
@ -407,9 +409,150 @@ number = Parser (\s a -> return $ step s a) initial (return . extract)
extract (SPAfterExponent mult num decimalPlaces powerMult powerNum) =
Done 0 $ exitSPAfterExponent mult num decimalPlaces powerMult powerNum
type MantissaInt = Int
type OverflowPower = Int
{-# ANN type DoubleParseState Fuse #-}
data DoubleParseState
= DPInitial
| DPSign !Multiplier
| DPAfterSign !Multiplier !MantissaInt !OverflowPower
| DPDot !Multiplier !MantissaInt !OverflowPower
| DPAfterDot !Multiplier !MantissaInt !OverflowPower
| DPExponent !Multiplier !MantissaInt !OverflowPower
| DPExponentWithSign !Multiplier !MantissaInt !OverflowPower !PowerMultiplier
| DPAfterExponent !Multiplier !MantissaInt !OverflowPower !PowerMultiplier !Power
-- | A fast, custom parser for double precision flaoting point numbers. Returns
-- (mantissa, exponent) tuple. This is much faster than 'number' because it
-- assumes the number will fit in a 'Double' type and uses 'Int' representation
-- to store mantissa.
--
-- Number larger than 'Double' may overflow. Int overflow is not checked in the
-- exponent.
--
{-# INLINE doubleParser #-}
doubleParser :: Monad m => Parser Char m (Int, Int)
doubleParser = Parser (\s a -> return $ step s a) initial (return . extract)
where
-- XXX Assuming Int = Int64
-- Up to 58 bits Int won't overflow
-- ghci> (2^59-1)*10+9 :: Int
-- 5764607523034234879
mask :: Word
mask = 0x7c00000000000000 -- 58 bits, ignore the sign bit
{-# INLINE combineNum #-}
combineNum :: Int -> Int -> Int -> (Int, Int)
combineNum mantissa power num =
if fromIntegral mantissa .&. mask == 0
then (mantissa * 10 + num, power)
else (mantissa, power + 1)
{-# INLINE initial #-}
initial = pure $ IPartial DPInitial
exitDPInitial msg =
"number: expecting sign or decimal digit, got " ++ msg
exitDPSign msg =
"number: expecting decimal digit, got " ++ msg
exitDPAfterSign multiplier num opower = (fromIntegral multiplier * num, opower)
exitDPAfterDot multiplier num opow =
(fromIntegral multiplier * num , opow)
exitDPAfterExponent mult num opow powerMult powerNum =
(fromIntegral mult * num, opow + powerMult * powerNum)
{-# INLINE step #-}
step DPInitial val =
case val of
'+' -> Continue 0 (DPSign 1)
'-' -> Continue 0 $ (DPSign (-1))
_ -> do
let num = ord val - 48
if num >= 0 && num <= 9
then Partial 0 $ DPAfterSign 1 num 0
else Error $ exitDPInitial $ show val
step (DPSign multiplier) val =
let num = ord val - 48
in if num >= 0 && num <= 9
then Partial 0 $ DPAfterSign multiplier num 0
else Error $ exitDPSign $ show val
step (DPAfterSign multiplier buf opower) val =
case val of
'.' -> Continue 0 $ DPDot multiplier buf opower
'e' -> Continue 0 $ DPExponent multiplier buf opower
'E' -> Continue 0 $ DPExponent multiplier buf opower
_ ->
let num = ord val - 48
in if num >= 0 && num <= 9
then
let (buf1, power1) = combineNum buf opower num
in Partial 0
$ DPAfterSign multiplier buf1 power1
else Done 1 $ exitDPAfterSign multiplier buf opower
step (DPDot multiplier buf opower) val =
let num = ord val - 48
in if num >= 0 && num <= 9
then
let (buf1, power1) = combineNum buf opower num
in Partial 0 $ DPAfterDot multiplier buf1 (power1 - 1)
else Done 2 $ exitDPAfterSign multiplier buf opower
step (DPAfterDot multiplier buf opower) val =
case val of
'e' -> Continue 0 $ DPExponent multiplier buf opower
'E' -> Continue 0 $ DPExponent multiplier buf opower
_ ->
let num = ord val - 48
in if num >= 0 && num <= 9
then
let (buf1, power1) = combineNum buf opower num
in Partial 0 $ DPAfterDot multiplier buf1 (power1 - 1)
else Done 1 $ exitDPAfterDot multiplier buf opower
step (DPExponent multiplier buf opower) val =
case val of
'+' -> Continue 0 (DPExponentWithSign multiplier buf opower 1)
'-' -> Continue 0 (DPExponentWithSign multiplier buf opower (-1))
_ -> do
let num = ord val - 48
if num >= 0 && num <= 9
then Partial 0 $ DPAfterExponent multiplier buf opower 1 num
else Done 2 $ exitDPAfterDot multiplier buf opower
step (DPExponentWithSign mult buf opower powerMult) val =
let num = ord val - 48
in if num >= 0 && num <= 9
then Partial 0 $ DPAfterExponent mult buf opower powerMult num
else Done 3 $ exitDPAfterDot mult buf opower
step (DPAfterExponent mult num opower powerMult buf) val =
let n = ord val - 48
in if n >= 0 && n <= 9
then
Partial 0
$ DPAfterExponent mult num opower powerMult (buf * 10 + n)
else Done 1 $ exitDPAfterExponent mult num opower powerMult buf
{-# INLINE extract #-}
extract DPInitial = Error $ exitDPInitial "end of input"
extract (DPSign _) = Error $ exitDPSign "end of input"
extract (DPAfterSign mult num opow) = Done 0 $ exitDPAfterSign mult num opow
extract (DPDot mult num opow) = Done 1 $ exitDPAfterSign mult num opow
extract (DPAfterDot mult num opow) =
Done 0 $ exitDPAfterDot mult num opow
extract (DPExponent mult num opow) =
Done 1 $ exitDPAfterDot mult num opow
extract (DPExponentWithSign mult num opow _) =
Done 2 $ exitDPAfterDot mult num opow
extract (DPAfterExponent mult num opow powerMult powerNum) =
Done 0 $ exitDPAfterExponent mult num opow powerMult powerNum
-- XXX We can have a `realFloat` parser instead to parse any RealFloat value.
-- And a integral parser to read any integral value.
-- XXX This is very expensive, takes much more time than the rest of the
-- parsing. Need to look into fromRational.
-- | @mkDouble mantissa exponent@ converts a mantissa and exponent to a
-- 'Double' value equivalent to @mantissa * 10^exponent@. It does not check for
-- overflow, powers more than 308 will overflow.
@ -417,7 +560,7 @@ number = Parser (\s a -> return $ step s a) initial (return . extract)
mkDouble :: Integer -> Int -> Double
mkDouble mantissa power =
if power > 0
then fromRational (fromIntegral (mantissa * 10 ^ power))
then fromRational ((mantissa * 10 ^ power) % 1)
else fromRational (mantissa % 10 ^ (-power))
-- | Parse a decimal 'Double' value. This parser accepts an optional sign (+ or
@ -473,4 +616,4 @@ mkDouble mantissa power =
--
{-# INLINE double #-}
double :: Monad m => Parser Char m Double
double = uncurry mkDouble <$> number
double = fmap (\(m,e) -> mkDouble (fromIntegral m) e) doubleParser

View File

@ -6,7 +6,7 @@ where
import Control.Monad.Identity (Identity(runIdentity))
import Debug.Trace (trace)
import Streamly.Internal.Data.Parser (ParseError (..))
import Streamly.Internal.Data.Parser (ParseError (..), Parser)
import Streamly.Test.Common (chooseDouble)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck
@ -89,28 +89,137 @@ double s d = monadicIO $ do
case x of
Right val -> if val == d
then assert (val == d)
else trace ("Expectedc = " ++ show d ++ " Got = "++ show val) (assert (val == d))
else trace ("Expected = " ++ show d ++ " Got = "++ show val) (assert (val == d))
Left (ParseError _) -> assert False
doubleErr :: String -> String -> Property
doubleErr s msg = monadicIO $ do
x <- run $ doubleParser s
numberP :: Monad m => Parser Char m Double
numberP = uncurry Parser.mkDouble <$> Parser.number
numberParser :: String -> IO (Either ParseError Double)
numberParser = Stream.parse numberP . Stream.fromList
number :: String -> Double -> Property
number s d = monadicIO $ do
x <- run $ numberParser s
case x of
Right val -> if val == d
then assert (val == d)
else trace ("Expected = " ++ show d ++ " Got = "++ show val) (assert (val == d))
Left (ParseError _) -> assert False
doubleErr :: (String -> IO (Either ParseError Double)) -> String -> String -> Property
doubleErr f s msg = monadicIO $ do
x <- run $ f s
case x of
Right _ -> assert False
Left (ParseError err) -> if err == msg
then assert (err == msg)
else trace err (assert (err == msg))
remainingStream :: String -> [String]
remainingStream x =
remainingStreamDouble :: String -> [String]
remainingStreamDouble x =
let f = Stream.parseBreak Unicode.double . Stream.fromList
in concatMap (Stream.toList . snd) (f x)
afterParse :: String -> String -> Property
afterParse si so = monadicIO $ do
let x = remainingStream si
remainingStreamNumber :: String -> [String]
remainingStreamNumber x =
let f = Stream.parseBreak numberP . Stream.fromList
in concatMap (Stream.toList . snd) (f x)
afterParse :: (String -> [String]) -> String -> String -> Property
afterParse f si so = monadicIO $ do
let x = f si
in assert (x == [so])
testParser :: String -> (String -> Double -> Property) -> H.SpecWith ()
testParser desc f = do
-- XXX We can combine the parser tests and remaining stream tests in the
-- same test.
H.describe ("Unicode Parser " ++ desc) $ do
prop "double \"00.00\"" $ f "00.00" 0.0
prop "double \"00.00e00\"" $ f "00.00e00" 0.0
prop "double \"4\" 4.0" $ f "4" 4.0
prop "double \"00.3\" 0.3" $ f "00.3" 0.3
prop "double \"0.003\" 3.0e-3" $ f "0.003" 3.0e-3
prop "double \"44.56.67\" 44.56" $ f "44.56.67" 44.56
prop "double \"0.0.00\" 0.0" $ f "0.0.00" 0.0
prop "double \"44.56-78\" 44.56" $ f "44.56-78" 44.56
prop "double \"44-\" 44.0" $ f "44-" 44.0
prop "double \"44-56\" 44.0" $ f "44-56" 44.0
prop "double \"44+\" 44.0" $ f "44+" 44.0
prop "double \"44+56\" 44.0" $ f "44+56" 44.0
prop "double \"44u\" 44.0" $ f "44u" 44.0
prop "double \"+123.345\" 123.345" $ f "+123.345" 123.345
prop "double \"-123.345\" (-123.345)" $ f "-123.345" (-123.345)
prop "double \"1e\"" $ f "1e" 1.0
prop "double \"1e+\"" $ f "1e+" 1.0
prop "double \"1ex\"" $ f "1ex" 1.0
prop "double \"1e+x\"" $ f "1e+x" 1.0
prop "double \"1e+0\"" $ f "1e+0" 1.0
prop "double \"1e-0\"" $ f "1e-0" 1.0
prop "double \"1.0e-1\"" $ f "1.0e-1" 0.1
prop "double \"1.0e+1\"" $ f "1.0e+1" 10.0
prop "double \"1.0e1\"" $ f "1.0e1" 10.0
prop "double \"1.0e+309\"" $ f "1.0e+309" 1.0e309
prop "double \"1.0e-309\"" $ f "1.0e-309" 1.0e-309
prop "double \"1.0e1.\"" $ f "1.0e1." 10.0
prop "double \"1.0e1+\"" $ f "1.0e1+" 10.0
prop "double \"1.0e1-\"" $ f "1.0e1-" 10.0
prop "double \"1.0e1e\"" $ f "1.0e1e" 10.0
prop "double \"1.0E+1\"" $ f "1.0E+1" 10.0
prop "double \"9223372036854775806\"" $ f "9223372036854775806" 9.223372036854776e18
-- maxBound :: Int
prop "double \"9223372036854775807\"" $ f "9223372036854775807" 9.223372036854776e18
prop "double \"9223372036854775808\"" $ f "9223372036854775808" 9.223372036854776e18
testParseErr :: String -> (String -> IO (Either ParseError Double)) -> H.SpecWith ()
testParseErr desc f = do
H.describe ("Unicode Parser Error " ++ desc) $ do
prop "double \"\" Error"
$ doubleErr f "" "number: expecting sign or decimal digit, got end of input"
prop "double \"a\" Error"
$ doubleErr f "a" "number: expecting sign or decimal digit, got 'a'"
prop "double \".4\" Error"
$ doubleErr f ".4" "number: expecting sign or decimal digit, got '.'"
prop "double \".\" Error"
$ doubleErr f "." "number: expecting sign or decimal digit, got '.'"
prop "double \"..\" Error"
$ doubleErr f ".." "number: expecting sign or decimal digit, got '.'"
prop "double \"-\" Error"
$ doubleErr f "-" "number: expecting decimal digit, got end of input"
prop "double \"+\" Error"
$ doubleErr f "+" "number: expecting decimal digit, got end of input"
prop "double \"++\" Error"
$ doubleErr f "++" "number: expecting decimal digit, got '+'"
testLeftOver :: String -> (String -> [String]) -> H.SpecWith ()
testLeftOver desc f = do
H.describe ("Unicode Parser leftover " ++ desc) $ do
prop "afterParse \"4.\" \".\"" $ afterParse f "4." "."
prop "afterParse \"4..\" \"..\"" $ afterParse f "4.." ".."
prop "afterParse \"4-\" \"-\"" $ afterParse f "4-" "-"
prop "afterParse \"4--\" \"--\"" $ afterParse f "4--" "--"
prop "afterParse \"4.9abc\" \"abc\"" $ afterParse f "4.9abc" "abc"
prop "afterParse \"4.9ex\"" $ afterParse f "4.9ex" "ex"
prop "afterParse \"4.9e+x\"" $ afterParse f "4.9e+x" "e+x"
prop "afterParse \"4.9\" \"\"" $ afterParse f "4.9" ""
prop "afterParse \"+4.9\" \"\"" $ afterParse f "+4.9" ""
prop "afterParse \"-4.9\" \"\"" $ afterParse f "-4.9" ""
prop "afterParse \"4.9.\" \".\"" $ afterParse f "4.9." "."
prop "afterParse \"4.9..\" \"..\"" $ afterParse f "4.9.." ".."
prop "afterParse \"4.9...\" \"...\"" $ afterParse f "4.9..." "..."
prop "afterParse \"4.9+\" \"+\"" $ afterParse f "4.9+" "+"
prop "afterParse \"4.9++\" \"++\"" $ afterParse f "4.9++" "++"
prop "afterParse \"4.9+++\" \"+++\"" $ afterParse f "4.9+++" "+++"
prop "afterParse \"4.9-\" \"-\"" $ afterParse f "4.9-" "-"
prop "afterParse \"4.9--\" \"--\"" $ afterParse f "4.9--" "--"
prop "afterParse \"4.9---\" \"---\"" $ afterParse f "4.9---" "---"
prop "afterParse \"\" \"\"" $ afterParse f "" ""
prop "afterParse \".\" \".\"" $ afterParse f "." "."
prop "afterParse \"..\" \"..\"" $ afterParse f ".." ".."
prop "afterParse \"+\" \"+\"" $ afterParse f "+" "+"
prop "afterParse \"++\" \"++\"" $ afterParse f "++" "++"
moduleName :: String
moduleName = "Unicode.Parser"
@ -118,74 +227,12 @@ main :: IO ()
main = do
H.hspec
$ H.describe moduleName $ do
H.describe "Unicode Parser" $ do
prop "double \"4\" 4.0" $ double "4" 4.0
prop "double \"00.3\" 0.3" $ double "00.3" 0.3
prop "double \"0.003\" 3.0e-3" $ double "0.003" 3.0e-3
prop "double \"44.56.67\" 44.56" $ double "44.56.67" 44.56
prop "double \"0.0.00\" 0.0" $ double "0.0.00" 0.0
prop "double \"44.56-78\" 44.56" $ double "44.56-78" 44.56
prop "double \"44-\" 44.0" $ double "44-" 44.0
prop "double \"44-56\" 44.0" $ double "44-56" 44.0
prop "double \"44+\" 44.0" $ double "44+" 44.0
prop "double \"44+56\" 44.0" $ double "44+56" 44.0
prop "double \"44u\" 44.0" $ double "44u" 44.0
prop "double \"+123.345\" 123.345" $ double "+123.345" 123.345
prop "double \"-123.345\" (-123.345)" $ double "-123.345" (-123.345)
prop "double \"1e\"" $ double "1e" 1.0
prop "double \"1e+\"" $ double "1e+" 1.0
prop "double \"1e+0\"" $ double "1e+0" 1.0
prop "double \"1e-0\"" $ double "1e-0" 1.0
prop "double \"1.0e-1\"" $ double "1.0e-1" 0.1
prop "double \"1.0e+1\"" $ double "1.0e+1" 10.0
prop "double \"1.0e1\"" $ double "1.0e1" 10.0
prop "double \"1.0e+309\"" $ double "1.0e+309" 1.0e309
prop "double \"1.0e-309\"" $ double "1.0e-309" 1.0e-309
prop "double \"1.0e1.\"" $ double "1.0e1." 10.0
prop "double \"1.0e1+\"" $ double "1.0e1+" 10.0
prop "double \"1.0e1-\"" $ double "1.0e1-" 10.0
prop "double \"1.0e1e\"" $ double "1.0e1e" 10.0
prop "double \"1.0E+1\"" $ double "1.0E+1" 10.0
prop "double \"\" Error"
$ doubleErr "" "number: expecting sign or decimal digit, got end of input"
prop "double \"a\" Error"
$ doubleErr "a" "number: expecting sign or decimal digit, got 'a'"
prop "double \".4\" Error"
$ doubleErr ".4" "number: expecting sign or decimal digit, got '.'"
prop "double \".\" Error"
$ doubleErr "." "number: expecting sign or decimal digit, got '.'"
prop "double \"..\" Error"
$ doubleErr ".." "number: expecting sign or decimal digit, got '.'"
prop "double \"-\" Error"
$ doubleErr "-" "number: expecting decimal digit, got end of input"
prop "double \"+\" Error"
$ doubleErr "+" "number: expecting decimal digit, got end of input"
prop "double \"++\" Error"
$ doubleErr "++" "number: expecting decimal digit, got '+'"
prop "afterParse \"4.\" \".\"" $ afterParse "4." "."
prop "afterParse \"4..\" \"..\"" $ afterParse "4.." ".."
prop "afterParse \"4-\" \"-\"" $ afterParse "4-" "-"
prop "afterParse \"4--\" \"--\"" $ afterParse "4--" "--"
prop "afterParse \"4.9abc\" \"abc\"" $ afterParse "4.9abc" "abc"
prop "afterParse \"4.9\" \"\"" $ afterParse "4.9" ""
prop "afterParse \"+4.9\" \"\"" $ afterParse "+4.9" ""
prop "afterParse \"-4.9\" \"\"" $ afterParse "-4.9" ""
prop "afterParse \"4.9.\" \".\"" $ afterParse "4.9." "."
prop "afterParse \"4.9..\" \"..\"" $ afterParse "4.9.." ".."
prop "afterParse \"4.9...\" \"...\"" $ afterParse "4.9..." "..."
prop "afterParse \"4.9+\" \"+\"" $ afterParse "4.9+" "+"
prop "afterParse \"4.9++\" \"++\"" $ afterParse "4.9++" "++"
prop "afterParse \"4.9+++\" \"+++\"" $ afterParse "4.9+++" "+++"
prop "afterParse \"4.9-\" \"-\"" $ afterParse "4.9-" "-"
prop "afterParse \"4.9--\" \"--\"" $ afterParse "4.9--" "--"
prop "afterParse \"4.9---\" \"---\"" $ afterParse "4.9---" "---"
prop "afterParse \"\" \"\"" $ afterParse "" ""
prop "afterParse \".\" \".\"" $ afterParse "." "."
prop "afterParse \"..\" \"..\"" $ afterParse ".." ".."
prop "afterParse \"+\" \"+\"" $ afterParse "+" "+"
prop "afterParse \"++\" \"++\"" $ afterParse "++" "++"
testParser "double" double
testParser "number" number
testParseErr "double" doubleParser
testParseErr "number" numberParser
testLeftOver "double" remainingStreamDouble
testLeftOver "number" remainingStreamNumber
H.describe "Scientific parser property test" $ do
prop "Exponent format" scientificExpFP
prop "Decimal format" scientificFixFP