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 chrono::{DateTime as ChronoDateTime, FixedOffset};
8use minidom::{IntoAttributeValue, Node};
9use std::str::FromStr;
10
11/// Implements the DateTime profile of XEP-0082, which represents a
12/// non-recurring moment in time, with an accuracy of seconds or fraction of
13/// seconds, and includes a timezone.
14#[derive(Debug, Clone, PartialEq)]
15pub struct DateTime(pub ChronoDateTime<FixedOffset>);
16
17impl DateTime {
18 /// Retrieves the associated timezone.
19 pub fn timezone(&self) -> FixedOffset {
20 self.0.timezone()
21 }
22
23 /// Returns a new `DateTime` with a different timezone.
24 pub fn with_timezone(&self, tz: FixedOffset) -> DateTime {
25 DateTime(self.0.with_timezone(&tz))
26 }
27
28 /// Formats this `DateTime` with the specified format string.
29 pub fn format(&self, fmt: &str) -> String {
30 format!("{}", self.0.format(fmt))
31 }
32}
33
34impl FromStr for DateTime {
35 type Err = chrono::ParseError;
36
37 fn from_str(s: &str) -> Result<DateTime, Self::Err> {
38 Ok(DateTime(ChronoDateTime::parse_from_rfc3339(s)?))
39 }
40}
41
42impl IntoAttributeValue for DateTime {
43 fn into_attribute_value(self) -> Option<String> {
44 Some(self.0.to_rfc3339())
45 }
46}
47
48impl From<DateTime> for Node {
49 fn from(date: DateTime) -> Node {
50 Node::Text(date.0.to_rfc3339())
51 }
52}
53
54#[cfg(test)]
55mod tests {
56 use super::*;
57 use chrono::{Datelike, Timelike};
58
59 // DateTime’s size doesn’t depend on the architecture.
60 #[test]
61 fn test_size() {
62 assert_size!(DateTime, 16);
63 }
64
65 #[test]
66 fn test_simple() {
67 let date: DateTime = "2002-09-10T23:08:25Z".parse().unwrap();
68 assert_eq!(date.0.year(), 2002);
69 assert_eq!(date.0.month(), 9);
70 assert_eq!(date.0.day(), 10);
71 assert_eq!(date.0.hour(), 23);
72 assert_eq!(date.0.minute(), 08);
73 assert_eq!(date.0.second(), 25);
74 assert_eq!(date.0.nanosecond(), 0);
75 assert_eq!(date.0.timezone(), FixedOffset::east_opt(0).unwrap());
76 }
77
78 #[test]
79 fn test_invalid_date() {
80 // There is no thirteenth month.
81 let error = DateTime::from_str("2017-13-01T12:23:34Z").unwrap_err();
82 assert_eq!(error.to_string(), "input is out of range");
83
84 // Timezone ≥24:00 aren’t allowed.
85 let error = DateTime::from_str("2017-05-27T12:11:02+25:00").unwrap_err();
86 assert_eq!(error.to_string(), "input is out of range");
87
88 // Timezone without the : separator aren’t allowed.
89 let error = DateTime::from_str("2017-05-27T12:11:02+0100").unwrap_err();
90 assert_eq!(error.to_string(), "input contains invalid characters");
91
92 // No seconds, error message could be improved.
93 let error = DateTime::from_str("2017-05-27T12:11+01:00").unwrap_err();
94 assert_eq!(error.to_string(), "input contains invalid characters");
95
96 // TODO: maybe we’ll want to support this one, as per XEP-0082 §4.
97 let error = DateTime::from_str("20170527T12:11:02+01:00").unwrap_err();
98 assert_eq!(error.to_string(), "input contains invalid characters");
99
100 // No timezone.
101 let error = DateTime::from_str("2017-05-27T12:11:02").unwrap_err();
102 assert_eq!(error.to_string(), "premature end of input");
103 }
104
105 #[test]
106 fn test_serialise() {
107 let date =
108 DateTime(ChronoDateTime::parse_from_rfc3339("2017-05-21T20:19:55+01:00").unwrap());
109 let attr = date.into_attribute_value();
110 assert_eq!(attr, Some(String::from("2017-05-21T20:19:55+01:00")));
111 }
112}