Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 144 additions & 127 deletions lib/inflex/pluralize.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
defmodule Inflex.Pluralize do
@moduledoc false
@default true

@uncountable [
"aircraft",
Expand Down Expand Up @@ -31,169 +30,187 @@ defmodule Inflex.Pluralize do
]

@irregular [
{~r/(alumn|cact|fung|radi|stimul|syllab)i/i, "\\1us"},
{~r/(alg|antenn|amoeb|larv|vertebr)ae/i, "\\1a"},
{~r/^(gen)era$/i, "\\1us"},
{~r/(pe)ople/i, "\\1rson"},
{~r/^(zombie)s$/i, "\\1"},
{~r/(g)eese/i, "\\1oose"},
{~r/(criteri)a/i, "\\1on"},
{~r/^(m)en$/i, "\\1an"},
{~r/^(echo)es/i, "\\1"},
{~r/^(hero)es/i, "\\1"},
{~r/^(potato)es/i, "\\1"},
{~r/^(tomato)es/i, "\\1"},
{~r/^(t)eeth/i, "\\1ooth"},
{~r/^(l)ice$/i, "\\1ouse"},
{~r/^(addend|bacteri|curricul|dat|memorand|quant)a$/i, "\\1um"},
{~r/^(di)ce/i, "\\1e"},
{~r/^(f)eet/i, "\\1oot"},
{~r/^(phenomen)a/i, "\\1on"}
{~s/(alumn|cact|fung|radi|stimul|syllab)i/, "i", "\\1us"},
{~s/(alg|antenn|amoeb|larv|vertebr)ae/, "i", "\\1a"},
{~s/^(gen)era$/, "i", "\\1us"},
{~s/(pe)ople/, "i", "\\1rson"},
{~s/^(zombie)s$/, "i", "\\1"},
{~s/(g)eese/, "i", "\\1oose"},
{~s/(criteri)a/, "i", "\\1on"},
{~s/^(m)en$/, "i", "\\1an"},
{~s/^(echo)es/, "i", "\\1"},
{~s/^(hero)es/, "i", "\\1"},
{~s/^(potato)es/, "i", "\\1"},
{~s/^(tomato)es/, "i", "\\1"},
{~s/^(t)eeth/, "i", "\\1ooth"},
{~s/^(l)ice$/, "i", "\\1ouse"},
{~s/^(addend|bacteri|curricul|dat|memorand|quant)a$/, "i", "\\1um"},
{~s/^(di)ce/, "i", "\\1e"},
{~s/^(f)eet/, "i", "\\1oot"},
{~s/^(phenomen)a/, "i", "\\1on"}
]

@plural_irregular [
{~r/(alumn|cact|fung|radi|stimul|syllab)us/i, "\\1i"},
{~r/(alg|antenn|amoeb|larv|vertebr)a/i, "\\1ae"},
{~r/^(gen)us$/i, "\\1era"},
{~r/(pe)rson$/i, "\\1ople"},
{~r/^(zombie)s$/i, "\\1"},
{~r/(g)oose$/i, "\\1eese"},
{~r/(criteri)on/i, "\\1a"},
{~r/^(men)$/i, "\\1"},
{~r/^(women)/i, "\\1"},
{~r/^(echo)$/i, "\\1es"},
{~r/^(hero)$/i, "\\1es"},
{~r/^(potato)/i, "\\1es"},
{~r/^(tomato)/i, "\\1es"},
{~r/^(t)ooth$/i, "\\1eeth"},
{~r/^(l)ouse$/i, "\\1ice"},
{~r/^(addend|bacteri|curricul|dat|memorand|quant)um$/i, "\\1a"},
{~r/^(di)e$/i, "\\1ce"},
{~r/^(f)oot$/i, "\\1eet"},
{~r/^(phenomen)on/i, "\\1a"}
{~s/(alumn|cact|fung|radi|stimul|syllab)us/, "i", "\\1i"},
{~s/(alg|antenn|amoeb|larv|vertebr)a/, "i", "\\1ae"},
{~s/^(gen)us$/, "i", "\\1era"},
{~s/(pe)rson$/, "i", "\\1ople"},
{~s/^(zombie)s$/, "i", "\\1"},
{~s/(g)oose$/, "i", "\\1eese"},
{~s/(criteri)on/, "i", "\\1a"},
{~s/^(men)$/, "i", "\\1"},
{~s/^(women)/, "i", "\\1"},
{~s/^(echo)$/, "i", "\\1es"},
{~s/^(hero)$/, "i", "\\1es"},
{~s/^(potato)/, "i", "\\1es"},
{~s/^(tomato)/, "i", "\\1es"},
{~s/^(t)ooth$/, "i", "\\1eeth"},
{~s/^(l)ouse$/, "i", "\\1ice"},
{~s/^(addend|bacteri|curricul|dat|memorand|quant)um$/, "i", "\\1a"},
{~s/^(di)e$/, "i", "\\1ce"},
{~s/^(f)oot$/, "i", "\\1eet"},
{~s/^(phenomen)on/, "i", "\\1a"}
]

