Skip to main content

pubhubs/misc/
time_ext.rs

1//! Tools for dealing with time.
2use std::fmt;
3use std::time;
4
5/// Returned by [`format_time_wrt`].
6#[derive(Clone, Debug)]
7pub struct FormattedTime {
8    prefix: &'static str,
9    suffix: &'static str,
10    duration: time::Duration,
11    t: time::SystemTime,
12}
13
14impl fmt::Display for FormattedTime {
15    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
16        write!(
17            f,
18            "{} ({}{}{})",
19            humantime::format_rfc3339_seconds(self.t),
20            self.prefix,
21            humantime::format_duration(self.duration),
22            self.suffix
23        )
24    }
25}
26
27/// Like [`format_time_wrt`], but computes `now` itself.
28pub fn format_time(t: time::SystemTime) -> FormattedTime {
29    format_time_wrt(t, time::SystemTime::now())
30}
31
32/// Formats the given time in UTC using RFC3339 and, to aid interpretation, adds
33/// a human readable time delta.
34///
35/// Both time and time delta are rounded to seconds.
36///
37/// For example, at the time of writing the unix epoch is displayed thusly:
38/// `1970-01-01T00:00:00Z (2810w 13h 13m 23s ago)`
39pub fn format_time_wrt(t: time::SystemTime, now: time::SystemTime) -> FormattedTime {
40    let (prefix, suffix, duration) = match now.duration_since(t) {
41        Ok(duration) => ("", " ago", duration),
42        Err(err) => ("in ", "", err.duration()),
43    };
44
45    let duration = time::Duration::from_secs(duration.as_secs());
46
47    FormattedTime {
48        prefix,
49        suffix,
50        duration,
51        t,
52    }
53}
54
55pub mod human_duration {
56    use serde::{Deserialize as _, de::Error as _};
57
58    pub fn deserialize<'de, D>(d: D) -> Result<core::time::Duration, D::Error>
59    where
60        D: serde::Deserializer<'de>,
61    {
62        // Could be more efficient with something like serde_cow::CowStr, but that'd require
63        // another dependency
64        let s = String::deserialize(d)?;
65
66        humantime::parse_duration(&s).map_err(D::Error::custom)
67    }
68
69    pub fn serialize<S>(duration: &core::time::Duration, s: S) -> Result<S::Ok, S::Error>
70    where
71        S: serde::ser::Serializer,
72    {
73        s.collect_str(&humantime::format_duration(*duration))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_format_time() {
83        let epoch = time::UNIX_EPOCH;
84        let t = epoch + time::Duration::from_secs_f64(0.002003004);
85        assert_eq!(
86            format_time_wrt(t, epoch).to_string(),
87            "1970-01-01T00:00:00Z (in 0s)".to_string()
88        );
89        assert_eq!(
90            format_time_wrt(epoch, t).to_string(),
91            "1970-01-01T00:00:00Z (0s ago)".to_string()
92        );
93        let t2 = epoch + time::Duration::from_secs_f64(1699535603.723209);
94        assert_eq!(
95            format_time_wrt(epoch, t2).to_string(),
96            "1970-01-01T00:00:00Z (53years 10months 7days 21h 37m 23s ago)".to_string()
97        );
98        assert_eq!(
99            format_time_wrt(t2, epoch).to_string(),
100            "2023-11-09T13:13:23Z (in 53years 10months 7days 21h 37m 23s)".to_string()
101        );
102    }
103
104    #[derive(serde::Deserialize, serde::Serialize)]
105    struct TestStruct {
106        #[serde(with = "human_duration")]
107        duration: core::time::Duration,
108    }
109
110    #[test]
111    fn test_human_duration() {
112        let ts: TestStruct = serde_json::from_str(r#"{"duration": "1w 5s"}"#).unwrap();
113        assert_eq!(
114            ts.duration,
115            core::time::Duration::from_secs(5 + 7 * 24 * 60 * 60)
116        );
117        assert_eq!(
118            serde_json::to_string(&ts).unwrap(),
119            r#"{"duration":"7days 5s"}"#.to_string()
120        );
121    }
122}