PageRenderTime 44ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/activesupport/lib/active_support/inflector/methods.rb

https://github.com/eparreno/rails
Ruby | 391 lines | 181 code | 31 blank | 179 comment | 22 complexity | a326558aea72fe5fb2534cbdb0aa1078 MD5 | raw file
  1. require "active_support/inflections"
  2. require "active_support/core_ext/regexp"
  3. module ActiveSupport
  4. # The Inflector transforms words from singular to plural, class names to table
  5. # names, modularized class names to ones without, and class names to foreign
  6. # keys. The default inflections for pluralization, singularization, and
  7. # uncountable words are kept in inflections.rb.
  8. #
  9. # The Rails core team has stated patches for the inflections library will not
  10. # be accepted in order to avoid breaking legacy applications which may be
  11. # relying on errant inflections. If you discover an incorrect inflection and
  12. # require it for your application or wish to define rules for languages other
  13. # than English, please correct or add them yourself (explained below).
  14. module Inflector
  15. extend self
  16. # Returns the plural form of the word in the string.
  17. #
  18. # If passed an optional +locale+ parameter, the word will be
  19. # pluralized using rules defined for that language. By default,
  20. # this parameter is set to <tt>:en</tt>.
  21. #
  22. # pluralize('post') # => "posts"
  23. # pluralize('octopus') # => "octopi"
  24. # pluralize('sheep') # => "sheep"
  25. # pluralize('words') # => "words"
  26. # pluralize('CamelOctopus') # => "CamelOctopi"
  27. # pluralize('ley', :es) # => "leyes"
  28. def pluralize(word, locale = :en)
  29. apply_inflections(word, inflections(locale).plurals)
  30. end
  31. # The reverse of #pluralize, returns the singular form of a word in a
  32. # string.
  33. #
  34. # If passed an optional +locale+ parameter, the word will be
  35. # singularized using rules defined for that language. By default,
  36. # this parameter is set to <tt>:en</tt>.
  37. #
  38. # singularize('posts') # => "post"
  39. # singularize('octopi') # => "octopus"
  40. # singularize('sheep') # => "sheep"
  41. # singularize('word') # => "word"
  42. # singularize('CamelOctopi') # => "CamelOctopus"
  43. # singularize('leyes', :es) # => "ley"
  44. def singularize(word, locale = :en)
  45. apply_inflections(word, inflections(locale).singulars)
  46. end
  47. # Converts strings to UpperCamelCase.
  48. # If the +uppercase_first_letter+ parameter is set to false, then produces
  49. # lowerCamelCase.
  50. #
  51. # Also converts '/' to '::' which is useful for converting
  52. # paths to namespaces.
  53. #
  54. # camelize('active_model') # => "ActiveModel"
  55. # camelize('active_model', false) # => "activeModel"
  56. # camelize('active_model/errors') # => "ActiveModel::Errors"
  57. # camelize('active_model/errors', false) # => "activeModel::Errors"
  58. #
  59. # As a rule of thumb you can think of +camelize+ as the inverse of
  60. # #underscore, though there are cases where that does not hold:
  61. #
  62. # camelize(underscore('SSLError')) # => "SslError"
  63. def camelize(term, uppercase_first_letter = true)
  64. string = term.to_s
  65. if uppercase_first_letter
  66. string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize }
  67. else
  68. string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
  69. end
  70. string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
  71. string.gsub!("/".freeze, "::".freeze)
  72. string
  73. end
  74. # Makes an underscored, lowercase form from the expression in the string.
  75. #
  76. # Changes '::' to '/' to convert namespaces to paths.
  77. #
  78. # underscore('ActiveModel') # => "active_model"
  79. # underscore('ActiveModel::Errors') # => "active_model/errors"
  80. #
  81. # As a rule of thumb you can think of +underscore+ as the inverse of
  82. # #camelize, though there are cases where that does not hold:
  83. #
  84. # camelize(underscore('SSLError')) # => "SslError"
  85. def underscore(camel_cased_word)
  86. return camel_cased_word unless /[A-Z-]|::/.match?(camel_cased_word)
  87. word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze)
  88. word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" }
  89. word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
  90. word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
  91. word.tr!("-".freeze, "_".freeze)
  92. word.downcase!
  93. word
  94. end
  95. # Tweaks an attribute name for display to end users.
  96. #
  97. # Specifically, performs these transformations:
  98. #
  99. # * Applies human inflection rules to the argument.
  100. # * Deletes leading underscores, if any.
  101. # * Removes a "_id" suffix if present.
  102. # * Replaces underscores with spaces, if any.
  103. # * Downcases all words except acronyms.
  104. # * Capitalizes the first word.
  105. #
  106. # The capitalization of the first word can be turned off by setting the
  107. # +:capitalize+ option to false (default is true).
  108. #
  109. # humanize('employee_salary') # => "Employee salary"
  110. # humanize('author_id') # => "Author"
  111. # humanize('author_id', capitalize: false) # => "author"
  112. # humanize('_id') # => "Id"
  113. #
  114. # If "SSL" was defined to be an acronym:
  115. #
  116. # humanize('ssl_error') # => "SSL error"
  117. #
  118. def humanize(lower_case_and_underscored_word, options = {})
  119. result = lower_case_and_underscored_word.to_s.dup
  120. inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
  121. result.sub!(/\A_+/, "".freeze)
  122. result.sub!(/_id\z/, "".freeze)
  123. result.tr!("_".freeze, " ".freeze)
  124. result.gsub!(/([a-z\d]*)/i) do |match|
  125. "#{inflections.acronyms[match] || match.downcase}"
  126. end
  127. if options.fetch(:capitalize, true)
  128. result.sub!(/\A\w/) { |match| match.upcase }
  129. end
  130. result
  131. end
  132. # Converts just the first character to uppercase.
  133. #
  134. # upcase_first('what a Lovely Day') # => "What a Lovely Day"
  135. # upcase_first('w') # => "W"
  136. # upcase_first('') # => ""
  137. def upcase_first(string)
  138. string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
  139. end
  140. # Capitalizes all the words and replaces some characters in the string to
  141. # create a nicer looking title. +titleize+ is meant for creating pretty
  142. # output. It is not used in the Rails internals.
  143. #
  144. # +titleize+ is also aliased as +titlecase+.
  145. #
  146. # titleize('man from the boondocks') # => "Man From The Boondocks"
  147. # titleize('x-men: the last stand') # => "X Men: The Last Stand"
  148. # titleize('TheManWithoutAPast') # => "The Man Without A Past"
  149. # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
  150. def titleize(word)
  151. humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize }
  152. end
  153. # Creates the name of a table like Rails does for models to table names.
  154. # This method uses the #pluralize method on the last word in the string.
  155. #
  156. # tableize('RawScaledScorer') # => "raw_scaled_scorers"
  157. # tableize('ham_and_egg') # => "ham_and_eggs"
  158. # tableize('fancyCategory') # => "fancy_categories"
  159. def tableize(class_name)
  160. pluralize(underscore(class_name))
  161. end
  162. # Creates a class name from a plural table name like Rails does for table
  163. # names to models. Note that this returns a string and not a Class (To
  164. # convert to an actual class follow +classify+ with #constantize).
  165. #
  166. # classify('ham_and_eggs') # => "HamAndEgg"
  167. # classify('posts') # => "Post"
  168. #
  169. # Singular names are not handled correctly:
  170. #
  171. # classify('calculus') # => "Calculus"
  172. def classify(table_name)
  173. # strip out any leading schema name
  174. camelize(singularize(table_name.to_s.sub(/.*\./, "".freeze)))
  175. end
  176. # Replaces underscores with dashes in the string.
  177. #
  178. # dasherize('puni_puni') # => "puni-puni"
  179. def dasherize(underscored_word)
  180. underscored_word.tr("_".freeze, "-".freeze)
  181. end
  182. # Removes the module part from the expression in the string.
  183. #
  184. # demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
  185. # demodulize('Inflections') # => "Inflections"
  186. # demodulize('::Inflections') # => "Inflections"
  187. # demodulize('') # => ""
  188. #
  189. # See also #deconstantize.
  190. def demodulize(path)
  191. path = path.to_s
  192. if i = path.rindex("::")
  193. path[(i + 2)..-1]
  194. else
  195. path
  196. end
  197. end
  198. # Removes the rightmost segment from the constant expression in the string.
  199. #
  200. # deconstantize('Net::HTTP') # => "Net"
  201. # deconstantize('::Net::HTTP') # => "::Net"
  202. # deconstantize('String') # => ""
  203. # deconstantize('::String') # => ""
  204. # deconstantize('') # => ""
  205. #
  206. # See also #demodulize.
  207. def deconstantize(path)
  208. path.to_s[0, path.rindex("::") || 0] # implementation based on the one in facets' Module#spacename
  209. end
  210. # Creates a foreign key name from a class name.
  211. # +separate_class_name_and_id_with_underscore+ sets whether
  212. # the method should put '_' between the name and 'id'.
  213. #
  214. # foreign_key('Message') # => "message_id"
  215. # foreign_key('Message', false) # => "messageid"
  216. # foreign_key('Admin::Post') # => "post_id"
  217. def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
  218. underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
  219. end
  220. # Tries to find a constant with the name specified in the argument string.
  221. #
  222. # constantize('Module') # => Module
  223. # constantize('Foo::Bar') # => Foo::Bar
  224. #
  225. # The name is assumed to be the one of a top-level constant, no matter
  226. # whether it starts with "::" or not. No lexical context is taken into
  227. # account:
  228. #
  229. # C = 'outside'
  230. # module M
  231. # C = 'inside'
  232. # C # => 'inside'
  233. # constantize('C') # => 'outside', same as ::C
  234. # end
  235. #
  236. # NameError is raised when the name is not in CamelCase or the constant is
  237. # unknown.
  238. def constantize(camel_cased_word)
  239. names = camel_cased_word.split("::".freeze)
  240. # Trigger a built-in NameError exception including the ill-formed constant in the message.
  241. Object.const_get(camel_cased_word) if names.empty?
  242. # Remove the first blank element in case of '::ClassName' notation.
  243. names.shift if names.size > 1 && names.first.empty?
  244. names.inject(Object) do |constant, name|
  245. if constant == Object
  246. constant.const_get(name)
  247. else
  248. candidate = constant.const_get(name)
  249. next candidate if constant.const_defined?(name, false)
  250. next candidate unless Object.const_defined?(name)
  251. # Go down the ancestors to check if it is owned directly. The check
  252. # stops when we reach Object or the end of ancestors tree.
  253. constant = constant.ancestors.inject(constant) do |const, ancestor|
  254. break const if ancestor == Object
  255. break ancestor if ancestor.const_defined?(name, false)
  256. const
  257. end
  258. # owner is in Object, so raise
  259. constant.const_get(name, false)
  260. end
  261. end
  262. end
  263. # Tries to find a constant with the name specified in the argument string.
  264. #
  265. # safe_constantize('Module') # => Module
  266. # safe_constantize('Foo::Bar') # => Foo::Bar
  267. #
  268. # The name is assumed to be the one of a top-level constant, no matter
  269. # whether it starts with "::" or not. No lexical context is taken into
  270. # account:
  271. #
  272. # C = 'outside'
  273. # module M
  274. # C = 'inside'
  275. # C # => 'inside'
  276. # safe_constantize('C') # => 'outside', same as ::C
  277. # end
  278. #
  279. # +nil+ is returned when the name is not in CamelCase or the constant (or
  280. # part of it) is unknown.
  281. #
  282. # safe_constantize('blargle') # => nil
  283. # safe_constantize('UnknownModule') # => nil
  284. # safe_constantize('UnknownModule::Foo::Bar') # => nil
  285. def safe_constantize(camel_cased_word)
  286. constantize(camel_cased_word)
  287. rescue NameError => e
  288. raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
  289. e.name.to_s == camel_cased_word.to_s)
  290. rescue ArgumentError => e
  291. raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
  292. end
  293. # Returns the suffix that should be added to a number to denote the position
  294. # in an ordered sequence such as 1st, 2nd, 3rd, 4th.
  295. #
  296. # ordinal(1) # => "st"
  297. # ordinal(2) # => "nd"
  298. # ordinal(1002) # => "nd"
  299. # ordinal(1003) # => "rd"
  300. # ordinal(-11) # => "th"
  301. # ordinal(-1021) # => "st"
  302. def ordinal(number)
  303. abs_number = number.to_i.abs
  304. if (11..13).include?(abs_number % 100)
  305. "th"
  306. else
  307. case abs_number % 10
  308. when 1; "st"
  309. when 2; "nd"
  310. when 3; "rd"
  311. else "th"
  312. end
  313. end
  314. end
  315. # Turns a number into an ordinal string used to denote the position in an
  316. # ordered sequence such as 1st, 2nd, 3rd, 4th.
  317. #
  318. # ordinalize(1) # => "1st"
  319. # ordinalize(2) # => "2nd"
  320. # ordinalize(1002) # => "1002nd"
  321. # ordinalize(1003) # => "1003rd"
  322. # ordinalize(-11) # => "-11th"
  323. # ordinalize(-1021) # => "-1021st"
  324. def ordinalize(number)
  325. "#{number}#{ordinal(number)}"
  326. end
  327. private
  328. # Mounts a regular expression, returned as a string to ease interpolation,
  329. # that will match part by part the given constant.
  330. #
  331. # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
  332. # const_regexp("::") # => "::"
  333. def const_regexp(camel_cased_word)
  334. parts = camel_cased_word.split("::".freeze)
  335. return Regexp.escape(camel_cased_word) if parts.blank?
  336. last = parts.pop
  337. parts.reverse.inject(last) do |acc, part|
  338. part.empty? ? acc : "#{part}(::#{acc})?"
  339. end
  340. end
  341. # Applies inflection rules for +singularize+ and +pluralize+.
  342. #
  343. # apply_inflections('post', inflections.plurals) # => "posts"
  344. # apply_inflections('posts', inflections.singulars) # => "post"
  345. def apply_inflections(word, rules)
  346. result = word.to_s.dup
  347. if word.empty? || inflections.uncountables.uncountable?(result)
  348. result
  349. else
  350. rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
  351. result
  352. end
  353. end
  354. end
  355. end