require 'hanami/utils/class_attribute' require 'hanami/utils/blank' module Hanami module Utils # String inflector # # @since 0.4.1 module Inflector # rubocop:disable Metrics/ModuleLength # Rules for irregular plurals # # @since 0.6.0 # @api private class IrregularRules # @since 0.6.0 # @api private def initialize(rules) @rules = rules end # @since 0.6.0 # @api private def add(key, value) @rules[key.downcase] = value.downcase end # @since 0.6.0 # @api private def ===(other) key = extract_last_alphanumeric_token(other) @rules.key?(key) || @rules.value?(key) end # @since 0.6.0 # @api private def apply(string) key = extract_last_alphanumeric_token(string) result = @rules[key] || @rules.rassoc(key).last prefix = if key == string.downcase string[0] else string[0..string.index(key)] end prefix + result[1..-1] end private # @since 1.3.3 # @api private def extract_last_alphanumeric_token(string) if string.downcase =~ /_([[:alpha:]]*)\z/ Regexp.last_match(1) else string.downcase end end end # @since 0.4.1 # @api private A = 'a'.freeze # @since 0.4.1 # @api private CH = 'ch'.freeze # @since 0.4.1 # @api private CHES = 'ches'.freeze # @since 0.4.1 # @api private EAUX = 'eaux'.freeze # @since 0.6.0 # @api private ES = 'es'.freeze # @since 0.4.1 # @api private F = 'f'.freeze # @since 0.4.1 # @api private I = 'i'.freeze # @since 0.4.1 # @api private ICE = 'ice'.freeze # @since 0.4.1 # @api private ICES = 'ices'.freeze # @since 0.4.1 # @api private IDES = 'ides'.freeze # @since 0.4.1 # @api private IES = 'ies'.freeze # @since 0.4.1 # @api private IFE = 'ife'.freeze # @since 0.4.1 # @api private IS = 'is'.freeze # @since 0.4.1 # @api private IVES = 'ives'.freeze # @since 0.4.1 # @api private MA = 'ma'.freeze # @since 0.4.1 # @api private MATA = 'mata'.freeze # @since 0.4.1 # @api private MEN = 'men'.freeze # @since 0.4.1 # @api private MINA = 'mina'.freeze # @since 0.6.0 # @api private NA = 'na'.freeze # @since 0.6.0 # @api private NON = 'non'.freeze # @since 0.4.1 # @api private O = 'o'.freeze # @since 0.4.1 # @api private OES = 'oes'.freeze # @since 0.4.1 # @api private OUSE = 'ouse'.freeze # @since 0.4.1 # @api private RSE = 'rse'.freeze # @since 0.4.1 # @api private RSES = 'rses'.freeze # @since 0.4.1 # @api private S = 's'.freeze # @since 0.4.1 # @api private SES = 'ses'.freeze # @since 0.4.1 # @api private SSES = 'sses'.freeze # @since 0.6.0 # @api private TA = 'ta'.freeze # @since 0.4.1 # @api private UM = 'um'.freeze # @since 0.4.1 # @api private US = 'us'.freeze # @since 0.4.1 # @api private USES = 'uses'.freeze # @since 0.4.1 # @api private VES = 'ves'.freeze # @since 0.4.1 # @api private X = 'x'.freeze # @since 0.4.1 # @api private XES = 'xes'.freeze # @since 0.4.1 # @api private Y = 'y'.freeze include Utils::ClassAttribute # Irregular rules for plurals # # @since 0.6.0 # @api private class_attribute :plurals self.plurals = IrregularRules.new( # irregular 'cactus' => 'cacti', 'child' => 'children', 'corpus' => 'corpora', 'foot' => 'feet', 'genus' => 'genera', 'goose' => 'geese', 'louse' => 'lice', 'man' => 'men', 'mouse' => 'mice', 'ox' => 'oxen', 'person' => 'people', 'quiz' => 'quizzes', 'sex' => 'sexes', 'testis' => 'testes', 'tooth' => 'teeth', 'woman' => 'women', # uncountable 'deer' => 'deer', 'equipment' => 'equipment', 'fish' => 'fish', 'information' => 'information', 'means' => 'means', 'money' => 'money', 'news' => 'news', 'offspring' => 'offspring', 'rice' => 'rice', 'series' => 'series', 'sheep' => 'sheep', 'species' => 'species', 'police' => 'police', # regressions # https://github.com/hanami/utils/issues/106 'album' => 'albums', 'area' => 'areas' ) # Irregular rules for singulars # # @since 0.6.0 # @api private class_attribute :singulars self.singulars = IrregularRules.new( # irregular 'cacti' => 'cactus', 'children' => 'child', 'corpora' => 'corpus', 'feet' => 'foot', 'genera' => 'genus', 'geese' => 'goose', 'lice' => 'louse', 'men' => 'man', 'mice' => 'mouse', 'oxen' => 'ox', 'people' => 'person', 'quizzes' => 'quiz', 'sexes' => 'sex', 'testes' => 'testis', 'teeth' => 'tooth', 'women' => 'woman', # uncountable 'deer' => 'deer', 'equipment' => 'equipment', 'fish' => 'fish', 'information' => 'information', 'means' => 'means', 'money' => 'money', 'news' => 'news', 'offspring' => 'offspring', 'rice' => 'rice', 'series' => 'series', 'sheep' => 'sheep', 'species' => 'species', 'police' => 'police', # fallback 'areas' => 'area', 'hives' => 'hive', 'phases' => 'phase', 'exercises' => 'exercise', 'releases' => 'release' ) # Block for custom inflection rules. # # @param [Proc] blk custom inflections # # @since 0.6.0 # # @see Hanami::Utils::Inflector.exception # @see Hanami::Utils::Inflector.uncountable # # @example # require 'hanami/utils/inflector' # # Hanami::Utils::Inflector.inflections do # exception 'analysis', 'analyses' # exception 'alga', 'algae' # uncountable 'music', 'butter' # end def self.inflections(&blk) class_eval(&blk) end # Adds a custom inflection exception # # @param [String] singular form # @param [String] plural form # # @since 0.6.0 # # @see Hanami::Utils::Inflector.inflections # @see Hanami::Utils::Inflector.uncountable # # @example # require 'hanami/utils/inflector' # # Hanami::Utils::Inflector.inflections do # exception 'alga', 'algae' # end def self.exception(singular, plural) add_to_inflecto(singular, plural) singulars.add(plural, singular) plurals.add(singular, plural) end # Since ROM uses Inflecto for it inferences, we need to add an exception to it # when one is registered against our Inflector. # @api private def self.add_to_inflecto(singular, plural) return unless defined? Inflecto Inflecto.inflections.irregular(singular, plural) end # Adds an uncountable word # # @param [Array] words # # @since 0.6.0 # # @see Hanami::Utils::Inflector.inflections # @see Hanami::Utils::Inflector.exception # # @example # require 'hanami/utils/inflector' # # Hanami::Utils::Inflector.inflections do # uncountable 'music', 'art' # end def self.uncountable(*words) Array(words).each do |word| exception(word, word) end end # Pluralizes the given string # # @param string [String] a string to pluralize # # @return [String,NilClass] the pluralized string, if present # # @api private # @since 0.4.1 # # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/MethodLength # rubocop:disable Style/PerlBackrefs def self.pluralize(string) return string if string.nil? || string =~ Utils::Blank::STRING_MATCHER case string when plurals plurals.apply(string) when /\A((.*)[^aeiou])ch\z/ $1 + CHES when /\A((.*)[^aeiou])y\z/ $1 + IES when /\A(.*)(ex|ix)\z/ $1 + ICES when /\A(.*)(eau|#{EAUX})\z/ $1 + EAUX when /\A(.*)x\z/ $1 + XES when /\A(.*)ma\z/ string + TA when /\A(.*)(um|#{A})\z/ $1 + A when /\A(buffal|domin|ech|embarg|her|mosquit|potat|tomat)#{O}\z/i $1 + OES when /\A(.*)(fee)\z/ $1 + $2 + S when /\A(.*)(?:([^f]))f[e]*\z/ $1 + $2 + VES when /\A(.*)us\z/ $1 + USES when /\A(.*)non\z/ $1 + NA when /\A((.*)[^aeiou])is\z/ $1 + ES when /\A(.*)ss\z/ $1 + SSES when /s\z/ string else string + S end end # rubocop:enable Style/PerlBackrefs # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/MethodLength # Singularizes the given string # # @param string [String] a string to singularize # # @return [String,NilClass] the singularized string, if present # # @api private # @since 0.4.1 # # rubocop:disable Metrics/AbcSize # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/MethodLength # rubocop:disable Metrics/PerceivedComplexity # rubocop:disable Style/PerlBackrefs def self.singularize(string) return string if string.nil? || string =~ Utils::Blank::STRING_MATCHER case string when singulars singulars.apply(string) when /\A.*[^aeiou]#{CHES}\z/ string.sub(CHES, CH) when /\A.*[^aeiou]#{IES}\z/ string.sub(IES, Y) when /\A.*#{EAUX}\z/ string.chop when /\A(.*)#{IDES}\z/ $1 + IS when /\A(.*)#{US}\z/ $1 + I when /\A(.*)#{RSES}\z/ $1 + RSE when /\A(.*)#{SES}\z/ $1 + S when /\A(.*)#{MATA}\z/ $1 + MA when /\A(.*)#{OES}\z/ $1 + O when /\A(.*)#{MINA}\z/ $1 + MEN when /\A(.*)#{XES}\z/ $1 + X when /\A(.*)#{IVES}\z/ $1 + IFE when /\A(.*)#{VES}\z/ $1 + F when /\A(.*)#{I}\z/ $1 + US when /\A(.*)ae\z/ $1 + A when /\A(.*)na\z/ $1 + NON when /\A(.*)#{A}\z/ $1 + UM when /[^s]\z/ string else string.chop end end # rubocop:enable Style/PerlBackrefs # rubocop:enable Metrics/AbcSize # rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/MethodLength # rubocop:enable Metrics/PerceivedComplexity end end end