antelude-0.1.0: src/Antelude/String/Case.hs
{- |
Module : Antelude.String.Case
Description : Contains some functions for handling cases for Strings.
Maintainer : dneavesdev@pm.me
-}
module Antelude.String.Case
( Case (..)
-- * String Deconstruction/Reconstruction
, buildCase
, identifyCase
, recase
, uncase
-- * Simple Conversions
, lowerCase
, screamingCase
, titleCase
) where
import safe Antelude.Bool ( and, otherwise )
import safe qualified Antelude.Bool as Bool ( all )
import safe Antelude.Function ( (.>), (|>) )
import safe Antelude.Internal.TypesClasses
( Bool (..)
, Char
, Eq
, List
, Maybe (..)
, Ord ((>))
, Show
, String
, (<>)
)
import safe qualified Antelude.List as List
import safe qualified Antelude.Maybe as Maybe ( map )
import safe qualified Antelude.String as String
import safe qualified Antelude.Tuple.Pair as Pair ( first )
import safe qualified Data.Char as Char
( isAlpha
, isAlphaNum
, isLowerCase
, isUpperCase
, toLower
, toUpper
)
-- | Convert an entire 'String' to lower case. May be good for preprocessing an ambiguous-case string before 'recase'-ing, but results vary depending on the string.
lowerCase :: String -> String
lowerCase = List.map Char.toLower
-- | Convert an entire 'String' to SCREAMING CASE. May be good for preprocessing an ambiguous-case string before 'recase'-ing, but results vary depending on the string.
screamingCase :: String -> String
screamingCase = List.map Char.toUpper
-- | Convert an entire 'String' to Title case. Results may vary depending on the string.
titleCase :: String -> String
titleCase string =
List.uncons string
|> \case
Just (head, tail) ->
[Char.toUpper head] <> List.map Char.toLower tail
Nothing ->
string
{- |
A listing of all real and theoretical cases to convert from/to.
For reference:
- sentence cases use spaces like a regular sentence
- snake_cases_use_underscores_between_words
- hyphen-cases-use-hyphens-as-one-would-expect
- dot.cases.use.periods.maybe.useful.for.codegen
- forward\/path\/case\/uses\/forward\/slashes
- back\\path\\case\\uses\\back\\slashes
- camelCase and PascalCase look like these
-}
data Case
-- sentence cases
= LowerSentenceCase
| UpperSentenceCase
| ScreamingSentenceCase
| TitleSentenceCase
-- snake_cases
| LowerSnakeCase
| UpperSnakeCase
| ScreamingSnakeCase
-- hyphen-cases
| LowerHyphenCase
| UpperHyphenCase
| ScreamingHyphenCase
-- dot.cases
| LowerDotCase
| UpperDotCase
| ScreamingDotCase
-- forward/path/case
| LowerForwardPathCase
| UpperForwardPathCase
| ScreamingForwardPathCase
-- back\path\case
| LowerBackPathCase
| UpperBackPathCase
| ScreamingBackPathCase
-- camelCases
| CamelCase
| PascalCase
deriving (Eq, Show)
-- | Identify the 'Case' the 'String' is using
identifyCase :: String -> Maybe Case
identifyCase string =
let checkSingleCase :: (Char -> Bool) -> String -> String -> Bool
checkSingleCase func sep str =
String.contains sep str
`and`
( String.split sep str
|> List.map
( List.filter Char.isAlpha
.> List.all func
)
|> Bool.all
)
checkCapitalCase :: String -> String -> Bool
checkCapitalCase sep str =
String.contains sep str
`and`
( String.split sep str
|> List.filter (\s -> List.length s > 0)
|> List.map
( List.filter Char.isAlpha
.> List.uncons
.> \case
Just (head, tail) ->
Char.isUpperCase head
`and`
(List.map Char.isLowerCase .> Bool.all) tail
Nothing ->
True
)
|> Bool.all
)
checkTitleCase :: String -> String -> Bool
checkTitleCase sep str =
String.contains sep str
`and`
( List.head str
|> \case
Just head ->
Char.isUpperCase head
Nothing ->
True
)
in
if | checkSingleCase Char.isUpperCase " " string
-> Just ScreamingSentenceCase
| checkCapitalCase " " string
-> Just UpperSentenceCase
| checkSingleCase Char.isLowerCase " " string
-> Just LowerSentenceCase
| checkTitleCase " " string
-> Just TitleSentenceCase
-- Checks indefinitely from here-on
| checkSingleCase Char.isUpperCase "-" string
-> Just ScreamingHyphenCase
| checkCapitalCase "-" string
-> Just UpperHyphenCase
| checkSingleCase Char.isLowerCase "-" string
-> Just LowerHyphenCase
| checkSingleCase Char.isUpperCase "_" string
-> Just ScreamingSnakeCase
| checkCapitalCase "_" string
-> Just UpperSnakeCase
| checkSingleCase Char.isLowerCase "_" string
-> Just LowerSnakeCase
| checkSingleCase Char.isUpperCase "." string
-> Just ScreamingDotCase
| checkCapitalCase "." string
-> Just UpperDotCase
| checkSingleCase Char.isLowerCase "." string
-> Just LowerDotCase
| checkSingleCase Char.isUpperCase "/" string
-> Just ScreamingForwardPathCase
| checkCapitalCase "/" string
-> Just UpperForwardPathCase
| checkSingleCase Char.isLowerCase "/" string
-> Just LowerForwardPathCase
| checkSingleCase Char.isUpperCase "\\" string
-> Just ScreamingBackPathCase
| checkCapitalCase "\\" string
-> Just UpperBackPathCase
| checkSingleCase Char.isLowerCase "\\" string
-> Just LowerBackPathCase
| List.all Char.isAlphaNum string
`and`
( List.head
.> \case
Just head -> Char.isLowerCase head
Nothing -> False
) string
-> Just CamelCase
| List.all Char.isAlphaNum string
`and`
( List.head
.> \case
Just head -> Char.isUpperCase head
Nothing -> False
) string
-> Just PascalCase
| otherwise -> Nothing
data Format
= Upper
| Lower
| Screaming
-- Specialies
| Title
| Pascal
| Camel
-- | Construct a 'String' of a desired 'Case' from a 'List' of parts
buildCase :: Case -> List String -> String
buildCase destinationCase parts =
let joiner :: String
joiner =
case destinationCase of
LowerSnakeCase ->
"_"
UpperSnakeCase ->
"_"
ScreamingSnakeCase ->
"_"
LowerHyphenCase ->
"-"
UpperHyphenCase ->
"-"
ScreamingHyphenCase ->
"-"
LowerSentenceCase ->
" "
UpperSentenceCase ->
" "
ScreamingSentenceCase ->
" "
TitleSentenceCase ->
" "
LowerDotCase ->
"."
UpperDotCase ->
"."
ScreamingDotCase ->
"."
LowerForwardPathCase ->
"/"
UpperForwardPathCase ->
"/"
ScreamingForwardPathCase ->
"/"
LowerBackPathCase ->
"\\"
UpperBackPathCase ->
"\\"
ScreamingBackPathCase ->
"\\"
CamelCase ->
""
PascalCase ->
""
format :: Format
format =
case destinationCase of
LowerSnakeCase ->
Lower
UpperSnakeCase ->
Upper
ScreamingSnakeCase ->
Screaming
LowerHyphenCase ->
Lower
UpperHyphenCase ->
Upper
ScreamingHyphenCase ->
Screaming
LowerSentenceCase ->
Lower
UpperSentenceCase ->
Upper
ScreamingSentenceCase ->
Screaming
TitleSentenceCase ->
Title
LowerDotCase ->
Lower
UpperDotCase ->
Upper
ScreamingDotCase ->
Screaming
LowerForwardPathCase ->
Lower
UpperForwardPathCase ->
Upper
ScreamingForwardPathCase ->
Screaming
LowerBackPathCase ->
Lower
UpperBackPathCase ->
Upper
ScreamingBackPathCase ->
Screaming
CamelCase ->
Camel
PascalCase ->
Pascal
in
case format of
Upper ->
List.map
-- while "titleCase" may look odd here, remember that it's _each word_ being Title-ed
titleCase
parts
|> String.joinWith joiner
Lower ->
List.map
lowerCase
parts
|> String.joinWith joiner
Screaming ->
List.map
screamingCase
parts
|> String.joinWith joiner
Title ->
List.uncons parts
|> \case
Just (frst, rest) ->
[ titleCase frst
]
<> List.map
lowerCase
rest
Nothing ->
[""]
|> String.joinWith joiner
Pascal ->
List.map
-- every part of a PascalCase is technically Title-ed
titleCase
parts
|> String.joinWith joiner
Camel ->
List.uncons parts
|> \case
Just (frst, rest) ->
[ lowerCase frst
]
<> List.map
-- the tail of a camelCase is technically Title-ed
titleCase
rest
Nothing ->
[""]
|> String.joinWith joiner
{- | Deconstruct a 'String' by identifying its 'Case'.
Returns a 'Maybe', in case the case could not be identified.
-}
uncase :: String -> Maybe (List String)
uncase string =
let oldCase :: Maybe Case
oldCase = identifyCase string
folderFunc :: (List String, String) -> (List String, String)
folderFunc (accm, []) = (accm, [])
folderFunc (accm, str) =
let getStr :: String
getStr =
List.take 1 str
<> ( List.drop 1 str
|> List.takeWhile Char.isLowerCase
)
in folderFunc (accm <> [getStr], List.drop (List.length getStr) str)
pascalCamelFolder :: String -> List String
pascalCamelFolder str =
folderFunc ([], str)
|> Pair.first
in
Maybe.map
(\case
LowerSnakeCase ->
String.split "_" string
UpperSnakeCase ->
String.split "_" string
ScreamingSnakeCase ->
String.split "_" string
LowerHyphenCase ->
String.split "-" string
UpperHyphenCase ->
String.split "-" string
ScreamingHyphenCase ->
String.split "-" string
LowerSentenceCase ->
String.split " " string
UpperSentenceCase ->
String.split " " string
ScreamingSentenceCase ->
String.split " " string
TitleSentenceCase ->
String.split " " string
LowerDotCase ->
String.split "." string
UpperDotCase ->
String.split "." string
ScreamingDotCase ->
String.split "." string
LowerForwardPathCase ->
String.split "/" string
UpperForwardPathCase ->
String.split "/" string
ScreamingForwardPathCase ->
String.split "/" string
LowerBackPathCase ->
String.split "\\" string
UpperBackPathCase ->
String.split "\\" string
ScreamingBackPathCase ->
String.split "\\" string
CamelCase ->
let firstWord :: String
firstWord =
List.takeWhile Char.isLowerCase string
restOfWords :: List String
restOfWords =
string
|> List.drop (List.length firstWord)
|> pascalCamelFolder
in
[firstWord] <> restOfWords
PascalCase ->
pascalCamelFolder string
)
oldCase
{- | Given a desired 'Case' and a 'String', try and convert to the given 'Case'.
Returns a 'Maybe', in case the original case could not be identified.
-}
recase :: Case -> String -> Maybe String
recase destinationCase string =
uncase string
|> Maybe.map (buildCase destinationCase)