git.iko.soy/typeable/inflections-hs
on master help sign in
22 files changed+507−161
since splitsnapshots
unifiedsplit
.travis.yml+17−26 CHANGELOG.md+30 LICENSE+17−18 README.md+11−8 Text/Inflections.hs+50−28 Text/Inflections/Camelize.hs+2−2 Text/Inflections/Humanize.hs+24−3 Text/Inflections/Ordinal.hs−1 Text/Inflections/Parse/CamelCase.hs+13−5 Text/Inflections/Parse/SnakeCase.hs+21−24 Text/Inflections/Titleize.hs+2−2 Text/Inflections/Types.hs+19−13 docs/CODE_OF_CONDUCT.md+35 docs/CONTRIBUTING.md+13 inflections.cabal+21−10 stack.yaml+1−6 stack.yaml.lock+12 test/Text/Inflections/HumanizeSpec.hs+40−12 test/Text/Inflections/Parse/CamelCaseSpec.hs+40−1 test/Text/Inflections/Parse/SnakeCaseSpec.hs+28−1 test/Text/Inflections/TypesSpec.hs+51 test/Text/InflectionsSpec.hs+60−1
.travis.yml+17−26
@@ -2,21 +2,21 @@ language: c
22
33sudo: false
44
5+cache:
6+ directories:
7+ - ~/.ghc
8+ - ~/.cabal
9+
510matrix:
611 include:
7− - env: CABALVER=1.16 GHCVER=7.6.3
8− addons: {apt: {packages: [cabal-install-1.16,ghc-7.6.3], sources: [hvr-ghc]}}
9− - env: CABALVER=1.18 GHCVER=7.8.4
10− addons: {apt: {packages: [cabal-install-1.18,ghc-7.8.4,],sources: [hvr-ghc]}}
11− - env: CABALVER=1.22 GHCVER=7.10.3
12− addons: {apt: {packages: [cabal-install-1.22,ghc-7.10.3],sources: [hvr-ghc]}}
13− - env: CABALVER=1.24 GHCVER=8.0.1
14− addons: {apt: {packages: [cabal-install-1.24,ghc-8.0.1],sources: [hvr-ghc]}}
15− - env: CABALVER=head GHCVER=head
16− addons: {apt: {packages: [cabal-install-head,ghc-head], sources: [hvr-ghc]}}
17−
18− allow_failures:
19− - env: CABALVER=head GHCVER=head
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]}}
2020
2121before_install:
2222 - export PATH=/opt/ghc/$GHCVER/bin:/opt/cabal/$CABALVER/bin:$PATH
@@ -30,23 +30,14 @@ install:
3030script:
3131 - case "$CABALVER" in
3232 "1.16") cabal configure -v2 -f dev ;;
33− "1.18") cabal configure --enable-tests --enable-library-coverage -v2 -f dev ;;
34− *) cabal configure --enable-tests --enable-coverage -v2 -f dev ;;
33+ *) cabal configure --enable-tests -v2 -f dev ;;
3534 esac
3635 - cabal build
36+ - cabal sdist
3737 - case "$GHCVER" in
38− "7.6.3") true ;;
39− *) cabal test --show-details=always ;;
38+ "8.4.3") cabal haddock | grep "100%" | wc -l | grep "1" ;;
39+ *) cabal haddock | grep "100%" | wc -l | grep "13" ;;
4040 esac
41− - cabal sdist
42− - cabal haddock | grep "100%" | wc -l | grep "13"
43−
44−# after_script:
45−# - cabal install hpc-coveralls
46−# - export PATH=~/.cabal/bin:$PATH
47−# - hpc-coveralls --coverage-mode=StrictlyFullLines
48−# --exclude-dir=tests
49−# test
5041
5142notifications:
5243 email: false
CHANGELOG.md+30
@@ -1,3 +1,26 @@
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
326* A more type-safe API forbidding creation of invalid words.
@@ -9,6 +32,13 @@
932
1033* Renamed `defaultMap` to `defaultTransliterations`.
1134
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+
1242* `parseCamelCase` and `parseSnakeCase` take any instance of `Foldable` as a
1343 collection of acronyms, not just lists.
1444
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+11−8
@@ -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+50−28
@@ -1,6 +1,6 @@
11-- |
22-- Module : Text.Inflections
3−-- Description : Rails-like inflections library for common String transformations.
3+-- Description : Rails-like inflections library for common Text transformations.
44-- Copyright : (c) Justin Leitgeb
55-- License : MIT
66--
@@ -8,7 +8,7 @@
88-- Stability : unstable
99-- Portability : portable
1010--
11−-- This module provides methods for common String transformations, similar
11+-- This module provides methods for common 'Text' transformations, similar
1212-- to the Inflections library found in Rails:
1313--
1414-- <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
@@ -17,7 +17,7 @@
1717-- implementations in Rails' ActiveSupport, the philosophy of this library
1818-- is fundamentally different. Where Rails tries to be as permissive as
1919-- possible, and return a String when given any input, this library tries to
20−-- output strings that make sense according to the function that is called.
20+-- output 'Text' that makes sense according to the function that is called.
2121--
2222-- When you look closely at many of the functions in Rails' inflections
2323-- library, you will notice that many of them are partial. That is, they
@@ -33,7 +33,7 @@
3333-- >>> "foo bar".underscore
3434-- "foo bar"
3535--
36−-- The output isn't underscored - it contains a space! It turns out that
36+-- The output isn't underscored — it contains a space! It turns out that
3737-- some of the functions from Inflections in ActiveSupport are /partial/.
3838-- I.e., the outputs are really only specified for a certain range of the
3939-- inputs allowed by the String type.
@@ -42,7 +42,7 @@
4242-- results by separating the parsing of strings into tokens from the
4343-- application of transformations. Let's see an example.
4444--
45−-- First, we tokenize an underscored String using 'parseSnakeCase':
45+-- First, we tokenize an underscored 'Text' using 'parseSnakeCase':
4646--
4747-- >>> parseSnakeCase [] "foo_bar"
4848-- Right [Word "foo",Word "bar"]
@@ -79,14 +79,24 @@
7979{-# LANGUAGE CPP #-}
8080
8181module Text.Inflections
82− ( -- * Parsing
83− parseSnakeCase
82+ ( -- * Types and helpers
83+ Word
84+ , WordType (..)
85+ , mkWord
86+ , mkAcronym
87+ , unWord
88+ , SomeWord (..)
89+ , unSomeWord
90+ , InflectionException (..)
91+ -- * Parsing
92+ , parseSnakeCase
8493 , parseCamelCase
8594 -- * Rendering
8695 , camelize
8796 , camelizeCustom
8897 , dasherize
8998 , humanize
99+ , humanizeCustom
90100 , underscore
91101 , titleize
92102 , Transliterations
@@ -97,28 +107,21 @@ module Text.Inflections
97107 , transliterateCustom
98108 , ordinalize
99109 , ordinal
100− -- * Types and helpers
101− , Word
102− , WordType (..)
103− , mkWord
104− , mkAcronym
105− , unWord
106− , SomeWord (..)
107− , unSomeWord
108− , InflectionException (..)
109110 -- * Often used combinators
110111 , toUnderscore
111112 , toDashed
112113 , toCamelCased
114+ , toHumanized
113115 , betterThrow )
114116where
115117
116118import Control.Monad.Catch (MonadThrow (..))
117119import Data.Text (Text)
120+import Data.Void (Void)
118121import Text.Inflections.Camelize (camelize, camelizeCustom)
119122import Text.Inflections.Dasherize (dasherize)
120123import Text.Inflections.Data (Transliterations, defaultTransliterations)
121−import Text.Inflections.Humanize (humanize)
124+import Text.Inflections.Humanize (humanize, humanizeCustom)
122125import Text.Inflections.Ordinal (ordinal, ordinalize)
123126import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
124127import Text.Inflections.Parse.CamelCase (parseCamelCase)
@@ -139,7 +142,7 @@ import Prelude hiding (Word)
139142--
140143-- >>> toUnderscore "FooBarBazz"
141144-- "foo_bar_bazz"
142−toUnderscore :: Text -> Either (ParseError Char Dec) Text
145+toUnderscore :: Text -> Either (ParseErrorBundle Text Void) Text
143146toUnderscore = fmap underscore . parseCamelCase []
144147
145148-- | Transforms CamelCasedString to snake-cased-string-with-dashes.
@@ -148,26 +151,45 @@ toUnderscore = fmap underscore . parseCamelCase []
148151--
149152-- >>> toDashed "FooBarBazz"
150153-- "foo-bar-bazz"
151−toDashed :: Text -> Either (ParseError Char Dec) Text
154+toDashed :: Text -> Either (ParseErrorBundle Text Void) Text
152155toDashed = fmap dasherize . parseCamelCase []
153156
154157-- | Transforms underscored_text to CamelCasedText. If first argument is
155−-- 'True' then FirstCharacter in result string will be in upper case. If
156−-- 'False' then firstCharacter will be in lower case.
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.
157160--
158−-- > toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
161+-- > toCamelCased c = fmap (camelizeCustom c) . parseSnakeCase []
159162--
160163-- >>> toCamelCased True "foo_bar_bazz"
161164-- "FooBarBazz"
162165-- >>> toCamelCased False "foo_bar_bazz"
163166-- "fooBarBazz"
164167toCamelCased
165− :: Bool -- ^ Capitalize the first character
166− -> Text -- ^ Input
167− -> Either (ParseError Char Dec) Text -- ^ Ouput
168−toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
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 []
169191
170−-- | Lift something of type @'Either' ('ParseError' 'Char' 'Dec') a@ to
192+-- | Lift something of type @'Either' ('ParseError' 'Char' 'Void') a@ to
171193-- an instance of 'MonadThrow'. Useful when you want to shortcut on parsing
172194-- failures and you're in an instance of 'MonadThrow'.
173195--
@@ -175,6 +197,6 @@ toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
175197--
176198-- /since 0.3.0.0/
177199
178−betterThrow :: MonadThrow m => Either (ParseError Char Dec) a -> m a
200+betterThrow :: MonadThrow m => Either (ParseErrorBundle Text Void) a -> m a
179201betterThrow (Left err) = throwM (InflectionParsingFailed err)
180202betterThrow (Right x) = return x
Text/Inflections/Camelize.hs+2−2
@@ -29,7 +29,7 @@ import Control.Applicative
2929--
3030-- >>> foo <- SomeWord <$> mkWord "foo"
3131-- >>> bar <- SomeWord <$> mkAcronym "bar"
32−-- >>> bazz <- SomeWord <$> mkAcronym "bazz"
32+-- >>> bazz <- SomeWord <$> mkWord "bazz"
3333-- >>> camelize [foo,bar,bazz]
3434-- "FoobarBazz"
3535camelize
@@ -41,7 +41,7 @@ camelize = camelizeCustom True
4141--
4242-- >>> foo <- SomeWord <$> mkWord "foo"
4343-- >>> bar <- SomeWord <$> mkAcronym "bar"
44−-- >>> bazz <- SomeWord <$> mkAcronym "bazz"
44+-- >>> bazz <- SomeWord <$> mkWord "bazz"
4545-- >>> camelizeCustom False [foo,bar,bazz]
4646-- "foobarBazz"
4747camelizeCustom
Text/Inflections/Humanize.hs+24−3
@@ -13,7 +13,8 @@
1313{-# LANGUAGE OverloadedStrings #-}
1414
1515module Text.Inflections.Humanize
16− ( humanize )
16+ ( humanize
17+ , humanizeCustom )
1718where
1819
1920import Data.Text (Text)
@@ -36,7 +37,27 @@ import Control.Applicative
3637humanize
3738 :: [SomeWord] -- ^ List of words, first of which will be capitalized
3839 -> Text -- ^ The humanized output
39−humanize xs' =
40+humanize = humanizeCustom True
41+
42+
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' =
4061 case unSomeWord (T.replace "_" " ") <$> xs' of
4162 [] -> ""
42− (x:xs) -> T.unwords $ T.toTitle x : (T.toLower <$> xs)
63+ (x:xs) -> T.unwords $ (if c then T.toTitle else T.toLower) x : (T.toLower <$> xs)
Text/Inflections/Ordinal.hs−1
@@ -16,7 +16,6 @@ module Text.Inflections.Ordinal
1616 , ordinal )
1717where
1818
19−import Data.Monoid ((<>))
2019import Data.Text (Text)
2120import qualified Data.Text as T
2221
Text/Inflections/Parse/CamelCase.hs+13−5
@@ -17,19 +17,27 @@ module Text.Inflections.Parse.CamelCase
1717 ( parseCamelCase )
1818where
1919
20−import Control.Applicative
20+# if MIN_VERSION_base(4,8,0)
21+import Control.Applicative (empty, many, (<|>))
22+#else
23+import Control.Applicative (empty, many, (<|>), (<$>), (<*))
24+#endif
2125import Data.Text (Text)
26+import Data.Void (Void)
2227import Text.Inflections.Types
23−import Text.Megaparsec
24−import Text.Megaparsec.Text
28+import Text.Megaparsec (Parsec, ParseErrorBundle, choice, eof, parse)
29+import Text.Megaparsec.Char
2530import qualified Data.Text as T
2631
2732#if MIN_VERSION_base(4,8,0)
2833import Prelude hiding (Word)
2934#else
3035import Data.Foldable
36+import Prelude hiding (elem)
3137#endif
3238
39+type Parser = Parsec Void Text
40+
3341-- | Parse a CamelCase string.
3442--
3543-- >>> bar <- mkAcronym "bar"
@@ -43,7 +51,7 @@ import Data.Foldable
4351parseCamelCase :: (Foldable f, Functor f)
4452 => f (Word 'Acronym) -- ^ Collection of acronyms
4553 -> Text -- ^ Input
46− -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
54+ -> Either (ParseErrorBundle Text Void) [SomeWord] -- ^ Result of parsing
4755parseCamelCase acronyms = parse (parser acronyms) ""
4856
4957parser :: (Foldable f, Functor f)
@@ -58,7 +66,7 @@ acronym :: (Foldable f, Functor f)
5866 => f (Word 'Acronym)
5967 -> Parser (Word 'Acronym)
6068acronym acronyms = do
61− x <- T.pack <$> choice (string . T.unpack . unWord <$> acronyms)
69+ x <- choice (string . unWord <$> acronyms)
6270 case mkAcronym x of
6371 Nothing -> empty -- cannot happen if the system is sound
6472 Just acr -> return acr
Text/Inflections/Parse/SnakeCase.hs+21−24
@@ -16,19 +16,27 @@ module Text.Inflections.Parse.SnakeCase
1616 ( parseSnakeCase )
1717where
1818
19−import Control.Applicative
19+# if MIN_VERSION_base(4,8,0)
20+import Control.Applicative (empty, some)
21+#else
22+import Control.Applicative (empty, some, (<$>), (<*))
23+#endif
2024import Data.Text (Text)
25+import Data.Void (Void)
2126import Text.Inflections.Types
22−import Text.Megaparsec
23−import Text.Megaparsec.Text
27+import Text.Megaparsec (Parsec, ParseErrorBundle, eof, sepBy, parse)
28+import Text.Megaparsec.Char
2429import qualified Data.Text as T
2530
2631#if MIN_VERSION_base(4,8,0)
2732import Prelude hiding (Word)
2833#else
2934import Data.Foldable
35+import Prelude hiding (elem)
3036#endif
3137
38+type Parser = Parsec Void Text
39+
3240-- | Parse a snake_case string.
3341--
3442-- >>> bar <- mkAcronym "bar"
@@ -42,31 +50,20 @@ import Data.Foldable
4250parseSnakeCase :: (Foldable f, Functor f)
4351 => f (Word 'Acronym) -- ^ Collection of acronyms
4452 -> Text -- ^ Input
45− -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
53+ -> Either (ParseErrorBundle Text Void) [SomeWord] -- ^ Result of parsing
4654parseSnakeCase acronyms = parse (parser acronyms) ""
4755
4856parser :: (Foldable f, Functor f)
4957 => f (Word 'Acronym)
5058 -> Parser [SomeWord]
51−parser acronyms = ((a <|> n) `sepBy` char '_') <* eof
52− where
53− n = SomeWord <$> word
54− a = SomeWord <$> acronym acronyms
59+parser acronyms = (pWord acronyms `sepBy` char '_') <* eof
5560
56−acronym :: (Foldable f, Functor f)
61+pWord :: (Foldable f, Functor f)
5762 => f (Word 'Acronym)
58− -> Parser (Word 'Acronym)
59−acronym acronyms = do
60− x <- T.pack <$> choice (string . T.unpack . unWord <$> acronyms)
61− case mkAcronym x of
62− Nothing -> empty -- cannot happen if the system is sound
63− Just acr -> return acr
64−{-# INLINE acronym #-}
65−
66−word :: Parser (Word 'Normal)
67−word = do
68− x <- T.pack <$> (some lowerChar <|> some digitChar)
69− case mkWord x of
70− Nothing -> empty -- cannot happen if the system is sound
71− Just wrd -> return wrd
72−{-# INLINE word #-}
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/Titleize.hs+2−2
@@ -17,7 +17,7 @@ import Data.Text (Text)
1717import Text.Inflections.Types
1818import qualified Data.Text as T
1919
20−-- | Capitalize all the Words in the input list.
20+-- | Capitalize all the 'SomeWord' words in the input list.
2121--
2222-- >>> foo <- SomeWord <$> mkWord "foo"
2323-- >>> bar <- SomeWord <$> mkAcronym "bar"
@@ -25,6 +25,6 @@ import qualified Data.Text as T
2525-- >>> titleize [foo,bar,bazz]
2626-- "Foo bar Bazz"
2727titleize
28− :: [SomeWord] -- ^ List of words, first of which will be capitalized
28+ :: [SomeWord] -- ^ List of words, of which all 'SomeWord' words will be capitalized and all acronyms will be left as is
2929 -> Text -- ^ The titleized 'Text'
3030titleize = T.unwords . fmap (unSomeWord T.toTitle)
Text/Inflections/Types.hs+19−13
@@ -31,8 +31,9 @@ module Text.Inflections.Types
3131where
3232
3333import Control.Monad.Catch
34−import Data.Char (isSpace)
34+import Data.Char (isAlphaNum)
3535import Data.Data (Data)
36+import Data.Void (Void)
3637import Data.Text (Text)
3738import Data.Typeable (Typeable)
3839import GHC.Generics (Generic)
@@ -43,28 +44,30 @@ import qualified Data.Text as T
4344import Prelude hiding (Word)
4445#endif
4546
46−-- | Create a word from given 'Text'. The input should not contain spaces or
47+-- | Create a word from given 'Text'. The input should consist of only
48+-- alpha-numeric characters (no white spaces or punctuation)
4749-- 'InflectionInvalidWord' will be thrown.
4850--
4951-- /since 0.3.0.0/
5052
5153mkWord :: MonadThrow m => Text -> m (Word 'Normal)
5254mkWord txt =
53− if T.any isSpace txt
54− then throwM (InflectionInvalidWord txt)
55− else return (Word txt)
56−
57−-- | Create an acronym from given 'Text'. The input should not contain
58−-- spaces or 'InflectionInvalidAcronym' will be thrown. Acronym is different
59−-- from normal word by that it may not be transformed by inflections.
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').
6063--
6164-- /since 0.3.0.0/
6265
6366mkAcronym :: MonadThrow m => Text -> m (Word 'Acronym)
6467mkAcronym txt =
65− if T.any isSpace txt
66− then throwM (InflectionInvalidAcronym txt)
67− else return (Word txt)
68+ if T.all isAlphaNum txt
69+ then return (Word txt)
70+ else throwM (InflectionInvalidAcronym txt)
6871
6972-- | A 'Text' value that should be kept whole through applied inflections.
7073
@@ -106,6 +109,9 @@ data SomeWord where
106109instance Show SomeWord where
107110 show (SomeWord w) = show w
108111
112+instance Eq SomeWord where
113+ x == y = unSomeWord id x == unSomeWord id y
114+
109115-- | Extract 'Text' from 'SomeWord' and apply given function only if the
110116-- word inside wasn't an acronym.
111117--
@@ -130,7 +136,7 @@ instance Transformable (Word 'Acronym) where
130136-- /since 0.3.0.0/
131137
132138data InflectionException
133− = InflectionParsingFailed (ParseError Char Dec)
139+ = InflectionParsingFailed (ParseErrorBundle Text Void)
134140 | InflectionInvalidWord Text
135141 | InflectionInvalidAcronym Text
136142 deriving (Eq, Show, Typeable, Data, Generic)
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+21−10
@@ -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,
@@ -48,10 +48,13 @@ library
4848 else
4949 ghc-options: -O2 -Wall
5050 build-depends: base >= 4.6 && < 5.0
51− , exceptions >= 0.6 && < 0.9
52− , megaparsec >= 5.0 && < 6.0
53− , text >= 0.2 && < 1.3
54− , unordered-containers >= 0.2.7 && < 0.3
51+ , exceptions
52+ , megaparsec
53+ , text
54+ , unordered-containers
55+ if !impl(ghc >= 7.10)
56+ build-depends: void == 0.7.*
57+
5558 default-language: Haskell2010
5659
5760test-suite test
@@ -59,12 +62,19 @@ test-suite test
5962 hs-source-dirs: test
6063 main-is: Spec.hs
6164 build-depends: inflections
62− , QuickCheck >= 2.7.6 && < 3.0
65+ , QuickCheck
6366 , base >= 4.6 && < 5.0
64− , hspec >= 2.0 && < 3.0
65− , hspec-megaparsec >= 0.3 && < 0.4
66− , megaparsec >= 5.1 && < 6.0
67− , text >= 0.2 && < 1.3
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+
6878 if flag(dev)
6979 ghc-options: -Wall -Werror
7080 else
@@ -78,5 +88,6 @@ test-suite test
7888 , Text.Inflections.Parse.SnakeCaseSpec
7989 , Text.Inflections.TitleizeSpec
8090 , Text.Inflections.TransliterateSpec
91+ , Text.Inflections.TypesSpec
8192 , Text.Inflections.UnderscoreSpec
8293 , Text.InflectionsSpec
stack.yaml+1−6
@@ -1,6 +1 @@
1−resolver: lts-7.13
2−packages:
3−- '.'
4−extra-deps:
5−- hspec-megaparsec-0.3.0
6−- megaparsec-5.1.2
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/HumanizeSpec.hs+40−12
@@ -12,15 +12,43 @@ import Control.Applicative
1212#endif
1313
1414spec :: Spec
15−spec = describe "humazine" $ do
16− it "converts snake case to a human-readable string" $ do
17− employee <- SomeWord <$> mkWord "employee"
18− salary <- SomeWord <$> mkWord "salary"
19− humanize [employee,salary] `shouldBe` "Employee salary"
20− it "turns underscores into spaces" $ do
21− employee <- SomeWord <$> mkWord "employee"
22− hasSalary <- SomeWord <$> mkWord "has_salary"
23− humanize [employee, hasSalary] `shouldBe` "Employee has salary"
24− it "capitalizes the first word of a sentence" $ do
25− underground <- SomeWord <$> mkWord "underground"
26− humanize [underground] `shouldBe` "Underground"
15+spec = do
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/Parse/CamelCaseSpec.hs+40−1
@@ -1,8 +1,47 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
13module Text.Inflections.Parse.CamelCaseSpec
24 ( spec )
35where
46
57import Test.Hspec
8+import Test.Hspec.Megaparsec
9+import Text.Inflections
610
711spec :: Spec
8−spec = return ()
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.hs+28−1
@@ -1,8 +1,35 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+
13module Text.Inflections.Parse.SnakeCaseSpec
24 ( spec )
35where
46
57import Test.Hspec
8+import Test.Hspec.Megaparsec
9+import Text.Inflections
610
711spec :: Spec
8−spec = return ()
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/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/InflectionsSpec.hs+60−1
@@ -1,10 +1,60 @@
1+{-# OPTIONS_GHC -fno-warn-orphans #-}
2+{-# LANGUAGE CPP #-}
13{-# LANGUAGE OverloadedStrings #-}
24
35module Text.InflectionsSpec (spec) where
46
7+import Data.Void
58import Test.Hspec
69import Test.QuickCheck
710import 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)
858
959spec :: Spec
1060spec = do
@@ -27,12 +77,21 @@ spec = do
2777 it "converts snake case to camel case with the first word capitalized" $
2878 toCamelCased True "underscored_text" `shouldBe` Right "UnderscoredText"
2979
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+
3088 describe "betterThrow" $ do
3189 context "when given a parse error" $
3290 it "throws the correct exception" $
33− property $ \err ->
91+ property $ forAll arbitraryParseErrorBundle $ \err ->
3492 betterThrow (Left err) `shouldThrow`
3593 (== InflectionParsingFailed err)
94+
3695 context "when given a value in Right" $
3796 it "returns the value" $
3897 property $ \x ->