1 # encoding: utf-8
2 require 'singleton'
3 require 'iconv'
4 require 'kconv'
5
6 module ActiveSupport
7 # The Inflector transforms words from singular to plural, class names to table names, modularized class names to ones without,
8 # and class names to foreign keys. The default inflections for pluralization, singularization, and uncountable words are kept
9 # in inflections.rb.
10 #
11 # The Rails core team has stated patches for the inflections library will not be accepted
12 # in order to avoid breaking legacy applications which may be relying on errant inflections.
13 # If you discover an incorrect inflection and require it for your application, you'll need
14 # to correct it yourself (explained below).
15 module Inflector
16 extend self
17
18 # A singleton instance of this class is yielded by Inflector.inflections, which can then be used to specify additional
19 # inflection rules. Examples:
20 #
21 # ActiveSupport::Inflector.inflections do |inflect|
22 # inflect.plural /^(ox)$/i, '\1\2en'
23 # inflect.singular /^(ox)en/i, '\1'
24 #
25 # inflect.irregular 'octopus', 'octopi'
26 #
27 # inflect.uncountable "equipment"
28 # end
29 #
30 # New rules are added at the top. So in the example above, the irregular rule for octopus will now be the first of the
31 # pluralization and singularization rules that is runs. This guarantees that your rules run before any of the rules that may
32 # already have been loaded.
33 class Inflections
34 include Singleton
35
36 attr_reader :plurals, :singulars, :uncountables, :humans
37
38 def initialize
39 @plurals, @singulars, @uncountables, @humans = [], [], [], []
40 end
41
42 # Specifies a new pluralization rule and its replacement. The rule can either be a string or a regular expression.
43 # The replacement should always be a string that may include references to the matched data from the rule.
44 def plural(rule, replacement)
45 @uncountables.delete(rule) if rule.is_a?(String)
46 @uncountables.delete(replacement)
47 @plurals.insert(0, [rule, replacement])
48 end
49
50 # Specifies a new singularization rule and its replacement. The rule can either be a string or a regular expression.
51 # The replacement should always be a string that may include references to the matched data from the rule.
52 def singular(rule, replacement)
53 @uncountables.delete(rule) if rule.is_a?(String)
54 @uncountables.delete(replacement)
55 @singulars.insert(0, [rule, replacement])
56 end
57
58 # Specifies a new irregular that applies to both pluralization and singularization at the same time. This can only be used
59 # for strings, not regular expressions. You simply pass the irregular in singular and plural form.
60 #
61 # Examples:
62 # irregular 'octopus', 'octopi'
63 # irregular 'person', 'people'
64 def irregular(singular, plural)
65 @uncountables.delete(singular)
66 @uncountables.delete(plural)
67 if singular[0,1].upcase == plural[0,1].upcase
68 plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
69 singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
70 else
71 plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
72 plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
73 singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1])
74 singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1])
75 end
76 end
77
78 # Add uncountable words that shouldn't be attempted inflected.
79 #
80 # Examples:
81 # uncountable "money"
82 # uncountable "money", "information"
83 # uncountable %w( money information rice )
84 def uncountable(*words)
85 (@uncountables << words).flatten!
86 end
87
88 # Specifies a humanized form of a string by a regular expression rule or by a string mapping.
89 # When using a regular expression based replacement, the normal humanize formatting is called after the replacement.
90 # When a string is used, the human form should be specified as desired (example: 'The name', not 'the_name')
91 #
92 # Examples:
93 # human /_cnt$/i, '\1_count'
94 # human "legacy_col_person_name", "Name"
95 def human(rule, replacement)
96 @humans.insert(0, [rule, replacement])
97 end
98
99 # Clears the loaded inflections within a given scope (default is <tt>:all</tt>).
100 # Give the scope as a symbol of the inflection type, the options are: <tt>:plurals</tt>,
101 # <tt>:singulars</tt>, <tt>:uncountables</tt>, <tt>:humans</tt>.
102 #
103 # Examples:
104 # clear :all
105 # clear :plurals
106 def clear(scope = :all)
107 case scope
108 when :all
109 @plurals, @singulars, @uncountables = [], [], []
110 else
111 instance_variable_set "@#{scope}", []
112 end
113 end
114 end
115
116 # Yields a singleton instance of Inflector::Inflections so you can specify additional
117 # inflector rules.
118 #
119 # Example:
120 # ActiveSupport::Inflector.inflections do |inflect|
121 # inflect.uncountable "rails"
122 # end
123 def inflections
124 if block_given?
125 yield Inflections.instance
126 else
127 Inflections.instance
128 end
129 end
130
131 # Returns the plural form of the word in the string.
132 #
133 # Examples:
134 # "post".pluralize # => "posts"
135 # "octopus".pluralize # => "octopi"
136 # "sheep".pluralize # => "sheep"
137 # "words".pluralize # => "words"
138 # "CamelOctopus".pluralize # => "CamelOctopi"
139 def pluralize(word)
140 result = word.to_s.dup
141
142 if word.empty? || inflections.uncountables.include?(result.downcase)
143 result
144 else
145 inflections.plurals.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
146 result
147 end
148 end
149
150 # The reverse of +pluralize+, returns the singular form of a word in a string.
151 #
152 # Examples:
153 # "posts".singularize # => "post"
154 # "octopi".singularize # => "octopus"
155 # "sheep".singluarize # => "sheep"
156 # "word".singularize # => "word"
157 # "CamelOctopi".singularize # => "CamelOctopus"
158 def singularize(word)
159 result = word.to_s.dup
160
161 if inflections.uncountables.any? { |inflection| result =~ /#{inflection}\Z/i }
162 result
163 else
164 inflections.singulars.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
165 result
166 end
167 end
168
169 # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
170 # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
171 #
172 # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
173 #
174 # Examples:
175 # "active_record".camelize # => "ActiveRecord"
176 # "active_record".camelize(:lower) # => "activeRecord"
177 # "active_record/errors".camelize # => "ActiveRecord::Errors"
178 # "active_record/errors".camelize(:lower) # => "activeRecord::Errors"
179 def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
180 if first_letter_in_uppercase
181 lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
182 else
183 lower_case_and_underscored_word.first.downcase + camelize(lower_case_and_underscored_word)[1..-1]
184 end
185 end
186
187 # Capitalizes all the words and replaces some characters in the string to create
188 # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
189 # used in the Rails internals.
190 #
191 # +titleize+ is also aliased as as +titlecase+.
192 #
193 # Examples:
194 # "man from the boondocks".titleize # => "Man From The Boondocks"
195 # "x-men: the last stand".titleize # => "X Men: The Last Stand"
196 def titleize(word)
197 humanize(underscore(word)).gsub(/\b('?[a-z])/) { $1.capitalize }
198 end
199
200 # The reverse of +camelize+. Makes an underscored, lowercase form from the expression in the string.
201 #
202 # Changes '::' to '/' to convert namespaces to paths.
203 #
204 # Examples:
205 # "ActiveRecord".underscore # => "active_record"
206 # "ActiveRecord::Errors".underscore # => active_record/errors
207 def underscore(camel_cased_word)
208 camel_cased_word.to_s.gsub(/::/, '/').
209 gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
210 gsub(/([a-z\d])([A-Z])/,'\1_\2').
211 tr("-", "_").
212 downcase
213 end
214
215 # Replaces underscores with dashes in the string.
216 #
217 # Example:
218 # "puni_puni" # => "puni-puni"
219 def dasherize(underscored_word)
220 underscored_word.gsub(/_/, '-')
221 end
222
223 # Capitalizes the first word and turns underscores into spaces and strips a
224 # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
225 #
226 # Examples:
227 # "employee_salary" # => "Employee salary"
228 # "author_id" # => "Author"
229 def humanize(lower_case_and_underscored_word)
230 result = lower_case_and_underscored_word.to_s.dup
231
232 inflections.humans.each { |(rule, replacement)| break if result.gsub!(rule, replacement) }
233 result.gsub(/_id$/, "").gsub(/_/, " ").capitalize
234 end
235
236 # Removes the module part from the expression in the string.
237 #
238 # Examples:
239 # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections"
240 # "Inflections".demodulize # => "Inflections"
241 def demodulize(class_name_in_module)
242 class_name_in_module.to_s.gsub(/^.*::/, '')
243 end
244
245 # Replaces special characters in a string so that it may be used as part of a 'pretty' URL.
246 #
247 # ==== Examples
248 #
249 # class Person
250 # def to_param
251 # "#{id}-#{name.parameterize}"
252 # end
253 # end
254 #
255 # @person = Person.find(1)
256 # # => #<Person id: 1, name: "Donald E. Knuth">
257 #
258 # <%= link_to(@person.name, person_path(@person)) %>
259 # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
260 def parameterize(string, sep = '-')
261 # remove malformed utf8 characters
262 string = string.toutf8 unless string.is_utf8?
263 # replace accented chars with ther ascii equivalents
264 parameterized_string = transliterate(string)
265 # Turn unwanted chars into the seperator
266 parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep)
267 unless sep.blank?
268 re_sep = Regexp.escape(sep)
269 # No more than one of the separator in a row.
270 parameterized_string.gsub!(/#{re_sep}{2,}/, sep)
271 # Remove leading/trailing separator.
272 parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '')
273 end
274 parameterized_string.downcase
275 end
276
277
278 # Replaces accented characters with their ascii equivalents.
279 def transliterate(string)
280 Iconv.iconv('ascii//ignore//translit', 'utf-8', string).to_s
281 end
282
283 if RUBY_VERSION >= '1.9'
284 undef_method :transliterate
285 def transliterate(string)
286 warn "Ruby 1.9 doesn't support Unicode normalization yet"
287 string.dup
288 end
289
290 # The iconv transliteration code doesn't function correctly
291 # on some platforms, but it's very fast where it does function.
292 elsif "foo" != (Inflector.transliterate("föö") rescue nil)
293 undef_method :transliterate
294 def transliterate(string)
295 string.mb_chars.normalize(:kd). # Decompose accented characters
296 gsub(/[^\x00-\x7F]+/, '') # Remove anything non-ASCII entirely (e.g. diacritics).
297 end
298 end
299
300 # Create the name of a table like Rails does for models to table names. This method
301 # uses the +pluralize+ method on the last word in the string.
302 #
303 # Examples
304 # "RawScaledScorer".tableize # => "raw_scaled_scorers"
305 # "egg_and_ham".tableize # => "egg_and_hams"
306 # "fancyCategory".tableize # => "fancy_categories"
307 def tableize(class_name)
308 pluralize(underscore(class_name))
309 end
310
311 # Create a class name from a plural table name like Rails does for table names to models.
312 # Note that this returns a string and not a Class. (To convert to an actual class
313 # follow +classify+ with +constantize+.)
314 #
315 # Examples:
316 # "egg_and_hams".classify # => "EggAndHam"
317 # "posts".classify # => "Post"
318 #
319 # Singular names are not handled correctly:
320 # "business".classify # => "Busines"
321 def classify(table_name)
322 # strip out any leading schema name
323 camelize(singularize(table_name.to_s.sub(/.*\./, '')))
324 end
325
326 # Creates a foreign key name from a class name.
327 # +separate_class_name_and_id_with_underscore+ sets whether
328 # the method should put '_' between the name and 'id'.
329 #
330 # Examples:
331 # "Message".foreign_key # => "message_id"
332 # "Message".foreign_key(false) # => "messageid"
333 # "Admin::Post".foreign_key # => "post_id"
334 def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
335 underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
336 end
337
338 # Ruby 1.9 introduces an inherit argument for Module#const_get and
339 # #const_defined? and changes their default behavior.
340 if Module.method(:const_get).arity == 1
341 # Tries to find a constant with the name specified in the argument string:
342 #
343 # "Module".constantize # => Module
344 # "Test::Unit".constantize # => Test::Unit
345 #
346 # The name is assumed to be the one of a top-level constant, no matter whether
347 # it starts with "::" or not. No lexical context is taken into account:
348 #
349 # C = 'outside'
350 # module M
351 # C = 'inside'
352 # C # => 'inside'
353 # "C".constantize # => 'outside', same as ::C
354 # end
355 #
356 # NameError is raised when the name is not in CamelCase or the constant is
357 # unknown.
358 def constantize(camel_cased_word)
359 names = camel_cased_word.split('::')
360 names.shift if names.empty? || names.first.empty?
361
362 constant = Object
363 names.each do |name|
364 constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
365 end
366 constant
367 end
368 else
369 def constantize(camel_cased_word) #:nodoc:
370 names = camel_cased_word.split('::')
371 names.shift if names.empty? || names.first.empty?
372
373 constant = Object
374 names.each do |name|
375 constant = constant.const_get(name, false) || constant.const_missing(name)
376 end
377 constant
378 end
379 end
380
381 # Turns a number into an ordinal string used to denote the position in an
382 # ordered sequence such as 1st, 2nd, 3rd, 4th.
383 #
384 # Examples:
385 # ordinalize(1) # => "1st"
386 # ordinalize(2) # => "2nd"
387 # ordinalize(1002) # => "1002nd"
388 # ordinalize(1003) # => "1003rd"
389 def ordinalize(number)
390 if (11..13).include?(number.to_i % 100)
391 "#{number}th"
392 else
393 case number.to_i % 10
394 when 1; "#{number}st"
395 when 2; "#{number}nd"
396 when 3; "#{number}rd"
397 else "#{number}th"
398 end
399 end
400 end
401 end
402 end
403
404 # in case active_support/inflector is required without the rest of active_support
405 require 'active_support/inflections'
406 require 'active_support/core_ext/string/inflections'
407 unless String.included_modules.include?(ActiveSupport::CoreExtensions::String::Inflections)
408 String.send :include, ActiveSupport::CoreExtensions::String::Inflections
409 end