Thursday, March 15, 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.

1 comment:

jancel said...

Good writeup, this gives a good definition of =~ (Equals-tilde) and I feel that this would be an interesting thing to add to Numeric. Any suggestion for arranging these types of add-ons so when upgrading ruby, we can make sure we don't lose them (if I want this sort of change to be permanent). Also, testing is essential, are there any suggestions on how you might test/regression test this keeping everything as simple as possible?