dataframe-parsing-1.0.2.0: tests/Unit/FastParsing.hs
{-# LANGUAGE OverloadedStrings #-}
{- | Pinned adversarial cases for "DataFrame.Internal.Parsing.Fast".
Each case asserts parity with the reference parsers plus, where the
parser-semantics contract (benchmarks/csv/results/code-audit.md section 2)
pins a concrete value, the value itself.
-}
module Unit.FastParsing (tests) where
import qualified Data.ByteString as BS
import Data.Word (Word64)
import GHC.Float (castDoubleToWord64)
import Test.HUnit
import DataFrame.Internal.Parsing (
isNullishBS,
readByteStringBool,
readByteStringDate,
readByteStringDouble,
readByteStringInt,
)
import DataFrame.Internal.Parsing.Fast
bitsOf :: Maybe Double -> Maybe Word64
bitsOf = fmap castDoubleToWord64
intCases :: [BS.ByteString]
intCases =
[ ""
, "-"
, "+"
, "0"
, "-0"
, "042"
, " \t42\n "
, "- 42"
, "+42"
, "1 2"
, "9223372036854775807"
, "9223372036854775808"
, "-9223372036854775808"
, "-9223372036854775809"
, "00000000000000000000042"
, "18446744073709551616"
, "99999999999999999999999999"
, "12e3"
, "0x10"
, BS.pack [0xA0, 0x34, 0x32, 0xA0]
]
doubleCases :: [BS.ByteString]
doubleCases =
[ ".5"
, "+.5"
, "5."
, "5.e3"
, "1e3.5"
, "1_000"
, "1e+3"
, "1E-3"
, " 1.5 "
, "1e999"
, "-1e999"
, "0e999"
, "-0e999"
, "1e-999"
, "0.1"
, "-0"
, "-0.0"
, "2.2250738585072011e-308"
, "1.7976931348623157e308"
, "4.9406564584124654e-324"
, "2.225073858507201e-308"
, "0.30000000000000004"
, "9007199254740993"
, "9007199254740993.9007199254740993"
, "12345678901234567890"
, "12345678901234567890.12345678901234567890"
, "123456789012345678901234567890e-25"
, "1e00000000000000005"
, "1e-00000000000000005"
, "00000000000000000000001.5"
, "1.00000000000000000000005"
, "0.000000000000000000000000000005"
, "1e308"
, "1e309"
, "1e-308"
, "1e-309"
, "1e1023"
, "1e1024"
, "1e1025"
, "1e-1024"
, "1e-1025"
, "Infinity"
, "NaN"
, "1e"
, "1e+"
, "1e-"
, "1.5e "
, "1.2.3"
, "--5"
, "1e18446744073709551617"
]
pinnedDoubles :: [(BS.ByteString, Maybe Double)]
pinnedDoubles =
[ ("1e+3", Just 1000)
, (" 1.5 ", Just 1.5)
, ("1e999", Just (1 / 0))
, ("1e-999", Just 0)
, (".5", Nothing)
, ("5.", Nothing)
, ("1e3.5", Nothing)
, ("1_000", Nothing)
, ("Infinity", Nothing)
, ("NaN", Nothing)
, -- The reference parser is NOT correctly rounded here; bit-exact
-- parity means we must reproduce its 2.2250738585072e-308.
("2.2250738585072011e-308", Just 2.2250738585072e-308)
]
boolCases :: [BS.ByteString]
boolCases =
[ "True"
, "true"
, "TRUE"
, "False"
, "false"
, "FALSE"
, " True"
, "True "
, "TRUe"
, "tRue"
, "FALSe"
, ""
, "T"
, "Truee"
]
missingCases :: [BS.ByteString]
missingCases =
[ "Nothing"
, "NULL"
, ""
, " "
, "nan"
, "null"
, "N/A"
, "NaN"
, "NAN"
, "NA"
, "na"
, "Na"
, "Null"
, "N/a"
, " "
, "NULL "
, " NA"
, "Nothing!"
, "nothing"
, "n"
, "N"
]
dateCases :: [BS.ByteString]
dateCases =
[ "2024-02-29"
, "2023-02-29"
, "2023-02-30"
, "2023-13-01"
, "2023-00-10"
, "2023-01-00"
, "2023-1-2"
, " 2024-02-29 "
, "0001-01-01"
, "0000-01-01"
, "10000-01-01"
, "2023/01/02"
, "2023-01-023"
, ""
]
parityCase ::
(Eq a, Show a) =>
String -> (BS.ByteString -> a) -> (BS.ByteString -> a) -> BS.ByteString -> Test
parityCase name fast ref input =
TestLabel (name ++ ": " ++ show input) . TestCase $
assertEqual (name ++ " parity on " ++ show input) (ref input) (fast input)
pinnedCase :: BS.ByteString -> Maybe Double -> Test
pinnedCase input expected =
TestLabel ("pinned double: " ++ show input) . TestCase $
assertEqual
("pinned value for " ++ show input)
(bitsOf expected)
(bitsOf (parseDoubleField input))
tests :: [Test]
tests =
map (parityCase "int" parseIntField readByteStringInt) intCases
++ map
(parityCase "double" (bitsOf . parseDoubleField) (bitsOf . readByteStringDouble))
doubleCases
++ map (uncurry pinnedCase) pinnedDoubles
++ map (parityCase "bool" parseBoolField readByteStringBool) boolCases
++ map (parityCase "missing" isMissingField isNullishBS) missingCases
++ map (parityCase "date" parseDateField (readByteStringDate "%Y-%m-%d")) dateCases
++ [ TestLabel "int maxBound" . TestCase $
assertEqual
"maxBound"
(Just (maxBound :: Int))
(parseIntField "9223372036854775807")
, TestLabel "int overflow" . TestCase $
assertEqual "maxBound+1" Nothing (parseIntField "9223372036854775808")
, TestLabel "int minBound" . TestCase $
assertEqual
"minBound"
(Just (minBound :: Int))
(parseIntField "-9223372036854775808")
, TestLabel "negative zero" . TestCase $
assertEqual "-0.0 bits" (bitsOf (Just (-0.0))) (bitsOf (parseDoubleField "-0"))
]