ofdb_entities/
time.rs

1use std::{
2    fmt,
3    ops::{Add, Sub},
4};
5
6use thiserror::Error;
7use time::{
8    format_description::FormatItem, macros::format_description, Duration, OffsetDateTime,
9    PrimitiveDateTime,
10};
11
12/// A primitive unix timestamp without time zone.
13///
14/// We assume that all timestamps must be specified in the UTC time zone.
15
16// This is a temporary workaround because
17// [`time`](::time) does not allow to convert [`OffsetDateTime`] to [`PrimitiveDateTime`]
18// (see <https://github.com/time-rs/time/pull/458>)
19// and [`PrimitiveDateTime`] has e.g. no `unix_timestamp` method.
20// So we internally use [`OffsetDateTime`] but the semantic is like [`PrimitiveDateTime`].
21#[derive(Debug, Copy, PartialEq, Eq, Clone, PartialOrd, Ord)]
22pub struct Timestamp(time::OffsetDateTime);
23
24const TIMESTAMP_FORMAT: &[FormatItem] =
25    format_description!("[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]");
26
27impl fmt::Display for Timestamp {
28    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
29        write!(f, "{}", self.0.format(TIMESTAMP_FORMAT).unwrap())
30    }
31}
32
33#[derive(Debug, Error)]
34#[error("Invalid time range: {0}")]
35pub struct OutOfRangeError(String);
36
37impl Timestamp {
38    pub fn now() -> Self {
39        Self(OffsetDateTime::now_utc())
40    }
41
42    pub fn try_from_secs(seconds: i64) -> Result<Self, OutOfRangeError> {
43        let date_time = time::OffsetDateTime::from_unix_timestamp(seconds)
44            .map_err(|err| OutOfRangeError(err.to_string()))?;
45        Ok(Self(date_time))
46    }
47
48    #[deprecated]
49    pub fn from_secs(seconds: i64) -> Self {
50        Self::try_from_secs(seconds).unwrap()
51    }
52
53    pub fn try_from_millis(milliseconds: i64) -> Result<Self, OutOfRangeError> {
54        let nanos = millis_to_nanos(milliseconds);
55        let date_time = time::OffsetDateTime::from_unix_timestamp_nanos(nanos)
56            .map_err(|err| OutOfRangeError(err.to_string()))?;
57        Ok(Self(date_time))
58    }
59
60    #[deprecated]
61    pub fn from_millis(milliseconds: i64) -> Self {
62        Self::try_from_millis(milliseconds).unwrap()
63    }
64
65    pub fn as_secs(self) -> i64 {
66        self.0.unix_timestamp()
67    }
68
69    pub fn as_millis(self) -> i64 {
70        nanos_to_millis(self.0.unix_timestamp_nanos())
71    }
72
73    pub fn format(&self, fmt: &[FormatItem<'_>]) -> String {
74        self.0.format(fmt).unwrap()
75    }
76    pub fn checked_sub(self, duration: Duration) -> Option<Self> {
77        self.0.checked_sub(duration).map(Self)
78    }
79}
80
81fn nanos_to_millis(nanos: i128) -> i64 {
82    (nanos / 1_000_000).try_into().unwrap()
83}
84
85fn millis_to_nanos(millis: i64) -> i128 {
86    i128::from(millis) * 1_000_000
87}
88
89impl From<PrimitiveDateTime> for Timestamp {
90    fn from(ts: PrimitiveDateTime) -> Self {
91        Self(ts.assume_utc())
92    }
93}
94
95impl Add<Duration> for Timestamp {
96    type Output = Self;
97    fn add(self, d: time::Duration) -> Self {
98        Self(self.0.add(d))
99    }
100}
101
102impl Sub<Duration> for Timestamp {
103    type Output = Self;
104    fn sub(self, d: time::Duration) -> Self {
105        Self(self.0.sub(d))
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn format_timestamp() {
115        let ts = Timestamp::try_from_millis(1_658_146_497_321).unwrap();
116        assert_eq!("2022-07-18 12:14:57.321", format!("{ts}"));
117    }
118}