Skip to main content

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))]
33#[cfg_attr(feature = "typescript", derive(ts_rs::TS), ts(export_to = "common/"))]
34pub struct Timestamp(DateTime<Utc>);
35
36impl Timestamp {
37    /// Create a timestamp with the date of the unix epoch start
38    /// (1970-01-01 00:00:00 UTC)
39    pub fn unix_epoch() -> Self {
40        Self(DateTime::from(std::time::UNIX_EPOCH))
41    }
42
43    /// Create a timestamp with the current system time
44    pub fn now() -> Timestamp {
45        Timestamp(Utc::now())
46    }
47
48    /// Format as a string that can be used in a filename easily
49    pub fn to_string_for_filename(&self) -> String {
50        // UTC is the only supported timezone for now so we can hardcode
51        // it because inserting timezone names is extra work due to
52        // https://github.com/chronotope/chrono/issues/960
53        self.0.format("%F_%H-%M-%S-UTC").to_string()
54    }
55
56    /// Round the timestamp to full seconds
57    pub fn rounded_to_seconds(self) -> Timestamp {
58        // This can only fail if the nanoseconds have an invalid value, 0 is
59        // valid here
60        Timestamp(self.0.with_nanosecond(0).expect("nanoseconds should be 0"))
61    }
62}
63
64impl ExampleData for Timestamp {
65    fn example_data() -> Self {
66        Timestamp(Utc.with_ymd_and_hms(2024, 7, 20, 14, 16, 19).unwrap())
67    }
68}
69
70impl From<SystemTime> for Timestamp {
71    fn from(value: SystemTime) -> Self {
72        Self(value.into())
73    }
74}
75
76impl From<DateTimeTz> for Timestamp {
77    fn from(value: DateTimeTz) -> Self {
78        value.datetime.into()
79    }
80}
81
82impl From<Timestamp> for DateTime<Utc> {
83    fn from(value: Timestamp) -> Self {
84        value.0
85    }
86}
87
88impl Add<chrono::Duration> for Timestamp {
89    type Output = Timestamp;
90
91    fn add(self, rhs: chrono::Duration) -> Self::Output {
92        Timestamp(self.0 + rhs)
93    }
94}
95
96#[cfg(feature = "redis")]
97impl redis::ToRedisArgs for Timestamp {
98    fn write_redis_args<W>(&self, out: &mut W)
99    where
100        W: ?Sized + redis::RedisWrite,
101    {
102        self.0.timestamp().write_redis_args(out)
103    }
104
105    fn describe_numeric_behavior(&self) -> redis::NumericBehavior {
106        redis::NumericBehavior::NumberIsInteger
107    }
108}
109
110#[cfg(feature = "redis")]
111impl redis::ToSingleRedisArg for Timestamp {}
112
113#[cfg(feature = "redis")]
114impl redis::FromRedisValue for Timestamp {
115    fn from_redis_value(v: redis::Value) -> Result<Timestamp, redis::ParsingError> {
116        use chrono::TimeZone as _;
117        let timestamp = Utc
118            .timestamp_opt(i64::from_redis_value(v)?, 0)
119            .latest()
120            .unwrap();
121        Ok(Timestamp(timestamp))
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use chrono::{TimeZone as _, Utc};
128
129    use super::Timestamp;
130
131    #[test]
132    fn to_string_for_filename() {
133        let timestamp = Timestamp::unix_epoch();
134        assert_eq!(
135            "1970-01-01_00-00-00-UTC",
136            timestamp.to_string_for_filename().as_str()
137        );
138
139        let timestamp = Timestamp(Utc.with_ymd_and_hms(2020, 5, 3, 14, 16, 19).unwrap());
140        assert_eq!(
141            "2020-05-03_14-16-19-UTC",
142            timestamp.to_string_for_filename().as_str()
143        );
144    }
145}