Repository: extendr/extendr (rust)

Issue:
Support `PartialOrd` trait for R scalars
At present, there is no way to compare the max or min of two `Rfloat`s. To do this, we need to grab the inner f64.  For example using an `Rfloat` `x` and `y` and return an Rfloat this is done: 

`Rfloat::from(x.0.min(y.0))`

Consider treating `NA` as `NaN` so no comparison is done. 

Pull request:
`PartialOrd` for scalars
- `Scalar<T>` to initiate abstraction of common scalar properties
- `PartialOrd` for `Rint`, `Rfloat`, `Rbool` (`NA`-aware)
- `min()/max()` on `Rint` & `Rfloat` based on `PartialOrd`
- `From<Rint>` and `From<i32>` for `Rfloat` (note that `i32::MIN_VALUE` will not be treated as `NA_real_`)

Closes #570

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7719b4a28..143ba2721 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### Added
 
 - [**either**] `TryFrom<&Robj> for Either<T, R>` and `From<Either<T, R>> for Robj` if `T` and `R` are themselves implement these traits. This unblocks scenarios like accepting any numeric vector from R via `Either<Integers, Doubles>` without extra memory allocation [[#480]](https://github.com/extendr/extendr/pull/480)
+- `PartialOrd` trait implementation for `Rfloat`, `Rint` and `Rbool`. `Rfloat` and `Rint` gained `min()` and `max()` methods [[#573]](https://github.com/extendr/extendr/pull/573)
 
 ### Fixed
 
diff --git a/extendr-api/src/robj/into_robj.rs b/extendr-api/src/robj/into_robj.rs
index a412d1415..ee50803e6 100644
--- a/extendr-api/src/robj/into_robj.rs
+++ b/extendr-api/src/robj/into_robj.rs
@@ -1,4 +1,5 @@
 use super::*;
+use crate::scalar::Scalar;
 use crate::single_threaded;
 
 pub(crate) fn str_to_character(s: &str) -> SEXP {
diff --git a/extendr-api/src/scalar/macros.rs b/extendr-api/src/scalar/macros.rs
index 1abb9b45e..b266d069b 100644
--- a/extendr-api/src/scalar/macros.rs
+++ b/extendr-api/src/scalar/macros.rs
@@ -599,27 +599,6 @@ macro_rules! gen_from_scalar {
     };
 }
 
-/// Generates an implementation of the instance `inner()` method for a type
-///
-/// This macro requires the following arguments:
-///
-/// * `$type`      - The Type the `inner()` method is implemented for
-/// * `$type_prim` - The primitive Rust scalar type that corresponds to `$type`
-///
-/// Example Usage:
-///
-/// ```ignore
-/// gen_impl!(Rint, i32);
-/// ```
-macro_rules! gen_impl {
-    ($type : ident, $type_prim : ty) => {
-        /// Get underlying value.
-        pub fn inner(&self) -> $type_prim {
-            self.0
-        }
-    };
-}
-
 /// Generates an implementation of a number of Traits for the specified Type
 ///
 /// This macro requires the following arguments:
@@ -714,19 +693,11 @@ macro_rules! gen_trait_impl {
             #[doc = "```"]
             impl PartialEq<$type> for $type {
                 fn eq(&self, other: &$type) -> bool {
-                    !(self.is_na() || other.is_na()) && self.0 == other.0
+                    !(self.is_na() || other.is_na()) && self.inner().eq(&other.inner())
                 }
             }
         }
 
-        // The 'example usage' expands to...
-        //
-        // /// Documentation comments/test built by the #[doc] attributes
-        // impl PartialEq<i32> for Rint {
-        //     fn eq(&self, other: &i32) -> bool {
-        //         !self.is_na() && self.0 == *other
-        //     }
-        // }
         paste::paste! {
             #[doc = "```"]
             #[doc = "use extendr_api::prelude::*;"]
@@ -734,33 +705,23 @@ macro_rules! gen_trait_impl {
             #[doc = "    assert!(<" $type ">::default().eq(&<" $type_prim ">::default()));"]
             #[doc = "}"]
             #[doc = "```"]
-            impl PartialEq<$type_prim> for &$type {
+            impl PartialEq<$type_prim> for $type {
                 /// NA always fails.
                 fn eq(&self, other: &$type_prim) -> bool {
-                    <Option<$type_prim>>::try_from(**self) == Ok(Some(*other))
+                    <Option<$type_prim>>::try_from(self.clone()) == Ok(Some(*other))
                 }
             }
         }
-
-        // The 'example usage' expands to...
-        //
-        // /// Documentation comments/test built by the #[doc] attributes
-        // impl PartialEq<i32> for Rint {
-        //     fn eq(&self, other: &i32) -> bool {
-        //         !self.is_na() && self.0 == *other
-        //     }
-        // }
         paste::paste! {
             #[doc = "```"]
             #[doc = "use extendr_api::prelude::*;"]
             #[doc = "test! {"]
-            #[doc = "    assert!(<" $type ">::default().eq(&<" $type_prim ">::default()));"]
+            #[doc = "    assert!(<" $type_prim ">::default().eq(&<" $type ">::default()));"]
             #[doc = "}"]
             #[doc = "```"]
-            impl PartialEq<$type_prim> for $type {
-                /// NA always fails.
-                fn eq(&self, other: &$type_prim) -> bool {
-                    <Option<$type_prim>>::try_from(self.clone()) == Ok(Some(*other))
+            impl PartialEq<$type> for $type_prim {
+                fn eq(&self, other: &$type) -> bool {
+                    <Option<$type_prim>>::try_from(*other) == Ok(Some(*self))
                 }
             }
         }
@@ -789,6 +750,75 @@ macro_rules! gen_trait_impl {
     };
 }
 
+macro_rules! gen_partial_ord {
+    ($type : ident, $type_prim : ty) => {
+        paste::paste! {
+            #[doc = "```"]
+            #[doc = "use extendr_api::prelude::*;"]
+            #[doc = "test! {"]
+            #[doc = "    assert_eq!(<" $type ">::default() <  <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() <= <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() >  <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() >= <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() <  <" $type ">::default(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() <= <" $type ">::default(), true);"]
+            #[doc = "    assert_eq!(<" $type ">::default() >  <" $type ">::default(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() >= <" $type ">::default(), true);"]
+            #[doc = "}"]
+            #[doc = "```"]
+            impl std::cmp::PartialOrd<$type> for $type {
+                fn partial_cmp(&self, other: &$type) -> Option<std::cmp::Ordering> {
+                    if self.is_na() || other.is_na() {
+                        None
+                    } else {
+                        self.inner().partial_cmp(&other.inner())
+                    }
+                }
+            }
+        }
+
+        paste::paste! {
+            #[doc = "```"]
+            #[doc = "use extendr_api::prelude::*;"]
+            #[doc = "test! {"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() <  <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() <= <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() >  <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() >= <" $type ">::na(), false);"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() <  <" $type ">::default(), false);"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() <= <" $type ">::default(), true);"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() >  <" $type ">::default(), false);"]
+            #[doc = "    assert_eq!(<" $type_prim ">::default() >= <" $type ">::default(), true);"]
+            #[doc = "}"]
+            #[doc = "```"]
+            impl std::cmp::PartialOrd<$type_prim> for $type {
+                fn partial_cmp(&self, other: &$type_prim) -> Option<std::cmp::Ordering> {
+                    let other: $type = (*other).try_into().unwrap_or($type::na());
+                    self.partial_cmp(&other)
+                }
+            }
+        }
+
+        paste::paste! {
+            #[doc = "```"]
+            #[doc = "use extendr_api::prelude::*;"]
+            #[doc = "test! {"]
+            #[doc = "    assert_eq!(<" $type ">::default() <  <" $type ">::default(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() <= <" $type ">::default(), true);"]
+            #[doc = "    assert_eq!(<" $type ">::default() >  <" $type ">::default(), false);"]
+            #[doc = "    assert_eq!(<" $type ">::default() >= <" $type ">::default(), true);"]
+            #[doc = "}"]
+            #[doc = "```"]
+            impl std::cmp::PartialOrd<$type> for $type_prim {
+                fn partial_cmp(&self, other: &$type) -> Option<std::cmp::Ordering> {
+                    let slf: $type = (*self).try_into().unwrap_or($type::na());
+                    slf.partial_cmp(other)
+                }
+            }
+        }
+    };
+}
+
 /// Generates an implementation of `std::iter::Sum` for a scalar type
 ///
 /// This macro requires the following argument:
@@ -859,7 +889,7 @@ pub(in crate::scalar) use gen_binop;
 pub(in crate::scalar) use gen_binopassign;
 pub(in crate::scalar) use gen_from_primitive;
 pub(in crate::scalar) use gen_from_scalar;
-pub(in crate::scalar) use gen_impl;
+pub(in crate::scalar) use gen_partial_ord;
 pub(in crate::scalar) use gen_sum_iter;
 pub(in crate::scalar) use gen_trait_impl;
 pub(in crate::scalar) use gen_unop;
diff --git a/extendr-api/src/scalar/mod.rs b/extendr-api/src/scalar/mod.rs
index 2d46f0cb2..e3ad9f6ee 100644
--- a/extendr-api/src/scalar/mod.rs
+++ b/extendr-api/src/scalar/mod.rs
@@ -17,3 +17,11 @@ mod rcplx_default;
 
 #[cfg(not(feature = "num-complex"))]
 pub use rcplx_default::{c64, Rcplx};
+
+pub trait Scalar<T>: crate::CanBeNA
+where
+    T: PartialEq + Copy,
+{
+    fn inner(&self) -> T;
+    fn new(val: T) -> Self;
+}
diff --git a/extendr-api/src/scalar/rbool.rs b/extendr-api/src/scalar/rbool.rs
index a96806bff..cc88aec9d 100644
--- a/extendr-api/src/scalar/rbool.rs
+++ b/extendr-api/src/scalar/rbool.rs
@@ -1,4 +1,5 @@
 use crate::scalar::macros::*;
+use crate::scalar::Scalar;
 use crate::*;
 use std::convert::TryFrom;
 
@@ -9,11 +10,20 @@ use std::convert::TryFrom;
 /// The value `i32::MIN` is used as `NA`.
 ///
 /// `Rbool` has the same footprint as an `i32` value allowing us to use it in zero copy slices.
+#[repr(transparent)]
 pub struct Rbool(i32);
 
-impl Rbool {
-    gen_impl!(Rbool, i32);
+impl Scalar<i32> for Rbool {
+    fn inner(&self) -> i32 {
+        self.0
+    }
+
+    fn new(val: i32) -> Self {
+        Rbool(val)
+    }
+}
 
+impl Rbool {
     /// Return a `true` `Rbool`.
     pub const fn true_value() -> Rbool {
         Rbool(1)
@@ -52,6 +62,7 @@ impl Rbool {
 
 gen_trait_impl!(Rbool, bool, |x: &Rbool| x.inner() == i32::MIN, i32::MIN);
 gen_from_primitive!(Rbool, i32);
+gen_partial_ord!(Rbool, bool);
 
 impl From<bool> for Rbool {
     fn from(v: bool) -> Self {
diff --git a/extendr-api/src/scalar/rcplx_default.rs b/extendr-api/src/scalar/rcplx_default.rs
index a7e4ac880..7b1b6ef91 100644
--- a/extendr-api/src/scalar/rcplx_default.rs
+++ b/extendr-api/src/scalar/rcplx_default.rs
@@ -1,8 +1,10 @@
 use crate::scalar::Rfloat;
+use crate::scalar::Scalar;
 use crate::*;
 
 #[allow(non_camel_case_types)]
 #[derive(Clone, Copy, Debug, Default, PartialEq)]
+#[repr(C)]
 pub struct c64 {
     re: f64,
     im: f64,
@@ -42,9 +44,19 @@ impl CanBeNA for c64 {
 ///
 /// Rcplx has the same footprint as R's complex value allowing us to use it in zero copy slices.
 #[derive(Clone, Copy, Default, PartialEq)]
-#[repr(C)]
+#[repr(transparent)]
 pub struct Rcplx(c64);
 
+impl Scalar<c64> for Rcplx {
+    fn inner(&self) -> c64 {
+        self.0
+    }
+
+    fn new(val: c64) -> Self {
+        Rcplx(val)
+    }
+}
+
 impl Rcplx {
     // gen_impl!(Rcplx, c64);
     pub fn new(re: f64, im: f64) -> Self {
diff --git a/extendr-api/src/scalar/rcplx_full.rs b/extendr-api/src/scalar/rcplx_full.rs
index 99d518e5c..a7f1d4fec 100644
--- a/extendr-api/src/scalar/rcplx_full.rs
+++ b/extendr-api/src/scalar/rcplx_full.rs
@@ -1,5 +1,5 @@
 use crate::scalar::macros::*;
-use crate::scalar::Rfloat;
+use crate::scalar::{Rfloat, Scalar};
 use crate::*;
 use std::convert::TryFrom;
 use std::ops::{Add, Div, Mul, Neg, Sub};
@@ -23,12 +23,20 @@ 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.
-#[repr(C)]
+#[repr(transparent)]
 pub struct Rcplx(c64);
 
-impl Rcplx {
-    gen_impl!(Rcplx, c64);
+impl Scalar<c64> for Rcplx {
+    fn inner(&self) -> c64 {
+        self.0
+    }
+
+    fn new(val: c64) -> Self {
+        Rcplx(val)
+    }
+}
 
+impl Rcplx {
     pub fn new(re: f64, im: f64) -> Self {
         Self(c64::new(re, im))
     }
diff --git a/extendr-api/src/scalar/rfloat.rs b/extendr-api/src/scalar/rfloat.rs
index 33fc398f4..dbc8f22f6 100644
--- a/extendr-api/src/scalar/rfloat.rs
+++ b/extendr-api/src/scalar/rfloat.rs
@@ -1,5 +1,7 @@
+use crate::prelude::{Rint, Scalar};
 use crate::scalar::macros::*;
 use crate::*;
+use std::cmp::Ordering::*;
 use std::convert::TryFrom;
 use std::ops::{Add, Div, Mul, Neg, Sub};
 use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
@@ -9,12 +11,20 @@ use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
 /// `Rfloat` has a special `NA` value, obtained from R headers via `R_NaReal`.
 ///
 /// `Rfloat` has the same footprint as an `f64` value allowing us to use it in zero copy slices.
-#[repr(C)]
-pub struct Rfloat(pub f64);
+#[repr(transparent)]
+pub struct Rfloat(f64);
 
-impl Rfloat {
-    gen_impl!(Rfloat, f64);
+impl Scalar<f64> for Rfloat {
+    fn inner(&self) -> f64 {
+        self.0
+    }
+
+    fn new(val: f64) -> Self {
+        Rfloat(val)
+    }
+}
 
+impl Rfloat {
     pub fn is_nan(&self) -> bool {
         self.0.is_nan()
     }
@@ -36,6 +46,42 @@ impl Rfloat {
     pub fn sqrt(&self) -> Rfloat {
         self.0.sqrt().into()
     }
+
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     assert!(Rfloat::na().min(Rfloat::default()).is_na());    
+    ///     assert!(Rfloat::default().min(Rfloat::na()).is_na());
+    ///     assert_eq!(Rfloat::default().min(Rfloat::default()), Rfloat::default());
+    ///     assert_eq!(Rfloat::from(1).min(Rfloat::from(2)), Rfloat::from(1));    
+    ///     assert_eq!(Rfloat::from(2).min(Rfloat::from(1)), Rfloat::from(1));    
+    /// }
+    /// ```
+    pub fn min(&self, other: Self) -> Self {
+        match self.partial_cmp(&other) {
+            Some(Less | Equal) => *self,
+            Some(Greater) => other,
+            _ => Self::na(),
+        }
+    }
+
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     assert!(Rfloat::na().max(Rfloat::default()).is_na());    
+    ///     assert!(Rfloat::default().max(Rfloat::na()).is_na());
+    ///     assert_eq!(Rfloat::default().max(Rfloat::default()), Rfloat::default());
+    ///     assert_eq!(Rfloat::from(1).max(Rfloat::from(2)), Rfloat::from(2));    
+    ///     assert_eq!(Rfloat::from(2).max(Rfloat::from(1)), Rfloat::from(2));    
+    /// }
+    /// ```
+    pub fn max(&self, other: Self) -> Self {
+        match self.partial_cmp(&other) {
+            Some(Less) => other,
+            Some(Greater | Equal) => *self,
+            _ => Self::na(),
+        }
+    }
 }
 
 // `NA_real_` is a `NaN` with specific bit representation.
@@ -44,6 +90,7 @@ gen_trait_impl!(Rfloat, f64, |x: &Rfloat| x.inner().is_na(), f64::na());
 gen_from_primitive!(Rfloat, f64);
 gen_from_scalar!(Rfloat, f64);
 gen_sum_iter!(Rfloat);
+gen_partial_ord!(Rfloat, f64);
 
 // Generate binary ops for +, -, * and /
 gen_binop!(
@@ -106,6 +153,22 @@ gen_binopassign!(
 // Generate unary ops for -, !
 gen_unop!(Rfloat, Neg, |lhs: f64| Some(-lhs), "Negate a Rfloat value.");
 
+impl From<i32> for Rfloat {
+    fn from(value: i32) -> Self {
+        Rfloat::from(value as f64)
+    }
+}
+
+impl From<Rint> for Rfloat {
+    fn from(value: Rint) -> Self {
+        if value.is_na() {
+            Rfloat::na()
+        } else {
+            Rfloat::from(value.inner())
+        }
+    }
+}
+
 impl TryFrom<&Robj> for Rfloat {
     type Error = Error;
 
diff --git a/extendr-api/src/scalar/rint.rs b/extendr-api/src/scalar/rint.rs
index af20e819a..5747d72b0 100644
--- a/extendr-api/src/scalar/rint.rs
+++ b/extendr-api/src/scalar/rint.rs
@@ -1,5 +1,7 @@
 use crate::scalar::macros::*;
+use crate::scalar::Scalar;
 use crate::*;
+use std::cmp::Ordering::*;
 use std::convert::TryFrom;
 use std::ops::{Add, Div, Mul, Neg, Not, Sub};
 use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
@@ -11,16 +13,62 @@ use std::ops::{AddAssign, DivAssign, MulAssign, SubAssign};
 /// The value `i32::MIN` is used as `"NA"`.
 ///
 /// `Rint` has the same footprint as an `i32` value allowing us to use it in zero copy slices.
-pub struct Rint(pub i32);
+#[repr(transparent)]
+pub struct Rint(i32);
+
+impl Scalar<i32> for Rint {
+    fn inner(&self) -> i32 {
+        self.0
+    }
+
+    fn new(val: i32) -> Self {
+        Rint(val)
+    }
+}
 
 impl Rint {
-    gen_impl!(Rint, i32);
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     assert!(Rint::na().min(Rint::default()).is_na());    
+    ///     assert!(Rint::default().min(Rint::na()).is_na());
+    ///     assert_eq!(Rint::default().min(Rint::default()), Rint::default());
+    ///     assert_eq!(Rint::from(1).min(Rint::from(2)), Rint::from(1));    
+    ///     assert_eq!(Rint::from(2).min(Rint::from(1)), Rint::from(1));    
+    /// }
+    /// ```
+    pub fn min(&self, other: Self) -> Self {
+        match self.partial_cmp(&other) {
+            Some(Less | Equal) => *self,
+            Some(Greater) => other,
+            _ => Self::na(),
+        }
+    }
+
+    /// ```
+    /// use extendr_api::prelude::*;
+    /// test! {
+    ///     assert!(Rint::na().max(Rint::default()).is_na());    
+    ///     assert!(Rint::default().max(Rint::na()).is_na());
+    ///     assert_eq!(Rint::default().max(Rint::default()), Rint::default());
+    ///     assert_eq!(Rint::from(1).max(Rint::from(2)), Rint::from(2));    
+    ///     assert_eq!(Rint::from(2).max(Rint::from(1)), Rint::from(2));    
+    /// }
+    /// ```
+    pub fn max(&self, other: Self) -> Self {
+        match self.partial_cmp(&other) {
+            Some(Less) => other,
+            Some(Greater | Equal) => *self,
+            _ => Self::na(),
+        }
+    }
 }
 
 gen_trait_impl!(Rint, i32, |x: &Rint| x.0 == i32::MIN, i32::MIN);
 gen_from_primitive!(Rint, i32);
 gen_from_scalar!(Rint, i32);
 gen_sum_iter!(Rint);
+gen_partial_ord!(Rint, i32);
 
 // Generate binary ops for `+`, `-`, `*` and `/`
 gen_binop!(
diff --git a/extendr-api/src/wrapper/altrep.rs b/extendr-api/src/wrapper/altrep.rs
index 923824619..cb3f204ff 100644
--- a/extendr-api/src/wrapper/altrep.rs
+++ b/extendr-api/src/wrapper/altrep.rs
@@ -1,4 +1,4 @@
-use prelude::{Rbool, Rcplx, Rfloat, Rint};
+use prelude::{Rbool, Rcplx, Rfloat, Rint, Scalar};
 
 use super::*;
 
diff --git a/extendr-api/src/wrapper/doubles.rs b/extendr-api/src/wrapper/doubles.rs
index 75a2f3b7e..7f9d08077 100644
--- a/extendr-api/src/wrapper/doubles.rs
+++ b/extendr-api/src/wrapper/doubles.rs
@@ -1,4 +1,4 @@
-use super::scalar::Rfloat;
+use super::scalar::{Rfloat, Scalar};
 use super::*;
 use std::iter::FromIterator;
 
diff --git a/extendr-api/src/wrapper/integers.rs b/extendr-api/src/wrapper/integers.rs
index 583373569..1a995b037 100644
--- a/extendr-api/src/wrapper/integers.rs
+++ b/extendr-api/src/wrapper/integers.rs
@@ -1,4 +1,4 @@
-use super::scalar::Rint;
+use super::scalar::{Rint, Scalar};
 use super::*;
 use std::iter::FromIterator;
 
diff --git a/extendr-api/src/wrapper/logicals.rs b/extendr-api/src/wrapper/logicals.rs
index 615f31847..b4c487be6 100644
--- a/extendr-api/src/wrapper/logicals.rs
+++ b/extendr-api/src/wrapper/logicals.rs
@@ -1,4 +1,4 @@
-use super::scalar::Rbool;
+use super::scalar::{Rbool, Scalar};
 use super::*;
 use std::iter::FromIterator;
 
diff --git a/extendr-api/src/wrapper/matrix.rs b/extendr-api/src/wrapper/matrix.rs
index 50894e73e..50401e4a1 100644
--- a/extendr-api/src/wrapper/matrix.rs
+++ b/extendr-api/src/wrapper/matrix.rs
@@ -2,6 +2,7 @@
 
 use super::*;
 use crate::robj::GetSexp;
+use crate::scalar::Scalar;
 use std::ops::{Index, IndexMut};
 
 /// Wrapper for creating and using matrices and arrays.


Test patch:
diff --git a/extendr-api/tests/altrep_tests.rs b/extendr-api/tests/altrep_tests.rs
index d35892d3d..b43c5d917 100644
--- a/extendr-api/tests/altrep_tests.rs
+++ b/extendr-api/tests/altrep_tests.rs
@@ -22,7 +22,7 @@ fn test_altinteger() {
                 if index == self.missing_index {
                     Rint::na()
                 } else {
-                    Rint(self.start + self.step * index as i32)
+                    Rint::new(self.start + self.step * index as i32)
                 }
             }
         }
@@ -76,7 +76,7 @@ fn test_altreal() {
                 if index == self.missing_index {
                     Rfloat::na()
                 } else {
-                    Rfloat(self.start + self.step * index as f64)
+                    Rfloat::new(self.start + self.step * index as f64)
                 }
             }
         }
diff --git a/extendr-api/tests/na_tests.rs b/extendr-api/tests/na_tests.rs
index 2a59b493a..1dad733e7 100644
--- a/extendr-api/tests/na_tests.rs
+++ b/extendr-api/tests/na_tests.rs
@@ -12,7 +12,7 @@ fn test_float_na_is_na() {
 fn test_float_from_bits_is_na() {
     test! {
         let na_bits = 0x7ff00000u64 << 32 | 1954;
-        let na_r = Rfloat(unsafe {std::mem::transmute(na_bits)});
+        let na_r = Rfloat::new(unsafe {std::mem::transmute(na_bits)});
         let na_f64 : f64 = unsafe {std::mem::transmute(na_bits)};
         assert!(na_r.is_na());
         assert!(na_f64.is_na());
@@ -22,13 +22,13 @@ fn test_float_from_bits_is_na() {
 #[test]
 fn test_float_not_na_is_not_na() {
     test! {
-        assert!(!Rfloat(42f64).is_na());
-        assert!(!Rfloat(f64::NAN).is_na());
-        assert!(!Rfloat(f64::INFINITY).is_na());
-        assert!(!Rfloat(f64::NEG_INFINITY).is_na());
-        assert!(!Rfloat(f64::MAX).is_na());
-        assert!(!Rfloat(f64::MIN).is_na());
-        assert!(!Rfloat(f64::MIN_POSITIVE).is_na());
+        assert!(!Rfloat::new(42f64).is_na());
+        assert!(!Rfloat::new(f64::NAN).is_na());
+        assert!(!Rfloat::new(f64::INFINITY).is_na());
+        assert!(!Rfloat::new(f64::NEG_INFINITY).is_na());
+        assert!(!Rfloat::new(f64::MAX).is_na());
+        assert!(!Rfloat::new(f64::MIN).is_na());
+        assert!(!Rfloat::new(f64::MIN_POSITIVE).is_na());
 
         assert!(!42f64.is_na());
         assert!(!f64::NAN.is_na());
diff --git a/extendr-api/tests/scalar_ordering_tests.rs b/extendr-api/tests/scalar_ordering_tests.rs
new file mode 100644
index 000000000..005149dc0
--- /dev/null
+++ b/extendr-api/tests/scalar_ordering_tests.rs
@@ -0,0 +1,209 @@
+use extendr_api::prelude::*;
+use rstest::rstest;
+
+// Tests without NA do not require `test!` macro
+
+#[rstest]
+#[case(Rfloat::from(2.0), Rfloat::from(1.0))]
+#[case(Rint::from(2), Rint::from(1))]
+#[case(Rbool::from(true), Rbool::from(false))]
+fn left_gt_right<T, U>(#[case] left: T, #[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    assert!(left > right);
+}
+
+#[rstest]
+#[case(Rfloat::from(2.0), Rfloat::from(1.0))]
+#[case(Rfloat::from(2.0), Rfloat::from(2.0))]
+#[case(Rint::from(2), Rint::from(1))]
+#[case(Rint::from(2), Rint::from(2))]
+#[case(Rbool::from(true), Rbool::from(true))]
+#[case(Rbool::from(false), Rbool::from(false))]
+fn left_gte_right<T, U>(#[case] left: T, #[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    assert!(left >= right);
+}
+
+#[rstest]
+#[case(Rfloat::from(1.0), Rfloat::from(2.0))]
+#[case(Rint::from(1), Rint::from(2))]
+#[case(Rbool::from(false), Rbool::from(true))]
+fn left_lt_right<T, U>(#[case] left: T, #[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    assert!(left < right);
+}
+
+#[rstest]
+#[case(Rfloat::from(1.0), Rfloat::from(2.0))]
+#[case(Rfloat::from(2.0), Rfloat::from(2.0))]
+#[case(Rint::from(1), Rint::from(2))]
+#[case(Rint::from(2), Rint::from(2))]
+#[case(Rbool::from(true), Rbool::from(true))]
+#[case(Rbool::from(false), Rbool::from(false))]
+fn left_lte_right<T, U>(#[case] left: T, #[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    assert!(left <= right);
+}
+
+#[rstest]
+#[case(Rfloat::from(2.0), Rfloat::from(2.0))]
+#[case(Rint::from(2), Rint::from(2))]
+#[case(Rbool::from(true), Rbool::from(true))]
+#[case(Rbool::from(false), Rbool::from(false))]
+fn left_eq_right<T, U>(#[case] left: T, #[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    assert!(left == right);
+    assert!(right == left);
+}
+
+#[rstest]
+#[case(Rfloat::from(1.0), Rfloat::from(2.0))]
+#[case(Rint::from(1), Rint::from(2))]
+#[case(Rbool::from(true), Rbool::from(false))]
+fn left_neq_right<T, U>(#[case] left: T, #[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    assert!(left != right);
+    assert!(right != left);
+}
+
+// `NA` should be created in `test!` macro block
+
+#[rstest]
+#[case(Rfloat::from(1.0))]
+#[case(Rint::from(1))]
+#[case(Rbool::from(true))]
+fn left_gt_or_gte_right_na<T, U>(#[case] left: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    test! {
+        let right = T::na();
+        assert_eq!(left > right, false);
+        assert_eq!(left >= right, false);
+    }
+}
+
+#[rstest]
+#[case(Rfloat::from(1.0))]
+#[case(Rint::from(1))]
+#[case(Rbool::from(true))]
+fn left_lt_or_lte_right_na<T, U>(#[case] left: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    test! {
+        let right = T::na();
+        assert_eq!(left < right, false);
+        assert_eq!(left <= right, false);
+    }
+}
+
+#[rstest]
+#[case(Rfloat::from(1.0))]
+#[case(Rint::from(1))]
+#[case(Rbool::from(true))]
+fn left_na_lt_or_lte_right<T, U>(#[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    test! {
+        let left = T::na();
+        assert_eq!(left < right, false);
+        assert_eq!(left <= right, false);
+    }
+}
+
+#[rstest]
+#[case(Rfloat::from(1.0))]
+#[case(Rint::from(1))]
+#[case(Rbool::from(true))]
+fn left_na_gt_or_gte_right<T, U>(#[case] right: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    test! {
+        let left = T::na();
+        assert_eq!(left > right, false);
+        assert_eq!(left >= right, false);
+    }
+}
+
+#[rstest]
+#[case(Rfloat::from(1.0))]
+#[case(Rint::from(1))]
+#[case(Rbool::from(true))]
+#[case(Rbool::from(false))]
+fn na_vs_value<T, U>(#[case] value: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy,
+    U: PartialEq + Copy,
+{
+    test! {
+        let na = T::na();
+        assert_eq!(value.partial_cmp(&na), None);
+        assert_eq!(na.partial_cmp(&value), None);
+        assert_eq!(na.partial_cmp(&na), None);
+    }
+}
+
+#[test]
+fn collection_sort_rint() {
+    let mut raw = vec![45, 192, 87, 23, 255];
+    let mut rints: Vec<Rint> = raw.iter().map(|&x| Rint::from(x)).collect();
+    raw.sort();
+    rints.sort_by(|a, b| a.partial_cmp(b).unwrap());
+    assert!(raw.eq(&rints));
+}
+
+#[test]
+fn collection_sort_rfloat() {
+    let mut raw = vec![45.0, 192.0, 87.0, 23.0, 255.0];
+    let mut rfloats: Vec<Rfloat> = raw.iter().map(|&x| Rfloat::from(x)).collect();
+    raw.sort_by(|a, b| a.partial_cmp(b).unwrap());
+    rfloats.sort_by(|a, b| a.partial_cmp(b).unwrap());
+    assert!(raw.eq(&rfloats));
+}
+
+#[rstest]
+#[case(vec![45, 192, 87, 23, 255], vec![23, 45, 87, 192, 255], Rint::default())]
+#[case(vec![45.0, 192.0, 87.0, 23.0, 255.0], vec![23.0, 45.0, 87.0, 192.0, 255.0], Rfloat::default())]
+fn collection_sort<T, U>(#[case] raw: Vec<U>, #[case] ordered: Vec<U>, #[case] _marker: T)
+where
+    T: Scalar<U> + PartialOrd + PartialEq + Copy + From<U>,
+    U: PartialEq + Copy + PartialEq<T>,
+{
+    let mut scalars: Vec<T> = raw.iter().map(|&x| x.into()).collect();
+    scalars.sort_by(|a, b| a.partial_cmp(b).unwrap());
+    assert!(ordered.eq(&scalars));
+}
+
+#[test]
+fn collection_sort_bool() {
+    let raw = vec![true, false, true, false, true];
+    let ordered = vec![false, false, true, true, true];
+    let mut scalars: Vec<Rbool> = raw.iter().map(|&x| x.into()).collect();
+    scalars.sort_by(|a, b| a.partial_cmp(b).unwrap());
+    assert!(ordered.eq(&scalars));
+}