@@ -98,8 +98,8 @@ module Data.Text
9898 , splitAt
9999 , span
100100 , break
101− -- , group
102− -- , groupBy
101+ , group
102+ , groupBy
103103 , inits
104104 , tails
105105
@@ -145,10 +145,10 @@ module Data.Text
145145
146146import Prelude (Char, Bool, Functor(..), Int, Maybe(..), String,
147147 Eq, (==), (++), error,
148− Show, showsPrec,
149− Read, readsPrec,
150− (&&), (||), (+), (-), (<), (>), (<=), (>=), (.),
148+ Read(..), Show(..),
149+ (&&), (||), (+), (-), (<), (>), (<=), (>=), (.), ($),
151150 not, return, otherwise)
151+import Control.Exception (assert)
152152import Data.Char (isSpace)
153153import Control.Monad.ST (ST)
154154import qualified Data.Text.Array as A
@@ -159,8 +159,9 @@ import Data.String (IsString(..))
159159
160160import qualified Data.Text.Fusion as S
161161import Data.Text.Fusion (Stream(..), Step(..), stream, unstream)
162−import Data.Text.Internal (Text(..), empty)
162+import Data.Text.Internal (Text(..), empty, text)
163163import qualified Prelude as P
164+import Data.Text.Unsafe (iter, iter_)
164165import Data.Text.UnsafeChar (unsafeChr)
165166import qualified Data.Text.Utf16 as U16
166167
@@ -273,32 +274,13 @@ head :: Text -> Char
273274head t = S.head (stream t)
274275{-# INLINE head #-}
275276
276−-- | Iterate one step through a UTF-16 array, returning the current
277−-- character and the step to add to give the next offset to iterate
278−-- at.
279−iter :: A.Array Word16 -> Int -> (Char,Int)
280−iter arr i | m < 0xD800 || m > 0xDBFF = (unsafeChr m, 1)
281− | otherwise = (U16.chr2 m n, 2)
282− where m = A.unsafeIndex arr i
283− n = A.unsafeIndex arr j
284− j = i + 1
285−{-# INLINE iter #-}
286−
287−-- | Iterate one step through a UTF-16 array, returning the next
288−-- offset to iterate at.
289−iter_ :: A.Array Word16 -> Int -> Int
290−iter_ arr i | m < 0xD800 || m > 0xDBFF = 1
291− | otherwise = 2
292− where m = A.unsafeIndex arr i
293−{-# INLINE iter_ #-}
294−
295277-- | /O(1)/ Returns the first character and rest of a 'Text', or
296278-- 'Nothing' if empty. Subject to array fusion.
297279uncons :: Text -> Maybe (Char, Text)
298−uncons (Text arr off len)
280+uncons t@(Text arr off len)
299281 | len <= 0 = Nothing
300282 | otherwise = Just (c, Text arr (off+d) (len-d))
301− where (c,d) = iter arr off
283+ where (c,d) = iter t 0
302284{-# INLINE uncons #-}
303285
304286second :: (b -> c) -> (a,b) -> (a,c)
@@ -329,24 +311,35 @@ last (Text arr off len)
329311 S.last (stream t) = last t
330312 #-}
331313
314+-- | Construct a 'Text' without invisibly pinning its byte array in
315+-- memory if its length has dwindled to zero.
316+textP :: A.Array Word16 -> Int -> Int -> Text
317+textP arr off len | len == 0 = empty
318+ | otherwise = text arr off len
319+{-# INLINE textP #-}
332320
333321-- | /O(1)/ Returns all characters after the head of a 'Text', which
334322-- must be non-empty. Subject to array fusion.
335323tail :: Text -> Text
336−tail (Text arr off len)
324+tail t@(Text arr off len)
337325 | len <= 0 = errorEmptyList "tail"
338− | otherwise = Text arr (off+d) (len-d)
339− where d = iter_ arr off
326+ | otherwise = textP arr (off+d) (len-d)
327+ where d = iter_ t 0
340328{-# INLINE [1] tail #-}
341329
342−
330+{-# RULES
331+"TEXT tail -> fused" [~1] forall t.
332+ tail t = unstream (S.tail (stream t))
333+"TEXT tail -> unfused" [1] forall t.
334+ unstream (S.tail (stream t)) = tail t
335+ #-}
343336
344337-- | /O(1)/ Returns all but the last character of a 'Text', which must
345338-- be non-empty. Subject to array fusion.
346339init :: Text -> Text
347340init (Text arr off len) | len <= 0 = errorEmptyList "init"
348− | n >= 0xDC00 && n <= 0xDFFF = Text arr off (len-2)
349− | otherwise = Text arr off (len-1)
341+ | n >= 0xDC00 && n <= 0xDFFF = textP arr off (len-2)
342+ | otherwise = textP arr off (len-1)
350343 where
351344 n = A.unsafeIndex arr (off+len-1)
352345{-# INLINE [1] init #-}
@@ -361,8 +354,15 @@ init (Text arr off len) | len <= 0 = errorEmptyList "init"
361354-- | /O(1)/ Tests whether a 'Text' is empty or not. Subject to array
362355-- fusion.
363356null :: Text -> Bool
364−null t = S.null (stream t)
365−{-# INLINE null #-}
357+null (Text _arr _off len) = assert (len >= 0) $ len <= 0
358+{-# INLINE [1] null #-}
359+
360+{-# RULES
361+"TEXT null -> fused" [~1] forall t.
362+ null t = S.null (stream t)
363+"TEXT null -> unfused" [1] forall t.
364+ S.null (stream t) = null t
365+ #-}
366366
367367-- | /O(n)/ Returns the number of characters in a 'Text'.
368368-- Subject to array fusion.
@@ -560,13 +560,12 @@ take :: Int -> Text -> Text
560560take n t@(Text arr off len)
561561 | n <= 0 = empty
562562 | n >= len = t
563− | otherwise = Text arr off (loop off 0)
563+ | otherwise = Text arr off (loop 0 0)
564564 where
565− end = off + len
566565 loop !i !count
567− | i >= end || count >= n = i - off
568− | otherwise = loop (i+d) (count+1)
569− where d = iter_ arr i
566+ | i >= len || count >= n = i
567+ | otherwise = loop (i+d) (count+1)
568+ where d = iter_ t i
570569{-# INLINE [1] take #-}
571570
572571{-# RULES
@@ -583,12 +582,12 @@ drop :: Int -> Text -> Text
583582drop n t@(Text arr off len)
584583 | n <= 0 = t
585584 | n >= len = empty
586− | otherwise = loop off 0 len
585+ | otherwise = loop 0 0
587586 where end = off + len
588− loop !i !count !l
589− | i >= end || count >= n = Text arr i l
590− | otherwise = loop (i+d) (count+1) (l-d)
591− where d = iter_ arr i
587+ loop !i !count
588+ | i >= end || count >= n = Text arr (off+i) (len-i)
589+ | otherwise = loop (i+d) (count+1)
590+ where d = iter_ t i
592591{-# INLINE [1] drop #-}
593592
594593{-# RULES
@@ -602,11 +601,11 @@ drop n t@(Text arr off len)
602601-- the longest prefix (possibly empty) of elements that satisfy @p@.
603602-- This function is subject to array fusion.
604603takeWhile :: (Char -> Bool) -> Text -> Text
605−takeWhile p t@(Text arr off len) = loop off 0
606− where loop !i !l | l >= len = t
607− | p c = loop (i+d) (l+d)
608− | otherwise = Text arr off l
609− where (c,d) = iter arr i
604+takeWhile p t@(Text arr off len) = loop 0
605+ where loop !i | i >= len = t
606+ | p c = loop (i+d)
607+ | otherwise = textP arr off i
608+ where (c,d) = iter t i
610609{-# INLINE [1] takeWhile #-}
611610
612611{-# RULES
@@ -619,11 +618,11 @@ takeWhile p t@(Text arr off len) = loop off 0
619618-- | /O(n)/ 'dropWhile' @p@ @xs@ returns the suffix remaining after
620619-- 'takeWhile' @p@ @xs@. This function is subject to array fusion.
621620dropWhile :: (Char -> Bool) -> Text -> Text
622−dropWhile p (Text arr off len) = loop off 0
621+dropWhile p t@(Text arr off len) = loop 0 0
623622 where loop !i !l | l >= len = empty
624623 | p c = loop (i+d) (l+d)
625− | otherwise = Text arr i (len-l)
626− where (c,d) = iter arr i
624+ | otherwise = Text arr (off+i) (len-l)
625+ where (c,d) = iter t i
627626{-# INLINE [1] dropWhile #-}
628627
629628{-# RULES
@@ -641,12 +640,11 @@ splitAt n t@(Text arr off len)
641640 | n <= 0 = (empty, t)
642641 | n >= len = (t, empty)
643642 | otherwise = (Text arr off k, Text arr (off+k) (len-k))
644− where k = loop off 0
645− end = off + len
643+ where k = loop 0 0
646644 loop !i !count
647− | i >= end || count >= n = i - off
645+ | i >= len || count >= n = i
648646 | otherwise = loop (i+d) (count+1)
649− where d = iter_ arr i
647+ where d = iter_ t i
650648{-# INLINE splitAt #-}
651649
652650-- | /O(n)/ 'span', applied to a predicate @p@ and text @t@, returns a
@@ -654,14 +652,11 @@ splitAt n t@(Text arr off len)
654652-- @t@ of elements that satisfy @p@, and whose second is the remainder
655653-- of the list.
656654span :: (Char -> Bool) -> Text -> (Text, Text)
657−span p t@(Text arr off len)
658− | k == 0 = (empty, t)
659− | k == len = (t, empty)
660− | otherwise = (Text arr off k, Text arr (off+k) (len-k))
661− where k = loop off 0
662− loop !i !l | l >= len || not (p c) = l
663− | otherwise = loop (i+d) (l+d)
664− where (c,d) = iter arr i
655+span p t@(Text arr off len) = (textP arr off k, textP arr (off+k) (len-k))
656+ where k = loop 0
657+ loop !i | i >= len || not (p c) = i
658+ | otherwise = loop (i+d)
659+ where (c,d) = iter t i
665660{-# INLINE span #-}
666661
667662-- | /O(n)/ 'break' is like 'span', but the prefix returned is over
@@ -670,12 +665,35 @@ break :: (Char -> Bool) -> Text -> (Text, Text)
670665break p = span (not . p)
671666{-# INLINE break #-}
672667
668+-- | /O(n)/ Group characters in a string according to a predicate.
669+groupBy :: (Char -> Char -> Bool) -> Text -> [Text]
670+groupBy p = loop
671+ where
672+ loop t@(Text arr off len)
673+ | null t = []
674+ | otherwise = text arr off n : loop (text arr (off+n) (len-n))
675+ where (c,d) = iter t 0
676+ n = d + findAIndexOrEnd (not . p c) (Text arr (off+d) (len-d))
677+
678+-- | Returns the /array/ index (in units of 'Word16') at which a
679+-- character may be found. This is /not/ the same as the logical
680+-- index returned by e.g. 'findIndex'.
681+findAIndexOrEnd :: (Char -> Bool) -> Text -> Int
682+findAIndexOrEnd q t@(Text _arr _off len) = go 0
683+ where go !i | i >= len || q c = i
684+ | otherwise = go (i+d)
685+ where (c,d) = iter t i
686+
687+-- | /O(n)/ Group characters in a string by equality.
688+group :: Text -> [Text]
689+group = groupBy (==)
690+
673691-- | /O(n)/ Return all initial segments of the given 'Text', shortest
674692-- first.
675693inits :: Text -> [Text]
676−inits t@(Text arr off len) = loop off
694+inits t@(Text arr off len) = loop 0
677695 where loop i | i >= len = [t]
678− | otherwise = Text arr off i : loop (i + iter_ arr i)
696+ | otherwise = Text arr off i : loop (i + iter_ t i)
679697
680698-- | /O(n)/ Return all final segments of the given 'Text', longest
681699-- first.
@@ -744,20 +762,18 @@ zipWith f t1 t2 = unstream (S.zipWith f (stream t1) (stream t2))
744762-- | /O(n)/ Breaks a 'Text' up into a list of words, delimited by 'Char's
745763-- representing white space.
746764words :: Text -> [Text]
747−words (Text arr off len) = loop0 off off
748− where
749− loop0 start n
750− | n >= len = if start == n
751− then []
752− else [Text arr (start+off) (n-start)]
753− | isSpace (unsafeChr c) =
754− if start == n
755− then loop0 (start+1) (start+1)
756− else Text arr (start+off) (n-start) : loop0 (n+1) (n+1)
757− | otherwise = if c < 0xD800 || c > 0xDBFF
758− then loop0 start (n+1)
759− else loop0 start (n+2)
760− where c = arr `A.unsafeIndex` n
765+words t@(Text arr off len) = loop 0 0
766+ where
767+ loop !start !n
768+ | n >= len = if start == n
769+ then []
770+ else [Text arr (start+off) (n-start)]
771+ | isSpace c =
772+ if start == n
773+ then loop (start+1) (start+1)
774+ else Text arr (start+off) (n-start) : loop (n+d) (n+d)
775+ | otherwise = loop start (n+d)
776+ where (c,d) = iter t n
761777{-# INLINE words #-}
762778
763779-- | /O(n)/ Breaks a 'Text' up into a list of 'Text's at