From ed7e07a178df8d68e25adfaab46b62b241f77ce2 Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@jeremyevans.net>
Date: Tue, 4 Jun 2019 21:41:02 -0700
Subject: [PATCH] Implement Complex#<=>, and treat Complex with zero imaginary
 part as real

A complex number with a zero imaginary part is a real number,
and should be treated as such.

Implement Complex#<=> so that it is usable as an argument when
calling <=> on objects of other classes (since #coerce will coerce
such numbers to Complex).  If the complex number has a zero imaginary
part, and the other argument is a real number, return -1, 0, or 1.
Otherwise, return nil, indicating the objects are not comparable.

Fixes [Bug #15857]
---
 complex.c                           | 51 ++++++++++++++++++++++++-----
 spec/ruby/core/complex/real_spec.rb | 30 +++++++++++++----
 test/ruby/test_complex.rb           | 19 ++++++++---
 test/ruby/test_complexrational.rb   |  8 ++---
 4 files changed, 84 insertions(+), 24 deletions(-)

diff --git a/complex.c b/complex.c
index aa905c1c88..24b1d2b0c2 100644
--- a/complex.c
+++ b/complex.c
@@ -36,7 +36,7 @@ static ID id_abs, id_arg,
     id_denominator, id_fdiv, id_numerator, id_quo,
     id_real_p, id_i_real, id_i_imag,
     id_finite_p, id_infinite_p, id_rationalize,
-    id_PI;
+    id_spaceship, id_PI;
 #define id_to_i idTo_i
 #define id_to_r idTo_r
 #define id_negate idUMinus
@@ -1021,14 +1021,45 @@ nucomp_eqeq_p(VALUE self, VALUE other)
     return f_boolcast(f_eqeq_p(other, self));
 }
 
