serenity/model/
timestamp.rs

1//! Utilities for parsing and formatting RFC 3339 timestamps.
2//!
3//! The [`Timestamp`] newtype wraps `chrono::DateTime<Utc>` or `time::OffsetDateTime` if the `time`
4//! feature is enabled.
5//!
6//! # Formatting
7//! ```
8//! # use serenity::model::id::GuildId;
9//! # use serenity::model::Timestamp;
10//! #
11//! let timestamp: Timestamp = GuildId::new(175928847299117063).created_at();
12//! assert_eq!(timestamp.unix_timestamp(), 1462015105);
13//! assert_eq!(timestamp.to_string(), "2016-04-30T11:18:25.796Z");
14//! ```
15//!
16//! # Parsing RFC 3339 string
17//! ```
18//! # use serenity::model::Timestamp;
19//! #
20//! let timestamp = Timestamp::parse("2016-04-30T11:18:25Z").unwrap();
21//! let timestamp = Timestamp::parse("2016-04-30T11:18:25+00:00").unwrap();
22//! let timestamp = Timestamp::parse("2016-04-30T11:18:25.796Z").unwrap();
23//!
24//! let timestamp: Timestamp = "2016-04-30T11:18:25Z".parse().unwrap();
25//! let timestamp: Timestamp = "2016-04-30T11:18:25+00:00".parse().unwrap();
26//! let timestamp: Timestamp = "2016-04-30T11:18:25.796Z".parse().unwrap();
27//!
28//! assert!(Timestamp::parse("2016-04-30T11:18:25").is_err());
29//! assert!(Timestamp::parse("2016-04-30T11:18").is_err());
30//! ```
31
32use std::fmt;
33use std::str::FromStr;
34
35#[cfg(feature = "chrono")]
36pub use chrono::ParseError as InnerError;
37#[cfg(feature = "chrono")]
38use chrono::{DateTime, SecondsFormat, TimeZone, Utc};
39#[cfg(not(feature = "chrono"))]
40pub use dep_time::error::Parse as InnerError;
41#[cfg(not(feature = "chrono"))]
42use dep_time::{format_description::well_known::Rfc3339, serde::rfc3339, Duration, OffsetDateTime};
43use serde::{Deserialize, Serialize};
44
45/// Discord's epoch starts at "2015-01-01T00:00:00+00:00"
46const DISCORD_EPOCH: u64 = 1_420_070_400_000;
47
48#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
49#[cfg_attr(feature = "typesize", derive(typesize::derive::TypeSize))]
50#[serde(transparent)]
51pub struct Timestamp(
52    #[cfg(feature = "chrono")] DateTime<Utc>,
53    #[cfg(not(feature = "chrono"))]
54    #[serde(with = "rfc3339")]
55    OffsetDateTime,
56);
57
58impl Timestamp {
59    /// Creates a new [`Timestamp`] from the number of milliseconds since 1970.
60    ///
61    /// # Errors
62    ///
63    /// Returns `Err` if the value is invalid.
64    pub fn from_millis(millis: i64) -> Result<Self, InvalidTimestamp> {
65        #[cfg(feature = "chrono")]
66        let x = Utc.timestamp_millis_opt(millis).single();
67        #[cfg(not(feature = "chrono"))]
68        let x = OffsetDateTime::from_unix_timestamp_nanos(
69            Duration::milliseconds(millis).whole_nanoseconds(),
70        )
71        .ok();
72        x.map(Self).ok_or(InvalidTimestamp)
73    }
74
75    pub(crate) fn from_discord_id(id: u64) -> Self {
76        // This can't fail because of the bit shifting
77        // `(u64::MAX >> 22) + DISCORD_EPOCH` = 5818116911103 = "Wed May 15 2154 07:35:11 GMT+0000"
78        Self::from_millis(((id >> 22) + DISCORD_EPOCH) as i64).expect("can't fail")
79    }
80
81    /// Create a new `Timestamp` with the current date and time in UTC.
82    #[must_use]
83    pub fn now() -> Self {
84        #[cfg(feature = "chrono")]
85        let x = Utc::now();
86        #[cfg(not(feature = "chrono"))]
87        let x = OffsetDateTime::now_utc();
88        Self(x)
89    }
90
91    /// Creates a new [`Timestamp`] from a UNIX timestamp (seconds since 1970).
92    ///
93    /// # Errors
94    ///
95    /// Returns `Err` if the value is invalid.
96    pub fn from_unix_timestamp(secs: i64) -> Result<Self, InvalidTimestamp> {
97        Self::from_millis(secs * 1000)
98    }
99
100    /// Returns the number of non-leap seconds since January 1, 1970 0:00:00 UTC
101    #[must_use]
102    pub fn unix_timestamp(&self) -> i64 {
103        #[cfg(feature = "chrono")]
104        let x = self.0.timestamp();
105        #[cfg(not(feature = "chrono"))]
106        let x = self.0.unix_timestamp();
107        x
108    }
109
110    /// Parse a timestamp from an RFC 3339 date and time string.
111    ///
112    /// # Examples
113    /// ```
114    /// # use serenity::model::Timestamp;
115    /// #
116    /// let timestamp = Timestamp::parse("2016-04-30T11:18:25Z").unwrap();
117    /// let timestamp = Timestamp::parse("2016-04-30T11:18:25+00:00").unwrap();
118    /// let timestamp = Timestamp::parse("2016-04-30T11:18:25.796Z").unwrap();
119    ///
120    /// assert!(Timestamp::parse("2016-04-30T11:18:25").is_err());
121    /// assert!(Timestamp::parse("2016-04-30T11:18").is_err());
122    /// ```
123    ///
124    /// # Errors
125    ///
126    /// Returns `Err` if the string is not a valid RFC 3339 date and time string.
127    pub fn parse(input: &str) -> Result<Timestamp, ParseError> {
128        #[cfg(feature = "chrono")]
129        let x = DateTime::parse_from_rfc3339(input).map_err(ParseError)?.with_timezone(&Utc);
130        #[cfg(not(feature = "chrono"))]
131        let x = OffsetDateTime::parse(input, &Rfc3339).map_err(ParseError)?;
132        Ok(Self(x))
133    }
134
135    #[must_use]
136    pub fn to_rfc3339(&self) -> Option<String> {
137        #[cfg(feature = "chrono")]
138        let x = self.0.to_rfc3339_opts(SecondsFormat::Millis, true);
139        #[cfg(not(feature = "chrono"))]
140        let x = self.0.format(&Rfc3339).ok()?;
141        Some(x)
142    }
143}
144
145impl std::fmt::Display for Timestamp {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.write_str(&self.to_rfc3339().ok_or(std::fmt::Error)?)
148    }
149}
150
151impl std::ops::Deref for Timestamp {
152    #[cfg(feature = "chrono")]
153    type Target = DateTime<Utc>;
154    #[cfg(not(feature = "chrono"))]
155    type Target = OffsetDateTime;
156
157    fn deref(&self) -> &Self::Target {
158        &self.0
159    }
160}
161
162#[cfg(feature = "chrono")]
163impl<Tz: TimeZone> From<DateTime<Tz>> for Timestamp {
164    fn from(dt: DateTime<Tz>) -> Self {
165        Self(dt.with_timezone(&Utc))
166    }
167}
168#[cfg(not(feature = "chrono"))]
169impl From<OffsetDateTime> for Timestamp {
170    fn from(dt: OffsetDateTime) -> Self {
171        Self(dt)
172    }
173}
174
175impl Default for Timestamp {
176    fn default() -> Self {
177        #[cfg(feature = "chrono")]
178        let x = DateTime::default();
179        #[cfg(not(feature = "chrono"))]
180        let x = OffsetDateTime::UNIX_EPOCH;
181        Self(x)
182    }
183}
184
185#[derive(Debug)]
186pub struct InvalidTimestamp;
187
188impl std::error::Error for InvalidTimestamp {}
189
190impl fmt::Display for InvalidTimestamp {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        f.write_str("invalid UNIX timestamp value")
193    }
194}
195
196/// Signifies the failure to parse the `Timestamp` from an RFC 3339 string.
197#[derive(Debug)]
198pub struct ParseError(InnerError);
199
200impl std::error::Error for ParseError {}
201
202impl fmt::Display for ParseError {
203    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204        fmt::Display::fmt(&self.0, f)
205    }
206}
207
208impl FromStr for Timestamp {
209    type Err = ParseError;
210
211    /// Parses an RFC 3339 date and time string such as `2016-04-30T11:18:25.796Z`.
212    fn from_str(s: &str) -> Result<Self, Self::Err> {
213        Timestamp::parse(s)
214    }
215}
216
217impl<'a> std::convert::TryFrom<&'a str> for Timestamp {
218    type Error = ParseError;
219
220    /// Parses an RFC 3339 date and time string such as `2016-04-30T11:18:25.796Z`.
221    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
222        Timestamp::parse(s)
223    }
224}
225
226impl From<&Timestamp> for Timestamp {
227    fn from(ts: &Timestamp) -> Self {
228        *ts
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::Timestamp;
235
236    #[test]
237    fn from_unix_timestamp() {
238        let timestamp = Timestamp::from_unix_timestamp(1462015105).unwrap();
239        assert_eq!(timestamp.unix_timestamp(), 1462015105);
240        if cfg!(feature = "chrono") {
241            assert_eq!(timestamp.to_string(), "2016-04-30T11:18:25.000Z");
242        } else {
243            assert_eq!(timestamp.to_string(), "2016-04-30T11:18:25Z");
244        }
245    }
246}