time: Test and document time precision edge-case

There is a slight edge case when adding and subtracting a `Duration`
from a `SystemTime`, namely when the duration itself is finer/smaller
than the time precision on the operating systems.

On most (if not all non-Windows) operating systems, the precision of
`Duration` aligns with the `SystemTime`, both being one nanosecond.

However, on Windows, this time precision is 100ns, meaning that adding
or subtracting a `Duration` whose value is `< Duration::new(0, 100)`
will result in that method behaving like an addition/subtracting of
`Duration::ZERO`, due to the `Duration` getting rounded-down to the zero
value.
This commit is contained in:
Clara Engler 2025-12-13 10:12:17 +01:00
parent ac5c70ad4d
commit 1b9b4f4dc6
2 changed files with 38 additions and 11 deletions

View file

@ -514,7 +514,9 @@ impl SystemTime {
/// Represents the maximum value representable by [`SystemTime`] on this platform.
///
/// This value differs a lot between platforms, but it is always the case
/// that any positive addition to [`SystemTime::MAX`] will fail.
/// that any positive addition of a [`Duration`], whose value is greater
/// than or equal to the time precision of the operating system, to
/// [`SystemTime::MAX`] will fail.
///
/// # Examples
///
@ -525,8 +527,13 @@ impl SystemTime {
/// // Adding zero will change nothing.
/// assert_eq!(SystemTime::MAX.checked_add(Duration::ZERO), Some(SystemTime::MAX));
///
/// // But adding just 1ns will already fail.
/// assert_eq!(SystemTime::MAX.checked_add(Duration::new(0, 1)), None);
/// // But adding just one second will already fail ...
/// //
/// // Keep in mind that this in fact may succeed, if the Duration is
/// // smaller than the time precision of the operating system, which
/// // happens to be 1ns on most operating systems, with Windows being the
/// // notable exception by using 100ns, hence why this example uses 1s.
/// assert_eq!(SystemTime::MAX.checked_add(Duration::new(1, 0)), None);
///
/// // Utilize this for saturating arithmetic to improve error handling.
/// // In this case, we will use a certificate with a timestamp in the
@ -543,7 +550,9 @@ impl SystemTime {
/// Represents the minimum value representable by [`SystemTime`] on this platform.
///
/// This value differs a lot between platforms, but it is always the case
/// that any positive subtraction from [`SystemTime::MIN`] will fail.
/// that any positive subtraction of a [`Duration`] from, whose value is
/// greater than or equal to the time precision of the operating system, to
/// [`SystemTime::MIN`] will fail.
///
/// Depending on the platform, this may be either less than or equal to
/// [`SystemTime::UNIX_EPOCH`], depending on whether the operating system
@ -560,8 +569,13 @@ impl SystemTime {
/// // Subtracting zero will change nothing.
/// assert_eq!(SystemTime::MIN.checked_sub(Duration::ZERO), Some(SystemTime::MIN));
///
/// // But subtracting just 1ns will already fail.
/// assert_eq!(SystemTime::MIN.checked_sub(Duration::new(0, 1)), None);
/// // But subtracting just one second will already fail.
/// //
/// // Keep in mind that this in fact may succeed, if the Duration is
/// // smaller than the time precision of the operating system, which
/// // happens to be 1ns on most operating systems, with Windows being the
/// // notable exception by using 100ns, hence why this example uses 1s.
/// assert_eq!(SystemTime::MIN.checked_sub(Duration::new(1, 0)), None);
///
/// // Utilize this for saturating arithmetic to improve error handling.
/// // In this case, we will use a cache expiry as a practical example.
@ -651,6 +665,9 @@ impl SystemTime {
/// Returns `Some(t)` where `t` is the time `self + duration` if `t` can be represented as
/// `SystemTime` (which means it's inside the bounds of the underlying data structure), `None`
/// otherwise.
///
/// In the case that the `duration` is smaller than the time precision of the operating
/// system, `Some(self)` will be returned.
#[stable(feature = "time_checked_add", since = "1.34.0")]
pub fn checked_add(&self, duration: Duration) -> Option<SystemTime> {
self.0.checked_add_duration(&duration).map(SystemTime)
@ -659,6 +676,9 @@ impl SystemTime {
/// Returns `Some(t)` where `t` is the time `self - duration` if `t` can be represented as
/// `SystemTime` (which means it's inside the bounds of the underlying data structure), `None`
/// otherwise.
///
/// In the case that the `duration` is smaller than the time precision of the operating
/// system, `Some(self)` will be returned.
#[stable(feature = "time_checked_add", since = "1.34.0")]
pub fn checked_sub(&self, duration: Duration) -> Option<SystemTime> {
self.0.checked_sub_duration(&duration).map(SystemTime)

View file

@ -250,15 +250,22 @@ fn system_time_duration_since_max_range_on_unix() {
#[test]
fn system_time_max_min() {
#[cfg(not(target_os = "windows"))]
/// Most (all?) non-Windows systems have nanosecond precision.
const MIN_INTERVAL: Duration = Duration::new(0, 1);
#[cfg(target_os = "windows")]
/// Windows' time precision is at 100ns.
const MIN_INTERVAL: Duration = Duration::new(0, 100);
// First, test everything with checked_* and Duration::ZERO.
assert_eq!(SystemTime::MAX.checked_add(Duration::ZERO), Some(SystemTime::MAX));
assert_eq!(SystemTime::MAX.checked_sub(Duration::ZERO), Some(SystemTime::MAX));
assert_eq!(SystemTime::MIN.checked_add(Duration::ZERO), Some(SystemTime::MIN));
assert_eq!(SystemTime::MIN.checked_sub(Duration::ZERO), Some(SystemTime::MIN));
// Now do the same again with checked_* but try by ± a single nanosecond.
assert!(SystemTime::MAX.checked_add(Duration::new(0, 1)).is_none());
assert!(SystemTime::MAX.checked_sub(Duration::new(0, 1)).is_some());
assert!(SystemTime::MIN.checked_add(Duration::new(0, 1)).is_some());
assert!(SystemTime::MIN.checked_sub(Duration::new(0, 1)).is_none());
// Now do the same again with checked_* but try by ± the lowest time precision.
assert!(SystemTime::MAX.checked_add(MIN_INTERVAL).is_none());
assert!(SystemTime::MAX.checked_sub(MIN_INTERVAL).is_some());
assert!(SystemTime::MIN.checked_add(MIN_INTERVAL).is_some());
assert!(SystemTime::MIN.checked_sub(MIN_INTERVAL).is_none());
}