date.rs

  1// Copyright (c) 2017 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
  2//
  3// This Source Code Form is subject to the terms of the Mozilla Public
  4// License, v. 2.0. If a copy of the MPL was not distributed with this
  5// file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6
  7use crate::util::error::Error;
  8use chrono::{DateTime as ChronoDateTime, FixedOffset};
  9use minidom::{IntoAttributeValue, Node};
 10use std::str::FromStr;
 11
 12/// Implements the DateTime profile of XEP-0082, which represents a
 13/// non-recurring moment in time, with an accuracy of seconds or fraction of
 14/// seconds, and includes a timezone.
 15#[derive(Debug, Clone, PartialEq)]
 16pub struct DateTime(pub ChronoDateTime<FixedOffset>);
 17
 18impl DateTime {
 19    /// Retrieves the associated timezone.
 20    pub fn timezone(&self) -> FixedOffset {
 21        self.0.timezone()
 22    }
 23
 24    /// Returns a new `DateTime` with a different timezone.
 25    pub fn with_timezone(&self, tz: FixedOffset) -> DateTime {
 26        DateTime(self.0.with_timezone(&tz))
 27    }
 28
 29    /// Formats this `DateTime` with the specified format string.
 30    pub fn format(&self, fmt: &str) -> String {
 31        format!("{}", self.0.format(fmt))
 32    }
 33}
 34
 35impl FromStr for DateTime {
 36    type Err = Error;
 37
 38    fn from_str(s: &str) -> Result<DateTime, Error> {
 39        Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
 40    }
 41}
 42
 43impl IntoAttributeValue for DateTime {
 44    fn into_attribute_value(self) -> Option<String> {
 45        Some(self.0.to_rfc3339())
 46    }
 47}
 48
 49impl From<DateTime> for Node {
 50    fn from(date: DateTime) -> Node {
 51        Node::Text(date.0.to_rfc3339())
 52    }
 53}
 54
 55#[cfg(test)]
 56mod tests {
 57    use super::*;
 58    use chrono::{Datelike, Timelike};
 59
 60    // DateTime’s size doesn’t depend on the architecture.
 61    #[test]
 62    fn test_size() {
 63        assert_size!(DateTime, 16);
 64    }
 65
 66    #[test]
 67    fn test_simple() {
 68        let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
 69        assert_eq!(date.0.year(), 2002);
 70        assert_eq!(date.0.month(), 9);
 71        assert_eq!(date.0.day(), 10);
 72        assert_eq!(date.0.hour(), 23);
 73        assert_eq!(date.0.minute(), 08);
 74        assert_eq!(date.0.second(), 25);
 75        assert_eq!(date.0.nanosecond(), 0);
 76        assert_eq!(date.0.timezone(), FixedOffset::east_opt(0).unwrap());
 77    }
 78
 79    #[test]
 80    fn test_invalid_date() {
 81        // There is no thirteenth month.
 82        let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
 83        let message = match error {
 84            Error::ChronoParseError(string) => string,
 85            _ => panic!(),
 86        };
 87        assert_eq!(message.to_string(), "input is out of range");
 88
 89        // Timezone ≥24:00 aren’t allowed.
 90        let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
 91        let message = match error {
 92            Error::ChronoParseError(string) => string,
 93            _ => panic!(),
 94        };
 95        assert_eq!(message.to_string(), "input is out of range");
 96
 97        // Timezone without the : separator aren’t allowed.
 98        let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
 99        let message = match error {
100            Error::ChronoParseError(string) => string,
101            _ => panic!(),
102        };
103        assert_eq!(message.to_string(), "input contains invalid characters");
104
105        // No seconds, error message could be improved.
106        let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
107        let message = match error {
108            Error::ChronoParseError(string) => string,
109            _ => panic!(),
110        };
111        assert_eq!(message.to_string(), "input contains invalid characters");
112
113        // TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
114        let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
115        let message = match error {
116            Error::ChronoParseError(string) => string,
117            _ => panic!(),
118        };
119        assert_eq!(message.to_string(), "input contains invalid characters");
120
121        // No timezone.
122        let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
123        let message = match error {
124            Error::ChronoParseError(string) => string,
125            _ => panic!(),
126        };
127        assert_eq!(message.to_string(), "premature end of input");
128    }
129
130    #[test]
131    fn test_serialise() {
132        let date =
133            DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
134        let attr = date.into_attribute_value();
135        assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
136    }
137}