defmodule Exflect do
alias Exflect.{Singularize, Pluralize, Word}
@spec singular?(String.t()) :: boolean()
@doc """
Returns true is the word is singular, else false.
"""
def singular?(string), do: Exflect.Detect.singular?(string)
@spec plural?(String.t()) :: boolean()
@doc """
Returns true is the word is plural, else false.
"""
def plural?(string), do: Exflect.Detect.plural?(string)
@spec singularize(String.t(), keyword()) :: String.t()
@doc """
Singlarizes an English word.
```elixir
iex> Exflect.singularize("leaves")
"leaf"
```
Takes two options:
`check`: `boolean` | default: `false`
`match_style`: `boolean` | default: `false`
Use the option `check`, if you're not sure about the form of the word you're passing in.
For performance reasons, to avoid a bunch of expensive regex call, by default the input is not checked to see if it's already singular.
This incurs about a 30x performance hit. ~40k ips vs ~1.2m ips unchecked.
```
iex> Exflect.singularize("bus")
"bu"
iex> Exflect.singularize("bus", check: true)
"bus"
```
Use the option `match_style` if you want it to maintain the current whitespace/case.
```elixir
iex> Exflect.singularize(" LEAVES ", match_style: true)
" LEAF "
```
"""
def singularize(text, opts \\ [match_style: false, check: false])
def singularize("" <> text, opts) do
check = Keyword.get(opts, :check, false)
match_style = Keyword.get(opts, :match_style, false)
cond do
check && Exflect.Detect.singular?(text) -> text
true -> unchecked_singularize(text, match_style)
end
end
def singularize(val, opts), do: val |> to_string() |> singularize(opts)
defp unchecked_singularize("" <> text, true) do
word = Word.new(text)
%{word | text: do_singularize(word.text)}
|> to_string()
end
defp unchecked_singularize("" <> text, _) do
text2 = downcase(text)
do_singularize({text, text2})
end
@spec pluralize(String.t(), keyword()) :: String.t()
@doc """
Pluralizes an English word.
```elixir
iex> Exflect.pluralize("leaf")
"leaves"
```
Takes two options:
`check`: `boolean` | default: `false`
`match_style`: `boolean` | default: `false`
Use the option `check`, if you're not sure about the form of the word you're passing in.
For performance reasons, to avoid a bunch of expensive regex call, by default the input is not checked to see if it's already plural.
This incurs about a 30x performance hit. ~40k ips vs ~1.2m ips unchecked.
```
iex> Exflect.pluralize("men")
"mens"
iex> Exflect.pluralize("men", check: true)
"men"
```
Use the option `match_style` if you want it to maintain the current whitespace/case.
```elixir
iex> Exflect.pluralize(" LEAF ", match_style: true)
" LEAVES "
```
"""
def pluralize(word, opts \\ [match_style: false, check: false])
def pluralize("" <> text, opts) do
check = Keyword.get(opts, :check, false)
match_style = Keyword.get(opts, :match_style, false)
cond do
check && Exflect.Detect.plural?(text) -> text
true -> unchecked_pluralize(text, match_style)
end
end
def pluralize(word, opts), do: word |> to_string() |> pluralize(opts)
defp unchecked_pluralize("" <> text, true) do
word = Word.new(text)
%{word | text: do_pluralize(word.text)}
|> to_string()
end
defp unchecked_pluralize("" <> text, _) do
text2 = downcase(text)
do_pluralize({text, text2})
end
@spec inflect(String.t(), pos_integer(), keyword()) :: String.t()
@doc """
Inflects the input word based on the the integer given.
```elixir
iex> Exflect.inflect("leaf", 0)
"leaves"
iex> Exflect.inflect("leaf", 1)
"leaf"
iex> Exflect.inflect("leaf", 2)
"leaves"
```
Also accepts the `match_style` option
"""
def inflect(word, n, opts \\ [match_style: false])
def inflect(word, n, opts) when n == 1, do: singularize(word, opts)
def inflect(word, n, opts) when is_number(n), do: pluralize(word, opts)
# The reason for tagging the match type is to always pick the highest quality one
###
# If the input is "French", then for pluralization the responses would be
# "french" -> {:default, "frenches"}
# "French" -> {:exception, "French"}
# Even though the exception text is identical to the input, it's actually the better choice because it comes from a more refined, specific match
# The ordering is: exception > match > default
###
# Exception: means there's a specific, custom rules for that word
# Match: means the word hit a match pattern
# Default: means the word got all the way to the end and the default rule is being applied(during pluralization the default is appending an "s").
defp do_singularize("" <> text), do: Singularize.match!(text)
defp do_singularize({text, text}), do: Singularize.match!(text)
defp do_singularize({text, text2}) do
case {Singularize.match(text), Singularize.match(text2)} do
{{_, text}, {_, text}} -> text
{{:exception, text}, _} -> text
{_, {:exception, text}} -> text
{{:uncountable, text}, _} -> text
{_, {:uncountable, text}} -> text
{{:match, text}, _} -> text
{_, {:match, text}} -> text
{{:default, text}, _} -> text
{_, {:default, text}} -> text
{{_, text}, {_, text2}} -> text || text2
end
end
defp do_pluralize("" <> text), do: Pluralize.match!(text)
defp do_pluralize({text, text}), do: Pluralize.match!(text)
defp do_pluralize({text, text2}) do
case {Pluralize.match(text), Pluralize.match(text2)} do
{{_, text}, {_, text}} -> text
{{:exception, text}, _} -> text
{_, {:exception, text}} -> text
{{:uncountable, text}, _} -> text
{_, {:uncountable, text}} -> text
{{:match, text}, _} -> text
{_, {:match, text}} -> text
{{:default, text}, _} -> text
{_, {:default, text}} -> text
{{_, text}, {_, text2}} -> text || text2
end
end
# defp uncountable?({text, text}), do: Shared.uncountable?(text)
# defp uncountable?({text, text2}), do: Shared.uncountable?(text) || Shared.uncountable?(text2)
# defp uncountable?("" <> text), do: Shared.uncountable?(text)
defp downcase(<<a, _::binary>> = string) when a in ?A..?Z,
do: String.downcase(string)
defp downcase(string), do: string
end