Saturday, February 9, 2008

RoR: Overriding Comparison Operators in Your Model

Ruby allows you to override everything. Ruby allows you to override everything using two lines of code. You just got to love ruby! This small entry is about overriding comparison operators (or, to be correct, it is about bulk-defining comparison operators for user class).

Common use case from Ruby on Rails is to define a simple model which is associating a Fixnum with some additional properties (like string name): examples are sport leagues (number), or bank account (balance), and so on. Whenever the primary objective of your model (say, League) is to store the number, you would want to override the comparison operators so that instead of that ugly code:

league.number > other_league.number
...
leagues.sort! {|a,b| a.number <=> b.number}
you would write this beautiful ruby:
league > other_league
...
leagues.sort!
But overriding each comparison operator would quickly make your model look like some java code or worse. Ruby "meta-programming" to the rescue:
Class League < ActiveRecord::Base
  %w(<=> == < > <= >=).each do |operator|
    define_method operator do |other|
      case other.class.to_s
        when "League"
          self.number.send(operator,other.number)
        when "Fixnum"
          self.number.send(operator,other)
      else
        raise ArgumentError, "Illegal argument"
      end
    end
  end
end
This not only would allow you to compare League with some other League in every possible perverted way you please, but also to compare apples and oranges, i.e. League and a Fixnum, and get an ArgumentError when comparing it to anything else.

You may find it useful to compare with a String, too, if a string attribute is sensible identifier for your model. We use send here to evaluate comparison operator for our number attribute, but you can send to String attributes just as easily (and to anything else which already has it's own comparison operators defined).

But, surely, while this code above defines 6 methods for you while keeping it DRY, there is a better way - using Comparable mixin. It would define 6 comparison methods (plus between?(min,max) method) all based on the <=> operator, and the code will look much clearer:

class League < ActiveRecord::Base
  include Comparable

  def <=>(other)
    case other.class.to_s
      when "League"
        self.number <=> other.number
      when "Fixnum"
        self.number <=> other
    else
      raise ArgumentError, "Illegal argument"
    end
  end
end
Have fun!

1 comment:

make sense