text-containers-0.1.0.0: src/Data/TextSet/Unboxed.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MagicHash #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE Trustworthy #-}
{-# LANGUAGE TypeFamilies #-}
-- |
-- Copyright: © 2017 Herbert Valerio Riedel
-- License: GPLv3
--
-- This module provides the 'TextSet' container for storing sets of text strings.
--
-- This module is intended to be imported @qualified@, e.g.
--
-- > import Data.TextSet.Unboxed (TextSet)
-- > import qualified Data.TextSet.Unboxed as TextSet
--
module Data.TextSet.Unboxed
( TextSet
, Key
-- * Querying & lookup
, size
, null
, member
, lookupMin
, lookupMax
, lookupLE
, lookupGE
, (!?)
, lookupIndex
-- * Construction
, empty
, singleton
, fromList
, fromDistinctAscList
, fromSet
-- * Deconstruction
, toList
, toArray
, toSet
) where
import Control.DeepSeq
import Data.Hashable (Hashable)
import qualified Data.Set as Set
import Data.Text.Short (ShortText)
import qualified GHC.Exts as GHC
import Prelude hiding (null)
import Data.TextArray.Unboxed (TextArray)
import qualified Data.TextArray.Unboxed.Internal as TA
import Internal
-- | A set of unboxed 'ShortText' strings
--
-- The memory footprint of this data-structure is a single heap object (an unlifted 'ByteArray#') with the size expressed in words
--
-- \[
-- 3 + n + \left\lceil \frac{1}{w} \sum_{i=0}^{n-1} len(s_i) \right\rceil
-- \]
--
-- where the word-size \(w\) is either \(w = 4\) or \(w = 8\) bytes; and where \(len(s_i)\) denotes the UTF-8 size in bytes of the \(i\)-th text string.
--
-- NOTE: Depending on whether you @UNPACK@ the 'TextSet' wrapper, you need at least one additional word for the pointer to the internal 'ByteArray#' heap object.
newtype TextSet = TS TextArray
deriving (Eq,Ord,NFData,Hashable)
type Key = ShortText
instance Show TextSet where
showsPrec p (TS ta) = showsPrec p ta
show (TS ta) = show ta
instance Read TextSet where
readsPrec p = map (\(x,s) -> (TS x,s)) . readsPrec p
instance GHC.IsList TextSet where
type Item TextSet = Key
fromList = fromList
toList = toList
-- | \(\mathcal{O}(1)\). An empty 'TextSet'.
empty :: TextSet
empty = TS TA.empty
-- | \(\mathcal{O}(1)\). Report number of elements in 'TextSet'.
--
-- >>> size empty
-- 0
--
-- >>> size (singleton "")
-- 1
--
-- >>> size (fromList ["Hey","Hey","Jude"])
-- 2
--
size :: TextSet -> Int
size (TS ta) = TA.length ta
-- | \(\mathcal{O}(1)\). Check for 'empty' set.
--
-- >>> null empty
-- True
--
-- >>> null (singleton "")
-- False
--
null :: TextSet -> Bool
null (TS ta) = TA.null ta
-- | \(\mathcal{O}(1)\). Construct set containing one element.
--
-- >>> toList (singleton "alone")
-- ["alone"]
--
singleton :: Key -> TextSet
singleton = TS . TA.singleton
-- | \(\mathcal{O}(n \log n)\). Construct set from list of elements.
--
-- >>> toList (fromList ["Hey","Jude","Hey","Law","Hey",""])
-- ["","Hey","Jude","Law"]
--
fromList :: [Key] -> TextSet
fromList [] = empty
fromList [x] = singleton x
fromList xs = fromSet (Set.fromList xs)
-- | \(\mathcal{O}(n)\). Construct set from list of distinct elements in ascending order.
--
-- __NOTE__: If the input list is not strictly ascending, an 'error' is thrown.
fromDistinctAscList :: [Key] -> TextSet
fromDistinctAscList [] = empty
fromDistinctAscList xs
| isMonotonic xs = TS (TA.fromList xs)
| otherwise = error "fromDistinctAscList: invalid argument"
isMonotonic :: [Key] -> Bool
isMonotonic [] = True
isMonotonic [_] = True
isMonotonic (x:xs) = go x xs
where
go _ [] = True
go y (y':ys)
| y < y' = go y' ys
| otherwise = False
-- | \(\mathcal{O}(n)\). Convert 'Set' to 'TextSet'.
fromSet :: Set.Set Key -> TextSet
fromSet = TS . TA.fromList . Set.toAscList
-- | \(\mathcal{O}(n)\). Convert 'TextSet' to 'Set'.
toSet :: TextSet -> Set.Set Key
toSet = Set.fromDistinctAscList . toList
-- | \(\mathcal{O}(n)\). Convert 'TextSet' to list of 'ShortText' in ascending order.
toList :: TextSet -> [Key]
toList (TS ta) = TA.toList ta
-- | \(\mathcal{O}(1)\). Convert 'TextSet' to 'TextArray' of 'ShortText' in ascending order.
--
-- >>> toList (fromList ["Hey","Jude","Hey","Law","Hey",""])
-- ["","Hey","Jude","Law"]
--
toArray :: TextSet -> TextArray
toArray (TS ta) = ta
-- | \(\mathcal{O}(1)\). Extract minimal element from set.
--
-- >>> lookupMin empty
-- Nothing
--
-- >>> lookupMin (fromList ["a","b","c"])
-- Just "a"
--
lookupMin :: TextSet -> Maybe Key
lookupMin (TS ta) = ta TA.!? 0
-- | \(\mathcal{O}(1)\). Extract maximal element from set.
--
-- >>> lookupMax empty
-- Nothing
--
-- >>> lookupMax (fromList ["a","b","c"])
-- Just "c"
--
lookupMax :: TextSet -> Maybe Key
lookupMax (TS ta) = ta TA.!? (TA.length ta - 1)
-- TODO: isProperSubsetOf isSubsetOf union intersection difference
----------------------------------------------------------------------------
-- lookup
-- | \(\mathcal{O}(\log n)\). Test whether set contains a string.
--
-- >>> member "empty" empty
-- False
--
-- >>> member "a" (fromList ["a","b","c"])
-- True
--
-- >>> member "d" (fromList ["a","b","c"])
-- False
--
member :: Key -> TextSet -> Bool
member t ts = case lookupIndexNear LBelow t ts of
LResExact _ -> True
_ -> False
-- | \(\mathcal{O}(1)\). Retrieve \(i\)-th element in the sorted sequence of elements.
--
-- >>> fromList ["Hey","","Jude"] !? 0
-- Just ""
--
-- >>> fromList ["Hey","","Jude"] !? 1
-- Just "Hey"
--
-- >>> fromList ["Hey","","Jude"] !? 3
-- Nothing
--
-- See also 'lookupIndex'.
(!?) :: TextSet -> Int -> Maybe Key
(TS ta) !? i = ta TA.!? i
-- | \(\mathcal{O}(\log n)\). Look up element in set and report its zero-based index in the sorted sequence elements.
--
-- >>> lookupIndex "" (fromList ["Hey","","Jude"])
-- Just 0
--
-- >>> lookupIndex "Hey" (fromList ["Hey","","Jude"])
-- Just 1
--
-- >>> lookupIndex "Law" (fromList ["Hey","","Jude"])
-- Nothing
--
-- See also '!?'.
lookupIndex :: Key -> TextSet -> Maybe Int
lookupIndex t ts = case lookupIndexNear LBelow t ts of
LResExact (IdxOfsLen i _ _) -> Just i
_ -> Nothing
mkist :: TextSet -> IdxOfsLen -> (Int, Key)
mkist (TS ta) (IdxOfsLen i ofs n) = (i, ba2st (sliceBA (TA.ta2ba ta) ofs n))
-- TODO: lookupLT lookupGT
-- | \(\mathcal{O}(\log n)\). Look up \"greatest\" string (together with its index) in set less or equal to given string.
--
-- >>> lookupLE "a" (fromList ["bb","cc"])
-- Nothing
--
-- >>> lookupLE "c" (fromList ["bb","cc"])
-- Just (0,"bb")
--
-- >>> lookupLE "cc" (fromList ["bb","cc"])
-- Just (1,"cc")
--
-- >>> lookupLE "z" (fromList ["bb","cc"])
-- Just (1,"cc")
--
lookupLE :: Key -> TextSet -> Maybe (Int,Key)
lookupLE t ts = case lookupIndexNear LAbove t ts of
LResAbove res -> Just (mkist ts res)
LResExact res -> Just (mkist ts res)
_ -> Nothing
-- | \(\mathcal{O}(\log n)\). Look up \"least\" string (together with its index) in set greater or equal to given string.
--
-- >>> lookupGE "a" (fromList ["bb","cc"])
-- Just (0,"bb")
--
-- >>> lookupGE "c" (fromList ["bb","cc"])
-- Just (1,"cc")
--
-- >>> lookupGE "cc" (fromList ["bb","cc"])
-- Just (1,"cc")
--
-- >>> lookupGE "z" (fromList ["bb","cc"])
-- Nothing
--
lookupGE :: Key -> TextSet -> Maybe (Int,Key)
lookupGE t ts = case lookupIndexNear LBelow t ts of
LResBelow res -> Just (mkist ts res)
LResExact res -> Just (mkist ts res)
_ -> Nothing
data LRes = LResEmpty -- empty set
| LResExact !IdxOfsLen -- exact match
| LResBelow !IdxOfsLen -- inexact match, needle is /below/ IdxOfsLen entry
| LResAbove !IdxOfsLen -- inexact match, needle is /above/ IdxOfsLen entry
deriving Show
-- | associativity of 'lookupIndexNear'
data LMode = LBelow -- if between two entries, return 'LResBelow'
| LAbove -- if between two entries, return 'LResAbove'
-- Find near entry
--
-- NB: the implementation of 'lookupIndex{L,R}' differs only in two places, marked with (I) and (II) comments
lookupIndexNear :: LMode -> Key -> TextSet -> LRes
lookupIndexNear mode x (TS ta)
| ui0 < 0 = LResEmpty
| otherwise = case cmpBA lv0 of
LT -> LResBelow lv0
EQ -> LResExact lv0
GT -> case cmpBA uv0 of
GT -> LResAbove uv0
EQ -> LResExact uv0
LT -> case mode of
LAbove -> goL li0 ui0 -- invariant: lv0 < v < uv0 && li < ui
LBelow -> goR li0 ui0 -- invariant: lv0 < v < uv0 && li < ui
where
li0 = 0
ui0 = TA.length ta - 1
lv0, uv0 :: IdxOfsLen
lv0 = TA.indexOfsLen' ta li0
uv0 = TA.indexOfsLen' ta ui0
cmpBA = cmpBA2OfsLen (st2ba x) (TA.ta2ba ta)
-----------------------------------------------
goL :: Int -> Int -> LRes
goL li ui -- invariant: li < ui
| mi == li = LResAbove mv -- closest match -- (I)
| otherwise = case cmpBA mv of
LT -> goL li mi -- go left
EQ -> LResExact mv -- exact match
GT -> goL mi ui -- go right
where
mi = mid li ui
mv = TA.indexOfsLen' ta mi
mid :: Int -> Int -> Int
mid li' ui' = li' + quot (ui'-li') 2 -- (II)
goR :: Int -> Int -> LRes
goR li ui -- invariant: li < ui
| mi == ui = LResBelow mv -- closest match -- (I)
| otherwise = case cmpBA mv of
LT -> goR li mi -- go left
EQ -> LResExact mv -- exact match
GT -> goR mi ui -- go right
where
mi = mid li ui
mv = TA.indexOfsLen' ta mi
mid :: Int -> Int -> Int
mid li' ui' = ui' - quot (ui'-li') 2 -- (II)
-- $setup
-- >>> :set -XOverloadedStrings