Adding custom boolean methods for DateTime columns in ActiveRecord

DB columns visualized in a house with columns

Is it possible to have ActiveRecord automagically create boolean methods for DateTime columns in your models?

Let’s say you have a model House which has a few timestamp columns such as construction_started_at, construction_finished_at, purchased_at, inspection_started_at, moved_in_at, and sold_at. It’s nice that your model stores this data, but you want to be able to ask questions like “Has construction on this house started? (House#construction_started?)”, or “Have the buyers moved in? (House#moved_in?)” etc. You can of course define the methods individually:

class House < ApplicationRecord
  def construction_started?
    construction_started_at.present?
  end

  def construction_finished?
    construction_finished_at.present?
  end

  # and so on...
end

This may work for you if all you have is one model with a few fields, but if you foresee yourself writing these methods all over your codebase because you have multiple models (like OfficeBuilding, Factory, ShoppingMall, and ApartmentBuilding for example), you may start to wonder if there is an approach that will save you time, both now and in the future if you have to change anything about how these methods work. Is there a way to DRY this up?

Dynamically define methods in a concern

In Rails, when you have some logic that needs to be shared across multiple models, consider creating a “concern”. A concern, in Rails, is a Ruby module which extends ActiveSupport::Concern. ActiveSupport::Concern gives you, the module author, a couple of convenience methods that make it easy to two do things:

  1. The included method gives you the ability to call class methods like validates, define_method and others when the concern is included into your model.
  2. The class_methods method gives you the ability to define class level methods on the model that is including your concern.

Before we start, it’d be nice to figure out what we want the “user experience” of this to look like. For our example problem, I think I’d want something that works like this:

class House < ApplicationRecord
  include DateTimeToBoolean

  date_time_to_boolean :construction_started_at, :construction_finished_at,
                       :purchased_at, :inspection_started_at

end

# this should "automagically" give me the ability to do things like house.construction_started?, house.purchased? etc

Now it is a bit clearer what we need to do make this happen.

module DateTimeToBoolean
  extend ActiveSupport::Concern

  class_methods do
    def date_time_to_boolean(*columns)
      columns.each do |column|
        # remove the _at suffix, assuming that your time columns end with _at. If they don't you'll
        # either want to modify the next line, or add a parameter to your method to specify the
        # method name
        column = column.to_s.gsub(/_at$/, '').to_sym
        define_method("#{column}?") do
          public_send(column).present?
        end
      end
    end
  end
end

We’re defining a class method in the “includer” (House, in our example) called date_time_to_boolean, that takes in a list of arguments corresponding to the DateTime fields we want to generate boolean methods for. date_time_to_boolean loops over these fields, and defines a boolean method for each of them which answers the question of if the given field is “truthy”.

Now, we can include this concern in any model that needs to have boolean methods automatically created for its DateTime fields.

Conclusion

Using Ruby meta-programming (define_method and send) and the ActiveSupport::Concern, we’ve seen how it is possible to dynamically create convenience methods for DateTime columns in our models. Hopefully, it should not be too much of a stretch to imagine how this might be done for other types of columns and methods.

If you got value out of this article, consider subscribing to my email newsletter to get notified whenever I publish a new one.

Want to be notified when I publish a new article?