[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

Standard Library patches - rational, delegate

Dave Burt

5/11/2005 2:31:00 PM

Hi,

I have two small things I would like to see changed in the standard library:
delegate.rb
rational.rb

I would like to hear from the list:
* how this might happen
* any comments on my changes

The first is a small change to Delegate to prevent infinite recursion from
occurring when an object is assigned to delegate to itself.

The second are some enhancements to Rational, mainly to make it more
friendly with Floats and Strings, allowing rationals to be made like
Rational("2/3"), Rational("1.2e-10"), Rational(0.5), as well as adding to_r
methods to String and Float.
I've added approximation methods (similar to those recently discussed),
which are useful when trying to reduce an approximate Float to its original
representation, like this:
1.6.to_r #=> Rational(3602879701896397, 2251799813685248)
1.6.to_r.approximate #=> Rational(8, 5)
I've also fixed to_f so that it doesn't return NaN for rationals with large
denominators, and added radix selection in to_s like Integers have.

Thanks,
Dave


>diff -u delegate.rb.bak delegate.rb
--- delegate.rb.bak Thu May 12 00:16:14 2005
+++ delegate.rb Thu May 12 00:16:17 2005
@@ -73,7 +73,7 @@
end

def __setobj__(obj)
- @_sd_obj = obj
+ @_sd_obj = obj unless eql?(obj)
end

def clone
@@ -110,7 +110,7 @@
@_dc_obj
end
def __setobj__(obj)
- @_dc_obj = obj
+ @_dc_obj = obj unless eql?(obj)
end
def clone
super


>diff -u rational.rb.bak rational.rb
--- rational.rb.bak Fri May 07 17:48:23 2004
+++ rational.rb Wed May 11 23:48:15 2005
@@ -10,7 +10,7 @@
# class Rational < Numeric
# (include Comparable)
#
-# Rational(a, b) --> a/b
+# Rational(n, d=1) --> n/d
#
# Rational::+
# Rational::-
@@ -23,13 +23,19 @@
# Rational::<=>
# Rational::to_i
# Rational::to_f
-# Rational::to_s
+# Rational::to_s(base=10)
+# Rational::trim(max_d)
+# Rational::approximate(err=1e-12)
+# Rational::inv
#
# Integer::gcd
# Integer::lcm
# Integer::gcdlcm
# Integer::to_r
#
+# Float::to_r
+# String::to_r
+#
# Fixnum::**
# Fixnum::quo
# Bignum::**
@@ -37,11 +43,36 @@
#

def Rational(a, b = 1)
- if a.kind_of?(Rational) && b == 1
- a
- else
+ if b == 1
+ case a
+ when Rational
+ a
+ when String
+ case a
+ when /\//
+ n, d = a.split('/', 2)
+ Rational(n) / Rational(d)
+ when /e/
+ mant, exp = a.split('e', 2)
+ Rational(mant) * (10 ** Integer(exp))
+ when /\./
+ i, f = a.split('.', 2)
+ Rational(Integer(i)) + Rational(Integer(f), 10 ** f.length)
+ else
+ Rational(Integer(a))
+ end
+ when Float
+ a.to_r
+ when Integer
+ Rational.new!(a)
+ 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: #{num.inspect}")
end

class Rational < Numeric
@@ -237,17 +268,79 @@
end

def to_f
- @numerator.to_f/@denominator.to_f
+ r = if @denominator > (1 << 1022)
+ self.trim(1 << 1022)
+ else
+ self
+ end
+ r.numerator.to_f / r.denominator.to_f
end

- def to_s
+ def to_s(base = 10)
if @denominator == 1
- @numerator.to_s
+ @numerator.to_s(base)
else
- @numerator.to_s+"/"+@denominator.to_s
+ @numerator.to_s(base) + "/" + @denominator.to_s(base)
+ end
+ 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
+ p "mod==0" if mod == 0
+ app
+ end
+
+ # Return the inverse
+ def inv
+ Rational(@denominator, @numerator)
+ end
+
def to_r
self
end
@@ -257,7 +350,7 @@
end

def hash
- @numerator.hash ^ @denominator.hash
+ @numerator.hash ^ @denominator.hash ^ 1.hash
end

attr :numerator
@@ -326,6 +419,75 @@
return gcd, (a.div(gcd)) * b
end

+end
+
+class Float
+ # Return Rational(top, bot), where top and bot are a pair of co-prime
+ # integers such that x = top/bot.
+ #
+ # The conversion is done exactly, without rounding.
+ # bot > 0 guaranteed.
+ # Some form of binary fp is assumed.
+ # Pass NaNs or infinities at your own risk.
+ #
+ # >>> rational(10.0)
+ # rational(10L, 1L)
+ # >>> rational(0.0)
+ # rational(0L)
+ # >>> rational(-.25)
+ # rational(-1L, 4L)
+ def to_r
+ x = self
+
+ if x == 0.0
+ return Rational(0, 1)
+ end
+ signbit = false
+ if x < 0
+ x = -x
+ signbit = true
+ end
+ f, e = Math.frexp(x)
+ raise unless 0.5 <= f and f < 1.0
+ # 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
+ top = 0
+ # invariant: x = (top + f) * 2**e exactly
+ while f > 0.0
+ f = Math.ldexp(f, chunk)
+ digit = f.to_i
+ raise unless digit >> chunk == 0
+ top = (top << chunk) | digit
+ f = f - digit
+ raise unless 0.0 <= f and f < 1.0
+ e = e - chunk
+ end
+ raise if top == 0
+
+ # now x = top * 2**e exactly; fold in 2**e
+ r = Rational(top, 1)
+ if e > 0
+ r *= 2**e
+ else
+ r /= 2**-e
+ end
+ if signbit
+ -r
+ else
+ r
+ end
+ end
+end # class Float
+
+class String
+ # Convert to Rational, returning Rational(0) if it's can't be converted
+ def to_r
+ Rational(self) rescue Rational(0)
+ end
end

class Fixnum


6 Answers

gabriele renzi

5/12/2005 8:17:00 AM

0

Dave Burt ha scritto:
> Hi,
>
> I have two small things I would like to see changed in the standard library:
> delegate.rb
> rational.rb
>
> I would like to hear from the list:
> * how this might happen
> * any comments on my changes
>
> The first is a small change to Delegate to prevent infinite recursion from
> occurring when an object is assigned to delegate to itself.

if this is a fix you can point out the bug on the ruby issue tracker on
rubyforge, and attach the patch. Or you can post it on ruby-core.

> The second are some enhancements to Rational, mainly to make it more
> friendly with Floats and Strings, allowing rationals to be made like
> Rational("2/3"), Rational("1.2e-10"), Rational(0.5), as well as adding to_r
> methods to String and Float.

This sounds more of an RCR, use RCRChive.net .

> I've added approximation methods (similar to those recently discussed),
> which are useful when trying to reduce an approximate Float to its original
> representation, like this:
> 1.6.to_r #=> Rational(3602879701896397, 2251799813685248)
> 1.6.to_r.approximate #=> Rational(8, 5)
> I've also fixed to_f so that it doesn't return NaN for rationals with large
> denominators, and added radix selection in to_s like Integers have.

I wonder if this would'nt cause a problem for new users which could get
the false sense of security from float operations.
Also, have you thought of intehration with BigDecimal ?
And finaly.. I don't love the to_x methods (since there is so little
room for them), but in general I think your patch makes sense.

