packages feed

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)