Complex#rationalize and to_r with approximate zeros
Currently, Complex#rationalize and Complex#to_r raise a RangeError if the imaginary part is nonzero or is a Float. Note that a BigDecimal(0) is accepted, though:
Complex(1, 0).to_r # => Rational(1,1) Complex(1, BigDecimal("0.0")).to_r # => Rational(1,1) Complex(1, 0.0).to_r # => RangeError
This is inconsistent. I recommend not raising an error for 0.0 (Float or BigDecimal). Any objection?
Updated by mrkn (Kenta Murata) over 12 years ago
- Assignee set to mrkn (Kenta Murata)
0.0 doesn't exactly represent zero. It may be 0.0+-10.0**(Float::MIN_10_EXP-17).
BigDecimal(0) doesn't exactly represent zero, too.
I believe this issue should be resolved by introducing Numeric#exact? and/or Numeric#inexact? methods.
Updated by jeremyevans0 (Jeremy Evans) almost 4 years ago
As Numeric#exact? has been rejected, and BigDecimal is not a core class, I'm not sure what to do about this issue. @mrkn (Kenta Murata) seems to recommend RangeError for
Complex(1, BigDecimal("0.0")).to_r (the same as with
Complex(1, 0.0).to_r ). I think the bigdecimal library would have to override Kernel#Complex for that behavior. Would that be considered acceptable?
Updated by mame (Yusuke Endoh) almost 4 years ago
We discussed #5321 at the dev-meeting. Whether a value is exact or inexact, is not decidable based on a class. For example, a literal
0.0 may be considered as precise and exact. But, if the same value is gained from an inaccurate calculation, it may be considered as inexact.
In regard to this paricular issue, Naruse pointed out that we already have
Float#to_r. So I think that it is reasonable that
Complex(1, 0.0).to_r returns
Rational(1, 1) instead of RangeError.
Updated by kjtsanaktsidis (KJ Tsanaktsidis) 7 months ago
I've explored the behaviour here a bit, and I think I do believe that
0.0 really does represent the concept of "zero". Specifically, it's the number that satisfies the following properties, for all possible non-NaN floats N:
abs(0.0 * N) == 0.0
0.0 + N == N
You might obtain a bit pattern for 0.0 from the result of a calculation which might have otherwise produced a non-zero result if there was more precision; however that doesn't change the fact that the bit pattern you have really does satisfy the mathematical properties of zero.
Additionally, I think the distinction that complex.c currently draws between floating-point and non-floating-point zero also results in some other surprising results:
irb(main):030:0> Complex(3, 7) ** 0.0 => (1.0+0.0i) irb(main):031:0> Complex(3, 7) ** 0 => (1+0i) irb(main):032:0> Complex(3.2, 7) ** 0 => (1+0i) irb(main):033:0> Complex(3.2, 7) ** 0.0 => (1.0+0.0i)
Why does the type of
A ** B depend on whether B is a float or not, but not A?
irb(main):034:0> Complex(3.2, 7) ** Complex(1, 0.0) => (3.200000000000001+6.999999999999999i) irb(main):035:0> Complex(3.2, 7) ** Complex(1, 0) => (3.2+7i)
This doesn't need to accumulate floating point error - the code in
(a + bi) ** (c + 0i) -->(a + bi) ** c, but the special-casing is skipped if
#to_f have the same issue as
irb(main):037:0> Complex(3, 0.0).to_i (irb):37:in `to_i': can't convert 3+0.0i into Integer (RangeError) irb(main):038:0> Complex(3, 0.0).to_f (irb):38:in `to_f': can't convert 3+0.0i into Float (RangeError)
This is especially odd for
#to_i because it of course has no problems truncating away the real part:
irb(main):040:0> Complex(3.1, 0).to_i => 3
I think we should change all the checks for
f_zero_p; i.e. in all the places where
complex.c special-cases integer zero, also make it special-case floating-point zero. If people agree with this (especially people who actually use Complex in their code - I have pretty much never used it!) I can send a pretty simple patch to cloe this out.