Change:
Hello. I am diagnosing why I've got a wrong duration computed from Duration::fromsecsf32()... I tried this code: I expected to see an exact 30-second duration. Instead, I've got this: which is: 30 seconds and 1024 nanos. I dig deeper into rust code and I've prepared a sample code which is computing it based on the source code: As the result I've got: Instead of exact 30 seconds and 0 nanos.
?
Works on current stable and nightly: I mean, that this issue exist for that versions.
yep, I also confirmed on:
What is interesting the function: fromsecsf64 doesn't seem to have this problem... so a quick workaround would be use it and cast from f32 to f64
Extending(?) f32 to u128 gives this addition in 1024:
CC: CC: Adding authors ... guys please take a quick look if you have a time...
f32 will naturally have trouble representing all possible nanosecond values and so doing calculations in f64 may help here somewhat. However the most appropriate approach would be to decompose the float into the whole part and the fractional part and then operate on the two separately to produce the values necessary to create the . and may work sufficiently well for the purpose. EDIT: I think we would still need to be careful about how the nanosecond fraction is handled. We cannot simply multiply the number by and then convert to an integer, because is not capable of representing integers of such degree losslessly.
We cannot simply multiply the number by and then convert to an integer, because f32 is not capable of representing integers of such degree losslessly. Small correction: is represented exactly by , the problem is with . The closest float is , which results in the 1024 ns error. For example, would have produced an exact value. I would say such loss of precision is expected for and if you want exact values, you should use the integer based methods. So I don't think this issue should be marked as a bug. It could be worthwhile to improve the code by processing the fractional part separately, but note that it will not solve this issue fundamentally. We still have the issue of mapping at most 23 fractional bits into ~30 bits, e.g. calling would still result in an inexact result.
specifically may be representable exactly, but f32 cannot represent all integers exactly. Though maybe its not a huge deal if after we end up roundtripping back to the actual integer in all cases.
Argh, I forgot that the method is not available on , so the linked PR will not work out of box. UPD: I have implemented manual fraction/exponent splitting and their conversion to integers.

