How to make Rational#to_s return strings without denominator 1 again

Posted . Visible to the public.

The way Rational#to_s works on Ruby has changed from Ruby 1.9 on. Here is how to get the old behavior back.

You may want this for things where Rationals are being used, like when subtracting Date objects from one another.

What's happening?

Converting a Rational to a String usually does something like this:

1.8.7 > Rational(2, 3).to_s
=> "2/3"
1.9.3 > Rational(2, 3).to_s
=> "2/3"
2.0.0 > Rational(2, 3).to_s
=> "2/3"

However, when you have a Rational that simplifies to an integer, you will only get a String that represents that integer on Ruby 1.8:

1.8.7 > Rational(8, 4).to_s
=> "2"
1.9.3 > Rational(8, 4).to_s
=> "2/1"
2.0.0 > Rational(8, 4).to_s
=> "2/1"

This will come bite you in the butt when you subtract dates, since they use Rational representation internally.

>> (Date.tomorrow - Date.yesterday).to_s
>> "2/1"

Sure, you could just call to_i on that, but often times the results of such subtractions go into a view. When upgrading a Rails application from Ruby 1.8 to 1.9, this will cause significant pain, and under most circumstances you probably do not want a "2/1" representation anyway.

How to get the old behavior back

Ruby 1.8 behaves as expected, and for Ruby 1.9 and Ruby 2.0 we will just check if the rational number's denominator is 1 and omit it. Put this into a place like lib/core_ext/rational.rb or whatever floats your boat.

if RUBY_VERSION >= '1.9'
  class Rational

    def to_s_with_proper_integers
      if denominator == 1
        numerator.to_s
      else
        to_s_without_proper_integers
      end
    end
    alias_method_chain :to_s, :proper_integers

  end
end

If you don't have alias_method_chain (plain Ruby scripts, for example), just manually do what the method does by using 2 alias_method calls.

Here is a spec that goes along with the fix:

require 'spec_helper'

describe Rational do

  describe '#to_s' do
    it 'should return correct results for actual rationals, as usual' do
      Rational(6, 4).to_s.should == '3/2'
      Rational(1, 11).to_s.should == '1/11'
      Rational(-2, 3).to_s.should == '-2/3'
    end

    it 'should not return a rational representation of integer results, like Ruby 1.8 did' do
      Rational(2, 1).to_s.should == '2'
      Rational(12, 4).to_s.should == '3'
      Rational(-10, 5).to_s.should == '-2'
      Rational(0, 5).to_s.should == '0'
    end
  end

end

That patch should not have a negative impact on your application, since you are only changing the conversion of Rational to a String, not any mathematical logic of the Rational class.

Arne Hartherz
Last edit
License
Source code in this card is licensed under the MIT License.
Posted by Arne Hartherz to makandra dev (2013-03-22 11:43)