packages feed

dataframe-core-1.1.0.0: src/DataFrame/Internal/Utf8.hs

{-# LANGUAGE BangPatterns #-}

{- | UTF-8 validation and @decodeUtf8Lenient@-parity slice decoding used by
'DataFrame.Internal.ColumnBuilder' to turn shared byte buffers into 'Text'.
-}
module DataFrame.Internal.Utf8 (
    isValidUtf8Slice,
    isUtf8Boundary,
    lenientDecodeSlice,
    sliceTextVector,
) where

import qualified Data.Text as T
import qualified Data.Text.Array as A
import qualified Data.Vector as VB
import qualified Data.Vector.Mutable as VBM
import qualified Data.Vector.Unboxed as VU

import Data.Text.Internal (Text (..))
import Data.Text.Internal.Encoding.Utf8 (
    DecoderResult (..),
    utf8DecodeContinue,
    utf8DecodeStart,
 )
import Data.Text.Internal.Validate (isValidUtf8ByteArray)
import Data.Word (Word8)

-- | Whether @len@ bytes starting at @off@ are well-formed UTF-8.
isValidUtf8Slice :: A.Array -> Int -> Int -> Bool
isValidUtf8Slice = isValidUtf8ByteArray
{-# INLINE isValidUtf8Slice #-}

{- | Whether a byte may start a code point (i.e. is not a continuation
byte). Field slices of a valid buffer are themselves valid iff every
field starts on a boundary.
-}
isUtf8Boundary :: Word8 -> Bool
isUtf8Boundary w = w < 0x80 || w >= 0xC0
{-# INLINE isUtf8Boundary #-}

{- | Decode a byte slice exactly like @decodeUtf8Lenient@: greedy decode at
each position; any byte that cannot begin a complete, valid sequence within
the slice becomes one U+FFFD and decoding resumes at the next byte.
-}
lenientDecodeSlice :: A.Array -> Int -> Int -> T.Text
lenientDecodeSlice arr off len = T.pack (go off)
  where
    !end = off + len
    go !i
        | i >= end = []
        | otherwise = case tryDecode i of
            Just (c, i') -> c : go i'
            Nothing -> '\xFFFD' : go (i + 1)
    tryDecode !i = loop (utf8DecodeStart (A.unsafeIndex arr i)) (i + 1)
      where
        loop (Accept c) !j = Just (c, j)
        loop Reject _ = Nothing
        loop (Incomplete st cp) !j
            | j >= end = Nothing
            | otherwise = loop (utf8DecodeContinue (A.unsafeIndex arr j) st cp) (j + 1)

{- | Slice forced 'Text' values off a shared array; row @i@ spans bytes
@[offs!i, offs!(i+1))@. The offsets need not start at byte 0, so a row
sub-range of a larger offset vector slices independently (parallel text
merging uses this). Fast path: validate the spanned bytes once and check
every field starts on a code-point boundary. Slow path: per-field
validation with lenient decoding of invalid fields.
-}
sliceTextVector :: A.Array -> VU.Vector Int -> VB.Vector T.Text
sliceTextVector arr offs = VB.create $ do
    mv <- VBM.unsafeNew n
    let fill dec = go 0
          where
            go !i
                | i >= n = pure ()
                | otherwise = do
                    let o = VU.unsafeIndex offs i
                        !t = dec o (VU.unsafeIndex offs (i + 1) - o)
                    VBM.unsafeWrite mv i t
                    go (i + 1)
    if fast then fill mkSlice else fill decodeField
    pure mv
  where
    n = VU.length offs - 1
    base = VU.unsafeIndex offs 0
    used = VU.unsafeIndex offs n
    boundariesOk !i
        | i >= n = True
        | otherwise =
            let o = VU.unsafeIndex offs i
             in (o >= used || isUtf8Boundary (A.unsafeIndex arr o))
                    && boundariesOk (i + 1)
    fast = isValidUtf8Slice arr base (used - base) && boundariesOk 0
    mkSlice o l = if l == 0 then T.empty else Text arr o l
    decodeField o l
        | l == 0 = T.empty
        | isValidUtf8Slice arr o l = Text arr o l
        | otherwise = lenientDecodeSlice arr o l