1use chrono::{
7 DateTime, Datelike, FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, ParseError,
8 TimeZone,
9};
10
11#[cfg(test)]
12mod tests;
13
14type Error = String;
15
16#[derive(Debug)]
32pub struct DateTimeFixedOffset(pub DateTime<FixedOffset>);
33
34impl std::str::FromStr for DateTimeFixedOffset {
35 type Err = Error;
36
37 fn from_str(s: &str) -> Result<Self, Error> {
38 parse_from(s).map(DateTimeFixedOffset)
39 }
40}
41
42fn parse_from(date_time: &str) -> Result<DateTime<FixedOffset>, Error> {
45 if date_time.is_empty() {
46 Err("cannot be empty".to_string())
47 } else {
48 let date_time = standardize_date(date_time);
49 from_unix_timestamp(&date_time)
50 .or_else(|_| DateTime::parse_from_str(&date_time, "%+"))
51 .or_else(|_| from_datetime_with_tz(&date_time))
52 .or_else(|_| from_datetime_without_tz(&date_time))
53 .or_else(|_| from_date_without_tz(&date_time))
54 .or_else(|_| from_time_without_tz(&date_time))
55 .or_else(|_| from_time_with_tz(&date_time))
56 .or_else(|_| try_yms_hms_tz(&date_time))
57 .or_else(|_| try_dmmmy_hms_tz(&date_time))
58 .or_else(|_| try_mmmddyyyy_hms_tz(&date_time))
59 .or_else(|_| from_datetime_with_tz_before_year(&date_time))
60 .or_else(|_| try_others(&date_time))
61 }
62}
63
64fn from_unix_timestamp(s: &str) -> Result<DateTime<FixedOffset>, Error> {
65 let tts = s.parse::<i64>().map_err(|e| e.to_string())?;
66 let dt = if s.len() <= 10 {
67 chrono::naive::NaiveDateTime::from_timestamp(tts, 0)
69 } else if s.len() <= 13 {
70 chrono::naive::NaiveDateTime::from_timestamp(tts / 1000, (tts % 1000) as u32 * 1000000)
72 } else if s.len() <= 16 {
73 chrono::naive::NaiveDateTime::from_timestamp(tts / 1000000, (tts % 1000000) as u32 * 1000)
75 } else {
76 chrono::naive::NaiveDateTime::from_timestamp(tts / 1000000000, (tts % 1000000000) as u32)
78 };
79 Ok(chrono::DateTime::<FixedOffset>::from_utc(
80 dt,
81 FixedOffset::east(0),
82 ))
83}
84fn from_datetime_with_tz(s: &str) -> Result<DateTime<FixedOffset>, ParseError> {
86 DateTime::parse_from_rfc3339(s)
87 .or_else(|_| DateTime::parse_from_rfc2822(s))
88 .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%dT%T%.f%z"))
89 .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%d %T%#z"))
90 .or_else(|_| DateTime::parse_from_str(s, "%Y-%m-%d %T.%f%#z"))
91 .or_else(|_| DateTime::parse_from_str(s, "%B %d %Y %T %#z"))
92 .or_else(|_| DateTime::parse_from_str(s, "%B %d %Y %T.%f%#z"))
93 .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %Y %T.%f%#z"))
94 .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %Y %T %#z"))
95 .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %T %#z %Y"))
96 .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %T %#z %Y"))
97 .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %T.%f %#z %Y"))
98 .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %T.%f %#z %Y"))
99 .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %H:%M %#z %Y"))
100 .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %H:%M %#z %Y"))
101 .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %I:%M %P %#z %Y"))
102 .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %I:%M %P %#z %Y"))
103 .or_else(|_| DateTime::parse_from_str(s, "%A %d %B %I:%M%P %#z %Y"))
104 .or_else(|_| DateTime::parse_from_str(s, "%A %B %d %I:%M%P %#z %Y"))
105}
106
107fn from_datetime_without_tz(s: &str) -> Result<DateTime<FixedOffset>, ParseError> {
110 Local
111 .datetime_from_str(s, "%Y-%m-%dT%T")
112 .or_else(|_| Local.datetime_from_str(s, "%c"))
113 .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%dT%T.%f"))
114 .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T"))
115 .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T.%f"))
116 .or_else(|_| Local.datetime_from_str(s, "%B %d %Y %T"))
117 .or_else(|_| Local.datetime_from_str(s, "%B %d %Y %T.%f"))
118 .or_else(|_| Local.datetime_from_str(s, "%B %d, %Y %T"))
119 .or_else(|_| Local.datetime_from_str(s, "%B %d, %Y %T.%f"))
120 .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T"))
121 .or_else(|_| Local.datetime_from_str(s, "%Y-%m-%d %T.%f"))
122 .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %T.%f"))
123 .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %T"))
124 .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M%P"))
125 .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M %P"))
126 .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M:%S%P"))
127 .or_else(|_| Local.datetime_from_str(s, "%A %d %B %Y %I:%M:%S %P"))
128 .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M%P"))
129 .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M %P"))
130 .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M:%S%P"))
131 .or_else(|_| Local.datetime_from_str(s, "%A %d %m %Y %I:%M:%S %P"))
132 .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M%P"))
133 .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M %P"))
134 .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M:%S%P"))
135 .or_else(|_| Local.datetime_from_str(s, "%d %B %Y %I:%M:%S %P"))
136 .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M%P"))
137 .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M %P"))
138 .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M:%S%P"))
139 .or_else(|_| Local.datetime_from_str(s, "%d %m %Y %I:%M:%S %P"))
140 .or_else(|_| Local.datetime_from_str(s, "%-m-%-d-%Y %-H:%-M:%-S %p"))
141 .map(|x| x.with_timezone(x.offset()))
142}
143
144fn from_date_without_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
146 NaiveDate::parse_from_str(s, "%Y-%m-%d")
147 .or_else(|_| NaiveDate::parse_from_str(s, "%m-%d-%y"))
148 .or_else(|_| NaiveDate::parse_from_str(s, "%D"))
149 .or_else(|_| NaiveDate::parse_from_str(s, "%F"))
150 .or_else(|_| NaiveDate::parse_from_str(s, "%v"))
151 .or_else(|_| NaiveDate::parse_from_str(s, "%B %d %Y"))
152 .or_else(|_| NaiveDate::parse_from_str(s, "%d %B %Y"))
153 .map(|x| x.and_hms(0, 0, 0))
154 .map(|x| Local.from_local_datetime(&x))
155 .map_err(|e| e.to_string())
156 .map(|x| x.unwrap().with_timezone(x.unwrap().offset()))
157}
158
159fn from_time_without_tz(s: &str) -> Result<DateTime<FixedOffset>, ParseError> {
162 NaiveTime::parse_from_str(s, "%T")
163 .or_else(|_| NaiveTime::parse_from_str(s, "%I:%M%P"))
164 .or_else(|_| NaiveTime::parse_from_str(s, "%I:%M %P"))
165 .map(|x| Local::now().date().and_time(x).unwrap())
166 .map(|x| x.with_timezone(x.offset()))
167}
168
169fn from_time_with_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
172 if let Some((dt, tz)) = is_tz_alpha(s) {
173 let date = format!("{} {}", Local::today().format("%Y-%m-%d"), dt);
174 to_rfc2822(&date, tz)
175 } else {
176 Err("custom parsing failed".to_string())
177 }
178}
179
180fn from_datetime_with_tz_before_year(s: &str) -> Result<DateTime<FixedOffset>, Error> {
183 let tokens = s.split_whitespace().collect::<Vec<_>>();
184 if tokens.len() < 2 {
185 return Err("custom parsing failed".to_string());
186 }
187 let dt = tokens[..tokens.len() - 2].join(" ") + " " + tokens.last().unwrap();
188 let tz = tokens[tokens.len() - 2];
189 to_rfc2822(&dt, tz)
190}
191
192fn try_yms_hms_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
196 if let Some((dt, tz)) = is_tz_alpha(s) {
197 to_rfc2822(dt, tz)
198 } else {
199 Err("custom parsing failed".to_string())
200 }
201}
202
203fn try_dmmmy_hms_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
208 if let Some((dt, tz)) = is_tz_alpha(s) {
209 to_rfc2822(dt, tz)
210 } else {
211 Err("custom parsing failed".to_string())
212 }
213}
214
215fn try_mmmddyyyy_hms_tz(s: &str) -> Result<DateTime<FixedOffset>, Error> {
219 let tz = s.rsplitn(2, ' ').take(2).collect::<Vec<_>>()[0].replace("GMT", "");
220 let dt = if s.rsplitn(2, ' ').count() > 1 {
221 s.rsplitn(2, ' ').take(2).collect::<Vec<_>>()[1].to_string()
222 } else {
223 return Err("custom parsing failed".to_string());
224 };
225 if !tz.is_empty() {
226 let x = dt + " " + &tz.replace(':', "");
227 DateTime::parse_from_str(&x, "%B %d %Y %H:%M:%S %z")
228 .or_else(|_| DateTime::parse_from_str(&x, "%B %d %Y %I:%M:%S%P %z"))
229 .or_else(|_| DateTime::parse_from_str(&x, "%B %d %Y %I:%M:%S %P %z"))
230 .or_else(|_| DateTime::parse_from_str(&x, "%A %B %d %Y %H:%M:%S %z"))
231 .or_else(|_| DateTime::parse_from_str(&x, "%A %B %d %Y %I:%M%P %z"))
232 .or_else(|_| DateTime::parse_from_str(&x, "%A %B %d %Y %I:%M %P %z"))
233 .map_err(|e| e.to_string())
234 } else {
235 Err("custom parsing failed".to_string())
236 }
237}
238
239fn try_others(s: &str) -> Result<DateTime<FixedOffset>, Error> {
243 let date = s.split_whitespace().collect::<Vec<_>>();
244 let year = Local::now().year();
245 if date.len().eq(&2) && date[0].chars().all(char::is_alphabetic) {
246 NaiveDate::parse_from_str(&format!("{} {}", s, year), "%B %d %Y")
248 .map(|x| x.and_hms(0, 0, 0))
249 .map(|x| Local.from_local_datetime(&x))
250 .map_err(|e| e.to_string())
251 .map(|x| x.unwrap().with_timezone(x.unwrap().offset()))
252 } else if date.len().eq(&2) && date[1].chars().all(char::is_alphabetic) {
253 NaiveDate::parse_from_str(&format!("{} {}", s, year), "%d %B %Y")
255 .map(|x| x.and_hms(0, 0, 0))
256 .map(|x| Local.from_local_datetime(&x))
257 .map_err(|e| e.to_string())
258 .map(|x| x.unwrap().with_timezone(x.unwrap().offset()))
259 } else if date.len().eq(&3) && date[0].replace(',', "").chars().all(char::is_alphabetic) {
260 Local
262 .datetime_from_str(
263 &format!("{} {} {} {}", date[0], date[1], year, date[2]),
264 "%B %d %Y %H:%M",
265 )
266 .or_else(|_| {
267 Local.datetime_from_str(
268 &format!("{} {} {} {}", date[0], date[1], year, date[2]),
269 "%B %d %Y %T",
270 )
271 })
272 .or_else(|_| {
273 Local.datetime_from_str(
274 &format!("{} {} {} {}", date[0], date[1], year, date[2]),
275 "%B %d %Y %I:%M%P",
276 )
277 })
278 .map(|x| x.with_timezone(x.offset()))
279 .map_err(|e| e.to_string())
280 } else if date.len().eq(&3) && date[1].chars().all(char::is_alphabetic) {
281 Local
283 .datetime_from_str(
284 &format!("{} {} {} {}", date[0], date[1], year, date[2]),
285 "%d %B %Y %H:%M",
286 )
287 .or_else(|_| {
288 Local.datetime_from_str(
289 &format!("{} {} {} {}", date[0], date[1], year, date[2]),
290 "%d %B %Y %T",
291 )
292 })
293 .or_else(|_| {
294 Local.datetime_from_str(
295 &format!("{} {} {} {}", date[0], date[1], year, date[2]),
296 "%d %B %Y %I:%M%P",
297 )
298 })
299 .map(|x| x.with_timezone(x.offset()))
300 .map_err(|e| e.to_string())
301 } else if date.len().eq(&4) && date[0].chars().all(char::is_alphabetic) {
302 Local
304 .datetime_from_str(
305 &format!("{} {} {} {} {}", date[0], date[1], year, date[2], date[3]),
306 "%B %d %Y %I:%M %P",
307 )
308 .map(|x| x.with_timezone(x.offset()))
309 .map_err(|e| e.to_string())
310 } else if date.len().eq(&4) && date[1].chars().all(char::is_alphabetic) {
311 Local
313 .datetime_from_str(
314 &format!("{} {} {} {} {}", date[0], date[1], year, date[2], date[3]),
315 "%d %B %Y %I:%M %P",
316 )
317 .map(|x| x.with_timezone(x.offset()))
318 .map_err(|e| e.to_string())
319 } else {
320 Err("failed brute force parsing".to_string())
321 }
322}
323
324fn is_tz_alpha(s: &str) -> Option<(&str, &str)> {
327 let mut dtz = s.trim().rsplitn(2, ' ');
328 let tz = dtz.next().unwrap_or_default();
329 let dt = dtz.next().unwrap_or_default();
330 if tz.chars().all(char::is_alphabetic) {
331 Some((dt, tz))
332 } else {
333 None
334 }
335}
336
337fn to_rfc2822(s: &str, tz: &str) -> Result<DateTime<FixedOffset>, Error> {
339 NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
340 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %I:%M%P"))
341 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %I:%M %P"))
342 .or_else(|_| NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M"))
343 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %Y %T"))
344 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %Y %T.%f"))
345 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %Y %H:%M"))
346 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %Y %T"))
347 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %Y %T.%f"))
348 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %Y %T.%f"))
349 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %Y %T"))
350 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %Y %T"))
351 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %Y %T.%f"))
352 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T.%f"))
353 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T"))
354 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T"))
355 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %Y %T.%f"))
356 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %T.%f %Y"))
357 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %T %Y"))
358 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %T.%f %Y"))
359 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %T %Y"))
360 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %T.%f %Y"))
361 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %T %Y"))
362 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %m %d %H:%M %Y"))
363 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %H:%M %Y"))
364 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %H:%M %Y"))
365 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %H:%M %Y"))
366 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %m %d %I:%M%P %Y"))
367 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %m %I:%M%P %Y"))
368 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %I:%M %P %Y"))
369 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %d %B %I:%M%P %Y"))
370 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %I:%M %P %Y"))
371 .or_else(|_| NaiveDateTime::parse_from_str(s, "%A %B %d %I:%M%P %Y"))
372 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %T.%f %Y"))
373 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %T %Y"))
374 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %T.%f %Y"))
375 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %T %Y"))
376 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %T.%f %Y"))
377 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %T %Y"))
378 .or_else(|_| NaiveDateTime::parse_from_str(s, "%m %d %I:%M %Y"))
379 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %I:%M %Y"))
380 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %I:%M %Y"))
381 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %I:%M %Y"))
382 .or_else(|_| NaiveDateTime::parse_from_str(s, "%m %d %I:%M%P %Y"))
383 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %m %I:%M%P %Y"))
384 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %I:%M %P %Y"))
385 .or_else(|_| NaiveDateTime::parse_from_str(s, "%d %B %I:%M%P %Y"))
386 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %I:%M %P %Y"))
387 .or_else(|_| NaiveDateTime::parse_from_str(s, "%B %d %I:%M%P %Y"))
388 .and_then(|x| {
389 DateTime::parse_from_rfc2822(
390 (x.format("%a, %d %b %Y %H:%M:%S").to_string() + " " + tz).as_str(),
391 )
392 })
393 .map_err(|e| e.to_string())
394}
395
396fn standardize_date(s: &str) -> String {
400 if s.len() < 8 {
401 s.to_string()
402 } else {
403 s.chars()
404 .take(8)
405 .map(|mut x| {
406 if x.eq(&'.') || x.eq(&'/') {
407 x = '-'
408 };
409 x
410 })
411 .collect::<String>()
412 + &s[8..]
413 }
414 .replace(" UTC", " GMT")
415 .replace(" UT", " GMT")
416 .replace([',', ';'], "")
417}