Rollup merge of #155133 - cuviper:duration_fp_docs, r=the8472

Document precision considerations of `Duration`-float methods

A `Duration` is essentially a 94-bit value (64-bit sec and ~30-bit ns),
so there's some inherent loss when converting to floating-point for
`mul_f64` and `div_f64`. We could go to greater lengths to compute these
with more accuracy, like rust-lang/rust#150933 or rust-lang/rust#154107,
but it's not clear that it's worth the effort. The least we can do is
document that some rounding is to be expected, which this commit does
with simple examples that only multiply or divide by `1.0`.

This also changes the `f32` methods to just forward to `f64`, so we keep
more of that duration precision, as the range is otherwise much more
limited there.
This commit is contained in:
Jonathan Brouwer
2026-04-22 19:18:27 +02:00
committed by GitHub
+82 -6
View File
@@ -1006,6 +1006,7 @@ pub fn from_secs_f32(secs: f32) -> Duration {
/// This method will panic if result is negative, overflows `Duration` or not finite.
///
/// # Examples
///
/// ```
/// use std::time::Duration;
///
@@ -1013,6 +1014,37 @@ pub fn from_secs_f32(secs: f32) -> Duration {
/// assert_eq!(dur.mul_f64(3.14), Duration::new(8, 478_000_000));
/// assert_eq!(dur.mul_f64(3.14e5), Duration::new(847_800, 0));
/// ```
///
/// Note that `f64` does not have enough bits ([`f64::MANTISSA_DIGITS`]) to represent the full
/// range of possible `Duration` with nanosecond precision, so rounding may occur even for
/// trivial operations like multiplying by 1.
///
/// ```
/// # #![feature(float_exact_integer_constants)]
/// use std::time::Duration;
///
/// // This is about 14.9 weeks, remaining precise to the nanosecond:
/// let weeks = Duration::from_nanos(f64::MAX_EXACT_INTEGER as u64);
/// assert_eq!(weeks, weeks.mul_f64(1.0));
///
/// // A larger value incurs rounding in the floating-point operation:
/// let weeks = Duration::from_nanos(u64::MAX);
/// assert_ne!(weeks, weeks.mul_f64(1.0));
///
/// // This is over 285 million years, remaining precise to the second:
/// let years = Duration::from_secs(f64::MAX_EXACT_INTEGER as u64);
/// assert_eq!(years, years.mul_f64(1.0));
///
/// // And again larger values incur rounding:
/// let years = Duration::from_secs(u64::MAX / 2);
/// assert_ne!(years, years.mul_f64(1.0));
/// ```
///
/// ```should_panic
/// # use std::time::Duration;
/// // In the extreme, rounding can even overflow `Duration`, which panics.
/// let _ = Duration::from_secs(u64::MAX).mul_f64(1.0);
/// ```
#[stable(feature = "duration_float", since = "1.38.0")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
@@ -1023,6 +1055,10 @@ pub fn mul_f64(self, rhs: f64) -> Duration {
/// Multiplies `Duration` by `f32`.
///
/// Since the significand of `f32` is quite limited compared to the range of `Duration`
/// -- only about 16.8ms of exact nanosecond precision -- this method currently forwards
/// to [`mul_f64`][Self::mul_f64] for greater accuracy.
///
/// # Panics
/// This method will panic if result is negative, overflows `Duration` or not finite.
///
@@ -1031,7 +1067,10 @@ pub fn mul_f64(self, rhs: f64) -> Duration {
/// use std::time::Duration;
///
/// let dur = Duration::new(2, 700_000_000);
/// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_641));
/// // Note that this `3.14_f32` argument already has more floating-point
/// // representation error than a direct `3.14_f64` would, so the result
/// // is slightly different from the ideal 8.478s.
/// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_283));
/// assert_eq!(dur.mul_f32(3.14e5), Duration::new(847_800, 0));
/// ```
#[stable(feature = "duration_float", since = "1.38.0")]
@@ -1039,7 +1078,7 @@ pub fn mul_f64(self, rhs: f64) -> Duration {
without modifying the original"]
#[inline]
pub fn mul_f32(self, rhs: f32) -> Duration {
Duration::from_secs_f32(rhs * self.as_secs_f32())
self.mul_f64(rhs.into())
}
/// Divides `Duration` by `f64`.
@@ -1048,6 +1087,7 @@ pub fn mul_f32(self, rhs: f32) -> Duration {
/// This method will panic if result is negative, overflows `Duration` or not finite.
///
/// # Examples
///
/// ```
/// use std::time::Duration;
///
@@ -1055,6 +1095,37 @@ pub fn mul_f32(self, rhs: f32) -> Duration {
/// assert_eq!(dur.div_f64(3.14), Duration::new(0, 859_872_611));
/// assert_eq!(dur.div_f64(3.14e5), Duration::new(0, 8_599));
/// ```
///
/// Note that `f64` does not have enough bits ([`f64::MANTISSA_DIGITS`]) to represent the full
/// range of possible `Duration` with nanosecond precision, so rounding may occur even for
/// trivial operations like dividing by 1.
///
/// ```
/// # #![feature(float_exact_integer_constants)]
/// use std::time::Duration;
///
/// // This is about 14.9 weeks, remaining precise to the nanosecond:
/// let weeks = Duration::from_nanos(f64::MAX_EXACT_INTEGER as u64);
/// assert_eq!(weeks, weeks.div_f64(1.0));
///
/// // A larger value incurs rounding in the floating-point operation:
/// let weeks = Duration::from_nanos(u64::MAX);
/// assert_ne!(weeks, weeks.div_f64(1.0));
///
/// // This is over 285 million years, remaining precise to the second:
/// let years = Duration::from_secs(f64::MAX_EXACT_INTEGER as u64);
/// assert_eq!(years, years.div_f64(1.0));
///
/// // And again larger values incur rounding:
/// let years = Duration::from_secs(u64::MAX / 2);
/// assert_ne!(years, years.div_f64(1.0));
/// ```
///
/// ```should_panic
/// # use std::time::Duration;
/// // In the extreme, rounding can even overflow `Duration`, which panics.
/// let _ = Duration::from_secs(u64::MAX).div_f64(1.0);
/// ```
#[stable(feature = "duration_float", since = "1.38.0")]
#[must_use = "this returns the result of the operation, \
without modifying the original"]
@@ -1065,6 +1136,10 @@ pub fn div_f64(self, rhs: f64) -> Duration {
/// Divides `Duration` by `f32`.
///
/// Since the significand of `f32` is quite limited compared to the range of `Duration`
/// -- only about 16.8ms of exact nanosecond precision -- this method currently forwards
/// to [`div_f64`][Self::div_f64] for greater accuracy.
///
/// # Panics
/// This method will panic if result is negative, overflows `Duration` or not finite.
///
@@ -1073,9 +1148,10 @@ pub fn div_f64(self, rhs: f64) -> Duration {
/// use std::time::Duration;
///
/// let dur = Duration::new(2, 700_000_000);
/// // note that due to rounding errors result is slightly
/// // different from 0.859_872_611
/// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_580));
/// // Note that this `3.14_f32` argument already has more floating-point
/// // representation error than a direct `3.14_f64` would, so the result
/// // is slightly different from the ideally rounded 0.859_872_611.
/// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_583));
/// assert_eq!(dur.div_f32(3.14e5), Duration::new(0, 8_599));
/// ```
#[stable(feature = "duration_float", since = "1.38.0")]
@@ -1083,7 +1159,7 @@ pub fn div_f64(self, rhs: f64) -> Duration {
without modifying the original"]
#[inline]
pub fn div_f32(self, rhs: f32) -> Duration {
Duration::from_secs_f32(self.as_secs_f32() / rhs)
self.div_f64(rhs.into())
}
/// Divides `Duration` by `Duration` and returns `f64`.