Skip to main content

To Monkey Patch Ruby

·3 mins

This is the first of a series of posts I intend to write in a new “J3RN vs J3RN” style. Essentially, I will be stating a problem with two or more possible solutions, taking both sides in turn, and hopefully ultimately reaching a well thought out solution to the problem.

The Problem #

For my Time Tracker application, I want to have a pretty date and time picker. However, Ruby only natively understands a handful of formats and “American date”, the format used by the pretty date and time picker, is not one of them.

The time specified in the date and picker, available as a string, needs to be converted to a Time instance on save, then converted back into the American date string when displayed.

My first solution was to put two methods in a controller, from_american_date and to_american_date, which would perform these functions. However, this approach felt messy. Why should the controller know about converting time formats? What if I need this functionality in another controller?

I saw two alternatives: Monkey patching Time and creating a new module, AmericanDate.

To Monkey Patch #

If you look at Ruby’s Time class, you will notice that it has an iso8601 class method as well as some rfc**** class methods whose purposes are to parse strings from those formats into Time instances. Additionally, each of these class methods has a corresponding instance method that converts the Time instance into the proper string. Thus, it would make sense to simply define a Time.american_date method to parse Time instances from American date strings and a Time#american_date method to convert Time instances into American date strings.1

Since Ruby modules can only provide instance methods through include, we will need to separate the class and instance methods into separate modules, which will be added to Time through extend and include, respectively.

# lib/core_extensions/time/custom_formats.rb
module CoreExtensions
  module Time
    module CustomFormats
      AMERICAN_DATE_FORMAT = "%m/%d/%Y %H:%M %p"

      module InstanceMethods
        def american_date
          strftime(AMERICAN_DATE_FORMAT)
        end
      end

      module ClassMethods
        def american_date str
          strptime(str, AMERICAN_DATE_FORMAT)
        end
      end
    end
  end
end

This results in the following initializer:

# config/initializers/monkey_patching.rb
Time.extend CoreExtensions::Time::CustomFormats::ClassMethods
Time.include CoreExtensions::Time::CustomFormats::InstanceMethods

Why do it this way? It’s elegant. It fits with the other methods in Time and follows Rails conventions.

Not to Monkey Patch #

Ruby gems are almost all modules. Perhaps because of this, perhaps simply because modules are cool, modules are a common way to encapsulate functionality in Ruby, even outside of gems. In fact, this is what the lib directory in a Rails application is for. In order to move our methods out of the controller, we would simply create a module in the lib directory.

# lib/american_date.rb
module AmericanDate
  AMERICAN_DATE_FORMAT = "%m/%d/%Y %H:%M %p"

  def to_time str
    Time.strptime(str, AMERICAN_DATE_FORMAT).in_time_zone
  end

  def to_string time
    time.strftime(AMERICAN_DATE_FORMAT)
  end
end

Done! Wasn’t that simple?

Why do it this way? It’s easy. If I one day want to turn this into a gem (though an American date gem already exists), it would be pretty simple.

Conclusion #

I’m going to monkey patch Time and add the method to ApplicationHelper. While the module solution is simpler, it doesn’t come near the elegance of the monkey patching solution. Hopefully, Monkey patching responsibly will help to avoid many of its pitfalls.

1 The style of monkey patching I used came from a great blog post by Justin Weiss.