Float -> Rational numbers?

I’m curious about the possibility to convert Float64 numbers to Rational numbers for exact computations. I’m a little puzzled by the following:

julia> x = 2.64
2.64

julia> y = Rational(x)
5944751508129055//2251799813685248

julia> z = 264//100
66//25

julia> Float64(y)
2.64

julia> Float64(z)
2.64

julia> y == z
false

julia> Float64(y) == Float64(z)
true

Why does the Rational function (constructor?) produce such a horrendously ugly fraction?

1 Like

It was discussed recently: Bug in Julia rational array inplace multiplication

2 Likes

Ah. OK.

… I already noticed that Floats such as 2 + 2^{-1} + 2^{-3}, etc. (which can be represented accurately in binary numbers) work as I had hoped…

The rational constructor isn’t doing anything incorrect. There isn’t an exact floating point representation of 2.64. So when you create that float it isn’t exactly 2.64, because it can’t be. When you convert it to a rational it creates a rational of the actual value you have, which again isn’t exactly 2.64. If you want to do exact computations with rationals, you need to create them from exact representations of the numbers you want to use, not inexact floating point representations.

5 Likes

So the following function handles my problem:

julia> function rationalize(x;sigdigits=16)
       return Int(round(x*10^(sigdigits-1),digits=0))//10^(sigdigits-1)
       end
rationalize (generic function with 1 method)

julia> x = 2.64
2.64

julia> round(x;sigdigits=3)
2.64

julia> round(x;sigdigits=2)
2.6

julia> round(x;sigdigits=1)
3.0

julia> rationalize(x)
66//25

julia> Float64(ans)
2.64

julia> rationalize(x;sigdigits=3)
66//25

julia> rationalize(x;sigdigits=2)
13//5

julia> Float64(ans)
2.6

where I have assumed that Float64 numbers have approximately 15 significant digits.

There’s already a built-in rationalize function:

julia> rationalize(2.64)
66//25

The Rational constructor and convert do exact conversion, however. If you want to do inexact conversion, you need to explicitly call rationalize.

6 Likes

Ah, so I have reinvented the wheel :-o .

I’m not sure that using such a function is a good idea, as it suffers from several issues:

issue #1: your implementation works only for number of magnitude approximately equal to 1:

julia> rationalize(1e-20)
0//1

This would be relatively easy to fix, taking into account the magnitude of x in the scaling

issue #2: this one is more severe IMO: your rationalize implementation can give the same rational representation for two different FP numbers:

julia> x = 0.1
0.1

julia> rationalize(x)
1//10

julia> y = nextfloat(x)
0.10000000000000002

julia> rationalize(y)
1//10

I don’t think this is a desired property for something that is designed to work around FP limitations but, depending on your use case, it might be enough. At least, you should be aware of it…

2 Likes

The built-in function doesn’t have this issue:

julia> rationalize(0.1)
1//10

julia> rationalize(nextfloat(0.1))
300239975158034//3002399751580339
3 Likes

…but the built-in function makes 10^{-20} equal to 0//1.

Also note the following:

julia> x = 2.64
2.64

julia> y = Rational(x)
5944751508129055//2251799813685248

julia> z = rationalize(x)
66//25

julia> x == y
true

julia> x == z
false

julia> y == z
false

I.e. the Rational conversion is exact: y is a rational representation of same value as x (which is a floating-point approximation of the decimal value 2.64), whereas z is not exactly equal to x or y but is exactly equal to the decimal value 2.64.

3 Likes

If it’s buggy, file issues!

3 Likes

Yes, because rationalize produces Rational{Int} by default, and an Int is not large enough to store the denominator in this case.

Everything works if large enough types are used for the numerator and denominator:

julia> rationalize(1e-20)
0//1

julia> rationalize(BigInt, 1e-20)
1//99999999999999983616

julia> rationalize(BigInt, 1e20)
99999999999999983616//1

I don’t think it is!

4 Likes
julia> rationalize(BigInt,1e-20;tol=1e-36)
1//99999999999999983616

julia> rationalize(BigInt,1e-20;tol=1e-37)
1//100000000000000000000

That’s again because 1e-20 is not exactly representable:

julia> big(1) / big(100000000000000000000)
1.000000000000000000000000000000000000000000000000000000000000000000000000000004e-20

julia> big(1) / big(99999999999999983616)
1.000000000000000163840000000000026843545600000004398046511104000720575940379275e-20
3 Likes