alfred-margaret-1.1.2.0: src/Data/Text/Utf8.hs
-- Alfred-Margaret: Fast Aho-Corasick string searching
-- Copyright 2022 Channable
--
-- Licensed under the 3-clause BSD license, see the LICENSE file in the
-- repository root.
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-- | This module provides functions that allow treating 'Text' values as series of UTF-8 code units
-- instead of characters. Currently, it also contains a stub 'Text' type which treats its internal byte array
-- as UTF-8 encoded. We use this as a placeholder until we can use @text-2@.
module Data.Text.Utf8
( CodePoint
, CodeUnit
, CodeUnitIndex (..)
, Text (..)
, lengthUtf8
, lowerCodePoint
, lowerUtf8
, toLowerAscii
, unicode2utf8
, unpackUtf8
-- * Decoding
--
-- $decoding
, decode2
, decode3
, decode4
, decodeUtf8
, stringToByteArray
-- * Indexing
--
-- $indexing
, indexCodeUnit
, unsafeIndexCodePoint
, unsafeIndexCodePoint'
, unsafeIndexCodeUnit
, unsafeIndexCodeUnit'
-- * Slicing Functions
--
-- $slicingFunctions
, unsafeCutUtf8
, unsafeSliceUtf8
-- * General Functions
--
-- $generalFunctions
, Data.Text.Utf8.concat
, Data.Text.Utf8.dropWhile
, Data.Text.Utf8.null
, Data.Text.Utf8.readFile
, Data.Text.Utf8.replicate
, indices
, isInfixOf
, pack
, unpack
) where
import Control.DeepSeq (NFData, rnf)
import Data.Bits (Bits (shiftL), shiftR, (.&.), (.|.))
import Data.Char (ord)
import Data.Foldable (for_)
import Data.Hashable (Hashable (hashWithSalt), hashByteArrayWithSalt)
import Data.Primitive.ByteArray (ByteArray (ByteArray), byteArrayFromList, compareByteArrays,
indexByteArray, newByteArray, sizeofByteArray,
unsafeFreezeByteArray, writeByteArray)
import Data.String (IsString (fromString))
import Data.Word (Word8)
import GHC.Generics (Generic)
import Prelude hiding (length)
#if defined(HAS_AESON)
import Data.Aeson (FromJSON, ToJSON, Value (String), parseJSON, toJSON, withText)
#endif
import qualified Data.ByteString as BS
import qualified Data.Char as Char
import qualified Data.Text as T
-- | A UTF-8 code unit is a byte. A Unicode code point can be encoded as up to four code units.
type CodeUnit = Word8
-- | A Unicode code point.
type CodePoint = Char
-- | An index into the raw UTF-8 data of a `Text`. This is not the code point
-- index as conventionally accepted by `Text`, so we wrap it to avoid confusing
-- the two. Incorrect index manipulation can lead to surrogate pairs being
-- sliced, so manipulate indices with care. This type is also used for lengths.
newtype CodeUnitIndex = CodeUnitIndex
{ codeUnitIndex :: Int
}
deriving stock (Eq, Ord, Show, Generic, Bounded)
#if defined(HAS_AESON)
deriving newtype (Hashable, Num, NFData, FromJSON, ToJSON)
#else
deriving newtype (Hashable, Num, NFData)
#endif
data Text
-- | A placeholder data type for UTF-8 encoded text until we can use text-2.0.
= Text
!ByteArray -- ^ Underlying array encoded using UTF-8.
!Int -- ^ Starting position of the UTF-8 sequence in bytes.
!Int -- ^ Length of the UTF-8 sequence in bytes.
-- This instance, as well as the Show instance above, is necessary for the test suite.
instance Eq Text where
(Text u8data offset length) == (Text u8data' offset' length') =
length == length' && compareByteArrays u8data offset u8data' offset' length == EQ
instance Ord Text where
compare (Text u8data offset length) (Text u8data' offset' length') =
compareByteArrays u8data offset u8data' offset' (min length length') <> compare length length'
instance Show Text where
show = show . unpack
-- Instances required for the Searcher modules etc.
#if defined(HAS_AESON)
-- NOTE: This is ugly and slow but will be removed once we move to text-2.0.
instance ToJSON Text where
toJSON = String . T.pack . unpack
instance FromJSON Text where
parseJSON = withText "Data.Text.Utf8.Text" (pure . pack . T.unpack)
#endif
-- Copied from https://hackage.haskell.org/package/hashable-1.4.0.2/docs/src/Data.Hashable.Class.html#line-746
instance Hashable Text where
hashWithSalt salt (Text (ByteArray arr) off len) =
hashByteArrayWithSalt arr (off `shiftL` 1) (len `shiftL` 1) (hashWithSalt salt len)
instance NFData Text where
rnf (Text (ByteArray !_) !_ !_) = ()
instance IsString Text where
fromString = pack
{-# INLINABLE unpackUtf8 #-}
unpackUtf8 :: Text -> [CodeUnit]
unpackUtf8 (Text u8data offset length) =
let
go _ 0 = []
go i n = unsafeIndexCodeUnit' u8data (CodeUnitIndex i) : go (i + 1) (n - 1)
in
go offset length
-- | The return value of this function is not really an index.
-- However the signature is supposed to make it clear that the length is returned in terms of code units, not code points.
lengthUtf8 :: Text -> CodeUnitIndex
lengthUtf8 (Text _ _ !length) = CodeUnitIndex length
-- | Convert a 'Text' value into a 'T.Text' value.
toUtf16Text :: Text -> T.Text
toUtf16Text (Text u8data off len) =
T.unfoldr go 0
where
go :: CodeUnitIndex -> Maybe (Char, CodeUnitIndex)
go i
| i >= CodeUnitIndex len = Nothing
| otherwise =
let
(codeUnits, codePoint) = unsafeIndexCodePoint' u8data $ CodeUnitIndex off + i
in
Just (codePoint, i + codeUnits)
-- | Lower-case the ASCII code points A-Z and leave the rest of ASCII intact.
{-# INLINE toLowerAscii #-}
toLowerAscii :: Char -> Char
toLowerAscii cp
| Char.isAsciiUpper cp = Char.chr (Char.ord cp + 0x20)
| otherwise = cp
-- TODO: Slow placeholder implementation until we can use text-2.0
{-# INLINE lowerUtf8 #-}
lowerUtf8 :: Text -> Text
lowerUtf8 = pack . map lowerCodePoint . unpack
asciiCount :: Int
asciiCount = 128
{-# INLINE lowerCodePoint #-}
-- | Lower-Case a UTF-8 codepoint.
-- Uses 'toLowerAscii' for ASCII and 'Char.toLower' otherwise.
lowerCodePoint :: Char -> Char
lowerCodePoint cp
| Char.ord cp < asciiCount = toLowerAscii cp
| otherwise = Char.toLower cp
-- | Convert a Unicode Code Point 'c' into a list of UTF-8 code units (bytes).
unicode2utf8 :: (Ord a, Num a, Bits a) => a -> [a]
unicode2utf8 c
| c < 0x80 = [c]
| c < 0x800 = [0xc0 .|. (c `shiftR` 6), 0x80 .|. (0x3f .&. c)]
| c < 0x10000 = [0xe0 .|. (c `shiftR` 12), 0x80 .|. (0x3f .&. (c `shiftR` 6)), 0x80 .|. (0x3f .&. c)]
| otherwise = [0xf0 .|. (c `shiftR` 18), 0x80 .|. (0x3f .&. (c `shiftR` 12)), 0x80 .|. (0x3f .&. (c `shiftR` 6)), 0x80 .|. (0x3f .&. c)]
-- $decoding
--
-- Functions that turns code unit sequences into code point sequences.
-- | Decode 2 UTF-8 code units into their code point.
-- The given code units should have the following format:
--
-- > ┌───────────────┬───────────────┐
-- > │1 1 0 x x x x x│1 0 x x x x x x│
-- > └───────────────┴───────────────┘
{-# INLINE decode2 #-}
decode2 :: CodeUnit -> CodeUnit -> CodePoint
decode2 cu0 cu1 =
Char.chr $ (fromIntegral cu0 .&. 0x1f) `shiftL` 6 .|. fromIntegral cu1 .&. 0x3f
-- | Decode 3 UTF-8 code units into their code point.
-- The given code units should have the following format:
--
-- > ┌───────────────┬───────────────┬───────────────┐
-- > │1 1 1 0 x x x x│1 0 x x x x x x│1 0 x x x x x x│
-- > └───────────────┴───────────────┴───────────────┘
{-# INLINE decode3 #-}
decode3 :: CodeUnit -> CodeUnit -> CodeUnit -> CodePoint
decode3 cu0 cu1 cu2 =
Char.chr $ (fromIntegral cu0 .&. 0xf) `shiftL` 12 .|. (fromIntegral cu1 .&. 0x3f) `shiftL` 6 .|. (fromIntegral cu2 .&. 0x3f)
-- | Decode 4 UTF-8 code units into their code point.
-- The given code units should have the following format:
--
-- > ┌───────────────┬───────────────┬───────────────┬───────────────┐
-- > │1 1 1 1 0 x x x│1 0 x x x x x x│1 0 x x x x x x│1 0 x x x x x x│
-- > └───────────────┴───────────────┴───────────────┴───────────────┘
{-# INLINE decode4 #-}
decode4 :: CodeUnit -> CodeUnit -> CodeUnit -> CodeUnit -> CodePoint
decode4 cu0 cu1 cu2 cu3 =
Char.chr $ (fromIntegral cu0 .&. 0x7) `shiftL` 18 .|. (fromIntegral cu1 .&. 0x3f) `shiftL` 12 .|. (fromIntegral cu2 .&. 0x3f) `shiftL` 6 .|. (fromIntegral cu3 .&. 0x3f)
-- | Decode a list of UTF-8 code units into a list of code points.
decodeUtf8 :: [CodeUnit] -> [CodePoint]
decodeUtf8 [] = []
decodeUtf8 (cu0 : cus) | cu0 < 0xc0 = Char.chr (fromIntegral cu0) : decodeUtf8 cus
decodeUtf8 (cu0 : cu1 : cus) | cu0 < 0xe0 = decode2 cu0 cu1 : decodeUtf8 cus
decodeUtf8 (cu0 : cu1 : cu2 : cus) | cu0 < 0xf0 = decode3 cu0 cu1 cu2 : decodeUtf8 cus
decodeUtf8 (cu0 : cu1 : cu2 : cu3 : cus) | cu0 < 0xf8 = decode4 cu0 cu1 cu2 cu3 : decodeUtf8 cus
decodeUtf8 cus = error $ "Invalid UTF-8 input sequence at " ++ show (take 4 cus)
stringToByteArray :: String -> ByteArray
stringToByteArray = byteArrayFromList . concatMap char2utf8
-- See https://en.wikipedia.org/wiki/UTF-8
where
char2utf8 :: Char -> [Word8]
char2utf8 = map fromIntegral . unicode2utf8 . ord
-- $indexing
--
-- 'Text' can be indexed by code units or code points.
-- A 'CodePoint' is a 21-bit Unicode code point and can consist of up to four code units.
-- A 'CodeUnit' is a single byte.
-- | Decode a code point at the given 'CodeUnitIndex'.
-- Returns garbage if there is no valid code point at that position.
-- Does not perform bounds checking.
-- See 'decode2', 'decode3' and 'decode4' for the expected format of multi-byte code points.
{-# INLINE unsafeIndexCodePoint' #-}
unsafeIndexCodePoint' :: ByteArray -> CodeUnitIndex -> (CodeUnitIndex, CodePoint)
unsafeIndexCodePoint' !u8data (CodeUnitIndex !idx)
| cu0 < 0xc0 = (1, Char.chr $ fromIntegral cu0)
| cu0 < 0xe0 = (2, decode2 cu0 (cuAt 1))
| cu0 < 0xf0 = (3, decode3 cu0 (cuAt 1) (cuAt 2))
| otherwise = (4, decode4 cu0 (cuAt 1) (cuAt 2) (cuAt 3))
where
cuAt !i = unsafeIndexCodeUnit' u8data $ CodeUnitIndex $ idx + i
!cu0 = cuAt 0
-- | Does exactly the same thing as 'unsafeIndexCodePoint'', but on 'Text' values.
{-# INLINE unsafeIndexCodePoint #-}
unsafeIndexCodePoint :: Text -> CodeUnitIndex -> (CodeUnitIndex, CodePoint)
unsafeIndexCodePoint (Text !u8data !off !_len) (CodeUnitIndex !index) =
unsafeIndexCodePoint' u8data $ CodeUnitIndex $ off + index
-- | Get the code unit at the given 'CodeUnitIndex'.
-- Performs bounds checking.
{-# INLINE indexCodeUnit #-}
indexCodeUnit :: Text -> CodeUnitIndex -> CodeUnit
indexCodeUnit !text (CodeUnitIndex !index)
| index < 0 || index >= codeUnitIndex (lengthUtf8 text) = error $ "Index out of bounds " ++ show index
| otherwise = unsafeIndexCodeUnit text $ CodeUnitIndex index
{-# INLINE unsafeIndexCodeUnit' #-}
unsafeIndexCodeUnit' :: ByteArray -> CodeUnitIndex -> CodeUnit
unsafeIndexCodeUnit' !u8data (CodeUnitIndex !idx) = indexByteArray u8data idx
{-# INLINE unsafeIndexCodeUnit #-}
unsafeIndexCodeUnit :: Text -> CodeUnitIndex -> CodeUnit
unsafeIndexCodeUnit (Text !u8data !off !_len) (CodeUnitIndex !index) =
unsafeIndexCodeUnit' u8data $ CodeUnitIndex $ off + index
-- $slicingFunctions
--
-- 'unsafeCutUtf8' and 'unsafeSliceUtf8' are used to retrieve slices of 'Text' values.
-- @unsafeSliceUtf8 begin length@ returns a substring of length @length@ starting at @begin@.
-- @unsafeSliceUtf8 begin length@ returns a tuple of the "surrounding" substrings.
--
-- They satisfy the following property:
--
-- > let (prefix, suffix) = unsafeCutUtf8 begin length t
-- > in concat [prefix, unsafeSliceUtf8 begin length t, suffix] == t
--
-- The following diagram visualizes the relevant offsets for @begin = CodeUnitIndex 2@, @length = CodeUnitIndex 6@ and @t = \"BCDEFGHIJKL\"@.
--
-- > off off+len
-- > │ │
-- > ▼ ▼
-- > ──┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬──
-- > A│B│C│D│E│F│G│H│I│J│K│L│M│N
-- > ──┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴──
-- > ▲ ▲
-- > │ │
-- > off+begin off+begin+length
-- >
-- > unsafeSliceUtf8 begin length t == "DEFGHI"
-- > unsafeCutUtf8 begin length t == ("BC", "JKL")
--
-- The shown array is open at each end because in general, @t@ may be a slice as well.
--
-- __WARNING__: As their name implies, these functions are not (necessarily) bounds-checked. Use at your own risk.
-- TODO: Make this more readable once we have text-2.0.
unsafeCutUtf8 :: CodeUnitIndex -- ^ Starting position of substring.
-> CodeUnitIndex -- ^ Length of substring.
-> Text -- ^ Initial string.
-> (Text, Text)
unsafeCutUtf8 (CodeUnitIndex !begin) (CodeUnitIndex !length) (Text !u8data !off !len) =
( Text u8data off begin
, Text u8data (off + begin + length) (len - begin - length)
)
-- TODO: Make this more readable once we have text-2.0.
unsafeSliceUtf8 :: CodeUnitIndex -> CodeUnitIndex -> Text -> Text
unsafeSliceUtf8 (CodeUnitIndex !begin) (CodeUnitIndex !length) (Text !u8data !off !_len) =
Text u8data (off + begin) length
-- $generalFunctions
--
-- These functions are available in @text@ as well and should be removed once this library moves to @text-2@.
-- You should be able to use these by doing @import qualified Data.Text.Utf8 as Text@ just like you would with @text@.
--
-- NOTE: The 'Text' instances for @Show@, @Eq@, @Ord@, @IsString@, @FromJSON@, @ToJSON@ and @Hashable@ in this file also fall in this category.
-- | TODO: Inefficient placeholder implementation.
concat :: [Text] -> Text
concat = pack . concatMap unpack
-- | See 'Data.Text.dropWhile'.
dropWhile :: (Char -> Bool) -> Text -> Text
dropWhile predicate text =
let
len = codeUnitIndex (lengthUtf8 text)
go i
| i >= CodeUnitIndex len = i
| otherwise =
let
(codeUnits, codePoint) = unsafeIndexCodePoint text i
in
if predicate codePoint then
go $ i + codeUnits
else
i
prefixEnd = go 0
in
unsafeSliceUtf8 prefixEnd (CodeUnitIndex len - prefixEnd) text
-- | Checks whether a text is the empty string.
null :: Text -> Bool
null (Text _ _ len) = len == 0
-- | TODO: Inefficient placeholder implementation.
pack :: String -> Text
pack = go . stringToByteArray
where
go !arr = Text arr 0 $ sizeofByteArray arr
-- | TODO: Inefficient placeholder implementation.
-- See 'Data.Text.replicate'
replicate :: Int -> Text -> Text
replicate n = pack . Prelude.concat . Prelude.replicate n . unpack
-- | TODO: Inefficient placeholder implementation.
-- This function implements very basic string search. It's @text@ counterpart is 'Data.Text.Internal.Search.indices', which implements the Boyer-Moore algorithm.
-- Since we have this function only to check whether our own Boyer-Moore implementation works, it would not make much sense to implement it using the same algorithm.
-- Once we can use @text-2@, we can compare our implementation to the official @text@ one which presumably works.
indices :: Text -> Text -> [Int]
indices needle haystack
| needleLen == 0 = []
| otherwise = go 0 0
where
needleLen = lengthUtf8 needle
haystackLen = lengthUtf8 haystack
go startIdx needleIdx
-- needle is longer than remaining haystack
| startIdx + needleLen > haystackLen = []
-- whole needle matched
| needleIdx >= needleLen = codeUnitIndex startIdx : go (startIdx + needleLen) 0
-- charachter mismatch
| needleCp /= haystackCp = go (startIdx + 1) 0
-- advance
| otherwise = go startIdx $ needleIdx + codeUnits
where
(codeUnits, needleCp) = unsafeIndexCodePoint needle needleIdx
(_, haystackCp) = unsafeIndexCodePoint haystack $ startIdx + needleIdx
-- | TODO: Inefficient placeholder implementation.
isInfixOf :: Text -> Text -> Bool
isInfixOf needle haystack = T.isInfixOf (toUtf16Text needle) (toUtf16Text haystack)
-- | See 'Data.Text.IO.readFile'.
-- TODO: Uses 'Data.ByteString.readFile' and loops through each byte individually.
-- Use 'Data.Primitive.Ptr.copyPtrToMutableByteArray' here if possible.
readFile :: FilePath -> IO Text
readFile path = do
contents <- BS.readFile path
array <- newByteArray $ BS.length contents
for_ [0..BS.length contents - 1] $ \i -> do
writeByteArray array i $ BS.index contents i
array' <- unsafeFreezeByteArray array
pure $ Text array' 0 $ BS.length contents
-- | TODO: Inefficient placeholder implementation.
unpack :: Text -> String
unpack = decodeUtf8 . unpackUtf8