Luke Galea

5/12/2005 12:15:00 PM

0

I know I definately needed those rational methods for a meal planner I'm
working on..

One more thing that might be useful for rational.rb: a to_s method for
Rational that produces results for 3/2 as '1 1/2'.. Not sure what that would
best be called but I think it's a good change to bundle in there..

On Wednesday 11 May 2005 10:35, Dave Burt wrote:
> Hi,
>
> I have two small things I would like to see changed in the standard
> library: delegate.rb
> rational.rb
>
> I would like to hear from the list:
> * how this might happen
> * any comments on my changes
>
> The first is a small change to Delegate to prevent infinite recursion from
> occurring when an object is assigned to delegate to itself.
>
> The second are some enhancements to Rational, mainly to make it more
> friendly with Floats and Strings, allowing rationals to be made like
> Rational("2/3"), Rational("1.2e-10"), Rational(0.5), as well as adding to_r
> methods to String and Float.
> I've added approximation methods (similar to those recently discussed),
> which are useful when trying to reduce an approximate Float to its original
> representation, like this:
> 1.6.to_r #=> Rational(3602879701896397, 2251799813685248)
> 1.6.to_r.approximate #=> Rational(8, 5)
> I've also fixed to_f so that it doesn't return NaN for rationals with large
> denominators, and added radix selection in to_s like Integers have.
>
> Thanks,
> Dave
>
> >diff -u delegate.rb.bak delegate.rb
>
> --- delegate.rb.bak Thu May 12 00:16:14 2005
> +++ delegate.rb Thu May 12 00:16:17 2005
> @@ -73,7 +73,7 @@
> end
>
> def __setobj__(obj)
> - @_sd_obj = obj
> + @_sd_obj = obj unless eql?(obj)
> end
>
> def clone
> @@ -110,7 +110,7 @@
> @_dc_obj
> end
> def __setobj__(obj)
> - @_dc_obj = obj
> + @_dc_obj = obj unless eql?(obj)
> end
> def clone
> super
>
> >diff -u rational.rb.bak rational.rb
>
> --- rational.rb.bak Fri May 07 17:48:23 2004
> +++ rational.rb Wed May 11 23:48:15 2005
> @@ -10,7 +10,7 @@
> # class Rational < Numeric
> # (include Comparable)
> #
> -# Rational(a, b) --> a/b
> +# Rational(n, d=1) --> n/d
> #
> # Rational::+
> # Rational::-
> @@ -23,13 +23,19 @@
> # Rational::<=>
> # Rational::to_i
> # Rational::to_f
> -# Rational::to_s
> +# Rational::to_s(base=10)
> +# Rational::trim(max_d)
> +# Rational::approximate(err=1e-12)
> +# Rational::inv
> #
> # Integer::gcd
> # Integer::lcm
> # Integer::gcdlcm
> # Integer::to_r
> #
> +# Float::to_r
> +# String::to_r
> +#
> # Fixnum::**
> # Fixnum::quo
> # Bignum::**
> @@ -37,11 +43,36 @@
> #
>
> def Rational(a, b = 1)
> - if a.kind_of?(Rational) && b == 1
> - a
> - else
> + if b == 1
> + case a
> + when Rational
> + a
> + when String
> + case a
> + when /\//
> + n, d = a.split('/', 2)
> + Rational(n) / Rational(d)
> + when /e/
> + mant, exp = a.split('e', 2)
> + Rational(mant) * (10 ** Integer(exp))
> + when /\./
> + i, f = a.split('.', 2)
> + Rational(Integer(i)) + Rational(Integer(f), 10 ** f.length)
> + else
> + Rational(Integer(a))
> + end
> + when Float
> + a.to_r
> + when Integer
> + Rational.new!(a)
> + 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: #{num.inspect}")
> end
>
> class Rational < Numeric
> @@ -237,17 +268,79 @@
> end
>
> def to_f
> - @numerator.to_f/@denominator.to_f
> + r = if @denominator > (1 << 1022)
> + self.trim(1 << 1022)
> + else
> + self
> + end
> + r.numerator.to_f / r.denominator.to_f
> end
>
> - def to_s
> + def to_s(base = 10)
> if @denominator == 1
> - @numerator.to_s
> + @numerator.to_s(base)
> else
> - @numerator.to_s+"/"+@denominator.to_s
> + @numerator.to_s(base) + "/" + @denominator.to_s(base)
> + end
> + 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
> + p "mod==0" if mod == 0
> + app
> + end
> +
> + # Return the inverse
> + def inv
> + Rational(@denominator, @numerator)
> + end
> +
> def to_r
> self
> end
> @@ -257,7 +350,7 @@
> end
>
> def hash
> - @numerator.hash ^ @denominator.hash
> + @numerator.hash ^ @denominator.hash ^ 1.hash
> end
>
> attr :numerator
> @@ -326,6 +419,75 @@
> return gcd, (a.div(gcd)) * b
> end
>
> +end
> +
> +class Float
> + # Return Rational(top, bot), where top and bot are a pair of co-prime
> + # integers such that x = top/bot.
> + #
> + # The conversion is done exactly, without rounding.
> + # bot > 0 guaranteed.
> + # Some form of binary fp is assumed.
> + # Pass NaNs or infinities at your own risk.
> + #
> + # >>> rational(10.0)
> + # rational(10L, 1L)
> + # >>> rational(0.0)
> + # rational(0L)
> + # >>> rational(-.25)
> + # rational(-1L, 4L)
> + def to_r
> + x = self
> +
> + if x == 0.0
> + return Rational(0, 1)
> + end
> + signbit = false
> + if x < 0
> + x = -x
> + signbit = true
> + end
> + f, e = Math.frexp(x)
> + raise unless 0.5 <= f and f < 1.0
> + # 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
> + top = 0
> + # invariant: x = (top + f) * 2**e exactly
> + while f > 0.0
> + f = Math.ldexp(f, chunk)
> + digit = f.to_i
> + raise unless digit >> chunk == 0
> + top = (top << chunk) | digit
> + f = f - digit
> + raise unless 0.0 <= f and f < 1.0
> + e = e - chunk
> + end
> + raise if top == 0
> +
> + # now x = top * 2**e exactly; fold in 2**e
> + r = Rational(top, 1)
> + if e > 0
> + r *= 2**e
> + else
> + r /= 2**-e
> + end
> + if signbit
> + -r
> + else
> + r
> + end
> + end
> +end # class Float
> +
> +class String
> + # Convert to Rational, returning Rational(0) if it's can't be converted
> + def to_r
> + Rational(self) rescue Rational(0)
> + end
> end
>
> class Fixnum


