liquid_core/model/scalar/
date.rs

1use std::{convert::TryInto, fmt, ops};
2
3/// Liquid's native date only type.
4#[derive(
5    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize,
6)]
7#[serde(transparent)]
8pub struct Date {
9    #[serde(with = "friendly_date")]
10    pub(crate) inner: DateImpl,
11}
12
13type DateImpl = time::Date;
14
15impl Date {
16    /// Makes a new NaiveDate from the calendar date (year, month and day).
17    ///
18    /// Panics on the out-of-range date, invalid month and/or day.
19    pub fn from_ymd(year: i32, month: u8, day: u8) -> Self {
20        Self {
21            inner: DateImpl::from_calendar_date(
22                year,
23                month.try_into().expect("the month is out of range"),
24                day,
25            )
26            .expect("one or more components were invalid"),
27        }
28    }
29
30    /// Convert a `str` to `Self`
31    #[allow(clippy::should_implement_trait)]
32    pub fn from_str(other: &str) -> Option<Self> {
33        parse_date(other).map(|d| Self { inner: d })
34    }
35}
36
37impl Date {
38    /// Get the year of the date.
39    #[inline]
40    pub fn year(&self) -> i32 {
41        self.inner.year()
42    }
43    /// Get the month.
44    #[inline]
45    pub fn month(&self) -> u8 {
46        self.inner.month() as u8
47    }
48    /// Get the day of the month.
49    ///
50    //// The returned value will always be in the range 1..=31.
51    #[inline]
52    pub fn day(&self) -> u8 {
53        self.inner.day()
54    }
55    /// Get the day of the year.
56    ///
57    /// The returned value will always be in the range 1..=366 (1..=365 for common years).
58    #[inline]
59    pub fn ordinal(&self) -> u16 {
60        self.inner.ordinal()
61    }
62    /// Get the ISO week number.
63    ///
64    /// The returned value will always be in the range 1..=53.
65    #[inline]
66    pub fn iso_week(&self) -> u8 {
67        self.inner.iso_week()
68    }
69}
70
71impl fmt::Display for Date {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        write!(
74            f,
75            "{}",
76            self.inner.format(DATE_FORMAT).map_err(|_e| fmt::Error)?
77        )
78    }
79}
80
81impl ops::Deref for Date {
82    type Target = DateImpl;
83    fn deref(&self) -> &Self::Target {
84        &self.inner
85    }
86}
87
88impl ops::DerefMut for Date {
89    fn deref_mut(&mut self) -> &mut Self::Target {
90        &mut self.inner
91    }
92}
93
94const DATE_FORMAT: &[time::format_description::FormatItem<'_>] =
95    time::macros::format_description!("[year]-[month]-[day]");
96
97mod friendly_date {
98    use super::*;
99    use serde::{self, Deserialize, Deserializer, Serializer};
100
101    pub(crate) fn serialize<S>(date: &DateImpl, serializer: S) -> Result<S::Ok, S::Error>
102    where
103        S: Serializer,
104    {
105        let s = date
106            .format(DATE_FORMAT)
107            .map_err(serde::ser::Error::custom)?;
108        serializer.serialize_str(&s)
109    }
110
111    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<DateImpl, D::Error>
112    where
113        D: Deserializer<'de>,
114    {
115        let s = String::deserialize(deserializer)?;
116        DateImpl::parse(&s, DATE_FORMAT).map_err(serde::de::Error::custom)
117    }
118}
119
120fn parse_date(s: &str) -> Option<DateImpl> {
121    const USER_FORMATS: &[&[time::format_description::FormatItem<'_>]] = &[
122        time::macros::format_description!("[day] [month repr:long] [year]"),
123        time::macros::format_description!("[day] [month repr:short] [year]"),
124        DATE_FORMAT,
125    ];
126
127    match s {
128        "today" => Some(time::OffsetDateTime::now_utc().date()),
129        _ => USER_FORMATS
130            .iter()
131            .filter_map(|f| DateImpl::parse(s, f).ok())
132            .next(),
133    }
134}
135
136#[cfg(test)]
137mod test {
138    use super::*;
139    #[test]
140    fn parse_date_time_empty_is_bad() {
141        let input = "";
142        let actual = parse_date(input);
143        assert!(actual.is_none());
144    }
145
146    #[test]
147    fn parse_date_time_bad() {
148        let input = "aaaaa";
149        let actual = parse_date(input);
150        assert!(actual.is_none());
151    }
152
153    #[test]
154    fn parse_date_today() {
155        let input = "today";
156        let actual = parse_date(input);
157        assert!(actual.is_some());
158    }
159
160    #[test]
161    fn parse_long_month() {
162        let input = "01 March 2022";
163        let actual = parse_date(input);
164        assert_eq!(
165            DateImpl::from_calendar_date(2022, time::Month::March, 1).unwrap(),
166            actual.unwrap()
167        );
168    }
169
170    #[test]
171    fn parse_short_month() {
172        let input = "01 Mar 2022";
173        let actual = parse_date(input);
174        assert_eq!(
175            DateImpl::from_calendar_date(2022, time::Month::March, 1).unwrap(),
176            actual.unwrap()
177        );
178    }
179
180    #[test]
181    fn parse_iso() {
182        let input = "2022-03-02";
183        let actual = parse_date(input);
184        assert_eq!(
185            DateImpl::from_calendar_date(2022, time::Month::March, 2).unwrap(),
186            actual.unwrap()
187        );
188    }
189}