Skip to main content

zenkit/
datetime.rs

1//! implementation of custom datetime type so we can use deserialization
2//! when the date has date only and no time. In that case, we coerce it to a time with 00:00:00,
3//! and force its timezone to Utc for consitency. The application can easily
4//! change timezeone by calling with_timezone(tz).
5
6// use and re-export the Utc timezone
7pub use chrono::Utc;
8use serde::{de, Serialize};
9use std::{fmt, ops::Deref, str::FromStr};
10
11/// Variation on [`chrono::DateTime`](chrono::DateTime) that can parse and deserialize
12/// a Zenkit date with or without time.
13/// The value is always converted to Utc during parse/deserialization, for consistency,
14/// but the app can change timezone by calling with_timezone(tz).
15/// The type is declared generic over Timezone Tz, and it should work with different timezones for
16/// most operations; but parsing from string and deserializing will always create a <Utc> variant.
17#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
18pub struct DateTime<Tz: chrono::offset::TimeZone>(chrono::DateTime<Tz>);
19
20/// Deref implementation so chrono::DateTime methods should be nearly seamless
21impl<Tz: chrono::offset::TimeZone> Deref for DateTime<Tz> {
22    type Target = chrono::DateTime<Tz>;
23    fn deref(&self) -> &Self::Target {
24        &self.0
25    }
26}
27
28/// Implement Display
29impl<Tz: chrono::offset::TimeZone> fmt::Display for DateTime<Tz>
30where
31    Tz::Offset: fmt::Display,
32{
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "{}", self.0)
35    }
36}
37
38/// Implement Debug
39impl<Tz: chrono::offset::TimeZone> fmt::Debug for DateTime<Tz>
40where
41    Tz::Offset: fmt::Display,
42{
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(f, "{}", self.0)
45    }
46}
47
48struct DateTimeVisitor;
49
50impl<'de> de::Visitor<'de> for DateTimeVisitor {
51    type Value = DateTime<Utc>;
52
53    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
54        write!(formatter, "a formatted date or date and time string")
55    }
56
57    fn visit_str<E>(self, value: &str) -> Result<DateTime<Utc>, E>
58    where
59        E: de::Error,
60    {
61        parse_date(value).map_err(|err| E::custom(format!("{}", err)))
62    }
63}
64
65impl<'de> de::Deserialize<'de> for DateTime<Utc> {
66    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67    where
68        D: de::Deserializer<'de>,
69    {
70        deserializer.deserialize_str(DateTimeVisitor)
71    }
72}
73
74impl Serialize for DateTime<Utc> {
75    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
76    where
77        S: serde::Serializer,
78    {
79        self.0.serialize(serializer)
80    }
81}
82
83/// Parse date into DateTime, using either a full datetime format. or just the date (YYYY-MM-DD)
84fn parse_date(s: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
85    let dt: chrono::DateTime<Utc> = if s.len() == 10 {
86        let nd = chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")?;
87        chrono::DateTime::from_utc(nd.and_hms(0, 0, 0), Utc)
88    } else {
89        s.parse::<chrono::DateTime<chrono::FixedOffset>>()
90            .map(|dt| dt.with_timezone(&Utc))?
91    };
92    Ok(DateTime(dt))
93}
94
95impl FromStr for DateTime<Utc> {
96    type Err = chrono::ParseError;
97
98    /// Parse date into DateTime, using either a full datetime format. or just the date (YYYY-MM-DD).
99    /// If the date has a timezone, it will be converted to equivalent time in Utc.
100    fn from_str(s: &str) -> chrono::ParseResult<DateTime<Utc>> {
101        parse_date(s)
102    }
103}
104
105impl From<chrono::DateTime<Utc>> for DateTime<Utc> {
106    fn from(d: chrono::DateTime<Utc>) -> Self {
107        DateTime(d)
108    }
109}
110
111/// test serialize and deserialize implementation
112#[test]
113fn test_datetime_ser_deser() {
114    #[derive(Debug, Serialize, serde::Deserialize)]
115    struct DateStruct {
116        date: DateTime<Utc>,
117    }
118
119    let val: DateStruct = serde_json::from_str(r#"{ "date": "2020-01-01" }"#).unwrap();
120    assert_eq!(
121        format!("date: {:?}", val.date),
122        "date: 2020-01-01 00:00:00 UTC"
123    );
124
125    let json = serde_json::to_string(&val).unwrap();
126    assert_eq!(json, r#"{"date":"2020-01-01T00:00:00Z"}"#);
127}
128
129/// test impl Debug
130#[test]
131fn test_datetime_debug() {
132    let dt = "2020-01-01T01:02:03Z".parse::<DateTime<Utc>>().unwrap();
133    assert_eq!(format!("{:?}", dt), "2020-01-01 01:02:03 UTC");
134}
135
136/// test impl Display
137#[test]
138fn test_datetime_display() {
139    let dt = "2020-01-01T01:02:03Z".parse::<DateTime<Utc>>().unwrap();
140    assert_eq!(format!("{}", dt), "2020-01-01 01:02:03 UTC");
141}
142
143/// test parse iso8601 format
144#[test]
145fn test_datetime_parse_iso8601() {
146    let dt = "2020-01-01T01:02:03Z".parse::<DateTime<Utc>>().unwrap();
147    assert_eq!(format!("{}", dt), "2020-01-01 01:02:03 UTC");
148    assert_eq!(format!("{}", dt.0), "2020-01-01 01:02:03 UTC");
149}
150
151/// test parse short date "YYYY-MM-DD"
152#[test]
153fn test_datetime_parse_short() {
154    let dt = "2020-01-01".parse::<DateTime<Utc>>().unwrap();
155    assert_eq!(format!("{}", dt), "2020-01-01 00:00:00 UTC");
156    assert_eq!(format!("{}", dt.0), "2020-01-01 00:00:00 UTC");
157}