Brian Schröder

5/12/2005 12:38:00 PM

0

On 12/05/05, Luke Galea <lgalea@gmmsolutions.com> wrote:
> I know I definately needed those rational methods for a meal planner I'm
> working on..
>
> One more thing that might be useful for rational.rb: a to_s method for
> Rational that produces results for 3/2 as '1 1/2'.. Not sure what that would
> best be called but I think it's a good change to bundle in there..
>

Here we go:
class Rational
def however_its_called()
self.divmod(1)
end
end

easy, isn't it?

regards,

Brian Schröder


--
http://ruby.brian-sch...

Stringed instrument chords: http://chordlist.brian-sch...


Dave Burt

5/12/2005 1:25:00 PM

0

"Luke Galea" <lgalea@gmmsolutions.com> commented:
>I know I definately needed those rational methods for a meal planner I'm
> working on..
>
> One more thing that might be useful for rational.rb: a to_s method for
> Rational that produces results for 3/2 as '1 1/2'.. Not sure what that would
> best be called but I think it's a good change to bundle in there..

Taking Brian's idea one step further:

def to_mixed_fraction
self.divmod(1).join(' ')
end



Dave Burt

5/12/2005 1:25:00 PM

0

"gabriele renzi" <surrender_it@remove-yahoo.it> kindly suggested...
> Dave Burt ha scritto:
>> Hi,
>>
>> I have two small things I would like to see changed in the standard
>> library:
>> delegate.rb
>> rational.rb
>
> if this is a fix you can point out the bug on the ruby issue tracker on
> rubyforge, and attach the patch. Or you can post it on ruby-core.

