defmodule Text.Inflect.En do @moduledoc """ Pluralisation for the English language based on the paper [An Algorithmic Approach to English Pluralization](http://users.monash.edu/~damian/papers/HTML/Plurals.html). """ @saved_data_path "priv/inflection/en/en.etf" @external_resource @saved_data_path @inflections File.read!(@saved_data_path) |> :erlang.binary_to_term() @doc false def inflections do @inflections end @doc """ Pluralize an english noun, pronoun, verb or adjective. ## Arguments * `word` is any English word. * `mode` is `:modern` or `:classical`. The default is `:modern`. This applies to nouns only. ## Returns * a `String` representing the pluralized word. ## Notes `mode` when `:classical` applies pluralization on latin nouns in english but with latin suffixes. ## Examples iex> Text.Inflect.En.pluralize "fish" "fish" iex> Text.Inflect.En.pluralize "soliloquy" "soliloquies" iex> Text.Inflect.En.pluralize "genius", :classical "genii" iex> Text.Inflect.En.pluralize "has" "have" iex> Text.Inflect.En.pluralize "catches" "catch" iex> Text.Inflect.En.pluralize "child's" "children's" iex> Text.Inflect.En.pluralize "Mary's" "Marys'" """ def pluralize(word, mode \\ :modern) do # Handle known adjectives... # try steps 2 through 4 of Algorithm 3 # Handle known verbs... # try steps 2 through 5 of Algorithm 2 # is_third_person_singular_s(word) || # Handle singular nouns ending in -s (ethos, axis, etc. - see Tables A.2, A.3, A.16, A.22, and A.23)... # if word is a noun ending in -s, # try steps 2 through 13 of Algorithm 1 is_indefinite_article(word) || is_possessive_pronoun(word) || is_genetive(word) || is_non_inflecting_verb(word) || is_irregular_verb(word) || is_third_person_singular(word) || if suffix?(word, "s") do pluralize_noun(word, mode) else # Handle 3rd person singular verbs (that is, any other words ending in -s)... # try steps 4 and 5 of Algorithm 2 # Treat the word as a noun... # try steps 2 through 13 of Algorithm 1 is_third_person_singular(word) || is_third_person_singular_s(word) || pluralize_noun(word, mode) end end @doc """ Pluralize an english noun. ## Arguments * `word` is any English noun. * `mode` is `:modern` or `:classical`. The default is `:modern`. ## Returns * a `String` representing the pluralized noun ## Notes `mode` when `:classical` applies pluralization on latin nouns used in english but with latin suffixes. ## Examples iex> Text.Inflect.En.pluralize_noun "Major general" "Major generals" iex> Text.Inflect.En.pluralize_noun "fish" "fish" iex> Text.Inflect.En.pluralize_noun "soliloquy" "soliloquies" iex> Text.Inflect.En.pluralize_noun "genius", :classical "genii" iex> Text.Inflect.En.pluralize_noun "genius" "geniuses" iex> Text.Inflect.En.pluralize_noun "platypus", :classical "platypodes" iex> Text.Inflect.En.pluralize_noun "platypus" "platypuses" """ def pluralize_noun(word, mode \\ :modern) do is_non_inflecting(word, mode) || is_pronoun(word, mode) || is_irregular_noun(word, mode) || is_irregular_suffix(word, mode) || is_assimilated_classical(word, mode) || is_classical(word, mode) || is_compound_plural(word, mode) || is_ves_plural(word, mode) || is_word_ending_in_y(word, mode) || is_o_suffix(word, mode) || is_general(word, mode) || is_regular(word, mode) end # Handle words that do not inflect in the plural (such as fish, travois, chassis, nationalities # ending in -ese etc. - see Tables A.2 and A.3)... # if suffix(-fish) or suffix(-ois) or suffix(-sheep) # or suffix(-deer) or suffix(-pox) or suffix(-[A-Z].*ese) # or suffix(-itis) or category(-,-), # return the original noun defp is_non_inflecting(word, mode) when is_binary(word) do cond do category?(word, "herd", mode) -> word category?(word, "nationalities", mode) -> word category?(word, "-", "-", mode) -> word true -> nil end end # Handle pronouns in the nominative, accusative, and dative (see Tables A.5), as well as # prepositional phrases... # if the word is a pronoun, # return the specified plural of the pronoun # # if the word is of the form: " ", # return " " defp is_pronoun(word, mode) do cond do category?(word, "pronoun", mode) -> pronoun(word, mode) true -> nil end end # Handle standard irregular plurals (mongooses, oxen, etc. - see table A.1)... # if the word has an irregular plural, # return the specified plural defp is_irregular_noun(word, mode) do cond do category?(word, "irregular_noun", mode) -> irregular_noun(word, mode) true -> nil end end # Handle irregular inflections for common suffixes (synopses, mice and men, etc.)... # if suffix(-man), return inflection(-man,-men) # if suffix(-[lm]ouse), return inflection(-ouse,-ice) # if suffix(-tooth), return inflection(-tooth,-teeth) # if suffix(-goose), return inflection(-goose,-geese) # if suffix(-foot), return inflection(-foot,-feet) # if suffix(-zoon), return inflection(-zoon,-zoa) # if suffix(-[csx]is), return inflection(-is,-es) defp is_irregular_suffix(word, _mode) do cond do suffix?(word, "man") -> replace_suffix(word, "man", "men") suffix?(word, "louse") -> replace_suffix(word, "louse", "lice") suffix?(word, "mouse") -> replace_suffix(word, "mouse", "mice") suffix?(word, "tooth") -> replace_suffix(word, "tooth", "teeth") suffix?(word, "goose") -> replace_suffix(word, "goose", "geese") suffix?(word, "foot") -> replace_suffix(word, "foot", "feet") suffix?(word, "zoon") -> replace_suffix(word, "zoon", "zoa") suffix?(word, "cis") -> replace_suffix(word, "cis", "ces") suffix?(word, "sis") -> replace_suffix(word, "sis", "ses") suffix?(word, "xis") -> replace_suffix(word, "xis", "xes") true -> nil end end # Handle fully assimilated classical inflections (vertebrae, codices, etc. - see tables A.10, # A.14, A.19 and A.20, and tables A.11, A.15 and A.21 if in "classical mode)... # if category(-ex,-ices), return inflection(-ex,-ices) # if category(-um,-a), return inflection(-um,-a) # if category(-on,-a), return inflection(-on,-a) # if category(-a,-ae), return inflection(-a,-ae) defp is_assimilated_classical(word, mode) do cond do category?(word, "-ex", "-ices", mode) -> replace_suffix(word, "ex", "ices") category?(word, "-um", "-a", mode) -> replace_suffix(word, "um", "a") category?(word, "-on", "-a", mode) -> replace_suffix(word, "on", "a") category?(word, "-a", "-ae", mode) -> replace_suffix(word, "a", "ae") true -> nil end end # Handle classical variants of modern inflections (stigmata, soprani, etc. - see tables A.11 to # A.13, A.15, A.16, A.18, A.21 to A.25)... # if in classical mode, # if suffix(-trix), return inflection(-trix,-trices) # if suffix(-eau), return inflection(-eau,-eaux) # if suffix(-ieu), return inflection(-ieu,-ieux) # if suffix(-..[iay]nx), return inflection(-nx,-nges) # if category(-en,-ina), return inflection(-en,-ina) # if category(-a,-ata), return inflection(-a,-ata) # if category(-is,-ides), return inflection(-is,-ides) # if category(-us,-i), return inflection(-us,-i) # if category(-us,-us), return the original noun # if category(-o,-i), return inflection(-o,-i) # if category(-,-i), return inflection(-,-i) # if category(-,-im), return inflection(-,-im) defp is_classical(word, :classical = mode) do cond do suffix?(word, "trix") -> replace_suffix(word, "trix", "trices") suffix?(word, "eau") -> word <> "x" suffix?(word, "ieu") -> word <> "x" suffix?(word, "inx") -> replace_suffix(word, "nx", "nges") suffix?(word, "anx") -> replace_suffix(word, "nx", "nges") suffix?(word, "ynx") -> replace_suffix(word, "nx", "nges") category?(word, "-en", "-ina", mode) -> replace_suffix(word, "en", "ina") category?(word, "-a", "-ata", mode) -> word <> "ta" category?(word, "-is", "-ides", mode) -> replace_suffix(word, "is", "ides") category?(word, "-us", "-i", mode) -> replace_suffix(word, "us", "i") category?(word, "-us", "-us", mode) -> word category?(word, "-o", "-i", mode) -> replace_suffix(word, "o", "i") category?(word, "-", "-i", mode) -> word <> "i" category?(word, "-", "-im", mode) -> word <> "im" true -> nil end end defp is_classical(word, :modern = mode) do cond do category?(word, "-us", "-i", mode) -> replace_suffix(word, "us", "uses") true -> nil end end # The suffixes -ch, -sh, and -ss all take -es in the plural (churches, classes, etc)... # if suffix(-[cs]h), return inflection(-h,-hes) # if suffix(-ss), return inflection(-ss,-sses) defp is_compound_plural(word, _mode) do cond do suffix?(word, "ch") -> replace_suffix(word, "h", "hes") suffix?(word, "sh") -> replace_suffix(word, "h", "hes") suffix?(word, "ss") -> replace_suffix(word, "h", "sses") true -> nil end end # Certain words ending in -f or -fe take -ves in the plural (lives, wolves, etc)... # if suffix(-[aeo]lf) or suffix(-[^d]eaf) or suffix(-arf), # return inflection(-f,-ves) # # if suffix(-[nlw]ife), # return inflection(-fe,-ves) defp is_ves_plural(word, _mode) do cond do suffix?(word, "alf") -> replace_suffix(word, "f", "ves") suffix?(word, "elf") -> replace_suffix(word, "f", "ves") suffix?(word, "olf") -> replace_suffix(word, "f", "ves") suffix?(word, "arf") -> replace_suffix(word, "f", "ves") suffix?(word, "nife") -> replace_suffix(word, "fe", "ves") suffix?(word, "life") -> replace_suffix(word, "fe", "ves") suffix?(word, "wife") -> replace_suffix(word, "fe", "ves") suffix?(word, "eaf") -> if String.at(word, -4) == "d", do: nil, else: replace_suffix(word, "f", "ves") true -> nil end end # Words ending in -y take -ys if preceded by a vowel (storeys, stays, etc.) or when a proper noun # (Marys, Tonys, etc.), but -ies if preceded by a consonant (stories, skies, etc.)... # if suffix(-[aeiou]y), return inflection(-y,-ys) # if suffix(-[A-Z].*y), return inflection(-y,-ys) # if suffix(-y), return inflection(-y,-ies) defp is_word_ending_in_y(word, _mode) do cond do suffix?(word, "y") && vowel?(word, -2) -> word <> "s" suffix?(word, "y") && starts_with_upper?(word) -> word <> "s" suffix?(word, "y") -> replace_suffix(word, "y", "ies") true -> nil end end # Some words ending in -o take -os (lassos, solos, etc. - see tables A.17 and A.18); the rest # take -oes (potatoes, dominoes, etc.) However, words in which the -o is preceded by a vowel # always take -os (folios, bamboos)... # if category(-o,-os) or suffix(-[aeiou]o), # return inflection(-o,-os) # # if suffix(-o), return inflection(-o,-oes) defp is_o_suffix(word, :modern = mode) do cond do category?(word, "-o", "-os", mode) -> word <> "s" suffix?(word, "o") && vowel?(word, -2) -> word <> "s" suffix?(word, "o") -> word <> "es" true -> nil end end defp is_o_suffix(word, :classical = mode) do cond do category?(word, "-o", "-os", mode) -> replace_suffix(word, "o", "i") suffix?(word, "o") -> word <> "es" true -> nil end end # Handle plurals of compound words (Postmasters General, Major Generals, mothers-in-law, etc) by # recursively applying the entire algorithm to the underlying noun. See Table A.26 for the # military suffix -general, which inflects to -generals... # if category(-general,-generals), return inflection(-l,-ls) # # if the word is of the form: " general", # return " general" # # if the word is of the form: " ", # return " " @generals @inflections |> Map.get("a26") for general <- @generals do defp is_general(unquote(general) <> suffix, _mode) do cond do suffix?(suffix, "l") -> unquote(general) <> suffix <> "s" true -> nil end end end defp is_general(_word, _mode) do nil end # Otherwise, assume that the plural just adds -s (cats, programmes, trees, etc.)... # otherwise, return inflection(-,-s) defp is_regular(word, _mode) do word <> "s" end @doc """ Pluralize an english verb. ## Arguments * `word` is any English verb. ## Returns * a `String` representing the pluralized verb ## Examples iex> Text.Inflect.En.pluralize_verb "has" "have" iex> Text.Inflect.En.pluralize_verb "catches" "catch" """ def pluralize_verb(word) do # All other cases are regular 1st or 2nd person verbs, which don't inflect... # otherwise, return the verb uninflected is_non_inflecting_verb(word) || is_irregular_verb(word) || is_third_person_singular(word) || is_third_person_singular_s(word) || is_ambiguous(word) || word end defp is_non_inflecting_verb(word) do cond do category?(word, "non_inflecting_verb") -> word true -> nil end end # Check if the verb is being used as an auxiliary and has a known irregular inflection (has seen, # was going, etc. See Table A.8 for irregular verbs)... # if the word has the form " " # and belongs to the category of irregular verbs, # return " " # Handle simple irregular verbs (has, is, etc. - see Table A.8)... # if the word belongs to the category of irregular verbs, # return the specified plural form # Combine the both cases in this simpler execution defp is_irregular_verb(word) do cond do category?(word, "irregular_verb") -> irregular_verb(word) true -> nil end end # Verbs in the regular 3rd person singular lose their -es, -ies, or -oes suffix (she catches - # they catch, he tries -> they try, it does -> they do, etc.)... # if suffix(-[cs]hes), return inflection(-hes,-h) # if suffix(-[sx]es), return inflection(-es,-) # if suffix(-zzes), return inflection(-es,-) # if suffix(-ies), return inflection(-ies,-y) # if suffix(-oes), return inflection(-oes,-o) defp is_third_person_singular(word) do cond do suffix?(word, "ches") -> replace_suffix(word, "hes", "h") suffix?(word, "shes") -> replace_suffix(word, "hes", "h") suffix?(word, "ses") -> replace_suffix(word, "es", "") suffix?(word, "xes") -> replace_suffix(word, "es", "") suffix?(word, "zzes") -> replace_suffix(word, "es", "") suffix?(word, "ies") -> replace_suffix(word, "ies", "y") suffix?(word, "oes") -> replace_suffix(word, "oes", "o") true -> nil end end # Other 3rd person singular verbs ending in -s (but not -ss) also lose their suffix... # if suffix(-[^s]s), return inflection(-s,-) defp is_third_person_singular_s(word) do cond do suffix?(word, "ss") -> nil suffix?(word, "s") -> replace_suffix(word, "s", "") true -> nil end end # Handle ambiguous simple verbs that might also be nouns (thought, sink, fly, etc. - see Table # A.4)... # if the word is in the ambiguous category, # return the specified plural form defp is_ambiguous(word) do cond do category?(word, "ambiguous") -> pluralize_noun(word) true -> nil end end @doc """ Pluralize an english adjective. ## Arguments * `word` is any English adjective. ## Returns * a `String` representing the pluralized adjective ## Examples iex> Text.Inflect.En.pluralize_adjective "a" "some" iex> Text.Inflect.En.pluralize_adjective "my" "our" iex> Text.Inflect.En.pluralize_adjective "child's" "children's" iex> Text.Inflect.En.pluralize_adjective "Mary's" "Marys'" """ def pluralize_adjective(word) do # In all other cases no inflection is required... # otherwise, return the adjective uninflected is_indefinite_article(word) || is_possessive_pronoun(word) || is_genetive(word) || word end # Handle indefinite articles and demonstratives... # if the word is "a" or "an", return "some" # if the word is "this", return "these" # if the word is "that", return "those" def is_indefinite_article(word) do cond do word in ["a", "an"] -> "some" word == "this" -> "these" word == "that" -> "those" true -> nil end end # Handle possessive pronouns (my -> our, its -> their, etc - see Table A.7)... # if the word is a personal possessive, # return the specified plural form def is_possessive_pronoun(word) do cond do category?(word, "personal_possessive") -> personal_possessive(word) true -> nil end end # Handle genitives (dog's -> dogs', child's -> children's, Mary's -> Marys', etc). The general # rule is: remove the apostrophe and any trailing -s, form the plural of the resultant noun, and # then append an apostrophe (or -'s if the pluralized noun doesn't end in -s)... # if suffix(-'s) or suffix(-'), # if suffix(-'), let the noun be inflection(-',-) # otherwise, let the noun be inflection(-'s,-) # let the noun be the noun plural of # if ends in -s, return "'" # otherwise, return "'s" def is_genetive(word) do cond do suffix?(word, "'s") -> do_genetive(word, "'s") suffix?(word, "'") -> do_genetive(word, "'") true -> nil end end def do_genetive(word, suffix) do plural_noun = word |> replace_suffix(suffix, "") |> pluralize_noun() if suffix?(plural_noun, "s") do plural_noun <> "'" else plural_noun <> "'s" end end ########################################## # Category definitions ########################################## @non_inflecting_nouns @inflections |> Map.take(["a2", "a3"]) |> Map.values() |> List.flatten() @ambiguous @inflections |> Map.get("a4") @personal_possessive @inflections |> Map.get("a7") |> Enum.drop(3) |> Enum.flat_map(&String.split(&1, " ")) |> Enum.reject(&(&1 == "->" || &1 == " ")) |> Enum.chunk_every(2) |> Enum.map(&List.to_tuple/1) |> Map.new() @pluralize_auxillary_irregular @inflections |> Map.get("a8") |> Enum.drop(3) |> Enum.map(&String.split(&1, " -> ")) |> Enum.map(&List.to_tuple/1) |> Map.new() @non_inflecting_verbs @inflections |> Map.get("a9") @a_ae_modern @inflections |> Map.get("a10") @a_ae_classical @inflections |> Map.take(["a10", "a11"]) |> Map.values() |> List.flatten() @a_ata @inflections |> Map.get("a12") @en_ina @inflections |> Map.get("a13") @ex_ices_modern @inflections |> Map.get("a14") @ex_ices_classical @inflections |> Map.take(["a14", "a15"]) |> Map.values() |> List.flatten() @is_ides @inflections |> Map.get("a16") @o_i @inflections |> Map.get("a18") @o_words_modern @inflections |> Map.take(["a17", "a18"]) |> Map.values() |> List.flatten() @o_words_classical @inflections |> Map.get("a17") @on_a @inflections |> Map.get("a19") @um_a_modern @inflections |> Map.get("a20") @um_a_classical @inflections |> Map.take(["a20", "a21"]) |> Map.values() |> List.flatten() @us_i @inflections |> Map.get("a22") @us_us @inflections |> Map.get("a23") @any_i @inflections |> Map.get("a24") @any_im @inflections |> Map.get("a25") @pronouns @inflections |> Map.get("a5") |> Enum.drop(3) |> Enum.reject(&(&1 == "->")) |> Enum.map(&String.replace(&1, " ->", "")) |> Enum.map(fn x -> if String.contains?(x, "|"), do: String.split(x, "|"), else: x end) |> Enum.chunk_every(2) |> Map.new(&List.to_tuple/1) @irregular @inflections |> Map.get("a1") |> Enum.chunk_every(3) |> Enum.drop(1) |> Enum.map(fn [word, "(none)", plural] -> {word, [plural, plural]} [word, plural, "(none)"] -> {word, [plural, plural]} [word, modern, classical] -> {word, [modern, classical]} [a, b] -> {a, [b, b]} end) |> Map.new() @doc false def category?(word, "irregular_verb") do Map.has_key?(@pluralize_auxillary_irregular, word) end def category?(word, "ambiguous") do word in @ambiguous end def category?(word, "non_inflecting_verb") do word in @non_inflecting_verbs end def category?(word, "personal_possessive") do Map.has_key?(@personal_possessive, word) end @doc false def category?(word, "irregular_noun", _mode) do Map.has_key?(@irregular, word) end def category?(word, "pronoun", _mode) do Map.has_key?(@pronouns, word) end @non_inflecting_suffix ["fish", "ois", "sheep", "deer", "pox", "itis"] def category?(word, "herd", _mode) do Enum.any?(@non_inflecting_suffix, &suffix?(word, &1)) end def category?(word, "nationalities", _mode) do suffix?(word, "ese") && starts_with_upper?(word) end @doc false def category?(word, "-", "-", _) do word in @non_inflecting_nouns end def category?(word, "-o", "-os", :classical) do word in @o_words_classical end def category?(word, "-o", "-os", :modern) do word in @o_words_modern end def category?(word, "-ex", "-ices", :modern) do word in @ex_ices_modern end def category?(word, "-ex", "-ices", :classical) do word in @ex_ices_classical end def category?(word, "-um", "-a", :modern) do word in @um_a_modern end def category?(word, "-um", "-a", :classical) do word in @um_a_classical end def category?(word, "-on", "-a", :modern) do word in @on_a end def category?(word, "-on", "-a", :classical) do word in @on_a end def category?(word, "-a", "-ae", :modern) do word in @a_ae_modern end def category?(word, "-a", "-ae", :classical) do word in @a_ae_classical end def category?(word, "-en", "-ina", :classical) do word in @en_ina end def category?(word, "-a", "-ata", _mode) do word in @a_ata end def category?(word, "-is", "-ides", _mode) do word in @is_ides end def category?(word, "-us", "-i", _mode) do word in @us_i end def category?(word, "-us", "-us", _mode) do word in @us_us end def category?(word, "-o", "-i", _mode) do word in @o_i end def category?(word, "-", "-i", _mode) do word in @any_i end def category?(word, "-", "-im", _mode) do word in @any_im end ########################################## # Helpers ########################################## defp suffix?(word, suffix) do String.ends_with?(word, suffix) end defp replace_suffix(word, suffix, replacement) do String.replace_trailing(word, suffix, replacement) end @vowels ["a", "e", "i", "o", "u"] defp vowel?(word, pos) do String.at(word, pos) in @vowels end defp irregular_noun(word, mode) do [modern, classical] = Map.fetch!(@irregular, word) if mode == :modern, do: modern, else: classical end defp irregular_verb(word) do Map.fetch!(@pluralize_auxillary_irregular, word) end defp pronoun(word, mode) do [modern, classical] = Map.fetch!(@pronouns, word) if mode == :modern, do: modern, else: classical end defp personal_possessive(word) do Map.fetch!(@personal_possessive, word) end defp starts_with_upper?(<>) when char in ?A..?Z, do: true defp starts_with_upper?(_word), do: false end