15 March 2007
All Things Being Equal...

Ruby, like many other languages, can test for equality. Like it’s contemporaries, the language’s equality operation returns true if two things are equal and false if not. For instance, when testing two real numbers in Ruby, equality acts as you’d expect

5.0/4 == 1.25                            # true
5.0/4 == 1.24                            # false

and works appropriately for other types as well.

This is just fine for numbers, until you have to fight round-off:

5.0/3 == 1.6666667                       # false

Because we’re dealing with arbitrary numeric precision in Ruby, there really isn’t a “close-enough” option. Instead we have to define it ourselves.

Before we jump into that however, let’s consider Ruby’s pattern matching facilities. Ruby defines pattern matching using an equal-tilde (=~) to match regular expressions against strings - the return value is the offset into the string of the start of the match, or nil if the string did not contain the pattern. Regular expressions are baked right into Ruby as part of the language, giving you direct access to matching.

The =~ was not chosen for matching patterns indiscriminately. In mathematics, the tilde combined with equals typically means congruent or approximately equal to. The expression on the left numerically matches the expression on the right, within some tolerance.

Well, since the =~ isn’t being used for Numerics in Ruby, there’s no reason we can’t make it do double duty. We can define a little code that will do our approximate numerical matching for us.

class Numeric

  @@epsilon = 0.0001

  def Numeric.epsilon
    @@epsilon
  end

  def Numeric.epsilon=(epsilon)
    @@epsilon = epsilon.abs
  end

  def approximately_equals(value,epsilon=@@epsilon)
    (self - value).abs < epsilon
  end

  alias =~ approximately_equals
end

Defining the operations on Numeric mean that any number will have the ability to do close matching as well as precise equality.

5.0/3 =~ 1.6666                          # true

The precision of the tests can be set by adjusting the epsilon value in Numeric, or specifying it directly when calling the approximately_equals method

Numeric.epsilon = 0.00001
5.0/3 =~ 1.6666                          # false
(5.0/3).approximately_equals(1.66,0.01)  # true
(5/3).approximately_equals(1,0.5)        # true

Yes, it is truly wonderful that Ruby gives you the ability to roll your own meanings to operations. Letting you deal with issues such as these really gets you past some gritty code issues with elegance.