liquid_core/model/scalar/
datetime.rs

1use std::convert::TryInto;
2use std::fmt;
3use std::ops;
4
5mod strftime;
6
7use super::Date;
8
9/// Liquid's native date + time type.
10#[derive(
11    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
12)]
13#[serde(transparent)]
14#[repr(transparent)]
15pub struct DateTime {
16    #[serde(with = "friendly_date_time")]
17    inner: DateTimeImpl,
18}
19
20type DateTimeImpl = time::OffsetDateTime;
21
22impl DateTime {
23    /// Create a `DateTime` from the current moment.
24    pub fn now() -> Self {
25        Self {
26            inner: DateTimeImpl::now_utc(),
27        }
28    }
29
30    /// Makes a new NaiveDate from the calendar date (year, month and day).
31    ///
32    /// Panics on the out-of-range date, invalid month and/or day.
33    pub fn from_ymd(year: i32, month: u8, day: u8) -> Self {
34        Self {
35            inner: time::Date::from_calendar_date(
36                year,
37                month.try_into().expect("the month is out of range"),
38                day,
39            )
40            .expect("one or more components were invalid")
41            .with_hms(0, 0, 0)
42            .expect("one or more components were invalid")
43            .assume_offset(time::macros::offset!(UTC)),
44        }
45    }
46
47    /// Convert a `str` to `Self`
48    #[allow(clippy::should_implement_trait)]
49    pub fn from_str(other: &str) -> Option<Self> {
50        parse_date_time(other).map(|d| Self { inner: d })
51    }
52
53    /// Replace date with `other`.
54    pub fn with_date(self, other: Date) -> Self {
55        Self {
56            inner: self.inner.replace_date(other.inner),
57        }
58    }
59
60    /// Changes the associated time zone. This does not change the actual DateTime (but will change the string representation).
61    pub fn with_offset(self, offset: time::UtcOffset) -> Self {
62        Self {
63            inner: self.inner.to_offset(offset),
64        }
65    }
66
67    /// Retrieves a date component.
68    pub fn date(self) -> Date {
69        Date {
70            inner: self.inner.date(),
71        }
72    }
73
74    /// Formats the combined date and time with the specified format string.
75    ///
76    /// See the [chrono::format::strftime](https://docs.rs/chrono/latest/chrono/format/strftime/index.html)
77    /// module on the supported escape sequences.
78    #[inline]
79    pub fn format(&self, fmt: &str) -> Result<String, strftime::DateFormatError> {
80        strftime::strftime(self.inner, fmt)
81    }
82
83    /// Returns an RFC 2822 date and time string such as `Tue, 1 Jul 2003 10:52:37 +0200`.
84    pub fn to_rfc2822(&self) -> String {
85        self.inner
86            .format(&time::format_description::well_known::Rfc2822)
87            .expect("always valid")
88    }
89}
90
91impl DateTime {
92    /// Get the year of the date.
93    #[inline]
94    pub fn year(&self) -> i32 {
95        self.inner.year()
96    }
97    /// Get the month.
98    #[inline]
99    pub fn month(&self) -> u8 {
100        self.inner.month() as u8
101    }
102    /// Get the day of the month.
103    ///
104    //// The returned value will always be in the range 1..=31.
105    #[inline]
106    pub fn day(&self) -> u8 {
107        self.inner.day()
108    }
109    /// Get the day of the year.
110    ///
111    /// The returned value will always be in the range 1..=366 (1..=365 for common years).
112    #[inline]
113    pub fn ordinal(&self) -> u16 {
114        self.inner.ordinal()
115    }
116    /// Get the ISO week number.
117    ///
118    /// The returned value will always be in the range 1..=53.
119    #[inline]
120    pub fn iso_week(&self) -> u8 {
121        self.inner.iso_week()
122    }
123}
124
125impl Default for DateTime {
126    fn default() -> Self {
127        Self {
128            inner: DateTimeImpl::UNIX_EPOCH,
129        }
130    }
131}
132
133const DATE_TIME_FORMAT: &[time::format_description::FormatItem<'static>] = time::macros::format_description!(
134    "[year]-[month]-[day] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"
135);
136
137const DATE_TIME_FORMAT_SUBSEC: &[time::format_description::FormatItem<'static>] = time::macros::format_description!(
138    "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond] [offset_hour sign:mandatory][offset_minute]"
139);
140
141impl fmt::Display for DateTime {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        let date_format = match self.inner.nanosecond() {
144            0 => DATE_TIME_FORMAT,
145            _ => DATE_TIME_FORMAT_SUBSEC,
146        };
147
148        write!(
149            f,
150            "{}",
151            self.inner.format(date_format).map_err(|_e| fmt::Error)?
152        )
153    }
154}
155
156impl ops::Deref for DateTime {
157    type Target = DateTimeImpl;
158    fn deref(&self) -> &Self::Target {
159        &self.inner
160    }
161}
162
163impl ops::DerefMut for DateTime {
164    fn deref_mut(&mut self) -> &mut Self::Target {
165        &mut self.inner
166    }
167}
168
169mod friendly_date_time {
170    use super::*;
171    use serde::{self, Deserialize, Deserializer, Serializer};
172
173    pub(crate) fn serialize<S>(date: &DateTimeImpl, serializer: S) -> Result<S::Ok, S::Error>
174    where
175        S: Serializer,
176    {
177        let date_format = match date.nanosecond() {
178            0 => DATE_TIME_FORMAT,
179            _ => DATE_TIME_FORMAT_SUBSEC,
180        };
181
182        let s = date
183            .format(date_format)
184            .map_err(serde::ser::Error::custom)?;
185        serializer.serialize_str(&s)
186    }
187
188    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<DateTimeImpl, D::Error>
189    where
190        D: Deserializer<'de>,
191    {
192        let s: std::borrow::Cow<'_, str> = Deserialize::deserialize(deserializer)?;
193        if let Ok(date) = DateTimeImpl::parse(&s, DATE_TIME_FORMAT_SUBSEC) {
194            Ok(date)
195        } else {
196            DateTimeImpl::parse(&s, DATE_TIME_FORMAT).map_err(serde::de::Error::custom)
197        }
198    }
199}
200
201/// Parse a string representing the date and time.
202///
203/// Accepts any of the formats listed below and builds return an `Option`
204/// containing a `DateTimeImpl`.
205///
206/// Supported formats:
207///
208/// * `default` - `YYYY-MM-DD HH:MM:SS`
209/// * `day_month` - `DD Month YYYY HH:MM:SS`
210/// * `day_mon` - `DD Mon YYYY HH:MM:SS`
211/// * `mdy` -  `MM/DD/YYYY HH:MM:SS`
212/// * `dow_mon` - `Dow Mon DD HH:MM:SS YYYY`
213///
214/// Offsets in one of the following forms, and are catenated with any of
215/// the above formats.
216///
217/// * `+HHMM`
218/// * `-HHMM`
219///
220/// Example:
221///
222/// * `dow_mon` format with an offset: "Tue Feb 16 10:00:00 2016 +0100"
223fn parse_date_time(s: &str) -> Option<DateTimeImpl> {
224    use regex::Regex;
225    use time::macros::format_description;
226
227    const USER_FORMATS: &[&[time::format_description::FormatItem<'_>]] = &[
228        DATE_TIME_FORMAT,
229        DATE_TIME_FORMAT_SUBSEC,
230        format_description!("[day] [month repr:long] [year] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"),
231        format_description!("[day] [month repr:short] [year] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"),
232        format_description!("[month]/[day]/[year] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"),
233        format_description!("[weekday repr:short] [month repr:short] [day padding:none] [hour]:[minute]:[second] [year] [offset_hour sign:mandatory][offset_minute]"),
234    ];
235
236    if s.is_empty() {
237        None
238    } else if let "now" | "today" = s.to_lowercase().trim() {
239        Some(DateTimeImpl::now_utc())
240    } else if s.parse::<i64>().is_ok() {
241        DateTimeImpl::parse(s, format_description!("[unix_timestamp]")).ok()
242    } else {
243        let offset_re = Regex::new(r"[+-][01][0-9]{3}$").unwrap();
244
245        let offset = if offset_re.is_match(s) { "" } else { " +0000" };
246        let s = s.to_owned() + offset;
247
248        USER_FORMATS
249            .iter()
250            .find_map(|f| DateTimeImpl::parse(s.as_str(), f).ok())
251    }
252}
253
254#[cfg(test)]
255mod test {
256    use super::*;
257
258    #[test]
259    fn parse_date_time_empty_is_bad() {
260        let input = "";
261        let actual = parse_date_time(input);
262        assert!(actual.is_none());
263    }
264
265    #[test]
266    fn parse_date_time_bad() {
267        let input = "aaaaa";
268        let actual = parse_date_time(input);
269        assert!(actual.is_none());
270    }
271
272    #[test]
273    fn parse_date_time_now() {
274        let input = "now";
275        let actual = parse_date_time(input);
276        assert!(actual.is_some());
277    }
278
279    #[test]
280    fn parse_date_time_today() {
281        let input = "today";
282        let actual = parse_date_time(input);
283        assert!(actual.is_some());
284
285        let input = "Today";
286        let actual = parse_date_time(input);
287        assert!(actual.is_some());
288    }
289
290    #[test]
291    fn parse_date_time_serialized_format() {
292        let input = "2016-02-16 10:00:00 +0100"; // default format with offset
293        let actual = parse_date_time(input);
294        assert!(actual.unwrap().unix_timestamp() == 1455613200);
295
296        let input = "2016-02-16 10:00:00 +0000"; // default format UTC
297        let actual = parse_date_time(input);
298        assert!(actual.unwrap().unix_timestamp() == 1455616800);
299
300        let input = "2016-02-16 10:00:00"; // default format no offset
301        let actual = parse_date_time(input);
302        assert!(actual.unwrap().unix_timestamp() == 1455616800);
303    }
304
305    #[test]
306    fn parse_date_time_serialized_format_with_subseconds() {
307        let input = "2016-02-16 10:00:00.123456789 +0100"; // default format with offset
308        let actual = parse_date_time(input);
309        assert!(actual.unwrap().unix_timestamp_nanos() == 1455613200123456789);
310
311        let input = "2016-02-16 10:00:00.123456789 +0000"; // default format UTC
312        let actual = parse_date_time(input);
313        assert!(actual.unwrap().unix_timestamp_nanos() == 1455616800123456789);
314
315        let input = "2016-02-16 10:00:00.123456789"; // default format no offset
316        let actual = parse_date_time(input);
317        assert!(actual.unwrap().unix_timestamp_nanos() == 1455616800123456789);
318    }
319
320    #[test]
321    fn parse_date_time_day_month_format() {
322        let input = "16 February 2016 10:00:00 +0100"; // day_month format with offset
323        let actual = parse_date_time(input);
324        assert!(actual.unwrap().unix_timestamp() == 1455613200);
325
326        let input = "16 February 2016 10:00:00 +0000"; // day_month format UTC
327        let actual = parse_date_time(input);
328        assert!(actual.unwrap().unix_timestamp() == 1455616800);
329
330        let input = "16 February 2016 10:00:00"; // day_month format no offset
331        let actual = parse_date_time(input);
332        assert!(actual.unwrap().unix_timestamp() == 1455616800);
333    }
334
335    #[test]
336    fn parse_date_time_day_mon_format() {
337        let input = "16 Feb 2016 10:00:00 +0100"; // day_mon format with offset
338        let actual = parse_date_time(input);
339        assert!(actual.unwrap().unix_timestamp() == 1455613200);
340
341        let input = "16 Feb 2016 10:00:00 +0000"; // day_mon format UTC
342        let actual = parse_date_time(input);
343        assert!(actual.unwrap().unix_timestamp() == 1455616800);
344
345        let input = "16 Feb 2016 10:00:00"; // day_mon format no offset
346        let actual = parse_date_time(input);
347        assert!(actual.unwrap().unix_timestamp() == 1455616800);
348    }
349
350    #[test]
351    fn parse_date_time_mdy_format() {
352        let input = "02/16/2016 10:00:00 +0100"; // mdy format with offset
353        let actual = parse_date_time(input);
354        assert!(actual.unwrap().unix_timestamp() == 1455613200);
355
356        let input = "02/16/2016 10:00:00 +0000"; // mdy format UTC
357        let actual = parse_date_time(input);
358        assert!(actual.unwrap().unix_timestamp() == 1455616800);
359
360        let input = "02/16/2016 10:00:00"; // mdy format no offset
361        let actual = parse_date_time(input);
362        assert!(actual.unwrap().unix_timestamp() == 1455616800);
363    }
364
365    #[test]
366    fn parse_date_time_dow_mon_format() {
367        let input = "Tue Feb 16 10:00:00 2016 +0100"; // dow_mon format with offset
368        let actual = parse_date_time(input);
369        assert!(actual.unwrap().unix_timestamp() == 1455613200);
370
371        let input = "Tue Feb 16 10:00:00 2016 +0000"; // dow_mon format UTC
372        let actual = parse_date_time(input);
373        assert!(actual.unwrap().unix_timestamp() == 1455616800);
374
375        let input = "Tue Feb 16 10:00:00 2016"; // dow_mon format no offset
376        let actual = parse_date_time(input);
377        assert!(actual.unwrap().unix_timestamp() == 1455616800);
378    }
379
380    #[test]
381    fn parse_date_time_unix_timestamp_format() {
382        let input = "0"; // epoch
383        let actual = parse_date_time(input);
384        assert!(actual.unwrap().unix_timestamp() == 0);
385
386        let input = "1455616800"; // positive
387        let actual = parse_date_time(input);
388        assert!(actual.unwrap().unix_timestamp() == 1455616800);
389
390        let input = "-1455616800"; // negative
391        let actual = parse_date_time(input);
392        assert!(actual.unwrap().unix_timestamp() == -1455616800);
393    }
394
395    #[test]
396    fn parse_date_time_to_string() {
397        let date = DateTime::now();
398        let input = date.to_string();
399        let actual = parse_date_time(&input);
400        assert!(actual.is_some());
401    }
402
403    #[derive(serde::Serialize, serde::Deserialize)]
404    struct TestSerde {
405        date: DateTime,
406    }
407
408    #[test]
409    fn serialize_deserialize_date_time() {
410        let yml = "---\ndate: \"2021-05-02 21:00:00 +0100\"\n";
411        let data: TestSerde = serde_yaml::from_str(yml).expect("could deserialize date");
412        let ser = serde_yaml::to_string(&data).expect("could serialize date");
413        assert_eq!(yml, ser);
414    }
415
416    #[test]
417    fn serialize_deserialize_date_time_ms() {
418        let yml = "---\ndate: \"2021-05-02 21:00:00.12 +0100\"\n";
419        let data: TestSerde = serde_yaml::from_str(yml).expect("could deserialize date");
420        let ser = serde_yaml::to_string(&data).expect("could serialize date");
421        assert_eq!(yml, ser);
422    }
423}