Skip to main content

tuwunel_core/utils/
time.rs

1//! Wall-clock conversion, parsing, and duration-formatting utilities.
2//!
3//! The helpers convert between `SystemTime` and the Unix epoch, parse
4//! human-readable durations, and choose display units. These clocks are not
5//! monotonic and can be affected by system-time changes.
6
7mod elapsed;
8pub mod exponential_backoff;
9
10use std::time::{Duration, SystemTime, UNIX_EPOCH};
11
12pub use elapsed::Elapsed;
13
14use crate::{Result, err};
15
16/// Returns the current wall-clock time as whole milliseconds since the Unix
17/// epoch.
18///
19/// The submillisecond remainder is discarded, and counts above `u64::MAX`
20/// retain only their low 64 bits. The function panics if the current system
21/// clock is earlier than the epoch.
22#[inline]
23#[must_use]
24#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
25pub fn now_millis() -> u64 { now().as_millis() as u64 }
26
27/// Returns the current wall-clock time as whole seconds since the Unix epoch.
28///
29/// The subsecond remainder is discarded. The function panics if the current
30/// system clock is earlier than the epoch.
31#[inline]
32#[must_use]
33pub fn now_secs() -> u64 { now().as_secs() }
34
35/// Returns the current wall-clock duration since the Unix epoch.
36///
37/// The value comes from `SystemTime` and is not monotonic. The function panics
38/// if the current system clock is earlier than the epoch.
39#[inline]
40#[must_use]
41pub fn now() -> Duration {
42	UNIX_EPOCH
43		.elapsed()
44		.expect("positive duration after epoch")
45}
46
47/// Converts a system time into a nonnegative duration since the Unix epoch.
48///
49/// Times before the epoch saturate to [`Duration::ZERO`]. Times at or after the
50/// epoch preserve their full representable duration.
51#[inline]
52#[must_use]
53pub fn duration_since_epoch(timepoint: SystemTime) -> Duration {
54	timepoint
55		.duration_since(UNIX_EPOCH)
56		.unwrap_or(Duration::ZERO)
57}
58
59/// Adds a duration to the Unix epoch using checked arithmetic.
60///
61/// The resulting system time is returned when representable. An arithmetic
62/// error is returned when the duration exceeds the platform's `SystemTime`
63/// range.
64#[inline]
65pub fn timepoint_from_epoch(duration: Duration) -> Result<SystemTime> {
66	UNIX_EPOCH
67		.checked_add(duration)
68		.ok_or_else(|| err!(Arithmetic("Duration {duration:?} from epoch is too large")))
69}
70
71/// Adds a duration to the current wall-clock time using checked arithmetic.
72///
73/// The current time is sampled once for the calculation. An arithmetic error is
74/// returned when the result exceeds the platform's `SystemTime` range.
75#[inline]
76pub fn timepoint_from_now(duration: Duration) -> Result<SystemTime> {
77	SystemTime::now()
78		.checked_add(duration)
79		.ok_or_else(|| err!(Arithmetic("Duration {duration:?} from now is too large")))
80}
81
82/// Subtracts a duration from the current wall-clock time using checked
83/// arithmetic.
84///
85/// The current time is sampled once for the calculation. An arithmetic error is
86/// returned when the result precedes the platform's `SystemTime` range.
87#[inline]
88pub fn timepoint_ago(duration: Duration) -> Result<SystemTime> {
89	SystemTime::now()
90		.checked_sub(duration)
91		.ok_or_else(|| err!(Arithmetic("Duration {duration:?} ago is too large")))
92}
93
94/// Parses a duration and returns the wall-clock time that far in the past.
95///
96/// Input syntax is delegated to [`parse_duration`]. Parsing and
97/// checked-subtraction errors are propagated.
98#[inline]
99pub fn parse_timepoint_ago(ago: &str) -> Result<SystemTime> {
100	timepoint_ago(parse_duration(ago)?)
101}
102
103/// Parses a human-readable duration with the `cyborgtime` parser.
104///
105/// Successful input is returned as a standard [`Duration`]. Parser failures are
106/// wrapped with the original input for context.
107#[inline]
108pub fn parse_duration(duration: &str) -> Result<Duration> {
109	cyborgtime::parse_duration(duration)
110		.map_err(|error| err!("'{duration:?}' is not a valid duration string: {error:?}"))
111}
112
113/// Checks whether a system time is at or before the current wall-clock time.
114///
115/// Equality is considered passed. A time later than the sampled current time
116/// returns `false`.
117#[inline]
118#[must_use]
119pub fn timepoint_has_passed(timepoint: SystemTime) -> bool {
120	SystemTime::now()
121		.duration_since(timepoint)
122		.is_ok()
123}
124
125/// Formats a signed Unix timestamp as RFC 2822 text in UTC.
126///
127/// Timestamps outside Chrono's supported range use its default UTC date and
128/// time before formatting.
129#[must_use]
130pub fn rfc2822_from_seconds(epoch: i64) -> String {
131	use chrono::{DateTime, Utc};
132
133	DateTime::<Utc>::from_timestamp(epoch, 0)
134		.unwrap_or_default()
135		.to_rfc2822()
136}
137
138/// Formats a system time in UTC with a Chrono format string.
139///
140/// The pattern is passed to Chrono without modification. The rendered value is
141/// returned as an owned string.
142#[must_use]
143pub fn format(ts: SystemTime, str: &str) -> String {
144	use chrono::{DateTime, Utc};
145
146	let dt: DateTime<Utc> = ts.into();
147	dt.format(str).to_string()
148}
149
150/// Formats a duration with one plural human-readable unit.
151///
152/// The unit and scale component come from [`whole_and_frac`]. Output has the
153/// form `{whole}.{scaled} {unit}`, where `scaled` is the component multiplied
154/// by 100, truncated to an integer, and zero-padded to two digits.
155#[must_use]
156#[expect(
157	clippy::as_conversions,
158	clippy::cast_possible_truncation,
159	clippy::cast_sign_loss
160)]
161pub fn pretty(d: Duration) -> String {
162	use Unit::*;
163
164	let fmt = |w, f, u| format!("{w}.{f:02} {u}");
165	let gen64 = |w, f, u| fmt(w, (f * 100.0) as u32, u);
166	let gen128 = |w, f, u| gen64(u64::try_from(w).expect("u128 to u64"), f, u);
167	match whole_and_frac(d) {
168		| (Days(whole), frac) => gen64(whole, frac, "days"),
169		| (Hours(whole), frac) => gen64(whole, frac, "hours"),
170		| (Mins(whole), frac) => gen64(whole, frac, "minutes"),
171		| (Secs(whole), frac) => gen64(whole, frac, "seconds"),
172		| (Millis(whole), frac) => gen128(whole, frac, "milliseconds"),
173		| (Micros(whole), frac) => gen128(whole, frac, "microseconds"),
174		| (Nanos(whole), frac) => gen128(whole, frac, "nanoseconds"),
175	}
176}
177
178/// Pairs a duration's selected whole unit with its fraction in `[0, 1)`.
179///
180/// For days through minutes, the second value is a whole-second remainder
181/// divided by the selected unit, so subsecond data is discarded. Seconds use
182/// milliseconds within the current second, discarding submillisecond data;
183/// milliseconds use microseconds, discarding nanoseconds; microseconds use
184/// nanoseconds; nanoseconds return `0.0`.
185#[must_use]
186#[expect(clippy::as_conversions, clippy::cast_precision_loss)]
187pub fn whole_and_frac(d: Duration) -> (Unit, f64) {
188	use Unit::*;
189
190	let whole = whole_unit(d);
191	(whole, match whole {
192		| Days(_) => (d.as_secs() % 86_400) as f64 / 86_400.0,
193		| Hours(_) => (d.as_secs() % 3_600) as f64 / 3_600.0,
194		| Mins(_) => (d.as_secs() % 60) as f64 / 60.0,
195		| Secs(_) => f64::from(d.subsec_millis()) / 1000.0,
196		| Millis(_) => f64::from(d.subsec_micros() % 1000) / 1000.0,
197		| Micros(_) => f64::from(d.subsec_nanos() % 1000) / 1000.0,
198		| Nanos(_) => 0.0,
199	})
200}
201
202/// Selects the largest integral unit represented by a duration.
203///
204/// The stored value is rounded down to a whole unit. A zero duration is
205/// represented as `Unit::Nanos(0)`.
206#[must_use]
207pub fn whole_unit(d: Duration) -> Unit {
208	use Unit::*;
209
210	match d.as_secs() {
211		| 86_400.. => Days(d.as_secs() / 86_400),
212		| 3_600..=86_399 => Hours(d.as_secs() / 3_600),
213		| 60..=3_599 => Mins(d.as_secs() / 60),
214		| _ => match d.as_micros() {
215			| 1_000_000.. => Secs(d.as_secs()),
216			| 1_000..=999_999 => Millis(d.subsec_millis().into()),
217			| _ => match d.as_nanos() {
218				| 1_000.. => Micros(d.subsec_micros().into()),
219				| _ => Nanos(d.subsec_nanos().into()),
220			},
221		},
222	}
223}
224
225/// Represents an integral duration in one selected unit.
226///
227/// Each variant stores the whole count for its named unit. [`whole_unit`]
228/// selects the largest unit with a nonzero count, except that zero is
229/// represented in nanoseconds.
230#[derive(Eq, PartialEq, Clone, Copy, Debug)]
231pub enum Unit {
232	/// A duration measured in whole 86,400-second days.
233	///
234	/// [`whole_unit`] selects this variant for durations of at least one day.
235	Days(u64),
236
237	/// A duration measured in whole hours.
238	///
239	/// [`whole_unit`] selects this variant below one day and at or above one
240	/// hour.
241	Hours(u64),
242
243	/// A duration measured in whole minutes.
244	///
245	/// [`whole_unit`] selects this variant below one hour and at or above one
246	/// minute.
247	Mins(u64),
248
249	/// A duration measured in whole seconds.
250	///
251	/// [`whole_unit`] selects this variant below one minute and at or above one
252	/// second.
253	Secs(u64),
254
255	/// A duration measured in whole milliseconds.
256	///
257	/// [`whole_unit`] selects this variant below one second and at or above one
258	/// millisecond.
259	Millis(u128),
260
261	/// A duration measured in whole microseconds.
262	///
263	/// [`whole_unit`] selects this variant below one millisecond and at or
264	/// above one microsecond.
265	Micros(u128),
266
267	/// A duration measured in whole nanoseconds.
268	///
269	/// [`whole_unit`] selects this variant below one microsecond, including for
270	/// zero.
271	Nanos(u128),
272}