liquid_core/model/scalar/
datetime.rs1use std::convert::TryInto;
2use std::fmt;
3use std::ops;
4
5mod strftime;
6
7use super::Date;
8
9#[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 pub fn now() -> Self {
25 Self {
26 inner: DateTimeImpl::now_utc(),
27 }
28 }
29
30 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 #[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 pub fn with_date(self, other: Date) -> Self {
55 Self {
56 inner: self.inner.replace_date(other.inner),
57 }
58 }
59
60 pub fn with_offset(self, offset: time::UtcOffset) -> Self {
62 Self {
63 inner: self.inner.to_offset(offset),
64 }
65 }
66
67 pub fn date(self) -> Date {
69 Date {
70 inner: self.inner.date(),
71 }
72 }
73
74 #[inline]
79 pub fn format(&self, fmt: &str) -> Result<String, strftime::DateFormatError> {
80 strftime::strftime(self.inner, fmt)
81 }
82
83 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 #[inline]
94 pub fn year(&self) -> i32 {
95 self.inner.year()
96 }
97 #[inline]
99 pub fn month(&self) -> u8 {
100 self.inner.month() as u8
101 }
102 #[inline]
106 pub fn day(&self) -> u8 {
107 self.inner.day()
108 }
109 #[inline]
113 pub fn ordinal(&self) -> u16 {
114 self.inner.ordinal()
115 }
116 #[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
201fn 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"; let actual = parse_date_time(input);
294 assert!(actual.unwrap().unix_timestamp() == 1455613200);
295
296 let input = "2016-02-16 10:00:00 +0000"; let actual = parse_date_time(input);
298 assert!(actual.unwrap().unix_timestamp() == 1455616800);
299
300 let input = "2016-02-16 10:00:00"; 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"; 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"; 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"; 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"; let actual = parse_date_time(input);
324 assert!(actual.unwrap().unix_timestamp() == 1455613200);
325
326 let input = "16 February 2016 10:00:00 +0000"; let actual = parse_date_time(input);
328 assert!(actual.unwrap().unix_timestamp() == 1455616800);
329
330 let input = "16 February 2016 10:00:00"; 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"; let actual = parse_date_time(input);
339 assert!(actual.unwrap().unix_timestamp() == 1455613200);
340
341 let input = "16 Feb 2016 10:00:00 +0000"; let actual = parse_date_time(input);
343 assert!(actual.unwrap().unix_timestamp() == 1455616800);
344
345 let input = "16 Feb 2016 10:00:00"; 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"; let actual = parse_date_time(input);
354 assert!(actual.unwrap().unix_timestamp() == 1455613200);
355
356 let input = "02/16/2016 10:00:00 +0000"; let actual = parse_date_time(input);
358 assert!(actual.unwrap().unix_timestamp() == 1455616800);
359
360 let input = "02/16/2016 10:00:00"; 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"; 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"; let actual = parse_date_time(input);
373 assert!(actual.unwrap().unix_timestamp() == 1455616800);
374
375 let input = "Tue Feb 16 10:00:00 2016"; 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"; let actual = parse_date_time(input);
384 assert!(actual.unwrap().unix_timestamp() == 0);
385
386 let input = "1455616800"; let actual = parse_date_time(input);
388 assert!(actual.unwrap().unix_timestamp() == 1455616800);
389
390 let input = "-1455616800"; 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}