Skip to main content

ruma_common/
time.rs

1use std::fmt;
2
3use js_int::{UInt, uint};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6use time::OffsetDateTime;
7use web_time::{Duration, SystemTime, UNIX_EPOCH};
8
9/// A timestamp represented as the number of milliseconds since the unix epoch.
10#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
11#[allow(clippy::exhaustive_structs)]
12#[serde(transparent)]
13pub struct MilliSecondsSinceUnixEpoch(pub UInt);
14
15impl MilliSecondsSinceUnixEpoch {
16    /// Creates a new `MilliSecondsSinceUnixEpoch` from the given `SystemTime`, if it is not before
17    /// the unix epoch, or too large to be represented.
18    pub fn from_system_time(time: SystemTime) -> Option<Self> {
19        let duration = time.duration_since(UNIX_EPOCH).ok()?;
20        let millis = duration.as_millis().try_into().ok()?;
21        Some(Self(millis))
22    }
23
24    /// The current system time in milliseconds since the unix epoch.
25    pub fn now() -> Self {
26        Self::from_system_time(SystemTime::now()).expect("date out of range")
27    }
28
29    /// Creates a new `SystemTime` from `self`, if it can be represented.
30    pub fn to_system_time(self) -> Option<SystemTime> {
31        UNIX_EPOCH.checked_add(Duration::from_millis(self.0.into()))
32    }
33
34    /// Adds the given Duration to the time, returning it if the new time can be represented.
35    pub fn checked_add(self, rhs: Duration) -> Option<Self> {
36        Some(Self(self.0.checked_add(rhs.as_millis().try_into().ok()?)?))
37    }
38
39    /// Get the time since the unix epoch in milliseconds.
40    pub fn get(&self) -> UInt {
41        self.0
42    }
43
44    /// Get time since the unix epoch in seconds.
45    pub fn as_secs(&self) -> UInt {
46        self.0 / uint!(1000)
47    }
48}
49
50impl fmt::Debug for MilliSecondsSinceUnixEpoch {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        match OffsetDateTime::from_unix_timestamp(i64::from(self.0) / 1000) {
53            Ok(date) => {
54                let date = date + Duration::from_millis(u64::from(self.0) % 1000);
55
56                let (year, month, day) = date.to_calendar_date();
57                let month = month as u8;
58                let (hours, minutes, seconds, milliseconds) = date.to_hms_milli();
59
60                write!(
61                    f,
62                    "{year}-{month:02}-{day:02}T\
63                     {hours:02}:{minutes:02}:{seconds:02}.{milliseconds:03}"
64                )
65            }
66            // Probably dead code..
67            Err(_) => {
68                // The default Debug impl would put the inner value on its own
69                // line if the formatter's alternate mode is enabled, which
70                // bloats debug strings unnecessarily
71                write!(f, "MilliSecondsSinceUnixEpoch({})", self.0)
72            }
73        }
74    }
75}
76
77impl TryFrom<SystemTime> for MilliSecondsSinceUnixEpoch {
78    type Error = TimeConversionError;
79
80    fn try_from(value: SystemTime) -> Result<Self, Self::Error> {
81        Self::from_system_time(value).ok_or(TimeConversionError::SystemTime(value))
82    }
83}
84
85impl TryFrom<MilliSecondsSinceUnixEpoch> for SystemTime {
86    type Error = TimeConversionError;
87
88    fn try_from(value: MilliSecondsSinceUnixEpoch) -> Result<Self, Self::Error> {
89        value.to_system_time().ok_or(TimeConversionError::MilliSecondsSinceUnixEpoch(value))
90    }
91}
92
93/// A timestamp represented as the number of seconds since the unix epoch.
94#[derive(Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
95#[allow(clippy::exhaustive_structs)]
96#[serde(transparent)]
97pub struct SecondsSinceUnixEpoch(pub UInt);
98
99impl SecondsSinceUnixEpoch {
100    /// Creates a new `MilliSecondsSinceUnixEpoch` from the given `SystemTime`, if it is not before
101    /// the unix epoch, or too large to be represented.
102    pub fn from_system_time(time: SystemTime) -> Option<Self> {
103        let duration = time.duration_since(UNIX_EPOCH).ok()?;
104        let millis = duration.as_secs().try_into().ok()?;
105        Some(Self(millis))
106    }
107
108    /// The current system-time as seconds since the unix epoch.
109    pub fn now() -> Self {
110        Self::from_system_time(SystemTime::now()).expect("date out of range")
111    }
112
113    /// Creates a new `SystemTime` from `self`, if it can be represented.
114    pub fn to_system_time(self) -> Option<SystemTime> {
115        UNIX_EPOCH.checked_add(Duration::from_secs(self.0.into()))
116    }
117
118    /// Adds the given Duration to the time, returning it if the new time can be represented.
119    pub fn checked_add(self, rhs: Duration) -> Option<Self> {
120        Some(Self(self.0.checked_add(rhs.as_millis().try_into().ok()?)?))
121    }
122
123    /// Get time since the unix epoch in seconds.
124    pub fn get(&self) -> UInt {
125        self.0
126    }
127}
128
129impl fmt::Debug for SecondsSinceUnixEpoch {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        match OffsetDateTime::from_unix_timestamp(i64::from(self.0)) {
132            Ok(date) => {
133                let (year, month, day) = date.to_calendar_date();
134                let month = month as u8;
135                let (hours, minutes, seconds) = date.to_hms();
136
137                write!(
138                    f,
139                    "{year}-{month:02}-{day:02}T\
140                     {hours:02}:{minutes:02}:{seconds:02}"
141                )
142            }
143            // Probably dead code..
144            Err(_) => {
145                // The default Debug impl would put the inner value on its own
146                // line if the formatter's alternate mode is enabled, which
147                // bloats debug strings unnecessarily
148                write!(f, "SecondsSinceUnixEpoch({})", self.0)
149            }
150        }
151    }
152}
153
154impl TryFrom<SystemTime> for SecondsSinceUnixEpoch {
155    type Error = TimeConversionError;
156
157    fn try_from(value: SystemTime) -> Result<Self, Self::Error> {
158        Self::from_system_time(value).ok_or(TimeConversionError::SystemTime(value))
159    }
160}
161
162impl TryFrom<SecondsSinceUnixEpoch> for SystemTime {
163    type Error = TimeConversionError;
164
165    fn try_from(value: SecondsSinceUnixEpoch) -> Result<Self, Self::Error> {
166        value.to_system_time().ok_or(TimeConversionError::SecondsSinceUnixEpoch(value))
167    }
168}
169
170/// The error type returned when failing to convert to or from SystemTime
171#[derive(Debug, Error)]
172#[non_exhaustive]
173pub enum TimeConversionError {
174    #[error("Cannot represent {0:?} as SystemTime")]
175    MilliSecondsSinceUnixEpoch(MilliSecondsSinceUnixEpoch),
176
177    #[error("Cannot represent {0:?} as SystemTime")]
178    SecondsSinceUnixEpoch(SecondsSinceUnixEpoch),
179
180    #[error("Cannot represent {0:?} as Ruma time type")]
181    SystemTime(SystemTime),
182}
183
184#[cfg(test)]
185mod tests {
186    use std::time::{Duration, UNIX_EPOCH};
187
188    use js_int::uint;
189    use serde::{Deserialize, Serialize};
190    use serde_json::json;
191
192    use super::{MilliSecondsSinceUnixEpoch, SecondsSinceUnixEpoch};
193
194    #[derive(Clone, Debug, Deserialize, Serialize)]
195    struct SystemTimeTest {
196        millis: MilliSecondsSinceUnixEpoch,
197        secs: SecondsSinceUnixEpoch,
198    }
199
200    #[test]
201    fn deserialize() {
202        let json = json!({ "millis": 3000, "secs": 60 });
203
204        let time = serde_json::from_value::<SystemTimeTest>(json).unwrap();
205        assert_eq!(time.millis.to_system_time(), Some(UNIX_EPOCH + Duration::from_millis(3000)));
206        assert_eq!(time.secs.to_system_time(), Some(UNIX_EPOCH + Duration::from_secs(60)));
207    }
208
209    #[test]
210    fn serialize() {
211        let request = SystemTimeTest {
212            millis: MilliSecondsSinceUnixEpoch::from_system_time(UNIX_EPOCH + Duration::new(2, 0))
213                .unwrap(),
214            secs: SecondsSinceUnixEpoch(uint!(0)),
215        };
216
217        assert_eq!(serde_json::to_value(request).unwrap(), json!({ "millis": 2000, "secs": 0 }));
218    }
219
220    #[test]
221    fn debug_s() {
222        let seconds = SecondsSinceUnixEpoch(uint!(0));
223        assert_eq!(format!("{seconds:?}"), "1970-01-01T00:00:00");
224    }
225
226    #[test]
227    fn debug_ms() {
228        let seconds = MilliSecondsSinceUnixEpoch(uint!(0));
229        assert_eq!(format!("{seconds:?}"), "1970-01-01T00:00:00.000");
230    }
231}