opentalk_types_common/time/
timestamp.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5use std::{ops::Add, time::SystemTime};
6
7use chrono::{DateTime, TimeZone as _, Timelike as _, Utc};
8use derive_more::{AsRef, Deref, Display, From, FromStr};
9
10use crate::{time::DateTimeTz, utils::ExampleData};
11
12/// A UTC DateTime wrapper that implements ToRedisArgs and FromRedisValue.
13///
14/// The values are stores as unix timestamps in redis.
15#[derive(
16    AsRef,
17    Deref,
18    Display,
19    From,
20    FromStr,
21    Debug,
22    Default,
23    Copy,
24    Clone,
25    Ord,
26    PartialOrd,
27    Eq,
28    PartialEq,
29    Hash,
30)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
33pub struct Timestamp(DateTime<Utc>);
34
35impl Timestamp {
36    /// Create a timestamp with the date of the unix epoch start
37    /// (1970-01-01 00:00:00 UTC)
38    pub fn unix_epoch() -> Self {
39        Self(DateTime::from(std::time::UNIX_EPOCH))
40    }
41
42    /// Create a timestamp with the current system time
43    pub fn now() -> Timestamp {
44        Timestamp(Utc::now())
45    }
46
47    /// Format as a string that can be used in a filename easily
48    pub fn to_string_for_filename(&self) -> String {
49        // UTC is the only supported timezone for now so we can hardcode
50        // it because inserting timezone names is extra work due to
51        // https://github.com/chronotope/chrono/issues/960
52        self.0.format("%F_%H-%M-%S-UTC").to_string()
53    }
54
55    /// Round the timestamp to full seconds
56    pub fn rounded_to_seconds(self) -> Timestamp {
57        // This can only fail if the nanoseconds have an invalid value, 0 is
58        // valid here
59        Timestamp(self.0.with_nanosecond(0).expect("nanoseconds should be 0"))
60    }
61}
62
63impl ExampleData for Timestamp {
64    fn example_data() -> Self {
65        Timestamp(Utc.with_ymd_and_hms(2024, 7, 20, 14, 16, 19).unwrap())
66    }
67}
68
69impl From<SystemTime> for Timestamp {
70    fn from(value: SystemTime) -> Self {
71        Self(value.into())
72    }
73}
74
75impl From<DateTimeTz> for Timestamp {
76    fn from(value: DateTimeTz) -> Self {
77        value.datetime.into()
78    }
79}
80
81impl From<Timestamp> for DateTime<Utc> {
82    fn from(value: Timestamp) -> Self {
83        value.0
84    }
85}
86
87impl Add<chrono::Duration> for Timestamp {
88    type Output = Timestamp;
89
90    fn add(self, rhs: chrono::Duration) -> Self::Output {
91        Timestamp(self.0 + rhs)
92    }
93}
94
95#[cfg(feature = "redis")]
96impl redis::ToRedisArgs for Timestamp {
97    fn write_redis_args<W>(&self, out: &mut W)
98    where
99        W: ?Sized + redis::RedisWrite,
100    {
101        self.0.timestamp().write_redis_args(out)
102    }
103
104    fn describe_numeric_behavior(&self) -> redis::NumericBehavior {
105        redis::NumericBehavior::NumberIsInteger
106    }
107}
108
109#[cfg(feature = "redis")]
110impl redis::ToSingleRedisArg for Timestamp {}
111
112#[cfg(feature = "redis")]
113impl redis::FromRedisValue for Timestamp {
114    fn from_redis_value(v: redis::Value) -> Result<Timestamp, redis::ParsingError> {
115        use chrono::TimeZone as _;
116        let timestamp = Utc
117            .timestamp_opt(i64::from_redis_value(v)?, 0)
118            .latest()
119            .unwrap();
120        Ok(Timestamp(timestamp))
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use chrono::{TimeZone as _, Utc};
127
128    use super::Timestamp;
129
130    #[test]
131    fn to_string_for_filename() {
132        let timestamp = Timestamp::unix_epoch();
133        assert_eq!(
134            "1970-01-01_00-00-00-UTC",
135            timestamp.to_string_for_filename().as_str()
136        );
137
138        let timestamp = Timestamp(Utc.with_ymd_and_hms(2020, 5, 3, 14, 16, 19).unwrap());
139        assert_eq!(
140            "2020-05-03_14-16-19-UTC",
141            timestamp.to_string_for_filename().as_str()
142        );
143    }
144}