Skip to main content

tuwunel_core/utils/
time.rs

1pub mod exponential_backoff;
2
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use crate::{Result, err};
6
7#[inline]
8#[must_use]
9#[expect(clippy::as_conversions, clippy::cast_possible_truncation)]
10pub fn now_millis() -> u64 { now().as_millis() as u64 }
11
12#[inline]
13#[must_use]
14pub fn now_secs() -> u64 { now().as_secs() }
15
16#[inline]
17#[must_use]
18pub fn now() -> Duration {
19	UNIX_EPOCH
20		.elapsed()
21		.expect("positive duration after epoch")
22}
23
24#[inline]
25#[must_use]
26pub fn duration_since_epoch(timepoint: SystemTime) -> Duration {
27	timepoint
28		.duration_since(UNIX_EPOCH)
29		.unwrap_or(Duration::ZERO)
30}
31
32#[inline]
33pub fn timepoint_from_epoch(duration: Duration) -> Result<SystemTime> {
34	UNIX_EPOCH
35		.checked_add(duration)
36		.ok_or_else(|| err!(Arithmetic("Duration {duration:?} from epoch is too large")))
37}
38
39#[inline]
40pub fn timepoint_from_now(duration: Duration) -> Result<SystemTime> {
41	SystemTime::now()
42		.checked_add(duration)
43		.ok_or_else(|| err!(Arithmetic("Duration {duration:?} from now is too large")))
44}
45
46#[inline]
47pub fn timepoint_ago(duration: Duration) -> Result<SystemTime> {
48	SystemTime::now()
49		.checked_sub(duration)
50		.ok_or_else(|| err!(Arithmetic("Duration {duration:?} ago is too large")))
51}
52
53#[inline]
54pub fn parse_timepoint_ago(ago: &str) -> Result<SystemTime> {
55	timepoint_ago(parse_duration(ago)?)
56}
57
58#[inline]
59pub fn parse_duration(duration: &str) -> Result<Duration> {
60	cyborgtime::parse_duration(duration)
61		.map_err(|error| err!("'{duration:?}' is not a valid duration string: {error:?}"))
62}
63
64#[inline]
65#[must_use]
66pub fn timepoint_has_passed(timepoint: SystemTime) -> bool {
67	SystemTime::now()
68		.duration_since(timepoint)
69		.is_ok()
70}
71
72#[must_use]
73pub fn rfc2822_from_seconds(epoch: i64) -> String {
74	use chrono::{DateTime, Utc};
75
76	DateTime::<Utc>::from_timestamp(epoch, 0)
77		.unwrap_or_default()
78		.to_rfc2822()
79}
80
81#[must_use]
82pub fn format(ts: SystemTime, str: &str) -> String {
83	use chrono::{DateTime, Utc};
84
85	let dt: DateTime<Utc> = ts.into();
86	dt.format(str).to_string()
87}
88
89#[must_use]
90#[expect(
91	clippy::as_conversions,
92	clippy::cast_possible_truncation,
93	clippy::cast_sign_loss
94)]
95pub fn pretty(d: Duration) -> String {
96	use Unit::*;
97
98	let fmt = |w, f, u| format!("{w}.{f} {u}");
99	let gen64 = |w, f, u| fmt(w, (f * 100.0) as u32, u);
100	let gen128 = |w, f, u| gen64(u64::try_from(w).expect("u128 to u64"), f, u);
101	match whole_and_frac(d) {
102		| (Days(whole), frac) => gen64(whole, frac, "days"),
103		| (Hours(whole), frac) => gen64(whole, frac, "hours"),
104		| (Mins(whole), frac) => gen64(whole, frac, "minutes"),
105		| (Secs(whole), frac) => gen64(whole, frac, "seconds"),
106		| (Millis(whole), frac) => gen128(whole, frac, "milliseconds"),
107		| (Micros(whole), frac) => gen128(whole, frac, "microseconds"),
108		| (Nanos(whole), frac) => gen128(whole, frac, "nanoseconds"),
109	}
110}
111
112/// Return a pair of (whole part, frac part) from a duration where. The whole
113/// part is the largest Unit containing a non-zero value, the frac part is a
114/// rational remainder left over.
115#[must_use]
116#[expect(clippy::as_conversions, clippy::cast_precision_loss)]
117pub fn whole_and_frac(d: Duration) -> (Unit, f64) {
118	use Unit::*;
119
120	let whole = whole_unit(d);
121	(whole, match whole {
122		| Days(_) => (d.as_secs() % 86_400) as f64 / 86_400.0,
123		| Hours(_) => (d.as_secs() % 3_600) as f64 / 3_600.0,
124		| Mins(_) => (d.as_secs() % 60) as f64 / 60.0,
125		| Secs(_) => f64::from(d.subsec_millis()) / 1000.0,
126		| Millis(_) => f64::from(d.subsec_micros()) / 1000.0,
127		| Micros(_) => f64::from(d.subsec_nanos()) / 1000.0,
128		| Nanos(_) => 0.0,
129	})
130}
131
132/// Return the largest Unit which represents the duration. The value is
133/// rounded-down, but never zero.
134#[must_use]
135pub fn whole_unit(d: Duration) -> Unit {
136	use Unit::*;
137
138	match d.as_secs() {
139		| 86_400.. => Days(d.as_secs() / 86_400),
140		| 3_600..=86_399 => Hours(d.as_secs() / 3_600),
141		| 60..=3_599 => Mins(d.as_secs() / 60),
142		| _ => match d.as_micros() {
143			| 1_000_000.. => Secs(d.as_secs()),
144			| 1_000..=999_999 => Millis(d.subsec_millis().into()),
145			| _ => match d.as_nanos() {
146				| 1_000.. => Micros(d.subsec_micros().into()),
147				| _ => Nanos(d.subsec_nanos().into()),
148			},
149		},
150	}
151}
152
153#[derive(Eq, PartialEq, Clone, Copy, Debug)]
154pub enum Unit {
155	Days(u64),
156	Hours(u64),
157	Mins(u64),
158	Secs(u64),
159	Millis(u128),
160	Micros(u128),
161	Nanos(u128),
162}