Discussion:
/// as `f64`. /// /// # Panics <del> /// This constructor will panic if `secs` is not finite, negative or overflows `Duration`. </del> <ins> /// This constructor will panic if `secs` is negative, overflows `Duration` or not finite. </ins> /// /// # Examples /// ``` /// use std::time::Duration; /// <del> /// let dur = Duration::from_secs_f64(2.7); /// assert_eq!(dur, Duration::new(2, 700_000_000)); </del> <ins> /// let res = Duration::from_secs_f64(0.0); /// assert_eq!(res, Duration::new(0, 0)); /// let res = Duration::from_secs_f64(1e-20); /// assert_eq!(res, Duration::new(0, 0)); /// let res = Duration::from_secs_f64(4.2e-7); /// assert_eq!(res, Duration::new(0, 420)); /// let res = Duration::from_secs_f64(2.7); /// assert_eq!(res, Duration::new(2, 700_000_000)); /// let res = Duration::from_secs_f64(3e10); /// assert_eq!(res, Duration::new(30_000_000_000, 0)); /// // subnormal float /// let res = Duration::from_secs_f64(f64::from_bits(1)); /// assert_eq!(res, Duration::new(0, 0)); /// // conversion uses truncation, not rounding /// let res = Duration::from_secs_f64(0.999e-9); /// assert_eq!(res, Duration::new(0, 0)); </ins> /// ``` #[stable(feature = "duration_float", since = "1.38.0")] #[must_use]

} } <del> /// The checked version of [`from_secs_f64`]. /// /// [`from_secs_f64`]: Duration::from_secs_f64 /// /// This constructor will return an `Err` if `secs` is not finite, negative or overflows `Duration`. /// /// # Examples /// ``` /// #![feature(duration_checked_float)] /// use std::time::Duration; /// /// let dur = Duration::try_from_secs_f64(2.7); /// assert_eq!(dur, Ok(Duration::new(2, 700_000_000))); /// /// let negative = Duration::try_from_secs_f64(-5.0); /// assert!(negative.is_err()); /// ``` #[unstable(feature = "duration_checked_float", issue = "83400")] #[inline] pub const fn try_from_secs_f64(secs: f64) -> Result<Duration, FromSecsError> { const MAX_NANOS_F64: f64 = ((u64::MAX as u128 + 1) * (NANOS_PER_SEC as u128)) as f64; let nanos = secs * (NANOS_PER_SEC as f64); if !nanos.is_finite() { Err(FromSecsError { kind: FromSecsErrorKind::NonFinite }) } else if nanos >= MAX_NANOS_F64 { Err(FromSecsError { kind: FromSecsErrorKind::Overflow }) } else if nanos < 0.0 { Err(FromSecsError { kind: FromSecsErrorKind::Negative }) } else { let nanos = nanos as u128; Ok(Duration { secs: (nanos / (NANOS_PER_SEC as u128)) as u64, nanos: (nanos % (NANOS_PER_SEC as u128)) as u32, }) } } </del> /// Creates a new `Duration` from the specified number of seconds represented /// as `f32`. /// /// # Panics <del> /// This constructor will panic if `secs` is not finite, negative or overflows `Duration`. </del> <ins> /// This constructor will panic if `secs` is negative, overflows `Duration` or not finite. </ins> /// /// # Examples /// ``` /// use std::time::Duration; /// <del> /// let dur = Duration::from_secs_f32(2.7); /// assert_eq!(dur, Duration::new(2, 700_000_000)); </del> <ins> /// let res = Duration::from_secs_f32(0.0); /// assert_eq!(res, Duration::new(0, 0)); /// let res = Duration::from_secs_f32(1e-20); /// assert_eq!(res, Duration::new(0, 0)); /// let res = Duration::from_secs_f32(4.2e-7); /// assert_eq!(res, Duration::new(0, 419)); /// let res = Duration::from_secs_f32(2.7); /// assert_eq!(res, Duration::new(2, 700_000_047)); /// let res = Duration::from_secs_f32(3e10); /// assert_eq!(res, Duration::new(30_000_001_024, 0)); /// // subnormal float /// let res = Duration::from_secs_f32(f32::from_bits(1)); /// assert_eq!(res, Duration::new(0, 0)); /// // conversion uses truncation, not rounding /// let res = Duration::from_secs_f32(0.999e-9); /// assert_eq!(res, Duration::new(0, 0)); </ins> /// ``` #[stable(feature = "duration_float", since = "1.38.0")] #[must_use]

} } <del> /// The checked version of [`from_secs_f32`]. /// /// [`from_secs_f32`]: Duration::from_secs_f32 /// /// This constructor will return an `Err` if `secs` is not finite, negative or overflows `Duration`. /// /// # Examples /// ``` /// #![feature(duration_checked_float)] /// use std::time::Duration; /// /// let dur = Duration::try_from_secs_f32(2.7); /// assert_eq!(dur, Ok(Duration::new(2, 700_000_000))); /// /// let negative = Duration::try_from_secs_f32(-5.0); /// assert!(negative.is_err()); /// ``` #[unstable(feature = "duration_checked_float", issue = "83400")] #[inline] pub const fn try_from_secs_f32(secs: f32) -> Result<Duration, FromSecsError> { const MAX_NANOS_F32: f32 = ((u64::MAX as u128 + 1) * (NANOS_PER_SEC as u128)) as f32; let nanos = secs * (NANOS_PER_SEC as f32); if !nanos.is_finite() { Err(FromSecsError { kind: FromSecsErrorKind::NonFinite }) } else if nanos >= MAX_NANOS_F32 { Err(FromSecsError { kind: FromSecsErrorKind::Overflow }) } else if nanos < 0.0 { Err(FromSecsError { kind: FromSecsErrorKind::Negative }) } else { let nanos = nanos as u128; Ok(Duration { secs: (nanos / (NANOS_PER_SEC as u128)) as u64, nanos: (nanos % (NANOS_PER_SEC as u128)) as u32, }) } } </del> /// Multiplies `Duration` by `f64`. /// /// # Panics <del> /// This method will panic if result is not finite, negative or overflows `Duration`. </del> <ins> /// This method will panic if result is negative, overflows `Duration` or not finite. </ins> /// /// # Examples /// ```

/// Multiplies `Duration` by `f32`. /// /// # Panics <del> /// This method will panic if result is not finite, negative or overflows `Duration`. </del> <ins> /// This method will panic if result is negative, overflows `Duration` or not finite. </ins> /// /// # Examples /// ``` /// use std::time::Duration; /// /// let dur = Duration::new(2, 700_000_000); <del> /// // note that due to rounding errors result is slightly different /// // from 8.478 and 847800.0 </del> /// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_640)); <del> /// assert_eq!(dur.mul_f32(3.14e5), Duration::new(847799, 969_120_256)); </del> <ins> /// assert_eq!(dur.mul_f32(3.14e5), Duration::new(847800, 0)); </ins> /// ``` #[stable(feature = "duration_float", since = "1.38.0")] #[must_use = "this returns the result of the operation, 