+static VALUE nucomp_real_p(VALUE self);
+
+/*
+ * call-seq:
+ *    cmp <=> object  ->  0, 1, -1, or nil
+ *
+ * If +cmp+ is a real number (imaginary part is zero), and +object+ is also a
+ * real number, compare the real part of +cmp+ to object.  Otherwise, return
+ * nil.
+ *
+ *    Complex(2, 3)  <=> Complex(2, 3)   #=> nil
+ *    Complex(2, 3)  <=> 1               #=> nil
+ *    Complex(2)     <=> 1               #=> 1
+ *    Complex(2)     <=> 2               #=> 0
+ *    Complex(2)     <=> 3               #=> -1
+ */
+static VALUE
+nucomp_cmp(VALUE self, VALUE other)
+{
+    if (nucomp_real_p(self) && k_numeric_p(other) && f_real_p(other)) {
+        if (RB_TYPE_P(other, T_COMPLEX)) {
+            get_dat2(self, other);
+            return rb_funcall(adat->real, id_spaceship, 1, bdat->real);
+        } else {
+            get_dat1(self);
+            return rb_funcall(dat->real, id_spaceship, 1, other);
+        }
+    }
+    return Qnil;
+}
+
 /* :nodoc: */
 static VALUE
 nucomp_coerce(VALUE self, VALUE other)
 {
-    if (k_numeric_p(other) && f_real_p(other))
-	return rb_assoc_new(f_complex_new_bang1(CLASS_OF(self), other), self);
     if (RB_TYPE_P(other, T_COMPLEX))
 	return rb_assoc_new(other, self);
+    if (k_numeric_p(other) && f_real_p(other))
+	return rb_assoc_new(f_complex_new_bang1(CLASS_OF(self), other), self);
 
     rb_raise(rb_eTypeError, "%"PRIsVALUE" can't be coerced into %"PRIsVALUE,
 	     rb_obj_class(other), rb_obj_class(self));
@@ -1147,14 +1178,16 @@ rb_complex_conjugate(VALUE self)
 
 /*
  * call-seq:
- *    cmp.real?  ->  false
+ *    Complex(1).real?     ->  true
+ *    Complex(1, 1).real?  ->  false
  *
- * Returns false.
+ * Returns true if the imaginary part is zero, and false otherwise.
  */
 static VALUE
-nucomp_false(VALUE self)
+nucomp_real_p(VALUE self)
 {
-    return Qfalse;
+    get_dat1(self);
+    return(f_zero_p(dat->imag) ? Qtrue : Qfalse);
 }
 
 /*
@@ -2211,6 +2244,7 @@ Init_Complex(void)
     id_finite_p = rb_intern("finite?");
     id_infinite_p = rb_intern("infinite?");
     id_rationalize = rb_intern("rationalize");
+    id_spaceship = rb_intern("<=>");
     id_PI = rb_intern("PI");
 
     rb_cComplex = rb_define_class("Complex", rb_cNumeric);
@@ -2254,6 +2288,7 @@ Init_Complex(void)
     rb_define_method(rb_cComplex, "**", rb_complex_pow, 1);
 
     rb_define_method(rb_cComplex, "==", nucomp_eqeq_p, 1);
+    rb_define_method(rb_cComplex, "<=>", nucomp_cmp, 1);
     rb_define_method(rb_cComplex, "coerce", nucomp_coerce, 1);
 
     rb_define_method(rb_cComplex, "abs", rb_complex_abs, 0);
@@ -2268,7 +2303,7 @@ Init_Complex(void)
     rb_define_method(rb_cComplex, "conjugate", rb_complex_conjugate, 0);
     rb_define_method(rb_cComplex, "conj", rb_complex_conjugate, 0);
 
-    rb_define_method(rb_cComplex, "real?", nucomp_false, 0);
+    rb_define_method(rb_cComplex, "real?", nucomp_real_p, 0);
 
     rb_define_method(rb_cComplex, "numerator", nucomp_numerator, 0);
     rb_define_method(rb_cComplex, "denominator", nucomp_denominator, 0);
diff --git a/spec/ruby/core/complex/real_spec.rb b/spec/ruby/core/complex/real_spec.rb
index 2ea791c005..e9287fa7d0 100644
--- a/spec/ruby/core/complex/real_spec.rb
+++ b/spec/ruby/core/complex/real_spec.rb
@@ -14,15 +14,31 @@
     Complex(2,3).real?.should be_false
   end
 
-  it "returns false if there is not an imaginary part" do
-    Complex(2).real?.should be_false
-  end
+  ruby_version_is "2.7" do
+    it "returns false if there is not an imaginary part" do
+      Complex(2).real?.should be_true
+    end
+
+    it "returns false if the real part is Infinity" do
+      Complex(infinity_value).real?.should be_true
+    end
 
-  it "returns false if the real part is Infinity" do
-    Complex(infinity_value).real?.should be_false
+    it "returns false if the real part is NaN" do
+      Complex(nan_value).real?.should be_true
+    end
   end
 
-  it "returns false if the real part is NaN" do
-    Complex(nan_value).real?.should be_false
+  ruby_version_is "" ... "2.7" do
+    it "returns false if there is not an imaginary part" do
+      Complex(2).real?.should be_false
+    end
+
+    it "returns false if the real part is Infinity" do
+      Complex(infinity_value).real?.should be_false
+    end
+
+    it "returns false if the real part is NaN" do
+      Complex(nan_value).real?.should be_false
+    end
   end
 end
diff --git a/test/ruby/test_complex.rb b/test/ruby/test_complex.rb
index 3da66890c1..ebfdb8f4e8 100644
--- a/test/ruby/test_complex.rb
+++ b/test/ruby/test_complex.rb
@@ -193,7 +193,9 @@ def test_attr2
     c = Complex(1)
 
     assert_not_predicate(c, :integer?)
-    assert_not_predicate(c, :real?)
+
+    assert_predicate(c, :real?)
+    assert_not_predicate(Complex(0,1), :real?)
 
     assert_predicate(Complex(0), :zero?)
     assert_predicate(Complex(0,0), :zero?)
@@ -518,11 +520,19 @@ def test_expt
   end
 
   def test_cmp
-    assert_raise(NoMethodError){1 <=> Complex(1,1)}
-    assert_raise(NoMethodError){Complex(1,1) <=> 1}
-    assert_raise(NoMethodError){Complex(1,1) <=> Complex(1,1)}
+    assert_nil(Complex(5, 1) <=> Complex(2))
+    assert_nil(5 <=> Complex(2, 1))
+
+    assert_equal(1, Complex(5) <=> Complex(2))
+    assert_equal(-1, Complex(2) <=> Complex(3))
+    assert_equal(0, Complex(2) <=> Complex(2))
+
+    assert_equal(1, Complex(5) <=> 2)
+    assert_equal(-1, Complex(2) <=> 3)
+    assert_equal(0, Complex(2) <=> 2)
   end
 
+
   def test_eqeq
     assert_equal(Complex(1), Complex(1,0))
     assert_equal(Complex(-1), Complex(-1,0))
@@ -891,7 +901,6 @@ def o.to_c; raise; end
   def test_respond
     c = Complex(1,1)
     assert_not_respond_to(c, :%)
-    assert_not_respond_to(c, :<=>)
     assert_not_respond_to(c, :div)
     assert_not_respond_to(c, :divmod)
     assert_not_respond_to(c, :floor)
diff --git a/test/ruby/test_complexrational.rb b/test/ruby/test_complexrational.rb
index 0360f5ee42..bf4e2b1809 100644
--- a/test/ruby/test_complexrational.rb
+++ b/test/ruby/test_complexrational.rb
@@ -102,7 +102,7 @@ def test_comp_srat
     assert_equal(Complex(SimpleRat(4,3),SimpleRat(1,1)), c * 2)
     assert_equal(Complex(SimpleRat(1,3),SimpleRat(1,4)), c / 2)
     assert_equal(Complex(SimpleRat(7,36),SimpleRat(2,3)), c ** 2)
-    assert_raise(NoMethodError){c <=> 2}
+    assert_nil(c <=> 2)
 
     assert_equal(Complex(SimpleRat(8,3),SimpleRat(1,2)), 2 + c)
     assert_equal(Complex(SimpleRat(4,3),SimpleRat(-1,2)), 2 - c)
@@ -111,7 +111,7 @@ def test_comp_srat
     r = 2 ** c
     assert_in_delta(1.4940, r.real, 0.001)
     assert_in_delta(0.5392, r.imag, 0.001)
-    assert_raise(NoMethodError){2 <=> c}
+    assert_nil(2 <=> c)
 
     assert_equal(Complex(SimpleRat(13,6),SimpleRat(5,2)), c + cc)
     assert_equal(Complex(SimpleRat(-5,6),SimpleRat(-3,2)), c - cc)
@@ -120,7 +120,7 @@ def test_comp_srat
     r = c ** cc
     assert_in_delta(0.1732, r.real, 0.001)
     assert_in_delta(0.1186, r.imag, 0.001)
-    assert_raise(NoMethodError){c <=> cc}
+    assert_nil(c <=> cc)
 
     assert_equal(Complex(SimpleRat(13,6),SimpleRat(5,2)), cc + c)
     assert_equal(Complex(SimpleRat(5,6),SimpleRat(3,2)), cc - c)
@@ -129,7 +129,7 @@ def test_comp_srat
     r = cc ** c
     assert_in_delta(0.5498, r.real, 0.001)
     assert_in_delta(1.0198, r.imag, 0.001)
-    assert_raise(NoMethodError){cc <=> c}
+    assert_nil(cc <=> c)
 
     assert_equal([SimpleRat,SimpleRat],
 		 (+c).instance_eval{[real.class, imag.class]})
-- 
2.21.0

