Skip to content

Instantly share code, notes, and snippets.

@mieko
Created April 21, 2014 11:59
Show Gist options
  • Save mieko/11140735 to your computer and use it in GitHub Desktop.
Save mieko/11140735 to your computer and use it in GitHub Desktop.
module Dirt
# This is the simplest inflector that would be useful. It doesn't have a list
# of any preloaded irregulars (e.g., Person -> People).
#
# Use Dirt::Inflector::irregular! to add your special cases.
#
# You can create an Inflector with #new, as expected, but the system has
# a default inflector, which is accessed via .instance. Calling methods
# on the class delegates to the default instance.
#
# We don't add methods to string, as everything in the world does that, and
# we don't want to clash.
class Inflector
# Regular expressions that convert plural forms into single forms, in
# priority order.
SINGULARIZE_REGEXPS = {
/(ss)es$/ => '\1',
/(ss)$/ => '\1',
/(s)$/ => '',
}
# Regular expressions that convert singular forms into plural forms, in
# priority order
PLURALIZE_REGEXPS = {
/(es)$/ => '\1',
/(s)$/ => '\1es',
/(.)$/ => '\1s',
}
def initialize
# We keep two inverted copies so we don't have to linearly
# search by value in one case or the other.
@irr_singular = {}
@irr_plural = {}
# @acronym entries have values equal to their keys. This is a trick
# stolen from Rails to avoid a lot of `@acronyms.include?` tests.
@acronyms = {}
# {/regexp/ => sub-pattern} replacement map for humanized words
@humans = {}
@singularize_regexps = SINGULARIZE_REGEXPS.dup
@pluralize_regexps = PLURALIZE_REGEXPS.dup
end
# Accepts:
# irregular!(singular, plural)
# or
# irregular!(singular1 => plural1,
# singularN => PluralN, ...)
def irregular!(singular, plural = nil)
# Were we passed the hash syntax?
if plural.nil? && singular.is_a?(Hash)
singular.each do |k,v|
irregular!(k, v)
end
else
@irr_singular[singular] = plural
@irr_plural[plural] = singular
end
end
def plural!(pattern, replacement)
@uncountable.delete(pattern) if pattern.is_a?(String)
@uncountable.delete(replacement)
@pluralize_regexps[pattern] = replacement
end
def singular!(pattern, replacement)
@uncountable.delete(pattern) if pattern.is_a?(String)
@uncountable.delete(replacement)
@singularize_regexps[pattern] = replacement
end
# When humanizing, if a word matches `regexp`, it'll be processed with
# `replacement`, a regexp replacement pattern.
def human!(regexp, replacement = nil)
if replacement.nil? && regexp.is_a?(Hash)
regexp.each do |k, v|
human!(k, v)
end
else
@humans[regexp] = replacement
end
end
# `term` is considered its own plural and single. For example:
#
# Inflector.uncountable!('fish')
# Inflector.pluralize('fish') # => 'fish'
# Inflector.singularize('fish') # => 'fish'
#
# # Note: the pseudo-pluralized form still gets the default behavior
# Inflector.singularize('fishes') # => 'fish'
def uncountable!(term)
irregular!(term => term)
end
# `term` is considered an acronym, and shouldnt be lowercased when
# camelized
def acronym!(term)
@acronyms[term] = term
@acronym_regexp = nil
end
# Applys the inflector methods in `args' to word, in-order. For example,
# the following two are identical:
#
# Inflector.pluralize(Inflector.underscore('User'))
# Inflector.chain('User', :pluralize, :underscore)
#
# This is useful as we don't mix in our methods into String.
def chain(word, *args)
args.each do |sym|
word = send(sym, *Array(word))
end
word
end
# Returns the pluralized form of `word`, respecting exceptions specified
# with `irregurlar!`
def pluralize(word)
apply_inflections(word, @pluralize_regexps, @irr_plural)
end
# Returns the singlular form of `word`, respecting exceptions specified
# with `irregular!`
def singularize(word)
apply_inflections(word, @singularize_regexps, @irr_singular)
end
# Turns CamelCase into under_score_case.
def underscore(word)
# RE's ripped straight from Rails and made compatible with Opal.
word.gsub('::', '/') \
.gsub(/(?:([A-Za-z\d])|^)(#{acronym_regexp})(?=\b|[^a-z])/) \
{ "#{$1}#{$1 && '_'}#{$2.downcase}" } \
.gsub(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') \
.gsub(/([a-z\d])([A-Z])/,'\1_\2') \
.gsub('-', '_') \
.downcase
end
# Returns the "humanized english" version of an underscored word.
# Regexps that were added with 'human!' are taken into account, and
# the first one that matches is applied.
def humanize(word)
@humans.each do |re, rep|
if word.match(re)
word = word.gsub(re, rep)
break
end
end
word.gsub(/_id$/, "") \
.gsub('_', ' ') \
.gsub(/([a-z\d]*)/) \
{ |match| @acronyms[match] || match.downcase } \
.gsub(/^\w/) { $&.upcase }
end
# The inverse of underscore.
# some_thing -> SomeThing
# app/some_model -> App::SomeModel
def camelize(word)
word.sub(/^[a-z\d]*/) \
{ @acronyms[$&] || humanize($&) } \
.gsub(/([_\/][a-z\d]*)/) { |v|
o, t = v[0], v[1..-1]
o = '' if o == '_'
"#{o}#{@acronyms[t] || humanize(t)}"
}.gsub('/', '::')
end
alias_method :classify, :camelize
private
# Finds the first entry in regexp_map that matches word, and returns the
# substitution. Transformations in irregular_map are attempted first.
# Failing any applicable transformation, word itself is returned.
def apply_inflections(word, regexp_map, irregular_map)
return irreg if (irreg = irregular_map[word])
regexp_map.each do |k, v|
if k.is_a?(Regexp)
return word.gsub(k, v) if word.match(k)
else
return v if word == k
end
end
return word
end
# Returns an OR-separated, escaped regexp that'll match any entry in
# the acronym list
def acronym_regexp
if @acronym_regexp.nil?
escaped = @acronyms.keys.map {|w| RegExp.escape(w)}
joined = escaped.join('|')
@acronym_regexp = /#{joined}/
end
@acronym_regexp
end
public
# This is the trick that makes Inflector itself act as an instance.
# Method calls on Inflector are delegated to Inflector.instance, which
# is a normal instance, with its own state, etc.
class << self
def instance
@instance ||= new
end
def respond_to?(method)
super || instance.respond_to?(method)
end
def method_missing(method, *args)
if instance.respond_to?(method)
return instance.send(method, *args)
end
super
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment