From 2b0290c7d492d81563c55aea7c6b79e7d1070990 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Mon, 15 Dec 2025 16:00:59 -0800 Subject: [PATCH] Add `PrimitiveNumber` methods for `as` type-casting ```rust trait PrimitiveNumber { // ... /// Creates a number using a type cast, `value as Self`. fn as_from(value: T) -> Self where Self: PrimitiveNumberAs; /// Returns this number converted with a type cast, `self as T`. fn as_to(self) -> T where Self: PrimitiveNumberAs; } ``` The helper `PrimitiveNumberAs` is also added as a supertrait for all concrete primitive numbers, so the methods can be used without any additional bounds if you're converting to/from specific types. --- src/lib.rs | 2 +- src/number.rs | 130 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d20c487..b47eafc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -72,6 +72,6 @@ mod tests; pub use self::error::PrimitiveError; pub use self::float::{PrimitiveFloat, PrimitiveFloatRef, PrimitiveFloatToInt}; pub use self::integer::{PrimitiveInteger, PrimitiveIntegerRef}; -pub use self::number::{PrimitiveNumber, PrimitiveNumberRef}; +pub use self::number::{PrimitiveNumber, PrimitiveNumberAs, PrimitiveNumberRef}; pub use self::signed::{PrimitiveSigned, PrimitiveSignedRef}; pub use self::unsigned::{PrimitiveUnsigned, PrimitiveUnsignedRef}; diff --git a/src/number.rs b/src/number.rs index 18c1d58..bbb88a5 100644 --- a/src/number.rs +++ b/src/number.rs @@ -1,6 +1,7 @@ use crate::PrimitiveError; trait Sealed {} +struct SealedToken; /// Trait for all primitive [numeric types]. /// @@ -45,6 +46,20 @@ trait Sealed {} pub trait PrimitiveNumber: 'static + Sealed + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + + PrimitiveNumberAs + core::cmp::PartialEq + core::cmp::PartialOrd + core::convert::From @@ -89,13 +104,13 @@ pub trait PrimitiveNumber: /// [`to_be_bytes`][Self::to_be_bytes]. It is effectively `[u8; size_of::()]`. type Bytes: core::borrow::Borrow<[u8]> + core::borrow::BorrowMut<[u8]>; - /// Creates a value from its representation as a byte array in big endian. + /// Creates a number from its representation as a byte array in big endian. fn from_be_bytes(bytes: Self::Bytes) -> Self; - /// Creates a value from its representation as a byte array in little endian. + /// Creates a number from its representation as a byte array in little endian. fn from_le_bytes(bytes: Self::Bytes) -> Self; - /// Creates a value from its representation as a byte array in native endian. + /// Creates a number from its representation as a byte array in native endian. fn from_ne_bytes(bytes: Self::Bytes) -> Self; /// Returns the memory representation of this number as a byte array in little-endian order. @@ -106,6 +121,28 @@ pub trait PrimitiveNumber: /// Returns the memory representation of this number as a byte array in native-endian order. fn to_ne_bytes(self) -> Self::Bytes; + + /// Creates a number using a type cast, `value as Self`. + /// + /// Note: unlike other `num-primitive` methods, there is no inherent method by this name on the + /// actual types. + fn as_from(value: T) -> Self + where + Self: PrimitiveNumberAs, + { + >::__as_from(value, SealedToken) + } + + /// Converts this number with a type cast, `self as T`. + /// + /// Note: unlike other `num-primitive` methods, there is no inherent method by this name on the + /// actual types. + fn as_to(self) -> T + where + Self: PrimitiveNumberAs, + { + >::__as_to(self, SealedToken) + } } /// Trait for references to primitive numbers ([`PrimitiveNumber`]). @@ -142,8 +179,71 @@ pub trait PrimitiveNumberRef: { } +/// Trait for numeric conversions supported by the [`as`] keyword. +/// +/// This is effectively the same as the [type cast] expression `self as N`, implemented for all +/// combinations of [`PrimitiveNumber`]. +/// +/// [`as`]: https://doc.rust-lang.org/std/keyword.as.html +/// [type cast]: https://doc.rust-lang.org/reference/expressions/operator-expr.html#type-cast-expressions +/// +/// # Examples +/// +/// `PrimitiveNumberAs<{number}>` is a supertrait of [`PrimitiveNumber`] for all primitive floats +/// and integers, so you do not need to use this trait directly when converting concrete types. +/// +/// ``` +/// use num_primitive::PrimitiveNumber; +/// +/// // Clamp any number to the interval 0..=100, unless it is NaN. +/// fn clamp_percentage(x: Number) -> Number { +/// let clamped = x.as_to::().clamp(0.0, 100.0); +/// Number::as_from(clamped) +/// } +/// +/// assert_eq!(clamp_percentage(-42_i8), 0_i8); +/// assert_eq!(clamp_percentage(42_u128), 42_u128); +/// assert_eq!(clamp_percentage(1e100_f64), 100_f64); +/// assert!(clamp_percentage(f32::NAN).is_nan()); +/// ``` +/// +/// However, if the other type is also generic, an explicit type constraint is needed. +/// +/// ``` +/// use num_primitive::{PrimitiveNumber, PrimitiveNumberAs}; +/// +/// fn clamp_any(x: Number, min: Limit, max: Limit) -> Number +/// where +/// Number: PrimitiveNumber + PrimitiveNumberAs, +/// Limit: PartialOrd, +/// { +/// assert!(min <= max); +/// let y = x.as_to::(); +/// if y <= min { +/// Number::as_from(min) +/// } else if y >= max { +/// Number::as_from(max) +/// } else { +/// x +/// } +/// } +/// +/// assert_eq!(clamp_any(1.23, 0_i8, 10_i8), 1.23); +/// assert_eq!(clamp_any(1.23, -1_i8, 1_i8), 1.0); +/// assert_eq!(clamp_any(i128::MAX, 0.0, 1e100), i128::MAX); +/// ``` +pub trait PrimitiveNumberAs { + #[doc(hidden)] + #[expect(private_interfaces)] + fn __as_from(x: T, _: SealedToken) -> Self; + + #[doc(hidden)] + #[expect(private_interfaces)] + fn __as_to(x: Self, _: SealedToken) -> T; +} + macro_rules! impl_primitive { - ($($Number:ident),*) => {$( + ($($Number:ident),+) => {$( impl Sealed for $Number {} impl Sealed for &$Number {} @@ -163,7 +263,27 @@ macro_rules! impl_primitive { } impl PrimitiveNumberRef<$Number> for &$Number {} - )*} + + impl_primitive!($Number as f32, f64); + impl_primitive!($Number as i8, i16, i32, i64, i128, isize); + impl_primitive!($Number as u8, u16, u32, u64, u128, usize); + )+}; + + ($Number:ident as $($Other:ident),+) => {$( + impl PrimitiveNumberAs<$Other> for $Number { + #[inline] + #[expect(private_interfaces)] + fn __as_from(x: $Other, _: SealedToken) -> Self { + x as Self + } + + #[inline] + #[expect(private_interfaces)] + fn __as_to(x: Self, _: SealedToken) -> $Other { + x as $Other + } + } + )+} } impl_primitive!(f32, f64);