@singular @irregular ++
[
{~r/(child)ren/i, "\\1"},
{~r/(wo|sea)men$/i, "\\1man"},
{~r/^(m|l)ice$/i, "\\1ouse"},
{~r/(bus|canvas|status|alias)(es)?$/i, "\\1"},
{~r/(ss)$/i, "\\1"},
{~r/(database)s$/i, "\\1"},
{~r/([ti])a$/i, "\\1um"},
{~r/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i,
{~s/(child)ren/, "i", "\\1"},
{~s/(wo|sea)men$/, "i", "\\1man"},
{~s/^(m|l)ice$/, "i", "\\1ouse"},
{~s/(bus|canvas|status|alias)(es)?$/, "i", "\\1"},
{~s/(ss)$/, "i", "\\1"},
{~s/(database)s$/, "i", "\\1"},
{~s/([ti])a$/, "i", "\\1um"},
{~s/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/, "i",
"\\1sis"},
{~r/(analy)(sis|ses)$/i, "\\1sis"},
{~r/(octop|vir)i$/i, "\\1us"},
{~r/(hive)s$/i, "\\1"},
{~r/(tive)s$/i, "\\1"},
{~r/(er)ves$/i, "\\1ve"},
{~r/([lora])ves$/i, "\\1f"},
{~r/([^f])ves$/i, "\\1fe"},
{~r/([^aeiouy]|qu)ies$/i, "\\1y"},
{~r/(m)ovies$/i, "\\1ovie"},
{~r/(x|ch|ss|sh)es$/i, "\\1"},
{~r/(shoe)s$/i, "\\1"},
{~r/(o)es$/i, "\\1"},
{~r/s$/i, ""}
{~s/(analy)(sis|ses)$/, "i", "\\1sis"},
{~s/(octop|vir)i$/, "i", "\\1us"},
{~s/(hive)s$/, "i", "\\1"},
{~s/(tive)s$/, "i", "\\1"},
{~s/(er)ves$/, "i", "\\1ve"},
{~s/([lora])ves$/, "i", "\\1f"},
{~s/([^f])ves$/, "i", "\\1fe"},
{~s/([^aeiouy]|qu)ies$/, "i", "\\1y"},
{~s/(m)ovies$/, "i", "\\1ovie"},
{~s/(x|ch|ss|sh)es$/, "i", "\\1"},
{~s/(shoe)s$/, "i", "\\1"},
{~s/(o)es$/, "i", "\\1"},
{~s/s$/, "i", ""}
]

@plural @plural_irregular ++
[
{~r/(child)$/i, "\\1ren"},
{~r/(m)an$/i, "\\1en"},
{~r/(m|l)ouse/i, "\\1ice"},
{~r/(database)s$/i, "\\1"},
{~r/(quiz)$/i, "\\1zes"},
{~r/^(ox)$/i, "\\1en"},
{~r/(matr|vert|ind)ix|ex$/i, "\\1ices"},
{~r/(x|ch|ss|sh)$/i, "\\1es"},
{~r/([^aeiouy]|qu)y$/i, "\\1ies"},
{~r/(hive)$/i, "\\1s"},
{~r/(sc[au]rf)$/i, "\\1s"},
{~r/(?:([^f])fe|((hoo)|([lra]))f)$/i, "\\2\\1ves"},
{~r/sis$/i, "ses"},
{~r/([ti])um$/i, "\\1a"},
{~r/(buffal|tomat)o$/i, "\\1oes"},
{~r/(octop|vir)us$/i, "\\1i"},
{~r/(bus|alias|status|canvas)$/i, "\\1es"},
{~r/(ax|test)is$/i, "\\1es"},
{~r/s$/i, "s"},
{~r/data$/i, "data"},
{~r/$/i, "s"}
{~s/(child)$/, "i", "\\1ren"},
{~s/(m)an$/, "i", "\\1en"},
{~s/(m|l)ouse/, "i", "\\1ice"},
{~s/(database)s$/, "i", "\\1"},
{~s/(quiz)$/, "i", "\\1zes"},
{~s/^(ox)$/, "i", "\\1en"},
{~s/(matr|vert|ind)ix|ex$/, "i", "\\1ices"},
{~s/(x|ch|ss|sh)$/, "i", "\\1es"},
{~s/([^aeiouy]|qu)y$/, "i", "\\1ies"},
{~s/(hive)$/, "i", "\\1s"},
{~s/(sc[au]rf)$/, "i", "\\1s"},
{~s/(?:([^f])fe|((hoo)|([lra]))f)$/, "i", "\\2\\1ves"},
{~s/sis$/, "i", "ses"},
{~s/([ti])um$/, "i", "\\1a"},
{~s/(buffal|tomat)o$/, "i", "\\1oes"},
{~s/(octop|vir)us$/, "i", "\\1i"},
{~s/(bus|alias|status|canvas)$/, "i", "\\1es"},
{~s/(ax|test)is$/, "i", "\\1es"},
{~s/s$/, "i", "s"},
{~s/data$/, "i", "data"},
{~s/$/, "i", "s"}
]

@singular @irregular ++
[
{~r/(child)ren/i, "\\1"},
{~r/(wo|sea)men$/i, "\\1man"},
{~r/^(m|l)ice$/i, "\\1ouse"},
{~r/(bus|canvas|status|alias)(es)?$/i, "\\1"},
{~r/(ss)$/i, "\\1"},
{~r/(database)s$/i, "\\1"},
{~r/([ti])a$/i, "\\1um"},
{~r/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i,
{~s/(child)ren/, "i", "\\1"},
{~s/(wo|sea)men$/, "i", "\\1man"},
{~s/^(m|l)ice$/, "i", "\\1ouse"},
{~s/(bus|canvas|status|alias)(es)?$/, "i", "\\1"},
{~s/(ss)$/, "i", "\\1"},
{~s/(database)s$/, "i", "\\1"},
{~s/([ti])a$/, "i", "\\1um"},
{~s/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/, "i",
"\\1sis"},
{~r/(analy)(sis|ses)$/i, "\\1sis"},
{~r/(octop|vir)i$/i, "\\1us"},
{~r/(hive)s$/i, "\\1"},
{~r/(tive)s$/i, "\\1"},
{~r/(er)ves$/i, "\\1ve"},
{~r/([lora])ves$/i, "\\1f"},
{~r/([^f])ves$/i, "\\1fe"},
{~r/([^aeiouy]|qu)ies$/i, "\\1y"},
{~r/(m)ovies$/i, "\\1ovie"},
{~r/(x|ch|ss|sh)es$/i, "\\1"},
{~r/(shoe)s$/i, "\\1"},
{~r/(o)es$/i, "\\1"},
{~r/s$/i, ""}
{~s/(analy)(sis|ses)$/, "i", "\\1sis"},
{~s/(octop|vir)i$/, "i", "\\1us"},
{~s/(hive)s$/, "i", "\\1"},
{~s/(tive)s$/, "i", "\\1"},
{~s/(er)ves$/, "i", "\\1ve"},
{~s/([lora])ves$/, "i", "\\1f"},
{~s/([^f])ves$/, "i", "\\1fe"},
{~s/([^aeiouy]|qu)ies$/, "i", "\\1y"},
{~s/(m)ovies$/, "i", "\\1ovie"},
{~s/(x|ch|ss|sh)es$/, "i", "\\1"},
{~s/(shoe)s$/, "i", "\\1"},
{~s/(o)es$/, "i", "\\1"},
{~s/s$/, "i", ""}
]

@plural @plural_irregular ++
[
{~r/(child)$/i, "\\1ren"},
{~r/(m)an$/i, "\\1en"},
{~r/(m|l)ouse/i, "\\1ice"},
{~r/(database)s$/i, "\\1"},
{~r/(quiz)$/i, "\\1zes"},
{~r/^(ox)$/i, "\\1en"},
{~r/(matr|vert|ind)ix|ex$/i, "\\1ices"},
{~r/(x|ch|ss|sh)$/i, "\\1es"},
{~r/([^aeiouy]|qu)y$/i, "\\1ies"},
{~r/(hive)$/i, "\\1s"},
{~r/(sc[au]rf)$/i, "\\1s"},
{~r/(?:([^f])fe|((hoo)|([lra]))f)$/i, "\\2\\1ves"},
{~r/sis$/i, "ses"},
{~r/([ti])um$/i, "\\1a"},
{~r/(buffal|tomat)o$/i, "\\1oes"},
{~r/(octop|vir)us$/i, "\\1i"},
{~r/(bus|alias|status|canvas)$/i, "\\1es"},
{~r/(ax|test)is$/i, "\\1es"},
{~r/s$/i, "s"},
{~r/data$/i, "data"},
{~r/$/i, "s"}
{~s/(child)$/, "i", "\\1ren"},
{~s/(m)an$/, "i", "\\1en"},
{~s/(m|l)ouse/, "i", "\\1ice"},
{~s/(database)s$/, "i", "\\1"},
{~s/(quiz)$/, "i", "\\1zes"},
{~s/^(ox)$/, "i", "\\1en"},
{~s/(matr|vert|ind)ix|ex$/, "i", "\\1ices"},
{~s/(x|ch|ss|sh)$/, "i", "\\1es"},
{~s/([^aeiouy]|qu)y$/, "i", "\\1ies"},
{~s/(hive)$/, "i", "\\1s"},
{~s/(sc[au]rf)$/, "i", "\\1s"},
{~s/(?:([^f])fe|((hoo)|([lra]))f)$/, "i", "\\2\\1ves"},
{~s/sis$/, "i", "ses"},
{~s/([ti])um$/, "i", "\\1a"},
{~s/(buffal|tomat)o$/, "i", "\\1oes"},
{~s/(octop|vir)us$/, "i", "\\1i"},
{~s/(bus|alias|status|canvas)$/, "i", "\\1es"},
{~s/(ax|test)is$/, "i", "\\1es"},
{~s/s$/, "i", "s"},
{~s/data$/, "i", "data"},
{~s/$/, "i", "s"}
]

defp compile_rules(rules) do
Enum.map(rules, fn {pattern, opts, repl} ->
{Regex.compile!(pattern, opts), repl}
end)
end

def singularize(word) when is_atom(word) do
find_match(@singular, to_string(word))
@singular
|> compile_rules()
|> find_match(to_string(word))
end

def singularize(word), do: find_match(@singular, word)
def singularize(word) do
@singular
|> compile_rules()
|> find_match(word)
end

def pluralize(word) when is_atom(word) do
find_match(@plural, to_string(word))
@plural
|> compile_rules()
|> find_match(to_string(word))
end

def pluralize(word), do: find_match(@plural, word)
def pluralize(word) do
@plural
|> compile_rules()
|> find_match(word)
end

def inflect(word, n) when n == 1, do: singularize(word)
def inflect(word, n) when is_number(n), do: pluralize(word)

defp find_match(set, word) do
cond do
uncountable?(word) -> word
@default -> replace_match(set, word)
true -> replace_match(set, word)
end
end

Expand Down
9 changes: 4 additions & 5 deletions test/inflex_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,21 @@ defmodule InflexTest do
end

test :camelize_upper do
assert "Upper" == camelize("upper")
assert "Upper" == camelize("upper")
assert "UpperCamelCase" == camelize("upper_camel_case")
assert "UpperCamelCase" == camelize("UpperCamelCase")
assert "UpperCamelCase" == camelize("UPPER_CAMEL_CASE")
assert "" == camelize("")
assert "" == camelize("")
refute "UpperCamelCase" == camelize("upper_camel_case", :lower)
end

test :camelize_lower do
assert "lower" == camelize("lower", :lower)
assert "lower" == camelize("lower", :lower)
assert "lowerCamelCase" == camelize("lower_camel_case", :lower)
assert "lowerCamelCase" == camelize("Lower_camel_case", :lower)
assert "lowerCamelCase" == camelize("lowerCamelCase", :lower)
assert "lowerCamelCase" == camelize("LOWER_CAMEL_CASE", :lower)
assert "" == camelize("", :lower)
assert "" == camelize("", :lower)
refute "lowerCamelCase" == camelize("lower_camel_case")
end

Expand Down Expand Up @@ -277,5 +277,4 @@ defmodule InflexTest do
assert "accomplice" == inflect("accomplice", 1)
assert "accomplice" == inflect("accomplice", 1.0)
end

end
2 changes: 1 addition & 1 deletion test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ExUnit.start
ExUnit.start()