1use super::Object;
2
3#[cfg(feature = "chrono")]
4mod chrono_impl {
5 use crate::{datetime::convert_utc_offset, Object};
6 use chrono::prelude::*;
7
8 impl From<DateTime<Local>> for Object {
9 fn from(date: DateTime<Local>) -> Self {
10 let mut timezone_str = date.format("D:%Y%m%d%H%M%S%:z'").to_string().into_bytes();
11 convert_utc_offset(&mut timezone_str);
12 Object::string_literal(timezone_str)
13 }
14 }
15
16 impl From<DateTime<Utc>> for Object {
17 fn from(date: DateTime<Utc>) -> Self {
18 Object::string_literal(date.format("D:%Y%m%d%H%M%SZ").to_string())
19 }
20 }
21
22 impl TryFrom<super::DateTime> for DateTime<Local> {
23 type Error = chrono::format::ParseError;
24
25 fn try_from(value: super::DateTime) -> Result<DateTime<Local>, Self::Error> {
26 let from_date = |date: NaiveDate| {
27 FixedOffset::east_opt(0)
28 .unwrap()
29 .from_utc_datetime(&date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap()))
30 };
31
32 DateTime::parse_from_str(&value.0, "%Y%m%d%H%M%S%#z")
33 .or_else(|_| DateTime::parse_from_str(&value.0, "%Y%m%d%H%M%#z"))
34 .or_else(|_| NaiveDate::parse_from_str(&value.0, "%Y%m%d").map(from_date))
35 .map(|date| date.with_timezone(&Local))
36 }
37 }
38}
39
40#[cfg(feature = "jiff")]
41mod jiff_impl {
42 use crate::{datetime::convert_utc_offset, Object};
43 use jiff::{Timestamp, Zoned};
44
45 impl From<Zoned> for Object {
46 fn from(date: Zoned) -> Self {
47 let mut timezone_str = date.strftime("D:%Y%m%d%H%M%S%:z'").to_string().into_bytes();
48 convert_utc_offset(&mut timezone_str);
49 Object::string_literal(timezone_str)
50 }
51 }
52
53 impl From<Timestamp> for Object {
54 fn from(date: Timestamp) -> Self {
55 Object::string_literal(date.strftime("D:%Y%m%d%H%M%SZ").to_string())
56 }
57 }
58
59 impl TryFrom<super::DateTime> for Zoned {
60 type Error = jiff::Error;
61
62 fn try_from(value: super::DateTime) -> Result<Self, Self::Error> {
63 use jiff::civil::{Date, DateTime};
64
65 Zoned::strptime("%Y%m%d%H%M%S%#z", &value.0)
86 .or_else(|_| DateTime::strptime("%Y%m%d%H%M%SZ", &value.0).and_then(|dt| dt.in_tz("UTC")))
87 .or_else(|_| Zoned::strptime("%Y%m%d%H%M%#z", &value.0))
88 .or_else(|_| DateTime::strptime("%Y%m%d%H%MZ", &value.0).and_then(|dt| dt.in_tz("UTC")))
89 .or_else(|_| Date::strptime("%Y%m%d", &value.0).and_then(|dt| dt.at(0, 0, 0, 0).in_tz("GMT")))
90 }
91 }
92}
93
94#[cfg(feature = "time")]
95mod time_impl {
96 use crate::Object;
97 use time::{format_description::FormatItem, OffsetDateTime, Time};
98
99 impl From<Time> for Object {
100 fn from(date: Time) -> Self {
101 Object::string_literal(
103 format!(
104 "D:{}",
105 date.format(&FormatItem::Literal("%Y%m%d%H%M%SZ".as_bytes())).unwrap()
106 )
107 .into_bytes(),
108 )
109 }
110 }
111
112 impl From<OffsetDateTime> for Object {
113 fn from(date: OffsetDateTime) -> Self {
114 Object::string_literal({
115 let format = time::format_description::parse(
117 "D:[year][month][day][hour][minute][second][offset_hour sign:mandatory]'[offset_minute]'",
118 )
119 .unwrap();
120 date.format(&format).unwrap()
121 })
122 }
123 }
124
125 impl TryFrom<super::DateTime> for OffsetDateTime {
130 type Error = time::Error;
131
132 fn try_from(value: super::DateTime) -> Result<OffsetDateTime, Self::Error> {
133 let format = time::format_description::parse(
134 "[year][month][day][hour][minute][second][offset_hour sign:mandatory][offset_minute]",
135 )
136 .unwrap();
137
138 Ok(OffsetDateTime::parse(&value.0, &format)?)
139 }
140 }
141}
142
143#[allow(dead_code)]
145fn convert_utc_offset(bytes: &mut [u8]) {
146 let mut index = bytes.len();
147 while let Some(last) = bytes[..index].last_mut() {
148 if *last == b':' {
149 *last = b'\'';
150 break;
151 }
152 index -= 1;
153 }
154}
155
156#[derive(Clone, Debug)]
157pub struct DateTime(String);
158
159impl Object {
160 fn datetime_string(&self) -> Option<String> {
162 if let Object::String(bytes, _) = self {
163 String::from_utf8(
164 bytes
165 .iter()
166 .filter(|b| ![b'D', b':', b'\''].contains(b))
167 .cloned()
168 .collect(),
169 )
170 .ok()
171 } else {
172 None
173 }
174 }
175
176 pub fn as_datetime(&self) -> Option<DateTime> {
177 self.datetime_string().map(DateTime)
178 }
179}
180
181#[cfg(feature = "chrono")]
182#[test]
183fn parse_datetime_local() {
184 use chrono::prelude::*;
185
186 let time = Local::now().with_nanosecond(0).unwrap();
187 let text: Object = time.into();
188 let time2: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
189 assert_eq!(time2, Some(time));
190}
191
192#[cfg(feature = "chrono")]
193#[test]
194fn parse_datetime_utc() {
195 use chrono::prelude::*;
196
197 let time = Utc::now().with_nanosecond(0).unwrap();
198 let text: Object = time.into();
199 let time2: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
200 assert_eq!(time2, Some(time.with_timezone(&Local)));
201}
202
203#[cfg(feature = "jiff")]
204#[test]
205fn parse_zoned() {
206 use jiff::Zoned;
207
208 let time = Zoned::now().with().subsec_nanosecond(0).build().unwrap();
209 let text: Object = time.clone().into();
210 let time2: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
211 assert_eq!(time2, Some(time));
212}
213
214#[cfg(feature = "jiff")]
215#[test]
216fn parse_timestamp() {
217 use jiff::Zoned;
218
219 let time = Zoned::now().with().subsec_nanosecond(0).build().unwrap();
220 let text: Object = time.timestamp().into();
221 let time2: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
222 assert_eq!(time2, Some(time));
223}
224
225#[cfg(feature = "chrono")]
226#[test]
227fn parse_datetime_seconds_missing_chrono() {
228 use chrono::prelude::*;
229
230 let text = Object::string_literal("D:199812231952-08'00'");
232 let dt: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
233 assert!(dt.is_some());
234}
235
236#[cfg(feature = "chrono")]
237#[test]
238fn parse_datetime_time_missing_chrono() {
239 use chrono::prelude::*;
240
241 let text = Object::string_literal("D:20040229");
242 let dt: Option<DateTime<Local>> = text.as_datetime().and_then(|dt| dt.try_into().ok());
243 assert!(dt.is_some());
244}
245
246#[cfg(feature = "jiff")]
247#[test]
248fn parse_datetime_seconds_missing_jiff() {
249 use jiff::Zoned;
250
251 let text = Object::string_literal("D:199812231952-08'00'");
253 let dt: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
254 assert!(dt.is_some());
255}
256
257#[cfg(feature = "jiff")]
258#[test]
259fn parse_datetime_time_missing_jiff() {
260 use jiff::Zoned;
261
262 let text = Object::string_literal("D:20040229");
263 let dt: Option<Zoned> = text.as_datetime().and_then(|dt| dt.try_into().ok());
264 assert!(dt.is_some());
265}
266
267#[cfg(feature = "time")]
268#[test]
269fn parse_datetime() {
270 use time::OffsetDateTime;
271
272 let time = OffsetDateTime::now_utc();
273
274 let text: Object = time.into();
275 let time2: OffsetDateTime = text.as_datetime().unwrap().try_into().unwrap();
276
277 assert_eq!(time2.date(), time.date());
278
279 assert_eq!(time2.time().hour(), time.time().hour());
282 assert_eq!(time2.time().minute(), time.time().minute());
283 assert_eq!(time2.time().second(), time.time().second());
284}