On Apr 3, 2013, at 6:30 PM, Tanaka Akira wrote:

> 2013/4/4 David MacMahon <davidm / astro.berkeley.edu>:
>>=20
>>>> f=3D57563.232824357045
>> =3D> 57563.232824357045
>>=20
>>>> puts "%016x\n"*5 % [f, f.to_r.to_f, f.to_s.to_f, f.to_s.to_r.to_f, =
f.rationalize.to_f].pack('D*').unpack('Q*')
>> 40ec1b67734c10e7
>> 40ec1b67734c10e7
>> 40ec1b67734c10e7
>> 40ec1b67734c10e6 <=3D=3D=3D String#to_r "error"
>> 40ec1b67734c10e7
>> =3D> nil
>=20
> I don't think that String#to_r is wrong.
>=20
> % ruby -e 'f=3D57563.232824357045
> p f, f.to_s, f.to_s.to_r
> '
> 57563.232824357045
> "57563.232824357045"
> (11512646564871409/200000000000)
>=20
> String#to_r is correct because
> 57563.232824357045 =3D=3D 11512646564871409/200000000000 in =
mathematical sense.
>=20
> The "error" is caused by Froat#to_s and it is expected.

Of course you're right about String#to_r being correct.  I think =
Float#to_s is correct as well.  I think the problem is actually in =
Rational#to_f.

Each distinct Float value has (or should have, IMHO) an unambiguous =
String representation such that f.to_s.to_f =3D=3D f, discounting NaN =
and Infinity for which this relationship doesn't hold due to a =
limitation (bug?) of String#to_f.

String#to_r works correctly as you pointed out.

The problem occurs because the Rational returned by String#to_r is =
reduced.  When converting the reduced fraction of this example to Float, =
Rational#to_f effectively computes:

>> 11512646564871409.to_f/200000000000.to_f
=3D> 57563.23282435704 <=3D=3D=3D does NOT equal original value

instead of the un-reduced computation of:

>> 57563232824357045.to_f/1000000000000.to_f
=3D> 57563.232824357045 <=3D=3D=3D DOES equal original value

As you can see, these two expressions do not product equal answers.  =
This limitation of Rational#to_f can also be seen by using BigDecimal to =
convert from Rational to Float:

>> class Rational
>>   def to_f_via_bd
>>     (BigDecimal.new(numerator)/denominator).to_f
>>   end
>> end

>> f=3D57563.232824357045
=3D> 57563.232824357045

>> f.to_s.to_r.to_f
=3D> 57563.23282435704 <=3D=3D=3D does NOT equal f

>> f.to_s.to_r.to_f_via_bd
=3D> 57563.232824357045 <=3D=3D=3D DOES equal f

This same limitation also explains the problem I saw with =
Float.rationalize:

>> 1.501852784991644e-17.rationalize.to_f
=3D> 1.5018527849916442e-17 <=3D=3D=3D does NOT equal original value

>> 1.501852784991644e-17.rationalize.to_f_via_bd
=3D> 1.501852784991644e-17 <=3D=3D=3D DOES equal original value

In an earlier message I wrote: "Converting Float to Rational is =
'perfect' in that the conversion back to Float results in the same =
(limited precision) value."  The above examples shows that this is not =
true.  I think this could be considered a bug in Rational#to_f.

> Anyway, I'm sure now that Float#rationalize should not be used
> internally/automatically.

I agree with this.  Float#rationalize returns a Rational that is an =
approximation of the Float.  This approximation is good enough that =
converting the Rational back to Float (avoiding intermediate rounding =
errors!) returns the original Float, but the Rational is NOT an exact =
representation.  This is not a problem when using a single DateTime =
object, but performing math on a DateTime object that contains such an =
approximation seems like a bad idea.

On the other hand, Float#to_s works well and String#to_r returns a =
Rational that exactly equals the floating point number represented by =
the String.  What about changing num_exact() in time.c to handle Floats =
by converting to String and then to Rational rather than calling =
Float#to_r?

> Anyone can use it as Time.utc(1970,1,1,0,0,12.860.rationalize) and it
> may (or may not) solve problem, though.

Or even better: Time.utc(1970,1,1,0,0,12.860.to_s.to_r).

Dave