lib/exflect.ex

defmodule Exflect do
  alias Exflect.{Singularize, Pluralize, Shared, Word}

  @spec singularize(String.t(), keyword()) :: String.t()
  @doc """
  Singlarizes an English word.
  ```
  $ Exflect.singularize("leaves")
  => "leaf"
  ```

  Takes the option `match_style` if you want it to maintain the current whitespace/case.
  ```
  $ Exflect.singularize("  LEAVES  ", match_style: true)
  => "  LEAF  "
  ```
  """
  def singularize(text, opts \\ [match_style: false])

  def singularize("" <> text, match_style: true) do
    word = Word.new(text)

    %{word | text: uncountable?(word.text) || do_singularize(word.text)}
    |> to_string()
  end

  def singularize("" <> text, _) do
    text2 = downcase(text)
    uncountable?({text2, text}) || do_singularize({text2, text})
  end

  def singularize(val, opts), do: val |> to_string() |> singularize(opts)

  @spec pluralize(String.t(), keyword()) :: String.t()
  @doc """
  Pluralizes an English word.
  ```
  $ Exflect.pluralize("leaf")
  => "leaves"
  ```

  Takes the option `match_style` if you want it to maintain the current whitespace/case.
  ```
  $ Exflect.pluralize("  LEAF  ", match_style: true)
  => "  LEAVES  "
  ```
  """
  def pluralize(word, opts \\ [match_style: false])

  def pluralize("" <> text, match_style: true) do
    word = Word.new(text)

    %{word | text: uncountable?(word.text) || do_pluralize(word.text)}
    |> to_string()
  end

  def pluralize("" <> text, _) do
    text2 = downcase(text)
    uncountable?({text2, text}) || do_pluralize({text2, text})
  end

  def pluralize(word, opts), do: word |> to_string() |> pluralize(opts)

  @spec inflect(String.t(), pos_integer(), keyword()) :: String.t()
  @doc """
  Inflects the input word based on the the integer given.
  ```
  $ Exflect.inflect("leaf", 0)
  => "leaves"
  $ Exflect.inflect("leaf", 1)
  => "leaf"
  $ 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
      {{: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
      {{: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