packages feed

dataframe-parsing-1.0.2.0: src/DataFrame/Internal/Parsing/Fast/Token.hs

{-# LANGUAGE MagicHash #-}
{-# LANGUAGE UnboxedTuples #-}

{- | Byte-level token tests: Bool fields, the canonical missing-token
set, and the default @%Y-%m-%d@ date shape. All length-bucketed with
first-byte dispatch; no Text decode, no list walk.
-}
module DataFrame.Internal.Parsing.Fast.Token (
    parseBoolField#,
    isMissingFieldSlice,
    isMissingFieldIn,
    parseDateFieldSlice,
) where

import qualified Data.ByteString as BS
import qualified Data.ByteString.Unsafe as BSU

import Data.Time (Day, fromGregorianValid)
import Data.Word (Word8)
import GHC.Exts (Int#)

import DataFrame.Internal.Parsing (readByteStringDate)
import DataFrame.Internal.Parsing.Fast.Common (isDigitByte)

{- | Exactly @True|true|TRUE|False|false|FALSE@, no strip (the
'readByteStringBool' grammar). Result is @(# ok, bool #)@.
-}
parseBoolField# :: BS.ByteString -> Int -> Int -> (# Int#, Int# #)
parseBoolField# bs s e = case e - s of
    4
        | ix 0 == 0x54 && rue 0x72 0x75 0x65 -> (# 1#, 1# #) -- True
        | ix 0 == 0x54 && rue 0x52 0x55 0x45 -> (# 1#, 1# #) -- TRUE
        | ix 0 == 0x74 && rue 0x72 0x75 0x65 -> (# 1#, 1# #) -- true
    5
        | ix 0 == 0x46 && alse 0x61 0x6C 0x73 0x65 -> (# 1#, 0# #) -- False
        | ix 0 == 0x46 && alse 0x41 0x4C 0x53 0x45 -> (# 1#, 0# #) -- FALSE
        | ix 0 == 0x66 && alse 0x61 0x6C 0x73 0x65 -> (# 1#, 0# #) -- false
    _ -> (# 0#, 0# #)
  where
    ix d = BSU.unsafeIndex bs (s + d)
    rue a b c = ix 1 == a && ix 2 == b && ix 3 == c
    alse a b c d = ix 1 == a && ix 2 == b && ix 3 == c && ix 4 == d
{-# INLINE parseBoolField# #-}

{- | Membership in the canonical missing list
@[\"Nothing\",\"NULL\",\"\",\" \",\"nan\",\"null\",\"N\/A\",\"NaN\",\"NAN\",\"NA\"]@
(case-sensitive, exact), dispatched on length then first byte.
-}
isMissingFieldSlice :: BS.ByteString -> Int -> Int -> Bool
isMissingFieldSlice bs s e = case e - s of
    0 -> True
    1 -> ix 0 == 0x20 -- " "
    2 -> ix 0 == 0x4E && ix 1 == 0x41 -- NA
    3 -> case ix 0 of
        0x6E -> ix 1 == 0x61 && ix 2 == 0x6E -- nan
        0x4E ->
            (ix 2 == 0x4E && (ix 1 == 0x61 || ix 1 == 0x41)) -- NaN NAN
                || (ix 1 == 0x2F && ix 2 == 0x41) -- N/A
        _ -> False
    4 ->
        (ix 0 == 0x4E && ix 1 == 0x55 && ix 2 == 0x4C && ix 3 == 0x4C) -- NULL
            || (ix 0 == 0x6E && ix 1 == 0x75 && ix 2 == 0x6C && ix 3 == 0x6C) -- null
    7 ->
        ix 0 == 0x4E -- Nothing
            && ix 1 == 0x6F
            && ix 2 == 0x74
            && ix 3 == 0x68
            && ix 4 == 0x69
            && ix 5 == 0x6E
            && ix 6 == 0x67
    _ -> False
  where
    ix :: Int -> Word8
    ix d = BSU.unsafeIndex bs (s + d)
    {-# INLINE ix #-}
{-# INLINE isMissingFieldSlice #-}

-- | Generic fallback for user-supplied missing-indicator lists.
isMissingFieldIn :: [BS.ByteString] -> BS.ByteString -> Bool
isMissingFieldIn toks f = f `elem` toks
{-# INLINE isMissingFieldIn #-}

{- | @%Y-%m-%d@: byte-level fast path for the padded 10-byte shape
(@dddd-dd-dd@); anything else (unpadded, whitespace-tolerant, long
years, invalid) falls back to 'readByteStringDate'.
-}
parseDateFieldSlice :: BS.ByteString -> Int -> Int -> Maybe Day
parseDateFieldSlice bs s e
    | e - s == 10
        && isDigitByte (ix 0)
        && isDigitByte (ix 1)
        && isDigitByte (ix 2)
        && isDigitByte (ix 3)
        && ix 4 == 0x2D
        && isDigitByte (ix 5)
        && isDigitByte (ix 6)
        && ix 7 == 0x2D
        && isDigitByte (ix 8)
        && isDigitByte (ix 9) =
        fromGregorianValid
            (toInteger (dig 0 * 1000 + dig 1 * 100 + dig 2 * 10 + dig 3))
            (dig 5 * 10 + dig 6)
            (dig 8 * 10 + dig 9)
    | otherwise =
        readByteStringDate "%Y-%m-%d" (BSU.unsafeTake (e - s) (BSU.unsafeDrop s bs))
  where
    ix d = BSU.unsafeIndex bs (s + d)
    dig :: Int -> Int
    dig d = fromIntegral (ix d) - 0x30
{-# INLINE parseDateFieldSlice #-}