Given an arbitrary large (or small) Rational
number that has a finite decimal representation, e.g.:
r = Rational(1, 2**15)
#=> (1/32768)
How can I get its full decimal value as a string?
The expected output for the above number is:
"0.000030517578125"
to_f
apparently doesn't work:
r.to_f
#=> 3.0517578125e-05
And sprintf
requires me to specify the number of digits:
sprintf('%.30f', r)
#=> "0.000030517578125000000000000000"
Most ten-year-olds know how to do that: use long division!1
Code
def finite_long_division(n,d)
return nil if d.zero?
sign = n*d >= 0 ? '' : '-'
n, d = n.abs, d.abs
pwr =
case n <=> d
when 1 then power(n,d)
when 0 then 0
else -power(d,n)-1
end
n *= 10**(-pwr) if pwr < 0
d *= 10**(pwr) if pwr >= 0
s = ld(n,d)
t = s.size == 1 ? '0' : s[1..-1]
"%s%s.%s x 10^%d" % [sign, s[0], t, pwr]
end
def power(n, d)
# n > d
ns = n.to_s
ds = d.to_s
pwr = ns.size - ds.size - 1
pwr += 1 if ns[0, ds.size].to_i >= ds.to_i
pwr
end
def ld(n,d)
s = ''
loop do # .with_object('') do |s|
m,n = n.divmod(d)
s << m.to_s
return s if n.zero?
n *= 10
end
end
Examples2
finite_long_division(1, 2**15)
#=> "3.0517578125 x 10^-5"
finite_long_division(-1, 2**15)
#=> "-3.0517578125 x 10^-5"
finite_long_division(-1, -2**15)
#=> "3.0517578125 x 10^-5"
finite_long_division(143, 16777216)
#=> "8.523464202880859375 x 10^-6"
143/16777216.0
#=> 8.52346420288086e-06
finite_long_division(8671,
803469022129495137770981046170581301261101496891396417650688)
#=> "1.079195309486679194852923588206549145803161531099624\
# 804222395643336829571798416196370119711226461255452\
# 67714596064934085006825625896453857421875 x 10^-56"
Recall that every rational number has either a decimal representation or contains an infinitely-repeating sequence of digits (e.g., 1/3 #=> 0.33333...
, 3227/555 #=> 5.8144144144...
and 1/9967 #=> 0.00010033109260559848...
3). This method would therefore never terminate if the rational was of the repeating sequence variety. Since one generally doesn't know in advance which type a rational number is, it might be useful to modify the method to first determine if the rational number has a finite decimal representation. It is known that a rational number n/d
which cannot be reduced (by removing common factors) has this property if and only if d
is divisible by 2
or by 5
and is not divisible by any other prime number.4 We could easily construct a method to determine if an already-reduced rational number has that property.
require 'prime'
def decimal_representation?(n, d)
primes = Prime.prime_division(d).map(&:first)
(primes & [2,5]).any? && (primes - [2, 5]).empty?
end
1 At least that was true when I was a kid.
2 See here for a partial list of rational numbers that have finite decimal representations.
3 This rational number's repeating sequence contains 9,966 digits.
4 Reference.
a = sprintf('%.30f', r)
a.gsub(/0*\z/,'')
That's all :) (or should :P)
It's not the best way, if the value have more than 30 decimals, you need to add more than 30 zeroes in sprintf. I think there is a better way to do it, but this way it works
Edited
require 'bigdecimal'
require 'bigdecimal/util'
b = BigDecimal.new(r, (r.denominator * r.numerator))
b.to_digits
Note about this solution. (r.denominator * r.numerator)
It's the precision, the precision will never be bigger than denominator * numerator (I think, but a mathematician can tell you this)
Edit 2
r = BigDecimal("1") / (BigDecimal("2") ** BigDecimal("99"))
r.to_digits
# Example
r = BigDecimal("1") / (BigDecimal("2")**BigDecimal("99"))
r.to_digits
# "0.000000000000000000000000000001577721810442023610823457130565572459346412870218046009540557861328125"
But reeeeelly big numbers, like:
r = BigDecimal("1") / (BigDecimal("2")**BigDecimal("999999999999"))
# RangeError: integer 999999999999 too big to convert to `int'
If you need something better, I think you need to got with your own implementation of of "string divisions".
Bigdecimal to_s
has an "F" option. It takes some converting to kick this rational into shape however.
require "bigdecimal"
r = Rational(1, 2**15)
p BigDecimal.new(r.to_f.to_s).to_s("F") # => "0.000030517578125"