From 4850316a8784aa0d92e414ab493365b54ee37377 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 16:01:49 +0000 Subject: [PATCH 01/17] Improve Conv/Cast doc; default impl for Conv::conv --- src/lib.rs | 127 +++++++++++++++++++++++++++++------------------------ 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 91d913c..81b976a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,43 +80,87 @@ pub enum Error { #[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", - } - ) + 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 +/// Like [`From`], but supports 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`]. +/// 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. /// -/// Two methods are provided: -/// -/// - [`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`] may be easier to use (as with [`Into`]). 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)) + } +} + +/// Like [`Into`], but for [`Conv`] +/// +/// 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) + } } /// Nearest / floor / ceil conversions from floating point types @@ -177,37 +221,6 @@ pub trait ConvFloat: Sized { 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) - } - #[inline] - fn try_cast(self) -> Result { - T::try_conv(self) - } -} - /// Like [`Into`], but for [`ConvFloat`] /// /// Two sets of methods are provided: From 0286e2108e50124b0699d16c81876938a17502eb Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 16:39:04 +0000 Subject: [PATCH 02/17] Breaking: remove Conv for T impl --- src/impl_basic.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/impl_basic.rs b/src/impl_basic.rs index 3978cc5..3d90579 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 { From 422a8b2d9abf3161aa95bb3b24ed138b8a38de25 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 16:39:31 +0000 Subject: [PATCH 03/17] MSRV=1.53; impl Conv<[S; N]> for [T; N] where T: Conv + Default --- .github/workflows/test.yml | 2 +- README.md | 2 +- src/impl_basic.rs | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) 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/README.md b/README.md index 7faceab..f73b7ee 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ 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 3d90579..6efd3bd 100644 --- a/src/impl_basic.rs +++ b/src/impl_basic.rs @@ -36,3 +36,15 @@ 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: remove T: Copy + Default bound +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) + } +} From 1c4acd396316361eeca8e471ddea9f0c7c5e8022 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 17:05:12 +0000 Subject: [PATCH 04/17] Support Conv for tuples (up to 6 arguments) --- src/impl_basic.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/impl_basic.rs b/src/impl_basic.rs index 6efd3bd..7cb7162 100644 --- a/src/impl_basic.rs +++ b/src/impl_basic.rs @@ -48,3 +48,77 @@ impl + Copy + Default, const N: usize> Conv<[S; N]> for [T; N] { Ok(tt) } } + +impl Conv<()> for () { + #[inline] + fn try_conv(_: ()) -> Result { + Ok(()) + } +} +impl> Conv<(S0,)> for (T0,) { + #[inline] + fn try_conv(ss: (S0,)) -> Result { + Ok((ss.0.try_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()?)) + } +} +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()?)) + } +} +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()?, + )) + } +} +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()?, + )) + } +} +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()?, + )) + } +} From 0fb99ba59e834b945be185279c1d1851f80a1a66 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 17:34:38 +0000 Subject: [PATCH 05/17] Update ConvFloat/CastFloat doc and add default impls to ConvFloat::conv_* --- src/lib.rs | 93 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 53 insertions(+), 40 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 81b976a..3eff430 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,76 +163,89 @@ impl> Cast for S { } } -/// Nearest / floor / ceil conversions from floating point types +/// Nearest / floor / ceiling conversions from floating point types /// /// This trait is explicitly for conversions from floating-point values to -/// integers, supporting four rounding modes for fallible and for -/// "success expected" conversions. +/// integers, supporting four rounding modes. /// -/// Two sets of methods are provided: +/// 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. /// -/// - `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. +/// The `conv_*` methods each have a default implementation over the `try_..` +/// variant which panics on failure. Implementations handle errors as follows: /// -/// 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. +/// - 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`] may be easier to use (as with [`Into`]). #[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 + /// Try converting to integer with truncation /// /// Rounds towards zero (same as `as`). - fn conv_trunc(x: T) -> Self; - /// Convert to the nearest integer + fn try_conv_trunc(x: T) -> Result; + /// Try converting to the nearest integer /// /// Half-way cases are rounded away from `0`. - fn conv_nearest(x: T) -> Self; - /// Convert the floor to an integer + fn try_conv_nearest(x: T) -> Result; + /// Try converting the floor to an integer /// /// Returns the largest integer less than or equal to `x`. - fn conv_floor(x: T) -> Self; - /// Convert the ceiling to an integer + fn try_conv_floor(x: T) -> Result; + /// Try convert the ceiling to an integer /// /// Returns the smallest integer greater than or equal to `x`. - fn conv_ceil(x: T) -> Self; + fn try_conv_ceil(x: T) -> Result; - /// Try converting to integer with truncation + /// Convert to integer with truncatation /// /// Rounds towards zero (same as `as`). - fn try_conv_trunc(x: T) -> Result; - /// Try converting to the nearest integer + fn conv_trunc(x: T) -> Self { + Self::try_conv_trunc(x).unwrap_or_else(|e| panic!("ConvFloat::conv_trunc(_) failed: {}", e)) + } + /// Convert to the nearest integer /// /// Half-way cases are rounded away from `0`. - fn try_conv_nearest(x: T) -> Result; - /// Try converting the floor to an integer + 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 try_conv_floor(x: T) -> Result; - /// Try convert the ceiling to an integer + 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 try_conv_ceil(x: T) -> Result; + 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: /// -/// - `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. +/// - `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. +/// +/// 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 { From e34b8af46432b823cc2d762d73ea3cd47338c465 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 19:36:38 +0000 Subject: [PATCH 06/17] Add ConvApprox and implement f64 to f32 conversion --- src/impl_float.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 12 ++++++++++++ tests/tests.rs | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/src/impl_float.rs b/src/impl_float.rs index a697f09..264c260 100644 --- a/src/impl_float.rs +++ b/src/impl_float.rs @@ -7,6 +7,44 @@ use super::*; +impl ConvApprox for f32 { + fn try_conv_approx(x: f64) -> Result { + use core::num::FpCategory; + + let sign = f32::from_bits((x.to_bits() >> 32) as u32 | 0f32.to_bits()); + + match x.classify() { + FpCategory::Nan => Err(Error::Range), + FpCategory::Infinite => Ok(f32::INFINITY.copysign(sign)), + FpCategory::Zero | FpCategory::Subnormal => Ok(0f32.copysign(sign)), + 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 sign = sign.to_bits() & 0x8000_0000; + let bits = sign | (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; diff --git a/src/lib.rs b/src/lib.rs index 3eff430..11a8696 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -163,6 +163,18 @@ impl> Cast for S { } } +/// Like [`From`], but for approximate numerical conversions +pub trait ConvApprox: Sized { + /// Try converting from `T` to `Self`, allowing approximation of value + fn try_conv_approx(x: T) -> Result; + + /// Converting from `T` to `Self`, allowing approximation of value + fn conv_approx(x: T) -> Self { + Self::try_conv_approx(x) + .unwrap_or_else(|e| panic!("ConvApprox::conv_approx(_) failed: {}", e)) + } +} + /// Nearest / floor / ceiling conversions from floating point types /// /// This trait is explicitly for conversions from floating-point values to diff --git a/tests/tests.rs b/tests/tests.rs index 81a3eb7..6c508ed 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -48,6 +48,44 @@ fn int_to_float_inexact() { f32::conv(0x01FF_FFFF); } +#[test] +fn approx() { + 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); + + assert_eq!(f32::conv_approx(1f64), 1f32); + assert_eq!( + f32::conv_approx(1f64 + f32::EPSILON as f64), + 1f32 + f32::EPSILON + ); + assert_eq!(f32::conv_approx(1f64 + (f32::EPSILON as f64) / 2.0), 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(f32::MAX as f64 * 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() { From dae8c800e93781ce44ce6300d3729c9ad149abca Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 19:41:25 +0000 Subject: [PATCH 07/17] Tests: add expected panic messages --- tests/tests.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/tests.rs b/tests/tests.rs index 6c508ed..a225d2a 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,7 +43,7 @@ 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); } @@ -108,14 +108,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); } From 81cc3ae06bba03c2be0066a744d813b2dab03300 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 19:48:43 +0000 Subject: [PATCH 08/17] Add CastApprox; ConvApprox doc and impl for all ConvFloat --- src/lib.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 11a8696..da671c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,17 +164,91 @@ impl> Cast for S { } /// Like [`From`], but for approximate numerical conversions +/// +/// This trait is implemented for all conversions supported by [`ConvFloat`]. +/// Prefer to use [`ConvFloat`] or [`CastFloat`] where precise control over +/// rounding is required. +/// +/// The sister-trait [`CastApprox`] may be easier to use (as with [`Into`]). pub trait ConvApprox: Sized { /// Try converting from `T` to `Self`, allowing approximation of value + /// + /// 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 + /// + /// This method must return the same result as [`Self::try_conv_approx`] + /// 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_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: Conv +impl> ConvApprox for T { + #[inline] + fn try_conv_approx(x: S) -> Result { + T::try_conv_trunc(x) + } + #[inline] + fn conv_approx(x: S) -> Self { + T::conv_trunc(x) + } +} + +/// Like [`Into`], but for [`ConvApprox`] +/// +/// 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 From f614f1bed18aefaacee0250aec588df01c4a36c4 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 20:22:52 +0000 Subject: [PATCH 09/17] Revise library doc --- src/lib.rs | 53 ++++++++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index da671c6..9433c63 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 From 7712c07ba95a04afd17696de75d9cb30436e5e41 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 20:29:58 +0000 Subject: [PATCH 10/17] Implement ConvApprox for all Conv and ConvFloat impls --- src/impl_float.rs | 22 ++++++++++++++++++++++ src/lib.rs | 11 ++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/impl_float.rs b/src/impl_float.rs index 264c260..69b08a5 100644 --- a/src/impl_float.rs +++ b/src/impl_float.rs @@ -182,6 +182,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); @@ -270,3 +281,14 @@ impl ConvFloat for u128 { } } } + +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 9433c63..573577b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -164,7 +164,8 @@ impl> Cast for S { /// Like [`From`], but for approximate numerical conversions /// -/// This trait is implemented for all conversions supported by [`ConvFloat`]. +/// This trait is implemented for all conversions supported by [`Conv`] and +/// [`ConvFloat`] (but the latter impls only as provided by this library). /// Prefer to use [`ConvFloat`] or [`CastFloat`] where precise control over /// rounding is required. /// @@ -201,15 +202,15 @@ pub trait ConvApprox: Sized { } } -// TODO(specialization): implement also where T: Conv -impl> ConvApprox for T { +// TODO(specialization): implement also where T: ConvFloat +impl> ConvApprox for T { #[inline] fn try_conv_approx(x: S) -> Result { - T::try_conv_trunc(x) + T::try_conv(x) } #[inline] fn conv_approx(x: S) -> Self { - T::conv_trunc(x) + T::conv(x) } } From bb2b64f8f5b236aa3c99f12021a1d31ecf3af5b2 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Mon, 14 Feb 2022 20:45:01 +0000 Subject: [PATCH 11/17] Fixes for no_std --- src/impl_float.rs | 14 +++++++++----- src/lib.rs | 14 ++++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/impl_float.rs b/src/impl_float.rs index 69b08a5..c344e04 100644 --- a/src/impl_float.rs +++ b/src/impl_float.rs @@ -11,12 +11,16 @@ impl ConvApprox for f32 { fn try_conv_approx(x: f64) -> Result { use core::num::FpCategory; - let sign = f32::from_bits((x.to_bits() >> 32) as u32 | 0f32.to_bits()); + 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(f32::INFINITY.copysign(sign)), - FpCategory::Zero | FpCategory::Subnormal => Ok(0f32.copysign(sign)), + 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 @@ -24,8 +28,7 @@ impl ConvApprox for f32 { 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 sign = sign.to_bits() & 0x8000_0000; - let bits = sign | (exp << 23) | frac; + let bits = sign_bits | (exp << 23) | frac; Ok(f32::from_bits(bits)) } else { Err(Error::Range) @@ -282,6 +285,7 @@ impl ConvFloat for u128 { } } +#[cfg(any(feature = "std", feature = "libm"))] impl ConvApprox for u128 { #[inline] fn try_conv_approx(x: f32) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 573577b..71823bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,9 +76,8 @@ pub enum Error { Inexact, } -#[cfg(feature = "std")] -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +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"), @@ -123,7 +122,9 @@ pub trait Conv: Sized { /// 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)) + Self::try_conv(v).unwrap_or_else(|e| { + panic!("Conv::conv(_) failed: {}", e); + }) } } @@ -197,8 +198,9 @@ pub trait ConvApprox: Sized { /// 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)) + Self::try_conv_approx(x).unwrap_or_else(|e| { + panic!("ConvApprox::conv_approx(_) failed: {}", e); + }) } } From 9350096c3db37eeedd56320b55bf9f62c27967c0 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 15 Feb 2022 09:41:50 +0000 Subject: [PATCH 12/17] Doc: specify that ConvApprox usually truncates --- src/lib.rs | 30 +++++++++++++++++++++++------- tests/tests.rs | 28 ++++++++++++++++++---------- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 71823bb..602c4a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,7 +96,7 @@ impl std::error::Error for Error {} /// implementations (specialization or otherwise), only conversions between /// the most important numeric types are supported for now. /// -/// The sister-trait [`Cast`] may be easier to use (as with [`Into`]). +/// The sister-trait [`Cast`] supports "into" style usage. pub trait Conv: Sized { /// Try converting from `T` to `Self` /// @@ -165,15 +165,23 @@ impl> Cast for S { /// Like [`From`], but for approximate numerical conversions /// -/// This trait is implemented for all conversions supported by [`Conv`] and -/// [`ConvFloat`] (but the latter impls only as provided by this library). -/// Prefer to use [`ConvFloat`] or [`CastFloat`] where precise control over -/// rounding is required. +/// 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`. /// -/// The sister-trait [`CastApprox`] may be easier to use (as with [`Into`]). +/// Precise rounding mode should usually be truncation (round towards zero), +/// but this is not required. Use [`ConvFloat`] where a specific rounding mode +/// is required. +/// +/// The sister-trait [`CastApprox`] supports "into" style usage. pub trait ConvApprox: Sized { /// Try converting from `T` to `Self`, allowing approximation of value /// + /// 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. + /// /// This method should allow approximate conversion, but fail on input not /// (approximately) in the target's range. fn try_conv_approx(x: T) -> Result; @@ -218,6 +226,14 @@ impl> ConvApprox for T { /// 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 { @@ -269,7 +285,7 @@ impl> CastApprox for S { /// - Implementations provided by this library will also panic if the /// `always_assert` or `assert_float` feature flag is used. /// -/// The sister-trait [`CastFloat`] may be easier to use (as with [`Into`]). +/// 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 { diff --git a/tests/tests.rs b/tests/tests.rs index a225d2a..fb166e9 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -49,17 +49,28 @@ fn int_to_float_inexact() { } #[test] -fn approx() { +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 + f32::EPSILON as f64), - 1f32 + f32::EPSILON - ); - assert_eq!(f32::conv_approx(1f64 + (f32::EPSILON as f64) / 2.0), 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); @@ -68,10 +79,7 @@ fn approx() { 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(f32::MAX as f64 * 2.0), - Err(Error::Range) - ); + assert_eq!(f32::try_conv_approx(MAX * 2.0), Err(Error::Range)); assert_eq!( f32::conv_approx(f64::INFINITY).to_bits(), From 6f1571ad976379ae7e87528358224e7042e6e70e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 15 Feb 2022 09:53:59 +0000 Subject: [PATCH 13/17] Array/tuple impls: directly implement conv method --- src/impl_basic.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/impl_basic.rs b/src/impl_basic.rs index 7cb7162..4aa1e36 100644 --- a/src/impl_basic.rs +++ b/src/impl_basic.rs @@ -47,6 +47,14 @@ impl + Copy + Default, const N: usize> Conv<[S; N]> for [T; N] { } 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 + } } impl Conv<()> for () { @@ -54,24 +62,40 @@ impl Conv<()> for () { 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) @@ -85,6 +109,10 @@ impl, T1: Conv, T2: Conv, T3: Conv> Con 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) @@ -99,6 +127,16 @@ impl, T1: Conv, T2: Conv, T3: Conv, 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) @@ -121,4 +159,15 @@ where 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(), + ) + } } From 24814e1908833903b32ab17949a22a57b50b841f Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 15 Feb 2022 10:06:39 +0000 Subject: [PATCH 14/17] Implement ConvFloat for arrays and 2-tuples --- src/impl_basic.rs | 107 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/src/impl_basic.rs b/src/impl_basic.rs index 4aa1e36..2379e2f 100644 --- a/src/impl_basic.rs +++ b/src/impl_basic.rs @@ -37,7 +37,8 @@ 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: remove T: Copy + Default bound +// 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 { @@ -57,6 +58,74 @@ impl + Copy + Default, const N: usize> Conv<[S; N]> for [T; N] { } } +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 { @@ -171,3 +240,39 @@ where ) } } + +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)) + } +} From 1b0a8779c68d19fe8ba645cc2d764f9d52375856 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 15 Feb 2022 10:08:49 +0000 Subject: [PATCH 15/17] Update CHANGELOG and README --- CHANGELOG.md | 6 +++++ Cargo.toml | 2 +- README.md | 74 ++++++++++++++++++++++++++++------------------------ 3 files changed, 47 insertions(+), 35 deletions(-) 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 f73b7ee..875cd77 100644 --- a/README.md +++ b/README.md @@ -6,38 +6,54 @@ 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 -When should you *not* use this library? +All traits support two methods: -- Only numeric conversions are supported -- Conversions from floats do not provide fine control of rounding modes -- This library has not been thoroughly tested correctness +- `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`). + +### 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,16 +66,6 @@ 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.53.0 (`IntoIterator for [T; N]`). From 0ba4e0cc09d36c8caa1587624a30e621ac0f066c Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 15 Feb 2022 10:25:14 +0000 Subject: [PATCH 16/17] README: add performance section --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 875cd77..b2ae9a6 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,16 @@ 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`). +### Performance + +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) From dc2c6e2a6563b2672cd4eeb28f0ed671652edb05 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Tue, 15 Feb 2022 10:42:06 +0000 Subject: [PATCH 17/17] Fix no_std --- src/impl_basic.rs | 2 ++ tests/tests.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/src/impl_basic.rs b/src/impl_basic.rs index 2379e2f..c952c41 100644 --- a/src/impl_basic.rs +++ b/src/impl_basic.rs @@ -58,6 +58,7 @@ impl + Copy + Default, const N: usize> Conv<[S; N]> for [T; N] { } } +#[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 { @@ -241,6 +242,7 @@ where } } +#[cfg(any(feature = "std", feature = "libm"))] impl, T1: ConvFloat> ConvFloat<(S0, S1)> for (T0, T1) { #[inline] fn try_conv_trunc(ss: (S0, S1)) -> Result { diff --git a/tests/tests.rs b/tests/tests.rs index fb166e9..fdf8917 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -49,6 +49,7 @@ fn int_to_float_inexact() { } #[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);