dataframe-parsing-1.0.2.0: tests/Properties/FastParsing.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
{- | QuickCheck parity properties for "DataFrame.Internal.Parsing.Fast".
Every fast slice parser must agree with the existing reference parser
(`readByteStringInt` / `readByteStringDouble` / `readByteStringBool` /
`readByteStringDate` / `isNullishBS`) on arbitrary byte fields, with
Doubles compared bit-for-bit ('castDoubleToWord64').
-}
module Properties.FastParsing (tests) where
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C
import qualified Data.Text.Encoding as TE
import Data.Word (Word64, Word8)
import GHC.Float (castDoubleToWord64)
import Test.QuickCheck
import DataFrame.Internal.Parsing (
isNullish,
isNullishBS,
readByteStringBool,
readByteStringDate,
readByteStringDouble,
readByteStringInt,
)
import DataFrame.Internal.Parsing.Fast
-- | Bytes 'Data.ByteString.Char8.strip' removes (probed over all 256).
stripBytes :: [Word8]
stripBytes = [0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x20, 0xA0]
genPad :: Gen BS.ByteString
genPad =
frequency
[ (4, pure BS.empty)
, (3, BS.pack <$> resize 3 (listOf (elements stripBytes)))
]
genDigits :: Int -> Int -> Gen BS.ByteString
genDigits lo hi = do
n <- chooseInt (lo, hi)
C.pack <$> vectorOf n (elements "0123456789")
-- | Digit runs that probe the 18/19/20-digit mantissa window edges.
genDigitRun :: Gen BS.ByteString
genDigitRun =
frequency
[ (6, genDigits 1 17)
, (3, genDigits 18 21)
, (1, genDigits 22 40)
, (2, BS.append <$> genDigits 1 6 <*> pure "")
,
( 1
, (BS.append . BS.pack . flip replicate 0x30 <$> chooseInt (1, 25))
<*> genDigits 0 19
)
]
genNumeric :: Gen BS.ByteString
genNumeric = do
sign <- elements ["", "-", "+"]
whole <- frequency [(8, genDigitRun), (1, pure "")]
frac <-
frequency
[ (3, pure "")
, (4, BS.append "." <$> genDigitRun)
, (1, pure ".")
]
ex <-
frequency
[ (4, pure "")
, (3, genExponent)
]
pre <- genPad
post <- genPad
pure (BS.concat [pre, sign, whole, frac, ex, post])
where
genExponent = do
e <- elements ["e", "E"]
s <- elements ["", "-", "+"]
ds <-
frequency
[ (6, genDigits 1 3)
, (2, genDigits 4 25)
, (1, pure "")
]
pure (BS.concat [e, s, ds])
genToken :: Gen BS.ByteString
genToken = do
tok <-
elements @BS.ByteString
[ "Nothing"
, "NULL"
, ""
, " "
, "nan"
, "null"
, "N/A"
, "NaN"
, "NAN"
, "NA"
, "True"
, "true"
, "TRUE"
, "False"
, "false"
, "FALSE"
, "Infinity"
, "-Infinity"
, "na"
, "Null"
, "N/a"
, "TRUe"
, "FALSe"
]
frequency [(4, pure tok), (1, flip BS.append tok <$> genPad)]
genDateLike :: Gen BS.ByteString
genDateLike = do
y <- chooseInt (0, 12000)
m <- chooseInt (0, 15)
d <- chooseInt (0, 35)
sep <- elements ["-", "/", "-"]
let pad2 n = (if n < 10 then "0" else "") ++ show n
body <-
elements
[ show y ++ "-" ++ pad2 m ++ "-" ++ pad2 d
, show y ++ sep ++ show m ++ sep ++ show d
, pad2 y ++ "-" ++ pad2 m ++ "-" ++ pad2 d
]
pre <- genPad
post <- genPad
pure (BS.concat [pre, C.pack body, post])
genJunk :: Gen BS.ByteString
genJunk =
frequency
[ (3, C.pack <$> resize 12 (listOf (elements "0123456789.eE+- aZ_,")))
, (1, BS.pack <$> resize 12 (listOf arbitrary))
]
-- | An arbitrary CSV field, weighted towards adversarial numerics.
newtype Field = Field {unField :: BS.ByteString}
deriving (Eq, Show)
instance Arbitrary Field where
arbitrary =
Field
<$> frequency
[ (6, genNumeric)
, (2, genToken)
, (1, genDateLike)
, (2, genJunk)
]
shrink (Field b) = map (Field . BS.pack) (shrink (BS.unpack b))
bitsOf :: Maybe Double -> Maybe Word64
bitsOf = fmap castDoubleToWord64
-- | Embed a field in junk and parse it through the slice interface.
slicedOn ::
(BS.ByteString -> Int -> Int -> a) ->
BS.ByteString ->
BS.ByteString ->
BS.ByteString ->
a
slicedOn p pre f post =
p (BS.concat [pre, f, post]) (BS.length pre) (BS.length pre + BS.length f)
prop_intParity :: Field -> Property
prop_intParity (Field f) = parseIntField f === readByteStringInt f
prop_intShowRoundTrip :: Int -> Property
prop_intShowRoundTrip n = parseIntField (C.pack (show n)) === Just n
prop_intSlice :: Field -> Field -> Field -> Property
prop_intSlice (Field pre) (Field f) (Field post) =
slicedOn parseIntFieldSlice pre f post === readByteStringInt f
prop_doubleParity :: Field -> Property
prop_doubleParity (Field f) =
bitsOf (parseDoubleField f) === bitsOf (readByteStringDouble f)
prop_doubleShowRoundTrip :: Double -> Property
prop_doubleShowRoundTrip d =
let f = C.pack (show d)
in bitsOf (parseDoubleField f) === bitsOf (readByteStringDouble f)
prop_doubleSlice :: Field -> Field -> Field -> Property
prop_doubleSlice (Field pre) (Field f) (Field post) =
bitsOf (slicedOn parseDoubleFieldSlice pre f post)
=== bitsOf (readByteStringDouble f)
prop_boolParity :: Field -> Property
prop_boolParity (Field f) = parseBoolField f === readByteStringBool f
prop_boolSlice :: Field -> Field -> Field -> Property
prop_boolSlice (Field pre) (Field f) (Field post) =
slicedOn parseBoolFieldSlice pre f post === readByteStringBool f
prop_missingParity :: Field -> Property
prop_missingParity (Field f) = isMissingField f === isNullishBS f
prop_missingTextParity :: Field -> Property
prop_missingTextParity (Field f) =
isMissingField f === isNullish (TE.decodeUtf8Lenient f)
prop_missingSlice :: Field -> Field -> Field -> Property
prop_missingSlice (Field pre) (Field f) (Field post) =
slicedOn isMissingFieldSlice pre f post === isNullishBS f
prop_missingIn :: Field -> Property
prop_missingIn (Field f) =
isMissingFieldIn ["NA", "nope", ""] f
=== (f `elem` ["NA", "nope", ""])
prop_dateParity :: Field -> Property
prop_dateParity (Field f) =
parseDateField f === readByteStringDate "%Y-%m-%d" f
prop_dateSlice :: Field -> Field -> Field -> Property
prop_dateSlice (Field pre) (Field f) (Field post) =
slicedOn parseDateFieldSlice pre f post
=== readByteStringDate "%Y-%m-%d" f
tests :: [(String, Property)]
tests =
[ ("intParity", property prop_intParity)
, ("intShowRoundTrip", property prop_intShowRoundTrip)
, ("intSlice", property prop_intSlice)
, ("doubleParity", property prop_doubleParity)
, ("doubleShowRoundTrip", property prop_doubleShowRoundTrip)
, ("doubleSlice", property prop_doubleSlice)
, ("boolParity", property prop_boolParity)
, ("boolSlice", property prop_boolSlice)
, ("missingParity", property prop_missingParity)
, ("missingTextParity", property prop_missingTextParity)
, ("missingSlice", property prop_missingSlice)
, ("missingIn", property prop_missingIn)
, ("dateParity", property prop_dateParity)
, ("dateSlice", property prop_dateSlice)
]