diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c56a28b..4b03d81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: "1.32.0" + toolchain: "1.53.0" override: true - name: Test run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index c7221aa..7c31ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +## [0.5.0] — unreleased + +- Add `ConvApprox` and `CastApprox` +- Support `Conv` and `ConvFloat` for arrays and tuples +- Remove `impl Conv for T` + ## [0.4.4] — 2021-04-12 - Fix negative int to float digits check (#18) diff --git a/Cargo.toml b/Cargo.toml index babeed7..95a14ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easy-cast" -version = "0.4.4" +version = "0.5.0" authors = ["Diggory Hardy "] edition = "2018" license = "Apache-2.0" diff --git a/README.md b/README.md index 7faceab..b2ae9a6 100644 --- a/README.md +++ b/README.md @@ -6,38 +6,64 @@ Easy-cast Type conversion, success expected -This library is written to make numeric type conversions easy. Such -conversions usually fall into one of the following cases: - -- the conversion must preserve values exactly (use [`From`] or [`Into`] - or [`Conv`] or [`Cast`]) -- the conversion is expected to preserve values exactly, though this is - not ensured by the types in question (use [`Conv`] or [`Cast`]) -- the conversion could fail and must be checked at run-time (use - [`TryFrom`] or [`TryInto`] or [`Conv::try_conv`] or [`Cast::try_cast`]) -- the conversion is from floating point values to integers and should - round to the "nearest" integer (use [`ConvFloat`] or [`CastFloat`]) -- the conversion is from `f32` to `f64` or vice-versa; in this case use of - `as f32` / `as f64` is likely acceptable since `f32` has special - representations for non-finite values and conversion to `f64` is exact -- truncating conversion (modular arithmetic) is desired; in this case `as` - probably does exactly what you want -- saturating conversion is desired (less common; not supported here) +This library exists to make fallible numeric type conversions easy, without +resorting to the `as` keyword. + +- [`Conv`] is like [`From`], but supports fallible conversions +- [`Cast`] is to [`Conv`] what [`Into`] is to [`From`] +- [`ConvApprox`] and [`CastApprox`] support fallible, approximate conversion +- [`ConvFloat`] and [`CastFloat`] are similar, providing precise control over rounding If you are wondering "why not just use `as`", there are a few reasons: -- integer conversions may silently truncate -- integer conversions to/from signed types silently reinterpret +- integer conversions may silently truncate or sign-extend which does not + preserve value - prior to Rust 1.45.0 float-to-int conversions were not fully defined; since this version they use saturating conversion (NaN converts to 0) - you want some assurance (at least in debug builds) that the conversion - will preserve values correctly without having to proof-read code + will preserve values correctly + +Why might you *not* want to use this library? + +- You want saturating / truncating / other non-value-preserving conversion +- You want to convert non-numeric types ([`From`] supports a lot more + conversions than [`Conv`] does)! +- You want a thoroughly tested library (we're not quite there yet) + +### Error handling + +All traits support two methods: + +- `try_*` methods return a `Result` and always fail if the correct + conversion is not possible +- other methods may panic or return incorrect results + +In debug builds, methods not returning `Result` must panic on failure. As +with the overflow checks on Rust's standard integer arithmetic, this is +considered a tool for finding logic errors. In release builds, these methods +are permitted to return defined but incorrect results similar to the `as` +keyword. + +If the `always_assert` feature flag is set, assertions will be turned on in +all builds. Some additional feature flags are available for finer-grained +control (see `Cargo.toml`). -When should you *not* use this library? +### Performance -- Only numeric conversions are supported -- Conversions from floats do not provide fine control of rounding modes -- This library has not been thoroughly tested correctness +Performance is "good enough that it hasn't been a concern". + +In debug builds and when `always_assert` is enabled, the priority is testing +but overhead should be small. + +In release builds without `always_assert`, `conv*` methods should reduce to +`x as T` (with necessary additions for rounding). + +### no_std support + +When the crate's default features are disabled (and `std` is not enabled) +then the library supports `no_std`. In this case, [`ConvFloat`] and +[`CastFloat`] are only available if the `libm` optional dependency is +enabled. [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html [`Into`]: https://doc.rust-lang.org/stable/std/convert/trait.Into.html @@ -50,19 +76,9 @@ When should you *not* use this library? [`ConvFloat`]: https://docs.rs/easy-cast/latest/easy_cast/trait.ConvFloat.html [`CastFloat`]: https://docs.rs/easy-cast/latest/easy_cast/trait.CastFloat.html -## Assertions - -All type conversions which are potentially fallible assert on failure in -debug builds. In release builds assertions may be omitted, thus making -incorrect conversions possible. - -If the `always_assert` feature flag is set, assertions will be turned on in -all builds. Some additional feature flags are available for finer-grained -control (see [Cargo.toml](Cargo.toml)). - ## MSRV and no_std -The Minumum Supported Rust Version is 1.32.0 (first release of Edition 2018). +The Minumum Supported Rust Version is 1.53.0 (`IntoIterator for [T; N]`). By default, `std` support is required. With default features disabled `no_std` is supported, but the `ConvFloat` and `CastFloat` traits are unavailable. diff --git a/src/impl_basic.rs b/src/impl_basic.rs index 3978cc5..c952c41 100644 --- a/src/impl_basic.rs +++ b/src/impl_basic.rs @@ -7,17 +7,6 @@ use super::*; -impl Conv for T { - #[inline] - fn conv(v: T) -> Self { - v - } - #[inline] - fn try_conv(v: T) -> Result { - Ok(v) - } -} - macro_rules! impl_via_from { ($x:ty: $y:ty) => { impl Conv<$x> for $y { @@ -47,3 +36,245 @@ impl_via_from!(u8: u16, u32, u64, u128); impl_via_from!(u16: f32, f64, i32, i64, i128, u32, u64, u128); impl_via_from!(u32: f64, i64, i128, u64, u128); impl_via_from!(u64: i128, u128); + +// TODO(unsize): remove T: Copy + Default bound +// TODO(specialization): implement ConvApprox for arrays and tuples +impl + Copy + Default, const N: usize> Conv<[S; N]> for [T; N] { + #[inline] + fn try_conv(ss: [S; N]) -> Result { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::try_conv(s)?; + } + Ok(tt) + } + #[inline] + fn conv(ss: [S; N]) -> Self { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::conv(s); + } + tt + } +} + +#[cfg(any(feature = "std", feature = "libm"))] +impl + Copy + Default, const N: usize> ConvFloat<[S; N]> for [T; N] { + #[inline] + fn try_conv_trunc(ss: [S; N]) -> Result { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::try_conv_trunc(s)?; + } + Ok(tt) + } + #[inline] + fn try_conv_nearest(ss: [S; N]) -> Result { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::try_conv_nearest(s)?; + } + Ok(tt) + } + #[inline] + fn try_conv_floor(ss: [S; N]) -> Result { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::try_conv_floor(s)?; + } + Ok(tt) + } + #[inline] + fn try_conv_ceil(ss: [S; N]) -> Result { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::try_conv_ceil(s)?; + } + Ok(tt) + } + + #[inline] + fn conv_trunc(ss: [S; N]) -> Self { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::conv_trunc(s); + } + tt + } + #[inline] + fn conv_nearest(ss: [S; N]) -> Self { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::conv_nearest(s); + } + tt + } + #[inline] + fn conv_floor(ss: [S; N]) -> Self { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::conv_floor(s); + } + tt + } + #[inline] + fn conv_ceil(ss: [S; N]) -> Self { + let mut tt = [T::default(); N]; + for (s, t) in IntoIterator::into_iter(ss).zip(tt.iter_mut()) { + *t = T::conv_ceil(s); + } + tt + } +} + +impl Conv<()> for () { + #[inline] + fn try_conv(_: ()) -> Result { + Ok(()) + } + #[inline] + fn conv(_: ()) -> Self { + () + } +} +impl> Conv<(S0,)> for (T0,) { + #[inline] + fn try_conv(ss: (S0,)) -> Result { + Ok((ss.0.try_cast()?,)) + } + #[inline] + fn conv(ss: (S0,)) -> Self { + (ss.0.cast(),) + } +} +impl, T1: Conv> Conv<(S0, S1)> for (T0, T1) { + #[inline] + fn try_conv(ss: (S0, S1)) -> Result { + Ok((ss.0.try_cast()?, ss.1.try_cast()?)) + } + #[inline] + fn conv(ss: (S0, S1)) -> Self { + (ss.0.cast(), ss.1.cast()) + } +} +impl, T1: Conv, T2: Conv> Conv<(S0, S1, S2)> for (T0, T1, T2) { + #[inline] + fn try_conv(ss: (S0, S1, S2)) -> Result { + Ok((ss.0.try_cast()?, ss.1.try_cast()?, ss.2.try_cast()?)) + } + #[inline] + fn conv(ss: (S0, S1, S2)) -> Self { + (ss.0.cast(), ss.1.cast(), ss.2.cast()) + } +} +impl, T1: Conv, T2: Conv, T3: Conv> Conv<(S0, S1, S2, S3)> + for (T0, T1, T2, T3) +{ + #[inline] + fn try_conv(ss: (S0, S1, S2, S3)) -> Result { + Ok(( + ss.0.try_cast()?, + ss.1.try_cast()?, + ss.2.try_cast()?, + ss.3.try_cast()?, + )) + } + #[inline] + fn conv(ss: (S0, S1, S2, S3)) -> Self { + (ss.0.cast(), ss.1.cast(), ss.2.cast(), ss.3.cast()) + } +} +impl, T1: Conv, T2: Conv, T3: Conv, T4: Conv> + Conv<(S0, S1, S2, S3, S4)> for (T0, T1, T2, T3, T4) +{ + #[inline] + fn try_conv(ss: (S0, S1, S2, S3, S4)) -> Result { + Ok(( + ss.0.try_cast()?, + ss.1.try_cast()?, + ss.2.try_cast()?, + ss.3.try_cast()?, + ss.4.try_cast()?, + )) + } + #[inline] + fn conv(ss: (S0, S1, S2, S3, S4)) -> Self { + ( + ss.0.cast(), + ss.1.cast(), + ss.2.cast(), + ss.3.cast(), + ss.4.cast(), + ) + } +} +impl Conv<(S0, S1, S2, S3, S4, S5)> + for (T0, T1, T2, T3, T4, T5) +where + T0: Conv, + T1: Conv, + T2: Conv, + T3: Conv, + T4: Conv, + T5: Conv, +{ + #[inline] + fn try_conv(ss: (S0, S1, S2, S3, S4, S5)) -> Result { + Ok(( + ss.0.try_cast()?, + ss.1.try_cast()?, + ss.2.try_cast()?, + ss.3.try_cast()?, + ss.4.try_cast()?, + ss.5.try_cast()?, + )) + } + #[inline] + fn conv(ss: (S0, S1, S2, S3, S4, S5)) -> Self { + ( + ss.0.cast(), + ss.1.cast(), + ss.2.cast(), + ss.3.cast(), + ss.4.cast(), + ss.5.cast(), + ) + } +} + +#[cfg(any(feature = "std", feature = "libm"))] +impl, T1: ConvFloat> ConvFloat<(S0, S1)> for (T0, T1) { + #[inline] + fn try_conv_trunc(ss: (S0, S1)) -> Result { + Ok((T0::try_conv_trunc(ss.0)?, T1::try_conv_trunc(ss.1)?)) + } + #[inline] + fn try_conv_nearest(ss: (S0, S1)) -> Result { + Ok((T0::try_conv_nearest(ss.0)?, T1::try_conv_nearest(ss.1)?)) + } + #[inline] + fn try_conv_floor(ss: (S0, S1)) -> Result { + Ok((T0::try_conv_floor(ss.0)?, T1::try_conv_floor(ss.1)?)) + } + #[inline] + fn try_conv_ceil(ss: (S0, S1)) -> Result { + Ok((T0::try_conv_ceil(ss.0)?, T1::try_conv_ceil(ss.1)?)) + } + + #[inline] + fn conv_trunc(ss: (S0, S1)) -> Self { + (T0::conv_trunc(ss.0), T1::conv_trunc(ss.1)) + } + #[inline] + fn conv_nearest(ss: (S0, S1)) -> Self { + (T0::conv_nearest(ss.0), T1::conv_nearest(ss.1)) + } + #[inline] + fn conv_floor(ss: (S0, S1)) -> Self { + (T0::conv_floor(ss.0), T1::conv_floor(ss.1)) + } + #[inline] + fn conv_ceil(ss: (S0, S1)) -> Self { + (T0::conv_ceil(ss.0), T1::conv_ceil(ss.1)) + } +} diff --git a/src/impl_float.rs b/src/impl_float.rs index a697f09..c344e04 100644 --- a/src/impl_float.rs +++ b/src/impl_float.rs @@ -7,6 +7,47 @@ use super::*; +impl ConvApprox for f32 { + fn try_conv_approx(x: f64) -> Result { + use core::num::FpCategory; + + let sign_bits = (x.to_bits() >> 32) as u32 & 0x8000_0000; + let with_sign = |x: f32| -> f32 { + // assumption: x is not negative + f32::from_bits(sign_bits | x.to_bits()) + }; + + match x.classify() { + FpCategory::Nan => Err(Error::Range), + FpCategory::Infinite => Ok(with_sign(f32::INFINITY)), + FpCategory::Zero | FpCategory::Subnormal => Ok(with_sign(0f32)), + FpCategory::Normal => { + // f32 exponent range: -126 to 127 + // f64, f32 bias: 1023, 127 represents 0 + let exp = (x.to_bits() & 0x7FF0_0000_0000_0000) >> 52; + if exp >= 1023 - 126 && exp <= 1023 + 127 { + let exp = ((exp + 127) - 1023) as u32; + let frac = ((x.to_bits() & 0x000F_FFFF_FFFF_FFFF) >> (52 - 23)) as u32; + let bits = sign_bits | (exp << 23) | frac; + Ok(f32::from_bits(bits)) + } else { + Err(Error::Range) + } + } + } + } + + fn conv_approx(x: f64) -> f32 { + if cfg!(any(debug_assertions, feature = "assert_float")) { + Self::try_conv_approx(x).unwrap_or_else(|_| { + panic!("cast x: f64 to f32 (approx): range error for x = {}", x) + }) + } else { + x as f32 + } + } +} + #[cfg(all(not(feature = "std"), feature = "libm"))] trait FloatRound { fn round(self) -> Self; @@ -144,6 +185,17 @@ macro_rules! impl_float { } } } + + impl ConvApprox<$x> for $y { + #[inline] + fn try_conv_approx(x: $x) -> Result { + ConvFloat::<$x>::try_conv_trunc(x) + } + #[inline] + fn conv_approx(x: $x) -> Self { + ConvFloat::<$x>::conv_trunc(x) + } + } }; ($x:ty: $y:tt, $($yy:tt),+) => { impl_float!($x: $y); @@ -232,3 +284,15 @@ impl ConvFloat for u128 { } } } + +#[cfg(any(feature = "std", feature = "libm"))] +impl ConvApprox for u128 { + #[inline] + fn try_conv_approx(x: f32) -> Result { + ConvFloat::::try_conv_trunc(x) + } + #[inline] + fn conv_approx(x: f32) -> Self { + ConvFloat::::conv_trunc(x) + } +} diff --git a/src/lib.rs b/src/lib.rs index 91d913c..602c4a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,44 +5,43 @@ //! Type conversion, success expected //! -//! This library is written to make numeric type conversions easy. Such -//! conversions usually fall into one of the following cases: +//! This library exists to make fallible numeric type conversions easy, without +//! resorting to the `as` keyword. //! -//! - the conversion must preserve values exactly (use [`From`] or [`Into`] -//! or [`Conv`] or [`Cast`]) -//! - the conversion is expected to preserve values exactly, though this is -//! not ensured by the types in question (use [`Conv`] or [`Cast`]) -//! - the conversion could fail and must be checked at run-time (use -//! [`TryFrom`] or [`TryInto`] or [`Conv::try_conv`] or [`Cast::try_cast`]) -//! - the conversion is from floating point values to integers and should -//! round to the "nearest" integer (use [`ConvFloat`] or [`CastFloat`]) -//! - the conversion is from `f32` to `f64` or vice-versa; in this case use of -//! `as f32` / `as f64` is likely acceptable since `f32` has special -//! representations for non-finite values and conversion to `f64` is exact -//! - truncating conversion (modular arithmetic) is desired; in this case `as` -//! probably does exactly what you want -//! - saturating conversion is desired (less common; not supported here) +//! - [`Conv`] is like [`From`], but supports fallible conversions +//! - [`Cast`] is to [`Conv`] what [`Into`] is to [`From`] +//! - [`ConvApprox`] and [`CastApprox`] support fallible, approximate conversion +//! - [`ConvFloat`] and [`CastFloat`] are similar, providing precise control over rounding //! //! If you are wondering "why not just use `as`", there are a few reasons: //! -//! - integer conversions may silently truncate -//! - integer conversions to/from signed types silently reinterpret +//! - integer conversions may silently truncate or sign-extend which does not +//! preserve value //! - prior to Rust 1.45.0 float-to-int conversions were not fully defined; //! since this version they use saturating conversion (NaN converts to 0) //! - you want some assurance (at least in debug builds) that the conversion -//! will preserve values correctly without having to proof-read code +//! will preserve values correctly //! -//! When should you *not* use this library? +//! Why might you *not* want to use this library? //! -//! - Only numeric conversions are supported -//! - Conversions from floats do not provide fine control of rounding modes -//! - This library has not been thoroughly tested correctness +//! - You want saturating / truncating / other non-value-preserving conversion +//! - You want to convert non-numeric types ([`From`] supports a lot more +//! conversions than [`Conv`] does)! +//! - You want a thoroughly tested library (we're not quite there yet) //! -//! ## Assertions +//! ## Error handling //! -//! All type conversions which are potentially fallible assert on failure in -//! debug builds. In release builds assertions may be omitted, thus making -//! incorrect conversions possible. +//! All traits support two methods: +//! +//! - `try_*` methods return a `Result` and always fail if the correct +//! conversion is not possible +//! - other methods may panic or return incorrect results +//! +//! In debug builds, methods not returning `Result` must panic on failure. As +//! with the overflow checks on Rust's standard integer arithmetic, this is +//! considered a tool for finding logic errors. In release builds, these methods +//! are permitted to return defined but incorrect results similar to the `as` +//! keyword. //! //! If the `always_assert` feature flag is set, assertions will be turned on in //! all builds. Some additional feature flags are available for finer-grained @@ -77,88 +76,219 @@ pub enum Error { Inexact, } -#[cfg(feature = "std")] -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "cast conversion: {}", - match self { - Error::Range => "source value not in target range", - Error::Inexact => "loss of precision or range error", - } - ) +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Error::Range => write!(f, "source value not in target range"), + Error::Inexact => write!(f, "loss of precision or range error"), + } } } #[cfg(feature = "std")] impl std::error::Error for Error {} -/// Like [`From`], but supporting potentially-fallible conversions -/// -/// This trait is intended to replace *many* uses of the `as` keyword for -/// numeric conversions, though not all. -/// Conversions from floating-point types are excluded since it is very easy to -/// (accidentally) produce non-integer values; instead use [`ConvFloat`]. +/// Like [`From`], but supports fallible conversions /// -/// Two methods are provided: +/// This trait is intented to be an extension over [`From`], also supporting +/// fallible conversions of numeric types. +/// Since Rust does not yet have stable support for handling conflicting +/// implementations (specialization or otherwise), only conversions between +/// the most important numeric types are supported for now. /// -/// - [`Conv::conv`] is for "success expected" conversions. In debug builds -/// and when using the `always_assert` feature flag, inexact conversions -/// will panic. In other cases, conversions may produce incorrect values -/// (according to the behaviour of `as`). This is similar to the behviour of -/// Rust's overflow checks on integer arithmetic, and intended for usage -/// when the user is "reasonably sure" that conversion will succeed. -/// - [`Conv::try_conv`] is for fallible conversions, and always produces an -/// error if the conversion would be inexact. +/// The sister-trait [`Cast`] supports "into" style usage. pub trait Conv: Sized { - /// Convert from `T` to `Self` - fn conv(v: T) -> Self; - /// Try converting from `T` to `Self` + /// + /// This method must fail on inexact conversions. fn try_conv(v: T) -> Result; + + /// Convert from `T` to `Self` + /// + /// This method must return the same result as [`Self::try_conv`] where that + /// method succeeds, but differs in the handling of errors: + /// + /// - In debug builds the method panics on error + /// - Otherwise, the method may panic or may return a different value, + /// but like with the `as` keyword all results must be well-defined and + /// *safe*. + /// + /// Default implementations use [`Self::try_conv`] and panic on error. + /// Implementations provided by this library will panic in debug builds + /// or if the `always_assert` feature flag is used, and otherwise will + /// behave identically to the `as` keyword. + /// + /// This mirrors the behaviour of Rust's overflow checks on integer + /// arithmetic in that it is a tool for diagnosing logic errors where + /// success is expected. + fn conv(v: T) -> Self { + Self::try_conv(v).unwrap_or_else(|e| { + panic!("Conv::conv(_) failed: {}", e); + }) + } } -/// Nearest / floor / ceil conversions from floating point types +/// Like [`Into`], but for [`Conv`] /// -/// This trait is explicitly for conversions from floating-point values to -/// integers, supporting four rounding modes for fallible and for -/// "success expected" conversions. +/// This trait is automatically implemented for every implementation of +/// [`Conv`]. +pub trait Cast { + /// Try converting from `Self` to `T` + /// + /// Use this method to explicitly handle errors. + fn try_cast(self) -> Result; + + /// Cast from `Self` to `T` + /// + /// Use this method *only* where success is expected: implementations are + /// permitted to panic or silently return a different (safe, defined) value + /// on error. + /// + /// In debug builds, implementations must panic. + /// + /// Implementations by this library will panic in debug builds or if the + /// `always_assert` feature flag is used, otherwise conversions have the + /// same behaviour as the `as` keyword. + fn cast(self) -> T; +} + +impl> Cast for S { + #[inline] + fn cast(self) -> T { + T::conv(self) + } + #[inline] + fn try_cast(self) -> Result { + T::try_conv(self) + } +} + +/// Like [`From`], but for approximate numerical conversions /// -/// Two sets of methods are provided: +/// On success, the result must be approximately the same as the input value: +/// the difference must be smaller than the precision of the target type. +/// For example, one may have `i32::conv_approx(1.9f32) = 1` or +/// `f32::conv_approx(1f64 + (f32::EPSILON as f64) / 2.0) = 1.0`. /// -/// - `conv_*` methods are for "success expected" conversions. In debug builds -/// and when using the `always_assert` or the `assert_float` feature flag, -/// out-of-range conversions will panic. In other cases, conversions may -/// produce incorrect values (according to the behaviour of as, which is -/// saturating cast since Rust 1.45.0 and undefined for older compilers). -/// Non-finite source values (`inf` and `NaN`) are considered out-of-range. -/// - `try_conv_*` methods are for fallible conversions and always produce an -/// error if the conversion would be out of range. +/// Precise rounding mode should usually be truncation (round towards zero), +/// but this is not required. Use [`ConvFloat`] where a specific rounding mode +/// is required. /// -/// For `f64` to `f32` where loss-of-precision is allowable, it is probably -/// acceptable to use `as` (and if need be, check that the result is finite -/// with `x.is_finite()`). The reverse, `f32` to `f64`, is always exact. -#[cfg(any(feature = "std", feature = "libm"))] -#[cfg_attr(doc_cfg, doc(cfg(any(feature = "std", feature = "libm"))))] -pub trait ConvFloat: Sized { - /// Convert to integer with truncatation +/// The sister-trait [`CastApprox`] supports "into" style usage. +pub trait ConvApprox: Sized { + /// Try converting from `T` to `Self`, allowing approximation of value /// - /// Rounds towards zero (same as `as`). - fn conv_trunc(x: T) -> Self; - /// Convert to the nearest integer + /// This conversion may truncate excess precision not supported by the + /// target type, so long as the *value* is approximately equal, from the + /// point of view of precision of the target type. /// - /// Half-way cases are rounded away from `0`. - fn conv_nearest(x: T) -> Self; - /// Convert the floor to an integer + /// This method should allow approximate conversion, but fail on input not + /// (approximately) in the target's range. + fn try_conv_approx(x: T) -> Result; + + /// Converting from `T` to `Self`, allowing approximation of value /// - /// Returns the largest integer less than or equal to `x`. - fn conv_floor(x: T) -> Self; - /// Convert the ceiling to an integer + /// This method must return the same result as [`Self::try_conv_approx`] + /// where that method succeeds, but differs in the handling of errors: /// - /// Returns the smallest integer greater than or equal to `x`. - fn conv_ceil(x: T) -> Self; + /// - In debug builds the method panics on error + /// - Otherwise, the method may panic or may return a different value, + /// but like with the `as` keyword all results must be well-defined and + /// *safe*. + /// + /// Default implementations use [`Self::try_conv_approx`] and panic on error. + /// Implementations provided by this library will panic in debug builds + /// or if the `always_assert` feature flag is used, and otherwise will + /// behave identically to the `as` keyword. + /// + /// This mirrors the behaviour of Rust's overflow checks on integer + /// arithmetic in that it is a tool for diagnosing logic errors where + /// success is expected. + #[inline] + fn conv_approx(x: T) -> Self { + Self::try_conv_approx(x).unwrap_or_else(|e| { + panic!("ConvApprox::conv_approx(_) failed: {}", e); + }) + } +} + +// TODO(specialization): implement also where T: ConvFloat +impl> ConvApprox for T { + #[inline] + fn try_conv_approx(x: S) -> Result { + T::try_conv(x) + } + #[inline] + fn conv_approx(x: S) -> Self { + T::conv(x) + } +} +/// Like [`Into`], but for [`ConvApprox`] +/// +/// On success, the result must be approximately the same as the input value: +/// the difference must be smaller than the precision of the target type. +/// For example, one may have `1.9f32.cast_approx() = 1`. +/// +/// Precise rounding mode should usually be truncation (round towards zero), +/// but this is not required. Use [`CastFloat`] where a specific rounding mode +/// is required. +/// +/// This trait is automatically implemented for every implementation of +/// [`ConvApprox`]. +pub trait CastApprox { + /// Try approximate conversion from `Self` to `T` + /// + /// Use this method to explicitly handle errors. + fn try_cast_approx(self) -> Result; + + /// Cast approximately from `Self` to `T` + /// + /// Use this method *only* where success is expected: implementations are + /// permitted to panic or silently return a different (safe, defined) value + /// on error. + /// + /// In debug builds, implementations must panic. + /// + /// Implementations by this library will panic in debug builds or if the + /// `always_assert` feature flag is used, otherwise conversions have the + /// same behaviour as the `as` keyword. + fn cast_approx(self) -> T; +} + +impl> CastApprox for S { + #[inline] + fn try_cast_approx(self) -> Result { + T::try_conv_approx(self) + } + #[inline] + fn cast_approx(self) -> T { + T::conv_approx(self) + } +} + +/// Nearest / floor / ceiling conversions from floating point types +/// +/// This trait is explicitly for conversions from floating-point values to +/// integers, supporting four rounding modes. +/// +/// As with [`Conv`], the `try_conv_*` methods must be implemented and must fail +/// if conversion to the expected value is not possible. If the source is non- +/// finite (`inf` or `NaN`), then `Error::Range` should be returned. +/// +/// The `conv_*` methods each have a default implementation over the `try_..` +/// variant which panics on failure. Implementations handle errors as follows: +/// +/// - In debug builds, the methods must panic +/// - Otherwise, the method may panic or may return a different value; all +/// results must be well-defined and *safe*. +/// - Implementations provided by this library will also panic if the +/// `always_assert` or `assert_float` feature flag is used. +/// +/// The sister-trait [`CastFloat`] supports "into" style usage. +#[cfg(any(feature = "std", feature = "libm"))] +#[cfg_attr(doc_cfg, doc(cfg(any(feature = "std", feature = "libm"))))] +pub trait ConvFloat: Sized { /// Try converting to integer with truncation /// /// Rounds towards zero (same as `as`). @@ -175,51 +305,51 @@ pub trait ConvFloat: Sized { /// /// Returns the smallest integer greater than or equal to `x`. fn try_conv_ceil(x: T) -> Result; -} -/// Like [`Into`], but for [`Conv`] -/// -/// Two methods are provided: -/// -/// - [`Cast::cast`] is for "success expected" conversions. In debug builds -/// and when using the `always_assert` feature flag, inexact conversions -/// will panic. In other cases, conversions may produce incorrect values -/// (according to the behaviour of `as`). This is similar to the behviour of -/// Rust's overflow checks on integer arithmetic, and intended for usage -/// when the user is "reasonably sure" that conversion will succeed. -/// - [`Cast::try_cast`] is for fallible conversions, and always produces an -/// error if the conversion would be inexact. -pub trait Cast { - /// Cast from `Self` to `T` - fn cast(self) -> T; - - /// Try converting from `Self` to `T` - fn try_cast(self) -> Result; -} - -impl> Cast for S { - #[inline] - fn cast(self) -> T { - T::conv(self) + /// Convert to integer with truncatation + /// + /// Rounds towards zero (same as `as`). + fn conv_trunc(x: T) -> Self { + Self::try_conv_trunc(x).unwrap_or_else(|e| panic!("ConvFloat::conv_trunc(_) failed: {}", e)) } - #[inline] - fn try_cast(self) -> Result { - T::try_conv(self) + /// Convert to the nearest integer + /// + /// Half-way cases are rounded away from `0`. + fn conv_nearest(x: T) -> Self { + Self::try_conv_nearest(x) + .unwrap_or_else(|e| panic!("ConvFloat::conv_nearest(_) failed: {}", e)) + } + /// Convert the floor to an integer + /// + /// Returns the largest integer less than or equal to `x`. + fn conv_floor(x: T) -> Self { + Self::try_conv_floor(x).unwrap_or_else(|e| panic!("ConvFloat::conv_floor(_) failed: {}", e)) + } + /// Convert the ceiling to an integer + /// + /// Returns the smallest integer greater than or equal to `x`. + fn conv_ceil(x: T) -> Self { + Self::try_conv_ceil(x).unwrap_or_else(|e| panic!("ConvFloat::conv_ceil(_) failed: {}", e)) } } /// Like [`Into`], but for [`ConvFloat`] /// -/// Two sets of methods are provided: +/// Use: +/// +/// - `try_cast_*` methods to explicitly handle errors +/// - `cast_*` methods *only* where success is expected. Implementations are +/// permitted to panic or silently return a different (safe, defined) value +/// on error. +/// +/// In debug builds, implementations must panic. +/// +/// Implementations by this library will panic in debug builds or if the +/// `always_assert` or `assert_float` feature flag is used, otherwise +/// conversions have similar behaviour to the `as` keyword. /// -/// - `cast_*` methods are for "success expected" conversions. In debug builds -/// and when using the `always_assert` or the `assert_float` feature flag, -/// out-of-range conversions will panic. In other cases, conversions may -/// produce incorrect values (according to the behaviour of as, which is -/// saturating cast since Rust 1.45.0 and undefined for older compilers). -/// Non-finite source values (`inf` and `NaN`) are considered out-of-range. -/// - `try_cast_*` methods are for fallible conversions and always produce an -/// error if the conversion would be out of range. +/// This trait is automatically implemented for every implementation of +/// [`ConvFloat`]. #[cfg(any(feature = "std", feature = "libm"))] #[cfg_attr(doc_cfg, doc(cfg(any(feature = "std", feature = "libm"))))] pub trait CastFloat { diff --git a/tests/tests.rs b/tests/tests.rs index 81a3eb7..fdf8917 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -17,7 +17,7 @@ fn signed_to_unsigned() { } #[test] -#[should_panic] +#[should_panic(expected = "cast x: i32 to u32: expected x >= 0, found x = -1")] fn signed_to_unsigned_n1() { u32::conv(-1i32); } @@ -30,7 +30,7 @@ fn unsigned_to_signed() { } #[test] -#[should_panic] +#[should_panic(expected = "cast x: u32 to i32: expected x <= 2147483647, found x = 2147483648")] fn unsigned_to_signed_large() { i32::conv(0x8000_0000u32); } @@ -43,11 +43,58 @@ fn int_to_float() { } #[test] -#[should_panic] +#[should_panic(expected = "cast x: i32 to f32: inexact for x = 33554431")] fn int_to_float_inexact() { f32::conv(0x01FF_FFFF); } +#[test] +#[cfg(any(feature = "std", feature = "libm"))] +fn approx_float_to_int() { + assert_eq!(i32::conv_approx(1.99f32), 1); + assert_eq!(i32::conv_approx(-1.99f32), -1); + assert_eq!(i32::conv_approx(9.1f64), 9); + + const MAX: f64 = i32::MAX as f64; + assert_eq!(i32::conv_approx(MAX), i32::MAX); + assert_eq!(i32::conv_approx(MAX + 0.9), i32::MAX); + assert_eq!(i32::try_conv_approx(MAX + 1.0), Err(Error::Range)); +} + +#[test] +fn approx_f64_f32() { + assert_eq!(f32::conv_approx(0f64), 0f32); + assert_eq!(f32::conv_approx(0f64).is_sign_positive(), true); + assert_eq!(f32::conv_approx(-0f64).is_sign_negative(), true); + + const E32: f64 = f32::EPSILON as f64; + assert_eq!(f32::conv_approx(1f64), 1f32); + assert_eq!(f32::conv_approx(1f64 + E32), 1f32 + f32::EPSILON); + assert_eq!(f32::conv_approx(1f64 + E32 / 2.0), 1f32); + assert_eq!(f32::conv_approx(1f64 + E32 - f64::EPSILON), 1f32); + + assert_eq!(f32::conv_approx(-10f64), -10f32); + assert!((f32::conv_approx(1f64 / 3.0) - 1f32 / 3.0).abs() <= f32::EPSILON); + + const MAX: f64 = f32::MAX as f64; + assert_eq!(f32::conv_approx(MAX), f32::MAX); + assert!(MAX + 2f64.powi(103) != MAX); + assert_eq!(f32::conv_approx(MAX + 2f64.powi(103)), f32::MAX); + assert_eq!(f32::try_conv_approx(MAX * 2.0), Err(Error::Range)); + + assert_eq!( + f32::conv_approx(f64::INFINITY).to_bits(), + f32::INFINITY.to_bits() + ); + assert_eq!(f32::try_conv_approx(f64::NAN), Err(Error::Range)); +} + +#[test] +#[should_panic(expected = "cast x: f64 to f32 (approx): range error for x = NaN")] +fn approx_nan() { + f32::conv_approx(f64::NAN); +} + #[test] #[cfg(any(feature = "std", feature = "libm"))] fn float_casts() { @@ -70,14 +117,14 @@ fn float_trunc() { } #[test] -#[should_panic] +#[should_panic(expected = "cast x: f32 to i16 (trunc): range error for x = 32768")] #[cfg(any(feature = "std", feature = "libm"))] fn float_trunc_fail1() { i16::conv_trunc(32768.0f32); } #[test] -#[should_panic] +#[should_panic(expected = "cast x: u32 to f32: inexact for x = 4294967295")] fn u32_max_f32() { f32::conv(core::u32::MAX); }