serenity/utils/
formatted_timestamp.rs

1use std::error::Error as StdError;
2use std::fmt;
3use std::str::FromStr;
4
5use crate::all::Timestamp;
6
7/// Represents a combination of a timestamp and a style for formatting time in messages.
8///
9/// [Discord docs](https://discord.com/developers/docs/reference#message-formatting-formats).
10#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
11pub struct FormattedTimestamp {
12    timestamp: i64,
13    style: Option<FormattedTimestampStyle>,
14}
15
16/// Enum representing various styles for formatting time in messages.
17///
18/// [Discord docs](https://discord.com/developers/docs/reference#message-formatting-timestamp-styles).
19#[derive(Default, Clone, Copy, PartialEq, Eq, Hash, Debug)]
20pub enum FormattedTimestampStyle {
21    /// Represents a short time format, e.g., "12:34 PM".
22    ShortTime,
23    /// Represents a long time format, e.g., "12:34:56 PM".
24    LongTime,
25    /// Represents a short date format, e.g., "2023-11-17".
26    ShortDate,
27    /// Represents a long date format, e.g., "November 17, 2023".
28    LongDate,
29    /// Represents a short date and time format, e.g., "November 17, 2023 12:34 PM".
30    #[default]
31    ShortDateTime,
32    /// Represents a long date and time format, e.g., "Thursday, November 17, 2023 12:34 PM".
33    LongDateTime,
34    /// Represents a relative time format, indicating the time relative to the current moment,
35    /// e.g., "2 hours ago" or "in 2 hours".
36    RelativeTime,
37}
38
39impl FormattedTimestamp {
40    /// Creates a new [`FormattedTimestamp`] instance from the given [`Timestamp`] and
41    /// [`FormattedTimestampStyle`].
42    #[must_use]
43    pub fn new(timestamp: Timestamp, style: Option<FormattedTimestampStyle>) -> Self {
44        Self {
45            timestamp: timestamp.unix_timestamp(),
46            style,
47        }
48    }
49
50    /// Creates a new [`FormattedTimestamp`] instance representing the current timestamp with the
51    /// default style.
52    #[must_use]
53    pub fn now() -> Self {
54        Self {
55            timestamp: Timestamp::now().unix_timestamp(),
56            style: None,
57        }
58    }
59
60    /// Returns the timestamp of this [`FormattedTimestamp`].
61    #[must_use]
62    pub fn timestamp(&self) -> i64 {
63        self.timestamp
64    }
65
66    /// Returns the style of this [`FormattedTimestamp`].
67    #[must_use]
68    pub fn style(&self) -> Option<FormattedTimestampStyle> {
69        self.style
70    }
71}
72
73impl From<Timestamp> for FormattedTimestamp {
74    /// Creates a new [`FormattedTimestamp`] instance from the given [`Timestamp`] with the default
75    /// style.
76    fn from(timestamp: Timestamp) -> Self {
77        Self {
78            timestamp: timestamp.unix_timestamp(),
79            style: None,
80        }
81    }
82}
83
84impl fmt::Display for FormattedTimestamp {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self.style {
87            Some(style) => write!(f, "<t:{}:{}>", self.timestamp, style),
88            None => write!(f, "<t:{}>", self.timestamp),
89        }
90    }
91}
92
93impl fmt::Display for FormattedTimestampStyle {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        let style = match self {
96            Self::ShortTime => "t",
97            Self::LongTime => "T",
98            Self::ShortDate => "d",
99            Self::LongDate => "D",
100            Self::ShortDateTime => "f",
101            Self::LongDateTime => "F",
102            Self::RelativeTime => "R",
103        };
104        f.write_str(style)
105    }
106}
107
108/// An error that can occur when parsing a [`FormattedTimestamp`] from a string.
109#[derive(Debug, Clone)]
110#[non_exhaustive]
111pub struct FormattedTimestampParseError {
112    string: String,
113}
114
115impl StdError for FormattedTimestampParseError {}
116
117impl fmt::Display for FormattedTimestampParseError {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "invalid formatted timestamp {:?}", self.string)
120    }
121}
122
123fn parse_formatted_timestamp(s: &str) -> Option<FormattedTimestamp> {
124    // A formatted timestamp looks like: <t:TIMESTAMP> or <t:TIMESTAMP:STYLE>
125    let inner = s.strip_prefix("<t:")?.strip_suffix('>')?;
126
127    Some(match inner.split_once(':') {
128        Some((timestamp, style)) => FormattedTimestamp {
129            timestamp: timestamp.parse().ok()?,
130            style: Some(style.parse().ok()?),
131        },
132        None => FormattedTimestamp {
133            timestamp: inner.parse().ok()?,
134            style: None,
135        },
136    })
137}
138
139impl FromStr for FormattedTimestamp {
140    type Err = FormattedTimestampParseError;
141    fn from_str(s: &str) -> Result<Self, Self::Err> {
142        match parse_formatted_timestamp(s) {
143            Some(x) => Ok(x),
144            None => Err(FormattedTimestampParseError {
145                string: s.into(),
146            }),
147        }
148    }
149}
150
151impl FromStr for FormattedTimestampStyle {
152    type Err = FormattedTimestampParseError;
153    fn from_str(s: &str) -> Result<Self, Self::Err> {
154        match s {
155            "t" => Ok(Self::ShortTime),
156            "T" => Ok(Self::LongTime),
157            "d" => Ok(Self::ShortDate),
158            "D" => Ok(Self::LongDate),
159            "f" => Ok(Self::ShortDateTime),
160            "F" => Ok(Self::LongDateTime),
161            "R" => Ok(Self::RelativeTime),
162            _ => Err(FormattedTimestampParseError {
163                string: s.into(),
164            }),
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn test_message_time() {
175        let timestamp = Timestamp::now();
176
177        let time = FormattedTimestamp::new(timestamp, Some(FormattedTimestampStyle::ShortDateTime));
178
179        let time_str = time.to_string();
180
181        assert_eq!(
182            time_str,
183            format!(
184                "<t:{}:{}>",
185                timestamp.unix_timestamp(),
186                FormattedTimestampStyle::ShortDateTime
187            )
188        );
189
190        let unstyled = FormattedTimestamp::new(timestamp, None);
191
192        let unstyled_str = unstyled.to_string();
193
194        assert_eq!(unstyled_str, format!("<t:{}>", timestamp.unix_timestamp()));
195    }
196
197    #[test]
198    fn test_message_time_style() {
199        assert_eq!(FormattedTimestampStyle::ShortTime.to_string(), "t");
200        assert_eq!(FormattedTimestampStyle::LongTime.to_string(), "T");
201        assert_eq!(FormattedTimestampStyle::ShortDate.to_string(), "d");
202        assert_eq!(FormattedTimestampStyle::LongDate.to_string(), "D");
203        assert_eq!(FormattedTimestampStyle::ShortDateTime.to_string(), "f");
204        assert_eq!(FormattedTimestampStyle::LongDateTime.to_string(), "F");
205        assert_eq!(FormattedTimestampStyle::RelativeTime.to_string(), "R");
206    }
207
208    #[test]
209    fn test_message_time_parse() {
210        let timestamp = Timestamp::now();
211
212        let time = FormattedTimestamp::new(timestamp, Some(FormattedTimestampStyle::ShortDateTime));
213
214        let time_str = format!(
215            "<t:{}:{}>",
216            timestamp.unix_timestamp(),
217            FormattedTimestampStyle::ShortDateTime
218        );
219
220        let time_parsed = time_str.parse::<FormattedTimestamp>().unwrap();
221
222        assert_eq!(time, time_parsed);
223
224        let unstyled = FormattedTimestamp::new(timestamp, None);
225
226        let unstyled_str = format!("<t:{}>", timestamp.unix_timestamp());
227
228        let unstyled_parsed = unstyled_str.parse::<FormattedTimestamp>().unwrap();
229
230        assert_eq!(unstyled, unstyled_parsed);
231    }
232
233    #[test]
234    fn test_message_time_style_parse() {
235        assert!(matches!("t".parse(), Ok(FormattedTimestampStyle::ShortTime)));
236        assert!(matches!("T".parse(), Ok(FormattedTimestampStyle::LongTime)));
237        assert!(matches!("d".parse(), Ok(FormattedTimestampStyle::ShortDate)));
238        assert!(matches!("D".parse(), Ok(FormattedTimestampStyle::LongDate)));
239        assert!(matches!("f".parse(), Ok(FormattedTimestampStyle::ShortDateTime)));
240        assert!(matches!("F".parse(), Ok(FormattedTimestampStyle::LongDateTime)));
241        assert!(matches!("R".parse(), Ok(FormattedTimestampStyle::RelativeTime)));
242    }
243}