Skip to content

Commit fe77711

Browse files
committed
Fix precision loss in Duration::mul_f32/f64 and div_f32/f64
The previous implementation converted the entire Duration to f64 before multiplying/dividing, which caused precision loss because Duration has ~93 bits of precision (u64 secs + u32 nanos) but f64 only has 53 bits of mantissa. This caused issues like: - `Duration::MAX.mul_f32(1.0)` panicking due to overflow - `d.mul_f64(1.0)` not preserving the exact value The fix multiplies/divides seconds and nanoseconds separately, then combines the results. This preserves precision for the common case while properly handling edge cases (NaN, infinity, overflow). Changes: - Add helper function `duration_from_float_secs()` to reduce duplication - Add `#[track_caller]` to all four methods for better panic messages - Add explicit infinity check with `is_infinite()` - Check overflow before casting to u64 and use `checked_add()` - Clamp final nanoseconds with `.max(0.0)` for floating-point errors - Update doctest expected values to match new (correct) behavior - Add comprehensive tests for edge cases
1 parent e96bb7e commit fe77711

File tree

2 files changed

+338
-6
lines changed

2 files changed

+338
-6
lines changed

library/core/src/time.rs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
//! ```
2121
2222
use crate::fmt;
23+
use crate::intrinsics;
2324
use crate::iter::Sum;
2425
use crate::num::niche_types::Nanoseconds;
2526
use crate::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
@@ -1019,8 +1020,13 @@ impl Duration {
10191020
#[must_use = "this returns the result of the operation, \
10201021
without modifying the original"]
10211022
#[inline]
1023+
#[track_caller]
10221024
pub fn mul_f64(self, rhs: f64) -> Duration {
1023-
Duration::from_secs_f64(rhs * self.as_secs_f64())
1025+
// Multiply seconds and nanoseconds separately to preserve precision.
1026+
// This avoids the precision loss from converting the full Duration to f64.
1027+
let secs_product = (self.secs as f64) * rhs;
1028+
let nanos_product = (self.nanos.as_inner() as f64) * rhs;
1029+
Self::duration_from_float_secs(secs_product, nanos_product, "Duration::mul_f64")
10241030
}
10251031

10261032
/// Multiplies `Duration` by `f32`.
@@ -1033,15 +1039,22 @@ impl Duration {
10331039
/// use std::time::Duration;
10341040
///
10351041
/// let dur = Duration::new(2, 700_000_000);
1036-
/// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_641));
1042+
/// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_283));
10371043
/// assert_eq!(dur.mul_f32(3.14e5), Duration::new(847_800, 0));
10381044
/// ```
10391045
#[stable(feature = "duration_float", since = "1.38.0")]
10401046
#[must_use = "this returns the result of the operation, \
10411047
without modifying the original"]
10421048
#[inline]
1049+
#[track_caller]
10431050
pub fn mul_f32(self, rhs: f32) -> Duration {
1044-
Duration::from_secs_f32(rhs * self.as_secs_f32())
1051+
// Multiply seconds and nanoseconds separately to preserve precision.
1052+
// This avoids the precision loss from converting the full Duration to f32.
1053+
// Use f64 for intermediate calculations to preserve precision.
1054+
let rhs_f64 = rhs as f64;
1055+
let secs_product = (self.secs as f64) * rhs_f64;
1056+
let nanos_product = (self.nanos.as_inner() as f64) * rhs_f64;
1057+
Self::duration_from_float_secs(secs_product, nanos_product, "Duration::mul_f32")
10451058
}
10461059

10471060
/// Divides `Duration` by `f64`.
@@ -1061,8 +1074,13 @@ impl Duration {
10611074
#[must_use = "this returns the result of the operation, \
10621075
without modifying the original"]
10631076
#[inline]
1077+
#[track_caller]
10641078
pub fn div_f64(self, rhs: f64) -> Duration {
1065-
Duration::from_secs_f64(self.as_secs_f64() / rhs)
1079+
// Divide seconds and nanoseconds separately to preserve precision.
1080+
// This avoids the precision loss from converting the full Duration to f64.
1081+
let secs_quotient = (self.secs as f64) / rhs;
1082+
let nanos_quotient = (self.nanos.as_inner() as f64) / rhs;
1083+
Self::duration_from_float_secs(secs_quotient, nanos_quotient, "Duration::div_f64")
10661084
}
10671085

10681086
/// Divides `Duration` by `f32`.
@@ -1077,15 +1095,84 @@ impl Duration {
10771095
/// let dur = Duration::new(2, 700_000_000);
10781096
/// // note that due to rounding errors result is slightly
10791097
/// // different from 0.859_872_611
1080-
/// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_580));
1098+
/// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_583));
10811099
/// assert_eq!(dur.div_f32(3.14e5), Duration::new(0, 8_599));
10821100
/// ```
10831101
#[stable(feature = "duration_float", since = "1.38.0")]
10841102
#[must_use = "this returns the result of the operation, \
10851103
without modifying the original"]
10861104
#[inline]
1105+
#[track_caller]
10871106
pub fn div_f32(self, rhs: f32) -> Duration {
1088-
Duration::from_secs_f32(self.as_secs_f32() / rhs)
1107+
// Divide seconds and nanoseconds separately to preserve precision.
1108+
// This avoids the precision loss from converting the full Duration to f32.
1109+
// Use f64 for intermediate calculations to preserve precision.
1110+
let rhs_f64 = rhs as f64;
1111+
let secs_quotient = (self.secs as f64) / rhs_f64;
1112+
let nanos_quotient = (self.nanos.as_inner() as f64) / rhs_f64;
1113+
Self::duration_from_float_secs(secs_quotient, nanos_quotient, "Duration::div_f32")
1114+
}
1115+
1116+
/// Helper function to create a Duration from floating-point seconds and nanoseconds.
1117+
/// Used by mul_f32, mul_f64, div_f32, div_f64 to avoid code duplication.
1118+
///
1119+
/// # Arguments
1120+
/// * `secs_f64` - The seconds component (may have fractional part)
1121+
/// * `nanos_f64` - The nanoseconds component (may have fractional part)
1122+
/// * `fn_name` - The name of the calling function for panic messages
1123+
#[inline]
1124+
#[track_caller]
1125+
fn duration_from_float_secs(secs_f64: f64, nanos_f64: f64, fn_name: &str) -> Duration {
1126+
// Check for negative, NaN, or infinity
1127+
// Note: !(x >= 0.0) catches negative numbers, NaN, and -infinity
1128+
// We also explicitly check for +infinity which would pass the >= 0.0 check
1129+
if !(secs_f64 >= 0.0 && nanos_f64 >= 0.0)
1130+
|| secs_f64.is_infinite()
1131+
|| nanos_f64.is_infinite()
1132+
{
1133+
panic!("{fn_name}: time is negative, NaN, or infinite");
1134+
}
1135+
1136+
// Split seconds into integer and fractional parts
1137+
let secs_whole = intrinsics::truncf64(secs_f64);
1138+
let secs_frac = secs_f64 - secs_whole;
1139+
1140+
// Check for overflow BEFORE casting to u64
1141+
if secs_whole > u64::MAX as f64 {
1142+
panic!("overflow in {fn_name}");
1143+
}
1144+
1145+
// Convert fractional seconds to nanoseconds and add to nanos_f64
1146+
let nanos_from_secs = secs_frac * (NANOS_PER_SEC as f64);
1147+
let total_nanos = nanos_f64 + nanos_from_secs;
1148+
1149+
// Handle nanoseconds overflow into seconds
1150+
let extra_secs = intrinsics::truncf64(total_nanos / (NANOS_PER_SEC as f64));
1151+
// Clamp to 0.0 to handle potential floating-point errors that could make this negative
1152+
let final_nanos = (total_nanos - extra_secs * (NANOS_PER_SEC as f64)).max(0.0);
1153+
1154+
// Round nanoseconds to nearest integer
1155+
let nanos_rounded = intrinsics::roundf64(final_nanos) as u32;
1156+
1157+
// Handle edge case where rounding pushes nanos to NANOS_PER_SEC
1158+
let (nanos_final, secs_adjust) = if nanos_rounded >= NANOS_PER_SEC {
1159+
(nanos_rounded - NANOS_PER_SEC, 1u64)
1160+
} else {
1161+
(nanos_rounded, 0u64)
1162+
};
1163+
1164+
// Compute total seconds with overflow checking
1165+
let secs_whole_u64 = secs_whole as u64;
1166+
let extra_secs_u64 = extra_secs as u64;
1167+
1168+
let total_secs = secs_whole_u64
1169+
.checked_add(extra_secs_u64)
1170+
.and_then(|s| s.checked_add(secs_adjust));
1171+
1172+
match total_secs {
1173+
Some(secs) => Duration::new(secs, nanos_final),
1174+
None => panic!("overflow in {fn_name}"),
1175+
}
10891176
}
10901177

10911178
/// Divides `Duration` by `Duration` and returns `f64`.

library/coretests/tests/time.rs

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,3 +598,248 @@ fn from_neg_zero() {
598598
assert_eq!(Duration::from_secs_f32(-0.0), Duration::ZERO);
599599
assert_eq!(Duration::from_secs_f64(-0.0), Duration::ZERO);
600600
}
601+
602+
/// Test that multiplying/dividing by 1.0 preserves precision.
603+
/// Regression test for <https://github.com/rust-lang/rust/issues/149794>
604+
#[test]
605+
fn mul_div_float_precision() {
606+
// Multiplying by 1.0 should preserve the exact value
607+
let d1 = Duration::from_millis(100);
608+
assert_eq!(d1.mul_f32(1.0), d1);
609+
assert_eq!(d1.mul_f64(1.0), d1);
610+
611+
let d2 = Duration::new(1, 111);
612+
assert_eq!(d2.mul_f32(1.0), d2);
613+
assert_eq!(d2.mul_f64(1.0), d2);
614+
615+
// Dividing by 1.0 should preserve the exact value
616+
assert_eq!(d1.div_f32(1.0), d1);
617+
assert_eq!(d1.div_f64(1.0), d1);
618+
assert_eq!(d2.div_f32(1.0), d2);
619+
assert_eq!(d2.div_f64(1.0), d2);
620+
621+
// Multiplying by small integers should match integer multiplication
622+
assert_eq!(d1.mul_f32(2.0), d1 * 2);
623+
assert_eq!(d1.mul_f32(3.0), d1 * 3);
624+
assert_eq!(d1.mul_f64(2.0), d1 * 2);
625+
assert_eq!(d1.mul_f64(3.0), d1 * 3);
626+
627+
assert_eq!(d2.mul_f32(2.0), d2 * 2);
628+
assert_eq!(d2.mul_f32(3.0), d2 * 3);
629+
assert_eq!(d2.mul_f64(2.0), d2 * 2);
630+
assert_eq!(d2.mul_f64(3.0), d2 * 3);
631+
632+
// Duration::MAX.mul_f32(1.0) should not panic and should preserve the value
633+
assert_eq!(Duration::MAX.mul_f32(1.0), Duration::MAX);
634+
assert_eq!(Duration::MAX.mul_f64(1.0), Duration::MAX);
635+
assert_eq!(Duration::MAX.div_f32(1.0), Duration::MAX);
636+
assert_eq!(Duration::MAX.div_f64(1.0), Duration::MAX);
637+
638+
// Fractional multipliers should work correctly
639+
assert_eq!(Duration::from_secs(10).mul_f64(0.5), Duration::from_secs(5));
640+
assert_eq!(Duration::from_secs(10).div_f64(2.0), Duration::from_secs(5));
641+
642+
// Nanosecond precision should be maintained
643+
let d3 = Duration::new(0, 123_456_789);
644+
assert_eq!(d3.mul_f64(1.0), d3);
645+
assert_eq!(d3.div_f64(1.0), d3);
646+
}
647+
648+
#[test]
649+
#[should_panic]
650+
fn mul_f64_negative() {
651+
let _ = Duration::from_secs(1).mul_f64(-1.0);
652+
}
653+
654+
#[test]
655+
#[should_panic]
656+
fn mul_f32_negative() {
657+
let _ = Duration::from_secs(1).mul_f32(-1.0);
658+
}
659+
660+
#[test]
661+
#[should_panic]
662+
fn div_f64_negative() {
663+
let _ = Duration::from_secs(1).div_f64(-1.0);
664+
}
665+
666+
#[test]
667+
#[should_panic]
668+
fn div_f32_negative() {
669+
let _ = Duration::from_secs(1).div_f32(-1.0);
670+
}
671+
672+
#[test]
673+
#[should_panic]
674+
fn mul_f64_nan() {
675+
let _ = Duration::from_secs(1).mul_f64(f64::NAN);
676+
}
677+
678+
#[test]
679+
#[should_panic]
680+
fn mul_f32_nan() {
681+
let _ = Duration::from_secs(1).mul_f32(f32::NAN);
682+
}
683+
684+
#[test]
685+
#[should_panic]
686+
fn div_f64_nan() {
687+
let _ = Duration::from_secs(1).div_f64(f64::NAN);
688+
}
689+
690+
#[test]
691+
#[should_panic]
692+
fn div_f32_nan() {
693+
let _ = Duration::from_secs(1).div_f32(f32::NAN);
694+
}
695+
696+
#[test]
697+
#[should_panic]
698+
fn mul_f64_infinity() {
699+
let _ = Duration::from_secs(1).mul_f64(f64::INFINITY);
700+
}
701+
702+
#[test]
703+
#[should_panic]
704+
fn mul_f32_infinity() {
705+
let _ = Duration::from_secs(1).mul_f32(f32::INFINITY);
706+
}
707+
708+
#[test]
709+
#[should_panic]
710+
fn div_f64_zero() {
711+
let _ = Duration::from_secs(1).div_f64(0.0);
712+
}
713+
714+
#[test]
715+
#[should_panic]
716+
fn div_f32_zero() {
717+
let _ = Duration::from_secs(1).div_f32(0.0);
718+
}
719+
720+
#[test]
721+
#[should_panic]
722+
fn mul_f64_overflow() {
723+
let _ = Duration::MAX.mul_f64(2.0);
724+
}
725+
726+
#[test]
727+
#[should_panic]
728+
fn mul_f32_overflow() {
729+
let _ = Duration::MAX.mul_f32(2.0);
730+
}
731+
732+
#[test]
733+
#[should_panic]
734+
fn mul_f64_neg_infinity() {
735+
let _ = Duration::from_secs(1).mul_f64(f64::NEG_INFINITY);
736+
}
737+
738+
#[test]
739+
#[should_panic]
740+
fn mul_f32_neg_infinity() {
741+
let _ = Duration::from_secs(1).mul_f32(f32::NEG_INFINITY);
742+
}
743+
744+
#[test]
745+
fn div_f64_neg_infinity() {
746+
// Division by negative infinity produces -0.0, which is treated as zero
747+
assert_eq!(Duration::from_secs(1).div_f64(f64::NEG_INFINITY), Duration::ZERO);
748+
}
749+
750+
#[test]
751+
fn div_f32_neg_infinity() {
752+
assert_eq!(Duration::from_secs(1).div_f32(f32::NEG_INFINITY), Duration::ZERO);
753+
}
754+
755+
#[test]
756+
fn div_f64_infinity() {
757+
assert_eq!(Duration::from_secs(1).div_f64(f64::INFINITY), Duration::ZERO);
758+
assert_eq!(Duration::MAX.div_f64(f64::INFINITY), Duration::ZERO);
759+
}
760+
761+
#[test]
762+
fn div_f32_infinity() {
763+
assert_eq!(Duration::from_secs(1).div_f32(f32::INFINITY), Duration::ZERO);
764+
assert_eq!(Duration::MAX.div_f32(f32::INFINITY), Duration::ZERO);
765+
}
766+
767+
#[test]
768+
fn mul_div_zero_duration() {
769+
assert_eq!(Duration::ZERO.mul_f64(1.0), Duration::ZERO);
770+
assert_eq!(Duration::ZERO.mul_f64(1000.0), Duration::ZERO);
771+
assert_eq!(Duration::ZERO.mul_f32(1.0), Duration::ZERO);
772+
assert_eq!(Duration::ZERO.mul_f32(1000.0), Duration::ZERO);
773+
774+
assert_eq!(Duration::ZERO.div_f64(1.0), Duration::ZERO);
775+
assert_eq!(Duration::ZERO.div_f64(1000.0), Duration::ZERO);
776+
assert_eq!(Duration::ZERO.div_f32(1.0), Duration::ZERO);
777+
assert_eq!(Duration::ZERO.div_f32(1000.0), Duration::ZERO);
778+
779+
assert_eq!(Duration::from_secs(100).mul_f64(0.0), Duration::ZERO);
780+
assert_eq!(Duration::from_secs(100).mul_f32(0.0), Duration::ZERO);
781+
}
782+
783+
#[test]
784+
fn mul_div_subnormal() {
785+
let subnormal_f64 = f64::from_bits(1);
786+
let subnormal_f32 = f32::from_bits(1);
787+
788+
assert_eq!(Duration::from_secs(1).mul_f64(subnormal_f64), Duration::ZERO);
789+
assert_eq!(Duration::from_secs(1).mul_f32(subnormal_f32), Duration::ZERO);
790+
791+
assert_eq!(Duration::from_nanos(1).mul_f64(1e-20), Duration::ZERO);
792+
assert_eq!(Duration::from_nanos(1).mul_f32(1e-20), Duration::ZERO);
793+
}
794+
795+
#[test]
796+
fn mul_div_rounding_boundaries() {
797+
let almost_one_sec = Duration::new(0, 999_999_999);
798+
assert_eq!(almost_one_sec.mul_f64(1.0), almost_one_sec);
799+
assert_eq!(almost_one_sec.div_f64(1.0), almost_one_sec);
800+
801+
let half_sec = Duration::from_millis(500);
802+
assert_eq!(half_sec.mul_f64(2.0), Duration::from_secs(1));
803+
assert_eq!(half_sec.mul_f32(2.0), Duration::from_secs(1));
804+
805+
let d = Duration::new(0, 999_999_999);
806+
assert_eq!(d.mul_f64(2.0), Duration::new(1, 999_999_998));
807+
808+
let d = Duration::from_secs(1);
809+
assert_eq!(d.mul_f64(2.0), Duration::from_secs(2));
810+
assert_eq!(d.mul_f64(4.0), Duration::from_secs(4));
811+
assert_eq!(d.mul_f64(8.0), Duration::from_secs(8));
812+
assert_eq!(d.mul_f64(0.5), Duration::from_millis(500));
813+
assert_eq!(d.mul_f64(0.25), Duration::from_millis(250));
814+
assert_eq!(d.mul_f64(0.125), Duration::from_millis(125));
815+
}
816+
817+
#[test]
818+
fn mul_div_small_durations() {
819+
let one_ns = Duration::from_nanos(1);
820+
assert_eq!(one_ns.mul_f64(1.0), one_ns);
821+
assert_eq!(one_ns.mul_f64(2.0), Duration::from_nanos(2));
822+
assert_eq!(one_ns.div_f64(1.0), one_ns);
823+
824+
let one_us = Duration::from_micros(1);
825+
assert_eq!(one_us.mul_f64(1.0), one_us);
826+
assert_eq!(one_us.mul_f64(1000.0), Duration::from_millis(1));
827+
assert_eq!(one_us.div_f64(1.0), one_us);
828+
829+
let one_ms = Duration::from_millis(1);
830+
assert_eq!(one_ms.mul_f64(1.0), one_ms);
831+
assert_eq!(one_ms.mul_f64(1000.0), Duration::from_secs(1));
832+
assert_eq!(one_ms.div_f64(1.0), one_ms);
833+
}
834+
835+
#[test]
836+
fn mul_div_large_factors() {
837+
let d = Duration::from_nanos(1);
838+
assert_eq!(d.mul_f64(1e9), Duration::from_secs(1));
839+
assert_eq!(d.mul_f64(1e6), Duration::from_millis(1));
840+
841+
let d = Duration::from_secs(1);
842+
assert_eq!(d.div_f64(1e9), Duration::from_nanos(1));
843+
assert_eq!(d.div_f64(1e6), Duration::from_micros(1));
844+
assert_eq!(d.div_f64(1e3), Duration::from_millis(1));
845+
}

0 commit comments

Comments
 (0)