cgrep-9.0.0: src/CGrep/Text.hs
--
-- Copyright (c) 2013-2025 Nicola Bonelli <nicola@larthia.com>
--
-- This program is free software; you can redistribute it and/or modify
-- it under the terms of the GNU General Public License as published by
-- the Free Software Foundation; either version 2 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-- GNU General Public License for more details.
--
-- You should have received a copy of the GNU General Public License
-- along with this program; if not, write to the Free Software
-- Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
--
module CGrep.Text (
iterM,
textIndices,
textSlice,
textOffsetWord8,
textContainsOneOf,
firstIndex,
charUtf8Length,
blankByWidth,
) where
import Data.Bits (unsafeShiftL, (.&.), (.|.))
import Data.Char (ord)
import Data.Maybe (isJust)
import qualified Data.Text as T
import qualified Data.Text.Array as A
import qualified Data.Text.Internal as TI
import Data.Text.Internal.ArrayUtils (memchr)
import qualified Data.Text.Internal.Search as TIS
import qualified Data.Text.Unsafe as TU
import Data.Word (Word64)
import GHC.Word (Word8)
iterM :: (Monad m) => T.Text -> ((# Char, Int, Int #) -> m ()) -> m ()
iterM txt f = go 0 (return ())
where
!len = TU.lengthWord8 txt
go !off !cont
| off >= len = cont
| otherwise =
let TU.Iter !c !delta = TU.iter txt off
in f (# c, off, delta #) >> go (off + delta) cont
{-# INLINE iterM #-}
textIndices :: [T.Text] -> T.Text -> [[Int]]
textIndices ps text = (`TIS.indices` text) <$> ps
{-# INLINE textIndices #-}
textContainsOneOf :: [T.Text] -> T.Text -> Bool
textContainsOneOf [] _ = True
textContainsOneOf ps text = any isJust ((`firstIndex` text) <$> ps)
{-# INLINE textContainsOneOf #-}
textSlice :: T.Text -> Int -> Int -> T.Text
textSlice txt start len = TU.takeWord8 len $ TU.dropWord8 start txt
{-# INLINE textSlice #-}
textOffsetWord8 :: T.Text -> Int
textOffsetWord8 (TI.Text _ off _) = off
{-# INLINE textOffsetWord8 #-}
charUtf8Length :: Char -> Int
charUtf8Length c
| n <= 0x7F = 1 -- 0-127
| n <= 0x7FF = 2 -- 128-2047
| n <= 0xFFFF = 3 -- 2048-65535
| otherwise = 4 -- 65536-1114111 (max Unicode code point)
where
-- Get the integer Unicode code point from the Char
n = ord c
{-# INLINE charUtf8Length #-}
blankByWidth :: Char -> Char
blankByWidth c
| n <= 0x7F = '\x0020' -- SPACE
| n <= 0x7FF = '\x00A0' -- NO-BREAK SPACE
| n <= 0xFFFF = '\x3000' -- IDEOGRAPHIC SPACE
| otherwise = '\x100000' -- (Placeholder PUA)
where
-- Get the integer Unicode code point from the Char
n = ord c
{-# INLINE blankByWidth #-}
data T = {-# UNPACK #-} !Word64 :* {-# UNPACK #-} !Int
{- | /O(n+m)/ Find the offsets of all non-overlapping indices of
@needle@ within @haystack@.
In (unlikely) bad cases, this algorithm's complexity degrades
towards /O(n*m)/.
-}
firstIndex ::
-- | Substring to search for (@needle@)
T.Text ->
-- | Text to search in (@haystack@)
T.Text ->
Maybe Int
firstIndex needle@(TI.Text narr noff nlen)
| nlen == 1 = scanOne (A.unsafeIndex narr noff)
| nlen <= 0 = const Nothing
| otherwise = firstIndex' needle
{-# INLINE firstIndex #-}
-- | nlen must be >= 2, otherwise nindex causes access violation
firstIndex' :: T.Text -> T.Text -> Maybe Int
firstIndex' (TI.Text narr noff nlen) (TI.Text harr@(A.ByteArray harr#) hoff hlen) = loop (hoff + nlen)
where
nlast = nlen - 1
!z = nindex nlast
nindex k = A.unsafeIndex narr (noff + k)
buildTable !i !msk !skp
| i >= nlast = (msk .|. swizzle z) :* skp
| otherwise = buildTable (i + 1) (msk .|. swizzle c) skp'
where
!c = nindex i
skp'
| c == z = nlen - i - 2
| otherwise = skp
!(mask :* skip) = buildTable 0 0 (nlen - 2)
swizzle :: Word8 -> Word64
swizzle !k = 1 `unsafeShiftL` (word8ToInt k .&. 0x3f)
loop !i
| i > hlen + hoff =
Nothing
| A.unsafeIndex harr (i - 1) == z =
if A.equal narr noff harr (i - nlen) nlen
then Just $ i - nlen - hoff
else loop (i + skip + 1)
| i == hlen + hoff =
Nothing
| mask .&. swizzle (A.unsafeIndex harr i) == 0 =
loop (i + nlen + 1)
| otherwise =
case memchr harr# i (hlen + hoff - i) z of
-1 -> Nothing
x -> loop (i + x + 1)
{-# INLINE firstIndex' #-}
scanOne :: Word8 -> T.Text -> Maybe Int
scanOne c (TI.Text harr hoff hlen) = loop 0
where
loop !i
| i >= hlen = Nothing
| A.unsafeIndex harr (hoff + i) == c = Just i
| otherwise = loop (i + 1)
{-# INLINE scanOne #-}
word8ToInt :: Word8 -> Int
word8ToInt = fromIntegral