diff --git a/lib/inflector.rb b/lib/inflector.rb new file mode 100644 index 00000000..652aaaca --- /dev/null +++ b/lib/inflector.rb @@ -0,0 +1,7 @@ +# Library namespace +module Inflector +end + +require 'inflector/inflections' +require 'inflector/defaults' +require 'inflector/methods' diff --git a/lib/inflector/defaults.rb b/lib/inflector/defaults.rb new file mode 100644 index 00000000..fb8d56dd --- /dev/null +++ b/lib/inflector/defaults.rb @@ -0,0 +1,62 @@ +module Inflector + Inflector::Inflections.instance.instance_eval do + plural(/$/, 's') + plural(/s$/i, 's') + plural(/us$/i, 'i') + plural(/(ax|test)is$/i, '\1es') + plural(/(octop|vir)us$/i, '\1i') + plural(/(octop|vir)i$/i, '\1i') + plural(/(alias|status)$/i, '\1es') + plural(/(bu)s$/i, '\1ses') + plural(/(buffal|tomat)o$/i, '\1oes') + plural(/([ti])um$/i, '\1a') + plural(/([ti])a$/i, '\1a') + plural(/sis$/i, 'ses') + plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves') + plural(/(hive)$/i, '\1s') + plural(/([^aeiouy]|qu)y$/i, '\1ies') + plural(/(x|ch|ss|sh)$/i, '\1es') + plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices') + plural(/([m|l])ouse$/i, '\1ice') + plural(/([m|l])ice$/i, '\1ice') + plural(/^(ox)$/i, '\1en') + plural(/^(oxen)$/i, '\1') + plural(/(quiz)$/i, '\1zes') + + singular(/s$/i, '') + singular(/i$/i, 'us') + singular(/(n)ews$/i, '\1ews') + singular(/([ti])a$/i, '\1um') + singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '\1\2sis') + singular(/(^analy)ses$/i, '\1sis') + singular(/([^f])ves$/i, '\1fe') + singular(/(hive)s$/i, '\1') + singular(/(tive)s$/i, '\1') + singular(/([lr])ves$/i, '\1f') + singular(/([^aeiouy]|qu)ies$/i, '\1y') + singular(/(s)eries$/i, '\1eries') + singular(/(m)ovies$/i, '\1ovie') + singular(/(x|ch|ss|sh)es$/i, '\1') + singular(/([m|l])ice$/i, '\1ouse') + singular(/(bus)es$/i, '\1') + singular(/(o)es$/i, '\1') + singular(/(shoe)s$/i, '\1') + singular(/(cris|ax|test)es$/i, '\1is') + singular(/(octop|vir)i$/i, '\1us') + singular(/(alias|status)es$/i, '\1') + singular(/^(ox)en/i, '\1') + singular(/(vert|ind)ices$/i, '\1ex') + singular(/(matr)ices$/i, '\1ix') + singular(/(quiz)zes$/i, '\1') + singular(/(database)s$/i, '\1') + + irregular('person', 'people') + irregular('man', 'men') + irregular('child', 'children') + irregular('sex', 'sexes') + irregular('move', 'moves') + irregular('cow', 'kine') + + uncountable(%w(grass equipment information rice money species series fish sheep jeans)) + end +end diff --git a/lib/inflector/inflections.rb b/lib/inflector/inflections.rb new file mode 100644 index 00000000..01f3cd82 --- /dev/null +++ b/lib/inflector/inflections.rb @@ -0,0 +1,209 @@ +module Inflector + # A singleton instance of this class is yielded by Inflector.inflections, which can then be used to specify additional + # inflection rules. Examples: + # + # Inflector.inflections do |inflect| + # inflect.plural /^(ox)$/i, '\1\2en' + # inflect.singular /^(ox)en/i, '\1' + # + # inflect.irregular 'octopus', 'octopi' + # + # inflect.uncountable "equipment" + # end + # + # New rules are added at the top. So in the example above, the irregular rule for octopus will now be the first of the + # pluralization and singularization rules that is runs. This guarantees that your rules run before any of the rules that may + # already have been loaded. + class Inflections + def self.instance + @__instance__ ||= new + end + + attr_reader :plurals, :singulars, :uncountables, :humans + + def initialize + @plurals, @singulars, @uncountables, @humans = [], [], [], [] + end + + # Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression. + # The replacement should always be a string that may include references to the matched data from the rule. + def plural(rule, replacement) + @uncountables.delete(rule) if rule.is_a?(String) + @uncountables.delete(replacement) + @plurals.insert(0, [rule, replacement]) + end + + # Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression. + # The replacement should always be a string that may include references to the matched data from the rule. + def singular(rule, replacement) + @uncountables.delete(rule) if rule.is_a?(String) + @uncountables.delete(replacement) + @singulars.insert(0, [rule, replacement]) + end + + # Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used + # for strings, not regular expressions. You simply pass the irregular in singular and plural form. + # + # Examples: + # irregular 'octopus', 'octopi' + # irregular 'person', 'people' + def irregular(singular, plural) + @uncountables.delete(singular) + @uncountables.delete(plural) + if singular[0,1].upcase == plural[0,1].upcase + plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1]) + plural(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + plural[1..-1]) + singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1]) + else + plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1]) + plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1]) + plural(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), plural[0,1].upcase + plural[1..-1]) + plural(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), plural[0,1].downcase + plural[1..-1]) + singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1]) + singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1]) + end + end + + # Add uncountable words that shouldn't be attempted inflected. + # + # Examples: + # uncountable "money" + # uncountable "money", "information" + # uncountable %w( money information rice ) + def uncountable(*words) + (@uncountables << words).flatten! + end + + # Specifies a humanized form of a string by a regular expression rule or by a string mapping. + # When using a regular expression based replacement, the normal humanize formatting is called after the replacement. + # When a string is used, the human form should be specified as desired (example: 'The name', not 'the_name') + # + # Examples: + # human /_cnt$/i, '\1_count' + # human "legacy_col_person_name", "Name" + def human(rule, replacement) + @humans.insert(0, [rule, replacement]) + end + + # Clears the loaded inflections within a given scope (default is :all). + # Give the scope as a symbol of the inflection type, the options are: :plurals, + # :singulars, :uncountables, :humans. + # + # Examples: + # clear :all + # clear :plurals + def clear(scope = :all) + case scope + when :all + @plurals, @singulars, @uncountables = [], [], [] + else + instance_variable_set "@#{scope}", [] + end + end + end + + # Yields a singleton instance of Inflector::Inflections so you can specify additional + # inflector rules. + # + # Example: + # Inflector.inflections do |inflect| + # inflect.uncountable "rails" + # end + def inflections + if block_given? + yield Inflections.instance + else + Inflections.instance + end + end + + # Returns the plural form of the word in the string. + # + # Examples: + # "post".pluralize # => "posts" + # "octopus".pluralize # => "octopi" + # "sheep".pluralize # => "sheep" + # "words".pluralize # => "words" + # "CamelOctopus".pluralize # => "CamelOctopi" + def pluralize(word) + result = word.to_s.dup + + if word.empty? || inflections.uncountables.include?(result.downcase) + result + else + inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result + end + end + + # The reverse of +pluralize+, returns the singular form of a word in a string. + # + # Examples: + # "posts".singularize # => "post" + # "octopi".singularize # => "octopus" + # "sheep".singularize # => "sheep" + # "word".singularize # => "word" + # "CamelOctopi".singularize # => "CamelOctopus" + def singularize(word) + result = word.to_s.dup + + if inflections.uncountables.any? { |inflection| result =~ /\b(#{inflection})\Z/i } + result + else + inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result + end + end + + # Capitalizes the first word and turns underscores into spaces and strips a + # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output. + # + # Examples: + # "employee_salary" # => "Employee salary" + # "author_id" # => "Author" + def humanize(lower_case_and_underscored_word) + result = lower_case_and_underscored_word.to_s.dup + + inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) } + result.gsub(/_id$/, "").gsub(/_/, " ").capitalize + end + + # Capitalizes all the words and replaces some characters in the string to create + # a nicer looking title. +titleize+ is meant for creating pretty output. It is not + # used in the Rails internals. + # + # +titleize+ is also aliased as as +titlecase+. + # + # Examples: + # "man from the boondocks".titleize # => "Man From The Boondocks" + # "x-men: the last stand".titleize # => "X Men: The Last Stand" + def titleize(word) + humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize } + end + + # Create the name of a table like Rails does for models to table names. This method + # uses the +pluralize+ method on the last word in the string. + # + # Examples + # "RawScaledScorer".tableize # => "raw_scaled_scorers" + # "egg_and_ham".tableize # => "egg_and_hams" + # "fancyCategory".tableize # => "fancy_categories" + def tableize(class_name) + pluralize(underscore(class_name)) + end + + # Create a class name from a plural table name like Rails does for table names to models. + # Note that this returns a string and not a Class. (To convert to an actual class + # follow +classify+ with +constantize+.) + # + # Examples: + # "egg_and_hams".classify # => "EggAndHam" + # "posts".classify # => "Post" + # + # Singular names are not handled correctly: + # "business".classify # => "Busines" + def classify(table_name) + # strip out any leading schema name + camelize(singularize(table_name.to_s.sub(/.*\./, ''))) + end +end diff --git a/lib/mutant/inflector/methods.rb b/lib/inflector/methods.rb similarity index 100% rename from lib/mutant/inflector/methods.rb rename to lib/inflector/methods.rb diff --git a/lib/mutant/inflector/version.rb b/lib/inflector/version.rb similarity index 100% rename from lib/mutant/inflector/version.rb rename to lib/inflector/version.rb diff --git a/lib/mutant/inflector.rb b/lib/mutant/inflector.rb deleted file mode 100644 index fb0c2e3a..00000000 --- a/lib/mutant/inflector.rb +++ /dev/null @@ -1,425 +0,0 @@ -# = English Nouns Number Inflection. -# -# This module provides english singular <-> plural noun inflections. -module Inflector - - # Take an underscored name and make it into a camelized name - # - # @example - # "egg_and_hams".classify #=> "EggAndHam" - # "enlarged_testes".classify #=> "EnlargedTestis" - # "post".classify #=> "Post" - # - def self.classify(name) - words = name.to_s.sub(/.*\./, '').split('_') - words[-1] = singularize(words[-1]) - words.collect { |word| word.capitalize }.join - end - - # By default, camelize converts strings to UpperCamelCase. - # - # camelize will also convert '/' to '::' which is useful for converting paths to namespaces - # - # @example - # "active_record".camelize #=> "ActiveRecord" - # "active_record/errors".camelize #=> "ActiveRecord::Errors" - # - def self.camelize(lower_case_and_underscored_word, *args) - lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase } - end - - - # The reverse of +camelize+. Makes an underscored form from the expression in the string. - # - # Changes '::' to '/' to convert namespaces to paths. - # - # @example - # "ActiveRecord".underscore #=> "active_record" - # "ActiveRecord::Errors".underscore #=> active_record/errors - # - def self.underscore(camel_cased_word) - camel_cased_word.to_const_path - end - - # Capitalizes the first word and turns underscores into spaces and strips _id. - # Like titleize, this is meant for creating pretty output. - # - # @example - # "employee_salary" #=> "Employee salary" - # "author_id" #=> "Author" - def self.humanize(lower_case_and_underscored_word) - lower_case_and_underscored_word.to_s.gsub(/_id$/, '').tr('_', ' ').capitalize - end - - # Removes the module part from the expression in the string - # - # @example - # "ActiveRecord::CoreExtensions::String::Inflections".demodulize #=> "Inflections" - # "Inflections".demodulize #=> "Inflections" - def self.demodulize(class_name_in_module) - class_name_in_module.to_s.gsub(/^.*::/, '') - end - - # Create the name of a table like Rails does for models to table names. This method - # uses the pluralize method on the last word in the string. - # - # @example - # "RawScaledScorer".tableize #=> "raw_scaled_scorers" - # "EnlargedTestis".tableize #=> "enlarged_testes" - # "egg_and_ham".tableize #=> "egg_and_hams" - # "fancyCategory".tableize #=> "fancy_categories" - def self.tableize(class_name) - words = class_name.to_const_path.tr('/', '_').split('_') - words[-1] = pluralize(words[-1]) - words.join('_') - end - - # Creates a foreign key name from a class name. - # - # @example - # "Message".foreign_key #=> "message_id" - # "Admin::Post".foreign_key #=> "post_id" - def self.foreign_key(class_name, key = "id") - underscore(demodulize(class_name.to_s)) << "_" << key.to_s - end - - # Constantize tries to find a declared constant with the name specified - # in the string. It raises a NameError when the name is not in CamelCase - # or is not initialized. - # - # @example - # "Module".constantize #=> Module - # "Class".constantize #=> Class - def self.constantize(camel_cased_word) - unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word - raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!" - end - - Object.module_eval("::#{$1}", __FILE__, __LINE__) - end -end - -@singular_of = {} -@plural_of = {} - -@singular_rules = [] -@plural_rules = [] - -class << self - # Defines a general inflection exception case. - # - # ==== Parameters - # singular:: - # singular form of the word - # plural:: - # plural form of the word - # - # ==== Examples - # - # Here we define erratum/errata exception case: - # - # English::Inflect.word "erratum", "errata" - # - # In case singular and plural forms are the same omit - # second argument on call: - # - # English::Inflect.word 'information' - def self.word(singular, plural=nil) - plural = singular unless plural - singular_word(singular, plural) - plural_word(singular, plural) - end - - def self.clear(type = :all) - if type == :singular || type == :all - @singular_of = {} - @singular_rules = [] - @singularization_rules, @singularization_regex = nil, nil - end - if type == :plural || type == :all - @singular_of = {} - @singular_rules = [] - @singularization_rules, @singularization_regex = nil, nil - end - end - - - # Define a singularization exception. - # - # ==== Parameters - # singular:: - # singular form of the word - # plural:: - # plural form of the word - def self.singular_word(singular, plural) - @singular_of[plural] = singular - @singular_of[plural.capitalize] = singular.capitalize - end - - # Define a pluralization exception. - # - # ==== Parameters - # singular:: - # singular form of the word - # plural:: - # plural form of the word - def self.plural_word(singular, plural) - @plural_of[singular] = plural - @plural_of[singular.capitalize] = plural.capitalize - end - - # Define a general rule. - # - # ==== Parameters - # singular:: - # ending of the word in singular form - # plural:: - # ending of the word in plural form - # whole_word:: - # for capitalization, since words can be - # capitalized (Man => Men) # - # ==== Examples - # Once the following rule is defined: - # English::Inflect.rule 'y', 'ies' - # - # You can see the following results: - # irb> "fly".plural - # => flies - # irb> "cry".plural - # => cries - # Define a general rule. - - def self.rule(singular, plural, whole_word = false) - singular_rule(singular, plural) - plural_rule(singular, plural) - word(singular, plural) if whole_word - end - - # Define a singularization rule. - # - # ==== Parameters - # singular:: - # ending of the word in singular form - # plural:: - # ending of the word in plural form - # - # ==== Examples - # Once the following rule is defined: - # English::Inflect.singular_rule 'o', 'oes' - # - # You can see the following results: - # irb> "heroes".singular - # => hero - def self.singular_rule(singular, plural) - @singular_rules << [singular, plural] - end - - # Define a plurualization rule. - # - # ==== Parameters - # singular:: - # ending of the word in singular form - # plural:: - # ending of the word in plural form - # - # ==== Examples - # Once the following rule is defined: - # English::Inflect.singular_rule 'fe', 'ves' - # - # You can see the following results: - # irb> "wife".plural - # => wives - def self.plural_rule(singular, plural) - @plural_rules << [singular, plural] - end - - # Read prepared singularization rules. - def self.singularization_rules - if defined?(@singularization_regex) && @singularization_regex - return [@singularization_regex, @singularization_hash] - end - # No sorting needed: Regexen match on longest string - @singularization_regex = Regexp.new("(" + @singular_rules.map {|s,p| p}.join("|") + ")$", "i") - @singularization_hash = Hash[*@singular_rules.flatten].invert - [@singularization_regex, @singularization_hash] - end - - # Read prepared pluralization rules. - def self.pluralization_rules - if defined?(@pluralization_regex) && @pluralization_regex - return [@pluralization_regex, @pluralization_hash] - end - @pluralization_regex = Regexp.new("(" + @plural_rules.map {|s,p| s}.join("|") + ")$", "i") - @pluralization_hash = Hash[*@plural_rules.flatten] - [@pluralization_regex, @pluralization_hash] - end - - attr_reader :singular_of, :plural_of - - # Convert an English word from plural to singular. - # - # "boys".singular #=> boy - # "tomatoes".singular #=> tomato - # - # ==== Parameters - # word:: word to singularize - # - # ==== Returns - # :: singularized form of word - # - # ==== Notes - # Aliased as singularize (a Railism) - def self.singular(word) - if result = singular_of[word] - return result.dup - end - result = word.dup - regex, hash = singularization_rules - result.sub!(regex) {|m| hash[m]} - singular_of[word] = result - return result - end - - # Alias for #singular (a Railism). - # - alias_method(:singularize, :singular) - - # Convert an English word from singular to plural. - # - # "boy".plural #=> boys - # "tomato".plural #=> tomatoes - # - # ==== Parameters - # word:: word to pluralize - # - # ==== Returns - # :: pluralized form of word - # - # ==== Notes - # Aliased as pluralize (a Railism) - def self.plural(word) - # special exceptions - return "" if word == "" - if result = plural_of[word] - return result.dup - end - result = word.dup - regex, hash = pluralization_rules - result.sub!(regex) {|m| hash[m]} - plural_of[word] = result - return result - end - - # Alias for #plural (a Railism). - alias_method(:pluralize, :plural) - end - - # One argument means singular and plural are the same. - - word 'equipment' - word 'fish' - word 'grass' - word 'hovercraft' - word 'information' - word 'milk' - word 'money' - word 'moose' - word 'plurals' - word 'postgres' - word 'rain' - word 'rice' - word 'series' - word 'sheep' - word 'species' - word 'status' - - # Two arguments defines a singular and plural exception. - word 'alias' , 'aliases' - word 'analysis' , 'analyses' - word 'axis' , 'axes' - word 'basis' , 'bases' - word 'buffalo' , 'buffaloes' - word 'cactus' , 'cacti' - word 'crisis' , 'crises' - word 'criterion' , 'criteria' - word 'cross' , 'crosses' - word 'datum' , 'data' - word 'diagnosis' , 'diagnoses' - word 'drive' , 'drives' - word 'erratum' , 'errata' - word 'goose' , 'geese' - word 'index' , 'indices' - word 'life' , 'lives' - word 'louse' , 'lice' - word 'matrix' , 'matrices' - word 'medium' , 'media' - word 'mouse' , 'mice' - word 'movie' , 'movies' - word 'octopus' , 'octopi' - word 'ox' , 'oxen' - word 'phenomenon' , 'phenomena' - word 'plus' , 'plusses' - word 'potato' , 'potatoes' - word 'quiz' , 'quizzes' - word 'status' , 'status' - word 'status' , 'statuses' - word 'Swiss' , 'Swiss' - word 'testis' , 'testes' - word 'thesaurus' , 'thesauri' - word 'thesis' , 'theses' - word 'thief' , 'thieves' - word 'tomato' , 'tomatoes' - word 'torpedo' , 'torpedoes' - word 'vertex' , 'vertices' - word 'wife' , 'wives' - - # One-way singularization exception (convert plural to singular). - - # General rules. - rule 'person' , 'people', true - rule 'shoe' , 'shoes', true - rule 'hive' , 'hives', true - rule 'man' , 'men', true - rule 'child' , 'children', true - rule 'news' , 'news', true - rule 'rf' , 'rves' - rule 'af' , 'aves' - rule 'ero' , 'eroes' - rule 'man' , 'men' - rule 'ch' , 'ches' - rule 'sh' , 'shes' - rule 'ss' , 'sses' - rule 'ta' , 'tum' - rule 'ia' , 'ium' - rule 'ra' , 'rum' - rule 'ay' , 'ays' - rule 'ey' , 'eys' - rule 'oy' , 'oys' - rule 'uy' , 'uys' - rule 'y' , 'ies' - rule 'x' , 'xes' - rule 'lf' , 'lves' - rule 'ffe' , 'ffes' - rule 'afe' , 'aves' - rule 'ouse' , 'ouses' - # more cases of words ending in -oses not being singularized properly - # than cases of words ending in -osis -# rule 'osis' , 'oses' - rule 'ox' , 'oxes' - rule 'us' , 'uses' - rule '' , 's' - - # One-way singular rules. - - singular_rule 'of' , 'ofs' # proof - singular_rule 'o' , 'oes' # hero, heroes - singular_rule 'f' , 'ves' - - # One-way plural rules. - - #plural_rule 'fe' , 'ves' # safe, wife - plural_rule 's' , 'ses' - plural_rule 'ive' , 'ives' # don't want to snag wife - plural_rule 'fe' , 'ves' # don't want to snag perspectives - -end