A Duration cannot be negative¶
Level: 201 · working knowledge
One line: std::time::Duration is whole seconds plus nanoseconds, unsigned, and one type for every unit — so there is no -1s to hold, and a subtraction that would make one is a None if you ask with checked_sub and a panic if you don't.
use std::time::Duration;
fn main() {
let d = Duration::from_millis(1_999);
println!("{d:?}"); // 1.999s
println!("{}", d.as_secs()); // 1
println!("{}", d.as_millis()); // 1999
let one = Duration::from_secs(1);
let two = Duration::from_secs(2);
println!("{:?}", one.checked_sub(two)); // None
}
One type, whole seconds plus nanoseconds¶
A Duration is two fields — in std's source, secs: u64 and nanos, with the comment "Always 0 <= nanos < NANOS_PER_SEC". Every constructor fills the same two, so from_secs(2) and from_millis(2_000) are the same value and compare equal. A millisecond is not a different type from a second, only a different way of writing one down.
Reading it back is where the units return, and each accessor answers a different question:
| Method | Returns | For 1.999 s |
|---|---|---|
as_secs ↗ |
u64, whole seconds |
1 — the fraction is dropped, not rounded |
subsec_nanos ↗ |
u32, the fraction alone |
999000000 |
as_secs_f64 ↗ |
f64 |
1.999 |
as_millis ↗ |
u128, whole milliseconds |
1999 |
as_secs is the trap in that table. Its docs say the value "does not include the fractional (nanosecond) part of the duration", so a 1.999-second timing logged with it reads 1. And as_millis returns a u128 because a u64 would not hold every answer: the output below prints Duration::MAX in milliseconds beside u64::MAX, and it is a thousand times larger.
There is no Display, either. The docs say that is deliberate — "there are a variety of ways to format spans of time for human readability" — and give you a Debug that picks the unit itself: 90s, 1.5s, 250µs. The µ is not ASCII, which the docs mention in case your output goes somewhere that cannot take it.
Below zero: ask, clamp, or panic¶
Unsigned means the subtraction has nowhere to put a negative answer, so std makes you say what you want instead:
| You write | When the result would be negative, you get |
|---|---|
a.checked_sub(b) ↗ |
None — "if the result would be negative or if overflow occurred" |
a.saturating_sub(b) ↗ |
Duration::ZERO |
a.abs_diff(b) ↗ |
the gap, whichever is larger (stable since 1.81) |
a - b |
a panic: overflow when subtracting durations |
The last row panics in a release build too, which makes it different from integer overflow — that panics in debug and wraps in release. Duration's Sub is written self.checked_sub(rhs).expect("overflow when subtracting durations"), and an expect does not care about the build profile:
one - two = panicked: overflow when subtracting durations
The C++ side: a signed count and a unit¶
C++ makes the opposite choice on both counts. A std::chrono::duration<Rep, Period> is a count of type Rep — a signed integer in every standard alias — and a unit, Period, that is part of the type. seconds and milliseconds are different types, and 1s - 2s is simply -1s:
#include <chrono>
#include <cstdio>
using namespace std::chrono_literals;
namespace sc = std::chrono;
int main() {
auto d = 1s - 2s;
std::printf("1s - 2s = %llds\n", static_cast<long long>(d.count()));
sc::milliseconds ms = 3s;
std::printf("milliseconds ms = 3s = %lldms\n", static_cast<long long>(ms.count()));
auto s = sc::duration_cast<sc::seconds>(1999ms);
std::printf("duration_cast<seconds>(1999ms) = %llds\n", static_cast<long long>(s.count()));
}
1s - 2s = -1s
milliseconds ms = 3s = 3000ms
duration_cast<seconds>(1999ms) = 1s
Converting to the finer unit is implicit, because nothing is lost. Converting to the coarser one does not compile until you ask:
#include <chrono>
namespace sc = std::chrono;
int main() {
sc::seconds s = sc::milliseconds{1500};
}
narrowing.cpp: In function 'int main()':
narrowing.cpp:5:25: error: conversion from 'duration<[...],ratio<[...],1000>>' to non-scalar type 'duration<[...],ratio<[...],1>>' requested
5 | sc::seconds s = sc::milliseconds{1500};
| ^~~~~~~~~~~~~~~~~~
That refusal is the one thing C++ checks here that Rust does not. duration_cast<seconds> truncates 1999 ms to 1s exactly as as_secs does, but C++ makes you write the cast to get there, and Rust's as_secs is one method call away with nothing in the type to slow you down. What Rust checks instead is the sign: C++'s -1s is a value you can carry around; Rust makes you decide, at the subtraction, what a negative answer should become.
The verified output¶
Verified output of a_duration_cannot_be_negative.rs — regenerated by tools/run_examples.py, never hand-typed.
1. One type: whole seconds plus nanoseconds
Duration::from_millis(1_999) = 1.999s
.as_secs() = 1 whole seconds: the fraction is dropped
.subsec_nanos() = 999000000 the fraction, on its own
.as_secs_f64() = 1.999 an f64
.as_millis() = 1999 a u128
from_secs(2) == from_millis(2_000) true
2. {:?} picks the unit; there is no Display
90s
1.5s
250µs
7ns
0ns
3. Why as_millis returns a u128
Duration::MAX = 18446744073709551615.999999999s
Duration::MAX.as_millis() = 18446744073709551615999
u64::MAX = 18446744073709551615
4. Below zero: ask, clamp, or panic
one.checked_sub(two) = None
one.saturating_sub(two) = 0ns
one.abs_diff(two) = 1s
one - two = panicked: overflow when subtracting durations
If you are coming from another language¶
- C++. The section above: a signed count with the unit in the type, against an unsigned count with the unit in the method name. The C++ side of this page is A duration is a count and a unit ↗.
- Python.
datetime.timedeltacan be negative, and normalises the sign into its days:timedelta(seconds=-1)isdatetime.timedelta(days=-1, seconds=86399)— minus one day, plus 86,399 seconds — andtimedelta(seconds=1) - timedelta(seconds=2)prints-1 day, 23:59:59. A plain difference of twotime.monotonic()floats can be negative too. Rust has neither, which moves "what if it is negative?" from a value you might mishandle later to aNoneyou handle now.timedelta.total_seconds()isas_secs_f64; there is no truncatingas_secsin Python to be surprised by. - ABAP. (Not machine-checked — CI cannot run ABAP; this is from the ABAP keyword documentation.) There is no duration type. The difference of two
GET RUN TIME FIELDreadings is anicounting microseconds, and it goes negative without complaint if you subtract in the wrong order; the unit lives in the variable's name, if anywhere — the convention C++ turned into a type and Rust turned into method names.
See also¶
- An
Instantis not aSystemTime— where the negative case goes instead: anErr, or aNone - Signed overflow — the integer version of say which behaviour you meant, with the same
checked_andsaturating_names OptionvsResult— whychecked_subanswers with anOption- What a panic costs — the
catch_unwindthe example uses to show the panic without dying of it
Po polsku¶
Duration to po polsku czas trwania (duration) i w Ruscie jest to jeden typ dla wszystkich jednostek: w środku są dwie liczby bez znaku (unsigned) — całe sekundy jako u64 i nanosekundy jako część ułamkowa. Milisekunda nie jest osobnym typem, tylko innym sposobem zapisania tej samej wartości, więc from_secs(2) == from_millis(2_000).
Skoro typ jest bez znaku, ujemnego czasu trwania nie ma. Odejmowanie, które dałoby wynik poniżej zera, trzeba nazwać: checked_sub zwraca None, saturating_sub — arytmetyka z nasyceniem (saturating arithmetic) — zwraca zero, abs_diff zwraca różnicę bez względu na kolejność, a zwykłe a - b panikuje z komunikatem overflow when subtracting durations. W odróżnieniu od przepełnienia liczb całkowitych ta panika zdarza się także w buildzie --release, bo implementacja to zwykłe expect.
C++ robi odwrotnie w obu sprawach: std::chrono::duration<Rep, Period> ma licznik ze znakiem i jednostkę w typie, więc 1s - 2s to po prostu -1s, a zamiana milisekund na sekundy wymaga jawnego duration_cast. W Ruscie as_secs obcina część ułamkową bez żadnego rzutowania (cast) — 1,999 s daje 1 — i to jest pułapka warta zapamiętania przy logowaniu pomiarów.
Szukaj po polsku: czas trwania w Ruscie · arytmetyka z nasyceniem · rust Duration checked_sub · rust Duration as_secs truncate · std::chrono::duration_cast