├── tests
├── .gitignore
├── elm-package.json
├── NonEmptyTest.elm
├── RemoveAccentsTest.elm
├── TestData.elm
├── ClassifyTest.elm
├── DasherizeTest.elm
├── CamelizeTest.elm
├── UnindentTest.elm
├── UnderscoredTest.elm
├── ReplaceSliceTest.elm
├── UnicodeTests.elm
├── HumanizeTest.elm
└── Tests.elm
├── .gitignore
├── .travis.yml
├── elm.json
├── package.json
├── README.md
├── LICENSE
└── src
└── String
└── Extra.elm
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | elm-stuff
2 | elm.js
3 | test.js
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # elm-package generated files
2 | elm-stuff/
3 | # elm-repl generated files
4 | repl-temp-*
5 | node_modules
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: node
3 |
4 |
5 | cache:
6 | directories:
7 | - elm-stuff/build-artifacts
8 | - elm-stuff/packages
9 | - test/elm-stuff/build-artifacts
10 | - test/elm-stuff/packages
11 | - sysconfcpus
12 |
13 | before_install:
14 | - if [ ${TRAVIS_OS_NAME} == "osx" ];
15 | then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh;
16 | fi
17 |
18 | install:
19 | - npm install
20 |
21 | script:
22 | - npm run test
23 |
--------------------------------------------------------------------------------
/elm.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "package",
3 | "name": "elm-community/string-extra",
4 | "summary": "String helper functions for Elm",
5 | "license": "BSD-3-Clause",
6 | "version": "4.0.0",
7 | "exposed-modules": [
8 | "String.Extra"
9 | ],
10 | "elm-version": "0.19.0 <= v < 0.20.0",
11 | "dependencies": {
12 | "elm/core": "1.0.0 <= v < 2.0.0",
13 | "elm/regex": "1.0.0 <= v < 2.0.0"
14 | },
15 | "test-dependencies": {
16 | "elm-explorations/test": "1.2.1 <= v < 2.0.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "summary": "String Addons library tests",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | ".",
8 | "../src"
9 | ],
10 | "exposed-modules": [],
11 | "dependencies": {
12 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0",
13 | "elm-community/shrink": "2.0.0 <= v < 3.0.0",
14 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
15 | "mgold/elm-random-pcg": "4.0.2 <= v < 5.0.0"
16 | },
17 | "elm-version": "0.18.0 <= v < 0.19.0"
18 | }
19 |
--------------------------------------------------------------------------------
/tests/NonEmptyTest.elm:
--------------------------------------------------------------------------------
1 | module NonEmptyTest exposing (nonEmptyTest)
2 |
3 | import Expect
4 | import String.Extra exposing (nonEmpty)
5 | import Test exposing (..)
6 |
7 |
8 | nonEmptyTest : Test
9 | nonEmptyTest =
10 | describe "nonEmpty"
11 | [ test "Should result in a just when string has greater length than 0" <|
12 | \() ->
13 | nonEmpty "Hello world"
14 | |> Expect.equal (Just "Hello world")
15 | , test "Should result in Nothing when an empty string is passed in" <|
16 | \() ->
17 | nonEmpty ""
18 | |> Expect.equal Nothing
19 | ]
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "string-extra",
3 | "version": "1.4.0",
4 | "description": "String helper functions for Elm",
5 | "main": "index.js",
6 | "directories": {
7 | "test": "tests"
8 | },
9 | "scripts": {
10 | "test": "elm-test"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/elm-community/string-extra.git"
15 | },
16 | "author": "",
17 | "license": "ISC",
18 | "bugs": {
19 | "url": "https://github.com/elm-community/string-extra/issues"
20 | },
21 | "homepage": "https://github.com/elm-community/string-extra#readme",
22 | "dependencies": {
23 | "elm": "0.19.0-bugfix6",
24 | "elm-test": "0.19.0-rev5"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/RemoveAccentsTest.elm:
--------------------------------------------------------------------------------
1 | module RemoveAccentsTest exposing (removeAccentsTest)
2 |
3 | import Expect
4 | import String.Extra exposing (removeAccents)
5 | import Test exposing (..)
6 |
7 |
8 | removeAccentsTest : Test
9 | removeAccentsTest =
10 | describe "removeAccents"
11 | [ test "Should result string without accents" <|
12 | \() ->
13 | removeAccents "áàãâäéèêëíìîïóòõôöúùûüçÁÀÃÂÄÉÈÊËÍÌÎÏÓÒÕÖÔÚÙÛÜÇ"
14 | |> Expect.equal "aaaaaeeeeiiiiooooouuuucAAAAAEEEEIIIIOOOOOUUUUC"
15 | , test "Should result in phrase without accents" <|
16 | \() ->
17 | removeAccents "andré JOÂO"
18 | |> Expect.equal "andre JOAO"
19 | ]
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | string-extra
2 | ============
3 |
4 | String helper functions for Elm, inspired by [underscore.string](http://epeli.github.io/underscore.string/).
5 |
6 | [](https://travis-ci.org/elm-community/string-extra)
7 |
8 | Contributing
9 | ------------
10 |
11 | Pull requests are welcome. If you want to talk to us, join us on the
12 | `#elm-community` or `#string-extra` channels on the [Elm Slack](https://elmlang.slack.com).
13 |
14 | Testing
15 | -------
16 |
17 | ```
18 | npm i
19 | npm run test
20 | ```
21 |
22 | Maintainers
23 | -----------
24 |
25 | This package is maintained by
26 |
27 | - [José Lorenzo Rodríguez](https://github.com/lorenzo)
28 | - [Jaap Broekhuizen](https://github.com/jaapz)
29 |
30 | Thanks
31 | ------
32 |
33 | This package is an effort to consolidate [lorenzo/elm-string-addons](https://github.com/lorenzo/elm-string-addons),
34 | [NoRedInk/elm-string-extra](https://github.com/NoRedInk/elm-string-extra), and
35 | add utility functions inspired by [underscore.string](http://epeli.github.io/underscore.string/).
36 |
37 | So thanks to the authors of those libraries for making `string-extra` possible.
38 |
39 | License
40 | -------
41 |
42 | The source code for this package is released under the terms of the BSD3
43 | license. See the `LICENSE` file.
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Elm Community
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of string-extra nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/tests/TestData.elm:
--------------------------------------------------------------------------------
1 | module TestData exposing
2 | ( randomString
3 | , randomStrings
4 | , randomStringsWithCharGenerators
5 | , randomStringsWithChars
6 | )
7 |
8 | import Fuzz exposing (Fuzzer)
9 | import Random
10 | import Shrink
11 |
12 |
13 | withChar : List Char -> Random.Generator Char
14 | withChar ch =
15 | withCharGenerators <| List.map Random.constant ch
16 |
17 |
18 | withCharGenerators : List (Random.Generator Char) -> Random.Generator Char
19 | withCharGenerators charGenerators =
20 | Random.andThen identity <|
21 | Random.uniform (Random.map Char.fromCode (Random.int 97 122)) <|
22 | Random.map Char.fromCode (Random.int 65 90)
23 | :: charGenerators
24 |
25 |
26 | randomString : Random.Generator String
27 | randomString =
28 | randomStringWithCharGenerators [ withChar [] ]
29 |
30 |
31 | randomStringWithCharGenerators : List (Random.Generator Char) -> Random.Generator String
32 | randomStringWithCharGenerators charGenerators =
33 | Random.int 1 10
34 | |> Random.andThen (\i -> Random.map String.fromList <| Random.list i (withCharGenerators charGenerators))
35 |
36 |
37 | randomStrings : Fuzzer String
38 | randomStrings =
39 | randomStringsWithChars []
40 |
41 |
42 | randomStringsWithChars : List Char -> Fuzzer String
43 | randomStringsWithChars chars =
44 | randomStringsWithCharGenerators [ withChar chars ]
45 |
46 |
47 | randomStringsWithCharGenerators : List (Random.Generator Char) -> Fuzzer String
48 | randomStringsWithCharGenerators charGenerators =
49 | Fuzz.custom (randomStringWithCharGenerators charGenerators) Shrink.string
50 |
--------------------------------------------------------------------------------
/tests/ClassifyTest.elm:
--------------------------------------------------------------------------------
1 | module ClassifyTest exposing (classifyTest)
2 |
3 | import Char
4 | import Expect
5 | import Fuzz exposing (..)
6 | import Random
7 | import Regex
8 | import String exposing (replace, uncons)
9 | import String.Extra exposing (..)
10 | import Test exposing (..)
11 | import TestData
12 | import Tuple exposing (first, second)
13 |
14 |
15 | classifyTest : Test
16 | classifyTest =
17 | describe "classify"
18 | [ fuzz string "It does not contain non-word characters" <|
19 | \string ->
20 | classify string
21 | |> Regex.contains (Regex.fromString "[\\W]" |> Maybe.withDefault Regex.never)
22 | |> Expect.false "Non word characters detected"
23 | , fuzz TestData.randomStrings "It starts with an uppercase letter" <|
24 | \string ->
25 | string
26 | |> classify
27 | |> uncons
28 | |> Maybe.map first
29 | |> Expect.equal (string |> String.trim |> String.toUpper |> uncons |> Maybe.map first)
30 | , fuzz validWords "It is camelized once replaced non word charactes with a compatible string" <|
31 | \string ->
32 | string
33 | |> classify
34 | |> uncons
35 | |> Maybe.map second
36 | |> Expect.equal (string |> replace "." "-" |> camelize |> uncons |> Maybe.map second)
37 | ]
38 |
39 |
40 | validWords : Fuzzer String
41 | validWords =
42 | TestData.randomStringsWithCharGenerators
43 | [ Random.map Char.fromCode (Random.int 45 46)
44 | , Random.constant (Char.fromCode 95)
45 | ]
46 |
--------------------------------------------------------------------------------
/tests/DasherizeTest.elm:
--------------------------------------------------------------------------------
1 | module DasherizeTest exposing (dasherizeTest)
2 |
3 | import Char
4 | import Expect
5 | import Fuzz exposing (..)
6 | import String exposing (replace, uncons)
7 | import String.Extra exposing (..)
8 | import Test exposing (..)
9 | import TestData
10 |
11 |
12 | dasherizeTest : Test
13 | dasherizeTest =
14 | describe "dasherize"
15 | [ fuzz string "It is a lowercased string" <|
16 | \s ->
17 | dasherize s
18 | |> String.toLower
19 | |> Expect.equal (dasherize s)
20 | , fuzz string "It replaces spaces and underscores with a dash" <|
21 | \s ->
22 | let
23 | expected =
24 | String.toLower
25 | >> String.trim
26 | >> replace " " " "
27 | >> replace " " "-"
28 | >> replace "_" "-"
29 | >> replace "--" "-"
30 | >> replace "--" "-"
31 | in
32 | dasherize (String.toLower s)
33 | |> String.toLower
34 | |> Expect.equal (expected s)
35 | , fuzz TestData.randomStrings "It puts dash before every single uppercase character" <|
36 | \s ->
37 | dasherize s
38 | |> Expect.equal (replaceUppercase s |> String.toLower)
39 | ]
40 |
41 |
42 | replaceUppercase : String -> String
43 | replaceUppercase string =
44 | string
45 | |> String.toList
46 | |> List.map
47 | (\c ->
48 | if Char.isUpper c then
49 | "-" ++ String.fromChar c
50 |
51 | else
52 | String.fromChar c
53 | )
54 | |> String.join ""
55 |
--------------------------------------------------------------------------------
/tests/CamelizeTest.elm:
--------------------------------------------------------------------------------
1 | module CamelizeTest exposing (camelizeTest)
2 |
3 | import Char
4 | import Expect
5 | import Fuzz exposing (..)
6 | import Regex
7 | import String exposing (replace, uncons)
8 | import String.Extra exposing (..)
9 | import Test exposing (..)
10 | import TestData
11 |
12 |
13 | camelizeTest : Test
14 | camelizeTest =
15 | describe "camelize"
16 | [ fuzz string "It does not contain dashes" <|
17 | \s ->
18 | camelize s
19 | |> String.contains "-"
20 | |> Expect.false "Camelize should remove dashes"
21 | , fuzz string "It does not contain underscores" <|
22 | \s ->
23 | camelize s
24 | |> String.contains "-"
25 | |> Expect.false "Camelize should remove underscores"
26 | , fuzz string "It is the same lowercased string after removing the dashes and spaces" <|
27 | \s ->
28 | let
29 | expected =
30 | replace "-" ""
31 | >> replace "_" ""
32 | >> Regex.replace (Regex.fromString "\\s+" |> Maybe.withDefault Regex.never) (\_ -> "")
33 | >> String.toLower
34 | in
35 | camelize s
36 | |> String.toLower
37 | |> Expect.equal (expected s)
38 | , fuzz (validWords '-') "The first letter after each dash is capitalized" <|
39 | \s ->
40 | camelize s
41 | |> Expect.equal (runCamelize "-" s)
42 | , fuzz (validWords ' ') "The first letter after each space is capitalized" <|
43 | \s ->
44 | camelize s
45 | |> Expect.equal (runCamelize " " s)
46 | ]
47 |
48 |
49 | runCamelize : String -> String -> String
50 | runCamelize separator string =
51 | string
52 | |> String.trim
53 | |> replace (separator ++ separator) separator
54 | |> String.split separator
55 | |> List.indexedMap capitalizeOdds
56 | |> String.join ""
57 |
58 |
59 | capitalizeOdds : Int -> String -> String
60 | capitalizeOdds pos str =
61 | if pos > 0 then
62 | toSentenceCase str
63 |
64 | else
65 | str
66 |
67 |
68 | validWords : Char -> Fuzzer String
69 | validWords ch =
70 | TestData.randomStringsWithChars [ ch ]
71 |
--------------------------------------------------------------------------------
/tests/UnindentTest.elm:
--------------------------------------------------------------------------------
1 | module UnindentTest exposing (unindentTest)
2 |
3 | import Expect
4 | import Fuzz exposing (..)
5 | import String exposing (uncons)
6 | import String.Extra exposing (..)
7 | import Test exposing (..)
8 | import Tuple exposing (first)
9 |
10 |
11 | unindentTest : Test
12 | unindentTest =
13 | describe "unindent"
14 | [ fuzz multilineProducerString "It produces the same trimmed string" <|
15 | \s ->
16 | let
17 | expected =
18 | String.lines >> List.map String.trimLeft
19 | in
20 | unindent s
21 | |> String.lines
22 | |> List.map String.trimLeft
23 | |> Expect.equal (expected s)
24 | , fuzz multilineProducerString "It produces at least one line with no leading whitespace" <|
25 | \s ->
26 | unindent s
27 | |> String.lines
28 | |> List.map (not << String.startsWith " ")
29 | |> List.any ((==) True)
30 | |> Expect.true "No lines with leading whitespace detected"
31 | , fuzz multilineProducer "All lines' length have been reduced by exactly the minimum indentation" <|
32 | \( s, spaces ) ->
33 | let
34 | expected =
35 | String.lines s
36 | |> List.map String.length
37 | |> List.map (\i -> i - spaces)
38 | in
39 | unindent s
40 | |> String.lines
41 | |> List.map String.length
42 | |> Expect.equal expected
43 | ]
44 |
45 |
46 | multilineProducerString : Fuzzer String
47 | multilineProducerString =
48 | map (convertToMultiline >> first)
49 | (tuple3 ( intRange 0 10, intRange 0 10, intRange 0 10 ))
50 |
51 |
52 | multilineProducer : Fuzzer ( String, Int )
53 | multilineProducer =
54 | map convertToMultiline
55 | (tuple3 ( intRange 0 10, intRange 0 10, intRange 0 10 ))
56 |
57 |
58 | convertToMultiline : ( Int, Int, Int ) -> ( String, Int )
59 | convertToMultiline ( a, b, c ) =
60 | ( [ String.repeat a " " ++ "aaaa aaa "
61 | , String.repeat b " " ++ "aaaa aaa"
62 | , String.repeat c " " ++ "ccc "
63 | ]
64 | |> String.join "\n"
65 | , min (min a b) c
66 | )
67 |
--------------------------------------------------------------------------------
/tests/UnderscoredTest.elm:
--------------------------------------------------------------------------------
1 | module UnderscoredTest exposing (underscoredTest)
2 |
3 | import Char
4 | import Expect
5 | import Fuzz exposing (..)
6 | import String exposing (replace, uncons)
7 | import String.Extra exposing (..)
8 | import Test exposing (..)
9 | import TestData
10 |
11 |
12 | underscoredTest : Test
13 | underscoredTest =
14 | describe "underscored"
15 | [ fuzz string "It is a lowercased string" <|
16 | \s ->
17 | underscored s
18 | |> String.toLower
19 | |> Expect.equal (underscored s |> String.toLower)
20 | , fuzz string "It replaces spaces and dashes with an underscore" <|
21 | \s ->
22 | let
23 | expected =
24 | String.toLower
25 | >> String.trim
26 | >> replace " " " "
27 | >> replace " " "-"
28 | >> replace "-" "_"
29 | >> replace "__" "_"
30 | >> replace "__" "_"
31 | in
32 | underscored (String.toLower s)
33 | |> Expect.equal (expected s)
34 | , fuzz TestData.randomStrings "It puts an underscore before each uppercase characters group unless it starts with uppercase" <|
35 | \s ->
36 | underscored s
37 | |> Expect.equal (replaceUppercase s |> String.toLower)
38 | ]
39 |
40 |
41 | replaceUppercase : String -> String
42 | replaceUppercase string =
43 | string
44 | |> String.toList
45 | |> List.indexedMap Tuple.pair
46 | |> List.foldr recordUpperCasePositions []
47 | |> List.foldl reduceList []
48 | |> List.foldl replacePositions string
49 |
50 |
51 | recordUpperCasePositions : ( Int, Char ) -> List ( Int, Char ) -> List ( Int, Char )
52 | recordUpperCasePositions ( index, char ) acc =
53 | if Char.isUpper char then
54 | ( index, char ) :: acc
55 |
56 | else
57 | acc
58 |
59 |
60 | reduceList : ( Int, Char ) -> List ( Int, Int, Char ) -> List ( Int, Int, Char )
61 | reduceList ( index, char ) acc =
62 | case acc of
63 | ( start, end, c ) :: rest ->
64 | if index == end + 1 then
65 | ( start, index, c ) :: rest
66 |
67 | else
68 | ( index, index, char ) :: acc
69 |
70 | [] ->
71 | ( index, index, char ) :: acc
72 |
73 |
74 | replacePositions : ( Int, Int, Char ) -> String -> String
75 | replacePositions ( start, _, c ) string =
76 | if start == 0 then
77 | string
78 |
79 | else
80 | replaceSlice ("_" ++ String.fromChar c) start (start + 1) string
81 |
--------------------------------------------------------------------------------
/tests/ReplaceSliceTest.elm:
--------------------------------------------------------------------------------
1 | module ReplaceSliceTest exposing (replaceSliceTest)
2 |
3 | import Expect
4 | import Fuzz exposing (..)
5 | import Random
6 | import Shrink
7 | import String
8 | import String.Extra exposing (..)
9 | import Test exposing (..)
10 | import TestData
11 |
12 |
13 | replaceSliceTest : Test
14 | replaceSliceTest =
15 | describe "replaceSlice"
16 | [ fuzz replaceSliceProducer "Result contains the substitution string" <|
17 | \( ( string, sub ), ( start, end ) ) ->
18 | case string of
19 | "" ->
20 | replaceSlice sub start end string
21 | |> Expect.equal sub
22 |
23 | _ ->
24 | replaceSlice sub start end string
25 | |> String.contains sub
26 | |> Expect.true "The slice was not subtituted"
27 | , fuzz replaceSliceProducer "Result string has the length of the substitution + string after removing the slice" <|
28 | \( ( string, sub ), ( start, end ) ) ->
29 | case string of
30 | "" ->
31 | replaceSlice sub start end string
32 | |> String.length
33 | |> Expect.equal (String.length sub)
34 |
35 | _ ->
36 | replaceSlice sub start end string
37 | |> String.length
38 | |> Expect.equal ((String.length string - (end - start)) + String.length sub)
39 | , fuzz replaceSliceProducer "Start of the original string remains the same" <|
40 | \( ( string, sub ), ( start, end ) ) ->
41 | case string of
42 | "" ->
43 | replaceSlice sub start end string
44 | |> Expect.equal sub
45 |
46 | _ ->
47 | replaceSlice sub start end string
48 | |> String.slice 0 start
49 | |> Expect.equal (String.slice 0 start string)
50 | , fuzz replaceSliceProducer "End of the original string remains the same" <|
51 | \( ( string, sub ), ( start, end ) ) ->
52 | let
53 | replaced =
54 | replaceSlice sub start end string
55 | in
56 | case string of
57 | "" ->
58 | replaced
59 | |> Expect.equal sub
60 |
61 | _ ->
62 | replaced
63 | |> String.slice (start + String.length sub) (String.length replaced)
64 | |> Expect.equal (String.slice end (String.length string) string)
65 | ]
66 |
67 |
68 | replaceSliceProducer : Fuzzer ( ( String, String ), ( Int, Int ) )
69 | replaceSliceProducer =
70 | let
71 | producer =
72 | Random.map2 Tuple.pair TestData.randomString TestData.randomString
73 | |> Random.andThen (\( str, sub ) -> Random.pair (Random.constant ( str, sub )) (Random.int 0 <| String.length str))
74 | |> Random.andThen (\( ( str, sub ), start ) -> Random.pair (Random.constant ( str, sub )) (Random.pair (Random.constant start) (Random.int start <| String.length str)))
75 | in
76 | Fuzz.custom producer Shrink.noShrink
77 |
--------------------------------------------------------------------------------
/tests/UnicodeTests.elm:
--------------------------------------------------------------------------------
1 | module UnicodeTests exposing (unicodeTests)
2 |
3 | import Char
4 | import Expect
5 | import Fuzz exposing (..)
6 | import String
7 | import String.Extra exposing (..)
8 | import Test exposing (..)
9 |
10 |
11 | bmpCodePointFuzzer : Fuzzer Int
12 | bmpCodePointFuzzer =
13 | frequency
14 | [ ( 1, intRange 0 0xD7FF )
15 | , ( 1, intRange 0xE000 0xFFFF )
16 | ]
17 |
18 |
19 | unicodeStringFuzzer : Fuzzer String
20 | unicodeStringFuzzer =
21 | let
22 | singletonFuzzer =
23 | bmpCodePointFuzzer |> map (\codePoint -> [ codePoint ])
24 |
25 | leadingSurrogateFuzzer =
26 | intRange 0xD800 0xDBFF
27 |
28 | trailingSurrogateFuzzer =
29 | intRange 0xDC00 0xDFFF
30 |
31 | surrogatePairFuzzer =
32 | tuple ( leadingSurrogateFuzzer, trailingSurrogateFuzzer )
33 | |> map (\( leading, trailing ) -> [ leading, trailing ])
34 |
35 | sublistFuzzer =
36 | frequency
37 | [ ( 1, singletonFuzzer )
38 | , ( 1, surrogatePairFuzzer )
39 | ]
40 | in
41 | list sublistFuzzer
42 | |> map List.concat
43 | |> map (List.map Char.fromCode)
44 | |> map String.fromList
45 |
46 |
47 | codePointFuzzer : Fuzzer Int
48 | codePointFuzzer =
49 | let
50 | astralCodePointFuzzer =
51 | intRange 0x00010000 0x0010FFFF
52 | in
53 | frequency
54 | [ ( 1, bmpCodePointFuzzer )
55 | , ( 1, astralCodePointFuzzer )
56 | ]
57 |
58 |
59 | expectedStringLength : List Int -> Int
60 | expectedStringLength codePoints =
61 | let
62 | numCodeUnits codePoint =
63 | if codePoint <= 0xFFFF then
64 | 1
65 |
66 | else
67 | 2
68 | in
69 | codePoints |> List.map numCodeUnits |> List.sum
70 |
71 |
72 | hardCodedTestCases : List ( String, List Int )
73 | hardCodedTestCases =
74 | [ ( "", [] )
75 | , ( "©§π", [ 169, 167, 960 ] )
76 | , ( "💩!", [ 128169, 33 ] )
77 | , ( "abc", [ 97, 98, 99 ] )
78 | ]
79 |
80 |
81 | unicodeTests : Test
82 | unicodeTests =
83 | describe "unicode"
84 | [ fuzz unicodeStringFuzzer "fromCodePoints is inverse of toCodePoints" <|
85 | \string ->
86 | fromCodePoints (toCodePoints string)
87 | |> Expect.equal string
88 | , fuzz (list codePointFuzzer) "toCodePoints is inverse of fromCodePoints" <|
89 | \codePoints ->
90 | toCodePoints (fromCodePoints codePoints)
91 | |> Expect.equal codePoints
92 | , fuzz (list codePointFuzzer) "string length is greater than or equal to number of code points" <|
93 | \codePoints ->
94 | String.length (fromCodePoints codePoints)
95 | |> Expect.atLeast (List.length codePoints)
96 | , fuzz unicodeStringFuzzer "number of code points is less than or equal to string length" <|
97 | \string ->
98 | List.length (toCodePoints string)
99 | |> Expect.atMost (String.length string)
100 | , fuzz (list codePointFuzzer) "encoded string length is as expected" <|
101 | \codePoints ->
102 | String.length (fromCodePoints codePoints)
103 | |> Expect.equal (expectedStringLength codePoints)
104 | , describe "toCodePoints works as expected on hard-coded test cases"
105 | (hardCodedTestCases
106 | |> List.indexedMap
107 | (\index ( string, codePoints ) ->
108 | test ("toCodePoints works properly - test case " ++ Debug.toString index)
109 | (\() -> toCodePoints string |> Expect.equal codePoints)
110 | )
111 | )
112 | , describe "fromCodePoints works as expected on hard-coded test cases"
113 | (hardCodedTestCases
114 | |> List.indexedMap
115 | (\index ( string, codePoints ) ->
116 | test ("fromCodePoints works properly - test case " ++ Debug.toString index)
117 | (\() -> fromCodePoints codePoints |> Expect.equal string)
118 | )
119 | )
120 | ]
121 |
--------------------------------------------------------------------------------
/tests/HumanizeTest.elm:
--------------------------------------------------------------------------------
1 | module HumanizeTest exposing (humanizeTest)
2 |
3 | import Char
4 | import Expect
5 | import Fuzz exposing (..)
6 | import Regex
7 | import String
8 | import String.Extra exposing (..)
9 | import Test exposing (..)
10 | import TestData
11 | import Tuple exposing (first, second)
12 |
13 |
14 | humanizeTest : Test
15 | humanizeTest =
16 | describe "humanize"
17 | [ test "All uppercase" <|
18 | \_ ->
19 | humanize "ALL_UPPERCASE IS FINE"
20 | |> Expect.equal "All uppercase is fine"
21 | , test "Some uppercase" <|
22 | \_ ->
23 | humanize "I like HTML"
24 | |> Expect.equal "I like html"
25 | , test "Snake case" <|
26 | \_ ->
27 | humanize "this_is_great"
28 | |> Expect.equal "This is great"
29 | , test "Capitalized" <|
30 | \_ ->
31 | humanize "ThisIsGreat"
32 | |> Expect.equal "This is great"
33 | , test "Kebab case" <|
34 | \_ ->
35 | humanize "this-is-great"
36 | |> Expect.equal "This is great"
37 | , test "Id suffix" <|
38 | \_ ->
39 | humanize "author_id"
40 | |> Expect.equal "Author"
41 | , fuzz (validWords []) "It starts with an uppercase letter after trimming" <|
42 | \s ->
43 | let
44 | expected =
45 | String.trim
46 | >> toSentenceCase
47 | >> String.uncons
48 | >> Maybe.map (first >> String.fromChar)
49 | >> Maybe.withDefault ""
50 | in
51 | humanize s
52 | |> String.uncons
53 | |> Maybe.map (first >> String.fromChar)
54 | |> Maybe.withDefault ""
55 | |> Expect.equal (expected s)
56 | , fuzz (validWords []) "The tail of the string is lowercased" <|
57 | \s ->
58 | humanize s
59 | |> String.uncons
60 | |> Maybe.map second
61 | |> Maybe.withDefault "a"
62 | |> String.filter ((/=) ' ')
63 | |> String.all Char.isLower
64 | |> Expect.true "Not all characters in the string are lowercased"
65 | , fuzz (validWords [ '_', '-' ]) "It removes a trailing `_id` & replaces underscores and dashes with a single whitespace" <|
66 | \s ->
67 | let
68 | expected =
69 | String.toLower
70 | >> Regex.replaceAtMost 1 (regex "_id$") (\_ -> "")
71 | >> String.replace "-" " "
72 | >> String.replace "_" " "
73 | >> Regex.replace (regex "\\s+") (\_ -> " ")
74 | >> String.trim
75 | in
76 | humanize (String.toLower s)
77 | |> String.toLower
78 | |> Expect.equal (expected s)
79 | , fuzz string "It yields the same string after removing underscores, dashes and spaces" <|
80 | \s ->
81 | let
82 | expected =
83 | String.replace "-" ""
84 | >> String.replace "_" ""
85 | >> Regex.replace (regex "\\s+") (\_ -> "")
86 | >> String.toLower
87 | in
88 | humanize s
89 | |> String.replace " " ""
90 | |> String.toLower
91 | |> Expect.equal (expected s)
92 | , fuzz (validWords []) "It adds a space before each group of uppercase letter" <|
93 | \s ->
94 | let
95 | expected =
96 | Regex.replace (regex "[A-Z]+") (\{ match } -> " " ++ match)
97 | >> String.toLower
98 | >> String.trim
99 | in
100 | humanize s
101 | |> String.toLower
102 | |> Expect.equal (expected s)
103 | , fuzz string "It does not leave double spaces around" <|
104 | \s ->
105 | let
106 | expected =
107 | replaceUppercase >> String.toLower >> String.trim
108 | in
109 | humanize s
110 | |> String.contains " "
111 | |> Expect.false "The string contains double spaces"
112 | , fuzz idString "It strips the _id at the end" <|
113 | \s ->
114 | humanize s
115 | |> String.endsWith "id"
116 | |> Expect.false "The string should not end with id"
117 | ]
118 |
119 |
120 | idString : Fuzzer String
121 | idString =
122 | validWords [ '-', '_' ]
123 | |> map (\s -> s ++ "s_id")
124 |
125 |
126 | validWords : List Char -> Fuzzer String
127 | validWords ch =
128 | TestData.randomStringsWithChars ch
129 |
130 |
131 | replaceUppercase : String -> String
132 | replaceUppercase string =
133 | string
134 | |> String.toList
135 | |> List.map
136 | (\c ->
137 | if Char.isUpper c then
138 | " " ++ String.fromChar c
139 |
140 | else
141 | String.fromChar c
142 | )
143 | |> String.join ""
144 |
145 |
146 | regex str =
147 | Maybe.withDefault Regex.never <|
148 | Regex.fromString str
149 |
--------------------------------------------------------------------------------
/tests/Tests.elm:
--------------------------------------------------------------------------------
1 | module Tests exposing (breakTest, cleanTest, countOccurrencesTest, decapitalizeTest, ellipsisTest, insertAtProducer, insertAtTest, isBlankTest, nonBlankTest, pluralizeTest, softBreakTest, surroundTest, tail, toSentenceCaseTest, toTitleCaseTest, unquoteTest, wrapTest)
2 |
3 | import Expect
4 | import Fuzz exposing (..)
5 | import Json.Encode exposing (Value)
6 | import String exposing (fromChar, replace, toLower, toUpper, uncons)
7 | import String.Extra exposing (..)
8 | import Test exposing (..)
9 | import Tuple exposing (first, second)
10 |
11 |
12 | tail : String -> String
13 | tail =
14 | uncons >> Maybe.map second >> Maybe.withDefault ""
15 |
16 |
17 | toSentenceCaseTest : Test
18 | toSentenceCaseTest =
19 | describe "toSentenceCase"
20 | [ fuzz string "It converts the first char of the string to uppercase" <|
21 | \string ->
22 | let
23 | result =
24 | string
25 | |> toSentenceCase
26 | |> uncons
27 | |> Maybe.map (first >> fromChar)
28 | |> Maybe.withDefault ""
29 |
30 | expected =
31 | string
32 | |> uncons
33 | |> Maybe.map (first >> fromChar >> toUpper)
34 | |> Maybe.withDefault ""
35 | in
36 | Expect.equal expected result
37 | , fuzz string "The tail of the string remains untouched" <|
38 | \string ->
39 | let
40 | result =
41 | (toSentenceCase >> tail) string
42 |
43 | expected =
44 | tail string
45 | in
46 | Expect.equal expected result
47 | ]
48 |
49 |
50 | decapitalizeTest : Test
51 | decapitalizeTest =
52 | describe "decapitalize"
53 | [ fuzz string "It only converst the first char in the string to lowercase" <|
54 | \string ->
55 | let
56 | result =
57 | string
58 | |> decapitalize
59 | |> uncons
60 | |> Maybe.map (first >> fromChar)
61 | |> Maybe.withDefault ""
62 |
63 | expected =
64 | string
65 | |> uncons
66 | |> Maybe.map (first >> fromChar >> toLower)
67 | |> Maybe.withDefault ""
68 | in
69 | Expect.equal expected result
70 | , fuzz string "It does not change the tail of the string" <|
71 | \string ->
72 | let
73 | result =
74 | (decapitalize >> tail) string
75 |
76 | expected =
77 | tail string
78 | in
79 | Expect.equal expected result
80 | ]
81 |
82 |
83 | toTitleCaseTest : Test
84 | toTitleCaseTest =
85 | describe "toTitleCase"
86 | [ fuzz (list string) "It converts the first letter of each word to uppercase" <|
87 | \strings ->
88 | let
89 | result =
90 | strings
91 | |> String.join " "
92 | |> toTitleCase
93 | |> String.words
94 |
95 | expected =
96 | strings
97 | |> String.join " "
98 | |> String.words
99 | |> List.map toSentenceCase
100 | in
101 | Expect.equal expected result
102 | , fuzz (list string) "It does not change the length of the string" <|
103 | \strings ->
104 | let
105 | result =
106 | strings
107 | |> String.join " "
108 | |> toTitleCase
109 | |> String.length
110 |
111 | expected =
112 | strings
113 | |> String.join " "
114 | |> String.length
115 | in
116 | Expect.equal expected result
117 | ]
118 |
119 |
120 | breakTest : Test
121 | breakTest =
122 | describe "break"
123 | [ fuzz2 string (intRange 0 100) "The list should have as many elements as the ceil division of the length" <|
124 | \string width ->
125 | case ( string, width ) of
126 | ( "", _ ) ->
127 | break width string
128 | |> List.length
129 | |> Expect.equal 1
130 |
131 | ( _, 0 ) ->
132 | break width string
133 | |> List.length
134 | |> Expect.equal 1
135 |
136 | _ ->
137 | break width string
138 | |> List.length
139 | |> Expect.equal (ceiling <| (toFloat << String.length) string / toFloat width)
140 | , fuzz2 string (intRange 1 10) "Concatenating the result yields the original string" <|
141 | \string width ->
142 | break width string
143 | |> String.concat
144 | |> Expect.equal string
145 | , fuzz2 string (intRange 1 10) "No element in the list should have more than `width` chars" <|
146 | \string width ->
147 | break width string
148 | |> List.map String.length
149 | |> List.filter ((<) width)
150 | |> List.isEmpty
151 | |> Expect.true "The list has some long elements"
152 | ]
153 |
154 |
155 | softBreakTest : Test
156 | softBreakTest =
157 | describe "softBreak"
158 | [ fuzz2 string (intRange 1 10) "Concatenating the result yields the original string" <|
159 | \string width ->
160 | softBreak width (String.trim string)
161 | |> String.concat
162 | |> Expect.equal (String.trim string)
163 | , fuzz2 string (intRange 1 10) "The list should not have more elements than words" <|
164 | \string width ->
165 | softBreak width string
166 | |> List.length
167 | |> Expect.atMost (String.words string |> List.length)
168 | ]
169 |
170 |
171 | cleanTest : Test
172 | cleanTest =
173 | describe "clean"
174 | [ fuzz string "The String.split result is the same as String.words" <|
175 | \string ->
176 | let
177 | result =
178 | string
179 | |> clean
180 | |> String.split " "
181 |
182 | expected =
183 | String.words string
184 | in
185 | Expect.equal expected result
186 | , fuzz string "It trims the string on the left side" <|
187 | \string ->
188 | string
189 | |> clean
190 | |> String.startsWith " "
191 | |> Expect.false "Did not trim the start of the string"
192 | , fuzz string "It trims the string on the right side" <|
193 | \string ->
194 | string
195 | |> clean
196 | |> String.endsWith " "
197 | |> Expect.false "Did not trim the end of the string"
198 | ]
199 |
200 |
201 | insertAtTest : Test
202 | insertAtTest =
203 | describe "insertAt"
204 | [ fuzz insertAtProducer "Result contains the substitution string" <|
205 | \( sub, at, string ) ->
206 | string
207 | |> insertAt sub at
208 | |> String.contains sub
209 | |> Expect.true "Could not find substitution string in result"
210 | , fuzz insertAtProducer "Resulting string has length as the sum of both arguments" <|
211 | \( sub, at, string ) ->
212 | insertAt sub at string
213 | |> String.length
214 | |> Expect.equal (String.length sub + String.length string)
215 | , fuzz insertAtProducer "Start of the string remains the same" <|
216 | \( sub, at, string ) ->
217 | insertAt sub at string
218 | |> String.slice 0 at
219 | |> Expect.equal (String.slice 0 at string)
220 | , fuzz insertAtProducer "End of the string remains the same" <|
221 | \( sub, at, string ) ->
222 | insertAt sub at string
223 | |> String.slice (at + String.length sub) (String.length string + String.length sub)
224 | |> Expect.equal (String.slice at (String.length string) string)
225 | ]
226 |
227 |
228 | insertAtProducer : Fuzzer ( String, Int, String )
229 | insertAtProducer =
230 | tuple3 ( intRange 0 10, intRange 1 10, string )
231 | |> map (\( a, b, s ) -> ( "b" ++ s, b, String.repeat (a + b) "a" ))
232 |
233 |
234 | isBlankTest : Test
235 | isBlankTest =
236 | describe "isBlank"
237 | [ test "Returns true if the given string is blank" <|
238 | \_ ->
239 | isBlank ""
240 | |> Expect.true "Did not return true"
241 | , test "Returns false if the given string is not blank" <|
242 | \_ ->
243 | isBlank " Slartibartfast"
244 | |> Expect.false "Did not return false"
245 | ]
246 |
247 |
248 | nonBlankTest : Test
249 | nonBlankTest =
250 | describe "nonBlank"
251 | [ test "Returns Nothing if the given string is blank" <|
252 | \_ ->
253 | nonBlank ""
254 | |> Expect.equal Nothing
255 | , test "Returns just the string if the given string is not blank" <|
256 | \_ ->
257 | nonBlank " Slartibartfast"
258 | |> Expect.equal (Just " Slartibartfast")
259 | ]
260 |
261 |
262 | surroundTest : Test
263 | surroundTest =
264 | describe "surround"
265 | [ fuzz2 string string "It starts with the wrapping string" <|
266 | \string wrap ->
267 | string
268 | |> surround wrap
269 | |> String.startsWith wrap
270 | |> Expect.true "Did not start with the wrapping string"
271 | , fuzz2 string string "It ends with the wrapping string" <|
272 | \string wrap ->
273 | string
274 | |> surround wrap
275 | |> String.endsWith wrap
276 | |> Expect.true "Did not end with the wrapping string"
277 | , fuzz2 string string "It contains the original string" <|
278 | \string wrap ->
279 | string
280 | |> surround wrap
281 | |> String.contains string
282 | |> Expect.true "Did not contain the string"
283 | , fuzz2 string string "It does not have anything else inside" <|
284 | \string wrap ->
285 | let
286 | result =
287 | String.length (surround wrap string)
288 |
289 | expected =
290 | String.length string + (2 * String.length wrap)
291 | in
292 | Expect.equal expected result
293 | ]
294 |
295 |
296 |
297 | -- TODO: ensure this test only gets strings that contain the needle in the
298 | -- haystack?
299 |
300 |
301 | countOccurrencesTest : Test
302 | countOccurrencesTest =
303 | describe "countOccurrences"
304 | [ fuzz2 string string "Removing the occurences should yield the right length" <|
305 | \needle haystack ->
306 | let
307 | times =
308 | countOccurrences needle haystack
309 |
310 | result =
311 | String.length haystack - (times * String.length needle)
312 |
313 | expected =
314 | String.length (replace needle "" haystack)
315 | in
316 | Expect.equal expected result
317 | ]
318 |
319 |
320 | ellipsisTest : Test
321 | ellipsisTest =
322 | describe "ellipsis"
323 | [ fuzz2 (intRange 3 20) string "The resulting string length does not exceed the specified length" <|
324 | \howLong string ->
325 | ellipsis howLong string
326 | |> String.length
327 | |> (>=) howLong
328 | |> Expect.true "Resulting string exceeds specified length"
329 | , fuzz2 (intRange 3 20) string "The resulting string contains three dots at the end if necessary" <|
330 | \howLong string ->
331 | let
332 | result =
333 | ellipsis howLong string
334 | in
335 | result
336 | |> String.endsWith "..."
337 | |> (if String.length string > howLong then
338 | Expect.true "Should add ellipsis to this string"
339 |
340 | else
341 | Expect.false "Should not add ellipsis"
342 | )
343 | , fuzz2 (intRange 3 20) string "It starts with the left of the original string" <|
344 | \howLong string ->
345 | let
346 | result =
347 | ellipsis howLong string
348 |
349 | resultLeft =
350 | String.dropRight 3 result
351 | in
352 | string
353 | |> String.startsWith resultLeft
354 | |> Expect.true "Should start with the original left"
355 | ]
356 |
357 |
358 | unquoteTest : Test
359 | unquoteTest =
360 | describe "unquote"
361 | [ test "Removes quotes from the start of the string" <|
362 | \_ ->
363 | unquote "\"Magrathea\""
364 | |> Expect.equal "Magrathea"
365 | ]
366 |
367 |
368 | wrapTest : Test
369 | wrapTest =
370 | describe "wrap"
371 | [ fuzz2 (intRange 1 20) string "Wraps given string at the requested length" <|
372 | \howLong string ->
373 | wrap howLong string
374 | |> String.split "\n"
375 | |> List.map (\str -> String.length str <= howLong)
376 | |> List.all ((==) True)
377 | |> Expect.true "Given string was not wrapped at requested length"
378 | , test "Does not wrap string shorter than the requested length" <|
379 | \_ ->
380 | wrap 50 "Heart of Gold"
381 | |> String.contains "\n"
382 | |> Expect.false "Short string was wrapped"
383 | ]
384 |
385 |
386 | pluralizeTest : Test
387 | pluralizeTest =
388 | describe "pluralize"
389 | [ test "It uses the singular version when the count is one" <|
390 | \() ->
391 | pluralize "elf" "elves" 1
392 | |> Expect.equal "1 elf"
393 | , test "It uses the plural version for > 1 count" <|
394 | \() ->
395 | pluralize "elf" "elves" 4
396 | |> Expect.equal "4 elves"
397 | , test "It uses the plural version for 0 count" <|
398 | \() ->
399 | pluralize "elf" "elves" 0
400 | |> Expect.equal "0 elves"
401 | ]
402 |
--------------------------------------------------------------------------------
/src/String/Extra.elm:
--------------------------------------------------------------------------------
1 | module String.Extra exposing
2 | ( toSentenceCase, toTitleCase, decapitalize
3 | , camelize, classify, underscored, dasherize, humanize
4 | , replaceSlice, insertAt, nonEmpty, nonBlank, removeAccents
5 | , break, softBreak
6 | , wrap, wrapWith, softWrap, softWrapWith, quote, surround
7 | , isBlank, countOccurrences
8 | , clean, unquote, unsurround, unindent, ellipsis, softEllipsis, ellipsisWith, stripTags, pluralize
9 | , toSentence, toSentenceOxford
10 | , rightOf, leftOf, rightOfBack, leftOfBack
11 | , toCodePoints, fromCodePoints
12 | )
13 |
14 | {-| Additional functions for working with Strings
15 |
16 |
17 | ## Change words casing
18 |
19 | @docs toSentenceCase, toTitleCase, decapitalize
20 |
21 |
22 | ## Inflector functions
23 |
24 | Functions borrowed from the Rails Inflector class
25 |
26 | @docs camelize, classify, underscored, dasherize, humanize
27 |
28 |
29 | ## Replace and Splice
30 |
31 | @docs replaceSlice, insertAt, nonEmpty, nonBlank, removeAccents
32 |
33 |
34 | ## Splitting
35 |
36 | @docs break, softBreak
37 |
38 |
39 | ## Wrapping
40 |
41 | @docs wrap, wrapWith, softWrap, softWrapWith, quote, surround
42 |
43 |
44 | ## Checks
45 |
46 | @docs isBlank, countOccurrences
47 |
48 |
49 | ## Formatting
50 |
51 | @docs clean, unquote, unsurround, unindent, ellipsis, softEllipsis, ellipsisWith, stripTags, pluralize
52 |
53 |
54 | ## Converting Lists
55 |
56 | @docs toSentence, toSentenceOxford
57 |
58 |
59 | ## Finding
60 |
61 | @docs rightOf, leftOf, rightOfBack, leftOfBack
62 |
63 |
64 | ## Converting UTF-32
65 |
66 | @docs toCodePoints, fromCodePoints
67 |
68 | -}
69 |
70 | import Char exposing (toLower, toUpper)
71 | import List
72 | import Maybe exposing (Maybe(..))
73 | import Regex exposing (Regex)
74 | import String exposing (cons, join, uncons)
75 | import Tuple
76 |
77 |
78 | {-| Change the case of the first letter of a string to either uppercase or
79 | lowercase, depending of the value of `wantedCase`. This is an internal
80 | function for use in `toSentenceCase` and `decapitalize`.
81 | -}
82 | changeCase : (Char -> Char) -> String -> String
83 | changeCase mutator word =
84 | uncons word
85 | |> Maybe.map (\( head, tail ) -> cons (mutator head) tail)
86 | |> Maybe.withDefault ""
87 |
88 |
89 | {-| Capitalize the first letter of a string.
90 |
91 | toSentenceCase "this is a phrase" == "This is a phrase"
92 |
93 | toSentenceCase "hello, world" == "Hello, world"
94 |
95 | -}
96 | toSentenceCase : String -> String
97 | toSentenceCase word =
98 | changeCase toUpper word
99 |
100 |
101 | {-| Decapitalize the first letter of a string.
102 |
103 | decapitalize "This is a phrase" == "this is a phrase"
104 |
105 | decapitalize "Hello, World" == "hello, World"
106 |
107 | -}
108 | decapitalize : String -> String
109 | decapitalize word =
110 | changeCase toLower word
111 |
112 |
113 | {-| Capitalize the first character of each word in a string.
114 |
115 | toTitleCase "this is a phrase" == "This Is A Phrase"
116 |
117 | toTitleCase "hello, world" == "Hello, World"
118 |
119 | -}
120 | toTitleCase : String -> String
121 | toTitleCase ws =
122 | let
123 | uppercaseMatch =
124 | Regex.replace (regexFromString "\\w+") (.match >> toSentenceCase)
125 | in
126 | ws
127 | |> Regex.replace
128 | (regexFromString "^([a-z])|\\s+([a-z])")
129 | (.match >> uppercaseMatch)
130 |
131 |
132 | {-| Replace text within a portion of a string given a substitution
133 | string, a start index and an end index. The substitution includes the character
134 | at the start index but not the one at the end index.
135 |
136 | replaceSlice "Sue" 4 7 "Hi, Bob" == "Hi, Sue"
137 |
138 | replaceSlice "elephants" 0 6 "snakes on a plane!" == "elephants on a plane!"
139 |
140 | replaceSlice "under" 7 9 "snakes on a plane!" == "snakes under a plane!"
141 |
142 | -}
143 | replaceSlice : String -> Int -> Int -> String -> String
144 | replaceSlice substitution start end string =
145 | String.slice 0 start string ++ substitution ++ String.slice end (String.length string) string
146 |
147 |
148 | {-| Insert a substring at the specified index.
149 |
150 | insertAt "world" 6 "Hello " == "Hello world"
151 |
152 | -}
153 | insertAt : String -> Int -> String -> String
154 | insertAt insert pos string =
155 | replaceSlice insert pos pos string
156 |
157 |
158 | {-| Break a string into a list of strings of a specified maximum length.
159 |
160 | break 10 "The quick brown fox" == [ "The quick ", "brown fox" ]
161 |
162 | break 2 "" == [ "" ]
163 |
164 | -}
165 | break : Int -> String -> List String
166 | break width string =
167 | if width == 0 || string == "" then
168 | [ string ]
169 |
170 | else
171 | breaker width string []
172 |
173 |
174 | breaker : Int -> String -> List String -> List String
175 | breaker width string acc =
176 | case string of
177 | "" ->
178 | List.reverse acc
179 |
180 | _ ->
181 | breaker width
182 | (String.dropLeft width string)
183 | (String.slice 0 width string :: acc)
184 |
185 |
186 | {-| Break a string into a list of strings of a specified maximum length,
187 | without truncating words.
188 |
189 | softBreak 6 "The quick brown fox" == [ "The quick", " brown", " fox" ]
190 |
191 | -}
192 | softBreak : Int -> String -> List String
193 | softBreak width string =
194 | if width <= 0 then
195 | []
196 |
197 | else
198 | string
199 | |> Regex.find (softBreakRegexp width)
200 | |> List.map .match
201 |
202 |
203 | softBreakRegexp : Int -> Regex.Regex
204 | softBreakRegexp width =
205 | regexFromString <| ".{1," ++ String.fromInt width ++ "}(\\s+|$)|\\S+?(\\s+|$)"
206 |
207 |
208 | {-| Trim the whitespace of both sides of the string and compress
209 | repeated whitespace internally to a single whitespace char.
210 |
211 | clean " The quick brown fox " == "The quick brown fox"
212 |
213 | -}
214 | clean : String -> String
215 | clean string =
216 | string
217 | |> Regex.replace (regexFromString "\\s\\s+") (always " ")
218 | |> String.trim
219 |
220 |
221 | {-| Test if a string is empty or only contains whitespace.
222 |
223 | isBlank "" == True
224 |
225 | isBlank "\n" == True
226 |
227 | isBlank " " == True
228 |
229 | isBlank " a" == False
230 |
231 | -}
232 | isBlank : String -> Bool
233 | isBlank string =
234 | Regex.contains (regexFromString "^\\s*$") string
235 |
236 |
237 | {-| Convert an underscored or dasherized string to a camelized one.
238 |
239 | camelize "-moz-transform" == "MozTransform"
240 |
241 | -}
242 | camelize : String -> String
243 | camelize string =
244 | Regex.replace
245 | (regexFromString "[-_\\s]+(.)?")
246 | (\{ submatches } ->
247 | case submatches of
248 | (Just match) :: _ ->
249 | String.toUpper match
250 |
251 | _ ->
252 | ""
253 | )
254 | (String.trim string)
255 |
256 |
257 | {-| Convert a string to a camelized string starting with an uppercase letter.
258 | All non-word characters will be stripped out of the original string.
259 |
260 | classify "some_class_name" == "SomeClassName"
261 |
262 | classify "myLittleCamel.class.name" == "MyLittleCamelClassName"
263 |
264 | -}
265 | classify : String -> String
266 | classify string =
267 | string
268 | |> Regex.replace (regexFromString "[\\W_]") (always " ")
269 | |> camelize
270 | |> String.replace " " ""
271 | |> toSentenceCase
272 |
273 |
274 | {-| Surround a string with another string.
275 |
276 | surround "bar" "foo" == "barfoobar"
277 |
278 | -}
279 | surround : String -> String -> String
280 | surround wrapper string =
281 | wrapper ++ string ++ wrapper
282 |
283 |
284 | {-| Remove surrounding strings from another string.
285 |
286 | unsurround "foo" "foobarfoo" == "bar"
287 |
288 | -}
289 | unsurround : String -> String -> String
290 | unsurround wrapper string =
291 | if String.startsWith wrapper string && String.endsWith wrapper string then
292 | let
293 | length =
294 | String.length wrapper
295 | in
296 | string
297 | |> String.dropLeft length
298 | |> String.dropRight length
299 |
300 | else
301 | string
302 |
303 |
304 | {-| Add quotes to a string.
305 |
306 | quote "foo" == "\"foo\""
307 |
308 | -}
309 | quote : String -> String
310 | quote string =
311 | surround "\"" string
312 |
313 |
314 | {-| Remove quotes that surround a string.
315 |
316 | unquote "\"foo\"" == "foo"
317 |
318 | unquote "\"foo\"bar\""
319 |
320 | -}
321 | unquote : String -> String
322 | unquote string =
323 | unsurround "\"" string
324 |
325 |
326 | {-| Return a string joined by underscores after separating it by its uppercase characters.
327 | Any sequence of spaces or dashes will also be converted to a single underscore.
328 | The final string will be lowercased.
329 |
330 | underscored "SomeClassName" == "some_class_name"
331 | underscored "some-class-name" == "some_class_name"
332 | underscored "SomeClass name" == "some_class_name
333 |
334 | -}
335 | underscored : String -> String
336 | underscored string =
337 | string
338 | |> String.trim
339 | |> Regex.replace (regexFromString "([a-z\\d])([A-Z]+)") (.submatches >> List.filterMap identity >> String.join "_")
340 | |> Regex.replace (regexFromString "[_-\\s]+") (always "_")
341 | |> String.toLower
342 |
343 |
344 | {-| Return a string joined by dashes after separating it by its uppercase characters.
345 | Any sequence of spaces or underscores will also be converted to a single dash.
346 | The final string will be lowercased.
347 |
348 | dasherize "SomeClassName" == "-some-class-name"
349 | dasherize "some_class_name" = "some-class-name"
350 | dasherize "someClass name" = "some-class-name"
351 |
352 | -}
353 | dasherize : String -> String
354 | dasherize string =
355 | string
356 | |> String.trim
357 | |> Regex.replace (regexFromString "([A-Z])") (.match >> String.append "-")
358 | |> Regex.replace (regexFromString "[_-\\s]+") (always "-")
359 | |> String.toLower
360 |
361 |
362 | {-| Separate a string into parts of a given width, using a given separator.
363 |
364 | Look at `wrap` if you just want to wrap using newlines.
365 |
366 | wrapWith 7 "\n" "My very long text" === "My very\nlong text"
367 |
368 | wrapWith 100 "\n" "Too short" === "Too short"
369 |
370 | -}
371 | wrapWith : Int -> String -> String -> String
372 | wrapWith width separator string =
373 | string
374 | |> break width
375 | |> String.join separator
376 |
377 |
378 | {-| Chop a given string into parts of a given width, separating them with a
379 | new line.
380 |
381 | wrap 7 "My very long text" === "My very\nlong te\nxt"
382 |
383 | wrap 100 "Too short" === "Too short"
384 |
385 | -}
386 | wrap : Int -> String -> String
387 | wrap width string =
388 | wrapWith width "\n" string
389 |
390 |
391 | {-| Chop a given string into parts of a given width without breaking words apart,
392 | and then separate them using a new line.
393 |
394 | softWrap 7 "My very long text" === "My very\nlong text"
395 |
396 | softWrap 3 "Hello World" === "Hello \nWorld"
397 |
398 | softWrap 100 "Too short" === "Too short"
399 |
400 | -}
401 | softWrap : Int -> String -> String
402 | softWrap width string =
403 | softWrapWith width "\n" string
404 |
405 |
406 | {-| Chop a given string into parts of a given width without breaking words apart,
407 | and then separate them using the given separator.
408 |
409 | softWrapWith 7 "..." "My very long text" === "My very...long text"
410 |
411 | softWrapWith 3 "\n" "Hello World" === "Hello \nWorld"
412 |
413 | softWrapWith 100 "\t" "Too short" === "Too short"
414 |
415 | -}
416 | softWrapWith : Int -> String -> String -> String
417 | softWrapWith width separator string =
418 | string
419 | |> softBreak width
420 | |> String.join separator
421 |
422 |
423 | {-| Convert an underscored, camelized, or dasherized string into one that can be
424 | read by humans. Also remove beginning and ending whitespace, and removes the
425 | postfix '\_id'. The first character will be capitalized.
426 |
427 | humanize "this_is_great" == "This is great"
428 | humanize "ThisIsGreat" = "This is great"
429 | humanize "this-is-great" = "This is great"
430 | humanize "author_id" = "Author"
431 |
432 | -}
433 | humanize : String -> String
434 | humanize string =
435 | string
436 | |> Regex.replace (regexFromString "[A-Z]+") (.match >> String.append "-")
437 | |> Regex.replace (regexFromString "_id$|[-_\\s]+") (always " ")
438 | |> String.trim
439 | |> String.toLower
440 | |> toSentenceCase
441 |
442 |
443 | {-| Remove the shortest sequence of leading spaces or tabs on each line
444 | of the string, so that at least one of the lines will not have any
445 | leading spaces nor tabs and the rest of the lines will have the same
446 | amount of indentation removed.
447 |
448 | unindent " Hello\n World " == "Hello\n World"
449 |
450 | unindent "\t\tHello\n\t\t\t\tWorld" == "Hello\n\t\tWorld"
451 |
452 | -}
453 | unindent : String -> String
454 | unindent multilineSting =
455 | let
456 | lines =
457 | String.lines multilineSting
458 |
459 | countLeadingWhitespace count line =
460 | case String.uncons line of
461 | Nothing ->
462 | count
463 |
464 | Just ( char, rest ) ->
465 | case char of
466 | ' ' ->
467 | countLeadingWhitespace (count + 1) rest
468 |
469 | '\t' ->
470 | countLeadingWhitespace (count + 1) rest
471 |
472 | _ ->
473 | count
474 |
475 | isNotWhitespace char =
476 | char /= ' ' && char /= '\t'
477 |
478 | minLead =
479 | lines
480 | |> List.filter (String.any isNotWhitespace)
481 | |> List.map (countLeadingWhitespace 0)
482 | |> List.minimum
483 | |> Maybe.withDefault 0
484 | in
485 | lines
486 | |> List.map (String.dropLeft minLead)
487 | |> String.join "\n"
488 |
489 |
490 | {-| Return the number of occurrences of a substring in another string.
491 |
492 | countOccurrences "Hello" "Hello World" == 1
493 |
494 | countOccurrences "o" "Hello World" == 2
495 |
496 | -}
497 | countOccurrences : String -> String -> Int
498 | countOccurrences needle haystack =
499 | if String.length needle == 0 || String.length haystack == 0 then
500 | 0
501 |
502 | else
503 | haystack
504 | |> String.indexes needle
505 | |> List.length
506 |
507 |
508 | {-| Truncate the second string at the specified length if the string is
509 | longer than the specified length, and replace the end of the truncated
510 | string with the first string, such that the resulting string is of the
511 | specified length.
512 |
513 | The resulting string will have at most the specified length.
514 |
515 | ellipsisWith 5 " .." "Hello World" == "He .."
516 |
517 | ellipsisWith 10 " .." "Hello World" == "Hello W .."
518 |
519 | ellipsisWith 10 " .." "Hello" == "Hello"
520 |
521 | ellipsisWith 8 " .." "Hello World" == "Hello .."
522 |
523 | -}
524 | ellipsisWith : Int -> String -> String -> String
525 | ellipsisWith howLong append string =
526 | if String.length string <= howLong then
527 | string
528 |
529 | else
530 | String.left (howLong - String.length append) string ++ append
531 |
532 |
533 | {-| Truncate the string at the specified length if the string is
534 | longer than the specified length, and replace the end of the truncated
535 | string with `"..."`, such that the resulting string is of the
536 | specified length.
537 |
538 | The resulting string will have at most the specified length.
539 |
540 | ellipsis 5 "Hello World" == "He..."
541 |
542 | ellipsis 10 "Hello World" == "Hello W..."
543 |
544 | ellipsis 10 "Hello" == "Hello"
545 |
546 | ellipsis 8 "Hello World" == "Hello..."
547 |
548 | -}
549 | ellipsis : Int -> String -> String
550 | ellipsis howLong string =
551 | ellipsisWith howLong "..." string
552 |
553 |
554 | {-| Truncate the string at the last complete word less than or equal to
555 | the specified length and append `"..."`. When the specified length is
556 | less than the length of the first word, the ellipsis is appended to the
557 | first word. When the specified length is greater than or equal to the
558 | length of the string, an identical string is returned.
559 |
560 | In contrast to `ellipsis`, this function will not produce incomplete
561 | words, and the resulting string can exceed the specified length. In
562 | addition, it removes trailing whitespace and punctuation characters at
563 | the end of the truncated string.
564 |
565 | softEllipsis 1 "Hello, World" == "Hello..."
566 |
567 | softEllipsis 5 "Hello, World" == "Hello..."
568 |
569 | softEllipsis 6 "Hello, World" == "Hello..."
570 |
571 | softEllipsis 15 "Hello, cruel world" == "Hello, cruel..."
572 |
573 | softEllipsis 10 "Hello" == "Hello"
574 |
575 | -}
576 | softEllipsis : Int -> String -> String
577 | softEllipsis howLong string =
578 | if String.length string <= howLong then
579 | string
580 |
581 | else
582 | string
583 | |> Regex.findAtMost 1 (softBreakRegexp howLong)
584 | |> List.map .match
585 | |> String.join ""
586 | |> Regex.replace (regexFromString "([\\.,;:\\s])+$") (always "")
587 | |> (\a -> String.append a "...")
588 |
589 |
590 | {-| Convert a list of strings into a human-readable list.
591 |
592 | toSentence [] == ""
593 |
594 | toSentence [ "lions" ] == "lions"
595 |
596 | toSentence [ "lions", "tigers" ] == "lions and tigers"
597 |
598 | toSentence [ "lions", "tigers", "bears" ] == "lions, tigers and bears"
599 |
600 | -}
601 | toSentence : List String -> String
602 | toSentence list =
603 | case list of
604 | x :: y :: z :: more ->
605 | toSentenceHelper " and " (x ++ ", " ++ y) (z :: more)
606 |
607 | _ ->
608 | toSentenceBaseCase list
609 |
610 |
611 | {-| Convert a list of strings into a human-readable list using an oxford comma.
612 |
613 | toSentenceOxford [] == ""
614 |
615 | toSentenceOxford [ "lions" ] == "lions"
616 |
617 | toSentenceOxford [ "lions", "tigers" ] == "lions and tigers"
618 |
619 | toSentenceOxford [ "lions", "tigers", "bears" ] == "lions, tigers, and bears"
620 |
621 | -}
622 | toSentenceOxford : List String -> String
623 | toSentenceOxford list =
624 | case list of
625 | x :: y :: z :: more ->
626 | toSentenceHelper ", and " (x ++ ", " ++ y) (z :: more)
627 |
628 | _ ->
629 | toSentenceBaseCase list
630 |
631 |
632 | toSentenceBaseCase : List String -> String
633 | toSentenceBaseCase list =
634 | case list of
635 | x :: [] ->
636 | x
637 |
638 | x :: y :: [] ->
639 | x ++ " and " ++ y
640 |
641 | _ ->
642 | ""
643 |
644 |
645 | toSentenceHelper : String -> String -> List String -> String
646 | toSentenceHelper lastPart sentence list =
647 | case list of
648 | [] ->
649 | sentence
650 |
651 | x :: [] ->
652 | sentence ++ lastPart ++ x
653 |
654 | x :: xs ->
655 | toSentenceHelper lastPart (sentence ++ ", " ++ x) xs
656 |
657 |
658 | {-| Remove all HTML tags from the string, preserving the text inside them.
659 |
660 | stripTags "a link" == "a link"
661 | stripTags " == "alert('hello world!')"
662 |
663 | -}
664 | stripTags : String -> String
665 | stripTags string =
666 | string
667 | |> Regex.replace (regexFromString "<\\/?[^>]+>") (always "")
668 |
669 |
670 | {-| Given a number, a singular string, and a plural string, return the number
671 | followed by a space, followed by either the singular string if the number was 1,
672 | or the plural string otherwise.
673 |
674 | pluralize "elf" "elves" 2 == "2 elves"
675 |
676 | pluralize "elf" "elves" 1 == "1 elf"
677 |
678 | pluralize "elf" "elves" 0 == "0 elves"
679 |
680 | -}
681 | pluralize : String -> String -> Int -> String
682 | pluralize singular plural count =
683 | if count == 1 then
684 | "1 " ++ singular
685 |
686 | else
687 | String.fromInt count ++ " " ++ plural
688 |
689 |
690 | {-| Search a string from left to right for a pattern and return a substring
691 | consisting of the characters in the string that are to the right of the pattern.
692 |
693 | rightOf "_" "This_is_a_test_string" == "is_a_test_string"
694 |
695 | -}
696 | rightOf : String -> String -> String
697 | rightOf pattern string =
698 | string
699 | |> Regex.findAtMost 1 (regexFromString <| regexEscape pattern ++ "(.*)$")
700 | |> List.map (.submatches >> firstResult)
701 | |> String.join ""
702 |
703 |
704 | {-| Search a string from left to right for a pattern and return a substring
705 | consisting of the characters in the string that are to the left of the pattern.
706 |
707 | leftOf "_" "This_is_a_test_string" == "This"
708 |
709 | -}
710 | leftOf : String -> String -> String
711 | leftOf pattern string =
712 | string
713 | |> Regex.findAtMost 1 (regexFromString <| "^(.*?)" ++ regexEscape pattern)
714 | |> List.map (.submatches >> firstResult)
715 | |> String.join ""
716 |
717 |
718 | firstResult : List (Maybe String) -> String
719 | firstResult list =
720 | firstResultHelp "" list
721 |
722 |
723 | firstResultHelp : String -> List (Maybe String) -> String
724 | firstResultHelp default list =
725 | case list of
726 | [] ->
727 | default
728 |
729 | (Just a) :: _ ->
730 | a
731 |
732 | Nothing :: rest ->
733 | firstResultHelp default rest
734 |
735 |
736 | {-| Search a string from right to left for a pattern and return a substring
737 | consisting of the characters in the string that are to the right of the pattern.
738 |
739 | rightOfBack "_" "This_is_a_test_string" == "string"
740 |
741 | -}
742 | rightOfBack : String -> String -> String
743 | rightOfBack pattern string =
744 | string
745 | |> String.indexes pattern
746 | |> List.reverse
747 | |> List.head
748 | |> Maybe.map ((+) (String.length pattern) >> (\a -> String.dropLeft a string))
749 | |> Maybe.withDefault ""
750 |
751 |
752 | {-| Search a string from right to left for a pattern and return a substring
753 | consisting of the characters in the string that are to the left of the pattern.
754 |
755 | leftOfBack "_" "This_is_a_test_string" == "This_is_a_test"
756 |
757 | -}
758 | leftOfBack : String -> String -> String
759 | leftOfBack pattern string =
760 | string
761 | |> String.indexes pattern
762 | |> List.reverse
763 | |> List.head
764 | |> Maybe.map (\a -> String.left a string)
765 | |> Maybe.withDefault ""
766 |
767 |
768 | {-| Convert a string into a list of UTF-32 code points.
769 |
770 | toCodePoints "abc" == [ 97, 98, 99 ]
771 |
772 | toCodePoints "©§π" == [ 169, 167, 960 ]
773 |
774 | toCodePoints "💩!" == [ 128169, 33 ]
775 |
776 | Note that code points do not necessarily correspond to logical/visual
777 | characters, since it is possible for things like accented characters to be
778 | represented as two separate UTF-32 code points (a base character and a
779 | combining accent).
780 |
781 | `toCodePoints string` is equivalent to:
782 |
783 | List.map Char.toCode (String.toList string)
784 |
785 | -}
786 | toCodePoints : String -> List Int
787 | toCodePoints string =
788 | List.map Char.toCode (String.toList string)
789 |
790 |
791 | {-| Convert a list of UTF-32 code points into a string. Inverse of
792 | `toCodePoints`.
793 |
794 | fromCodePoints [ 97, 98, 99 ] == "abc"
795 |
796 | fromCodePoints [ 169, 167, 960 ] == "©§π"
797 |
798 | fromCodePoints [ 128169, 33 ] == "💩!"
799 |
800 | `fromCodePoints codePoints` is equivalent to:
801 |
802 | String.fromList (List.map Char.fromCode codePoints)
803 |
804 | -}
805 | fromCodePoints : List Int -> String
806 | fromCodePoints codePoints =
807 | String.fromList (List.map Char.fromCode codePoints)
808 |
809 |
810 | {-| Convert a string to a Nothing when empty.
811 |
812 | nonEmpty "" == Nothing
813 |
814 | nonEmpty "Hello world" == Just "Hello world"
815 |
816 | -}
817 | nonEmpty : String -> Maybe String
818 | nonEmpty string =
819 | if String.isEmpty string then
820 | Nothing
821 |
822 | else
823 | Just string
824 |
825 |
826 | {-| Convert a string to a Nothing when blank.
827 |
828 | nonBlank "" == Nothing
829 |
830 | nonBlank " " == Nothing
831 |
832 | nonBlank "Hello world" == Just "Hello world"
833 |
834 | -}
835 | nonBlank : String -> Maybe String
836 | nonBlank string =
837 | if isBlank string then
838 | Nothing
839 |
840 | else
841 | Just string
842 |
843 |
844 | {-| Remove accents from string.
845 |
846 | removeAccents "andré" == "andre"
847 |
848 | removeAccents "Atenção" == "Atencao"
849 |
850 | -}
851 | removeAccents : String -> String
852 | removeAccents string =
853 | if String.isEmpty string then
854 | string
855 |
856 | else
857 | let
858 | do_regex_to_remove_acents ( regex, replace_character ) =
859 | Regex.replace regex (\_ -> replace_character)
860 | in
861 | List.foldl do_regex_to_remove_acents string accentRegex
862 |
863 |
864 | {-| Create list with regex and char to replace.
865 | -}
866 | accentRegex : List ( Regex.Regex, String )
867 | accentRegex =
868 | let
869 | matches =
870 | [ ( "[à-æ]", "a" )
871 | , ( "[À-Æ]", "A" )
872 | , ( "ç", "c" )
873 | , ( "Ç", "C" )
874 | , ( "[è-ë]", "e" )
875 | , ( "[È-Ë]", "E" )
876 | , ( "[ì-ï]", "i" )
877 | , ( "[Ì-Ï]", "I" )
878 | , ( "ñ", "n" )
879 | , ( "Ñ", "N" )
880 | , ( "[ò-ö]", "o" )
881 | , ( "[Ò-Ö]", "O" )
882 | , ( "[ù-ü]", "u" )
883 | , ( "[Ù-Ü]", "U" )
884 | , ( "ý", "y" )
885 | , ( "ÿ", "y" )
886 | , ( "Ý", "Y" )
887 | ]
888 | in
889 | List.map (Tuple.mapFirst regexFromString) matches
890 |
891 |
892 | regexEscape : String -> String
893 | regexEscape =
894 | Regex.replace (regexFromString "[-/\\^$*+?.()|[\\]{}]") (\{ match } -> "\\" ++ match)
895 |
896 |
897 | regexFromString : String -> Regex
898 | regexFromString =
899 | Regex.fromString >> Maybe.withDefault Regex.never
900 |
--------------------------------------------------------------------------------