Does ruby-core deal with standard library as well as core?

>> The second are some enhancements to Rational, mainly to make it more
>> friendly with Floats and Strings, allowing rationals to be made like
>> Rational("2/3"), Rational("1.2e-10"), Rational(0.5), as well as adding
>> to_r methods to String and Float.
>
> This sounds more of an RCR, use RCRChive.net .

You've suggested ruby-core and RCR; I thought that as it's not a language
change, they're not the right avenues. You have to do an explicit "require"
to get these libraries.

Is it appropriate to submit an RCR for a standard library change?

>> 1.6.to_r #=> Rational(3602879701896397, 2251799813685248)
>> 1.6.to_r.approximate #=> Rational(8, 5)
>
> I wonder if this would'nt cause a problem for new users which could get
> the false sense of security from float operations.
> Also, have you thought of intehration with BigDecimal ?
> And finaly.. I don't love the to_x methods (since there is so little room
> for them), but in general I think your patch makes sense.

By default, the conversion is exact - the Rational is an exact
representation of the Float's value. You need to explicitly invoke an
approximation method.

Reciprocal integration with BigDecimal is a good suggestion, thank you for
it.

Thanks very much for the feedback, Gabriele.

Dave.


Dave Burt

5/12/2005 1:31:00 PM

0

> Reciprocal integration with BigDecimal is a good suggestion, thank you for
> it.

.... and it's already provided by requiring 'bigdecimal/util' (You get
Rational#to_d and BigDecimal#to_r).