Using method_missing in Ruby

I first learned about method_missing when Christopher Bennage asked me about it. Even after looking it up and explaining it to Christopher, I didn't find any practical use for it in the things I was doing.

At least until tonight, that is.

I've been working on expanding Hair Forecast into Canada. I have the Canadian weather data described in active record like so:

    create_table :canadaforecasts do |t|
      t.column :fcstdate, :date
      t.column :lat, :float
      t.column :lon, :float
    end

    create_table :canadaparameters do |t|
      t.column :canadaforecast_id, :integer
      t.column :parameter, :string
      t.column :interval, :int
      t.column :value, :float
    end

As you can imagine from that, a canadaforecast has_many

canadaparameters. Each parameter is one of T for temperature, D for dewpoint, W for wind, etc. Each parameter has an interval, which is described in hours past 0:00 UTC. This is so I can have each weather parameter for morning, day, night, etc.

Now here's the interesting part. When I'm retrieving a forecast, and wish to access temperature for the 12th hour of the period, I would end up with this:

forecast = Canadaforecast.find(some_criteria)
forecast.canadaparameters.find_by_parameter_and_interval("T", 12)

That second line just ain't that pretty. Ruby is supposed to be pretty. So I decided to simplify it.

I could create a fetch method in the Canadaforecast model that simply takes two args (parameter and interval) and returns the value. So then I'd have a bunch of lines like:

t = forecast.fetch("T", 12)
d = forecast.fetch("D", 12)

But what if I thought the following was more readable and just plain looked cool?
t = forecast.T(12)
d = forecast.D(12)

Well, method_missing is the tool for this. Consider the following model in my Rails app:
class Canadaforecast < ActiveRecord::Base
  has_many :canadaparameters
 
  Valid_parameters = [ "T", "D", "C", "W", "P", "H" ]
 
  def method_missing(method, *args)
        if Valid_parameters.include?(method.to_s)
          fetch(method.to_s, args[0])
        else
          super
        end
  end
 
  private
  def fetch(parametername, interval)
    parameter = self.canadaparameters.find_by_parameter_and_interval(parametername, interval)
    if parameter.nil?
      nil
    else
      parameter.value
    end
  end
end

The method_missing method checks to see if you are trying one of the valid forecast parameters as a method. If not it just sends you back to the super and throws an error. If you have used a valid parameter as a method, then it calls the simple fetch method and passes in the parameter and interval you are trying to get. Note how method_missing provides an array of args.

Now I can fetch any forecast parameter for any interval with very easy to read code.

No model would be complete without a proper test, so just to show off my (lack of) testing skills, here's the test for the model above:

require File.dirname(__FILE__) + '/../test_helper'
 
class CanadaForecastTest < Test::Unit::TestCase
  fixtures :canadaforecasts, :canadaparameters
 
  def test_fetching_parameters_by_using_parameter_name_as_method
    forecast = Canadaforecast.find(1)
    assert_not_nil forecast  
    value = forecast.H(12)
    assert_equal 10, value
  end
 
  def test_fetching_parameters_not_found
    forecast = Canadaforecast.find(1)
    assert_not_nil forecast  
    value = forecast.H(1)
    assert_nil value
  end
end