Change:
The above assertion succeeds on Rust standard library versions prior to 1.60.0. On 1.60.0 it fails with: The exact mathematical value represented by 0. is: which is about .999999999998ns. This is astronomically closer to 42000ns than to 41999ns. I believe the correct behavior for would be to produce 42000ns as before, not truncate to 41999ns. Mentioning since seems related and is in the right commit range. The PR summary mentions that the PR would be performing truncation instead of rounding but I don't see that this decision was ever discussed by the library API team. (Mentioning to comment on desired behavior.)
See also
says that the decision in favor of truncating was motivated in part because the integer accessors on Duration, such as , do truncation not rounding. For example a duration with nanos would say it has 41 subsec micros, not 42. I don't believe this rationale is correct to apply to . People's expectations are quite different for integer manipulation vs floats. I think it is widely understood that performs truncating division for integers and rounding for floats (mandated by IEEE 754). Truncating is especially not a good behavior for because every float that is the closest f64 to an exact number of nanoseconds (like 0. is) is going to be in some tiny tiny interval around that number of nanoseconds, "randomly" either slightly smaller or slightly greater. It would be silly to split that interval and send the bottom half of it to a different result than the top half. (In reality these intervals are 10000000000x smaller than shown in the ASCII art, which makes the truncating behavior even more absurd.) None of the integer operations on Duration share a similar characteristic to this.
Hm, rounds, while truncates. This is basically the situation, which would suggest that truncating is the right option. Except that integers (whole numbers) are exactly representable, which makes the issue disappear: So, while is slightly less than it should be, still produces exactly . Interestingly, the rounding that makes this work happens in the multiplication, not in the conversion to i64.
That's not universal though. For example, is 14, not 15. And that already happened in Duration::fromsecsf64 before 1.60: Latest Rust: Rust 1.59.0:
It might be reasonable to round to nanoseconds. (Simply add half a nanosecond to the f64 before converting it.) But the previous behaviour was also truncating, not rounding. It's just that rounding errors happen in slightly different places, making this a regression for 0..
Testing for every whole number of nanoseconds between 0 and 1, we had 'wrong' answers for 1.7% of them before the recent change (like 0.000000015s = 14ns), and for 48% of them after the change (like 0.00042s = 41999ns).
Here's another argument for doing rounding, that does not involve floating point literals: It doesn't round-trip, even if f64 has more than enough precision: If we simply add 0.5e-9 to the f64 before converting it, to make it round instead of truncate, all these issues disappear.
This is not something we can do. Taking the ASCII art given by dtolnay above, if we were to adopt a round-to-nearest scheme, we'd end up with something like this: If we were to add half a nanosecond to the floating point value before truncating, we'd end up rounding wrong at the exact points where rounding… “direction” changes (as denoted by ). There are some descriptions on how to correctly implement round-to-nearest in software for example , but it'll naturally make the algorithm that much more complicated. The roundtrip property does not and cannot hold for the values representable by . can hold 96 bits worth of data.
I've created the experimental PR which implements rounding. I am not 100% sure that rounding is implemented correctly and that I haven't missed any corner cases. Personally, I think that having truncation in float-duration conversion is fine and that we should prefer it for code simplicity.
As wrote, the notion of round-trip you are using (duration -&gt; f64 -&gt; duration) is not something that can be lossless in general, so this doesn't work as an argument against truncating. However there is a different round trip that would be valuable to support but broken by truncation: f64 -&gt; duration -&gt; f64 -&gt; duration. Or in practical terms: You have a config file containing that you TOML deserialization / YAML deserialize using Serde into a Duration to do stuff with it, then rewrite the config file back out to disk with changes to some of the other fieldsor, You have a REST server that accepts over HTTP, deserializes it from JSON using Serde into a Duration for validation, then re-encode to JSON and transmit forward to another server, which will deserialize and use the value. The exact string representation of the float in the original incoming input is obviously not preserved because there could've been arbitrarily many irrelevant digits there, but a desirable property is that the first Duration and the second Duration have the same value. Truncation breaks this because if 2 consecutive nanosecond quantities both truncate, then each time you deserialize+serialize, your data is gonna decrease by 1 nanosecond, potentially several times in the course of processing this request/whatever.
For me, ultimately I don't care so much about any of the "round trip" properties as brought up. Whatever you end up doing with these duration values, or whatever, nobody is going to ever observe the physical effect being off by 1 nanosecond here or there. The use case I do care about is logging. For example we have a config file containing that gets deserialized and printed to the user as our application does stuff. (I suspect config files are the dominant real-world use of , maybe followed by cross-language data transmission like JSON APIs.) The user absolutely will notice if we print , and will have a worse experience than if we manage to print their 0.7 accurately as .
The user absolutely will notice if we print 699.999999ms, and will have a worse experience than if we manage to print their 0.7 accurately as 700ms. Even with rounding such cases are absolutely possible. For example, with both rounding and truncation 0.7s represented as will be converted to 0 seconds and nanoseconds.
I understand that f32 does not have 9 complete digits of sub-second precision, and neither does f64 for durations longer than tens of millions of seconds. But f64 has way more than enough precision that we shouldn't be making these inaccuracies for all the ordinary durations that get used in the real world. f64 has enough precision that you can uniquely represent every duration up to 97 days to nanosecond precision. We really should not be messing this up in for values as small as 0.7 seconds.
Wouldn't it be better for cases like this to introduce a impl than to roundtrip through floats?
That's possible but I wouldn't consider it better than rounding in . Currently there isn't a impl on Duration so I am skeptical that we could settle on a widely suitable impl. Especially consider that the impl uses the Greek letter for "micro" which is not something you'd probably want to require someone to type in a config file.
Popping up a level here, given that there was a change in behavior that wasn't explicitly discussed by libs-api, does it make sense to revert first and then discuss later whether the change should be made? I understand it might be expedient to just try and have the discussion now, but if there's no immediately obvious consensus, then it might make sense to revert this behavior for now. (Unless I'm missing something and this behavior change was tied to something else that makes reverting difficult?)
The initially stabilized implementation was in a sense wrong, so is effectively a bug fix. I (the one who submitted both the initial implementation and ) strongly think that we either should merge or leave everything as-is. Especially consider that the Debug impl uses the Greek letter μ for "micro" which is not something you'd probably want to require someone to type in a config file. is a pretty common alternative to µs. BTW, you've used a wrong character, the correct one is µ, i.e. U+B5, not U+03BC in the Greek and Coptic block. It's one of the "fun" Unicode peculiarities.
This seems like an extremely compelling argument.
We discussed this in today's meeting, and while we don't think this is anywhere near serious enough to warrant a stable backport, it's worth fixing to round again.
Not that it's particularly important, this is not really accurate. That is, the character does exist, but according to U+03BC should be preferred, and the dedicated micro character is mostly present for compatibility:
In this context, compatibility and wider symbol availability would be a good thing. But I think that's off-topic for this issue.

Discussion:
/// // subnormal float /// let res = Duration::from_secs_f64(f64::from_bits(1)); /// assert_eq!(res, Duration::new(0, 0)); <del> /// // conversion uses truncation, not rounding </del> <ins> /// // conversion uses rounding </ins> /// let res = Duration::from_secs_f64(0.999e-9); <del> /// assert_eq!(res, Duration::new(0, 0)); </del> <ins> /// assert_eq!(res, Duration::new(0, 1)); </ins> /// ``` #[stable(feature = "duration_float", since = "1.38.0")] #[must_use]

/// let res = Duration::from_secs_f32(1e-20); /// assert_eq!(res, Duration::new(0, 0)); /// let res = Duration::from_secs_f32(4.2e-7); <del> /// assert_eq!(res, Duration::new(0, 419)); </del> <ins> /// assert_eq!(res, Duration::new(0, 420)); </ins> /// let res = Duration::from_secs_f32(2.7); <del> /// assert_eq!(res, Duration::new(2, 700_000_047)); </del> <ins> /// assert_eq!(res, Duration::new(2, 700_000_048)); </ins> /// 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)); <del> /// // conversion uses truncation, not rounding </del> <ins> /// // conversion uses rounding </ins> /// let res = Duration::from_secs_f32(0.999e-9); <del> /// assert_eq!(res, Duration::new(0, 0)); </del> <ins> /// assert_eq!(res, Duration::new(0, 1)); </ins> /// ``` #[stable(feature = "duration_float", since = "1.38.0")] #[must_use]

/// use std::time::Duration; /// /// let dur = Duration::new(2, 700_000_000); <del> /// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_640)); </del> <ins> /// assert_eq!(dur.mul_f32(3.14), Duration::new(8, 478_000_641)); </ins> /// assert_eq!(dur.mul_f32(3.14e5), Duration::new(847800, 0)); /// ``` #[stable(feature = "duration_float", since = "1.38.0")]

/// /// let dur = Duration::new(2, 700_000_000); /// assert_eq!(dur.div_f64(3.14), Duration::new(0, 859_872_611)); <del> /// // note that truncation is used, not rounding /// assert_eq!(dur.div_f64(3.14e5), Duration::new(0, 8_598)); </del> <ins> /// assert_eq!(dur.div_f64(3.14e5), Duration::new(0, 8_599)); </ins> /// ``` #[stable(feature = "duration_float", since = "1.38.0")] #[must_use = "this returns the result of the operation, 

/// 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_579)); /// // note that truncation is used, not rounding /// assert_eq!(dur.div_f32(3.14e5), Duration::new(0, 8_598)); </del> <ins> /// assert_eq!(dur.div_f32(3.14), Duration::new(0, 859_872_580)); /// assert_eq!(dur.div_f32(3.14e5), Duration::new(0, 8_599)); </ins> /// ``` #[stable(feature = "duration_float", since = "1.38.0")] #[must_use = "this returns the result of the operation, 

let mant = (bits & MANT_MASK) | (MANT_MASK + 1); let exp = ((bits >> $mant_bits) & EXP_MASK) as i16 + MIN_EXP; <del> let (secs, nanos) = if exp < -30 { // the input represents less than 1ns. </del> <ins> let (secs, nanos) = if exp < -31 { // the input represents less than 1ns and can not be rounded to it </ins> (0u64, 0u32) } else if exp < 0 { // the input is less than 1 second let t = <$double_ty>::from(mant) << ($offset + exp); <del> let nanos = (u128::from(NANOS_PER_SEC) * u128::from(t)) >> ($mant_bits + $offset); (0, nanos as u32) </del> <ins> let nanos_offset = $mant_bits + $offset; let nanos_tmp = u128::from(NANOS_PER_SEC) * u128::from(t); let nanos = (nanos_tmp >> nanos_offset) as u32; let rem_mask = (1 << nanos_offset) - 1; let rem_msb_mask = 1 << (nanos_offset - 1); let rem = nanos_tmp & rem_mask; let is_tie = rem == rem_msb_mask; let is_even = (nanos & 1) == 0; let rem_msb = nanos_tmp & rem_msb_mask == 0; let add_ns = !(rem_msb || (is_even && is_tie)); // f32 does not have enough presicion to trigger the second branch // since it can not represent numbers between 0.999_999_940_395 and 1.0. let nanos = nanos + add_ns as u32; if ($mant_bits == 23) || (nanos != NANOS_PER_SEC) { (0, nanos) } else { (1, 0) } </ins> } else if exp < $mant_bits { <del> let secs = mant >> ($mant_bits - exp); </del> <ins> let secs = u64::from(mant >> ($mant_bits - exp)); </ins> let t = <$double_ty>::from((mant << exp) & MANT_MASK); <del> let nanos = (<$double_ty>::from(NANOS_PER_SEC) * t) >> $mant_bits; (u64::from(secs), nanos as u32) </del> <ins> let nanos_offset = $mant_bits; let nanos_tmp = <$double_ty>::from(NANOS_PER_SEC) * t; let nanos = (nanos_tmp >> nanos_offset) as u32; let rem_mask = (1 << nanos_offset) - 1; let rem_msb_mask = 1 << (nanos_offset - 1); let rem = nanos_tmp & rem_mask; let is_tie = rem == rem_msb_mask; let is_even = (nanos & 1) == 0; let rem_msb = nanos_tmp & rem_msb_mask == 0; let add_ns = !(rem_msb || (is_even && is_tie)); // f32 does not have enough presicion to trigger the second branch. // For example, it can not represent numbers between 1.999_999_880... // and 2.0. Bigger values result in even smaller presicion of the // fractional part. let nanos = nanos + add_ns as u32; if ($mant_bits == 23) || (nanos != NANOS_PER_SEC) { (secs, nanos) } else { (secs + 1, 0) } </ins> } else if exp < 64 { // the input has no fractional part let secs = u64::from(mant) << (exp - $mant_bits);

/// 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); <del> /// assert_eq!(res, Ok(Duration::new(0, 419))); </del> <ins> /// assert_eq!(res, Ok(Duration::new(0, 420))); </ins> /// let res = Duration::try_from_secs_f32(2.7); <del> /// assert_eq!(res, Ok(Duration::new(2, 700_000_047))); </del> <ins> /// assert_eq!(res, Ok(Duration::new(2, 700_000_048))); </ins> /// 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))); <del> /// // conversion uses truncation, not rounding /// let res = Duration::try_from_secs_f32(0.999e-9); /// assert_eq!(res, Ok(Duration::new(0, 0))); </del> /// /// let res = Duration::try_from_secs_f32(-5.0); /// assert!(res.is_err());

/// assert!(res.is_err()); /// let res = Duration::try_from_secs_f32(2e19); /// assert!(res.is_err()); <ins> /// /// // the conversion uses rounding with tie resolution to even /// let res = Duration::try_from_secs_f32(0.999e-9); /// assert_eq!(res, Ok(Duration::new(0, 1))); /// /// // this float represents exactly 976562.5e-9 /// let val = f32::from_bits(0x3A80_0000); /// let res = Duration::try_from_secs_f32(val); /// assert_eq!(res, Ok(Duration::new(0, 976_562))); /// /// // this float represents exactly 2929687.5e-9 /// let val = f32::from_bits(0x3B40_0000); /// let res = Duration::try_from_secs_f32(val); /// assert_eq!(res, Ok(Duration::new(0, 2_929_688))); /// /// // this float represents exactly 1.000_976_562_5 /// let val = f32::from_bits(0x3F802000); /// let res = Duration::try_from_secs_f32(val); /// assert_eq!(res, Ok(Duration::new(1, 976_562))); /// /// // this float represents exactly 1.002_929_687_5 /// let val = f32::from_bits(0x3F806000); /// let res = Duration::try_from_secs_f32(val); /// assert_eq!(res, Ok(Duration::new(1, 2_929_688))); </ins> /// ``` #[unstable(feature = "duration_checked_float", issue = "83400")] #[inline]

/// // subnormal float /// let res = Duration::try_from_secs_f64(f64::from_bits(1)); /// assert_eq!(res, Ok(Duration::new(0, 0))); <del> /// // conversion uses truncation, not rounding /// let res = Duration::try_from_secs_f32(0.999e-9); /// assert_eq!(res, Ok(Duration::new(0, 0))); </del> /// /// let res = Duration::try_from_secs_f64(-5.0); /// assert!(res.is_err());

/// assert!(res.is_err()); /// let res = Duration::try_from_secs_f64(2e19); /// assert!(res.is_err()); <ins> /// /// // the conversion uses rounding with tie resolution to even /// let res = Duration::try_from_secs_f64(0.999e-9); /// assert_eq!(res, Ok(Duration::new(0, 1))); /// let res = Duration::try_from_secs_f64(0.999_999_999_499); /// assert_eq!(res, Ok(Duration::new(0, 999_999_999))); /// let res = Duration::try_from_secs_f64(0.999_999_999_501); /// assert_eq!(res, Ok(Duration::new(1, 0))); /// let res = Duration::try_from_secs_f64(42.999_999_999_499); /// assert_eq!(res, Ok(Duration::new(42, 999_999_999))); /// let res = Duration::try_from_secs_f64(42.999_999_999_501); /// assert_eq!(res, Ok(Duration::new(43, 0))); /// /// // this float represents exactly 976562.5e-9 /// let val = f64::from_bits(0x3F50_0000_0000_0000); /// let res = Duration::try_from_secs_f64(val); /// assert_eq!(res, Ok(Duration::new(0, 976_562))); /// /// // this float represents exactly 2929687.5e-9 /// let val = f64::from_bits(0x3F68_0000_0000_0000); /// let res = Duration::try_from_secs_f64(val); /// assert_eq!(res, Ok(Duration::new(0, 2_929_688))); /// /// // this float represents exactly 1.000_976_562_5 /// let val = f64::from_bits(0x3FF0_0400_0000_0000); /// let res = Duration::try_from_secs_f64(val); /// assert_eq!(res, Ok(Duration::new(1, 976_562))); /// /// // this float represents exactly 1.002_929_687_5 /// let val = f64::from_bits(0x3_FF00_C000_0000_000); /// let res = Duration::try_from_secs_f64(val); /// assert_eq!(res, Ok(Duration::new(1, 2_929_688))); </ins> /// ``` #[unstable(feature = "duration_checked_float", issue = "83400")] #[inline]