git.iko.soy/typeable/inflections-hs
on master help sign in
Merge pull request #43 from stackbuilders/hide-word-constructors
c964f2aMark Karpov
.travis.yml+1−1
@@ -39,7 +39,7 @@ script:
39 *) cabal test --show-details=always ;;
39 *) cabal test --show-details=always ;;
40 esac
40 esac
41 - cabal sdist
41 - cabal sdist
42− - cabal haddock | grep "100%" | wc -l | grep "14"
42+ - cabal haddock | grep "100%" | wc -l | grep "13"
43
43
44# after_script:
44# after_script:
45# - cabal install hpc-coveralls
45# - cabal install hpc-coveralls
CHANGELOG.md+21−5
@@ -1,16 +1,32 @@
1## Inflections 0.3.0.0
1## Inflections 0.3.0.0
2
2
3−* Added the `CHANGELOG.md` file.
3+* A more type-safe API forbidding creation of invalid words.
4
4
5−* Switched test suite to Hspec.
5+* Made the API use `Text` instead of `String` (which significally improved
6+ speed).
6
7
7* Switched to Megaparsec 5 for parsing.
8* Switched to Megaparsec 5 for parsing.
8
9
9−* Made the API use `Text` instead of `String` (which significally improved
10+* Renamed `defaultMap` to `defaultTransliterations`.
10− speed).
11+
12+* Words now can contain digits (recognized by all parsers).
13+
14+* `parseSnakeCase` now is not confused when a word happens to have prefix
15+ coinciding with an acronym. This is harder to fix for `parseCamelCase`
16+ because acronym may contain capital letters, so old behavior is preserved
17+ for `parseCamelCase` for now.
18+
19+* `parseCamelCase` and `parseSnakeCase` take any instance of `Foldable` as a
20+ collection of acronyms, not just lists.
21+
22+* Added the `CHANGELOG.md` file.
23+
24+* Switched test suite to Hspec.
11
25
12* The `toUnderscore`, `toDashed`, and `toCamelCased` are not partial
26* The `toUnderscore`, `toDashed`, and `toCamelCased` are not partial
13− anymore, now they operate in `MonadThrow`.
27+ anymore. They return parse error in `Left` just like parsing functions,
28+ but this result can be lifted into any instance of `MonadThrow` with
29+ `betterThrow` helper.
14
30
15* Improved documentation.
31* Improved documentation.
16
32
Text/Inflections.hs+143−125
@@ -1,162 +1,180 @@
1−{- |
1+-- |
2−Module : Text.Inflections
2+-- Module : Text.Inflections
3−Description : Rails-like inflections library for common String transformations.
3+-- Description : Rails-like inflections library for common String transformations.
4−Copyright : (c) Justin Leitgeb
4+-- Copyright : (c) Justin Leitgeb
5−License : MIT
5+-- License : MIT
6−
6+--
7−Maintainer : justin@stackbuilders.com
7+-- Maintainer : justin@stackbuilders.com
8−Stability : unstable
8+-- Stability : unstable
9−Portability : portable
9+-- Portability : portable
10−
10+--
11−This module provides methods for common String transformations, similar to the
11+-- This module provides methods for common String transformations, similar
12−"Inflections" library found in Rails:
12+-- to the Inflections library found in Rails:
13−
13+--
14−<http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
14+-- <http://api.rubyonrails.org/classes/ActiveSupport/Inflector.html>
15−
15+--
16−While many of the functions in this library are the same as in implementations
16+-- While many of the functions in this library are the same as in
17−in Rails' ActiveSupport, the philosophy of this library is fundamentally
17+-- implementations in Rails' ActiveSupport, the philosophy of this library
18−different. Where Rails tries to be as permissive as possible, and return a
18+-- is fundamentally different. Where Rails tries to be as permissive as
19−String when given any input, this library tries to output strings that make
19+-- possible, and return a String when given any input, this library tries to
20−sense according to the function that is called.
20+-- output strings that make sense according to the function that is called.
21−
21+--
22−When you look closely at many of the functions in Rails' inflections
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
23+-- library, you will notice that many of them are partial. That is, they
24−have well-defined output for some of the possible inputs to the function allowed
24+-- only have well-defined output for some of the possible inputs to the
25−by the type system. As an example, let's take the @underscore@ function. In
25+-- function allowed by the type system. As an example, let's take the
26−Rails, it works like this:
26+-- @underscore@ function. In Rails, it works like this:
27−
27+--
28−>>> "fooBar".underscore
28+-- >>> "fooBar".underscore
29−"foo_bar"
29+-- "foo_bar"
30−
30+--
31−Looks ok so far. However, it's also easy to produce less expected results:
31+-- Looks OK so far. However, it's also easy to produce less expected results:
32−
32+--
33−>>> "foo bar".underscore
33+-- >>> "foo bar".underscore
34−"foo bar"
34+-- "foo bar"
35−
35+--
36−The output isn't underscored - it contains a space! It turns out that some of
36+-- The output isn't underscored - it contains a space! It turns out that
37−the functions from Inflections in ActiveSupport are /partial/. Ie., the outputs
37+-- some of the functions from Inflections in ActiveSupport are /partial/.
38−are really only specified for a certain range of the inputs allowed by the
38+-- I.e., the outputs are really only specified for a certain range of the
39−String type.
39+-- inputs allowed by the String type.
40−
40+--
41−In the Haskell inflections library, we aim to deliver more predictable results
41+-- In the Haskell inflections library, we aim to deliver more predictable
42−by separating the parsing of strings into tokens from the application of
42+-- results by separating the parsing of strings into tokens from the
43−transformations. Let's see an example.
43+-- application of transformations. Let's see an example.
44−
44+--
45−First, we tokenize an underscored String using 'parseSnakeCase':
45+-- First, we tokenize an underscored String using 'parseSnakeCase':
46−
46+--
47−>>> parseSnakeCase [] "foo_bar"
47+-- >>> parseSnakeCase [] "foo_bar"
48−Right [Word "foo",Word "bar"]
48+-- Right [Word "foo",Word "bar"]
49−
49+--
50−We can chain together the tokenization of the input String and the
50+-- We can chain together the tokenization of the input String and the
51−transformation to CamelCase by using 'Control.Monad.LiftM':
51+-- transformation to CamelCase by using 'fmap':
52−
52+--
53−>>> import Control.Monad (liftM)
54−>>> liftM camelize $ parseSnakeCase "foo_bar"
53+-- >>> camelize <$> parseSnakeCase [] "foo_bar"
55−
54+-- Right "FooBar"
55+--
56−By separating out the tokenization from the application of inflections, we also
56+-- By separating out the tokenization from the application of inflections,
57−end up with useful libraries for validating input which can be used
57+-- we also end up with useful libraries for validating input which can be
58−independently:
58+-- used independently:
59−
59+--
60−>>> parseSnakeCase [] "fooBar"
60+-- >>> parseSnakeCase [] "fooBar"
61−Left "(unknown)" (line 1, column 4):
61+-- 1:4:
62−unexpected 'B'
62+-- unexpected 'B'
63−expecting lowercase letter, "_" or end of input
63+-- expecting '_', end of input, or lowercase letter
64−
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+--
65−This library is still a work-in-progress, and contributions are welcome for
73+-- This library is still a work-in-progress, and contributions are welcome
66−missing pieces and to fix bugs. Please see the Github page to contribute with
74+-- for missing pieces and to fix bugs. Please see the Github page to
67−code or bug reports:
75+-- contribute with code or bug reports:
68−
76+--
69−<https://github.com/stackbuilders/inflections-hs>
77+-- <https://github.com/stackbuilders/inflections-hs>
70
78
71−-}
79+{-# LANGUAGE CPP #-}
72
80
73module Text.Inflections
81module Text.Inflections
82+ ( -- * Parsing
83+ parseSnakeCase
84+ , parseCamelCase
85+ -- * Rendering
74− ( camelize
86+ , camelize
75− , camelizeCustom
87+ , camelizeCustom
76−
77− , dasherize
88+ , dasherize
78−
79− , humanize
89+ , humanize
80−
81− , underscore
90+ , underscore
82−
83− , titleize
91+ , titleize
84−
85− , Transliterations
92+ , Transliterations
86− , defaultMap
93+ , defaultTransliterations
87−
88− , parameterize
94+ , parameterize
89− , parameterizeCustom
95+ , parameterizeCustom
90−
91− , transliterate
96+ , transliterate
92− , transliterateCustom
97+ , transliterateCustom
93−
98+ , ordinalize
94− , ordinal
99+ , ordinal
95− , ordinalize
100+ -- * Types and helpers
96−
101+ , Word
97− , parseSnakeCase
102+ , WordType (..)
98− , parseCamelCase
103+ , mkWord
104+ , mkAcronym
105+ , unWord
106+ , SomeWord (..)
107+ , unSomeWord
108+ , InflectionException (..)
99 -- * Often used combinators
109 -- * Often used combinators
100− , toUnderscore
110+ , toUnderscore
101− , toDashed
111+ , toDashed
102− , toCamelCased
112+ , toCamelCased
103− )
113+ , betterThrow )
104where
114where
105
115
106−import Control.Monad (liftM)
107import Control.Monad.Catch (MonadThrow (..))
116import Control.Monad.Catch (MonadThrow (..))
108import Data.Text (Text)
117import Data.Text (Text)
109import Text.Inflections.Camelize (camelize, camelizeCustom)
118import Text.Inflections.Camelize (camelize, camelizeCustom)
110import Text.Inflections.Dasherize (dasherize)
119import Text.Inflections.Dasherize (dasherize)
111−import Text.Inflections.Data (Transliterations, defaultMap)
120+import Text.Inflections.Data (Transliterations, defaultTransliterations)
112import Text.Inflections.Humanize (humanize)
121import Text.Inflections.Humanize (humanize)
113import Text.Inflections.Ordinal (ordinal, ordinalize)
122import Text.Inflections.Ordinal (ordinal, ordinalize)
114import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
123import Text.Inflections.Parameterize (parameterize, parameterizeCustom)
115import Text.Inflections.Parse.CamelCase (parseCamelCase)
124import Text.Inflections.Parse.CamelCase (parseCamelCase)
116import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
125import Text.Inflections.Parse.SnakeCase (parseSnakeCase)
117−import Text.Inflections.Parse.Types
118import Text.Inflections.Titleize (titleize)
126import Text.Inflections.Titleize (titleize)
119import Text.Inflections.Transliterate (transliterate, transliterateCustom)
127import Text.Inflections.Transliterate (transliterate, transliterateCustom)
128+import Text.Inflections.Types
120import Text.Inflections.Underscore (underscore)
129import Text.Inflections.Underscore (underscore)
121import Text.Megaparsec
130import Text.Megaparsec
122
131
132+#if MIN_VERSION_base(4,8,0)
133+import Prelude hiding (Word)
134+#endif
135+
123−-- | Transforms CamelCasedString to snake_cased_string_with_underscores. In
136+-- | Transforms CamelCasedString to snake_cased_string_with_underscores.
124−-- case of failed parsing 'InflectionException' is thrown.
137+--
138+-- > toUnderscore = fmap underscore . parseCamelCase []
125--
139--
126-- >>> toUnderscore "FooBarBazz"
140-- >>> toUnderscore "FooBarBazz"
127-- "foo_bar_bazz"
141-- "foo_bar_bazz"
128−toUnderscore :: MonadThrow m => Text -> m Text
142+toUnderscore :: Text -> Either (ParseError Char Dec) Text
129−toUnderscore = liftM underscore . handleEither . parseCamelCase []
143+toUnderscore = fmap underscore . parseCamelCase []
130−{-# INLINE toUnderscore #-}
131
144
132−-- | Transforms CamelCasedString to snake-cased-string-with-dashes. In case
145+-- | Transforms CamelCasedString to snake-cased-string-with-dashes.
133−-- of failed parsing 'InflectionException' is thrown.
146+--
147+-- > toDashed = fmap dasherize . parseCamelCase []
134--
148--
135-- >>> toDashed "FooBarBazz"
149-- >>> toDashed "FooBarBazz"
136-- "foo-bar-bazz"
150-- "foo-bar-bazz"
137−toDashed :: MonadThrow m => Text -> m Text
151+toDashed :: Text -> Either (ParseError Char Dec) Text
138−toDashed = liftM dasherize . handleEither . parseCamelCase []
152+toDashed = fmap dasherize . parseCamelCase []
139−{-# INLINE toDashed #-}
140
153
141-- | Transforms underscored_text to CamelCasedText. If first argument is
154-- | Transforms underscored_text to CamelCasedText. If first argument is
142-- 'True' then FirstCharacter in result string will be in upper case. If
155-- 'True' then FirstCharacter in result string will be in upper case. If
143−-- 'False' then firstCharacter will be in lower case. In case of failed
156+-- 'False' then firstCharacter will be in lower case.
144−-- parsing 'InflectionException' is thrown.
157+--
158+-- > toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
145--
159--
146-- >>> toCamelCased True "foo_bar_bazz"
160-- >>> toCamelCased True "foo_bar_bazz"
147-- "FooBarBazz"
161-- "FooBarBazz"
148-- >>> toCamelCased False "foo_bar_bazz"
162-- >>> toCamelCased False "foo_bar_bazz"
149-- "fooBarBazz"
163-- "fooBarBazz"
150−toCamelCased :: MonadThrow m
164+toCamelCased
151− => Bool -- ^ Capitalize the first character
165+ :: Bool -- ^ Capitalize the first character
152 -> Text -- ^ Input
166 -> Text -- ^ Input
153− -> m Text -- ^ Ouput
167+ -> Either (ParseError Char Dec) Text -- ^ Ouput
154−toCamelCased t = liftM (camelizeCustom t) . handleEither . parseSnakeCase []
168+toCamelCased t = fmap (camelizeCustom t) . parseSnakeCase []
155−{-# INLINE toCamelCased #-}
169+
170+-- | Lift something of type @'Either' ('ParseError' 'Char' 'Dec') a@ to
171+-- an instance of 'MonadThrow'. Useful when you want to shortcut on parsing
172+-- failures and you're in an instance of 'MonadThrow'.
173+--
174+-- This throws 'InflectionParsingFailed' if given value is inside 'Left'.
175+--
176+-- /since 0.3.0.0/
156
177
157−-- | Take an 'Either' that can contain a parser error and throw it if
158−-- necessary. If everything is OK, just return 'Right' value.
159−handleEither :: MonadThrow m => Either (ParseError Char Dec) a -> m a
178+betterThrow :: MonadThrow m => Either (ParseError Char Dec) a -> m a
160−handleEither (Left err) = throwM (InflectionParsingFailed err)
179+betterThrow (Left err) = throwM (InflectionParsingFailed err)
161−handleEither (Right x) = return x
180+betterThrow (Right x) = return x
162−{-# INLINE handleEither #-}
Text/Inflections/Camelize.hs+18−16
@@ -18,34 +18,36 @@ module Text.Inflections.Camelize
18where
18where
19
19
20import Data.Text (Text)
20import Data.Text (Text)
21−import Text.Inflections.Parse.Types
21+import Text.Inflections.Types
22import qualified Data.Text as T
22import qualified Data.Text as T
23
23
24−#if MIN_VERSION_base(4,8,0)
24+#if !MIN_VERSION_base(4,8,0)
25−import Prelude hiding (Word)
26−#else
27import Control.Applicative
25import Control.Applicative
28#endif
26#endif
29
27
30−-- |Turns a an input Word List in into CamelCase. Returns the CamelCase String.
28+-- | Turn an input word list in into CamelCase.
31--
29--
30+-- >>> foo <- SomeWord <$> mkWord "foo"
31+-- >>> bar <- SomeWord <$> mkAcronym "bar"
32+-- >>> bazz <- SomeWord <$> mkAcronym "bazz"
32−-- >>> camelize [ Word "foo", Acronym "bar", Word "bazz" ]
33+-- >>> camelize [foo,bar,bazz]
33-- "FoobarBazz"
34-- "FoobarBazz"
34camelize
35camelize
35− :: [Word] -- ^ Input Words to separate with underscores
36+ :: [SomeWord] -- ^ Input words
36− -> Text -- ^ The camelized 'Text'
37+ -> Text -- ^ The camelized 'Text'
37camelize = camelizeCustom True
38camelize = camelizeCustom True
38−{-# INLINE camelize #-}
39
39
40−-- |Turns an input Word List into a CamelCase String.
40+-- | Turn an input word list into a CamelCase String.
41--
41--
42+-- >>> foo <- SomeWord <$> mkWord "foo"
43+-- >>> bar <- SomeWord <$> mkAcronym "bar"
44+-- >>> bazz <- SomeWord <$> mkAcronym "bazz"
42−-- >>> camelizeCustom False [ Word "foo", Acronym "bar", Word "bazz" ]
45+-- >>> camelizeCustom False [foo,bar,bazz]
43-- "foobarBazz"
46-- "foobarBazz"
44camelizeCustom
47camelizeCustom
45− :: Bool -- ^ Whether to capitalize the first character in the output String
48+ :: Bool -- ^ Whether to capitalize the first character in the output String
46− -> [Word] -- ^ The input Words
49+ -> [SomeWord] -- ^ The input Words
47− -> Text -- ^ The camelized 'Text'
50+ -> Text -- ^ The camelized 'Text'
48camelizeCustom _ [] = ""
51camelizeCustom _ [] = ""
49camelizeCustom c (x:xs) = T.concat $
52camelizeCustom c (x:xs) = T.concat $
50− mapWord (if c then T.toTitle else T.toLower) x : (mapWord T.toTitle <$> xs)
53+ unSomeWord (if c then T.toTitle else T.toLower) x : (unSomeWord T.toTitle <$> xs)
51−{-# INLINE camelizeCustom #-}
Text/Inflections/Dasherize.hs+12−13
@@ -9,25 +9,24 @@
9--
9--
10-- Conversion to dasherized phrases.
10-- Conversion to dasherized phrases.
11
11
12−{-# LANGUAGE CPP #-}
13{-# LANGUAGE OverloadedStrings #-}
12{-# LANGUAGE OverloadedStrings #-}
14
13
15−module Text.Inflections.Dasherize ( dasherize ) where
14+module Text.Inflections.Dasherize
15+ ( dasherize )
16+where
16
17
17import Data.Text (Text)
18import Data.Text (Text)
18−import Text.Inflections.Parse.Types
19+import Text.Inflections.Types
19import qualified Data.Text as T
20import qualified Data.Text as T
20
21
21−#if MIN_VERSION_base(4,8,0)
22−import Prelude hiding (Word)
23−#endif
24−
25−-- | Replaces underscores in a snake_cased string with dashes (hyphens).
22+-- | Produce a string with words separated by dashes (hyphens).
26--
23--
24+-- >>> foo <- SomeWord <$> mkWord "foo"
25+-- >>> bar <- SomeWord <$> mkAcronym "bar"
26+-- >>> bazz <- SomeWord <$> mkWord "bazz"
27−-- >>> dasherize [ Word "foo", Acronym "bar", Word "bazz" ]
27+-- >>> dasherize [foo,bar,bazz]
28-- "foo-bar-bazz"
28-- "foo-bar-bazz"
29dasherize
29dasherize
30− :: [Word] -- ^ Input Words to separate with dashes
30+ :: [SomeWord] -- ^ Input words to separate with dashes
31− -> Text -- ^ The dasherized String
31+ -> Text -- ^ The dasherized 'Text'
32−dasherize = T.intercalate "-" . fmap (mapWord T.toLower)
32+dasherize = T.intercalate "-" . fmap (unSomeWord T.toLower)
33−{-# INLINE dasherize #-}
Text/Inflections/Data.hs+11−5
@@ -9,19 +9,25 @@
9--
9--
10-- Auxiliary data used in the library.
10-- Auxiliary data used in the library.
11
11
12−module Text.Inflections.Data where
12+module Text.Inflections.Data
13+ ( Transliterations
14+ , defaultTransliterations )
15+where
13
16
14import Data.HashMap.Strict (HashMap)
17import Data.HashMap.Strict (HashMap)
15import qualified Data.HashMap.Strict as M
18import qualified Data.HashMap.Strict as M
16
19
17−-- |A 'Data.Map.Map' containing mappings from international characters to
20+-- | A 'HashMap' containing mappings from international characters to
18-- sequences approximating these characters within the ASCII range.
21-- sequences approximating these characters within the ASCII range.
19type Transliterations = HashMap Char String
22type Transliterations = HashMap Char String
20
23
21−-- |These default transliterations stolen from the Ruby i18n library - see
24+-- | These default transliterations are stolen from the Ruby i18n library -
25+-- see
22-- <https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/transliterator.rb#L41:L69>.
26-- <https://github.com/svenfuchs/i18n/blob/master/lib/i18n/backend/transliterator.rb#L41:L69>.
27+--
28+-- NOTE: before version 0.3.0.0 this was called @defaultMap@.
23−defaultMap :: Transliterations
29+defaultTransliterations :: Transliterations
24−defaultMap = M.fromList [
30+defaultTransliterations = M.fromList [
25 ('À', "A"), ('Á', "A"), ('Â', "A"), ('Ã', "A"), ('Ä', "A"), ('Å', "A"),
31 ('À', "A"), ('Á', "A"), ('Â', "A"), ('Ã', "A"), ('Ä', "A"), ('Å', "A"),
26 ('Æ', "AE"), ('Ç', "C"), ('È', "E"), ('É', "E"), ('Ê', "E"), ('Ë', "E"),
32 ('Æ', "AE"), ('Ç', "C"), ('È', "E"), ('É', "E"), ('Ê', "E"), ('Ë', "E"),
27 ('Ì', "I"), ('Í', "I"), ('Î', "I"), ('Ï', "I"), ('Ð', "D"), ('Ñ', "N"),
33 ('Ì', "I"), ('Í', "I"), ('Î', "I"), ('Ï', "I"), ('Ð', "D"), ('Ñ', "N"),
Text/Inflections/Humanize.hs+10−10
@@ -17,26 +17,26 @@ module Text.Inflections.Humanize
17where
17where
18
18
19import Data.Text (Text)
19import Data.Text (Text)
20−import Text.Inflections.Parse.Types
20+import Text.Inflections.Types
21import qualified Data.Text as T
21import qualified Data.Text as T
22
22
23−#if MIN_VERSION_base(4,8,0)
23+#if !MIN_VERSION_base(4,8,0)
24−import Prelude hiding (Word)
25−#else
26import Control.Applicative
24import Control.Applicative
27#endif
25#endif
28
26
29−-- |Capitalizes the first word and turns underscores into spaces. Like
27+-- | Capitalize the first word and separate words with spaces. Like
30-- 'Text.Inflections.Titleize.titleize', this is meant for creating pretty
28-- 'Text.Inflections.Titleize.titleize', this is meant for creating pretty
31-- output.
29-- output.
32--
30--
31+-- >>> foo <- SomeWord <$> mkWord "foo"
32+-- >>> bar <- SomeWord <$> mkAcronym "bar"
33+-- >>> bazz <- SomeWord <$> mkWord "bazz"
33−-- >>> humanize [ Word "foo", Acronym "bar", Word "bazz" ]
34+-- >>> humanize [foo,bar,bazz]
34-- "Foo bar bazz"
35-- "Foo bar bazz"
35humanize
36humanize
36− :: [Word] -- ^ List of Words, first of which will be capitalized
37+ :: [SomeWord] -- ^ List of words, first of which will be capitalized
37− -> Text -- ^ The humanized output
38+ -> Text -- ^ The humanized output
38humanize xs' =
39humanize xs' =
39− case mapWord (T.replace "_" " ") <$> xs' of
40+ case unSomeWord (T.replace "_" " ") <$> xs' of
40 [] -> ""
41 [] -> ""
41 (x:xs) -> T.unwords $ T.toTitle x : (T.toLower <$> xs)
42 (x:xs) -> T.unwords $ T.toTitle x : (T.toLower <$> xs)
42−{-# INLINE humanize #-}
Text/Inflections/Ordinal.hs+14−15
@@ -12,14 +12,26 @@
12{-# LANGUAGE OverloadedStrings #-}
12{-# LANGUAGE OverloadedStrings #-}
13
13
14module Text.Inflections.Ordinal
14module Text.Inflections.Ordinal
15− ( ordinal
15+ ( ordinalize
16− , ordinalize )
16+ , ordinal )
17where
17where
18
18
19import Data.Monoid ((<>))
19import Data.Monoid ((<>))
20import Data.Text (Text)
20import Data.Text (Text)
21import qualified Data.Text as T
21import qualified Data.Text as T
22
22
23+-- |Turns a number into an ordinal string used to denote the position in an
24+-- ordered sequence such as 1st, 2nd, 3rd, 4th.
25+--
26+-- >>> ordinalize 1
27+-- "1st"
28+-- >>> ordinalize 2
29+-- "2nd"
30+-- >>> ordinalize 10
31+-- "10th"
32+ordinalize :: (Integral a, Show a) => a -> Text
33+ordinalize n = T.pack (show n) <> ordinal n
34+
23-- |Returns the suffix that should be added to a number to denote the position
35-- |Returns the suffix that should be added to a number to denote the position
24-- in an ordered sequence such as 1st, 2nd, 3rd, 4th.
36-- in an ordered sequence such as 1st, 2nd, 3rd, 4th.
25--
37--
@@ -39,16 +51,3 @@ ordinal number
39 where abs_number = abs number
51 where abs_number = abs number
40 remainder10 = abs_number `mod` 10
52 remainder10 = abs_number `mod` 10
41 remainder100 = abs_number `mod` 100
53 remainder100 = abs_number `mod` 100
42−
43−-- |Turns a number into an ordinal string used to denote the position in an
44−-- ordered sequence such as 1st, 2nd, 3rd, 4th.
45−--
46−-- >>> ordinalize 1
47−-- "1st"
48−-- >>> ordinalize 2
49−-- "2nd"
50−-- >>> ordinalize 10
51−-- "10th"
52−ordinalize :: (Integral a, Show a) => a -> Text
53−ordinalize n = T.pack (show n) <> ordinal n
54−{-# INLINE ordinalize #-}
Text/Inflections/Parameterize.hs+4−4
@@ -27,13 +27,13 @@ import qualified Data.Text as T
27import Control.Applicative
27import Control.Applicative
28#endif
28#endif
29
29
30−-- |Replaces special characters in a string so that it may be used as part of a
30+-- | Replace special characters in a string so that it may be used as part
31−-- 'pretty' URL. Uses the default transliterations in this library.
31+-- of a 'pretty' URL. Uses the 'defaultTransliterations'.
32parameterize :: Text -> Text
32parameterize :: Text -> Text
33−parameterize = parameterizeCustom defaultMap
33+parameterize = parameterizeCustom defaultTransliterations
34{-# INLINE parameterize #-}
34{-# INLINE parameterize #-}
35
35
36−-- |Transliterate 'Text' with a custom transliteration table.
36+-- | Transliterate 'Text' with a custom transliteration table.
37parameterizeCustom :: Transliterations -> Text -> Text
37parameterizeCustom :: Transliterations -> Text -> Text
38parameterizeCustom m txt = (T.intercalate "-" . T.words) (T.unfoldr f ("", txt))
38parameterizeCustom m txt = (T.intercalate "-" . T.words) (T.unfoldr f ("", txt))
39 where
39 where
Text/Inflections/Parse/Acronym.hsdeleted−32
@@ -1,32 +0,0 @@
1−-- |
2−-- Module : Text.Inflections.Parse.Acronym
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 acronyms.
11−
12−{-# LANGUAGE CPP #-}
13−
14−module Text.Inflections.Parse.Acronym
15− ( acronym )
16−where
17−
18−import Data.Text (Text)
19−import Text.Inflections.Parse.Types
20−import Text.Megaparsec
21−import Text.Megaparsec.Text
22−import qualified Data.Text as T
23−
24−#if MIN_VERSION_base(4,8,0)
25−import Prelude hiding (Word)
26−#endif
27−
28−-- | Parser that accepts a 'Text' from given collection and turns it into
29−-- an 'Acronym'.
30−acronym :: [Text] -> Parser Word
31−acronym = fmap (Acronym . T.pack) . choice . fmap (string . T.unpack)
32−{-# INLINE acronym #-}
Text/Inflections/Parse/CamelCase.hs+36−17
@@ -10,15 +10,16 @@
10-- Parser for camel case “symbols”.
10-- Parser for camel case “symbols”.
11
11
12{-# LANGUAGE CPP #-}
12{-# LANGUAGE CPP #-}
13+{-# LANGUAGE DataKinds #-}
13{-# LANGUAGE OverloadedStrings #-}
14{-# LANGUAGE OverloadedStrings #-}
14
15
15module Text.Inflections.Parse.CamelCase
16module Text.Inflections.Parse.CamelCase
16 ( parseCamelCase )
17 ( parseCamelCase )
17where
18where
18
19
20+import Control.Applicative
19import Data.Text (Text)
21import Data.Text (Text)
20−import Text.Inflections.Parse.Acronym (acronym)
22+import Text.Inflections.Types
21−import Text.Inflections.Parse.Types (Word(..))
22import Text.Megaparsec
23import Text.Megaparsec
23import Text.Megaparsec.Text
24import Text.Megaparsec.Text
24import qualified Data.Text as T
25import qualified Data.Text as T
@@ -26,31 +27,49 @@ import qualified Data.Text as T
26#if MIN_VERSION_base(4,8,0)
27#if MIN_VERSION_base(4,8,0)
27import Prelude hiding (Word)
28import Prelude hiding (Word)
28#else
29#else
29−import Control.Applicative
30+import Data.Foldable
31+import Prelude hiding (elem)
30#endif
32#endif
31
33
32−-- |Parse a CamelCase string.
34+-- | Parse a CamelCase string.
33--
35--
36+-- >>> bar <- mkAcronym "bar"
34−-- >>> parseCamelCase ["Bar"] "FooBarBazz"
37+-- >>> parseCamelCase [bar] "FooBarBazz"
35-- Right [Word "Foo",Acronym "Bar",Word "Bazz"]
38-- Right [Word "Foo",Acronym "Bar",Word "Bazz"]
39+--
36-- >>> parseCamelCase [] "foo_bar_bazz"
40-- >>> parseCamelCase [] "foo_bar_bazz"
37−-- Left "(unknown)" (line 1, column 4):
41+-- 1:4:
38-- unexpected '_'
42-- unexpected '_'
43+-- expecting end of input, lowercase letter, or uppercase letter
39−parseCamelCase
44+parseCamelCase :: (Foldable f, Functor f)
40− :: [Text] -- ^ Collection of acronyms
45+ => f (Word 'Acronym) -- ^ Collection of acronyms
41− -> Text -- ^ Input
46+ -> Text -- ^ Input
42− -> Either (ParseError Char Dec) [Word] -- ^ Result of parsing
47+ -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
43parseCamelCase acronyms = parse (parser acronyms) ""
48parseCamelCase acronyms = parse (parser acronyms) ""
44
49
45−parser
50+parser :: (Foldable f, Functor f)
46− :: [Text] -- ^ Collection of acronyms
51+ => f (Word 'Acronym) -- ^ Collection of acronyms
47− -> Parser [Word] -- ^ CamelCase parser
52+ -> Parser [SomeWord] -- ^ CamelCase parser
48−parser acronyms = many (acronym acronyms <|> word) <* eof
53+parser acronyms = many (a <|> n) <* eof
54+ where
55+ n = SomeWord <$> word
56+ a = SomeWord <$> acronym acronyms
57+
58+acronym :: (Foldable f, Functor f)
59+ => f (Word 'Acronym)
60+ -> Parser (Word 'Acronym)
61+acronym acronyms = do
62+ x <- T.pack <$> choice (string . T.unpack . unWord <$> acronyms)
63+ case mkAcronym x of
64+ Nothing -> empty -- cannot happen if the system is sound
65+ Just acr -> return acr
49−{-# INLINE parser #-}
66+{-# INLINE acronym #-}
50
67
51−word :: Parser Word
68+word :: Parser (Word 'Normal)
52word = do
69word = do
53 firstChar <- upperChar <|> lowerChar
70 firstChar <- upperChar <|> lowerChar
54 restChars <- many $ lowerChar <|> digitChar
71 restChars <- many $ lowerChar <|> digitChar
55− return . Word . T.pack $ firstChar : restChars
72+ case (mkWord . T.pack) (firstChar : restChars) of
73+ Nothing -> empty -- cannot happen if the system is sound
74+ Just wrd -> return wrd
56{-# INLINE word #-}
75{-# INLINE word #-}
Text/Inflections/Parse/SnakeCase.hs+28−21
@@ -9,15 +9,16 @@
9--
9--
10-- Parser for snake case “symbols”.
10-- Parser for snake case “symbols”.
11
11
12−{-# LANGUAGE CPP #-}
12+{-# LANGUAGE CPP #-}
13+{-# LANGUAGE DataKinds #-}
13
14
14module Text.Inflections.Parse.SnakeCase
15module Text.Inflections.Parse.SnakeCase
15 ( parseSnakeCase )
16 ( parseSnakeCase )
16where
17where
17
18
19+import Control.Applicative
18import Data.Text (Text)
20import Data.Text (Text)
19−import Text.Inflections.Parse.Acronym (acronym)
21+import Text.Inflections.Types
20−import Text.Inflections.Parse.Types (Word(..))
21import Text.Megaparsec
22import Text.Megaparsec
22import Text.Megaparsec.Text
23import Text.Megaparsec.Text
23import qualified Data.Text as T
24import qualified Data.Text as T
@@ -25,31 +26,37 @@ import qualified Data.Text as T
25#if MIN_VERSION_base(4,8,0)
26#if MIN_VERSION_base(4,8,0)
26import Prelude hiding (Word)
27import Prelude hiding (Word)
27#else
28#else
28−import Control.Applicative
29+import Data.Foldable
30+import Prelude hiding (elem)
29#endif
31#endif
30
32
31−-- |Parses a snake_case string.
33+-- | Parse a snake_case string.
32--
34--
35+-- >>> bar <- mkAcronym "bar"
33−-- >>> parseSnakeCase ["bar"] "foo_bar_bazz"
36+-- >>> parseSnakeCase [bar] "foo_bar_bazz"
34-- Right [Word "foo",Acronym "bar",Word "bazz"]
37-- Right [Word "foo",Acronym "bar",Word "bazz"]
38+--
35-- >>> parseSnakeCase [] "fooBarBazz"
39-- >>> parseSnakeCase [] "fooBarBazz"
36−-- Left "(unknown)" (line 1, column 4):
40+-- 1:4:
37-- unexpected 'B'
41-- unexpected 'B'
42+-- expecting '_', end of input, or lowercase letter
38−parseSnakeCase
43+parseSnakeCase :: (Foldable f, Functor f)
39− :: [Text] -- ^ Collection of acronyms
44+ => f (Word 'Acronym) -- ^ Collection of acronyms
40 -> Text -- ^ Input
45 -> Text -- ^ Input
41− -> Either (ParseError Char Dec) [Word] -- ^ Result of parsing
46+ -> Either (ParseError Char Dec) [SomeWord] -- ^ Result of parsing
42parseSnakeCase acronyms = parse (parser acronyms) ""
47parseSnakeCase acronyms = parse (parser acronyms) ""
43
48
44−parser
49+parser :: (Foldable f, Functor f)
45− :: [Text]
50+ => f (Word 'Acronym)
46− -> Parser [Word]
51+ -> Parser [SomeWord]
47−parser acronyms = do
52+parser acronyms = (pWord acronyms `sepBy` char '_') <* eof
48− ws <- (acronym acronyms <|> word) `sepBy` char '_'
49− eof
50− return ws
51−{-# INLINE parser #-}
52
53
54+pWord :: (Foldable f, Functor f)
55+ => f (Word 'Acronym)
53−word :: Parser Word
56+ -> Parser SomeWord
54−word = Word . T.pack <$> (some lowerChar <|> some digitChar)
57+pWord acronyms = do
55−{-# INLINE word #-}
58+ let acs = unWord <$> acronyms
59+ r <- T.pack <$> some alphaNumChar
60+ if r `elem` acs
61+ then maybe empty (return . SomeWord) (mkAcronym r)
62+ else maybe empty (return . SomeWord) (mkWord r)
Text/Inflections/Parse/Types.hsdeleted−54
@@ -1,54 +0,0 @@
1−-- |
2−-- Module : Text.Inflections.Parse.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.
11−
12−{-# LANGUAGE CPP #-}
13−{-# LANGUAGE DeriveDataTypeable #-}
14−
15−module Text.Inflections.Parse.Types
16− ( Word (..) -- FIXME we should not export the constructor
17− , unWord
18− , mapWord
19− , InflectionException (..) )
20−where
21−
22−import Control.Monad.Catch (Exception)
23−import Data.Text (Text)
24−import Data.Typeable (Typeable)
25−import Text.Megaparsec
26−
27−#if MIN_VERSION_base(4,8,0)
28−import Prelude hiding (Word)
29−#endif
30−
31−-- | A 'String' that should be kept whole through applied inflections
32−data Word
33− = Word Text -- ^ A word that may be transformed by inflection
34− | Acronym Text -- ^ A word that may not be transformed by inflections
35− deriving (Show, Eq)
36−
37−-- | Get a 'Text' value from 'Word'.
38−unWord :: Word -> Text
39−unWord (Word s) = s
40−unWord (Acronym s) = s
41−{-# INLINE unWord #-}
42−
43−-- | Apply 'Text' transforming function to a 'Word' unless it's a 'Acronym'.
44−mapWord :: (Text -> Text) -> Word -> Text
45−mapWord f (Word s) = f s
46−mapWord _ (Acronym s) = s
47−{-# INLINE mapWord #-}
48−
49−-- | The exceptions that is thrown when parsing of input fails.
50−
51−data InflectionException = InflectionParsingFailed (ParseError Char Dec)
52− deriving (Eq, Show, Typeable)
53−
54−instance Exception InflectionException
Text/Inflections/Titleize.hs+8−12
@@ -9,26 +9,22 @@
9--
9--
10-- Conversion to titleized phrases.
10-- Conversion to titleized phrases.
11
11
12−{-# LANGUAGE CPP #-}
13−
14module Text.Inflections.Titleize
12module Text.Inflections.Titleize
15 ( titleize )
13 ( titleize )
16where
14where
17
15
18import Data.Text (Text)
16import Data.Text (Text)
19−import Text.Inflections.Parse.Types
17+import Text.Inflections.Types
20import qualified Data.Text as T
18import qualified Data.Text as T
21
19
22−#if MIN_VERSION_base(4,8,0)
23−import Prelude hiding (Word)
24−#endif
25−
26-- | Capitalize all the Words in the input list.
20-- | Capitalize all the Words in the input list.
27--
21--
22+-- >>> foo <- SomeWord <$> mkWord "foo"
23+-- >>> bar <- SomeWord <$> mkAcronym "bar"
24+-- >>> bazz <- SomeWord <$> mkWord "bazz"
28−-- >>> titleize [ Word "foo", Acronym "bar", Word "bazz" ]
25+-- >>> titleize [foo,bar,bazz]
29-- "Foo bar Bazz"
26-- "Foo bar Bazz"
30titleize
27titleize
31− :: [Word] -- ^ List of Words, first of which will be capitalized
28+ :: [SomeWord] -- ^ List of words, first of which will be capitalized
32− -> Text -- ^ The titleized String
29+ -> Text -- ^ The titleized 'Text'
33−titleize = T.unwords . fmap (mapWord T.toTitle)
30+titleize = T.unwords . fmap (unSomeWord T.toTitle)
34−{-# INLINE titleize #-}
Text/Inflections/Transliterate.hs+3−4
@@ -27,15 +27,14 @@ import qualified Data.Text as T
27import Control.Applicative
27import Control.Applicative
28#endif
28#endif
29
29
30−-- |Returns a String after default approximations for changing Unicode
30+-- | Returns a 'Text' after default approximations for changing Unicode
31-- characters to a valid ASCII range are applied. If you want to supplement
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
32-- the default approximations with your own, you should use the
33-- 'transliterateCustom' function instead of 'transliterate'.
33-- 'transliterateCustom' function instead of 'transliterate'.
34transliterate :: Text -> Text
34transliterate :: Text -> Text
35−transliterate = transliterateCustom "?" defaultMap
35+transliterate = transliterateCustom "?" defaultTransliterations
36−{-# INLINE transliterate #-}
37
36
38−-- |Returns a String after default approximations for changing Unicode
37+-- | Returns a 'Text' after default approximations for changing Unicode
39-- characters to a valid ASCII range are applied.
38-- characters to a valid ASCII range are applied.
40transliterateCustom
39transliterateCustom
41 :: String -- ^ The default replacement
40 :: String -- ^ The default replacement
Text/Inflections/Types.hsnew file+143
@@ -0,0 +1,143 @@
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.Text (Text)
37+import Data.Typeable (Typeable)
38+import GHC.Generics (Generic)
39+import Text.Megaparsec
40+import qualified Data.Text as T
41+
42+#if MIN_VERSION_base(4,8,0)
43+import Prelude hiding (Word)
44+#endif
45+
46+-- | Create a word from given 'Text'. The input should consist of only
47+-- alpha-numeric characters (no white spaces or punctuation)
48+-- 'InflectionInvalidWord' will be thrown.
49+--
50+-- /since 0.3.0.0/
51+
52+mkWord :: MonadThrow m => Text -> m (Word 'Normal)
53+mkWord txt =
54+ if T.all isAlphaNum txt
55+ then return (Word txt)
56+ else throwM (InflectionInvalidWord txt)
57+
58+-- | Create an acronym from given 'Text'. The input should consist of only
59+-- alpha-numeric characters 'InflectionInvalidAcronym' will be thrown.
60+-- Acronym is different from normal word by that it may not be transformed
61+-- by inflections (also see 'unSomeWord').
62+--
63+-- /since 0.3.0.0/
64+
65+mkAcronym :: MonadThrow m => Text -> m (Word 'Acronym)
66+mkAcronym txt =
67+ if T.all isAlphaNum txt
68+ then return (Word txt)
69+ else throwM (InflectionInvalidAcronym txt)
70+
71+-- | A 'Text' value that should be kept whole through applied inflections.
72+
73+data Word (t :: WordType) = Word Text
74+ deriving (Eq, Ord)
75+
76+instance Show (Word 'Normal) where
77+ show (Word x) = "Word " ++ show x
78+
79+instance Show (Word 'Acronym) where
80+ show (Word x) = "Acronym " ++ show x
81+
82+-- | A type-level tag for words.
83+--
84+-- /since 0.3.0.0/
85+
86+data WordType = Normal | Acronym
87+
88+-- | Get a 'Text' value from 'Word'.
89+--
90+-- /since 0.3.0.0/
91+
92+unWord :: Word t -> Text
93+unWord (Word s) = s
94+
95+-- | An existential wrapper that allows to keep words and acronyms in single
96+-- list for example. The only thing that receiver of 'SomeWord' can do is to
97+-- apply 'unWord' on it, of course. This is faciliated by 'unSomeWord'.
98+--
99+-- /since 0.3.0.0/
100+
101+data SomeWord where
102+ SomeWord :: (Transformable (Word t), Show (Word t)) => Word t -> SomeWord
103+ -- NOTE The constraint is only needed because GHC is not smart enough
104+ -- (yet?) to figure out that t cannot be anything other than Normal and
105+ -- Acronym and thus all cases are already covered by the instances
106+ -- provided below.
107+
108+instance Show SomeWord where
109+ show (SomeWord w) = show w
110+
111+instance Eq SomeWord where
112+ x == y = unSomeWord id x == unSomeWord id y
113+
114+-- | Extract 'Text' from 'SomeWord' and apply given function only if the
115+-- word inside wasn't an acronym.
116+--
117+-- /since 0.3.0.0/
118+
119+unSomeWord :: (Text -> Text) -> SomeWord -> Text
120+unSomeWord f (SomeWord w) = transform f w
121+
122+-- | Non public stuff.
123+
124+class Transformable a where
125+ transform :: (Text -> Text) -> a -> Text
126+
127+instance Transformable (Word 'Normal) where
128+ transform f = f . unWord
129+
130+instance Transformable (Word 'Acronym) where
131+ transform _ = unWord
132+
133+-- | The exceptions that is thrown when parsing of input fails.
134+--
135+-- /since 0.3.0.0/
136+
137+data InflectionException
138+ = InflectionParsingFailed (ParseError Char Dec)
139+ | InflectionInvalidWord Text
140+ | InflectionInvalidAcronym Text
141+ deriving (Eq, Show, Typeable, Data, Generic)
142+
143+instance Exception InflectionException
Text/Inflections/Underscore.hs+9−12
@@ -9,7 +9,6 @@
9--
9--
10-- Conversion to phrases separated by underscores.
10-- Conversion to phrases separated by underscores.
11
11
12−{-# LANGUAGE CPP #-}
13{-# LANGUAGE OverloadedStrings #-}
12{-# LANGUAGE OverloadedStrings #-}
14
13
15module Text.Inflections.Underscore
14module Text.Inflections.Underscore
@@ -17,19 +16,17 @@ module Text.Inflections.Underscore
17where
16where
18
17
19import Data.Text (Text)
18import Data.Text (Text)
20−import Text.Inflections.Parse.Types
19+import Text.Inflections.Types
21import qualified Data.Text as T
20import qualified Data.Text as T
22
21
23−#if MIN_VERSION_base(4,8,0)
22+-- | Separate given words by underscores.
24−import Prelude hiding (Word)
25−#endif
26−
27−-- |Turns a CamelCase string into an underscore_separated 'Text'.
28--
23--
24+-- >>> foo <- SomeWord <$> mkWord "foo"
25+-- >>> bar <- SomeWord <$> mkAcronym "bar"
26+-- >>> bazz <- SomeWord <$> mkWord "bazz"
29−-- >>> underscore [ Word "foo", Acronym "bar", Word "bazz" ]
27+-- >>> underscore [foo,bar,bazz]
30-- "foo_bar_bazz"
28-- "foo_bar_bazz"
31underscore
29underscore
32− :: [Word] -- ^ Input Words to separate with underscores
30+ :: [SomeWord] -- ^ Input words to separate with underscores
33− -> Text -- ^ The underscored String
31+ -> Text -- ^ The underscored String
34−underscore = T.intercalate "_" . fmap (mapWord T.toLower)
32+underscore = T.intercalate "_" . fmap (unSomeWord T.toLower)
35−{-# INLINE underscore #-}
inflections.cabal+6−3
@@ -29,7 +29,6 @@ flag dev
29
29
30library
30library
31 exposed-modules: Text.Inflections
31 exposed-modules: Text.Inflections
32− , Text.Inflections.Parse.Types
33
32
34 other-modules: Text.Inflections.Data
33 other-modules: Text.Inflections.Data
35 , Text.Inflections.Camelize
34 , Text.Inflections.Camelize
@@ -37,11 +36,11 @@ library
37 , Text.Inflections.Humanize
36 , Text.Inflections.Humanize
38 , Text.Inflections.Ordinal
37 , Text.Inflections.Ordinal
39 , Text.Inflections.Parameterize
38 , Text.Inflections.Parameterize
40− , Text.Inflections.Parse.Acronym
41 , Text.Inflections.Parse.CamelCase
39 , Text.Inflections.Parse.CamelCase
42 , Text.Inflections.Parse.SnakeCase
40 , Text.Inflections.Parse.SnakeCase
43 , Text.Inflections.Titleize
41 , Text.Inflections.Titleize
44 , Text.Inflections.Transliterate
42 , Text.Inflections.Transliterate
43+ , Text.Inflections.Types
45 , Text.Inflections.Underscore
44 , Text.Inflections.Underscore
46
45
47 if flag(dev)
46 if flag(dev)
@@ -63,7 +62,8 @@ test-suite test
63 , QuickCheck >= 2.7.6 && < 3.0
62 , QuickCheck >= 2.7.6 && < 3.0
64 , base >= 4.6 && < 5.0
63 , base >= 4.6 && < 5.0
65 , hspec >= 2.0 && < 3.0
64 , hspec >= 2.0 && < 3.0
65+ , hspec-megaparsec >= 0.3 && < 0.4
66− , megaparsec >= 5.0 && < 6.0
66+ , megaparsec >= 5.1 && < 6.0
67 , text >= 0.2 && < 1.3
67 , text >= 0.2 && < 1.3
68 if flag(dev)
68 if flag(dev)
69 ghc-options: -Wall -Werror
69 ghc-options: -Wall -Werror
@@ -74,7 +74,10 @@ test-suite test
74 , Text.Inflections.HumanizeSpec
74 , Text.Inflections.HumanizeSpec
75 , Text.Inflections.OrdinalSpec
75 , Text.Inflections.OrdinalSpec
76 , Text.Inflections.ParametrizeSpec
76 , Text.Inflections.ParametrizeSpec
77+ , Text.Inflections.Parse.CamelCaseSpec
78+ , Text.Inflections.Parse.SnakeCaseSpec
77 , Text.Inflections.TitleizeSpec
79 , Text.Inflections.TitleizeSpec
78 , Text.Inflections.TransliterateSpec
80 , Text.Inflections.TransliterateSpec
81+ , Text.Inflections.TypesSpec
79 , Text.Inflections.UnderscoreSpec
82 , Text.Inflections.UnderscoreSpec
80 , Text.InflectionsSpec
83 , Text.InflectionsSpec
stack.yaml+3
@@ -1,3 +1,6 @@
1resolver: lts-7.13
1resolver: lts-7.13
2packages:
2packages:
3- '.'
3- '.'
4+extra-deps:
5+- hspec-megaparsec-0.3.0
6+- megaparsec-5.1.2
test/Text/Inflections/DasherizeSpec.hs+10−4
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
2
3
3module Text.Inflections.DasherizeSpec
4module Text.Inflections.DasherizeSpec
@@ -6,10 +7,15 @@ where
6
7
7import Test.Hspec
8import Test.Hspec
8
9
9−import Text.Inflections (dasherize)
10+import Text.Inflections
10−import Text.Inflections.Parse.Types (Word (..))
11+
12+#if !MIN_VERSION_base(4,8,0)
13+import Control.Applicative
14+#endif
11
15
12spec :: Spec
16spec :: Spec
13spec = describe "dasherize" $
17spec = describe "dasherize" $
14− it "dasherizes a collection of words sentence" $
18+ it "dasherizes a collection of words sentence" $ do
19+ foo <- SomeWord <$> mkWord "foo"
20+ bar <- SomeWord <$> mkWord "bar"
15− dasherize [Word "foo", Word "bar"] `shouldBe` "foo-bar"
21+ dasherize [foo,bar] `shouldBe` "foo-bar"
test/Text/Inflections/HumanizeSpec.hs+18−8
@@ -1,17 +1,27 @@
1+{-# LANGUAGE CPP #-}
1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
2
3
3module Text.Inflections.HumanizeSpec (spec) where
4module Text.Inflections.HumanizeSpec (spec) where
4
5
5import Test.Hspec
6import Test.Hspec
6
7
7−import Text.Inflections (humanize)
8+import Text.Inflections
8−import Text.Inflections.Parse.Types (Word(..))
9+
10+#if !MIN_VERSION_base(4,8,0)
11+import Control.Applicative
12+#endif
9
13
10spec :: Spec
14spec :: Spec
11spec = describe "humazine" $ do
15spec = describe "humazine" $ do
12− it "converts snake case to a human-readable string" $
16+ it "converts snake case to a human-readable string" $ do
17+ employee <- SomeWord <$> mkWord "employee"
18+ salary <- SomeWord <$> mkWord "salary"
13− humanize [Word "employee", Word "salary"] `shouldBe` "Employee salary"
19+ humanize [employee,salary] `shouldBe` "Employee salary"
14− it "turns underscores into spaces" $
20+ it "turns underscores into spaces" $ do
21+ employee <- SomeWord <$> mkWord "employee"
22+ has <- SomeWord <$> mkWord "has"
23+ salary <- SomeWord <$> mkWord "salary"
15− humanize [Word "employee", Word "has_salary"] `shouldBe` "Employee has salary"
24+ humanize [employee, has, salary] `shouldBe` "Employee has salary"
16− it "capitalizes the first word of a sentence" $
25+ it "capitalizes the first word of a sentence" $ do
26+ underground <- SomeWord <$> mkWord "underground"
17− humanize [Word "underground"] `shouldBe` "Underground"
27+ humanize [underground] `shouldBe` "Underground"
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/TitleizeSpec.hs+12−6
@@ -1,15 +1,21 @@
1+{-# LANGUAGE CPP #-}
1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
2
3
3module Text.Inflections.TitleizeSpec (spec) where
4module Text.Inflections.TitleizeSpec (spec) where
4
5
5import Test.Hspec
6import Test.Hspec
7+import Text.Inflections
6
8
7−import Text.Inflections (titleize)
9+#if !MIN_VERSION_base(4,8,0)
8−import Text.Inflections.Parse.Types (Word(..))
10+import Control.Applicative
11+#endif
9
12
10spec :: Spec
13spec :: Spec
11spec = describe "titleize" $ do
14spec = describe "titleize" $ do
12− it "converts two words to title case" $
15+ it "converts two words to title case" $ do
16+ employee <- SomeWord <$> mkWord "Employee"
17+ salary <- SomeWord <$> mkWord "Salary"
13− titleize [Word "Employee", Word "Salary"] `shouldBe` "Employee Salary"
18+ titleize [employee,salary] `shouldBe` "Employee Salary"
14− it "converts one word to title case" $
19+ it "converts one word to title case" $ do
20+ underground <- SomeWord <$> mkWord "underground"
15− titleize [Word "underground"] `shouldBe` "Underground"
21+ titleize [underground] `shouldBe` "Underground"
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+10−4
@@ -1,12 +1,18 @@
1+{-# LANGUAGE CPP #-}
1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
2
3
3module Text.Inflections.UnderscoreSpec (spec) where
4module Text.Inflections.UnderscoreSpec (spec) where
4
5
5import Test.Hspec
6import Test.Hspec
6−import Text.Inflections (underscore)
7+import Text.Inflections
7−import Text.Inflections.Parse.Types (Word(..))
8+
9+#if !MIN_VERSION_base(4,8,0)
10+import Control.Applicative
11+#endif
8
12
9spec :: Spec
13spec :: Spec
10spec = describe "underscore" $
14spec = describe "underscore" $
11− it "converts a word list to snake case" $
15+ it "converts a word list to snake case" $ do
16+ test <- SomeWord <$> mkWord "test"
17+ this <- SomeWord <$> mkWord "this"
12− underscore [Word "test", Word "this"] `shouldBe` "test_this"
18+ underscore [test, this] `shouldBe` "test_this"
test/Text/InflectionsSpec.hs+22−7
@@ -3,22 +3,37 @@
3module Text.InflectionsSpec (spec) where
3module Text.InflectionsSpec (spec) where
4
4
5import Test.Hspec
5import Test.Hspec
6+import Test.QuickCheck
6−import Text.Inflections (toUnderscore, toDashed, toCamelCased)
7+import Text.Inflections
7
8
8spec :: Spec
9spec :: Spec
9spec = do
10spec = do
11+
10 describe "toUnderscore" $ do
12 describe "toUnderscore" $ do
11 it "converts camel case to snake case" $
13 it "converts camel case to snake case" $
12− toUnderscore "camelCasedText" `shouldReturn` "camel_cased_text"
14+ toUnderscore "camelCasedText" `shouldBe` Right "camel_cased_text"
13 it "converts camel case to snake case with numbers" $
15 it "converts camel case to snake case with numbers" $
14− toUnderscore "ipv4Address" `shouldReturn` "ipv4_address"
16+ toUnderscore "ipv4Address" `shouldBe` Right "ipv4_address"
17+
15 describe "toDashed" $
18 describe "toDashed" $
16 it "converts camel case to dashed" $
19 it "converts camel case to dashed" $
17− toDashed "camelCasedText" `shouldReturn` "camel-cased-text"
20+ toDashed "camelCasedText" `shouldBe` Right "camel-cased-text"
21+
18 describe "toCamelCased" $ do
22 describe "toCamelCased" $ do
19 context "when the first argument is False" $
23 context "when the first argument is False" $
20 it "converts snake case to camel case" $
24 it "converts snake case to camel case" $
21− toCamelCased False "underscored_text" `shouldReturn` "underscoredText"
25+ toCamelCased False "underscored_text" `shouldBe` Right "underscoredText"
22 context "when the first argument is True" $
26 context "when the first argument is True" $
23− it "converts snake case to camel case" $
27+ it "converts snake case to camel case with the first word capitalized" $
24− toCamelCased True "underscored_text" `shouldReturn` "UnderscoredText"
28+ toCamelCased True "underscored_text" `shouldBe` Right "UnderscoredText"
29+
30+ describe "betterThrow" $ do
31+ context "when given a parse error" $
32+ it "throws the correct exception" $
33+ property $ \err ->
34+ betterThrow (Left err) `shouldThrow`
35+ (== InflectionParsingFailed err)
36+ context "when given a value in Right" $
37+ it "returns the value" $
38+ property $ \x ->
39+ betterThrow (Right x) `shouldReturn` (x :: Int)