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(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 Into<Node> for DateTime {
 50    fn into(self) -> Node {
 51        Node::Text(self.0.to_rfc3339())
 52    }
 53}
 54
 55#[cfg(test)]
 56mod tests {
 57    use super::*;
 58    use chrono::{Datelike, Timelike};
 59    use std::error::Error as StdError;
 60
 61    // DateTime’s size doesn’t depend on the architecture.
 62    #[test]
 63    fn test_size() {
 64        assert_size!(DateTime, 16);
 65    }
 66
 67    #[test]
 68    fn test_simple() {
 69        let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
 70        assert_eq!(date.0.year(), 2002);
 71        assert_eq!(date.0.month(), 9);
 72        assert_eq!(date.0.day(), 10);
 73        assert_eq!(date.0.hour(), 23);
 74        assert_eq!(date.0.minute(), 08);
 75        assert_eq!(date.0.second(), 25);
 76        assert_eq!(date.0.nanosecond(), 0);
 77        assert_eq!(date.0.timezone(), FixedOffset::east(0));
 78    }
 79
 80    #[test]
 81    fn test_invalid_date() {
 82        // There is no thirteenth month.
 83        let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
 84        let message = match error {
 85            Error::ChronoParseError(string) => string,
 86            _ => panic!(),
 87        };
 88        assert_eq!(message.description(), "input is out of range");
 89
 90        // Timezone ≥24:00 aren’t allowed.
 91        let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
 92        let message = match error {
 93            Error::ChronoParseError(string) => string,
 94            _ => panic!(),
 95        };
 96        assert_eq!(message.description(), "input is out of range");
 97
 98        // Timezone without the : separator aren’t allowed.
 99        let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
100        let message = match error {
101            Error::ChronoParseError(string) => string,
102            _ => panic!(),
103        };
104        assert_eq!(message.description(), "input contains invalid characters");
105
106        // No seconds, error message could be improved.
107        let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
108        let message = match error {
109            Error::ChronoParseError(string) => string,
110            _ => panic!(),
111        };
112        assert_eq!(message.description(), "input contains invalid characters");
113
114        // TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
115        let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
116        let message = match error {
117            Error::ChronoParseError(string) => string,
118            _ => panic!(),
119        };
120        assert_eq!(message.description(), "input contains invalid characters");
121
122        // No timezone.
123        let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
124        let message = match error {
125            Error::ChronoParseError(string) => string,
126            _ => panic!(),
127        };
128        assert_eq!(message.description(), "premature end of input");
129    }
130
131    #[test]
132    fn test_serialise() {
133        let date =
134            DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
135        let attr = date.into_attribute_value();
136        assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
137    }
138}