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