git.iko.soy/typeable/inflections-hs
on master help sign in
37 files changed+1432−775
since splitsnapshots
unifiedsplit
.travis.yml+32−28 CHANGELOG.md+47 LICENSE+17−18 README.md+12−9 Text/Inflections.hs+182−151 Text/Inflections/Camelize.hs+41−28 Text/Inflections/Dasherize.hs+26−16 Text/Inflections/Data.hs+27−5 Text/Inflections/Humanize.hs+54−18 Text/Inflections/Ordinal.hs+32−15 Text/Inflections/Parameterize.hs+44−86 Text/Inflections/Parse/Acronym.hs−16 Text/Inflections/Parse/CamelCase.hs+72−18 Text/Inflections/Parse/Parameterizable.hs−42 Text/Inflections/Parse/SnakeCase.hs+61−17 Text/Inflections/Parse/Types.hs−18 Text/Inflections/Titleize.hs+24−15 Text/Inflections/Transliterate.hs+46−22 Text/Inflections/Types.hs+144 Text/Inflections/Underscore.hs+25−16 docs/CODE_OF_CONDUCT.md+35 docs/CONTRIBUTING.md+13 inflections.cabal+65−42 stack.yaml+1−1 stack.yaml.lock+12 test/Text/Inflections/DasherizeSpec.hs+21 test/Text/Inflections/HumanizeSpec.hs+47−14 test/Text/Inflections/OrdinalSpec.hs+31−51 test/Text/Inflections/ParametrizeSpec.hs+39 test/Text/Inflections/Parse/CamelCaseSpec.hs+47 test/Text/Inflections/Parse/SnakeCaseSpec.hs+35 test/Text/Inflections/PropertiesSpec.hs−90 test/Text/Inflections/TitleizeSpec.hs+15−15 test/Text/Inflections/TransliterateSpec.hs+29 test/Text/Inflections/TypesSpec.hs+51 test/Text/Inflections/UnderscoreSpec.hs+13−5 test/Text/InflectionsSpec.hs+92−19
.travis.yml+32−28
@@ -1,39 +1,43 @@
1−# This file is based on a generated file.
2−# See https://github.com/hvr/multi-ghc-travis.
3−
41language: c
52
63sudo: false
74
8−# GHC depends on GMP. You can add other dependencies here as well.
9−addons:
10− apt:
11− packages:
12− - libgmp-dev
13−
145cache:
156 directories:
16− - $HOME/.stack
7+ - ~/.ghc
8+ - ~/.cabal
179
1810matrix:
1911 include:
20− - env: ARGS="--resolver lts-2"
21− compiler: ": # lts-2 gch-7.8.4"
22− - env: ARGS="--resolver lts-3"
23− compiler: ": # lts-3 gch-7.10.2"
24− - env: ARGS="--resolver lts"
25− compiler: ": # lts"
12+ - env: CABALVER=1.24 GHCVER=7.10.3
13+ addons: {apt: {packages: [cabal-install-1.24,ghc-7.10.3],sources: [hvr-ghc]}}
14+ - env: CABALVER=1.24 GHCVER=8.0.2
15+ addons: {apt: {packages: [cabal-install-1.24,ghc-8.0.2],sources: [hvr-ghc]}}
16+ - env: CABALVER=2.2 GHCVER=8.2.2
17+ addons: {apt: {packages: [cabal-install-2.2,ghc-8.2.2], sources: [hvr-ghc]}}
18+ - env: CABALVER=2.2 GHCVER=8.4.3
19+ addons: {apt: {packages: [cabal-install-2.2,ghc-8.4.3], sources: [hvr-ghc]}}
2620
2721before_install:
28−# Using compiler above sets CC to an invalid value, so unset it
29−- unset CC
30−
31−# Download and unpack the stack executable
32−- mkdir -p ~/.local/bin
33−- export PATH=$HOME/.local/bin:$PATH
34−- curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack'
35−
36−# This line does all of the work: installs GHC if necessary, build the library,
37−# executables, and test suites, and runs the test suites. --no-terminal works
38−# around some quirks in Travis's terminal implementation.
39−script: stack $ARGS --no-terminal --install-ghc test --haddock --no-haddock-deps
22+ - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
23+
24+install:
25+ - cabal --version
26+ - echo "$(ghc --version) [$(ghc --print-project-git-commit-id 2> /dev/null || echo '?')]"
27+ - travis_retry cabal update
28+ - cabal install --only-dependencies --enable-tests
29+
30+script:
31+ - case "$CABALVER" in
32+ "1.16") cabal configure -v2 -f dev ;;
33+ *) cabal configure --enable-tests -v2 -f dev ;;
34+ esac
35+ - cabal build
36+ - cabal sdist
37+ - case "$GHCVER" in
38+ "8.4.3") cabal haddock | grep "100%" | wc -l | grep "1" ;;
39+ *) cabal haddock | grep "100%" | wc -l | grep "13" ;;
40+ esac
41+
42+notifications:
43+ email: false
CHANGELOG.md+47
@@ -1,9 +1,56 @@
1+## Inflections 0.4.0.6
2+* Support `megaparsec` == 9.0
3+
4+## Inflections 0.4.0.5
5+* Support `megaparsec` == 8.0
6+
7+## Inflections 0.4.0.3
8+* Support `megaparsec` == 7.0*.
9+* **Drop support** for `GHC 7.8.4`.
10+
11+## Inflections 0.4.0.3
12+* Support `exceptions` == 0.10.*
13+
14+## Inflections 0.4.0.2
15+* Support `exceptions` == 0.9.*
16+
17+## Inflections 0.4.0.1
18+* Support `megaparsec` >= 6.4.0
19+
20+## Inflections 0.4.0.0
21+
22+* Update megaparsec to version 6.
23+
124## Inflections 0.3.0.0
225
26+* A more type-safe API forbidding creation of invalid words.
27+
28+* Made the API use `Text` instead of `String` (which significally improved
29+ speed).
30+
31+* Switched to Megaparsec 5 for parsing.
32+
33+* Renamed `defaultMap` to `defaultTransliterations`.
34+
35+* Words now can contain digits (recognized by all parsers).
36+
37+* `parseSnakeCase` now is not confused when a word happens to have prefix
38+ coinciding with an acronym. This is harder to fix for `parseCamelCase`
39+ because acronym may contain capital letters, so old behavior is preserved
40+ for `parseCamelCase` for now.
41+
42+* `parseCamelCase` and `parseSnakeCase` take any instance of `Foldable` as a
43+ collection of acronyms, not just lists.
44+
345* Added the `CHANGELOG.md` file.
446
547* Switched test suite to Hspec.
648
49+* The `toUnderscore`, `toDashed`, and `toCamelCased` are not partial
50+ anymore. They return parse error in `Left` just like parsing functions,
51+ but this result can be lifted into any instance of `MonadThrow` with
52+ `betterThrow` helper.
53+
754* Improved documentation.
855
956## Inflections 0.2.0.1
LICENSE+17−18
@@ -1,22 +1,21 @@
1−Copyright (c) 2014-2016 Justin Leitgeb
2−
31MIT License
42
5−Permission is hereby granted, free of charge, to any person obtaining
6−a copy of this software and associated documentation files (the
7−"Software"), to deal in the Software without restriction, including
8−without limitation the rights to use, copy, modify, merge, publish,
9−distribute, sublicense, and/or sell copies of the Software, and to
10−permit persons to whom the Software is furnished to do so, subject to
11−the following conditions:
3+Copyright (c) 2022 Stack Builders Inc.
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy
6+of this software and associated documentation files (the "Software"), to deal
7+in the Software without restriction, including without limitation the rights
8+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+copies of the Software, and to permit persons to whom the Software is
10+furnished to do so, subject to the following conditions:
1211
13−The above copyright notice and this permission notice shall be
14−included in all copies or substantial portions of the Software.
12+The above copyright notice and this permission notice shall be included in all
13+copies or substantial portions of the Software.
1514
16−THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17−EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18−MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19−NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20−LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21−OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22−WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+SOFTWARE.
README.md+12−9
@@ -4,7 +4,7 @@
44[![Hackage](https://img.shields.io/hackage/v/inflections.svg)](http://hackage.haskell.org/package/inflections)
55[![Stackage Nightly](http://stackage.org/package/inflections/badge/nightly)](http://stackage.org/nightly/package/inflections)
66[![Stackage LTS](http://stackage.org/package/inflections/badge/lts)](http://stackage.org/lts/package/inflections)
7−[![Build Status](https://travis-ci.org/stackbuilders/inflections-hs.png)](https://travis-ci.org/stackbuilders/inflections-hs)
7+[![Build Status](https://travis-ci.org/stackbuilders/inflections-hs.svg?branch=master)](https://travis-ci.org/stackbuilders/inflections-hs)
88
99This library is a partial port of the
1010[String Inflector](http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html)
@@ -49,11 +49,10 @@ If you want to specify a custom default replacement or approximation table for
4949the `transliterate` function, you should instead call the `transliterateCustom`
5050function which accepts a String for replacements and a Map for substitution.
5151
52−## TODO
52+## Future work
5353
54−I'd like this library to implement other functions found in the Rails
55−inflections library. If you need one of those functions, please submit a pull
56−request!
54+Ideally, we want to implement other functions found in the Rails
55+inflections library. If you need one of those functions, please submit a pull request!
5756
5857## Further documentation
5958
@@ -64,10 +63,14 @@ For more information, please see the the
6463
6564Justin Leitgeb <justin@stackbuilders.com>
6665
67−## Contributing
66+## License
6867
69−You may submit pull requests to this repository on GitHub. Tests are appreciated with your contribution.
68+MIT, see [the LICENSE file](LICENSE).
7069
71−## License
70+## Contributing
71+
72+Do you want to contribute to this project? Please take a look at our [contributing guideline](/docs/CONTRIBUTING.md) to know how you can help us build it.
7273
73−MIT - see [the LICENSE file](LICENSE).
74+---
75+<img src="https://cdn.stackbuilders.com/media/images/Sb-supports.original.png" alt="Stack Builders" width="50%"></img>
76+[Check out our libraries](https://github.com/stackbuilders/) | [Join our team](https://www.stackbuilders.com/join-us/)
Text/Inflections.hs+182−151
@@ -1,171 +1,202 @@
1−{- |
2−Module : Text.Inflections
3−Description : Rails-like inflections library for common String transformations.
4−Copyright : (c) Justin Leitgeb
5−License : MIT
6−
7−Maintainer : justin@stackbuilders.com
8−Stability : unstable
9−Portability : portable
10−
11−This module provides methods for common String transformations, similar to the
12−"Inflections" library found in Rails:
13−
14−<http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
15−
16−While many of the functions in this library are the same as in implementations
17−in Rails' ActiveSupport, the philosophy of this library is fundamentally
18−different. Where Rails tries to be as permissive as possible, and return a
19−String when given any input, this library tries to output strings that make
20−sense according to the function that is called.
21−
22−When you look closely at many of the functions in Rails' inflections
23−library, you will notice that many of them are partial. That is, they only
24−have well-defined output for some of the possible inputs to the function allowed
25−by the type system. As an example, let's take the @underscore@ function. In
26−Rails, it works like this:
27−
28−>>> "fooBar".underscore
29−"foo_bar"
30−
31−Looks ok so far. However, it's also easy to produce less expected results:
32−
33−>>> "foo bar".underscore
34−"foo bar"
35−
36−The output isn't underscored - it contains a space! It turns out that some of
37−the functions from Inflections in ActiveSupport are /partial/. Ie., the outputs
38−are really only specified for a certain range of the inputs allowed by the
39−String type.
40−
41−In the Haskell inflections library, we aim to deliver more predictable results
42−by separating the parsing of strings into tokens from the application of
43−transformations. Let's see an example.
44−
45−First, we tokenize an underscored String using 'parseSnakeCase':
46−
47−>>> parseSnakeCase [] "foo_bar"
48−Right [Word "foo",Word "bar"]
49−
50−We can chain together the tokenization of the input String and the
51−transformation to CamelCase by using 'Control.Monad.LiftM':
52−
53−>>> import Control.Monad (liftM)
54−>>> liftM camelize $ parseSnakeCase "foo_bar"
55−
56−By separating out the tokenization from the application of inflections, we also
57−end up with useful libraries for validating input which can be used
58−independently:
59−
60−>>> parseSnakeCase [] "fooBar"
61−Left "(unknown)" (line 1, column 4):
62−unexpected 'B'
63−expecting lowercase letter, "_" or end of input
64−
65−This library is still a work-in-progress, and contributions are welcome for
66−missing pieces and to fix bugs. Please see the Github page to contribute with
67−code or bug reports:
68−
69−<https://github.com/stackbuilders/inflections-hs>
1+-- |
2+-- Module : Text.Inflections
3+-- Description : Rails-like inflections library for common Text transformations.
4+-- Copyright : (c) Justin Leitgeb
5+-- License : MIT
6+--
7+-- Maintainer : justin@stackbuilders.com
8+-- Stability : unstable
9+-- Portability : portable
10+--
11+-- This module provides methods for common 'Text' transformations, similar
12+-- to the Inflections library found in Rails:
13+--
14+-- <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
15+--
16+-- While many of the functions in this library are the same as in
17+-- implementations in Rails' ActiveSupport, the philosophy of this library
18+-- is fundamentally different. Where Rails tries to be as permissive as
19+-- possible, and return a String when given any input, this library tries to
20+-- output 'Text' that makes sense according to the function that is called.
21+--
22+-- When you look closely at many of the functions in Rails' inflections
23+-- library, you will notice that many of them are partial. That is, they
24+-- only have well-defined output for some of the possible inputs to the
25+-- function allowed by the type system. As an example, let's take the
26+-- @underscore@ function. In Rails, it works like this:
27+--
28+-- >>> "fooBar".underscore
29+-- "foo_bar"
30+--
31+-- Looks OK so far. However, it's also easy to produce less expected results:
32+--
33+-- >>> "foo bar".underscore
34+-- "foo bar"
35+--
36+-- The output isn't underscored — it contains a space! It turns out that
37+-- some of the functions from Inflections in ActiveSupport are /partial/.
38+-- I.e., the outputs are really only specified for a certain range of the
39+-- inputs allowed by the String type.
40+--
41+-- In the Haskell inflections library, we aim to deliver more predictable
42+-- results by separating the parsing of strings into tokens from the
43+-- application of transformations. Let's see an example.
44+--
45+-- First, we tokenize an underscored 'Text' using 'parseSnakeCase':
46+--
47+-- >>> parseSnakeCase [] "foo_bar"
48+-- Right [Word "foo",Word "bar"]
49+--
50+-- We can chain together the tokenization of the input String and the
51+-- transformation to CamelCase by using 'fmap':
52+--
53+-- >>> camelize <$> parseSnakeCase [] "foo_bar"
54+-- Right "FooBar"
55+--
56+-- By separating out the tokenization from the application of inflections,
57+-- we also end up with useful libraries for validating input which can be
58+-- used independently:
59+--
60+-- >>> parseSnakeCase [] "fooBar"
61+-- 1:4:
62+-- unexpected 'B'
63+-- expecting '_', end of input, or lowercase letter
64+--
65+-- As of version 0.3.0.0, we don't permit creation of invalid 'Word's by
66+-- using of the smart constructors 'mkWord' and 'mkAcronym'. This is done
67+-- because not every 'Text' value is a valid 'Word', as it should not
68+-- contain whitespace, for example. Normal words have the type @'Word'
69+-- 'Normal'@, while acronyms have the type @'Word' 'Acronym'@. If you need
70+-- to have several words\/acronyms in a single list, use the existential
71+-- wrapper 'SomeWord'. Parsing functions now produce 'SomeWord's.
72+--
73+-- This library is still a work-in-progress, and contributions are welcome
74+-- for missing pieces and to fix bugs. Please see the Github page to
75+-- contribute with code or bug reports:
76+--
77+-- <https://github.com/stackbuilders/inflections-hs>
7078
71−-}
79+{-# LANGUAGE CPP #-}
7280
7381module Text.Inflections
74− ( camelize
75− , camelizeCustom
76−
77− , dasherize
78−
79− , humanize
80−
81− , underscore
82−
83− , titleize
84−
85− , defaultMap
86−
87− , parameterize
88− , parameterizeCustom
89−
90− , transliterate
91− , transliterateCustom
92−
93− , ordinal
94− , ordinalize
95−
96− , parseSnakeCase
97− , parseCamelCase
98− , Transliterations
82+ ( -- * Types and helpers
83+ Word
84+ , WordType (..)
85+ , mkWord
86+ , mkAcronym
87+ , unWord
88+ , SomeWord (..)
89+ , unSomeWord
90+ , InflectionException (..)
91+ -- * Parsing
92+ , parseSnakeCase
93+ , parseCamelCase
94+ -- * Rendering
95+ , camelize
96+ , camelizeCustom
97+ , dasherize
98+ , humanize
99+ , humanizeCustom
100+ , underscore
101+ , titleize
102+ , Transliterations
103+ , defaultTransliterations
104+ , parameterize
105+ , parameterizeCustom
106+ , transliterate
107+ , transliterateCustom
108+ , ordinalize
109+ , ordinal
99110 -- * Often used combinators
100− , toUnderscore
101− , toDashed
102− , toCamelCased
103− )
111+ , toUnderscore
112+ , toDashed
113+ , toCamelCased
114+ , toHumanized
115+ , betterThrow )
104116where
105117
106−
107−import Text.Inflections.Data ( defaultMap )
108−
109−import Text.Inflections.Parameterize ( Transliterations
110− , parameterize
111− , parameterizeCustom )
112−
113−import Text.Inflections.Underscore ( underscore )
114−
115−import Text.Inflections.Camelize ( camelize, camelizeCustom )
116−
117−import Text.Inflections.Humanize ( humanize )
118−
119−import Text.Inflections.Titleize ( titleize )
120−
121−import Text.Inflections.Transliterate ( transliterate, transliterateCustom )
122−
123−import Text.Inflections.Dasherize ( dasherize )
124−
125−import Text.Inflections.Ordinal ( ordinal, ordinalize )
126−
127−import Text.Inflections.Parse.SnakeCase ( parseSnakeCase )
128−
129−import Text.Inflections.Parse.Types ( mapWord )
130−
131−import Text.Inflections.Parse.CamelCase ( parseCamelCase )
132−
133−import Data.Char ( toLower )
134−
135−-- | Transforms CamelCasedString to
136−-- snake_cased_string_with_underscores. Throws exception if parsing failed
118+import Control.Monad.Catch (MonadThrow (..))
119+import Data.Text (Text)
120+import Data.Void (Void)
121+import Text.Inflections.Camelize (camelize, camelizeCustom)
122+import Text.Inflections.Dasherize (dasherize)
123+import Text.Inflections.Data (Transliterations, defaultTransliterations)
124+import Text.Inflections.Humanize (humanize, humanizeCustom)
125+import Text.Inflections.Ordinal (ordinal, ordinalize)
126+import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
127+import Text.Inflections.Parse.CamelCase (parseCamelCase)
128+import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
129+import Text.Inflections.Titleize (titleize)
130+import Text.Inflections.Transliterate (transliterate, transliterateCustom)
131+import Text.Inflections.Types
132+import Text.Inflections.Underscore (underscore)
133+import Text.Megaparsec
134+
135+#if MIN_VERSION_base(4,8,0)
136+import Prelude hiding (Word)
137+#endif
138+
139+-- | Transforms CamelCasedString to snake_cased_string_with_underscores.
140+--
141+-- > toUnderscore = fmap underscore . parseCamelCase []
137142--
138143-- >>> toUnderscore "FooBarBazz"
139144-- "foo_bar_bazz"
140−toUnderscore :: String -> String
141−toUnderscore =
142− underscore
143− . either (error . ("toUnderscore: " ++) . show) id
144− . parseCamelCase []
145+toUnderscore :: Text -> Either (ParseErrorBundle Text Void) Text
146+toUnderscore = fmap underscore . parseCamelCase []
145147
146−-- | Transforms CamelCasedString to snake-cased-string-with-dashes. Throws
147−-- exception if parsing failed.
148+-- | Transforms CamelCasedString to snake-cased-string-with-dashes.
149+--
150+-- > toDashed = fmap dasherize . parseCamelCase []
148151--
149152-- >>> toDashed "FooBarBazz"
150153-- "foo-bar-bazz"
151−toDashed :: String -> String
152−toDashed =
153− dasherize
154− . map (mapWord (map toLower))
155− . either (error . ("toDashed: " ++) . show) id
156− . parseCamelCase []
154+toDashed :: Text -> Either (ParseErrorBundle Text Void) Text
155+toDashed = fmap dasherize . parseCamelCase []
157156
158157-- | Transforms underscored_text to CamelCasedText. If first argument is
159−-- 'True' then FirstCharacter in result string will be in upper case. If
160−-- 'False' then firstCharacter will be in lower case. Throws exception if
161−-- parsing failed
158+-- 'True' then the first character in the result string will be in upper case. If
159+-- 'False' then the first character will be in lower case.
160+--
161+-- > toCamelCased c = fmap (camelizeCustom c) . parseSnakeCase []
162162--
163163-- >>> toCamelCased True "foo_bar_bazz"
164164-- "FooBarBazz"
165165-- >>> toCamelCased False "foo_bar_bazz"
166166-- "fooBarBazz"
167−toCamelCased :: Bool -> String -> String
168−toCamelCased t =
169− camelizeCustom t
170− . either (error . ("toCamelCased: " ++) . show) id
171− . parseSnakeCase []
167+toCamelCased
168+ :: Bool -- ^ Capitalize the first character
169+ -> Text -- ^ Input
170+ -> Either (ParseErrorBundle Text Void) Text -- ^ Output
171+toCamelCased c = fmap (camelizeCustom c) . parseSnakeCase []
172+
173+-- | Transforms underscored_text to space-separated human-readable text.
174+-- If first argument is 'True' then the first character in the result
175+-- string will be in upper case. If 'False' then the first character will be
176+-- in lower case.
177+--
178+-- > toHumanized c = fmap (humanizeCustom c) . parseSnakeCase []
179+--
180+-- >>> toHumanized True "foo_bar_bazz"
181+-- "Foo bar bazz"
182+-- >>> toHumanized False "foo_bar_bazz"
183+-- "foo bar bazz"
184+--
185+-- /since 0.3.0.0/
186+toHumanized
187+ :: Bool -- ^ Capitalize the first character
188+ -> Text -- ^ Input
189+ -> Either (ParseErrorBundle Text Void) Text -- ^ Output
190+toHumanized c = fmap (humanizeCustom c) . parseSnakeCase []
191+
192+-- | Lift something of type @'Either' ('ParseError' 'Char' 'Void') a@ to
193+-- an instance of 'MonadThrow'. Useful when you want to shortcut on parsing
194+-- failures and you're in an instance of 'MonadThrow'.
195+--
196+-- This throws 'InflectionParsingFailed' if given value is inside 'Left'.
197+--
198+-- /since 0.3.0.0/
199+
200+betterThrow :: MonadThrow m => Either (ParseErrorBundle Text Void) a -> m a
201+betterThrow (Left err) = throwM (InflectionParsingFailed err)
202+betterThrow (Right x) = return x
Text/Inflections/Camelize.hs+41−28
@@ -1,40 +1,53 @@
1−module Text.Inflections.Camelize ( camelize, camelizeCustom ) where
1+-- |
2+-- Module : Text.Inflections.Camelize
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Conversion to CamelCased phrases.
211
3−import Text.Inflections.Parse.Types (Word(..))
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE OverloadedStrings #-}
414
5−import Data.Char (toUpper, toLower)
15+module Text.Inflections.Camelize
16+ ( camelize
17+ , camelizeCustom )
18+where
619
7−import Prelude (String, Bool(..), concatMap, (.), zip, ($), repeat)
20+import Data.Text (Text)
21+import Text.Inflections.Types
22+import qualified Data.Text as T
823
9−-- |Turns a an input Word List in into CamelCase. Returns the CamelCase String.
24+#if !MIN_VERSION_base(4,8,0)
25+import Control.Applicative
26+#endif
27+
28+-- | Turn an input word list in into CamelCase.
1029--
11−-- >>> camelize [ Word "foo", Acronym "bar", Word "bazz" ]
30+-- >>> foo <- SomeWord <$> mkWord "foo"
31+-- >>> bar <- SomeWord <$> mkAcronym "bar"
32+-- >>> bazz <- SomeWord <$> mkWord "bazz"
33+-- >>> camelize [foo,bar,bazz]
1234-- "FoobarBazz"
13−
1435camelize
15− :: [Word] -- ^ Input Words to separate with underscores
16− -> String -- ^ The camelized String
36+ :: [SomeWord] -- ^ Input words
37+ -> Text -- ^ The camelized 'Text'
1738camelize = camelizeCustom True
1839
19−-- |Turns an input Word List into a CamelCase String.
40+-- | Turn an input word list into a CamelCase String.
2041--
21−-- >>> camelizeCustom False [ Word "foo", Acronym "bar", Word "bazz" ]
42+-- >>> foo <- SomeWord <$> mkWord "foo"
43+-- >>> bar <- SomeWord <$> mkAcronym "bar"
44+-- >>> bazz <- SomeWord <$> mkWord "bazz"
45+-- >>> camelizeCustom False [foo,bar,bazz]
2246-- "foobarBazz"
23−
2447camelizeCustom
25− :: Bool -- ^ Whether to capitalize the first character in the output String
26− -> [Word] -- ^ The input Words
27− -> String -- ^ The camelized String
28−camelizeCustom isFirstCap = concatMap (caseForWord isFirstCap) . isFirstList
29−
30−
31−caseForWord :: Bool -> (Word, Bool) -> String
32−caseForWord True (Word (c:cs), True) = toUpper c : cs
33−caseForWord False (Word (c:cs), True) = toLower c : cs
34−caseForWord _ (Word (c:cs), _) = toUpper c : cs
35−caseForWord _ (Word [], _) = []
36−caseForWord _ (Acronym s, _) = s
37−
38−-- |Returns list with Bool indicating if an element is first.
39−isFirstList :: [a] -> [(a, Bool)]
40−isFirstList xs = zip xs $ True : repeat False
48+ :: Bool -- ^ Whether to capitalize the first character in the output String
49+ -> [SomeWord] -- ^ The input Words
50+ -> Text -- ^ The camelized 'Text'
51+camelizeCustom _ [] = ""
52+camelizeCustom c (x:xs) = T.concat $
53+ unSomeWord (if c then T.toTitle else T.toLower) x : (unSomeWord T.toTitle <$> xs)
Text/Inflections/Dasherize.hs+26−16
@@ -1,22 +1,32 @@
1−module Text.Inflections.Dasherize ( dasherize ) where
2−
3−import Text.Inflections.Parse.Types (Word(..))
1+-- |
2+-- Module : Text.Inflections.Dasherize
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Conversion to dasherized phrases.
411
5−import Data.List (intercalate)
12+{-# LANGUAGE OverloadedStrings #-}
613
7−import Prelude (String, (.), map)
14+module Text.Inflections.Dasherize
15+ ( dasherize )
16+where
817
9−-- | Replaces underscores in a snake_cased string with dashes (hyphens).
18+import Data.Text (Text)
19+import Text.Inflections.Types
20+import qualified Data.Text as T
1021
11−-- |
12−-- >>> dasherize [ Word "foo", Acronym "bar", Word "bazz" ]
22+-- | Produce a string with words separated by dashes (hyphens).
23+--
24+-- >>> foo <- SomeWord <$> mkWord "foo"
25+-- >>> bar <- SomeWord <$> mkAcronym "bar"
26+-- >>> bazz <- SomeWord <$> mkWord "bazz"
27+-- >>> dasherize [foo,bar,bazz]
1328-- "foo-bar-bazz"
14−
1529dasherize
16− :: [Word] -- ^ Input Words to separate with dashes
17− -> String -- ^ The dasherized String
18−dasherize = intercalate "-" . map toString
19−
20−toString :: Word -> String
21−toString (Acronym s) = s
22−toString (Word s) = s
30+ :: [SomeWord] -- ^ Input words to separate with dashes
31+ -> Text -- ^ The dasherized 'Text'
32+dasherize = T.intercalate "-" . fmap (unSomeWord T.toLower)
Text/Inflections/Data.hs+27−5
@@ -1,11 +1,33 @@
1−module Text.Inflections.Data where
1+-- |
2+-- Module : Text.Inflections.Data
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Auxiliary data used in the library.
211
3−import Data.Map (Map, fromList)
12+module Text.Inflections.Data
13+ ( Transliterations
14+ , defaultTransliterations )
15+where
416
5−-- |These default transliterations stolen from the Ruby i18n library - see
17+import Data.HashMap.Strict (HashMap)
18+import qualified Data.HashMap.Strict as M
19+
20+-- | A 'HashMap' containing mappings from international characters to
21+-- sequences approximating these characters within the ASCII range.
22+type Transliterations = HashMap Char String
23+
24+-- | These default transliterations are stolen from the Ruby i18n library -
25+-- see
626-- <https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/transliterator.rb#L41:L69>.
7−defaultMap :: Map Char String
8−defaultMap = fromList [
27+--
28+-- NOTE: before version 0.3.0.0 this was called @defaultMap@.
29+defaultTransliterations :: Transliterations
30+defaultTransliterations = M.fromList [
931 ('À', "A"), ('Á', "A"), ('Â', "A"), ('Ã', "A"), ('Ä', "A"), ('Å', "A"),
1032 ('Æ', "AE"), ('Ç', "C"), ('È', "E"), ('É', "E"), ('Ê', "E"), ('Ë', "E"),
1133 ('Ì', "I"), ('Í', "I"), ('Î', "I"), ('Ï', "I"), ('Ð', "D"), ('Ñ', "N"),
Text/Inflections/Humanize.hs+54−18
@@ -1,27 +1,63 @@
1−module Text.Inflections.Humanize (humanize) where
1+-- |
2+-- Module : Text.Inflections.Humanize
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Conversion to “humanized” phrases.
11+
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE OverloadedStrings #-}
214
3−import Text.Inflections.Parse.Types (Word(..))
15+module Text.Inflections.Humanize
16+ ( humanize
17+ , humanizeCustom )
18+where
419
5−import Data.Char (toUpper)
20+import Data.Text (Text)
21+import Text.Inflections.Types
22+import qualified Data.Text as T
623
7−import Prelude (String, Bool(..), (.), map, zip, ($), unwords, repeat)
24+#if !MIN_VERSION_base(4,8,0)
25+import Control.Applicative
26+#endif
827
9−-- |Capitalizes the first word and turns underscores into spaces. Like titleize,
10−-- this is meant for creating pretty output.
28+-- | Capitalize the first word and separate words with spaces. Like
29+-- 'Text.Inflections.Titleize.titleize', this is meant for creating pretty
30+-- output.
1131--
12−-- >>> humanize [ Word "foo", Acronym "bar", Word "bazz" ]
32+-- >>> foo <- SomeWord <$> mkWord "foo"
33+-- >>> bar <- SomeWord <$> mkAcronym "bar"
34+-- >>> bazz <- SomeWord <$> mkWord "bazz"
35+-- >>> humanize [foo,bar,bazz]
1336-- "Foo bar bazz"
1437humanize
15− :: [Word] -- ^ List of Words, first of which will be capitalized
16− -> String -- ^ The humanized output
17−humanize = unwords . map caseForWord . isFirstList
38+ :: [SomeWord] -- ^ List of words, first of which will be capitalized
39+ -> Text -- ^ The humanized output
40+humanize = humanizeCustom True
1841
19−-- |Returns list with Bool indicating if an element is first.
20−isFirstList :: [a] -> [(a, Bool)]
21−isFirstList xs = zip xs $ True : repeat False
2242
23−caseForWord :: (Word, Bool) -> String
24−caseForWord (Word (c:cs), True) = toUpper c : cs
25−caseForWord (Word s, False) = s
26−caseForWord (Word [], _) = []
27−caseForWord (Acronym s, _) = s -- Acronyms are left intact
43+-- | Separate words with spaces, optionally capitalizing the first word. Like
44+-- 'Text.Inflections.Titleize.titleize', this is meant for creating pretty
45+-- output.
46+--
47+-- >>> foo <- SomeWord <$> mkWord "foo"
48+-- >>> bar <- SomeWord <$> mkAcronym "bar"
49+-- >>> bazz <- SomeWord <$> mkWord "bazz"
50+-- >>> humanizeCustom True [foo,bar,bazz]
51+-- "Foo bar bazz"
52+-- >>> humanizeCustom False [foo,bar,bazz]
53+-- "foo bar bazz"
54+--
55+-- /since 0.3.0.0/
56+humanizeCustom
57+ :: Bool -- ^ Whether to capitalize the first character in the output String
58+ -> [SomeWord] -- ^ List of words, first of which will be capitalized
59+ -> Text -- ^ The humanized output
60+humanizeCustom c xs' =
61+ case unSomeWord (T.replace "_" " ") <$> xs' of
62+ [] -> ""
63+ (x:xs) -> T.unwords $ (if c then T.toTitle else T.toLower) x : (T.toLower <$> xs)
Text/Inflections/Ordinal.hs+32−15
@@ -1,6 +1,36 @@
1−module Text.Inflections.Ordinal (ordinal, ordinalize)
1+-- |
2+-- Module : Text.Inflections.Ordinal
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Conversion to spelled ordinal numbers.
11+
12+{-# LANGUAGE OverloadedStrings #-}
13+
14+module Text.Inflections.Ordinal
15+ ( ordinalize
16+ , ordinal )
217where
318
19+import Data.Text (Text)
20+import qualified Data.Text as T
21+
22+-- |Turns a number into an ordinal string used to denote the position in an
23+-- ordered sequence such as 1st, 2nd, 3rd, 4th.
24+--
25+-- >>> ordinalize 1
26+-- "1st"
27+-- >>> ordinalize 2
28+-- "2nd"
29+-- >>> ordinalize 10
30+-- "10th"
31+ordinalize :: (Integral a, Show a) => a -> Text
32+ordinalize n = T.pack (show n) <> ordinal n
33+
434-- |Returns the suffix that should be added to a number to denote the position
535-- in an ordered sequence such as 1st, 2nd, 3rd, 4th.
636--
@@ -10,7 +40,7 @@ where
1040-- "nd"
1141-- >>> ordinal 10
1242-- "th"
13−ordinal :: Integer -> String
43+ordinal :: Integral a => a -> Text
1444ordinal number
1545 | remainder100 `elem` [11..13] = "th"
1646 | remainder10 == 1 = "st"
@@ -20,16 +50,3 @@ ordinal number
2050 where abs_number = abs number
2151 remainder10 = abs_number `mod` 10
2252 remainder100 = abs_number `mod` 100
23−
24−-- |Turns a number into an ordinal string used to denote the position in an
25−-- ordered sequence such as 1st, 2nd, 3rd, 4th.
26−--
27−-- >>> ordinalize 1
28−-- "1st"
29−-- >>> ordinalize 2
30−-- "2nd"
31−-- >>> ordinalize 10
32−-- "10th"
33−ordinalize :: Integer -> String
34−ordinalize n = show n ++ ordinal n
35−
Text/Inflections/Parameterize.hs+44−86
@@ -1,90 +1,48 @@
1+-- |
2+-- Module : Text.Inflections.Parametrize
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Parametrization for strings, useful for transliteration.
11+
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE OverloadedStrings #-}
14+
115module Text.Inflections.Parameterize
216 ( parameterize
3− , parameterizeCustom
4− , Transliterations )
17+ , parameterizeCustom )
518where
619
7−import qualified Data.Map as Map
8−import Control.Monad (guard)
9−import Data.Maybe (mapMaybe)
10−
11−import Data.Char (toLower)
12−
13−import Data.List (group)
14−import qualified Text.Parsec as P
15−
16−import Text.Inflections.Data (defaultMap)
17−import Text.Inflections.Parse.Parameterizable ( PChar(..)
18− , parser
19− , isValidParamChar )
20−
21−-- |A 'Data.Map.Map' containing mappings from international characters to
22−-- sequences approximating these characters within the ASCII range.
23−type Transliterations = Map.Map Char String
24−
25−-- |Replaces special characters in a string so that it may be used as part of a
26−-- 'pretty' URL. Uses the default transliterations in this library
27−parameterize :: String -> String
28−parameterize = parameterizeCustom defaultMap
29−
30−-- |Transliterate a String with a custom transliteration table.
31−parameterizeCustom :: Transliterations -> String -> String
32−parameterizeCustom ts s =
33− case parsed of
34− Right ast -> (concatMap pCharToC . squeezeSeparators .
35− trimUnwanted wanted . mapMaybe (parameterizeChar ts))
36− ast
37−
38− -- Note that this should never fail, since we accommodate all Unicode
39− -- characters as valid input.
40− Left err -> fail $ "Parse failed, please report a bug! Error: " ++
41− show err
42−
43− where parsed = P.parse parser "" s
44− wanted :: [PChar] -- All valid URL chars - we shouldn't trim these.
45− wanted = Underscore :
46− map (Acceptable . (: [])) (['a'..'z'] ++ ['0'..'9'])
47−
48−-- |Look up character in transliteration list. Accepts a Transliteration map
49−-- which has Chars as keys and Strings as values for approximating common
50−-- international Unicode characters within the ASCII range.
51−transliteratePCharCustom :: Transliterations -> Char -> Maybe PChar
52−transliteratePCharCustom ts c = do
53− -- We may have expanded into multiple characters during
54− -- transliteration, so check validity of all characters in
55− -- result.
56− v <- Map.lookup c ts
57− guard (all isValidParamChar v)
58− return (Acceptable v)
59−
60−-- |Given a Transliteration table and a PChar, returns Maybe PChar indicating
61−-- how this character should appear in a URL.
62−parameterizeChar :: Transliterations -> PChar -> Maybe PChar
63−parameterizeChar _ (UCase c) = Just $ Acceptable [toLower c]
64−parameterizeChar _ (Acceptable c) = Just $ Acceptable c
65−parameterizeChar _ Separator = Just Separator
66−parameterizeChar _ Underscore = Just Underscore
67−parameterizeChar _ (OtherAscii _) = Just Separator
68−parameterizeChar ts (NonAscii c) = transliteratePCharCustom ts c
69−
70−-- |Turns PChar tokens into their String representation.
71−pCharToC :: PChar -> String
72−pCharToC (UCase c) = [c]
73−pCharToC (Acceptable str) = str
74−pCharToC Separator = "-"
75−pCharToC Underscore = "_"
76−pCharToC (OtherAscii c) = [c]
77−pCharToC (NonAscii c) = [c]
78−
79−-- |Reduce sequences of separators down to only one separator.
80−squeezeSeparators :: [PChar] -> [PChar]
81−squeezeSeparators ps = concatMap squashSeparatorGroup $ group ps
82− where squashSeparatorGroup g = case head g of
83− Separator -> [Separator] -- only take head
84− _ -> g -- don't change
85−
86−-- |Trim non-wanted elements from the beginning and end of list.
87−trimUnwanted :: Eq a => [a] -> [a] -> [a]
88−trimUnwanted wanted = dropWhile notWanted . reverse . dropWhile notWanted
89− . reverse
90− where notWanted = (`notElem` wanted)
20+import Data.Char (isAscii, isAlphaNum, isPunctuation, toLower)
21+import Data.Text (Text)
22+import Text.Inflections.Data
23+import qualified Data.HashMap.Strict as M
24+import qualified Data.Text as T
25+
26+#if !MIN_VERSION_base(4,8,0)
27+import Control.Applicative
28+#endif
29+
30+-- | Replace special characters in a string so that it may be used as part
31+-- of a 'pretty' URL. Uses the 'defaultTransliterations'.
32+parameterize :: Text -> Text
33+parameterize = parameterizeCustom defaultTransliterations
34+{-# INLINE parameterize #-}
35+
36+-- | Transliterate 'Text' with a custom transliteration table.
37+parameterizeCustom :: Transliterations -> Text -> Text
38+parameterizeCustom m txt = (T.intercalate "-" . T.words) (T.unfoldr f ("", txt))
39+ where
40+ f ("", t) = uncurry g <$> T.uncons t
41+ f (x:xs, t) = Just (x, (xs, t))
42+ g x xs
43+ | (isAscii x && isAlphaNum x) || x == '_' = (toLower x, ("", xs))
44+ | isPunctuation x = (' ', ("", xs))
45+ | otherwise =
46+ case toLower <$> M.lookupDefault " " x m of
47+ "" -> (' ', ("",xs))
48+ (y:ys) -> (y, (ys,xs))
Text/Inflections/Parse/Acronym.hsdeleted−16
@@ -1,16 +0,0 @@
1−{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
2−
3−module Text.Inflections.Parse.Acronym ( acronym ) where
4−
5−import qualified Text.ParserCombinators.Parsec.Char as C
6−import qualified Text.Parsec as P
7−import qualified Text.Parsec.Prim as Prim
8−
9−import Text.Inflections.Parse.Types
10−
11−import Control.Applicative ((<$>))
12−
13−import Prelude (Char, String, (.), map)
14−
15−acronym :: P.Stream s m Char => [String] -> P.ParsecT s u m Word
16−acronym as = Acronym <$> P.choice (map (Prim.try . C.string) as)
Text/Inflections/Parse/CamelCase.hs+72−18
@@ -1,28 +1,82 @@
1−{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
1+-- |
2+-- Module : Text.Inflections.Parse.CamelCase
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Parser for camel case “symbols”.
211
3−module Text.Inflections.Parse.CamelCase ( parseCamelCase )
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE DataKinds #-}
14+{-# LANGUAGE OverloadedStrings #-}
15+
16+module Text.Inflections.Parse.CamelCase
17+ ( parseCamelCase )
418where
519
6−import Text.Parsec
20+# if MIN_VERSION_base(4,8,0)
21+import Control.Applicative (empty, many, (<|>))
22+#else
23+import Control.Applicative (empty, many, (<|>), (<$>), (<*))
24+#endif
25+import Data.Text (Text)
26+import Data.Void (Void)
27+import Text.Inflections.Types
28+import Text.Megaparsec (Parsec, ParseErrorBundle, choice, eof, parse)
29+import Text.Megaparsec.Char
30+import qualified Data.Text as T
731
8−import Text.Inflections.Parse.Types (Word(..))
9−import Text.Inflections.Parse.Acronym (acronym)
32+#if MIN_VERSION_base(4,8,0)
33+import Prelude hiding (Word)
34+#else
35+import Data.Foldable
36+import Prelude hiding (elem)
37+#endif
1038
11−import Prelude (Char, String, Either, return, ($))
39+type Parser = Parsec Void Text
1240
13−parseCamelCase :: [String] -> String -> Either ParseError [Word]
14−parseCamelCase acronyms = parse (parser acronyms) "(unknown)"
41+-- | Parse a CamelCase string.
42+--
43+-- >>> bar <- mkAcronym "bar"
44+-- >>> parseCamelCase [bar] "FooBarBazz"
45+-- Right [Word "Foo",Acronym "Bar",Word "Bazz"]
46+--
47+-- >>> parseCamelCase [] "foo_bar_bazz"
48+-- 1:4:
49+-- unexpected '_'
50+-- expecting end of input, lowercase letter, or uppercase letter
51+parseCamelCase :: (Foldable f, Functor f)
52+ => f (Word 'Acronym) -- ^ Collection of acronyms
53+ -> Text -- ^ Input
54+ -> Either (ParseErrorBundle Text Void) [SomeWord] -- ^ Result of parsing
55+parseCamelCase acronyms = parse (parser acronyms) ""
1556
57+parser :: (Foldable f, Functor f)
58+ => f (Word 'Acronym) -- ^ Collection of acronyms
59+ -> Parser [SomeWord] -- ^ CamelCase parser
60+parser acronyms = many (a <|> n) <* eof
61+ where
62+ n = SomeWord <$> word
63+ a = SomeWord <$> acronym acronyms
1664
17−-- |Recognizes an input String in CamelCase.
18−parser :: Stream s m Char => [String] -> ParsecT s u m [Word]
19−parser acronyms = do
20− ws <- many $ choice [ acronym acronyms, word ]
21− eof
22− return ws
65+acronym :: (Foldable f, Functor f)
66+ => f (Word 'Acronym)
67+ -> Parser (Word 'Acronym)
68+acronym acronyms = do
69+ x <- choice (string . unWord <$> acronyms)
70+ case mkAcronym x of
71+ Nothing -> empty -- cannot happen if the system is sound
72+ Just acr -> return acr
73+{-# INLINE acronym #-}
2374
24−word :: Stream s m Char => ParsecT s u m Word
75+word :: Parser (Word 'Normal)
2576word = do
26− firstChar <- upper <|> lower
27− restChars <- many lower
28− return $ Word $ firstChar : restChars
77+ firstChar <- upperChar <|> lowerChar
78+ restChars <- many $ lowerChar <|> digitChar
79+ case (mkWord . T.pack) (firstChar : restChars) of
80+ Nothing -> empty -- cannot happen if the system is sound
81+ Just wrd -> return wrd
82+{-# INLINE word #-}
Text/Inflections/Parse/Parameterizable.hsdeleted−42
@@ -1,42 +0,0 @@
1−{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
2−
3−module Text.Inflections.Parse.Parameterizable
4− ( parser
5− , isValidParamChar
6− , PChar(..) )
7−where
8−
9−import Data.Char (isAsciiLower, isAsciiUpper, isAscii, isDigit)
10−import Control.Applicative
11−import qualified Text.Parsec as P
12−import qualified Text.ParserCombinators.Parsec.Char as C
13−
14−data PChar = UCase Char
15− -- Since some of the transliterating approximations expand from
16− -- one Unicode to two ASCII chars (eg., œ to oe), we represent
17− -- this as a String.
18− | Acceptable String
19− | Separator
20− | Underscore
21− | OtherAscii Char
22− | NonAscii Char
23− deriving (Eq, Show)
24−
25−-- |Matches 'acceptable' characters for parameterization purposes.
26−acceptableParser :: P.Stream s m Char => P.ParsecT s u m PChar
27−acceptableParser = do
28− c <- C.satisfy isValidParamChar
29− return $ Acceptable [c]
30−
31−parser :: P.Stream s m Char => P.ParsecT s u m [PChar]
32−parser = P.many $ P.choice [
33− acceptableParser
34− , UCase <$> C.satisfy isAsciiUpper
35− , Separator <$ C.char '-'
36− , Underscore <$ C.char '_'
37− , OtherAscii <$> C.satisfy isAscii
38− , NonAscii <$> C.satisfy (not . isAscii)
39− ]
40−
41−isValidParamChar :: Char -> Bool
42−isValidParamChar c = isAsciiLower c || isDigit c
Text/Inflections/Parse/SnakeCase.hs+61−17
@@ -1,25 +1,69 @@
1−{-# LANGUAGE FlexibleContexts, NoMonomorphismRestriction #-}
1+-- |
2+-- Module : Text.Inflections.Parse.SnakeCase
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Parser for snake case “symbols”.
211
3−module Text.Inflections.Parse.SnakeCase ( parseSnakeCase )
4−where
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE DataKinds #-}
514
6−import Control.Applicative ((<$>))
7−import Text.Parsec
15+module Text.Inflections.Parse.SnakeCase
16+ ( parseSnakeCase )
17+where
818
9−import Text.Inflections.Parse.Types (Word(..))
10−import Text.Inflections.Parse.Acronym (acronym)
19+# if MIN_VERSION_base(4,8,0)
20+import Control.Applicative (empty, some)
21+#else
22+import Control.Applicative (empty, some, (<$>), (<*))
23+#endif
24+import Data.Text (Text)
25+import Data.Void (Void)
26+import Text.Inflections.Types
27+import Text.Megaparsec (Parsec, ParseErrorBundle, eof, sepBy, parse)
28+import Text.Megaparsec.Char
29+import qualified Data.Text as T
1130
12−import Prelude (Char, String, Either, return)
31+#if MIN_VERSION_base(4,8,0)
32+import Prelude hiding (Word)
33+#else
34+import Data.Foldable
35+import Prelude hiding (elem)
36+#endif
1337
14−parseSnakeCase :: [String] -> String -> Either ParseError [Word]
15−parseSnakeCase acronyms = parse (parser acronyms) "(unknown)"
38+type Parser = Parsec Void Text
1639
40+-- | Parse a snake_case string.
41+--
42+-- >>> bar <- mkAcronym "bar"
43+-- >>> parseSnakeCase [bar] "foo_bar_bazz"
44+-- Right [Word "foo",Acronym "bar",Word "bazz"]
45+--
46+-- >>> parseSnakeCase [] "fooBarBazz"
47+-- 1:4:
48+-- unexpected 'B'
49+-- expecting '_', end of input, or lowercase letter
50+parseSnakeCase :: (Foldable f, Functor f)
51+ => f (Word 'Acronym) -- ^ Collection of acronyms
52+ -> Text -- ^ Input
53+ -> Either (ParseErrorBundle Text Void) [SomeWord] -- ^ Result of parsing
54+parseSnakeCase acronyms = parse (parser acronyms) ""
1755
18−parser :: Stream s m Char => [String] -> ParsecT s u m [Word]
19−parser acronyms = do
20− ws <- (acronym acronyms <|> word) `sepBy` char '_'
21− eof
22− return ws
56+parser :: (Foldable f, Functor f)
57+ => f (Word 'Acronym)
58+ -> Parser [SomeWord]
59+parser acronyms = (pWord acronyms `sepBy` char '_') <* eof
2360
24−word :: Stream s m Char => ParsecT s u m Word
25−word = Word <$> (many1 lower <|> many1 digit)
61+pWord :: (Foldable f, Functor f)
62+ => f (Word 'Acronym)
63+ -> Parser SomeWord
64+pWord acronyms = do
65+ let acs = unWord <$> acronyms
66+ r <- T.pack <$> some alphaNumChar
67+ if r `elem` acs
68+ then maybe empty (return . SomeWord) (mkAcronym r)
69+ else maybe empty (return . SomeWord) (mkWord r)
Text/Inflections/Parse/Types.hsdeleted−18
@@ -1,18 +0,0 @@
1−module Text.Inflections.Parse.Types ( Word(..), mapWord ) where
2−
3−import Prelude (String, Show, Eq, ($))
4−
5−-- | A 'String' that should be kept whole through applied inflections
6−data Word
7−
8− -- | A word that may be transformed by inflection
9− = Word String
10−
11− -- | A word that may not be transformed by inflections
12− | Acronym String
13−
14− deriving (Show, Eq)
15−
16−mapWord :: (String -> String) -> Word -> Word
17−mapWord f (Word s) = Word $ f s
18−mapWord f (Acronym s) = Acronym $ f s
Text/Inflections/Titleize.hs+24−15
@@ -1,21 +1,30 @@
1−module Text.Inflections.Titleize (titleize) where
2−
3−import Text.Inflections.Parse.Types (Word(..))
1+-- |
2+-- Module : Text.Inflections.Titleize
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Conversion to titleized phrases.
411
5−import Data.Char (toUpper)
12+module Text.Inflections.Titleize
13+ ( titleize )
14+where
615
7−import Prelude (String, unwords, map, ($))
16+import Data.Text (Text)
17+import Text.Inflections.Types
18+import qualified Data.Text as T
819
9−-- | Capitalizes all the Words in the input 'Data.List'.
20+-- | Capitalize all the 'SomeWord' words in the input list.
1021--
11−-- >>> titleize [ Word "foo", Acronym "bar", Word "bazz" ]
22+-- >>> foo <- SomeWord <$> mkWord "foo"
23+-- >>> bar <- SomeWord <$> mkAcronym "bar"
24+-- >>> bazz <- SomeWord <$> mkWord "bazz"
25+-- >>> titleize [foo,bar,bazz]
1226-- "Foo bar Bazz"
1327titleize
14− :: [Word] -- ^ List of Words, first of which will be capitalized
15− -> String -- ^ The titleized String
16−titleize s = unwords $ map upperCaseWord s
17−
18−upperCaseWord :: Word -> String
19−upperCaseWord (Word (c:cs)) = toUpper c : cs
20−upperCaseWord (Word []) = []
21−upperCaseWord (Acronym s) = s -- Acronyms are left intact
28+ :: [SomeWord] -- ^ List of words, of which all 'SomeWord' words will be capitalized and all acronyms will be left as is
29+ -> Text -- ^ The titleized 'Text'
30+titleize = T.unwords . fmap (unSomeWord T.toTitle)
Text/Inflections/Transliterate.hs+46−22
@@ -1,30 +1,54 @@
1+-- |
2+-- Module : Text.Inflections.Transliterate
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Support for transliteration.
11+
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE OverloadedStrings #-}
14+
115module Text.Inflections.Transliterate
216 ( transliterate
3− , transliterateCustom
4− )
17+ , transliterateCustom )
518where
619
7−import Text.Inflections.Parameterize ( Transliterations )
8−import Text.Inflections.Data (defaultMap)
9−
1020import Data.Char (isAscii)
11−import Data.Maybe(fromMaybe)
21+import Data.Text (Text)
22+import Text.Inflections.Data
23+import qualified Data.HashMap.Strict as M
24+import qualified Data.Text as T
1225
13−import qualified Data.Map as Map
26+#if !MIN_VERSION_base(4,8,0)
27+import Control.Applicative
28+#endif
1429
15−-- |Returns a String after default approximations for changing Unicode characters
16−-- to a valid ASCII range are applied. If you want to supplement the default
17−-- approximations with your own, you should use the transliterateCustom
18−-- function instead of transliterate.
19−transliterate :: String -> String
20−transliterate = transliterateCustom "?" defaultMap
30+-- | Returns a 'Text' after default approximations for changing Unicode
31+-- characters to a valid ASCII range are applied. If you want to supplement
32+-- the default approximations with your own, you should use the
33+-- 'transliterateCustom' function instead of 'transliterate'.
34+transliterate :: Text -> Text
35+transliterate = transliterateCustom "?" defaultTransliterations
2136
22−-- |Returns a String after default approximations for changing Unicode characters
23−-- to a valid ASCII range are applied.
24−transliterateCustom :: String -> Transliterations -> String -> String
25−transliterateCustom replacement ts = concatMap lookupCharTransliteration
26− where lookupCharTransliteration c =
27− if isAscii c then -- Don't bother looking up Chars in ASCII range
28− [c]
29− else
30− fromMaybe replacement (Map.lookup c ts)
37+-- | Returns a 'Text' after default approximations for changing Unicode
38+-- characters to a valid ASCII range are applied.
39+transliterateCustom
40+ :: String -- ^ The default replacement
41+ -> Transliterations -- ^ The table of transliterations
42+ -> Text -- ^ The input
43+ -> Text -- ^ The output
44+transliterateCustom replacement m txt = T.unfoldr f ("", txt)
45+ where
46+ f ("", t) = uncurry g <$> T.uncons t
47+ f (x:xs, t) = Just (x, (xs, t))
48+ g x xs =
49+ if isAscii x
50+ then (x, ("", xs))
51+ else
52+ case M.lookupDefault replacement x m of
53+ "" -> ('?', ("",xs))
54+ (y:ys) -> (y, (ys,xs))
Text/Inflections/Types.hsnew file+144
@@ -0,0 +1,144 @@
1+-- |
2+-- Module : Text.Inflections.Types
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Types used in the library. Usually you don't need to import this module
11+-- and "Text.Inflections" should be enough.
12+
13+{-# LANGUAGE CPP #-}
14+{-# LANGUAGE DataKinds #-}
15+{-# LANGUAGE DeriveDataTypeable #-}
16+{-# LANGUAGE DeriveGeneric #-}
17+{-# LANGUAGE FlexibleContexts #-}
18+{-# LANGUAGE FlexibleInstances #-}
19+{-# LANGUAGE GADTs #-}
20+{-# LANGUAGE KindSignatures #-}
21+
22+module Text.Inflections.Types
23+ ( Word
24+ , WordType (..)
25+ , mkWord
26+ , mkAcronym
27+ , unWord
28+ , SomeWord (..)
29+ , unSomeWord
30+ , InflectionException (..) )
31+where
32+
33+import Control.Monad.Catch
34+import Data.Char (isAlphaNum)
35+import Data.Data (Data)
36+import Data.Void (Void)
37+import Data.Text (Text)
38+import Data.Typeable (Typeable)
39+import GHC.Generics (Generic)
40+import Text.Megaparsec
41+import qualified Data.Text as T
42+
43+#if MIN_VERSION_base(4,8,0)
44+import Prelude hiding (Word)
45+#endif
46+
47+-- | Create a word from given 'Text'. The input should consist of only
48+-- alpha-numeric characters (no white spaces or punctuation)
49+-- 'InflectionInvalidWord' will be thrown.
50+--
51+-- /since 0.3.0.0/
52+
53+mkWord :: MonadThrow m => Text -> m (Word 'Normal)
54+mkWord txt =
55+ if T.all isAlphaNum txt
56+ then return (Word txt)
57+ else throwM (InflectionInvalidWord txt)
58+
59+-- | Create an acronym from given 'Text'. The input should consist of only
60+-- alpha-numeric characters 'InflectionInvalidAcronym' will be thrown.
61+-- Acronym is different from normal word by that it may not be transformed
62+-- by inflections (also see 'unSomeWord').
63+--
64+-- /since 0.3.0.0/
65+
66+mkAcronym :: MonadThrow m => Text -> m (Word 'Acronym)
67+mkAcronym txt =
68+ if T.all isAlphaNum txt
69+ then return (Word txt)
70+ else throwM (InflectionInvalidAcronym txt)
71+
72+-- | A 'Text' value that should be kept whole through applied inflections.
73+
74+data Word (t :: WordType) = Word Text
75+ deriving (Eq, Ord)
76+
77+instance Show (Word 'Normal) where
78+ show (Word x) = "Word " ++ show x
79+
80+instance Show (Word 'Acronym) where
81+ show (Word x) = "Acronym " ++ show x
82+
83+-- | A type-level tag for words.
84+--
85+-- /since 0.3.0.0/
86+
87+data WordType = Normal | Acronym
88+
89+-- | Get a 'Text' value from 'Word'.
90+--
91+-- /since 0.3.0.0/
92+
93+unWord :: Word t -> Text
94+unWord (Word s) = s
95+
96+-- | An existential wrapper that allows to keep words and acronyms in single
97+-- list for example. The only thing that receiver of 'SomeWord' can do is to
98+-- apply 'unWord' on it, of course. This is faciliated by 'unSomeWord'.
99+--
100+-- /since 0.3.0.0/
101+
102+data SomeWord where
103+ SomeWord :: (Transformable (Word t), Show (Word t)) => Word t -> SomeWord
104+ -- NOTE The constraint is only needed because GHC is not smart enough
105+ -- (yet?) to figure out that t cannot be anything other than Normal and
106+ -- Acronym and thus all cases are already covered by the instances
107+ -- provided below.
108+
109+instance Show SomeWord where
110+ show (SomeWord w) = show w
111+
112+instance Eq SomeWord where
113+ x == y = unSomeWord id x == unSomeWord id y
114+
115+-- | Extract 'Text' from 'SomeWord' and apply given function only if the
116+-- word inside wasn't an acronym.
117+--
118+-- /since 0.3.0.0/
119+
120+unSomeWord :: (Text -> Text) -> SomeWord -> Text
121+unSomeWord f (SomeWord w) = transform f w
122+
123+-- | Non public stuff.
124+
125+class Transformable a where
126+ transform :: (Text -> Text) -> a -> Text
127+
128+instance Transformable (Word 'Normal) where
129+ transform f = f . unWord
130+
131+instance Transformable (Word 'Acronym) where
132+ transform _ = unWord
133+
134+-- | The exceptions that is thrown when parsing of input fails.
135+--
136+-- /since 0.3.0.0/
137+
138+data InflectionException
139+ = InflectionParsingFailed (ParseErrorBundle Text Void)
140+ | InflectionInvalidWord Text
141+ | InflectionInvalidAcronym Text
142+ deriving (Eq, Show, Typeable, Data, Generic)
143+
144+instance Exception InflectionException
Text/Inflections/Underscore.hs+25−16
@@ -1,23 +1,32 @@
1−module Text.Inflections.Underscore ( underscore ) where
1+-- |
2+-- Module : Text.Inflections.Underscore
3+-- Copyright : © 2016 Justin Leitgeb
4+-- License : MIT
5+--
6+-- Maintainer : Justin Leitgeb <justin@stackbuilders.com>
7+-- Stability : experimental
8+-- Portability : portable
9+--
10+-- Conversion to phrases separated by underscores.
211
3−import Text.Inflections.Parse.Types (Word(..))
12+{-# LANGUAGE OverloadedStrings #-}
413
5−import Data.Char (toLower)
6−import Data.List (intercalate)
14+module Text.Inflections.Underscore
15+ ( underscore )
16+where
717
8−import Prelude (String, (.), map)
18+import Data.Text (Text)
19+import Text.Inflections.Types
20+import qualified Data.Text as T
921
10−-- |Turns a CamelCase string into an underscore_separated String.
22+-- | Separate given words by underscores.
1123--
12−-- >>> underscore [ Word "foo", Acronym "bar", Word "bazz" ]
24+-- >>> foo <- SomeWord <$> mkWord "foo"
25+-- >>> bar <- SomeWord <$> mkAcronym "bar"
26+-- >>> bazz <- SomeWord <$> mkWord "bazz"
27+-- >>> underscore [foo,bar,bazz]
1328-- "foo_bar_bazz"
14−
1529underscore
16− :: [Word] -- ^ Input Words to separate with underscores
17− -> String -- ^ The underscored String
18−underscore = intercalate "_" . map toDowncasedString
19−
20−
21−toDowncasedString :: Word -> String
22−toDowncasedString (Acronym s) = map toLower s
23−toDowncasedString (Word s) = map toLower s
30+ :: [SomeWord] -- ^ Input words to separate with underscores
31+ -> Text -- ^ The underscored String
32+underscore = T.intercalate "_" . fmap (unSomeWord T.toLower)
docs/CODE_OF_CONDUCT.mdnew file+35
@@ -0,0 +1,35 @@
1+# Code of conduct
2+
3+## Purpose
4+The primary goal of this Code of Conduct is to enable an open and welcoming environment. We pledge to making participation in our project a harassment-free experience for everyone, regardless of gender, sexual
5+orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
6+
7+## General recommendations
8+Examples of behavior that contributes to creating a positive environment include:
9+
10+- Using welcoming and inclusive language
11+- Being respectful of differing viewpoints and experiences
12+- Gracefully accepting constructive criticism
13+- Focusing on what is best for the community
14+- Showing empathy towards other community members
15+
16+Examples of unacceptable behavior by participants include:
17+
18+- The use of sexualized language or imagery and unwelcome sexual attention or advances
19+- Trolling, insulting/derogatory comments, and personal or political attacks
20+- Public or private harassment
21+- Publishing others' private information, such as a physical or electronic address, without explicit permission
22+- Other conduct which could reasonably be considered inappropriate in a professional setting
23+
24+## Maintainer responsibilities
25+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
26+
27+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
28+
29+## Scope
30+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
31+
32+## Enforcement
33+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [community@stackbuilders.com](mailto:community@stackbuilders.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
34+
35+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
docs/CONTRIBUTING.mdnew file+13
@@ -0,0 +1,13 @@
1+
2+Thank you for your interest in contributing to this Stack Builders' library. To contribute, please take our [Code of Conduct](CODE_OF_CONDUCT.md) into account, along with the following recommendations:
3+
4+- When submitting contributions to this repository, please make sure to discuss with the maintainer(s) the change you want to make. You can do this through an issue, or by sending an email to [community@stackbuilders.com](mailto:community@stackbuilders.com)
5+
6+- Once the change has been discussed with the maintainer(s), feel free to open a Pull Request. Please include a link to the issue you're trying to solve, or a quick summary of the discussed changes.
7+
8+- If adding any new features that you think should be considered in the README file, please add that information in your Pull Request.
9+
10+- Once you get an approval from any of the maintainers, please merge your Pull Request. Keep in mind that some of our Stack Builders repositories use CI/CD pipelines, so you will need to pass all of the required checks before merging.
11+
12+## Getting help
13+Contact any of our current maintainers, or send us an email at [community@stackbuilders.com](mailto:community@stackbuilders.com) for more information. Thank you for contributing!
inflections.cabal+65−42
@@ -1,5 +1,5 @@
11name: inflections
2−version: 0.2.0.1
2+version: 0.4.0.6
33synopsis: Inflections library for Haskell
44description:
55 Inflections provides methods for singularization, pluralization,
@@ -7,64 +7,87 @@ description:
77
88license: MIT
99license-file: LICENSE
10−author: Justin Leitgeb
10+author: Justin Leitgeb <justin@stackbuilders.com>
1111homepage: https://github.com/stackbuilders/inflections-hs
12−maintainer: justin@stackbuilders.com
13−copyright: 2014 Justin Leitgeb
12+bug-reports: https://github.com/stackbuilders/inflections-hs/issues
13+maintainer: Justin Leitgeb <justin@stackbuilders.com>
14+copyright: 2014–2016 Justin Leitgeb
1415category: Text
1516build-type: Simple
1617cabal-version: >=1.10
18+extra-source-files: CHANGELOG.md
19+ , README.md
1720
1821source-repository head
1922 type: git
2023 location: https://github.com/stackbuilders/inflections-hs.git
2124
22−library
23− exposed-modules: Text.Inflections
24− , Text.Inflections.Parse.Types
25+flag dev
26+ description: Turn on development settings.
27+ manual: True
28+ default: False
2529
26− other-modules: Text.Inflections.Data
27− , Text.Inflections.Parameterize
28− , Text.Inflections.Humanize
29− , Text.Inflections.Underscore
30− , Text.Inflections.Camelize
31− , Text.Inflections.Dasherize
32− , Text.Inflections.Ordinal
33− , Text.Inflections.Titleize
34− , Text.Inflections.Transliterate
30+library
31+ exposed-modules: Text.Inflections
3532
36− , Text.Inflections.Parse.Acronym
37− , Text.Inflections.Parse.Parameterizable
33+ other-modules: Text.Inflections.Data
34+ , Text.Inflections.Camelize
35+ , Text.Inflections.Dasherize
36+ , Text.Inflections.Humanize
37+ , Text.Inflections.Ordinal
38+ , Text.Inflections.Parameterize
39+ , Text.Inflections.Parse.CamelCase
40+ , Text.Inflections.Parse.SnakeCase
41+ , Text.Inflections.Titleize
42+ , Text.Inflections.Transliterate
43+ , Text.Inflections.Types
44+ , Text.Inflections.Underscore
3845
39− , Text.Inflections.Parse.SnakeCase
40− , Text.Inflections.Parse.CamelCase
46+ if flag(dev)
47+ ghc-options: -Wall -Werror
48+ else
49+ ghc-options: -O2 -Wall
50+ build-depends: base >= 4.6 && < 5.0
51+ , exceptions
52+ , megaparsec
53+ , text
54+ , unordered-containers
55+ if !impl(ghc >= 7.10)
56+ build-depends: void == 0.7.*
4157
42− ghc-options: -Wall
43− build-depends: base >=4.2 && <4.10, parsec, containers
4458 default-language: Haskell2010
4559
4660test-suite test
4761 type: exitcode-stdio-1.0
4862 hs-source-dirs: test
4963 main-is: Spec.hs
50− build-depends:
51− inflections
52− , base >=4.2 && <4.10
53− , test-framework
54− , HUnit
55− , hspec
56− , hspec-discover
57− , QuickCheck
58− , test-framework-hunit
59− , test-framework-quickcheck2
60− , parsec
61− , containers
62− ghc-options: -Wall
64+ build-depends: inflections
65+ , QuickCheck
66+ , base >= 4.6 && < 5.0
67+ , containers
68+ , hspec
69+ , hspec-megaparsec
70+ , megaparsec
71+ , text
72+ if !impl(ghc >= 7.10)
73+ build-depends: void == 0.7.*
74+
75+ if !impl(ghc >= 8.0)
76+ build-depends: semigroups == 0.18.*
77+
78+ if flag(dev)
79+ ghc-options: -Wall -Werror
80+ else
81+ ghc-options: -O2 -Wall
6382 default-language: Haskell2010
64− other-modules:
65− Text.Inflections.HumanizeSpec
66− , Text.Inflections.OrdinalSpec
67− , Text.Inflections.PropertiesSpec
68− , Text.Inflections.TitleizeSpec
69− , Text.Inflections.UnderscoreSpec
70− , Text.InflectionsSpec
83+ other-modules: Text.Inflections.DasherizeSpec
84+ , Text.Inflections.HumanizeSpec
85+ , Text.Inflections.OrdinalSpec
86+ , Text.Inflections.ParametrizeSpec
87+ , Text.Inflections.Parse.CamelCaseSpec
88+ , Text.Inflections.Parse.SnakeCaseSpec
89+ , Text.Inflections.TitleizeSpec
90+ , Text.Inflections.TransliterateSpec
91+ , Text.Inflections.TypesSpec
92+ , Text.Inflections.UnderscoreSpec
93+ , Text.InflectionsSpec
stack.yaml+1−1
@@ -1 +1 @@
1−resolver: lts-4.0
1+resolver: lts-21.3
stack.yaml.locknew file+12
@@ -0,0 +1,12 @@
1+# This file was autogenerated by Stack.
2+# You should not edit this file by hand.
3+# For more information, please see the documentation at:
4+# https://docs.haskellstack.org/en/stable/lock_files
5+
6+packages: []
7+snapshots:
8+- completed:
9+ sha256: 97710f56bf093fca0ee5d8dbe19d96b654c752e405b66795b4baedd84a794c60
10+ size: 640010
11+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/3.yaml
12+ original: lts-21.3
test/Text/Inflections/DasherizeSpec.hsnew file+21
@@ -0,0 +1,21 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Text.Inflections.DasherizeSpec
5+ ( spec )
6+where
7+
8+import Test.Hspec
9+
10+import Text.Inflections
11+
12+#if !MIN_VERSION_base(4,8,0)
13+import Control.Applicative
14+#endif
15+
16+spec :: Spec
17+spec = describe "dasherize" $
18+ it "dasherizes a collection of words sentence" $ do
19+ foo <- SomeWord <$> mkWord "foo"
20+ bar <- SomeWord <$> mkWord "bar"
21+ dasherize [foo,bar] `shouldBe` "foo-bar"
test/Text/Inflections/HumanizeSpec.hs+47−14
@@ -1,21 +1,54 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
14module Text.Inflections.HumanizeSpec (spec) where
25
36import Test.Hspec
47
5−import Text.Inflections (humanize)
6−import Text.Inflections.Parse.Types (Word(..))
8+import Text.Inflections
9+
10+#if !MIN_VERSION_base(4,8,0)
11+import Control.Applicative
12+#endif
713
814spec :: Spec
915spec = do
10− snakeCaseHumanize
11− capitalizeFirstWord
12−
13−snakeCaseHumanize :: Spec
14−snakeCaseHumanize =
15− it "converts snake case to a human-readable string" $
16− humanize [Word "employee", Word "salary"] `shouldBe` "Employee salary"
17−
18−capitalizeFirstWord :: Spec
19−capitalizeFirstWord =
20− it "capitalizes the first word of a sentence" $
21− humanize [Word "underground"] `shouldBe` "Underground"
16+ describe "humanize" $ do
17+ it "converts snake case to a human-readable string" $ do
18+ employee <- SomeWord <$> mkWord "employee"
19+ salary <- SomeWord <$> mkWord "salary"
20+ humanize [employee,salary] `shouldBe` "Employee salary"
21+ it "turns underscores into spaces" $ do
22+ employee <- SomeWord <$> mkWord "employee"
23+ has <- SomeWord <$> mkWord "has"
24+ salary <- SomeWord <$> mkWord "salary"
25+ humanize [employee, has, salary] `shouldBe` "Employee has salary"
26+ it "capitalizes the first word of a sentence" $ do
27+ underground <- SomeWord <$> mkWord "underground"
28+ humanize [underground] `shouldBe` "Underground"
29+ describe "humanizeCustom False" $ do
30+ it "converts snake case to a human-readable string" $ do
31+ employee <- SomeWord <$> mkWord "employee"
32+ salary <- SomeWord <$> mkWord "salary"
33+ humanizeCustom False [employee,salary] `shouldBe` "employee salary"
34+ it "turns underscores into spaces" $ do
35+ employee <- SomeWord <$> mkWord "employee"
36+ has <- SomeWord <$> mkWord "has"
37+ salary <- SomeWord <$> mkWord "salary"
38+ humanizeCustom False [employee, has, salary] `shouldBe` "employee has salary"
39+ it "lower-cases the first word of a sentence" $ do
40+ underground <- SomeWord <$> mkWord "underground"
41+ humanizeCustom False [underground] `shouldBe` "underground"
42+ describe "humanizeCustom True" $ do
43+ it "converts snake case to a human-readable string" $ do
44+ employee <- SomeWord <$> mkWord "employee"
45+ salary <- SomeWord <$> mkWord "salary"
46+ humanizeCustom True [employee,salary] `shouldBe` "Employee salary"
47+ it "turns underscores into spaces" $ do
48+ employee <- SomeWord <$> mkWord "employee"
49+ has <- SomeWord <$> mkWord "has"
50+ salary <- SomeWord <$> mkWord "salary"
51+ humanizeCustom True [employee, has, salary] `shouldBe` "Employee has salary"
52+ it "capitalizes the first word of a sentence" $ do
53+ underground <- SomeWord <$> mkWord "underground"
54+ humanizeCustom True [underground] `shouldBe` "Underground"
test/Text/Inflections/OrdinalSpec.hs+31−51
@@ -1,62 +1,42 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
14module Text.Inflections.OrdinalSpec (spec) where
25
3−import Control.Applicative ((<$>))
6+import Data.Text (Text)
47import Test.Hspec
58import Test.QuickCheck.Property
9+import qualified Data.Text as T
610
711import Text.Inflections (ordinal, ordinalize)
812
13+#if !MIN_VERSION_base(4,8,0)
14+import Control.Applicative
15+#endif
16+
917spec :: Spec
1018spec = do
1119 describe "ordinal" $ do
12− one
13− two
14− thousands
15− negatives
16− ordinalReturnsNotEmpty
20+ it "returns the ordinal for 1" $
21+ ordinal (1 :: Integer) `shouldBe` "st"
22+ it "returns the ordinal for 2" $
23+ ordinal (2 :: Integer) `shouldBe` "nd"
24+ it "returns the ordinal for 1002" $
25+ ordinal (1002 :: Integer) `shouldBe` "nd"
26+ it "returns the ordinal for 1003" $
27+ ordinal (1003 :: Integer) `shouldBe` "rd"
28+ it "returns the ordinal for -11" $
29+ ordinal (-11 :: Integer) `shouldBe` "th"
30+ it "returns the ordinal for -1021" $
31+ ordinal (-1021 :: Integer) `shouldBe` "st"
32+ it "never returns empty output" $ property $
33+ property <$> not . T.null . (ordinal :: Integer -> Text)
1734 describe "ordinalize" $ do
18− fullOrdinals
19− ordinalizeContainsTheSameNumber
20−
21−one :: Spec
22−one =
23− it "returns the ordinal for 1" $
24− ordinal 1 `shouldBe` "st"
25−
26−two :: Spec
27−two =
28− it "returns the ordinal for 2" $
29− ordinal 2 `shouldBe` "nd"
30−
31−thousands :: Spec
32−thousands = do
33− it "returns the ordinal for 1002" $
34− ordinal 1002 `shouldBe` "nd"
35− it "returns the ordinal for 1003" $
36− ordinal 1003 `shouldBe` "rd"
37−
38−negatives :: Spec
39−negatives = do
40− it "returns the ordinal for -11" $
41− ordinal (-11) `shouldBe` "th"
42− it "returns the ordinal for -1021" $
43− ordinal (-1021) `shouldBe` "st"
44−
45−fullOrdinals :: Spec
46−fullOrdinals = do
47− it "returns the full ordinal for 1" $
48− ordinalize 1 `shouldBe` "1st"
49− it "returns the full ordinal for -1021" $
50− ordinalize (-1021) `shouldBe` "-1021st"
51−
52−ordinalReturnsNotEmpty :: Spec
53−ordinalReturnsNotEmpty =
54− it "never returns empty" $ property $
55− property <$> not . null . ordinal
56−
57−ordinalizeContainsTheSameNumber :: Spec
58−ordinalizeContainsTheSameNumber =
59− it "always returns the number as part of the result" $ property ordinalizeSamePrefix
60−
61−ordinalizeSamePrefix :: Integer -> Bool
62−ordinalizeSamePrefix n = show n == take (length $ show n) (ordinalize n)
35+ it "returns the full ordinal for 1" $
36+ ordinalize (1 :: Integer) `shouldBe` "1st"
37+ it "returns the full ordinal for -1021" $
38+ ordinalize (-1021 :: Integer) `shouldBe` "-1021st"
39+ it "always returns the number as prefix of the result" $
40+ property $ \n ->
41+ let s = show (n :: Integer)
42+ in T.pack s == T.take (length s) (ordinalize n)
test/Text/Inflections/ParametrizeSpec.hsnew file+39
@@ -0,0 +1,39 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Text.Inflections.ParametrizeSpec (spec) where
4+
5+import Data.Char (toLower)
6+import Data.List (group)
7+import Test.Hspec
8+import Test.QuickCheck
9+import Text.Inflections
10+import qualified Data.Text as T
11+
12+spec :: Spec
13+spec =
14+ describe "parameterize" $ do
15+ it "returns only valid characters" $ property $ \sf ->
16+ T.all (`elem` (alphaNumerics ++ "-_")) $ parameterize (T.pack sf)
17+ it "never returns a string beginning with a separator" $ property $ \s ->
18+ let parameterized = parameterize (T.pack s)
19+ in (not . T.null) parameterized ==> T.head parameterized /= '-'
20+ it "never returns a string ending with a separator" $ property $ \s ->
21+ let parameterized = parameterize (T.pack s) in
22+ (not . T.null) parameterized ==> T.last parameterized /= '-'
23+ it "returns every alphanumeric character from the input" $ property $ \s ->
24+ let parameterized = parameterize (T.pack s)
25+ in all (\c -> c `notElem` alphaNumerics ||
26+ c `elem` (alphaNumerics ++ "-") &&
27+ c `elem` T.unpack parameterized) $ map toLower s
28+ it "never returns a string with a sequence of dashes" $ property $ \s ->
29+ let parameterized = parameterize (T.pack s)
30+ in longestSequenceOf '-' (T.unpack parameterized) <= 1
31+
32+longestSequenceOf :: Char -> String -> Int
33+longestSequenceOf _ [] = 0
34+longestSequenceOf c s =
35+ if null subseqLengths then 0 else maximum subseqLengths
36+ where subseqLengths = (map length . filter (\str -> head str == c) . group) s
37+
38+alphaNumerics :: String
39+alphaNumerics = ['a'..'z'] ++ ['0'..'9']
test/Text/Inflections/Parse/CamelCaseSpec.hsnew file+47
@@ -0,0 +1,47 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Text.Inflections.Parse.CamelCaseSpec
4+ ( spec )
5+where
6+
7+import Test.Hspec
8+import Test.Hspec.Megaparsec
9+import Text.Inflections
10+
11+spec :: Spec
12+spec =
13+ describe "parseCamelCase" $ do
14+ context "when given no acronyms" $ do
15+ context "when first word is capitalized" $
16+ it "parses CamelCase correctly" $ do
17+ r <- mapM (fmap SomeWord . mkWord) ["One","Two","Three"]
18+ parseCamelCase [] "OneTwoThree" `shouldParse` r
19+ context "when first word is not capitalized" $
20+ it "parses camelCase correctly" $ do
21+ r <- mapM (fmap SomeWord . mkWord) ["one","Two","Three"]
22+ parseCamelCase [] "oneTwoThree" `shouldParse` r
23+ context "when there are digits in the words" $
24+ it "parses CamelCase correctly" $ do
25+ r <- mapM (fmap SomeWord . mkWord) ["one1two","Three3"]
26+ parseCamelCase [] "one1twoThree3" `shouldParse` r
27+ context "when given some acronyms" $ do
28+ context "when first word is capitalized" $
29+ it "parses CamelCase correctly" $ do
30+ a <- mkAcronym "BOO"
31+ r <- mapM (fmap SomeWord . mkWord) ["One","BOO","One"]
32+ parseCamelCase [a] "OneBOOOne" `shouldParse` r
33+ context "when first word is not capitalized" $
34+ it "parses camelCase correctly" $ do
35+ a <- mkAcronym "BOO"
36+ r <- mapM (fmap SomeWord . mkWord) ["one","Two","BOO"]
37+ parseCamelCase [a] "oneTwoBOO" `shouldParse` r
38+ context "when there are digits in the words" $
39+ it "parses CamelCase correctly" $ do
40+ a <- mkAcronym "BOO"
41+ r <- mapM (fmap SomeWord . mkWord) ["one1two","Three3"]
42+ parseCamelCase [a] "one1twoThree3" `shouldParse` r
43+ context "when a word has a suffix coinciding with a acronym" $
44+ it "is still parsed correctly as a normala word" $ do
45+ a <- mkAcronym "boo"
46+ r <- mapM (fmap SomeWord . mkWord) ["fooboo","Bar"]
47+ parseCamelCase [a] "foobooBar" `shouldParse` r
test/Text/Inflections/Parse/SnakeCaseSpec.hsnew file+35
@@ -0,0 +1,35 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Text.Inflections.Parse.SnakeCaseSpec
4+ ( spec )
5+where
6+
7+import Test.Hspec
8+import Test.Hspec.Megaparsec
9+import Text.Inflections
10+
11+spec :: Spec
12+spec =
13+ describe "parseSnakeCase" $ do
14+ context "when given no acronyms" $ do
15+ it "parses snake_case correctly" $ do
16+ r <- mapM (fmap SomeWord . mkWord) ["OneTwo","Three"]
17+ parseSnakeCase [] "OneTwo_Three" `shouldParse` r
18+ it "handles digits in the words correctly" $ do
19+ r <- mapM (fmap SomeWord . mkWord) ["one4a","two00"]
20+ parseSnakeCase [] "one4a_two00" `shouldParse` r
21+ context "when given some acronyms" $ do
22+ it "parses snake_case correctly" $ do
23+ a <- mkAcronym "BOO"
24+ r <- mapM (fmap SomeWord . mkWord) ["BOO","one","one"]
25+ parseSnakeCase [a] "BOO_one_one" `shouldParse` r
26+ context "when acronym happens to be a prefix of other word" $
27+ it "parses the word correctly" $ do
28+ a <- mkAcronym "BOO"
29+ r <- mapM (fmap SomeWord . mkWord) ["one","BOOtwo"]
30+ parseSnakeCase [a] "one_BOOtwo" `shouldParse` r
31+ context "when acronym happens to be a suffix of other word" $
32+ it "parses the word correctly" $ do
33+ a <- mkAcronym "BOO"
34+ r <- mapM (fmap SomeWord . mkWord) ["oneBOO","two"]
35+ parseSnakeCase [a] "oneBOO_two" `shouldParse` r
test/Text/Inflections/PropertiesSpec.hsdeleted−90
@@ -1,90 +0,0 @@
1−module Text.Inflections.PropertiesSpec (spec) where
2−
3−import Data.Char (toLower)
4−import Data.List (group)
5−import Test.Hspec
6−import Test.QuickCheck
7−
8−import Text.Inflections
9−import Text.Inflections.Parse.Types (Word(..))
10−
11−spec :: Spec
12−spec = do
13− describe "dasherize"
14− dasherizeSpacedSentence
15− describe "transliterate" $ do
16− withSubstitutions
17− withoutSubstitutions
18− missingSubstitutions
19− describe "parameterize" $ do
20− onlyValidCharacters
21− notBeginWithSeparator
22− notEndWithSeparator
23− noMissingAlphanumerics
24− noMoreThanOneHyphen
25−
26−withoutSubstitutions :: Spec
27−withoutSubstitutions =
28− it "transliterates without subsitutions" $
29− transliterate "this is a test" `shouldBe` "this is a test"
30−
31−withSubstitutions :: Spec
32−withSubstitutions =
33− it "transliterates with substitution" $
34− transliterate "Feliz año nuevo" `shouldBe` "Feliz ano nuevo"
35−
36−missingSubstitutions :: Spec
37−missingSubstitutions =
38− it "transliterates with missing substitutions" $
39− transliterate "Have a ❤ ñ!" `shouldBe` "Have a ? n!"
40−
41−dasherizeSpacedSentence :: Spec
42−dasherizeSpacedSentence =
43− it "dasherizes a space separated sentence" $
44− dasherize [Word "foo", Word "bar"] `shouldBe` "foo-bar"
45−
46−onlyValidCharacters :: Spec
47−onlyValidCharacters =
48− it "returns only valid characters" (property onlyValidCharactersPredicate)
49− where onlyValidCharactersPredicate sf = all (`elem` (alphaNumerics ++ "-_")) $ parameterize sf
50−
51−notBeginWithSeparator :: Spec
52−notBeginWithSeparator =
53− it "never returns a string beginning ending with a separator" (property notBeginWithSeparatorPredicate)
54− where
55− notBeginWithSeparatorPredicate s =
56− let parameterized = parameterize s in
57− (not . null) parameterized ==> head parameterized /= '-'
58−
59−notEndWithSeparator :: Spec
60−notEndWithSeparator =
61− it "never returns a string beginning with a separator" (property notBeginWithSeparatorPredicate)
62− where
63− notBeginWithSeparatorPredicate s =
64− let parameterized = parameterize s in
65− (not . null) parameterized ==> last parameterized /= '-'
66−
67−noMissingAlphanumerics :: Spec
68−noMissingAlphanumerics =
69− it "returns every alphanumeric character from the input" (property noMissingAlphanumericsPredicate)
70− where noMissingAlphanumericsPredicate s =
71− let parameterized = parameterize s in
72− all (\c -> c `notElem` alphaNumerics ||
73− c `elem` (alphaNumerics ++ "-") &&
74− c `elem` parameterized) $ map toLower s
75−
76−noMoreThanOneHyphen :: Spec
77−noMoreThanOneHyphen =
78− it "never returns a string with a sequence of dashes" (property noMoreThanOneHyphenPredicate)
79− where noMoreThanOneHyphenPredicate s =
80− let parameterized = parameterize s in longestSequenceOf '-' parameterized <= 1
81−
82−longestSequenceOf :: Char -> String -> Int
83−longestSequenceOf _ [] = 0
84−longestSequenceOf c s =
85− if null subseqLengths then 0 else maximum subseqLengths
86−
87− where subseqLengths = (map length . filter (\str -> head str == c) . group) s
88−
89−alphaNumerics :: String
90−alphaNumerics = ['a'..'z'] ++ ['0'..'9']
test/Text/Inflections/TitleizeSpec.hs+15−15
@@ -1,21 +1,21 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
14module Text.Inflections.TitleizeSpec (spec) where
25
36import Test.Hspec
7+import Text.Inflections
48
5−import Text.Inflections (titleize)
6−import Text.Inflections.Parse.Types (Word(..))
9+#if !MIN_VERSION_base(4,8,0)
10+import Control.Applicative
11+#endif
712
813spec :: Spec
9−spec = do
10− twoWordsToTitleCase
11− oneWordToTitleCase
12−
13−twoWordsToTitleCase :: Spec
14−twoWordsToTitleCase =
15− it "converts two words to title case" $
16− titleize [Word "Employee", Word "Salary"] `shouldBe` "Employee Salary"
17−
18−oneWordToTitleCase :: Spec
19−oneWordToTitleCase =
20− it "converts one word to title case" $
21− titleize [Word "underground"] `shouldBe` "Underground"
14+spec = describe "titleize" $ do
15+ it "converts two words to title case" $ do
16+ employee <- SomeWord <$> mkWord "Employee"
17+ salary <- SomeWord <$> mkWord "Salary"
18+ titleize [employee,salary] `shouldBe` "Employee Salary"
19+ it "converts one word to title case" $ do
20+ underground <- SomeWord <$> mkWord "underground"
21+ titleize [underground] `shouldBe` "Underground"
test/Text/Inflections/TransliterateSpec.hsnew file+29
@@ -0,0 +1,29 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
3+module Text.Inflections.TransliterateSpec
4+ ( spec )
5+where
6+
7+import Test.Hspec
8+import Text.Inflections (transliterate)
9+
10+spec :: Spec
11+spec = describe "transliterate" $ do
12+ withSubstitutions
13+ withoutSubstitutions
14+ missingSubstitutions
15+
16+withoutSubstitutions :: Spec
17+withoutSubstitutions =
18+ it "transliterates without subsitutions" $
19+ transliterate "this is a test" `shouldBe` "this is a test"
20+
21+withSubstitutions :: Spec
22+withSubstitutions =
23+ it "transliterates with substitution" $
24+ transliterate "Feliz año nuevo" `shouldBe` "Feliz ano nuevo"
25+
26+missingSubstitutions :: Spec
27+missingSubstitutions =
28+ it "transliterates with missing substitutions" $
29+ transliterate "Have a ❤ ñ!" `shouldBe` "Have a ? n!"
test/Text/Inflections/TypesSpec.hsnew file+51
@@ -0,0 +1,51 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
4+module Text.Inflections.TypesSpec
5+ ( spec )
6+where
7+
8+import Test.Hspec
9+import Text.Inflections
10+import qualified Data.Text as T
11+
12+#if !MIN_VERSION_base(4,8,0)
13+import Control.Applicative
14+#endif
15+
16+spec :: Spec
17+spec = do
18+ describe "mkWord" $ do
19+ context "when provided a correct Text value" $
20+ it "creates a normal word" $ do
21+ x <- mkWord "foo"
22+ unWord x `shouldBe` "foo"
23+ context "when provided an incorrect Text value" $
24+ it "throws the correct exception" $ do
25+ mkWord "foo bar" `shouldThrow` (== InflectionInvalidWord "foo bar")
26+ mkWord "foo_" `shouldThrow` (== InflectionInvalidWord "foo_")
27+ mkWord "&$?%" `shouldThrow` (== InflectionInvalidWord "&$?%")
28+ describe "mkAcronym" $ do
29+ context "when provided a correct Text value" $
30+ it "creates an acronym" $ do
31+ x <- mkAcronym "foo"
32+ unWord x `shouldBe` "foo"
33+ context "when providde an incorrect Text value" $
34+ it "throws the correct exception" $ do
35+ mkAcronym "foo bar" `shouldThrow` (== InflectionInvalidAcronym "foo bar")
36+ mkAcronym "foo_" `shouldThrow` (== InflectionInvalidAcronym "foo_")
37+ mkAcronym "&$?%" `shouldThrow` (== InflectionInvalidAcronym "&$?%")
38+ describe "unWord" $
39+ it "extracts the inner Text value" $ do
40+ (unWord <$> mkWord "foo") `shouldReturn` "foo"
41+ (unWord <$> mkWord "bar") `shouldReturn` "bar"
42+ (unWord <$> mkAcronym "baz") `shouldReturn` "baz"
43+ describe "unSomeWord" $ do
44+ context "when inner value is a normal word" $
45+ it "Text is extracted and the given function applied" $ do
46+ x <- SomeWord <$> mkWord "word"
47+ unSomeWord T.toUpper x `shouldBe` "WORD"
48+ context "when inner value is an acronym" $
49+ it "Text is extracted, but the function is not applied" $ do
50+ x <- SomeWord <$> mkAcronym "acronym"
51+ unSomeWord T.toUpper x `shouldBe` "acronym"
test/Text/Inflections/UnderscoreSpec.hs+13−5
@@ -1,10 +1,18 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+
14module Text.Inflections.UnderscoreSpec (spec) where
25
36import Test.Hspec
4−import Text.Inflections (underscore)
5−import Text.Inflections.Parse.Types (Word(..))
7+import Text.Inflections
8+
9+#if !MIN_VERSION_base(4,8,0)
10+import Control.Applicative
11+#endif
612
713spec :: Spec
8−spec =
9− it "converts a word list to snake case" $
10− underscore [Word "test", Word "this"] `shouldBe` "test_this"
14+spec = describe "underscore" $
15+ it "converts a word list to snake case" $ do
16+ test <- SomeWord <$> mkWord "test"
17+ this <- SomeWord <$> mkWord "this"
18+ underscore [test, this] `shouldBe` "test_this"
test/Text/InflectionsSpec.hs+92−19
@@ -1,25 +1,98 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE OverloadedStrings #-}
4+
15module Text.InflectionsSpec (spec) where
26
7+import Data.Void
38import Test.Hspec
4−import Text.Inflections (toUnderscore, toDashed, toCamelCased)
9+import Test.QuickCheck
10+import Text.Inflections
11+import Text.Megaparsec
12+import Data.Text
13+
14+import qualified Data.List.NonEmpty as NE
15+import qualified Data.Set as S
16+
17+#if !MIN_VERSION_base(4,8,0)
18+import Control.Applicative
19+#endif
20+
21+instance Arbitrary Text where
22+ arbitrary = pack <$> (arbitrary :: Gen String)
23+
24+arbitraryParseErrorBundle :: Gen (ParseErrorBundle Text Void)
25+arbitraryParseErrorBundle = ParseErrorBundle <$> nonEmptyParseErrors <*> arbitraryPosState
26+ where
27+ posArbitrary = mkPos <$> ((+1) . abs <$> arbitrary)
28+ nonEmptyParseErrors :: Gen (NE.NonEmpty (ParseError Text Void))
29+ nonEmptyParseErrors = NE.fromList <$> listOf1 arbitraryParseError
30+ arbitrarySourcePos :: Gen SourcePos
31+ arbitrarySourcePos = SourcePos <$> arbitrary <*> posArbitrary <*> posArbitrary
32+ arbitraryPosState :: Gen (PosState Text)
33+ arbitraryPosState =
34+ PosState
35+ <$> arbitrary
36+ <*> arbitrary
37+ <*> arbitrarySourcePos
38+ <*> posArbitrary
39+ <*> arbitrary
40+
41+arbitraryParseError :: Gen (ParseError Text Void)
42+arbitraryParseError = oneof [trivialError, fancyError]
43+ where
44+ trivialError = TrivialError <$> arbitrary <*> maybeErrorItem <*> setErrorItem
45+ fancyError = FancyError <$> arbitrary <*> setErrorFancy
46+ setErrorFancy = S.fromList <$> listOf arbitraryErrorFancy
47+ maybeErrorItem = oneof [ Just <$> arbitraryErrorItem, return Nothing]
48+ setErrorItem = S.fromList <$> listOf arbitraryErrorItem
49+
50+arbitraryErrorFancy :: Gen (ErrorFancy e)
51+arbitraryErrorFancy = oneof [ ErrorFail <$> arbitrary ]
52+
53+arbitraryErrorItem :: (Arbitrary e) => Gen (ErrorItem e)
54+arbitraryErrorItem = oneof [ tokens_, labels_, return EndOfInput ]
55+ where
56+ tokens_ = Tokens <$> (NE.fromList <$> listOf1 arbitrary)
57+ labels_ = Label <$> (NE.fromList <$> listOf1 arbitrary)
558
659spec :: Spec
760spec = do
8− camelCaseToSnakeCase
9− camelCaseToDashed
10− snakeCaseToCamelCase
11−
12−camelCaseToSnakeCase :: Spec
13−camelCaseToSnakeCase =
14− it "converts camel case snake case" $
15− toUnderscore "camelCasedText" `shouldBe` "camel_cased_text"
16−
17−camelCaseToDashed :: Spec
18−camelCaseToDashed =
19− it "converts camel case to dashed" $
20− toDashed "camelCasedText" `shouldBe` "camel-cased-text"
21−
22−snakeCaseToCamelCase :: Spec
23−snakeCaseToCamelCase =
24− it "converts snake case to camel case" $
25− toCamelCased False "underscored_text" `shouldBe` "underscoredText"
61+
62+ describe "toUnderscore" $ do
63+ it "converts camel case to snake case" $
64+ toUnderscore "camelCasedText" `shouldBe` Right "camel_cased_text"
65+ it "converts camel case to snake case with numbers" $
66+ toUnderscore "ipv4Address" `shouldBe` Right "ipv4_address"
67+
68+ describe "toDashed" $
69+ it "converts camel case to dashed" $
70+ toDashed "camelCasedText" `shouldBe` Right "camel-cased-text"
71+
72+ describe "toCamelCased" $ do
73+ context "when the first argument is False" $
74+ it "converts snake case to camel case" $
75+ toCamelCased False "underscored_text" `shouldBe` Right "underscoredText"
76+ context "when the first argument is True" $
77+ it "converts snake case to camel case with the first word capitalized" $
78+ toCamelCased True "underscored_text" `shouldBe` Right "UnderscoredText"
79+
80+ describe "toHumanized" $ do
81+ context "when the first argument is False" $
82+ it "converts snake case to human-readable form with lower-case initial letter" $
83+ toHumanized False "underscored_text" `shouldBe` Right "underscored text"
84+ context "when the first argument is True" $
85+ it "converts snake case to human-readable form with the first word capitalized" $
86+ toHumanized True "underscored_text" `shouldBe` Right "Underscored text"
87+
88+ describe "betterThrow" $ do
89+ context "when given a parse error" $
90+ it "throws the correct exception" $
91+ property $ forAll arbitraryParseErrorBundle $ \err ->
92+ betterThrow (Left err) `shouldThrow`
93+ (== InflectionParsingFailed err)
94+
95+ context "when given a value in Right" $
96+ it "returns the value" $
97+ property $ \x ->
98+ betterThrow (Right x) `shouldReturn` (x :: Int)