Repository: extendr/extendr
Pull request #444 (2023-01-07 21:43:08)

`Debug` trait for `Rcplx` and `Complexes`
- Add proper `Debug` trait implementation for `Rcplx`
- Add proper `Debug` trait implementation for `Complexes`
- Add `Rfloat::abs()` function
- Fix `Rstr` `Debug` implementation
- Correct debug strings for `NA` values
- Make `Rany::Complexes` wrap `Complexes` instead of `Robj`
- Redirect `Debug` for `Rany::Complexes` to `Debug` implementation for `Complexes`
- Add tests to check `Debug` implementation for scalars and vector wrappers


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65c582b30..be12aca3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
 
 ### Added
 
+- `Debug` trait implementation for `Rcplx` and `Complexes` [[#444]](https://github.com/extendr/extendr/pull/444)
 - `Function` type that wraps an R function, which can be invoked using the `call()` method. [[#188]](https://github.com/extendr/extendr/pull/188)
 - `pairlist!` macro for generating `Pairlist` objects, e.g. for use in function calls. [[#202]](https://github.com/extendr/extendr/pull/202)
 - `use_try_from` option for the `extendr` macro, which allows the use of any type that implements `TryInto<Robj>`/`TryFrom<Robj>`, e.g. `#[extendr(use_try_from = true)]`. [[#222]](https://github.com/extendr/extendr/pull/222)
diff --git a/extendr-api/src/lib.rs b/extendr-api/src/lib.rs
index 52f9203e2..ffc119e1c 100644
--- a/extendr-api/src/lib.rs
+++ b/extendr-api/src/lib.rs
@@ -471,7 +471,7 @@ pub enum Rany<'a> {
     Logicals(&'a Logicals),       // LGLSXP
     Integers(&'a Integers),       // INTSXP
     Doubles(&'a Doubles),         // REALSXP
-    Complexes(&'a Robj),          // CPLXSXP
+    Complexes(&'a Complexes),     // CPLXSXP
     Strings(&'a Strings),         // STRSXP
     Dot(&'a Robj),                // DOTSXP
     Any(&'a Robj),                // ANYSXP
diff --git a/extendr-api/src/robj/debug.rs b/extendr-api/src/robj/debug.rs
index 7267d22fb..ab0e0c483 100644
--- a/extendr-api/src/robj/debug.rs
+++ b/extendr-api/src/robj/debug.rs
@@ -35,9 +35,7 @@ impl std::fmt::Debug for Robj {
             Rany::Logicals(value) => value.fmt(f),
             Rany::Integers(value) => value.fmt(f),
             Rany::Doubles(value) => value.fmt(f),
-            Rany::Complexes(_complex) => {
-                todo!("implement Complexes")
-            }
+            Rany::Complexes(value) => value.fmt(f),
             Rany::Strings(value) => write!(f, "{:?}", value.as_slice()),
             Rany::Dot(_dot) => write!(f, "Dot"),
             Rany::Any(_any) => write!(f, "Any"),
diff --git a/extendr-api/src/scalar/rcplx_default.rs b/extendr-api/src/scalar/rcplx_default.rs
index e09bfd688..a7e4ac880 100644
--- a/extendr-api/src/scalar/rcplx_default.rs
+++ b/extendr-api/src/scalar/rcplx_default.rs
@@ -41,11 +41,15 @@ impl CanBeNA for c64 {
 /// Rcplx has a special NA value, obtained from R headers via R_NaReal.
 ///
 /// Rcplx has the same footprint as R's complex value allowing us to use it in zero copy slices.
-#[derive(Clone, Copy, Debug, Default, PartialEq)]
+#[derive(Clone, Copy, Default, PartialEq)]
+#[repr(C)]
 pub struct Rcplx(c64);
 
 impl Rcplx {
     // gen_impl!(Rcplx, c64);
+    pub fn new(re: f64, im: f64) -> Self {
+        Self(c64::new(re, im))
+    }
 
     pub fn re(&self) -> Rfloat {
         Rfloat::from(self.0.re)
@@ -117,3 +121,23 @@ impl PartialEq<f64> for Rcplx {
         self.re().inner() == *other && self.im() == 0.0
     }
 }
+
+impl std::fmt::Debug for Rcplx {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if self.is_na() {
+            write!(f, "NA_COMPLEX")
+        } else {
+            write!(
+                f,
+                "{:?} {} {:?}i",
+                self.re(),
+                if self.im().is_sign_negative() {
+                    '-'
+                } else {
+                    '+'
+                },
+                self.im().abs()
+            )
+        }
+    }
+}
diff --git a/extendr-api/src/scalar/rcplx_full.rs b/extendr-api/src/scalar/rcplx_full.rs
index 18047f217..c55b2cf11 100644
--- a/extendr-api/src/scalar/rcplx_full.rs
+++ b/extendr-api/src/scalar/rcplx_full.rs
@@ -23,12 +23,16 @@ impl CanBeNA for c64 {
 /// Rcplx has a special NA value, obtained from R headers via R_NaReal.
 ///
 /// Rcplx has the same footprint as R's complex value allowing us to use it in zero copy slices.
-#[derive(Debug)]
+#[repr(C)]
 pub struct Rcplx(c64);
 
 impl Rcplx {
     gen_impl!(Rcplx, c64);
 
+    pub fn new(re: f64, im: f64) -> Self {
+        Self(c64::new(re, im))
+    }
+
     pub fn is_nan(&self) -> bool {
         self.0.is_nan()
     }
@@ -159,3 +163,23 @@ impl PartialEq<f64> for Rcplx {
         self.re().inner() == *other && self.im() == 0.0
     }
 }
+
+impl std::fmt::Debug for Rcplx {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if self.is_na() {
+            write!(f, "NA_COMPLEX")
+        } else {
+            write!(
+                f,
+                "{:?} {} {:?}i",
+                self.re(),
+                if self.im().is_sign_negative() {
+                    '-'
+                } else {
+                    '+'
+                },
+                self.im().abs()
+            )
+        }
+    }
+}
diff --git a/extendr-api/src/scalar/rfloat.rs b/extendr-api/src/scalar/rfloat.rs
index 71e457fd5..8411988a5 100644
--- a/extendr-api/src/scalar/rfloat.rs
+++ b/extendr-api/src/scalar/rfloat.rs
@@ -30,6 +30,9 @@ impl Rfloat {
     pub fn is_subnormal(&self) -> bool {
         self.0.is_subnormal()
     }
+    pub fn abs(&self) -> Rfloat {
+        self.0.abs().into()
+    }
 }
 
 // `NA_real_` is a `NaN` with specific bit representation.
diff --git a/extendr-api/src/wrapper/complexes.rs b/extendr-api/src/wrapper/complexes.rs
index 9b677a530..ce854e6ed 100644
--- a/extendr-api/src/wrapper/complexes.rs
+++ b/extendr-api/src/wrapper/complexes.rs
@@ -12,7 +12,7 @@ use std::iter::FromIterator;
 ///     assert_eq!(vec.len(), 5);
 /// }
 /// ```  
-#[derive(Debug, PartialEq, Clone)]
+#[derive(PartialEq, Clone)]
 pub struct Complexes {
     pub(crate) robj: Robj,
 }
@@ -68,3 +68,13 @@ impl DerefMut for Complexes {
         }
     }
 }
+
+impl std::fmt::Debug for Complexes {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        if self.len() == 1 {
+            write!(f, "{:?}", self.elt(0))
+        } else {
+            f.debug_list().entries(self.iter()).finish()
+        }
+    }
+}
diff --git a/extendr-api/src/wrapper/rstr.rs b/extendr-api/src/wrapper/rstr.rs
index 1525b7aa6..e0e7a74e9 100644
--- a/extendr-api/src/wrapper/rstr.rs
+++ b/extendr-api/src/wrapper/rstr.rs
@@ -89,8 +89,12 @@ impl PartialEq<str> for Rstr {
 
 impl std::fmt::Debug for Rstr {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let s = self.as_str();
-        write!(f, "{:?}", s)
+        if self.is_na() {
+            write!(f, "NA_CHARACTER")
+        } else {
+            let s = self.as_str();
+            write!(f, "{:?}", s)
+        }
     }
 }
 


Test patch:
diff --git a/extendr-api/tests/debug_tests.rs b/extendr-api/tests/debug_tests.rs
index 51e652d4e..57f81e127 100644
--- a/extendr-api/tests/debug_tests.rs
+++ b/extendr-api/tests/debug_tests.rs
@@ -1,4 +1,5 @@
 use extendr_api::prelude::*;
+use std::fmt::format;
 
 #[test]
 fn test_debug() {
@@ -62,3 +63,84 @@ fn test_debug() {
         assert_eq!(format!("{:?}", r), "S4");
     }
 }
+
+#[test]
+fn test_debug_scalar() {
+    test! {
+        let test_data = vec![(true, "TRUE"), (false, "FALSE")];
+        for (val, dbg_str) in test_data {
+            assert_eq!(format!("{:?}", Rbool::from(val)), dbg_str);
+        }
+        assert_eq!(format!("{:?}", Rbool::na()), "NA_LOGICAL");
+
+        let test_data = vec![42, -42, 0];
+        for val in test_data {
+            assert_eq!(format!("{:?}", Rint::from(val)), format!("{:?}", val),);
+        }
+        assert_eq!(format!("{:?}", Rint::na()), "NA_INTEGER");
+
+        let test_data = vec![
+            42.,
+            -42.,
+            0.,
+            f64::INFINITY,
+            f64::NEG_INFINITY,
+            f64::NAN,
+            3.141592653589793,
+            -3.141592653589793,
+        ];
+        for val in test_data {
+            assert_eq!(format!("{:?}", Rfloat::from(val)), format!("{:?}", val));
+        }
+        assert_eq!(format!("{:?}", Rfloat::na()), "NA_REAL");
+
+        let test_data = vec![
+            (42., 42., "42.0 + 42.0i"),
+            (-42., 42., "-42.0 + 42.0i"),
+            (42., -42., "42.0 - 42.0i"),
+            (-42., -42., "-42.0 - 42.0i"),
+            (0., 0., "0.0 + 0.0i"),
+        ];
+        for (re, im, dbg_str) in test_data {
+            assert_eq!(format!("{:?}", Rcplx::new(re, im)), dbg_str);
+        }
+        assert_eq!(format!("{:?}", Rcplx::na()), "NA_COMPLEX");
+
+        let test_data = vec!["Hello", "World"];
+        for str in test_data {
+            assert_eq!(format!("{:?}", Rstr::from(str)), format!("{:?}", str));
+        }
+        assert_eq!(format!("{:?}", Rstr::na()), "NA_CHARACTER");
+    }
+}
+
+#[test]
+fn test_debug_vectors() {
+    test! {
+        let r: Logicals = Logicals::from_values([TRUE]);
+        assert_eq!(format!("{:?}", r), "TRUE");
+        let r: Logicals = Logicals::from_values([TRUE, FALSE, NA_LOGICAL]);
+        assert_eq!(format!("{:?}", r), "[TRUE, FALSE, NA_LOGICAL]");
+
+        let r: Integers = Integers::new(1);
+        assert_eq!(format!("{:?}", r), "0");
+        let r: Integers = Integers::from_values([Rint::from(0), Rint::from(0), Rint::na()]);
+        assert_eq!(format!("{:?}", r), "[0, 0, NA_INTEGER]");
+
+        let r: Doubles = Doubles::new(1);
+        assert_eq!(format!("{:?}", r), "0.0");
+        let r: Doubles = Doubles::from_values([Rfloat::from(0.0), Rfloat::from(0.0), Rfloat::na()]);
+        assert_eq!(format!("{:?}", r), "[0.0, 0.0, NA_REAL]");
+
+        let r: Strings = Strings::from_values(["xyz"]);
+        assert_eq!(format!("{:?}", r), "\"xyz\"");
+        let r: Strings = Strings::from_values([Rstr::from("xyz"), Rstr::from("abc"), Rstr::na()]);
+        assert_eq!(format!("{:?}", r), "[\"xyz\", \"abc\", NA_CHARACTER]");
+
+        let r: Complexes = Complexes::from_values([Rcplx::new(42.0, -42.0)]);
+        assert_eq!(format!("{:?}", r), "42.0 - 42.0i");
+        let r: Complexes =
+            Complexes::from_values([Rcplx::new(42.0, -42.0), Rcplx::new(0.0, 0.0), Rcplx::na()]);
+        assert_eq!(format!("{:?}", r), "[42.0 - 42.0i, 0.0 + 0.0i, NA_COMPLEX]");
+    }
+}
diff --git a/extendr-api/tests/scalar_tests.rs b/extendr-api/tests/scalar_tests.rs
index 8956989af..a56b979c5 100644
--- a/extendr-api/tests/scalar_tests.rs
+++ b/extendr-api/tests/scalar_tests.rs
@@ -18,6 +18,8 @@ fn test_rint() {
     assert_eq!(-&a, Rint::from(-20));
     assert_eq!(!&a, Rint::from(-21));
 
+    assert!(Rint::na().is_na());
+
     // NA lhs
     let a = Rint::na();
     let b = Rint::from(10);
@@ -171,6 +173,8 @@ fn test_rfloat() {
         assert_eq!(&a / b, Rfloat::from(2.));
         assert_eq!(-&a, Rfloat::from(-20.));
 
+        assert!(Rfloat::na().is_na());
+
         // NA lhs
         let a = Rfloat::na();
         let b = Rfloat::from(10.);
@@ -229,6 +233,11 @@ fn test_rfloat() {
         // Some more, testing mixed binary operators
         assert!((Rfloat::from(f64::INFINITY) + 1.).is_infinite());
         assert!((42. - Rfloat::from(f64::INFINITY)).is_sign_negative());
+
+        // Absolute value
+        assert_eq!(Rfloat::from(-42.).abs(), Rfloat::from(42.));
+        assert_eq!(Rfloat::from(42.).abs(), Rfloat::from(42.));
+        assert_eq!(Rfloat::from(0.).abs(), Rfloat::from(0.));
     }
 }
 
@@ -253,6 +262,8 @@ fn test_rcplx() {
         assert_eq!(&a / b, Rcplx::from(2.));
         assert_eq!(-&a, Rcplx::from(-20.));
 
+        assert!(Rcplx::na().is_na());
+
         // NA lhs
         let a = Rcplx::na();
         let b = Rcplx::from(10.);