relay_common/
time.rs

1//! Utilities to deal with date-time types. (DateTime, Instant, SystemTime, etc)
2
3use std::fmt;
4use std::time::{Duration, Instant, SystemTime};
5
6use chrono::{DateTime, TimeZone, Utc};
7use serde::{Deserialize, Serialize};
8
9/// Converts an `Instant` into a `SystemTime`.
10pub fn instant_to_system_time(instant: Instant) -> SystemTime {
11    SystemTime::now() - instant.elapsed()
12}
13
14/// Returns the number of milliseconds contained by this `Duration` as `f64`.
15///
16/// The returned value does include the fractional (nanosecond) part of the duration.
17///
18/// # Example
19///
20/// ```
21/// use std::time::Duration;
22///
23/// let duration = Duration::from_nanos(2_125_000);
24/// let millis = relay_common::time::duration_to_millis(duration);
25/// assert_eq!(millis, 2.125);
26/// ```
27pub fn duration_to_millis(duration: Duration) -> f64 {
28    (duration.as_secs_f64() * 1_000_000_000f64).round() / 1_000_000f64
29}
30
31/// Returns the positive number of milliseconds contained by this `Duration` as `f64`.
32///
33/// The returned value does include the fractional (nanosecond) part of the duration. If the
34/// duration is negative, this returns `0.0`;
35///
36/// # Example
37///
38/// ```
39/// use chrono::Duration;
40///
41/// let duration = Duration::nanoseconds(2_125_000);
42/// let millis = relay_common::time::chrono_to_positive_millis(duration);
43/// assert_eq!(millis, 2.125);
44/// ```
45///
46/// Negative durations are clamped to `0.0`:
47///
48/// ```
49/// use chrono::Duration;
50///
51/// let duration = Duration::nanoseconds(-2_125_000);
52/// let millis = relay_common::time::chrono_to_positive_millis(duration);
53/// assert_eq!(millis, 0.0);
54/// ```
55pub fn chrono_to_positive_millis(duration: chrono::Duration) -> f64 {
56    duration_to_millis(duration.to_std().unwrap_or_default())
57}
58
59/// A unix timestamp (full seconds elapsed since 1970-01-01 00:00 UTC).
60#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
61pub struct UnixTimestamp(u64);
62
63impl UnixTimestamp {
64    /// Creates a unix timestamp from the given number of seconds.
65    pub const fn from_secs(secs: u64) -> Self {
66        Self(secs)
67    }
68
69    /// Creates a unix timestamp from the given system time.
70    pub fn from_system(time: SystemTime) -> Self {
71        let duration = time
72            .duration_since(SystemTime::UNIX_EPOCH)
73            .unwrap_or_default()
74            .as_secs();
75
76        Self(duration)
77    }
78
79    /// Creates a unix timestamp from the given chrono `DateTime`.
80    ///
81    /// Returns `Some` if this is a valid date time starting with 1970-01-01 00:00 UTC. If the date
82    /// lies before the UNIX epoch, this function returns `None`.
83    pub fn from_datetime(date_time: DateTime<impl TimeZone>) -> Option<Self> {
84        let timestamp = date_time.timestamp();
85        if timestamp >= 0 {
86            Some(UnixTimestamp::from_secs(timestamp as u64))
87        } else {
88            None
89        }
90    }
91
92    /// Converts the given `Instant` into a UNIX timestamp.
93    ///
94    /// This is done by comparing the `Instant` with the current system time. Note that the system
95    /// time is subject to skew, so subsequent calls to `from_instant` may return different values.
96    #[inline]
97    pub fn from_instant(instant: Instant) -> Self {
98        Self::from_system(instant_to_system_time(instant))
99    }
100
101    /// Returns the current timestamp.
102    #[inline]
103    pub fn now() -> Self {
104        Self::from_system(SystemTime::now())
105    }
106
107    /// Returns the number of seconds since the UNIX epoch start.
108    pub fn as_secs(self) -> u64 {
109        self.0
110    }
111
112    /// Returns the number of nanoseconds since the UNIX epoch start. Precision limited to seconds.
113    pub fn as_nanos(self) -> u64 {
114        self.as_secs() * 1_000_000_000
115    }
116
117    /// Returns the timestamp as chrono datetime.
118    pub fn as_datetime(self) -> Option<DateTime<Utc>> {
119        DateTime::from_timestamp(self.0 as i64, 0)
120    }
121}
122
123impl fmt::Debug for UnixTimestamp {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        write!(f, "UnixTimestamp({})", self.as_secs())
126    }
127}
128
129impl fmt::Display for UnixTimestamp {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        self.as_secs().fmt(f)
132    }
133}
134
135/// Adds _whole_ seconds of the given duration to the timestamp.
136impl std::ops::Add<Duration> for UnixTimestamp {
137    type Output = Self;
138
139    fn add(self, rhs: Duration) -> Self::Output {
140        Self(self.0.saturating_add(rhs.as_secs()))
141    }
142}
143
144impl std::ops::Sub for UnixTimestamp {
145    type Output = Duration;
146
147    fn sub(self, rhs: Self) -> Self::Output {
148        Duration::from_secs(self.0 - rhs.0)
149    }
150}
151
152#[derive(Debug)]
153/// An error returned from parsing [`UnixTimestamp`].
154pub struct ParseUnixTimestampError(());
155
156impl std::str::FromStr for UnixTimestamp {
157    type Err = ParseUnixTimestampError;
158
159    fn from_str(s: &str) -> Result<Self, Self::Err> {
160        if let Ok(datetime) = s.parse::<DateTime<Utc>>() {
161            let timestamp = datetime.timestamp();
162            if timestamp >= 0 {
163                return Ok(UnixTimestamp(timestamp as u64));
164            }
165        }
166        let ts = s.parse().or(Err(ParseUnixTimestampError(())))?;
167        Ok(Self(ts))
168    }
169}
170
171impl Serialize for UnixTimestamp {
172    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
173    where
174        S: serde::Serializer,
175    {
176        serializer.serialize_u64(self.as_secs())
177    }
178}
179
180struct UnixTimestampVisitor;
181
182impl serde::de::Visitor<'_> for UnixTimestampVisitor {
183    type Value = UnixTimestamp;
184
185    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
186        formatter.write_str("a non-negative timestamp or datetime string")
187    }
188
189    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
190    where
191        E: serde::de::Error,
192    {
193        Ok(UnixTimestamp::from_secs(v))
194    }
195
196    fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
197    where
198        E: serde::de::Error,
199    {
200        if v < 0.0 || v > u64::MAX as f64 {
201            return Err(E::custom("timestamp out-of-range"));
202        }
203
204        Ok(UnixTimestamp::from_secs(v.trunc() as u64))
205    }
206
207    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
208    where
209        E: serde::de::Error,
210    {
211        let datetime = v.parse::<DateTime<Utc>>().map_err(E::custom)?;
212        let timestamp = datetime.timestamp();
213
214        if timestamp >= 0 {
215            Ok(UnixTimestamp(timestamp as u64))
216        } else {
217            Err(E::custom("timestamp out-of-range"))
218        }
219    }
220}
221
222impl<'de> Deserialize<'de> for UnixTimestamp {
223    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224    where
225        D: serde::Deserializer<'de>,
226    {
227        deserializer.deserialize_any(UnixTimestampVisitor)
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use serde_test::{assert_de_tokens, assert_de_tokens_error, assert_tokens, Token};
234
235    use super::*;
236
237    #[test]
238    fn test_parse_timestamp_int() {
239        assert_tokens(&UnixTimestamp::from_secs(123), &[Token::U64(123)]);
240    }
241
242    #[test]
243    fn test_parse_timestamp_neg_int() {
244        assert_de_tokens_error::<UnixTimestamp>(
245            &[Token::I64(-1)],
246            "invalid type: integer `-1`, expected a non-negative timestamp or datetime string",
247        );
248    }
249
250    #[test]
251    fn test_parse_timestamp_float() {
252        assert_de_tokens(&UnixTimestamp::from_secs(123), &[Token::F64(123.4)]);
253    }
254
255    #[test]
256    fn test_parse_timestamp_large_float() {
257        assert_de_tokens_error::<UnixTimestamp>(
258            &[Token::F64(2.0 * (u64::MAX as f64))],
259            "timestamp out-of-range",
260        );
261    }
262
263    #[test]
264    fn test_parse_timestamp_neg_float() {
265        assert_de_tokens_error::<UnixTimestamp>(&[Token::F64(-1.0)], "timestamp out-of-range");
266    }
267
268    #[test]
269    fn test_parse_timestamp_str() {
270        assert_de_tokens(
271            &UnixTimestamp::from_secs(123),
272            &[Token::Str("1970-01-01T00:02:03Z")],
273        );
274    }
275
276    #[test]
277    fn test_parse_timestamp_other() {
278        assert_de_tokens_error::<UnixTimestamp>(
279            &[Token::Bool(true)],
280            "invalid type: boolean `true`, expected a non-negative timestamp or datetime string",
281        );
282    }
283
284    #[test]
285    fn test_parse_datetime_bogus() {
286        assert_de_tokens_error::<UnixTimestamp>(
287            &[Token::Str("adf3rt546")],
288            "input contains invalid characters",
289        );
290    }
291}