From: Luke Galea Date: 2005-05-12T21:14:56+09:00 Subject: Re: Standard Library patches - rational, delegate 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