Hi matz -- I'd like to hear your opinion about the policy of Math module. I have considered Math module is just a wrapper of libm. But current Math absorbs a part of platform-dependent behavior of libm: - in [ruby-core:7019] and r10626, you decided Ruby absorbed FreeBSD's peculiar behavior for sqrt(-1) by explicit NaN checking, in spite of (maybe) the FreeBSD's spec. - in [ruby-core:26647], you also rejected consistency against Linux's bizarre behavior for atanh(1), in spite of the Linux's *bug* (even written explicitly as bug in manpage) http://www.kernel.org/doc/man-pages/online/pages/man3/atanh.3.html It is good to decide the policy of Math. Please choice: 1) Math is still just a wrapper of libm 2) Math is (aims to be) platform-independent math module (though it is very similar to libm's API) If you choice #1, I think the NaN check of r10626 should be removed because it goes against the policy. BTW, recently I found that libm is not so portable; it has different behavior depending on platform. Also, libm of glibc seems not to be stable enough yet; the above behavior of atanh(1) was changed (fixed?) in 2009-04-26: http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/libc/ChangeLog?rev=1.11655.2.1&content-type=text/plain&cvsroot=glibc So I prefer #2. If Math is just a wrapper, users must know and care the difference. For example, when atanh fails, it may return NaN or Infinity or raise Errno::EDOM or Errno:: ERANGE. Each user must check them manually to write portable Ruby script. It is too cumbersome. To aim platform-independent math module, it is first needed to do explicit bound check to avoid the different exception raised. It may need human-resource, but I think it is now ok only to decide the policy. We can insert each check whenever someone registers bug tickets. And we can refer SUSv3 etc., so we don't have to worry what and how to check. What do you think?
on 2010-02-18 04:17
on 2010-02-18 06:27
I also support the second option of a platform independent module (as I stated in [ruby-core:26657] ). Even if the first option was desired, Ruby can not be completely faithful to libm because functions in libm have the dual effect of returning a value (potentially NaN) and setting error flags at the same time. Since obviously Ruby methods can either a value or raise an error but not both, the current approach is not a direct wrap of libm. This is particularly problematic in case of pole errors where libm states that a mathematically valid result +/-Infinity should be returned and the FE_DIVBYZERO error flag set [redmine:2189]. Especially now that we have Float::INFINITY (thanks Yui!), I hope we can make Ruby mathematically sound on all platforms. -- Marc-Andre Lafortune
on 2010-02-18 06:56
Hi,
In message "Re: [ruby-core:28206] Is Math module a wrapper of libm?"
on Thu, 18 Feb 2010 12:17:08 +0900, Yusuke ENDOH <mame@tsg.ne.jp>
writes:
|It is good to decide the policy of Math. Please choice:
|
|1) Math is still just a wrapper of libm
|2) Math is (aims to be) platform-independent math module
| (though it is very similar to libm's API)
I choose #2. I also want Math module to work with complex and
rational as much as their support is reasonable.
matz.
on 2010-02-18 13:25
Hi, 2010/2/18 Yukihiro Matsumoto <matz@ruby-lang.org>: > In message "Re: [ruby-core:28206] Is Math module a wrapper of libm?" > ? ?on Thu, 18 Feb 2010 12:17:08 +0900, Yusuke ENDOH <mame@tsg.ne.jp> writes: > > |It is good to decide the policy of Math. ?Please choice: > | > |1) Math is still just a wrapper of libm > |2) Math is (aims to be) platform-independent math module > | ? (though it is very similar to libm's API) > > I choose #2. Thank you for your smart choice! At first, I'll soon change the behaviors of atanh(1) and lgamma(inf) which are currently platform-dependent. According to SUSv3, the former should raise ERANGE (currently raises EDOM on Linux), and the latter should return inf (currently raises EDOM on darwin).
on 2010-02-18 16:26
Hi, > 2010/2/18 Yukihiro Matsumoto <matz@ruby-lang.org>: > I choose #2. Great! On Thu, Feb 18, 2010 at 7:25 AM, Yusuke ENDOH <mame@tsg.ne.jp> wrote: > At first, I'll soon change the behaviors of atanh(1) and lgamma(inf) > which are currently platform-dependent. According to SUSv3, the former > should raise ERANGE (currently raises EDOM on Linux), and the latter > should return inf (currently raises EDOM on darwin). I disagree with this reading. SUSv3 states that atanh(1) should return Infinity and (only when the application has requested that behavior) set the ERANGE error flag. Ruby must choose to do one or the other (or else add a way to do both). Mathematically speaking, there is no doubt that it should return Infinity and that this is more helpful than raising an error. The question is also valid for Math.log(0.0) Math.atanh(1.0) Math.gamma(0.0) 0.0**(-n) Look at the graph of 1/x. Look at the graph of Log(x). Now explain to me why 1/0.0 is +Inf and Log(x) would not be -Inf?
on 2010-02-18 17:39
Hi, 2010/2/19 Marc-Andre Lafortune <ruby-core-mailing-list@marc-andre.ca>: > On Thu, Feb 18, 2010 at 7:25 AM, Yusuke ENDOH <mame@tsg.ne.jp> wrote: >> At first, I'll soon change the behaviors of atanh(1) and lgamma(inf) >> which are currently platform-dependent. According to SUSv3, the former >> should raise ERANGE (currently raises EDOM on Linux), and the latter >> should return inf (currently raises EDOM on darwin). > > I disagree with this reading. SUSv3 states that atanh(1) should return > Infinity and (only when the application has requested that behavior) > set the ERANGE error flag. Ruby must choose to do one or the other (or > else add a way to do both). Ah, I'm sorry for my neglect. We should decide the policy too. But I have already commited the change to quickly fix platform- dependent behavior. And I pass the baton on to muraken. He'll suggest a patch. > Look at the graph of 1/x. Look at the graph of Log(x). Now explain to > me why 1/0.0 is +Inf and Log(x) would not be -Inf? Hmm. In aspect of software engineering, I guess it is parhaps a bug if log(0.0) is actually evaluated, so it is good to raise an exception. I think 1/0.0 also should raise an exception because of the same reason. But it cannot be changed for compatibility. Anyway, please wait for muraken's suggestion.
on 2010-02-18 18:34
Hi, On 2010/02/19, at 0:25, Marc-Andre Lafortune wrote: > that it should return Infinity and that this is more helpful than > raising an error. lgamma(x) is defined as log(fabs(tgamma(x))). SUSv3 defines that lgamma(-INFINITY) is +INFINITY though tgamma(-INFINITY) is defined a domain error. Do you think what does Math.lgamma(-Float::INFINITY) return? > The question is also valid for > Math.log(0.0) > Math.atanh(1.0) > Math.gamma(0.0) > 0.0**(-n) > > Look at the graph of 1/x. Look at the graph of Log(x). Now explain to > me why 1/0.0 is +Inf and Log(x) would not be -Inf? I try to write a patch for atanh, log, log2, log10, and sqrt according to the following policies: (1) Pick up domain and pole error cases before calling actual C functions. (2) Returns NAN if a domain error is picked up. (3) Returns INFINITY with suitable flag if a pole error is picked up. But, I hope that a suitable exception is raised when picked up domain errors. Unfortunately the existence of EDOM depends on platform. So I believe that we need an error class like Math::DomainError. -- Kenta Murata OpenPGP FP = FA26 35D7 4F98 3498 0810 E0D5 F213 966F E9EB 0BCC E-mail: mrkn@mrkn.jp twitter: http://twitter.com/mrkn/ blog: http://d.hatena.ne.jp/mrkn/ diff --git a/math.c b/math.c index 027932d..ebde179 100644 --- a/math.c +++ b/math.c @@ -310,6 +310,11 @@ math_atanh(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* domain error check */ + if (d0 < -1.0 || +1.0 < d0) return DBL2NUM(NAN); + /* pole error cases */ + if (d0 == -1.0) return DBL2NUM(-INFINITY); + if (d0 == +1.0) return DBL2NUM(+INFINITY); d = atanh(d0); domain_check(d0, d, "atanh"); infinity_check(x, d, "atanh"); @@ -370,6 +375,10 @@ math_log(int argc, VALUE *argv) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* domain error case */ + if (signbit(d0)) return DBL2NUM(NAN); + /* pole error case */ + if (d0 == +0.0) return DBL2NUM(-INFINITY); d = log(d0); if (argc == 2) { Need_Float(base); @@ -413,6 +422,10 @@ math_log2(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* domain error case */ + if (signbit(d0)) return DBL2NUM(NAN); + /* pole error case */ + if (d0 == +0.0) return DBL2NUM(-INFINITY); d = log2(d0); domain_check(d0, d, "log2"); infinity_check(x, d, "log2"); @@ -439,6 +452,10 @@ math_log10(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* domain error case */ + if (signbit(d0)) return DBL2NUM(NAN); + /* pole error case */ + if (d0 == +0.0) return DBL2NUM(-INFINITY); d = log10(d0); domain_check(d0, d, "log10"); infinity_check(x, d, "log10"); @@ -477,6 +494,8 @@ math_sqrt(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* domain error case */ + if (signbit(d0)) return DBL2NUM(NAN); d = sqrt(d0); domain_check(d0, d, "sqrt"); return DBL2NUM(d); diff --git a/test/ruby/test_math.rb b/test/ruby/test_math.rb index 5b49291..8189b31 100644 --- a/test/ruby/test_math.rb +++ b/test/ruby/test_math.rb @@ -2,10 +2,15 @@ require 'test/unit' class TestMath < Test::Unit::TestCase def assert_infinity(a, *rest) - rest = ["not infinity"] if rest.empty? + rest = ["not infinity: #{a.inspect}"] if rest.empty? assert(!a.finite?, *rest) end + def assert_nan(a, *rest) + rest = ["not nan: #{a.inspect}"] if rest.empty? + assert(a.nan?, *rest) + end + def check(a, b) err = [Float::EPSILON * 4, [a.abs, b.abs].max * Float::EPSILON * 256].max assert_in_delta(a, b, err) @@ -99,7 +104,10 @@ class TestMath < Test::Unit::TestCase check(0, Math.atanh(Math.sinh(0) / Math.cosh(0))) check(1, Math.atanh(Math.sinh(1) / Math.cosh(1))) check(2, Math.atanh(Math.sinh(2) / Math.cosh(2))) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.atanh(-1) } + assert_nothing_raised { assert_infinity Math.atanh(1) } + assert_nothing_raised { assert_infinity -Math.atanh(-1) } + assert_nothing_raised { assert_nan Math.atanh(1 + Float::EPSILON) } + assert_nothing_raised { assert_nan Math.atanh(-1 - Float::EPSILON) } end def test_exp @@ -116,8 +124,8 @@ class TestMath < Test::Unit::TestCase check(1, Math.log(10, 10)) check(2, Math.log(100, 10)) assert_equal(1.0/0, Math.log(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log(0) } - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log(-1) } + assert_nothing_raised { assert_infinity -Math.log(+0.0) } + assert_nothing_raised { assert_nan Math.log(-0.0) } assert_raise(TypeError) { Math.log(1,nil) } end @@ -126,8 +134,8 @@ class TestMath < Test::Unit::TestCase check(1, Math.log2(2)) check(2, Math.log2(4)) assert_equal(1.0/0, Math.log2(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log2(0) } - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log2(-1) } + assert_nothing_raised { assert_infinity -Math.log2(+0.0) } + assert_nothing_raised { assert_nan Math.log2(-0.0) } end def test_log10 @@ -135,8 +143,8 @@ class TestMath < Test::Unit::TestCase check(1, Math.log10(10)) check(2, Math.log10(100)) assert_equal(1.0/0, Math.log10(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log10(0) } - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log10(-1) } + assert_nothing_raised { assert_infinity -Math.log10(+0.0) } + assert_nothing_raised { assert_nan Math.log10(-0.0) } end def test_sqrt @@ -144,7 +152,7 @@ class TestMath < Test::Unit::TestCase check(1, Math.sqrt(1)) check(2, Math.sqrt(4)) assert_equal(1.0/0, Math.sqrt(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.sqrt(-1) } + assert_nothing_raised { assert_nan Math.sqrt(-0.0) } end def test_frexp
on 2010-02-20 05:12
Hi, On Thu, Feb 18, 2010 at 12:31 PM, Kenta Murata <muraken@gmail.com> wrote: > lgamma(x) is defined as log(fabs(tgamma(x))). > SUSv3 defines that lgamma(-INFINITY) is +INFINITY > though tgamma(-INFINITY) is defined a domain error. > > Do you think what does Math.lgamma(-Float::INFINITY) return? Indeed, it makes no mathematical sense that Math.lgamma(-Float::INFINITY) return +INFINITY. Since lgamma oscillates wildly for negative arguments, it should return NaN or raise an error, same as tgamma. > I try to write a patch for atanh, log, log2, log10, and sqrt > according to the following policies: > > (1) Pick up domain and pole error cases before calling actual C functions. > (2) Returns NAN if a domain error is picked up. Are you proposing to never raise an error for any domain errors and instead returning NaN? I am a bit confused here, since you suggest a bit afterwardsto create a new error class Math::DomainError. > (3) Returns INFINITY with suitable flag if a pole error is picked up. Returning +-INFINITY is great. What "suitable flag" did you have in mind? > ... > diff --git a/math.c b/math.c > ... The patch posted is a great step in the right direction. Some care has to be taken with -0.0. I completely understand why you feel that Log(-0.0) should return NaN. IEEE decided it was wiser to return -Infinity, though. It can be argued that having an actual value that could be exact is more useful than a NaN. On the other hand, don't ask me why IEEE decided that Sqrt(-0.0) == -0.0 instead of +0.0, I have no clue!
on 2010-02-21 19:34
Hi First, I apologise to confuse you in the previous mail. I ideally hope that Math functions raise appropriate errors for their domain errors. On 2010/02/20, at 13:11, Marc-Andre Lafortune wrote: > Math.lgamma(-Float::INFINITY) return +INFINITY. Since lgamma > oscillates wildly for negative arguments, it should return NaN or > raise an error, same as tgamma. So I want lgamma raises an error for -INFINITY input. >> I try to write a patch for atanh, log, log2, log10, and sqrt >> according to the following policies: >> >> (1) Pick up domain and pole error cases before calling actual C functions. >> (2) Returns NAN if a domain error is picked up. > > Are you proposing to never raise an error for any domain errors and > instead returning NaN? > I am a bit confused here, since you suggest a bit afterwardsto create > a new error class Math::DomainError. As above description, I hope Math functions raise errors for their domain errors. >> (3) Returns INFINITY with suitable flag if a pole error is picked up. > > Returning +-INFINITY is great. What "suitable flag" did you have in mind? I made a mistake, "flag" is not right but "sign". > don't ask me why IEEE decided that Sqrt(-0.0) == -0.0 instead of > +0.0, I have no clue! When I wrote the previous mail, I thought that -0.0 includes negative underflowed values, so I defined Log(-0.0) as NaN. But my thought isn't useful for users who are unfamiliar with characteristics of float values. It is a difficult issue. I think it needs more discussions. Now I refine the previous patch. Math::DomainError is introduced now and used by the following cases: - Math.acos(|x| > 1) - Math.acosh(x < 1) - Math.asin(|x| > 1) - Math.atan2(+-0, +-0) # needs more discussions - Math.atanh(|x| > 1) - Math.log(x < 0) - Math.log2(x < 0) - Math.log10(x < 0) - Math.log(-0.0) # needs more discussions - Math.log2(-0.0) # needs more discussions - Math.log10(-0.0) # needs more discussions - Math.sqrt(x < 0) - Math.sqrt(-0.0) # needs more discussions - Math.gamma(-inf) - Math.lgamma(-inf) Please review it. diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h index 5d87c0d..d56d42c 100644 --- a/include/ruby/ruby.h +++ b/include/ruby/ruby.h @@ -1234,6 +1234,8 @@ RUBY_EXTERN VALUE rb_eNameError; RUBY_EXTERN VALUE rb_eSyntaxError; RUBY_EXTERN VALUE rb_eLoadError; +RUBY_EXTERN VALUE rb_eMathDomainError; + RUBY_EXTERN VALUE rb_stdin, rb_stdout, rb_stderr; static inline VALUE diff --git a/math.c b/math.c index 51a89c3..5064d05 100644 --- a/math.c +++ b/math.c @@ -16,6 +16,7 @@ #define numberof(array) (int)(sizeof(array) / sizeof((array)[0])) VALUE rb_mMath; +VALUE rb_eMathDomainError; extern VALUE rb_to_float(VALUE val); #define Need_Float(x) do {if (TYPE(x) != T_FLOAT) {(x) = rb_to_float(x);}} while(0) @@ -24,6 +25,9 @@ extern VALUE rb_to_float(VALUE val); Need_Float(y);\ } while (0) +#define domain_error(msg) \ + rb_raise(rb_eMathDomainError, "Numerical argument is out of domain - " #msg); + static void domain_check(double x, double y, const char *msg) { @@ -86,8 +90,12 @@ infinity_check(VALUE arg, double res, const char *msg) static VALUE math_atan2(VALUE obj, VALUE y, VALUE x) { + double dx, dy; Need_Float2(y, x); - return DBL2NUM(atan2(RFLOAT_VALUE(y), RFLOAT_VALUE(x))); + dx = RFLOAT_VALUE(x); + dy = RFLOAT_VALUE(y); + if (dx == 0.0 && dy == 0.0) domain_error("atan2"); + return DBL2NUM(atan2(dy, dx)); } @@ -153,6 +161,8 @@ math_acos(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (d0 < -1.0 || 1.0 < d0) domain_error("acos"); d = acos(d0); domain_check(d0, d, "acos"); return DBL2NUM(d); @@ -173,6 +183,8 @@ math_asin(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (d0 < -1.0 || 1.0 < d0) domain_error("asin"); d = asin(d0); domain_check(d0, d, "asin"); return DBL2NUM(d); @@ -276,6 +288,8 @@ math_acosh(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (d0 < 1.0) domain_error("acosh"); d = acosh(d0); domain_check(d0, d, "acosh"); return DBL2NUM(d); @@ -310,10 +324,11 @@ math_atanh(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); - if (d0 == 1.0 || d0 == -1.0) { - errno = ERANGE; - rb_sys_fail("atanh"); - } + /* check for domain error */ + if (d0 < -1.0 || +1.0 < d0) domain_error("atanh"); + /* check for pole error */ + if (d0 == -1.0) return DBL2NUM(-INFINITY); + if (d0 == +1.0) return DBL2NUM(+INFINITY); d = atanh(d0); domain_check(d0, d, "atanh"); infinity_check(x, d, "atanh"); @@ -374,6 +389,10 @@ math_log(int argc, VALUE *argv) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (signbit(d0)) domain_error("log"); + /* check for pole error */ + if (d0 == +0.0) return DBL2NUM(-INFINITY); d = log(d0); if (argc == 2) { Need_Float(base); @@ -417,6 +436,10 @@ math_log2(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (signbit(d0)) domain_error("log2"); + /* check for pole error */ + if (d0 == +0.0) return DBL2NUM(-INFINITY); d = log2(d0); domain_check(d0, d, "log2"); infinity_check(x, d, "log2"); @@ -443,6 +466,10 @@ math_log10(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (signbit(d0)) domain_error("log10"); + /* check for pole error */ + if (d0 == +0.0) return DBL2NUM(-INFINITY); d = log10(d0); domain_check(d0, d, "log10"); infinity_check(x, d, "log10"); @@ -481,6 +508,8 @@ math_sqrt(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (signbit(d0)) domain_error("sqrt"); d = sqrt(d0); domain_check(d0, d, "sqrt"); return DBL2NUM(d); @@ -686,6 +715,8 @@ math_gamma(VALUE obj, VALUE x) double intpart, fracpart; Need_Float(x); d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (isinf(d0) && signbit(d0)) domain_error("gamma"); fracpart = modf(d0, &intpart); if (fracpart == 0.0 && 0 < intpart && @@ -719,6 +750,8 @@ math_lgamma(VALUE obj, VALUE x) Need_Float(x); errno = 0; d0 = RFLOAT_VALUE(x); + /* check for domain error */ + if (isinf(d0) && signbit(d0)) domain_error("lgamma"); if (isinf(d0)) { return rb_assoc_new(DBL2NUM(INFINITY), INT2FIX(1)); } @@ -772,6 +805,7 @@ void Init_Math(void) { rb_mMath = rb_define_module("Math"); + rb_eMathDomainError = rb_define_class_under(rb_mMath, "DomainError", rb_eArgError); #ifdef M_PI rb_define_const(rb_mMath, "PI", DBL2NUM(M_PI)); diff --git a/test/ruby/test_math.rb b/test/ruby/test_math.rb index 5b49291..7270511 100644 --- a/test/ruby/test_math.rb +++ b/test/ruby/test_math.rb @@ -2,16 +2,22 @@ require 'test/unit' class TestMath < Test::Unit::TestCase def assert_infinity(a, *rest) - rest = ["not infinity"] if rest.empty? + rest = ["not infinity: #{a.inspect}"] if rest.empty? assert(!a.finite?, *rest) end + def assert_nan(a, *rest) + rest = ["not nan: #{a.inspect}"] if rest.empty? + assert(a.nan?, *rest) + end + def check(a, b) err = [Float::EPSILON * 4, [a.abs, b.abs].max * Float::EPSILON * 256].max assert_in_delta(a, b, err) end def test_atan2 + assert_raise(Math::DomainError) { Math.atan2(0, 0) } check(0, Math.atan2(0, 1)) check(Math::PI / 4, Math.atan2(1, 1)) check(Math::PI / 2, Math.atan2(1, 0)) @@ -46,7 +52,9 @@ class TestMath < Test::Unit::TestCase check(1 * Math::PI / 4, Math.acos( 1.0 / Math.sqrt(2))) check(2 * Math::PI / 4, Math.acos( 0.0)) check(4 * Math::PI / 4, Math.acos(-1.0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.acos(2.0) } + assert_raise(Math::DomainError) { Math.acos(+1.0 + Float::EPSILON) } + assert_raise(Math::DomainError) { Math.acos(-1.0 - Float::EPSILON) } + assert_raise(Math::DomainError) { Math.acos(2.0) } end def test_asin @@ -54,7 +62,9 @@ class TestMath < Test::Unit::TestCase check( 1 * Math::PI / 4, Math.asin( 1.0 / Math.sqrt(2))) check( 2 * Math::PI / 4, Math.asin( 1.0)) check(-2 * Math::PI / 4, Math.asin(-1.0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.asin(2.0) } + assert_raise(Math::DomainError) { Math.asin(+1.0 + Float::EPSILON) } + assert_raise(Math::DomainError) { Math.asin(-1.0 - Float::EPSILON) } + assert_raise(Math::DomainError) { Math.asin(2.0) } end def test_atan @@ -86,7 +96,8 @@ class TestMath < Test::Unit::TestCase check(0, Math.acosh(1)) check(1, Math.acosh((Math::E ** 1 + Math::E ** -1) / 2)) check(2, Math.acosh((Math::E ** 2 + Math::E ** -2) / 2)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.acosh(0) } + assert_raise(Math::DomainError) { Math.acosh(1.0 - Float::EPSILON) } + assert_raise(Math::DomainError) { Math.acosh(0) } end def test_asinh @@ -99,7 +110,10 @@ class TestMath < Test::Unit::TestCase check(0, Math.atanh(Math.sinh(0) / Math.cosh(0))) check(1, Math.atanh(Math.sinh(1) / Math.cosh(1))) check(2, Math.atanh(Math.sinh(2) / Math.cosh(2))) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.atanh(-1) } + assert_nothing_raised { assert_infinity Math.atanh(1) } + assert_nothing_raised { assert_infinity -Math.atanh(-1) } + assert_raise(Math::DomainError) { Math.atanh(+1.0 + Float::EPSILON) } + assert_raise(Math::DomainError) { Math.atanh(-1.0 - Float::EPSILON) } end def test_exp @@ -116,8 +130,9 @@ class TestMath < Test::Unit::TestCase check(1, Math.log(10, 10)) check(2, Math.log(100, 10)) assert_equal(1.0/0, Math.log(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log(0) } - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log(-1) } + assert_nothing_raised { assert_infinity -Math.log(+0.0) } + assert_raise(Math::DomainError) { Math.log(-0.0) } # TODO: [ruby-core:28265] + assert_raise(Math::DomainError) { Math.log(-1.0) } assert_raise(TypeError) { Math.log(1,nil) } end @@ -126,8 +141,9 @@ class TestMath < Test::Unit::TestCase check(1, Math.log2(2)) check(2, Math.log2(4)) assert_equal(1.0/0, Math.log2(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log2(0) } - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log2(-1) } + assert_nothing_raised { assert_infinity -Math.log2(+0.0) } + assert_raise(Math::DomainError) { Math.log2(-0.0) } # TODO: [ruby-core:28265] + assert_raise(Math::DomainError) { Math.log2(-1.0) } end def test_log10 @@ -135,8 +151,9 @@ class TestMath < Test::Unit::TestCase check(1, Math.log10(10)) check(2, Math.log10(100)) assert_equal(1.0/0, Math.log10(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log10(0) } - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.log10(-1) } + assert_nothing_raised { assert_infinity -Math.log10(+0.0) } + assert_raise(Math::DomainError) { Math.log10(-0.0) } # TODO: [ruby-core:28265] + assert_raise(Math::DomainError) { Math.log10(-1.0) } end def test_sqrt @@ -144,7 +161,8 @@ class TestMath < Test::Unit::TestCase check(1, Math.sqrt(1)) check(2, Math.sqrt(4)) assert_equal(1.0/0, Math.sqrt(1.0/0)) - assert_raise(Errno::EDOM, Errno::ERANGE) { Math.sqrt(-1) } + assert_raise(Math::DomainError) { Math.sqrt(-0.0) } # TODO: [ruby-core:28265] + assert_raise(Math::DomainError) { Math.sqrt(-1.0) } end def test_frexp @@ -201,6 +219,8 @@ class TestMath < Test::Unit::TestCase assert_infinity(Math.gamma(i), "Math.gamma(#{i}) should be INF") assert_infinity(Math.gamma(i-1), "Math.gamma(#{i-1}) should be INF") end + + assert_raise(Math::DomainError) { Math.gamma(-Float::INFINITY) } end def test_lgamma @@ -241,6 +261,8 @@ class TestMath < Test::Unit::TestCase g, s = Math.lgamma(4) check(Math.log(6), g) assert_equal(s, 1) + + assert_raise(Math::DomainError) { Math.lgamma(-Float::INFINITY) } end def test_cbrt -- Kenta Murata OpenPGP FP = FA26 35D7 4F98 3498 0810 E0D5 F213 966F E9EB 0BCC E-mail: mrkn@mrkn.jp twitter: http://twitter.com/mrkn/ blog: http://d.hatena.ne.jp/mrkn/
on 2010-02-22 09:31
Hi!
I like the direction we're taking.
I've been thinking about a general decision algorithm. It would look
like:
Try to come up with a valid mathematical value (including +-inf but
excluding NaN):
- Check all possible values (excluding NaN). If there is exactly one,
you're done.
- If there are many possible values, are they all basically equal
(e.g. -0.0 vs +0.0, or +pi vs -pi for an angle)? If so, then return
the best one.
- If the possible values are +-Infinity, and the input is +-0.0 can
tell you which to pick, then return the right infinity (pole error
case)
- Otherwise return NaN or raise an error.
This algorithm agrees with your proposal that atan2(0,0) should raise
an error. While we're contradicting IEEE, have you thought of
atan2(+-Inf, +-Inf)? I feel it should also be an error, in the same
way that Inf/Inf doesn't return 1!
Here's an incremental patch that modifies the code according to this.
Note that infinity_check was already redundant, so I removed it.
domain_check was also redundant except potentially for some corner
cases. Indeed SUSv3 states that implementations may set the ERANGE
error flag in case of subnormal values. domain_check would raise an
error if this was the case. Since this behavior is platform dependant
(and far from useful), I removed domain_check also.
I included a line so that sqrt(-0.0) = +0.0 instead of -0.0, because I
feel it makes sense and is consistent with pow(-0.0, 0.5) == +0.0, but
I don't think it matters all that much and I definitely won't fight
over it.
The question that remains in my mind: is it better to return NaN or
raise a domain error? Why should 0.0/0.0 == NaN and atan2(0.0, 0.0)
raise an error? I personally would lean towards always returning NaN
instead of raising an error, but I am not confident in this
preference. Or never return NaN? Thoughts?
diff --git a/math.c b/math.c
index 5064d05..77a8757 100644
--- a/math.c
+++ b/math.c
@@ -11,7 +11,6 @@
#include "ruby/ruby.h"
#include <math.h>
-#include <errno.h>
#define numberof(array) (int)(sizeof(array) / sizeof((array)[0]))
@@ -28,45 +27,6 @@ extern VALUE rb_to_float(VALUE val);
#define domain_error(msg) \
rb_raise(rb_eMathDomainError, "Numerical argument is out of
domain - " #msg);
-static void
-domain_check(double x, double y, const char *msg)
-{
- if (errno) {
- if (isinf(y)) return;
- }
- else {
- if (!isnan(y)) return;
- else if (isnan(x)) return;
- else {
-#if defined(EDOM)
- errno = EDOM;
-#else
- errno = ERANGE;
-#endif
- }
- }
- rb_sys_fail(msg);
-}
-
-static void
-infinity_check(VALUE arg, double res, const char *msg)
-{
- while(1) {
- if (errno) {
- rb_sys_fail(msg);
- }
- if (isinf(res) && !isinf(RFLOAT_VALUE(arg))) {
-#if defined(EDOM)
- errno = EDOM;
-#elif defined(ERANGE)
- errno = ERANGE;
-#endif
- continue;
- }
- break;
- }
-}
-
/*
* call-seq:
* Math.atan2(y, x) => float
@@ -95,6 +55,7 @@ math_atan2(VALUE obj, VALUE y, VALUE x)
dx = RFLOAT_VALUE(x);
dy = RFLOAT_VALUE(y);
if (dx == 0.0 && dy == 0.0) domain_error("atan2");
+ if (isinf(dx) && isinf(dy)) domain_error("atan2");
return DBL2NUM(atan2(dy, dx));
}
@@ -159,12 +120,10 @@ math_acos(VALUE obj, VALUE x)
double d0, d;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
if (d0 < -1.0 || 1.0 < d0) domain_error("acos");
d = acos(d0);
- domain_check(d0, d, "acos");
return DBL2NUM(d);
}
@@ -181,12 +140,10 @@ math_asin(VALUE obj, VALUE x)
double d0, d;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
if (d0 < -1.0 || 1.0 < d0) domain_error("asin");
d = asin(d0);
- domain_check(d0, d, "asin");
return DBL2NUM(d);
}
@@ -286,12 +243,10 @@ math_acosh(VALUE obj, VALUE x)
double d0, d;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
if (d0 < 1.0) domain_error("acosh");
d = acosh(d0);
- domain_check(d0, d, "acosh");
return DBL2NUM(d);
}
@@ -322,7 +277,6 @@ math_atanh(VALUE obj, VALUE x)
double d0, d;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
if (d0 < -1.0 || +1.0 < d0) domain_error("atanh");
@@ -330,8 +284,6 @@ math_atanh(VALUE obj, VALUE x)
if (d0 == -1.0) return DBL2NUM(-INFINITY);
if (d0 == +1.0) return DBL2NUM(+INFINITY);
d = atanh(d0);
- domain_check(d0, d, "atanh");
- infinity_check(x, d, "atanh");
return DBL2NUM(d);
}
@@ -387,19 +339,16 @@ math_log(int argc, VALUE *argv)
rb_scan_args(argc, argv, "11", &x, &base);
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
- if (signbit(d0)) domain_error("log");
+ if (d0 < 0.0) domain_error("log");
/* check for pole error */
- if (d0 == +0.0) return DBL2NUM(-INFINITY);
+ if (d0 == 0.0) return DBL2NUM(-INFINITY);
d = log(d0);
if (argc == 2) {
Need_Float(base);
d /= log(RFLOAT_VALUE(base));
}
- domain_check(d0, d, "log");
- infinity_check(x, d, "log");
return DBL2NUM(d);
}
@@ -434,15 +383,12 @@ math_log2(VALUE obj, VALUE x)
double d0, d;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
- if (signbit(d0)) domain_error("log2");
+ if (d0 < 0.0) domain_error("log2");
/* check for pole error */
- if (d0 == +0.0) return DBL2NUM(-INFINITY);
+ if (d0 == 0.0) return DBL2NUM(-INFINITY);
d = log2(d0);
- domain_check(d0, d, "log2");
- infinity_check(x, d, "log2");
return DBL2NUM(d);
}
@@ -464,15 +410,12 @@ math_log10(VALUE obj, VALUE x)
double d0, d;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
- if (signbit(d0)) domain_error("log10");
+ if (d0 < 0.0) domain_error("log10");
/* check for pole error */
- if (d0 == +0.0) return DBL2NUM(-INFINITY);
+ if (d0 == 0.0) return DBL2NUM(-INFINITY);
d = log10(d0);
- domain_check(d0, d, "log10");
- infinity_check(x, d, "log10");
return DBL2NUM(d);
}
@@ -506,12 +449,11 @@ math_sqrt(VALUE obj, VALUE x)
double d0, d;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
- if (signbit(d0)) domain_error("sqrt");
+ if (d0 < 0) domain_error("sqrt");
+ if (d0 == 0) return DBL2NUM(0);
d = sqrt(d0);
- domain_check(d0, d, "sqrt");
return DBL2NUM(d);
}
@@ -718,14 +660,14 @@ math_gamma(VALUE obj, VALUE x)
/* check for domain error */
if (isinf(d0) && signbit(d0)) domain_error("gamma");
fracpart = modf(d0, &intpart);
- if (fracpart == 0.0 &&
- 0 < intpart &&
- intpart - 1 < (double)numberof(fact_table)) {
- return DBL2NUM(fact_table[(int)intpart - 1]);
+ if (fracpart == 0.0) {
+ if (intpart < 0) domain_error("gamma");
+ if (0 < intpart &&
+ intpart - 1 < (double)numberof(fact_table)) {
+ return DBL2NUM(fact_table[(int)intpart - 1]);
+ }
}
- errno = 0;
d = tgamma(d0);
- domain_check(d0, d, "gamma");
return DBL2NUM(d);
}
@@ -748,7 +690,6 @@ math_lgamma(VALUE obj, VALUE x)
int sign=1;
VALUE v;
Need_Float(x);
- errno = 0;
d0 = RFLOAT_VALUE(x);
/* check for domain error */
if (isinf(d0) && signbit(d0)) domain_error("lgamma");
@@ -756,7 +697,6 @@ math_lgamma(VALUE obj, VALUE x)
return rb_assoc_new(DBL2NUM(INFINITY), INT2FIX(1));
}
d = lgamma_r(d0, &sign);
- domain_check(d0, d, "lgamma");
v = DBL2NUM(d);
return rb_assoc_new(v, INT2FIX(sign));
}
diff --git a/test/ruby/test_math.rb b/test/ruby/test_math.rb
index 3963e91..2f654c1 100644
--- a/test/ruby/test_math.rb
+++ b/test/ruby/test_math.rb
@@ -18,6 +18,7 @@ class TestMath < Test::Unit::TestCase
def test_atan2
assert_raise(Math::DomainError) { Math.atan2(0, 0) }
+ assert_raise(Math::DomainError) { Math.atan2(1.0 / 0.0, 1.0 / 0.0)
}
check(0, Math.atan2(0, 1))
check(Math::PI / 4, Math.atan2(1, 1))
check(Math::PI / 2, Math.atan2(1, 0))
@@ -131,7 +132,7 @@ class TestMath < Test::Unit::TestCase
check(2, Math.log(100, 10))
assert_equal(1.0/0, Math.log(1.0/0))
assert_nothing_raised { assert_infinity -Math.log(+0.0) }
- assert_raise(Math::DomainError) { Math.log(-0.0) } # TODO:
[ruby-core:28265]
+ assert_nothing_raised { assert_infinity -Math.log(-0.0) }
assert_raise(Math::DomainError) { Math.log(-1.0) }
assert_raise(TypeError) { Math.log(1,nil) }
end
@@ -142,7 +143,7 @@ class TestMath < Test::Unit::TestCase
check(2, Math.log2(4))
assert_equal(1.0/0, Math.log2(1.0/0))
assert_nothing_raised { assert_infinity -Math.log2(+0.0) }
- assert_raise(Math::DomainError) { Math.log2(-0.0) } # TODO:
[ruby-core:28265]
+ assert_nothing_raised { assert_infinity -Math.log2(-0.0) }
assert_raise(Math::DomainError) { Math.log2(-1.0) }
end
@@ -152,7 +153,7 @@ class TestMath < Test::Unit::TestCase
check(2, Math.log10(100))
assert_equal(1.0/0, Math.log10(1.0/0))
assert_nothing_raised { assert_infinity -Math.log10(+0.0) }
- assert_raise(Math::DomainError) { Math.log10(-0.0) } # TODO:
[ruby-core:28265]
+ assert_nothing_raised { assert_infinity -Math.log10(-0.0) }
assert_raise(Math::DomainError) { Math.log10(-1.0) }
end
@@ -161,7 +162,7 @@ class TestMath < Test::Unit::TestCase
check(1, Math.sqrt(1))
check(2, Math.sqrt(4))
assert_equal(1.0/0, Math.sqrt(1.0/0))
- assert_raise(Math::DomainError) { Math.sqrt(-0.0) } # TODO:
[ruby-core:28265]
+ assert_equal("0.0", Math.sqrt(-0.0).to_s) # insure it is +0.0, not
-0.0
assert_raise(Math::DomainError) { Math.sqrt(-1.0) }
end
on 2010-02-22 17:24
Hi, I agree with your patch including the change of sqrt and logs. These functions will return complex values for negative input values in future version of ruby (I believe it is 1.9.3 or later). So, I don't stick to them. I feel that returning NaN isn't programmer friendly. I think generating NaN is need to be noticed for programmers. Though NaN ruins all of computations with it, the execution isn't interrupted. Because of that, I suggest raising DomainError instead of retuning NaN. If raising DomainError, programmers will have to write codes handling DomainErrors may be raised. So far, the literal 0.0/0.0 is used for explicitly generating NaN. Since ruby now have Float::NAN, I think 0.0/0.0 may raises DomainError. -- Kenta Murata OpenPGP FP = FA26 35D7 4F98 3498 0810 E0D5 F213 966F E9EB 0BCC $BK\$r=q$-$^$7$?(B!! $B!X(BRuby $B5U0z$-%l%7%T!Y(B http://www.amazon.co.jp/dp/4798119881/mrkn-22 E-mail: mrkn@mrkn.jp twitter: http://twitter.com/mrkn/ blog: http://d.hatena.ne.jp/mrkn/
on 2010-02-23 06:15
So here's a summary of the changes that Kenta and I propose, followed by final questions for everyone. ** Compared to Ruby 1.8: - New Exception class: DomainError, raised by mathematical functions for inputs without well defined results - Errno::EDOM and Errno::ERANGE are no longer raised. - Corner cases are now platform independent; a few deviate from SUSv3 (see below) ** Interpretation of SUSv3: - Domain Errors: Ruby 1.9 raises a DomainError in these cases - Pole Errors: Ruby 1.9 returns +-Infinity, as appropriate. - Range Errors: Ruby 1.9 returns the under/overflowed result. ** Ruby 1.9 deviates from SUSv3 in a few corner cases: - Math.atan2(+-0.0, +-0.0) raises a DomainError (instead of returning +-pi/2 or +-0.0) - Math.atan2(+-Infinity, +-Infinity) raises a DomainError (instead of returning +-pi/4 or +-3pi/4) - Math.lgamma(-Infinity) raises a DomainError (instead of returning +Infinity) - Math.sqrt(-0.0) returns 0.0 (instead of -0.0) ** Remaining questions 1) Is it ok if 0.0/0.0 raises a DomainError instead of returning NaN? This has the benefits of making the treatment of domain errors consistent. The only way to get a NaN from a calculation would then be to explicitly use Float::NAN for an input. This might imply some incompatibilities, though? Should Ruby 1.8.7/8 issue a warning? 2) Should DivisionByZero error be a subclass of DomainError? I feel it should, but I don't know how much changes to error class hierarchy should be avoided. 3) Should the new DomainError be a subclass of ArgumentError, or of RuntimeError? ArgumentError makes sense to me, but I'm not sure why it was originally chosen that DivisionByZero was not an ArgumentError. 4) Since we never raise Errno::EDOM and Errno::ERANGE, should they still be defined? Removing them might cause incompatibilities (?) but keeping them might give the wrong impression. Hopefully others can chime in on these 4 questions?
on 2010-02-23 08:07
2010/2/23 Marc-Andre Lafortune <ruby-core-mailing-list@marc-andre.ca>: > 4) Since we never raise Errno::EDOM and Errno::ERANGE, should they > still be defined? They are not only for math library. % ruby -rsocket -ve ' s = Socket.new(:INET, :DGRAM) s.setsockopt(:SOCKET, :SNDTIMEO, "\x7f"*8) ' ruby 1.9.2dev (2009-09-24 trunk 25067) [i686-linux] -e:3:in `setsockopt': Numerical argument out of domain (Errno::EDOM) from -e:3:in `<main>'
on 2010-02-23 17:41
Hi On 2010/02/23, at 14:15, Marc-Andre Lafortune <ruby-core-mailing-list@marc-andre.ca > wrote: > ArgumentError makes sense to me, but I'm not sure why it was > originally chosen that DivisionByZero was not an ArgumentError. I believe DivideByZero may be a subclass of ArgumentError because it is almost synonym to the pole error.
on 2010-02-25 15:15
Hi,
I've written a patch for rubyspec.
diff --git a/core/math/gamma_spec.rb b/core/math/gamma_spec.rb
index 4215075..4b291d3 100644
--- a/core/math/gamma_spec.rb
+++ b/core/math/gamma_spec.rb
@@ -33,23 +33,39 @@ ruby_version_is "1.9" do
it "returns good numerical approximation for gamma(-0.00001)" do
Math.gamma(-0.00001).should be_close(-100000.577225, TOLERANCE)
end
-
- it "raises Domain Error given -1" do
- lambda { Math.gamma(-1) }.should raise_error(Errno::EDOM)
+
+ ruby_version_is ""..."1.9" do
+ it "raises Domain Error given -1" do
+ lambda { Math.gamma(-1) }.should raise_error(Errno::EDOM)
+ end
end
-
+
+ ruby_version_is "1.9" do
+ it "raises Math::DomainError given -1" do
+ lambda { Math.gamma(-1) }.should raise_error(Math::DomainError)
+ end
+ end
+
# See http://redmine.ruby-lang.org/issues/show/2189
it "returns +infinity given +infinity" do
Math.gamma(infinity_value).infinite?.should == 1
end
-
- it "raises Domain Error given negative infinity" do
- lambda { Math.gamma(-infinity_value) }.should
raise_error(Errno::EDOM)
+
+ ruby_version_is ""..."1.9" do
+ it "raises Domain Error given negative infinity" do
+ lambda { Math.gamma(-infinity_value) }.should
raise_error(Errno::EDOM)
+ end
end
-
+
+ ruby_version_is "1.9" do
+ it "raises Math::DomainError given negative infinity" do
+ lambda { Math.gamma(-infinity_value) }.should
raise_error(Math::DomainError)
+ end
+ end
+
it "returns NaN given NaN" do
Math.gamma(nan_value).nan?.should be_true
end
end
-end
\ No newline at end of file
+end
diff --git a/core/math/lgamma_spec.rb b/core/math/lgamma_spec.rb
index c3bc48e..1ea5bf4 100644
--- a/core/math/lgamma_spec.rb
+++ b/core/math/lgamma_spec.rb
@@ -34,8 +34,16 @@ ruby_version_is "1.9" do
lg2[1].should == 1
end
- it "returns [Infinity, 1] when passed -Infinity" do
- Math.lgamma(-infinity_value).should == [infinity_value, 1]
+ ruby_version_is ""..."1.9" do
+ it "returns [Infinity, 1] when passed -Infinity" do
+ Math.lgamma(-infinity_value).should == [infinity_value, 1]
+ end
+ end
+
+ ruby_version_is "1.9" do
+ it "raises Math::DomainError when passed -Infinity" do
+ lambda { Math.lgamma(-infinity_value) }.should
raise_error(Math::DomainError)
+ end
end
# Note: see related issue
http://redmine.ruby-lang.org/issues/show/2189
@@ -50,15 +58,23 @@ ruby_version_is "1.9" do
# Note: see related issue
http://redmine.ruby-lang.org/issues/show/2189
# If you would like to see simpler Ruby behavior, lobby for Ruby to
# have platform-independent Math functions.
- platform_is :darwin do
- # JRuby has platform-independent math and behaves as above
- not_compliant_on :jruby do
- it "raises an Errno::EDOM when passed Infinity" do
- lambda { Math.lgamma(infinity_value) }.should
raise_error(Errno::EDOM)
+ ruby_version_is ""..."1.9" do
+ platform_is :darwin do
+ # JRuby has platform-independent math and behaves as above
+ not_compliant_on :jruby do
+ it "raises an Errno::EDOM when passed Infinity" do
+ lambda { Math.lgamma(infinity_value) }.should
raise_error(Errno::EDOM)
+ end
end
end
end
+ ruby_version_is "1.9" do
+ it "returns [Infinity, 1] when passed Infinity" do
+ Math.lgamma(infinity_value).should == [infinity_value, 1]
+ end
+ end
+
it "returns [NaN, 1] when passed NaN" do
Math.lgamma(nan_value)[0].nan?.should be_true
Math.lgamma(nan_value)[1].should == 1
diff --git a/library/mathn/math/shared/rsqrt.rb
b/library/mathn/math/shared/rsqrt.rb
index ff12abf..3d1b424 100644
--- a/library/mathn/math/shared/rsqrt.rb
+++ b/library/mathn/math/shared/rsqrt.rb
@@ -13,9 +13,19 @@ describe :mathn_math_rsqrt, :shared => true do
@object.send(:rsqrt, 12.34).should == Math.sqrt!(12.34)
end
- it "raises an Errno::EDOM if the argument is a negative number" do
- lambda { @object.send(:rsqrt, -1) }.should raise_error(Errno::EDOM)
- lambda { @object.send(:rsqrt, -4.0) }.should
raise_error(Errno::EDOM)
- lambda { @object.send(:rsqrt, -16/64) }.should
raise_error(Errno::EDOM)
+ ruby_version_is ""..."1.9" do
+ it "raises an Errno::EDOM if the argument is a negative number" do
+ lambda { @object.send(:rsqrt, -1) }.should
raise_error(Errno::EDOM)
+ lambda { @object.send(:rsqrt, -4.0) }.should
raise_error(Errno::EDOM)
+ lambda { @object.send(:rsqrt, -16/64) }.should
raise_error(Errno::EDOM)
+ end
+ end
+
+ ruby_version_is "1.9" do
+ it "raises an Math::DomainError if the argument is a negative
number" do
+ lambda { @object.send(:rsqrt, -1) }.should
raise_error(Math::DomainError)
+ lambda { @object.send(:rsqrt, -4.0) }.should
raise_error(Math::DomainError)
+ lambda { @object.send(:rsqrt, -16/64) }.should
raise_error(Math::DomainError)
+ end
end
end
diff --git a/shared/math/atanh.rb b/shared/math/atanh.rb
index b520171..69341af 100644
--- a/shared/math/atanh.rb
+++ b/shared/math/atanh.rb
@@ -120,11 +120,23 @@ describe :math_atanh_private, :shared => true do
end
describe :math_atanh_no_complex, :shared => true do
- it "raises an Errno::EDOM for arguments greater than 1.0" do
- lambda { @object.send(@method, 1.0 + Float::EPSILON) }.should
raise_error(Errno::EDOM)
+ ruby_version_is ""..."1.9" do
+ it "raises an Errno::EDOM for arguments greater than 1.0" do
+ lambda { @object.send(@method, 1.0 + Float::EPSILON) }.should
raise_error(Errno::EDOM)
+ end
+
+ it "raises an Errno::EDOM for arguments less than -1.0" do
+ lambda { @object.send(@method, -1.0 - Float::EPSILON) }.should
raise_error(Errno::EDOM)
+ end
end
- it "raises an Errno::EDOM for arguments less than -1.0" do
- lambda { @object.send(@method, -1.0 - Float::EPSILON) }.should
raise_error(Errno::EDOM)
+ ruby_version_is "1.9" do
+ it "raises an Math::DomainError for arguments greater than 1.0" do
+ lambda { @object.send(@method, 1.0 + Float::EPSILON) }.should
raise_error(Math::DomainError)
+ end
+
+ it "raises an Math::DomainError for arguments less than -1.0" do
+ lambda { @object.send(@method, -1.0 - Float::EPSILON) }.should
raise_error(Math::DomainError)
+ end
end
end
--
Kenta Murata
OpenPGP FP = FA26 35D7 4F98 3498 0810 E0D5 F213 966F E9EB 0BCC
E-mail: mrkn@mrkn.jp
twitter: http://twitter.com/mrkn/
blog: http://d.hatena.ne.jp/mrkn/
on 2010-02-25 19:27
Hi, On Thu, Feb 25, 2010 at 6:14 AM, Kenta Murata <muraken@gmail.com> wrote: > Hi, > > I've written a patch for rubyspec. The previous behavior of Math is impossible to write rubyspecs for without an extreme amount of pain because it is platform-dependent. By this I mean that it even depends on the version of libc and at least on Linux glibc changed whether EDOM or ERANGE is set for atanh(-1) between versions. The consequence is that there is no accessible information in Ruby to determine one version of Linux with a libc that sets EDOM from one that sets ERANGE, which makes it almost impossible to write a sane rubyspec guard. I have carefully looked through all rbconfig.rb settings and other constants. I would be happy to be proven wrong. Even with the existing platform information used by the rubyspec guards, the specs are extremely convoluted and difficult. I am very pleased to see this effort to make Math sane in the face of platform oddities. But this still leaves the question of how to provide rubyspecs for the behavior of Math prior to 1.9.2. Here is my proposal. I have added two new guards to rubyspec: #unspecified, and #specified_on. The #unspecified guard never yields on MRI (regardless of version). It is used to contain and document specs for particular behaviors that MRI has either specifically said are "unspecified" or "implementation-specific" or are impossible to spec in a consistent manner. The #specified_on guard takes one or more implementation designators and yields on those implementations. For the Math specs, this is what I propose: 1. For all the standard behaviors (ie inputs in the domain), a single set of rubyspecs for each method should be consistent across MRI versions. These behaviors are spec'd normally. 2. For exceptional behaviors, the specs are written for the new MRI platform-independent behaviors (eg using Math::DomainError exception). 3. The existing platform-dependent behaviors in 1.8.x are explicitly "unspecified" in the specs. 4. Implementations are free to opt-in to the sane behaviors being established for 1.9.2 This is an example of how the specs for Math.atanh would appear describe "Math.atanh" do # standard specs for basic behaviors # ... ruby_version_is ""..."1.9" do unspecified do specified_on :rubinius, :jruby, :ironruby do it_behaves_like :math_atanh_exceptional, :atanh, Math end end end ruby_version_is "1.9" do it_behaves_like :math_atanh_exceptional, :atanh, Math end end Please note the following: * I am not proposing the Math behavior be *generally* unspecified on MRI versions before 1.9.2. Only the exceptional behaviors that are currently wildly platform-dependent. * I am not proposing anything really different than the existing MRI behaviors since they are so platform-dependent they appear almost random to normal Ruby code. I would only be formalizing that in the specs with the #unspecified guard. * Alternate implementations must have some specs to guide their implementation. Only saying the behavior is implementation-specific with no specs is not acceptable for rubyspec. * I am not proposing anything really different for existing alternative implementations. For example, Java already has platform independent behaviors for eg atanh() and that is exposed in JRuby, thereby requiring multiple "not_compliant_on :jruby" and "compliant_on :jruby" guards which further complicate the specs and are rather unfair (is it fair to say that since JRuby has nice, consistent behavior when MRI has weird platform-dependent behavior, that JRuby is "non-comforming"? Technically yes, but the whole point of rubyspec is to provide consistent behavior to Ruby code!) * The exceptional behaviors should impact much less code than the normal behaviors. If an alternative implementation follows the 1.9.2 sane behavior in compatibility versions < 1.9.2, that may result in bugs with existing Ruby code. Should that occur, the implementation can decided how to deal with it. The value of having an explicit, reasonable behavior for Math methods outweighs the possibility that the consistent behavior may cause compatibility issues. * This particular area of rubyspec (ie Math methods) is highly exceptional in that most of MRI does not have such difficult-to-define behavior. * I would be using the behavior of 1.9.2 for the #specified_on behaviors for compatibility versions < 1.9.2. In other words, for alternative implementations that opt-in, the behaviors would be those of 1.9.2 not some random other behavior. Rubyspec patches for the new 1.9.2 platform-independent Math behaviors are very welcome and much appreciated. Cheers, Brian
on 2010-02-28 11:37
Hi, On 2010/02/26, at 3:27, brian ford wrote: > Rubyspec patches for the new 1.9.2 platform-independent Math behaviors > are very welcome and much appreciated. I've merged and committed patches of platform-independent Math to ruby-trunk and rubyspec-master. -- Kenta Murata OpenPGP FP = FA26 35D7 4F98 3498 0810 E0D5 F213 966F E9EB 0BCC $BK\$r=q$-$^$7$?(B!! $B!X(BRuby $B5U0z$-%l%7%T!Y(B http://www.amazon.co.jp/dp/4798119881/mrkn-22 E-mail: mrkn@mrkn.jp twitter: http://twitter.com/mrkn/ blog: http://d.hatena.ne.jp/mrkn/
on 2010-03-02 06:15
Hi, 2010/2/28 Kenta Murata <muraken@gmail.com>: > I've merged and committed patches of platform-independent Math to > ruby-trunk and rubyspec-master. Great! A couple of things about the new exception class: 1) I just noticed that the new Exception class is Math::DomainError, and not simply DomainError as I thought. No other Exception class belongs to a Module/Class (well, except from Errno), even though some are only raise by a specific class (e.g. ThreadError, FiberError). Moreover, there might be other methods than Math:: methods that raise it (among others, maybe Float#/ will raise it). For these reasons, I feel it should not be scoped. 2) Although I suggested that DomainError could be a subclass of ArgumentError, I feel now, after reviewing the exception classes, that it should not, in the same way that ZeroDivisionError/TypeError/IndexError/KeyError aren't. Moreover, maybe one day we'll want Float#log, say, and then it will clearly make no sense to be an ArgumentError. 3) Finally, I can not see when a ZeroDivisionError should not be considered to also be a DomainError, so should we should have ZeroDivisionError < DomainError. What do you think of the following patch I propose? diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h index d56d42c..1e18dbe 100644 --- a/include/ruby/ruby.h +++ b/include/ruby/ruby.h @@ -1234,7 +1234,7 @@ RUBY_EXTERN VALUE rb_eNameError; RUBY_EXTERN VALUE rb_eSyntaxError; RUBY_EXTERN VALUE rb_eLoadError; -RUBY_EXTERN VALUE rb_eMathDomainError; +RUBY_EXTERN VALUE rb_eDomainError; RUBY_EXTERN VALUE rb_stdin, rb_stdout, rb_stderr; diff --git a/math.c b/math.c index d4251df..0232be9 100644 --- a/math.c +++ b/math.c @@ -16,7 +16,6 @@ #define numberof(array) (int)(sizeof(array) / sizeof((array)[0])) VALUE rb_mMath; -VALUE rb_eMathDomainError; extern VALUE rb_to_float(VALUE val); #define Need_Float(x) do {if (TYPE(x) != T_FLOAT) {(x) = rb_to_float(x);}} while(0) @@ -26,7 +25,7 @@ extern VALUE rb_to_float(VALUE val); } while (0) #define domain_error(msg) \ - rb_raise(rb_eMathDomainError, "Numerical argument is out of domain - " #msg); + rb_raise(rb_eDomainError, "Numerical argument is out of domain - " #msg); /* * call-seq: @@ -746,7 +745,6 @@ void Init_Math(void) { rb_mMath = rb_define_module("Math"); - rb_eMathDomainError = rb_define_class_under(rb_mMath, "DomainError", rb_eArgError); #ifdef M_PI rb_define_const(rb_mMath, "PI", DBL2NUM(M_PI)); diff --git a/numeric.c b/numeric.c index 02a3da6..b27bb71 100644 --- a/numeric.c +++ b/numeric.c @@ -104,6 +104,7 @@ VALUE rb_cFloat; VALUE rb_cInteger; VALUE rb_cFixnum; +VALUE rb_eDomainError; VALUE rb_eZeroDivError; VALUE rb_eFloatDomainError; @@ -3218,7 +3219,8 @@ Init_Numeric(void) id_to_i = rb_intern("to_i"); id_eq = rb_intern("=="); - rb_eZeroDivError = rb_define_class("ZeroDivisionError", rb_eStandardError); + rb_eDomainError = rb_define_class("DomainError", rb_eArgError); + rb_eZeroDivError = rb_define_class("ZeroDivisionError", rb_eDomainError); rb_eFloatDomainError = rb_define_class("FloatDomainError", rb_eRangeError); rb_cNumeric = rb_define_class("Numeric", rb_cObject);
on 2010-03-02 10:34
On 2010/03/02 14:15, Marc-Andre Lafortune wrote: > 1) I just noticed that the new Exception class is Math::DomainError, > and not simply DomainError as I thought. > > No other Exception class belongs to a Module/Class (well, except from > Errno), even though some are only raise by a specific class (e.g. > ThreadError, FiberError). What about Encoding::InvalidByteSequenceError and similar? Regards, Martin.
on 2010-03-09 04:59
Hi,
On Tue, Mar 2, 2010 at 4:33 AM, "Martin J.
Dürst"<duerst@it.aoyama.ac.jp> wrote:
> What about Encoding::InvalidByteSequenceError and similar?
Thanks for bringing these up, I was not aware of these. I'll add these
to the Exceptions that should be documented [redmine:2829]
In view of this, I'm fine with Math::DomainError.
Remaining questions:
2) should it subclass ArgumentError?
As Yusuke & Yui stated in another thread [ruby-core:28440], the
hierarchy of errors might benefit in a restructuring. Even when that
happens, DomainError shouldn't be a subclass of ArgumentError, because
it might not be caused by an argument at all, like in my example:
-1.log, if #log was defined for Numerics.
3) Are there cases where ZeroDivisionError should not be also a
Math::DomainError?
on 2010-03-09 17:38
Hi, 2010/2/19 Kenta Murata <muraken@gmail.com>: > I try to write a patch for atanh, log, log2, log10, and sqrt > according to the following policies: We cannot use isinf and signbit directly because they are in C99. In fact, make fails on OpenSolaris 2009.06. Compatibility functions are needed for such environments. I think Naruse will do :-)
on 2010-04-28 18:15
Unless there is an objection, I will make Math::DomainError a direct descendant of StandardError (instead of ArgumentError), the same way that RangeError or IndexError are. I also think that ZeroDivisionError should be a descendant of Math::DomainError, but that is less important and should be confirmed. On Mon, Mar 8, 2010 at 11:58 PM, Marc-Andre Lafortune
on 2010-04-29 10:07
On Thu, Apr 29, 2010 at 2:14 AM, Marc-Andre Lafortune <ruby-core-mailing-list@marc-andre.ca> wrote: > I also think that ZeroDivisionError should be a descendant of > Math::DomainError, but that is less important and should be confirmed. To me, "divide by zero" is such a fundamental low-level error, whereas Math::DomainError sounds more high-level, and the two don't mix well. I understand your logic though, and don't mind either way. Just throwing in another thought. Thanks for your work on this, Marc-Andre and others. I'm looking forward to Ruby having more mathematical correctness and goodies out of the box. Gavin
Please log in before posting. Registration is free and takes only a minute.
Existing account
(Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
Log in with Google account | Log in with Yahoo account
No account? Register here.