├── 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 | [![Build Status](https://travis-ci.org/elm-community/string-extra.svg?branch=master)](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 | --------------------------------------------------------------------------------