Keeping it Small and Simple

2007.12.31

Modifying Ruby’s Numeric class for fun

Filed under: Ruby Programming — Tags: , — Lorenzo E. Danielsson @ 02:22

Imagine you wanted to write a small program that calculates the length of the hypotenuse of a right-angled triangle. It might look something as follows:

1 #! /usr/bin/ruby
2
3 # Calculate the hypotenuse of a right-angled triangle.
4
5 abort "You should pass the lengths of the catheti" unless ARGV.size == 2
6
7 a, b = ARGV.shift.to_f, ARGV.shift.to_f
8 hyp = Math::sqrt(a ** 2 + b ** 2)
9 puts "The length of the hypotenuse is #{hyp}."

Notice something? Line 8 doesn’t look so nice. Wouldn’t it be nice if we could do a (a.sq + b.sq).sqrt instead? Well, you can.

The first thing we are going to do is add a method called Numeric#square. Since I am lazy I will also create alias it as Numeric#sqr. Here is a little test program (note that I added a Numeric#cube method as well, since I don’t have anything better to do with my time):

1 #! /usr/bin/ruby
2
3 # Calculate squares and cubes.
4
5 class Numeric
6   def square
7     return self ** 2
8   end
9   alias :sqr :square
10
11   def cube
12     return self ** 3
13   end
14 end
15
16 abort "You have to pass in some numbers." if ARGV.empty?
17
18 ARGV.each do |n|
19   begin
20     num = n =~ /\./ ? Float(n) : Integer(n)
21     puts "#{num}² = #{num.sqr}, #{num}³ = #{num.cube} (#{num.class})"
22   rescue
23     puts "Skipping #{n} because it doesn’t smell like a number.."
24     next
25   end
26 end

Now line 20 looks highly suspect. What it does is convert the string n to either a floating point number (Float) or an integer (Fixnum) depending on whether or not it finds a decimal point. I’m just doing this to demonstrate that Numeric#square is available to both Fixnum and Float objects. That’s why we chose to modify Numeric.

If you wonder how to get the ² character in your editor, in insert mode, hit CTRL+K followed by 2S. That is, if you are using vim. If you are using another editor, chances are you have to upgrade to the Professional Enterprise++ version of your software.

Running the program above on some numbers I get:
``` % ruby sc.rb 2 4 5.2 -2 2² = 4, 2³ = 8 (Fixnum) 4² = 16, 4³ = 64 (Fixnum) 5.2² = 27.04, 5.2³ = 140.608 (Float) -2² = 4, -2³ = -8 (Fixnum) ```

Adding Numeric#sqrt is similar. Let’s put it to the test!

1 #! /usr/bin/ruby
2
3 # Calculate square roots.
4
5 class Numeric
6   def sqrt
7     require complex if self < 0
8     Math::sqrt(self)
9   end
10 end
11
12 abort "I need some numbers to work with." if ARGV.empty?
13
14 ARGV.each do |arg|
15   begin
16     num = Float(arg)
17     puts "#{num} = #{num.sqrt}"
18   rescue
19     puts "You know, #{arg} is a weird number.."
20     next
21   end
22 end

This program contains one extra detail, which you can find on line 7. If the number is negative then we bring in the complex module before the call to Math::sqrt. That way it seamlessly uses complex numbers as and when they are needed. Now you may or may not think this is a good idea, but whatever the case may be, Ruby allows you to do whatever you think is right.

Testing this on a few numbers, I get:
``` % ruby sr.rb 100 -4.0 2.3 -64 √100.0 = 10.0 √-4.0 = 2.0i √2.3 = 1.51657508881031 √-64.0 = 8.0i ```

Back to our right-angled triangle

Okay, now that we have tested that all the details work as expected, we can get back to our program, which was all about calculating the hypotenuse of a right-angled triangle. Here is our new version of that program:

1 #! /usr/bin/ruby
2
3 # Calculate the hypotenuse of a right-angled triangle.
4
5 class Numeric
6   def sq
7     self ** 2
8   end
9
10   def sqrt
11     Math::sqrt self
12   end
13 end
14
15 abort "Please pass in the lengths of the catheti." unless ARGV.size == 2
16
17 a, b = ARGV.shift.to_f, ARGV.shift.to_f
18 hyp = (a.sq + b.sq).sqrt
19 puts "The length of the hypotenuse is #{hyp}."

I’ve cut out things from the Numeric class that we don’t need for our program, such as Numeric#cube and requiring complex, since we cannot get a negative number from the sum of two numbers squared. Also, I got extra lazy and shortened the square method to Numeric#sq (which seems to make sense, since square meters is abbreviated sq m).

Our second version of the hypotenuse calculator is a whole lot longer, 19 lines instead of 9 lines. But wasn’t it worth it, just to clean up that single ugly Math::sqrt(a ** 2 + b ** 2) expression? 😀