
Submitted by dburt (Tue Oct 11 00:47:36 UTC 2005)
The proposed solution allows Floats, Strings and BigDecimals to be passed to Rational(), adds a to_r method to String and Float, and adds reduction methods to Rational (trim(max_denominator) and approximate(max_err)).
Also Rational#to_f should not return NaN for Rationals with large numerators and denominators, as happens now if the numerator and denominator are not both convertible to Float.
Additionally, the following minor changes are included in the implementation below:
These are backwards-compatible changes that add needed functionality to the Rational class, allowing interoperability with Float.
Some people would expect that 0.1 would convert to Rational(1, 10); for some cases, that type of approximate conversion is needed. The best way to deal with this unambiguously is to always convert exactly by default, but to also provide approximation methods:
0.1.to_r #=> Rational(3602879701896397, 36028797018963968) 0.1.to_r.approximate #=> Rational(1, 10)
Here is some of what this RCR allows:
# Rational() is backwards-compatible
Rational(1) #=> Rational(1, 1)
Rational(1, 2) #=> Rational(1, 2)
# ... but also accepts floats
Rational(1.3) #=> Rational(5854679515581645, 4503599627370496)
# ... and strings
Rational("1.3") #=> Rational(13, 10)
Rational("1.2e-3") #=> Rational(3, 2500)
Rational("1/2") #=> Rational(1, 2)
Rational("1.5/2.3") #=> Rational(15, 23)
Rational("1.5", "2.3") #=> Rational(15, 23)
# Floats and strings have an explicit cast to_r method
"1.5/2.3".to_r #=> Rational(15, 23)
-0.25.to_r #=> Rational(-1, 4)
# You can get fractions back from the floats:
Rational(1.3).approximate #=> Rational(13, 10)
Rational(0.3333333333333).approximate #=> Rational(1, 3)
#
# An extension to Ruby's standard Rational library
#
# Includes conversions from String and Float, inversion, reduction methods
# (trim and approximate), and a to_s with a base conversion parameter.
#
# A bunch of this code is taken from the Python project:
# http://cvs.sourceforge.net/viewcvs.py/python/python/nondist/sandbox
# /rational/Rational.py?rev=1.3&view=markup
#
# Author: Dave Burt <dave at burt.id.au>
# Created: 5 May 2005
# Last modified: 11 Oct 2005
#
# Methods:
#
# Rational(n, d = 1) # replaced - now accepts Floats and Strings
# # like Rational(1.3e15) or Rational("4/5")
#
# Rational#to_s(n = 10) # replaced - now accepts a radix
# Rational#to_f # replaced - doesn't return NaN any more
# Rational#hash # replaced - r.hash==r.to_i.hash if r==r.to_i
# Rational#trim(max_d) # simplify approximately, set max denominator
# Rational#approximate(err = 1e-12) # simplify approximately, within +/-err
# Rational#inv # invert
#
# Rational::from_f(f) # use Rational(f)
# Rational::from_s(s) # use Rational(s)
#
# Float#to_r # converts to Rational, exactly
# String#to_r # converts to Rational if possible, or returns Rational(0)
#
require 'rational'
#
# This improved Rational() handles Floats and Strings, so you can do
# +Rational(1.3e15)+ and +Rational("2/3")+
#
##alias std_Rational Rational
def Rational(a, b = 1)
if b == 1
case a
when Rational
a
when String
Rational.from_s(a)
when Integer
Rational.new!(a)
when Float
Rational.from_f(a)
when BigDecimal
a.to_r
else
raise ArgumentError
end
elsif a.kind_of?(Integer) and b.kind_of?(Integer)
Rational.reduce(a, b)
else
Rational(a) / Rational(b)
end
rescue ArgumentError
raise ArgumentError.new("invalid value for Rational: #{a} / #{b}")
end
class Rational
#
# Cast to Float. This improved version won't return NaN for Rationals with
# large numerators or denominators.
#
def to_f
r = if @denominator > (1 << 1022) # Where did the extra bit go?
self.trim(1 << 1022) # (Python handles 1 << 1023)
else
self
end
r.numerator.to_f / r.denominator.to_f
end
#
# This hash function returns the same hash as the numerator itself if
# the denominator is 1.
#
def hash
@numerator.hash ^ @denominator.hash ^ 1.hash
end
#
# Return the closest rational number such that the denominator is at most
# +max_d+
#
def trim(max_d)
n, d = @numerator, @denominator
if max_d == 1
return Rational(n/d, 1)
end
last_n, last_d = 0, 1
current_n, current_d = 1, 0
begin
div, mod = n.divmod(d)
n, d = d, mod
before_last_n, before_last_d = last_n, last_d
next_n = last_n + current_n * div
next_d = last_d + current_d * div
last_n, last_d = current_n, current_d
current_n, current_d = next_n, next_d
end until mod == 0 or current_d >= max_d
if current_d == max_d
return Rational(current_n, current_d)
end
i = (max_d - before_last_d) / last_d
alternative_n = before_last_n + i*last_n
alternative_d = before_last_d + i*last_d
alternative = Rational(alternative_n, alternative_d)
last = Rational(last_n, last_d)
if (alternative - self).abs < (last - self).abs
alternative
else
last
end
end
#
# Return the simplest rational number within +err+
#
def approximate(err = Rational(1, 1e12))
r = self
n, d = @numerator, @denominator
last_n, last_d = 0, 1
current_n, current_d = 1, 0
begin
div, mod = n.divmod(d)
n, d = d, mod
next_n = last_n + current_n * div
next_d = last_d + current_d * div
last_n, last_d = current_n, current_d
current_n, current_d = next_n, next_d
app = Rational(current_n, current_d)
end until mod == 0 or (app - r).abs < err
app
end
#
# Return the inverse
#
def inv
Rational(@denominator, @numerator)
end
#
# Represent the fraction as a string, in the given base.
#
def to_s(base = 10)
if @denominator == 1
@numerator.to_s(base)
else
@numerator.to_s(base) + "/" + @denominator.to_s(base)
end
end
class << self
#
# Use Rational(s) instead.
#
def from_s(s)
unless s.respond_to?(:to_str)
raise TypeError.new("#{s.inspect} is not a String")
end
s = s.to_str
case s
when /\//
n, d = s.split('/', 2)
Rational(n) / Rational(d)
when /e/
mant, exp = s.split('e', 2)
Rational(mant) * (10 ** Integer(exp))
when /\./
i, f = s.split('.', 2)
Rational(Integer(i)) + Rational(Integer(f), 10 ** f.length)
else
Rational(Integer(s))
end
end
#
# Use Rational(x) instead.
#
def from_f(x)
raise TypeError.new("#{x} is not a Float") unless x.kind_of?(Float)
if x == 0.0
return Rational(0, 1)
end
signbit =
if x < 0
x = -x
true
else
false
end
f, e = Math.frexp(x)
# for Infinity and NaN, frexp returns [NaN, -1]
unless 0.5 <= f and f < 1.0
raise ArgumentError("invalid value for Rational: #{self}")
end
# x = f * 2**e exactly
# Suck up chunk bits at a time; 28 is enough so that we suck
# up all bits in 2 iterations for all known binary double-
# precision formats, and small enough to fit in an int.
chunk = 28
num = 0
# invariant: x = (num + f) * 2**e exactly
while f > 0.0
f = Math.ldexp(f, chunk)
digit = f.to_i
raise unless digit >> chunk == 0
num = (num << chunk) | digit
f = f - digit
raise unless 0.0 <= f and f < 1.0
e = e - chunk
end
raise if num == 0
# now x = num * 2**e exactly; fold in 2**e
r = Rational(num, 1)
if e > 0
r *= 2**e
else
r /= 2**-e
end
if signbit
-r
else
r
end
end
end
end
class Float
#
# Convert to Rational exactly, returning Rational(0) if float can't be
# converted.
#
# Return Rational(num, den), where num and den are a pair of co-prime
# integers such that x = num/den.
#
# The conversion is done exactly, without rounding. Use
# Rational#approximate to round.
#
# "0.1".to_r #=> Rational(1, 10)
# 0.1.to_r #=> Rational(3602879701896397, 36028797018963968)
# 0.1.to_r.approximate #=> Rational(1, 10)
#
def to_r
Rational(self) rescue Rational(0)
end
end # class Float
class String
#
# Convert the string into a Rational, returning Rational(0) if it
# can't be converted.
#
# Valid strings are valid Integer or Float literals, and may also include
# a slash (/) separating a numerator and denominator.
# "2/3".to_r #=> Rational(2, 3)
# "1/1.2e2".to_r #=> Rational(1, 120)
#
def to_r
Rational(self) rescue Rational(0)
end
end
if $0 == __FILE__
# http://www.dave.burt.id.au/ruby/rational_ext_test.rb
require 'rational_ext_test'
end

