-- | A built-in representation for efficient string manipulation. -- @Text@ values are /not/ lists of characters. module Text ( -- * Text Text, isEmpty, length, reverse, repeat, replace, -- * Building and Splitting append, concat, split, join, words, lines, -- * Get Substrings slice, left, right, dropLeft, dropRight, -- * Check for Substrings contains, startsWith, endsWith, indexes, indices, -- * Int Conversions toInt, fromInt, -- * Float Conversions toFloat, fromFloat, -- * Char Conversions fromChar, cons, uncons, -- * List Conversions toList, fromList, -- * Formatting -- | Cosmetic operations such as padding with extra characters or trimming whitespace. toUpper, toLower, pad, padLeft, padRight, trim, trimLeft, trimRight, -- * Higher-Order Functions map, filter, foldl, foldr, any, all, ) where import Basics ( Bool, Float, Int, clamp, (+), (-), (<), (<<), (<=), (>>), (|>), ) import Char (Char) import qualified Data.Text import List (List) import qualified List import Maybe (Maybe) import qualified Text.Read import Prelude (otherwise) import qualified Prelude -- | A @Text@ is a chunk of text: -- -- > "Hello!" -- > "How are you?" -- > "🙈🙉🙊" -- > -- > -- strings with escape characters -- > "this\n\t\"that\"" -- > "\x1F648\x1F649\x1F64A" -- "🙈🙉🙊" -- -- A @Text@ can represent any sequence of [unicode characters](https://en.wikipedia.org/wiki/Unicode). You can use the unicode escapes from @\x0000@ to @\x10FFFF@ to represent characters by their code point. You can also include the unicode characters directly. Using the escapes can be better if you need one of the many whitespace characters with different widths. type Text = Data.Text.Text -- | Determine if a string is empty. -- -- > isEmpty "" == True -- > isEmpty "the world" == False isEmpty :: Text -> Bool isEmpty = Data.Text.null -- | Get the length of a string. -- -- > length "innumerable" == 11 -- > length "" == 0 length :: Text -> Int length = Data.Text.length >> Prelude.fromIntegral -- | Reverse a string. -- -- > reverse "stressed" == "desserts" reverse :: Text -> Text reverse = Data.Text.reverse -- | Repeat a string /n/ times. -- -- > repeat 3 "ha" == "hahaha" repeat :: Int -> Text -> Text repeat = Prelude.fromIntegral >> Data.Text.replicate -- | Replace all occurrences of some substring. -- -- > replace "." "-" "Json.Decode.succeed" == "Json-Decode-succeed" -- > replace "," "/" "a,b,c,d,e" == "a/b/c/d/e" replace :: Text -> Text -> Text -> Text replace = Data.Text.replace -- BUILDING AND SPLITTING -- | Append two strings. You can also use the @(++)@ operator to do this. -- -- > append "butter" "fly" == "butterfly" append :: Text -> Text -> Text append = Data.Text.append -- | Concatenate many strings into one. -- -- > concat ["never","the","less"] == "nevertheless" concat :: List Text -> Text concat = Data.Text.concat -- | Split a string using a given separator. -- -- > split "," "cat,dog,cow" == ["cat","dog","cow"] -- > split "/" "home/evan/Desktop/" == ["home","evan","Desktop", ""] split :: Text -> Text -> List Text split = Data.Text.splitOn -- | Put many strings together with a given separator. -- -- > join "a" ["H","w","ii","n"] == "Hawaiian" -- > join " " ["cat","dog","cow"] == "cat dog cow" -- > join "/" ["home","evan","Desktop"] == "home/evan/Desktop" join :: Text -> List Text -> Text join = Data.Text.intercalate -- | Break a string into words, splitting on chunks of whitespace. -- -- > words "How are \t you? \n Good?" == ["How","are","you?","Good?"] words :: Text -> List Text words = Data.Text.words -- | Break a string into lines, splitting on newlines. -- -- > lines "How are you?\nGood?" == ["How are you?", "Good?"] lines :: Text -> List Text lines = Data.Text.lines -- SUBSTRINGS -- | Take a substring given a start and end index. Negative indexes -- are taken starting from the /end/ of the list. -- -- > slice 7 9 "snakes on a plane!" == "on" -- > slice 0 6 "snakes on a plane!" == "snakes" -- > slice 0 -7 "snakes on a plane!" == "snakes on a" -- > slice -6 -1 "snakes on a plane!" == "plane" slice :: Int -> Int -> Text -> Text slice from to text | to' - from' <= 0 = Data.Text.empty | otherwise = Data.Text.drop from' (Data.Text.take to' text) where len = Data.Text.length text handleNegative value | value < 0 = len + value | otherwise = value normalize = Prelude.fromIntegral >> handleNegative >> clamp 0 len from' = normalize from to' = normalize to -- | Take /n/ characters from the left side of a string. -- -- > left 2 "Mulder" == "Mu" left :: Int -> Text -> Text left = Prelude.fromIntegral >> Data.Text.take -- | Take /n/ characters from the right side of a string. -- -- > right 2 "Scully" == "ly" right :: Int -> Text -> Text right = Prelude.fromIntegral >> Data.Text.takeEnd -- | Drop /n/ characters from the left side of a string. -- -- > dropLeft 2 "The Lone Gunmen" == "e Lone Gunmen" dropLeft :: Int -> Text -> Text dropLeft = Prelude.fromIntegral >> Data.Text.drop -- | Drop /n/ characters from the right side of a string. -- -- > dropRight 2 "Cigarette Smoking Man" == "Cigarette Smoking M" dropRight :: Int -> Text -> Text dropRight = Prelude.fromIntegral >> Data.Text.dropEnd -- DETECT SUBSTRINGS -- | See if the second string contains the first one. -- -- > contains "the" "theory" == True -- > contains "hat" "theory" == False -- > contains "THE" "theory" == False contains :: Text -> Text -> Bool contains = Data.Text.isInfixOf -- | See if the second string starts with the first one. -- -- > startsWith "the" "theory" == True -- > startsWith "ory" "theory" == False startsWith :: Text -> Text -> Bool startsWith = Data.Text.isPrefixOf -- | See if the second string ends with the first one. -- -- > endsWith "the" "theory" == False -- > endsWith "ory" "theory" == True endsWith :: Text -> Text -> Bool endsWith = Data.Text.isSuffixOf -- | Get all of the indexes for a substring in another string. -- -- > indexes "i" "Mississippi" == [1,4,7,10] -- > indexes "ss" "Mississippi" == [2,5] -- > indexes "needle" "haystack" == [] indexes :: Text -> Text -> List Int indexes n h | isEmpty n = [] | otherwise = indexes' n h where indexes' needle haystack = Data.Text.breakOnAll needle haystack |> List.map ( \(lhs, _) -> Data.Text.length lhs |> Prelude.fromIntegral ) -- | Alias for @indexes@. indices :: Text -> Text -> List Int indices = indexes -- FORMATTING -- | Convert a string to all upper case. Useful for case-insensitive comparisons -- and VIRTUAL YELLING. -- -- > toUpper "skinner" == "SKINNER" toUpper :: Text -> Text toUpper = Data.Text.toUpper -- | Convert a string to all lower case. Useful for case-insensitive comparisons. -- -- > toLower "X-FILES" == "x-files" toLower :: Text -> Text toLower = Data.Text.toLower -- | Pad a string on both sides until it has a given length. -- -- > pad 5 ' ' "1" == " 1 " -- > pad 5 ' ' "11" == " 11 " -- > pad 5 ' ' "121" == " 121 " pad :: Int -> Char -> Text -> Text pad = Prelude.fromIntegral >> Data.Text.center -- | Pad a string on the left until it has a given length. -- -- > padLeft 5 '.' "1" == "....1" -- > padLeft 5 '.' "11" == "...11" -- > padLeft 5 '.' "121" == "..121" padLeft :: Int -> Char -> Text -> Text padLeft = Prelude.fromIntegral >> Data.Text.justifyRight -- | Pad a string on the right until it has a given length. -- -- > padRight 5 '.' "1" == "1...." -- > padRight 5 '.' "11" == "11..." -- > padRight 5 '.' "121" == "121.." padRight :: Int -> Char -> Text -> Text padRight = Prelude.fromIntegral >> Data.Text.justifyLeft -- | Get rid of whitespace on both sides of a string. -- -- > trim " hats \n" == "hats" trim :: Text -> Text trim = Data.Text.strip -- | Get rid of whitespace on the left of a string. -- -- > trimLeft " hats \n" == "hats \n" trimLeft :: Text -> Text trimLeft = Data.Text.stripStart -- | Get rid of whitespace on the right of a string. -- -- > trimRight " hats \n" == " hats" trimRight :: Text -> Text trimRight = Data.Text.stripEnd -- INT CONVERSIONS -- | Try to convert a string into an int, failing on improperly formatted strings. -- -- > Text.toInt "123" == Just 123 -- > Text.toInt "-42" == Just -42 -- > Text.toInt "3.1" == Nothing -- > Text.toInt "31a" == Nothing -- -- If you are extracting a number from some raw user input, you will typically -- want to use [@Maybe.withDefault@](Maybe#withDefault) to handle bad data: -- -- > Maybe.withDefault 0 (Text.toInt "42") == 42 -- > Maybe.withDefault 0 (Text.toInt "ab") == 0 toInt :: Text -> Maybe Int toInt text = Text.Read.readMaybe str' where str = Data.Text.unpack text str' = case str of '+' : rest -> rest other -> other -- | Convert an @Int@ to a @Text@. -- -- > Text.fromInt 123 == "123" -- > Text.fromInt -42 == "-42" fromInt :: Int -> Text fromInt = Data.Text.pack << Prelude.show -- FLOAT CONVERSIONS -- | Try to convert a string into a float, failing on improperly formatted strings. -- -- > Text.toFloat "123" == Just 123.0 -- > Text.toFloat "-42" == Just -42.0 -- > Text.toFloat "3.1" == Just 3.1 -- > Text.toFloat "31a" == Nothing -- -- If you are extracting a number from some raw user input, you will typically -- want to use [@Maybe.withDefault@](Maybe#withDefault) to handle bad data: -- -- > Maybe.withDefault 0 (Text.toFloat "42.5") == 42.5 -- > Maybe.withDefault 0 (Text.toFloat "cats") == 0 toFloat :: Text -> Maybe Float toFloat text = Text.Read.readMaybe str' where str = Data.Text.unpack text str' = case str of '+' : rest -> rest '.' : rest -> '0' : '.' : rest other -> other -- | Convert a @Float@ to a @Text@. -- -- > Text.fromFloat 123 == "123" -- > Text.fromFloat -42 == "-42" -- > Text.fromFloat 3.9 == "3.9" fromFloat :: Float -> Text fromFloat = Data.Text.pack << Prelude.show -- LIST CONVERSIONS -- | Convert a Text to a list of characters. -- -- > toList "abc" == ['a','b','c'] -- > toList "🙈🙉🙊" == ['🙈','🙉','🙊'] toList :: Text -> List Char toList = Data.Text.unpack -- | Convert a list of characters into a Text. Can be useful if you -- want to create a string primarily by consing, perhaps for decoding -- something. -- -- > fromList ['a','b','c'] == "abc" -- > fromList ['🙈','🙉','🙊'] == "🙈🙉🙊" fromList :: List Char -> Text fromList = Data.Text.pack -- CHAR CONVERSIONS -- | Create a Text from a given character. -- -- > fromChar 'a' == "a" fromChar :: Char -> Text fromChar = Data.Text.singleton -- | Add a character to the beginning of a Text. -- -- > cons 'T' "he truth is out there" == "The truth is out there" cons :: Char -> Text -> Text cons = Data.Text.cons -- | Split a non-empty Text into its head and tail. This lets you -- pattern match on strings exactly as you would with lists. -- -- > uncons "abc" == Just ('a',"bc") -- > uncons "" == Nothing uncons :: Text -> Maybe (Char, Text) uncons = Data.Text.uncons -- HIGHER-ORDER FUNCTIONS -- | Transform every character in a Text -- -- > map (\c -> if c == '/' then '.' else c) "a/b/c" == "a.b.c" map :: (Char -> Char) -> Text -> Text map = Data.Text.map -- | Keep only the characters that pass the test. -- -- > filter isDigit "R2-D2" == "22" filter :: (Char -> Bool) -> Text -> Text filter = Data.Text.filter -- | Reduce a Text from the left. -- -- > foldl cons "" "time" == "emit" foldl :: (Char -> b -> b) -> b -> Text -> b foldl f = Data.Text.foldl' (\a b -> f b a) -- | Reduce a Text from the right. -- -- > foldr cons "" "time" == "time" foldr :: (Char -> b -> b) -> b -> Text -> b foldr = Data.Text.foldr -- | Determine whether /any/ characters pass the test. -- -- > any isDigit "90210" == True -- > any isDigit "R2-D2" == True -- > any isDigit "heart" == False any :: (Char -> Bool) -> Text -> Bool any = Data.Text.any -- | Determine whether /all/ characters pass the test. -- -- > all isDigit "90210" == True -- > all isDigit "R2-D2" == False -- > all isDigit "heart" == False all :: (Char -> Bool) -> Text -> Bool all = Data.Text.all