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}