| Comments | Current voting | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|


RCRchive copyright © David Alan Black, 2003-2005.
Powered by .
What bits do you like? What bits don't you like? Why? -- Dave Burt
The main thing I dislike is this RCR is too complex for a single proposal. I see several proposals:
Here are a few things I don't like:
I do like that you are using Rational.from_* methods like my RCR. It promotes better encapsulation in addition to more flexibility if it was done elswhere. I do like the idea of having conversions from Float and String to Rational.
- Eric
Eric, thanks for the response. To your objections:
from_s and from_f exist because Rational() needs to perform both of these non-trivial operations, and Float and String's to_r can be defined in terms of Rational(), but not the other way around. Ideally, they would not be part of the public interface. I've moved their documentation to Rational().
This RCR is about conversions from float (and string) to rational.
What I mean by overloading is the same "overloading" functionality you get in java - where one method can have different functionalities depending on "type". In java you simply give multiple definitions for the same method and in ruby you have a case statement or some conditional based on "type" to do it. This clashes with the duck-typing concepts. I know this overloading (based on type) is done all over the ruby core. I would hope additional overloading could be minimized.
But it's a validating type-cast method -- it needs to accept objects of different types! And it's consistent: it's Rational(numerator, denominator), where denominator is optional and defaults to 1. Makes sense to me.
I know many disagree with me, but I just don't think type-based overloading is appropriate for ruby. You should just use multiple methods instead. I don't have a problem with other kinds of overloading (based on number of args, existence of block, arg flags, arbitrary non type-based condition of an arg, etc). I think the only value that overloading gives you is fewer method names to remember.