/// Divide `Duration` by `f64`. /// /// # Panics <del> /// This method will panic if result is not finite, negative or overflows `Duration`. </del> <ins> /// This method will panic if result is negative, overflows `Duration` or not finite. </ins> /// /// # Examples /// ```

/// Divide `Duration` by `f32`. /// /// # Panics <del> /// This method will panic if result is not finite, negative or overflows `Duration`. </del> <ins> /// This method will panic if result is negative, overflows `Duration` or not finite. </ins> /// /// # Examples /// ```

/// let dur = Duration::new(2, 700_000_000); /// // note that due to rounding errors result is slightly /// // different from 0.859_872_611 <del> /// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_576)); </del> <ins> /// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_579)); </ins> /// // note that truncation is used, not rounding /// assert_eq!(dur.div_f32(3.14e5), Duration::new(0, 8_598)); /// ```

/// ``` #[derive(Debug, Clone, PartialEq, Eq)] #[unstable(feature = "duration_checked_float", issue = "83400")] <del> pub struct FromSecsError { kind: FromSecsErrorKind, </del> <ins> pub struct FromFloatSecsError { kind: FromFloatSecsErrorKind, </ins> } <del> impl FromSecsError { </del> <ins> impl FromFloatSecsError { </ins> const fn description(&self) -> &'static str { match self.kind { <del> FromSecsErrorKind::NonFinite => "non-finite value when converting float to duration", FromSecsErrorKind::Overflow => "overflow when converting float to duration", FromSecsErrorKind::Negative => "negative value when converting float to duration", </del> <ins> FromFloatSecsErrorKind::Negative => { "can not convert float seconds to Duration: value is negative" } FromFloatSecsErrorKind::OverflowOrNan => { "can not convert float seconds to Duration: value is either too big or NaN" } </ins> } } } #[unstable(feature = "duration_checked_float", issue = "83400")] <del> impl fmt::Display for FromSecsError { </del> <ins> impl fmt::Display for FromFloatSecsError { </ins> fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { <del> fmt::Display::fmt(self.description(), f) </del> <ins> self.description().fmt(f) </ins> } } #[derive(Debug, Clone, PartialEq, Eq)] <del> enum FromSecsErrorKind { // Value is not a finite value (either + or - infinity or NaN). NonFinite, // Value is too large to store in a `Duration`. Overflow, </del> <ins> enum FromFloatSecsErrorKind { </ins> // Value is negative. Negative, <ins> // Value is either too big to be represented as `Duration` or `NaN`. OverflowOrNan, } macro_rules! try_from_secs { ( secs = $secs: expr, mantissa_bits = $mant_bits: literal, exponent_bits = $exp_bits: literal, offset = $offset: literal, bits_ty = $bits_ty:ty, double_ty = $double_ty:ty, ) => {{ const MIN_EXP: i16 = 1 - (1i16 << $exp_bits) / 2; const MANT_MASK: $bits_ty = (1 << $mant_bits) - 1; const EXP_MASK: $bits_ty = (1 << $exp_bits) - 1; if $secs.is_sign_negative() { return Err(FromFloatSecsError { kind: FromFloatSecsErrorKind::Negative }); } let bits = $secs.to_bits(); let mant = (bits & MANT_MASK) | (MANT_MASK + 1); let exp = ((bits >> $mant_bits) & EXP_MASK) as i16 + MIN_EXP; let (secs, nanos) = if exp < -30 { // the input represents less than 1ns. (0u64, 0u32) } else if exp < 0 { // the input is less than 1 second let t = <$double_ty>::from(mant) << ($offset + exp); let nanos = (u128::from(NANOS_PER_SEC) * u128::from(t)) >> ($mant_bits + $offset); (0, nanos as u32) } else if exp < $mant_bits { let secs = mant >> ($mant_bits - exp); let t = <$double_ty>::from((mant << exp) & MANT_MASK); let nanos = (<$double_ty>::from(NANOS_PER_SEC) * t) >> $mant_bits; (u64::from(secs), nanos as u32) } else if exp < 64 { // the input has no fractional part let secs = u64::from(mant) << (exp - $mant_bits); (secs, 0) } else { return Err(FromFloatSecsError { kind: FromFloatSecsErrorKind::OverflowOrNan }); }; Ok(Duration { secs, nanos }) }}; } impl Duration { /// The checked version of [`from_secs_f32`]. /// /// [`from_secs_f32`]: Duration::from_secs_f32 /// /// This constructor will return an `Err` if `secs` is negative, overflows `Duration` or not finite. /// /// # Examples /// ``` /// #![feature(duration_checked_float)] /// /// use std::time::Duration; /// /// let res = Duration::try_from_secs_f32(0.0); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// let res = Duration::try_from_secs_f32(1e-20); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// let res = Duration::try_from_secs_f32(4.2e-7); /// assert_eq!(res, Ok(Duration::new(0, 419))); /// let res = Duration::try_from_secs_f32(2.7); /// assert_eq!(res, Ok(Duration::new(2, 700_000_047))); /// let res = Duration::try_from_secs_f32(3e10); /// assert_eq!(res, Ok(Duration::new(30_000_001_024, 0))); /// // subnormal float: /// let res = Duration::try_from_secs_f32(f32::from_bits(1)); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// // conversion uses truncation, not rounding /// let res = Duration::try_from_secs_f32(0.999e-9); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// /// let res = Duration::try_from_secs_f32(-5.0); /// assert!(res.is_err()); /// let res = Duration::try_from_secs_f32(f32::NAN); /// assert!(res.is_err()); /// let res = Duration::try_from_secs_f32(2e19); /// assert!(res.is_err()); /// ``` #[unstable(feature = "duration_checked_float", issue = "83400")] #[inline] pub const fn try_from_secs_f32(secs: f32) -> Result<Duration, FromFloatSecsError> { try_from_secs!( secs = secs, mantissa_bits = 23, exponent_bits = 8, offset = 41, bits_ty = u32, double_ty = u64, ) } /// The checked version of [`from_secs_f64`]. /// /// [`from_secs_f64`]: Duration::from_secs_f64 /// /// This constructor will return an `Err` if `secs` is negative, overflows `Duration` or not finite. /// /// # Examples /// ``` /// #![feature(duration_checked_float)] /// /// use std::time::Duration; /// /// let res = Duration::try_from_secs_f64(0.0); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// let res = Duration::try_from_secs_f64(1e-20); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// let res = Duration::try_from_secs_f64(4.2e-7); /// assert_eq!(res, Ok(Duration::new(0, 420))); /// let res = Duration::try_from_secs_f64(2.7); /// assert_eq!(res, Ok(Duration::new(2, 700_000_000))); /// let res = Duration::try_from_secs_f64(3e10); /// assert_eq!(res, Ok(Duration::new(30_000_000_000, 0))); /// // subnormal float /// let res = Duration::try_from_secs_f64(f64::from_bits(1)); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// // conversion uses truncation, not rounding /// let res = Duration::try_from_secs_f32(0.999e-9); /// assert_eq!(res, Ok(Duration::new(0, 0))); /// /// let res = Duration::try_from_secs_f64(-5.0); /// assert!(res.is_err()); /// let res = Duration::try_from_secs_f64(f64::NAN); /// assert!(res.is_err()); /// let res = Duration::try_from_secs_f64(2e19); /// assert!(res.is_err()); /// ``` #[unstable(feature = "duration_checked_float", issue = "83400")] #[inline] pub const fn try_from_secs_f64(secs: f64) -> Result<Duration, FromFloatSecsError> { try_from_secs!( secs = secs, mantissa_bits = 52, exponent_bits = 11, offset = 44, bits_ty = u64, double_ty = u128, ) } </ins> }

impl Error for alloc::collections::TryReserveError {} #[unstable(feature = "duration_checked_float", issue = "83400")] <del> impl Error for time::FromSecsError {} </del> <ins> impl Error for time::FromFloatSecsError {} </ins> // Copied from `any.rs`. impl dyn Error + 'static {

pub use core::time::Duration; #[unstable(feature = "duration_checked_float", issue = "83400")] <del> pub use core::time::FromSecsError; </del> <ins> pub use core::time::FromFloatSecsError; </ins> /// A measurement of a monotonically nondecreasing clock. /// Opaque and useful only with [`Duration`].