1#![allow(deprecated)]
2use crate::timezone;
3use anyhow::{Result, anyhow};
4use chrono::prelude::*;
5use regex::Regex;
6
7macro_rules! regex {
8 ($re:literal $(,)?) => {{
9 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
10 RE.get_or_init(|| unsafe {
11 regex::RegexBuilder::new($re)
12 .unicode(false)
13 .build()
14 .unwrap_unchecked()
15 })
16 }};
17}
18pub struct Parse<'z, Tz2> {
20 tz: &'z Tz2,
21 default_time: NaiveTime,
22 prefer_dmy: bool,
23}
24
25impl<'z, Tz2> Parse<'z, Tz2>
26where
27 Tz2: TimeZone,
28{
29 pub const fn new(tz: &'z Tz2, default_time: NaiveTime) -> Self {
32 Self {
33 tz,
34 default_time,
35 prefer_dmy: false,
36 }
37 }
38
39 pub const fn prefer_dmy(&mut self, yes: bool) -> &Self {
40 self.prefer_dmy = yes;
41 self
42 }
43
44 pub const fn new_with_preference(
47 tz: &'z Tz2,
48 default_time: NaiveTime,
49 prefer_dmy: bool,
50 ) -> Self {
51 Self {
52 tz,
53 default_time,
54 prefer_dmy,
55 }
56 }
57
58 #[inline]
61 pub fn parse(&self, input: &str) -> Result<DateTime<Utc>> {
62 self.rfc2822(input)
63 .or_else(|| self.unix_timestamp(input))
64 .or_else(|| self.slash_mdy_family(input))
65 .or_else(|| self.slash_ymd_family(input))
66 .or_else(|| self.ymd_family(input))
67 .or_else(|| self.month_ymd(input))
68 .or_else(|| self.month_mdy_family(input))
69 .or_else(|| self.month_dmy_family(input))
70 .unwrap_or_else(|| Err(anyhow!("{} did not match any formats.", input)))
71 }
72
73 #[inline]
74 fn ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
75 let re: &Regex = regex! {
76 r"^\d{4}-\d{2}"
77
78 };
79
80 if !re.is_match(input) {
81 return None;
82 }
83 self.rfc3339(input)
84 .or_else(|| self.ymd_hms(input))
85 .or_else(|| self.ymd_hms_z(input))
86 .or_else(|| self.ymd(input))
87 .or_else(|| self.ymd_z(input))
88 }
89
90 #[inline]
91 fn month_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
92 let re: &Regex = regex! {
93 r"^[a-zA-Z]{3,9}\.?\s+\d{1,2}"
94 };
95
96 if !re.is_match(input) {
97 return None;
98 }
99 self.month_mdy_hms(input)
100 .or_else(|| self.month_mdy_hms_z(input))
101 .or_else(|| self.month_mdy(input))
102 }
103
104 #[inline]
105 fn month_dmy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
106 let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}"
107 };
108
109 if !re.is_match(input) {
110 return None;
111 }
112 self.month_dmy_hms(input).or_else(|| self.month_dmy(input))
113 }
114
115 #[inline]
116 fn slash_mdy_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
117 let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}"
118 };
119 if !re.is_match(input) {
120 return None;
121 }
122 if self.prefer_dmy {
123 self.slash_dmy_hms(input)
124 .or_else(|| self.slash_dmy(input))
125 .or_else(|| self.slash_mdy_hms(input))
126 .or_else(|| self.slash_mdy(input))
127 } else {
128 self.slash_mdy_hms(input)
129 .or_else(|| self.slash_mdy(input))
130 .or_else(|| self.slash_dmy_hms(input))
131 .or_else(|| self.slash_dmy(input))
132 }
133 }
134
135 #[inline]
136 fn slash_ymd_family(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
137 let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}"};
138 if !re.is_match(input) {
139 return None;
140 }
141 self.slash_ymd_hms(input).or_else(|| self.slash_ymd(input))
142 }
143
144 #[inline]
149 fn unix_timestamp(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
150 let ts_sec_val: f64 = if let Ok(val) = fast_float2::parse(input) {
151 val
152 } else {
153 return None;
154 };
155
156 let ts_ns_val = ts_sec_val * 1_000_000_000_f64;
158
159 let result = Utc.timestamp_nanos(ts_ns_val as i64).with_timezone(&Utc);
160 Some(Ok(result))
161 }
162
163 #[inline]
167 fn rfc3339(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
168 DateTime::parse_from_rfc3339(input)
169 .ok()
170 .map(|parsed| parsed.with_timezone(&Utc))
171 .map(Ok)
172 }
173
174 #[inline]
177 fn rfc2822(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
178 DateTime::parse_from_rfc2822(input)
179 .ok()
180 .map(|parsed| parsed.with_timezone(&Utc))
181 .map(Ok)
182 }
183
184 #[inline]
193 fn ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
194 let re: &Regex = regex! {
195 r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(:\d{2})?(\.\d{1,9})?\s*(am|pm|AM|PM)?$"
196
197 };
198 if !re.is_match(input) {
199 return None;
200 }
201
202 self.tz
203 .datetime_from_str(input, "%Y-%m-%d %H:%M:%S")
204 .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %H:%M"))
205 .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %H:%M:%S%.f"))
206 .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %I:%M:%S %P"))
207 .or_else(|_| self.tz.datetime_from_str(input, "%Y-%m-%d %I:%M %P"))
208 .ok()
209 .map(|parsed| parsed.with_timezone(&Utc))
210 .map(Ok)
211 }
212
213 #[inline]
223 fn ymd_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
224 if input.len() < 17 || !input.as_bytes()[10].is_ascii_whitespace() {
226 return None;
227 }
228 let re: &Regex = regex! {
229 r"^\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(:\d{2})?(\.\d{1,9})?(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
230 };
231
232 if let Some(caps) = re.captures(input)
233 && let Some(matched_tz) = caps.name("tz")
234 {
235 let parse_from_str = NaiveDateTime::parse_from_str;
236 return match timezone::parse(matched_tz.as_str().trim()) {
237 Ok(offset) => parse_from_str(input, "%Y-%m-%d %H:%M:%S %Z")
238 .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M %Z"))
239 .or_else(|_| parse_from_str(input, "%Y-%m-%d %H:%M:%S%.f %Z"))
240 .ok()
241 .and_then(|parsed| offset.from_local_datetime(&parsed).single())
242 .map(|datetime| datetime.with_timezone(&Utc))
243 .map(Ok),
244 Err(err) => Some(Err(err)),
245 };
246 }
247 None
248 }
249
250 #[inline]
253 fn ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
254 let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}$"
255 };
256
257 if !re.is_match(input) {
258 return None;
259 }
260 let now = Utc::now()
261 .date()
262 .and_time(self.default_time)?
263 .with_timezone(self.tz);
264 NaiveDate::parse_from_str(input, "%Y-%m-%d")
265 .ok()
266 .map(|parsed| parsed.and_time(now.time()))
267 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
268 .map(|at_tz| at_tz.with_timezone(&Utc))
269 .map(Ok)
270 }
271
272 #[inline]
277 fn ymd_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
278 if input.len() <= 10 {
280 return None;
281 }
282 let re: &Regex = regex! {r"^\d{4}-\d{2}-\d{2}(?P<tz>\s*[+-:a-zA-Z0-9]{3,6})$"
283 };
284 if let Some(caps) = re.captures(input)
285 && let Some(matched_tz) = caps.name("tz")
286 {
287 return match timezone::parse(matched_tz.as_str().trim()) {
288 Ok(offset) => {
289 let now = Utc::now()
290 .date()
291 .and_time(self.default_time)?
292 .with_timezone(&offset);
293 NaiveDate::parse_from_str(input, "%Y-%m-%d %Z")
294 .ok()
295 .map(|parsed| parsed.and_time(now.time()))
296 .and_then(|datetime| offset.from_local_datetime(&datetime).single())
297 .map(|at_tz| at_tz.with_timezone(&Utc))
298 .map(Ok)
299 }
300 Err(err) => Some(Err(err)),
301 };
302 }
303 None
304 }
305
306 #[inline]
309 fn month_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
310 let re: &Regex = regex! {r"^\d{4}-\w{3,9}-\d{2}$"
311 };
312 if !re.is_match(input) {
313 return None;
314 }
315
316 let now = Utc::now()
317 .date()
318 .and_time(self.default_time)?
319 .with_timezone(self.tz);
320 NaiveDate::parse_from_str(input, "%Y-%m-%d")
321 .or_else(|_| NaiveDate::parse_from_str(input, "%Y-%b-%d"))
322 .ok()
323 .map(|parsed| parsed.and_time(now.time()))
324 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
325 .map(|at_tz| at_tz.with_timezone(&Utc))
326 .map(Ok)
327 }
328
329 #[inline]
334 fn month_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
335 let re: &Regex = regex! {
336 r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4},?\s+\d{1,2}:\d{2}(:\d{2})?\s*(am|pm|AM|PM)?$"
337 };
338 if !re.is_match(input) {
339 return None;
340 }
341
342 let dt = input.replace([',', '.'], "");
347 self.tz
348 .datetime_from_str(&dt, "%B %d %Y %H:%M:%S")
349 .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %H:%M"))
350 .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M:%S %P"))
351 .or_else(|_| self.tz.datetime_from_str(&dt, "%B %d %Y %I:%M %P"))
352 .ok()
353 .map(|at_tz| at_tz.with_timezone(&Utc))
354 .map(Ok)
355 }
356
357 #[inline]
363 fn month_mdy_hms_z(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
364 if input.len() < 20 {
367 return None;
368 }
369 let bytes = input.as_bytes();
370 let has_year = (0..bytes.len().saturating_sub(3)).any(|i| {
371 bytes[i..i + 4].iter().all(|b| b.is_ascii_digit())
372 && (i == 0 || !bytes[i - 1].is_ascii_digit())
373 && bytes.get(i + 4).is_none_or(|b| !b.is_ascii_digit())
374 });
375 if !has_year {
376 return None;
377 }
378 let re: &Regex = regex! {
379 r"^[a-zA-Z]{3,9}\s+\d{1,2},?\s+\d{4}\s*,?(at)?\s+\d{2}:\d{2}(:\d{2})?\s*(am|pm|AM|PM)?(?P<tz>\s+[+-:a-zA-Z0-9]{3,6})$",
380 };
381 if let Some(caps) = re.captures(input)
382 && let Some(matched_tz) = caps.name("tz")
383 {
384 let parse_from_str = NaiveDateTime::parse_from_str;
385 return match timezone::parse(matched_tz.as_str().trim()) {
386 Ok(offset) => {
387 let mut dt = input.replace(',', "");
388 if let Some(pos) = dt.find("at") {
389 dt.replace_range(pos..pos + 2, "");
390 }
391 parse_from_str(&dt, "%B %d %Y %H:%M:%S %Z")
392 .or_else(|_| parse_from_str(&dt, "%B %d %Y %H:%M %Z"))
393 .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M:%S %P %Z"))
394 .or_else(|_| parse_from_str(&dt, "%B %d %Y %I:%M %P %Z"))
395 .ok()
396 .and_then(|parsed| offset.from_local_datetime(&parsed).single())
397 .map(|datetime| datetime.with_timezone(&Utc))
398 .map(Ok)
399 }
400 Err(err) => Some(Err(err)),
401 };
402 }
403 None
404 }
405
406 #[inline]
414 fn month_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
415 let re: &Regex = regex! {r"^[a-zA-Z]{3,9}\.?\s+\d{1,2},\s+\d{2,4}$"
416 };
417 if !re.is_match(input) {
418 return None;
419 }
420
421 let now = Utc::now()
422 .date()
423 .and_time(self.default_time)?
424 .with_timezone(self.tz);
425 let dt = input.replace([',', '.'], "");
429 NaiveDate::parse_from_str(&dt, "%B %d %y")
430 .or_else(|_| NaiveDate::parse_from_str(&dt, "%B %d %Y"))
431 .ok()
432 .map(|parsed| parsed.and_time(now.time()))
433 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
434 .map(|at_tz| at_tz.with_timezone(&Utc))
435 .map(Ok)
436 }
437
438 #[inline]
443 fn month_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
444 if !input.as_bytes().contains(&b':') {
446 return None;
447 }
448 let re: &Regex = regex! {
449 r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4},?\s+\d{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?$"
450 };
451 if !re.is_match(input) {
452 return None;
453 }
454
455 let dt = input.replace(',', "");
456 self.tz
457 .datetime_from_str(&dt, "%d %B %Y %H:%M:%S")
458 .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M"))
459 .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %H:%M:%S%.f"))
460 .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M:%S %P"))
461 .or_else(|_| self.tz.datetime_from_str(&dt, "%d %B %Y %I:%M %P"))
462 .ok()
463 .map(|at_tz| at_tz.with_timezone(&Utc))
464 .map(Ok)
465 }
466
467 #[inline]
473 fn month_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
474 let re: &Regex = regex! {r"^\d{1,2}\s+[a-zA-Z]{3,9}\s+\d{2,4}$"
475 };
476 if !re.is_match(input) {
477 return None;
478 }
479
480 let now = Utc::now()
481 .date()
482 .and_time(self.default_time)?
483 .with_timezone(self.tz);
484 let bytes = input.as_bytes();
487 let len = bytes.len();
488 let four_digit_year = len >= 5
489 && bytes[len - 4..].iter().all(|b| b.is_ascii_digit())
490 && bytes[len - 5].is_ascii_whitespace();
491 let parsed = if four_digit_year {
492 NaiveDate::parse_from_str(input, "%d %B %Y")
493 } else {
494 NaiveDate::parse_from_str(input, "%d %B %y")
495 .or_else(|_| NaiveDate::parse_from_str(input, "%d %B %Y"))
496 };
497 parsed
498 .ok()
499 .map(|parsed| parsed.and_time(now.time()))
500 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
501 .map(|at_tz| at_tz.with_timezone(&Utc))
502 .map(Ok)
503 }
504
505 #[inline]
519 fn slash_mdy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
520 let re: &Regex = regex! {
521 r"^\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2}(:\d{2})?(\.\d{1,9})?\s*(am|pm|AM|PM)?$"
522 };
523 if !re.is_match(input) {
524 return None;
525 }
526
527 self.tz
528 .datetime_from_str(input, "%m/%d/%y %H:%M:%S")
529 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %H:%M"))
530 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %H:%M:%S%.f"))
531 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %I:%M:%S %P"))
532 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%y %I:%M %P"))
533 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M:%S"))
534 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M"))
535 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %H:%M:%S%.f"))
536 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %I:%M:%S %P"))
537 .or_else(|_| self.tz.datetime_from_str(input, "%m/%d/%Y %I:%M %P"))
538 .ok()
539 .map(|at_tz| at_tz.with_timezone(&Utc))
540 .map(Ok)
541 }
542
543 #[inline]
557 fn slash_dmy_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
558 let re: &Regex = regex! {
559 r"^\d{1,2}/\d{1,2}/\d{2,4}\s+\d{1,2}:\d{2}(:\d{2})?(\.\d{1,9})?\s*(am|pm|AM|PM)?$"
560 };
561 if !re.is_match(input) {
562 return None;
563 }
564
565 self.tz
566 .datetime_from_str(input, "%d/%m/%y %H:%M:%S")
567 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%y %H:%M"))
568 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%y %H:%M:%S%.f"))
569 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%y %I:%M:%S %P"))
570 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%y %I:%M %P"))
571 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%Y %H:%M:%S"))
572 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%Y %H:%M"))
573 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%Y %H:%M:%S%.f"))
574 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%Y %I:%M:%S %P"))
575 .or_else(|_| self.tz.datetime_from_str(input, "%d/%m/%Y %I:%M %P"))
576 .ok()
577 .map(|at_tz| at_tz.with_timezone(&Utc))
578 .map(Ok)
579 }
580
581 #[inline]
587 fn slash_mdy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
588 let re: &Regex = regex! {r"^\d{1,2}/\d{1,2}/\d{2,4}$"
589 };
590 if !re.is_match(input) {
591 return None;
592 }
593
594 let now = Utc::now()
595 .date()
596 .and_time(self.default_time)?
597 .with_timezone(self.tz);
598 NaiveDate::parse_from_str(input, "%m/%d/%y")
599 .or_else(|_| NaiveDate::parse_from_str(input, "%m/%d/%Y"))
600 .ok()
601 .map(|parsed| parsed.and_time(now.time()))
602 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
603 .map(|at_tz| at_tz.with_timezone(&Utc))
604 .map(Ok)
605 }
606
607 #[inline]
613 fn slash_dmy(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
614 let re: &Regex = regex! {r"^[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}$"
615 };
616 if !re.is_match(input) {
617 return None;
618 }
619
620 let now = Utc::now()
621 .date()
622 .and_time(self.default_time)?
623 .with_timezone(self.tz);
624 NaiveDate::parse_from_str(input, "%d/%m/%y")
625 .or_else(|_| NaiveDate::parse_from_str(input, "%d/%m/%Y"))
626 .ok()
627 .map(|parsed| parsed.and_time(now.time()))
628 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
629 .map(|at_tz| at_tz.with_timezone(&Utc))
630 .map(Ok)
631 }
632
633 #[inline]
641 fn slash_ymd_hms(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
642 let re: &Regex = regex! {
643 r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}\s+[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?(\.[0-9]{1,9})?\s*(am|pm|AM|PM)?$"
644 };
645 if !re.is_match(input) {
646 return None;
647 }
648
649 self.tz
650 .datetime_from_str(input, "%Y/%m/%d %H:%M:%S")
651 .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M"))
652 .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %H:%M:%S%.f"))
653 .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M:%S %P"))
654 .or_else(|_| self.tz.datetime_from_str(input, "%Y/%m/%d %I:%M %P"))
655 .ok()
656 .map(|at_tz| at_tz.with_timezone(&Utc))
657 .map(Ok)
658 }
659
660 #[inline]
664 fn slash_ymd(&self, input: &str) -> Option<Result<DateTime<Utc>>> {
665 let re: &Regex = regex! {r"^[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}$"
666 };
667 if !re.is_match(input) {
668 return None;
669 }
670
671 let now = Utc::now()
672 .date()
673 .and_time(self.default_time)?
674 .with_timezone(self.tz);
675 NaiveDate::parse_from_str(input, "%Y/%m/%d")
676 .ok()
677 .map(|parsed| parsed.and_time(now.time()))
678 .and_then(|datetime| self.tz.from_local_datetime(&datetime).single())
679 .map(|at_tz| at_tz.with_timezone(&Utc))
680 .map(Ok)
681 }
682}
683
684#[cfg(test)]
685mod tests {
686 use super::*;
687
688 #[test]
689 fn unix_timestamp() {
690 let parse = Parse::new(&Utc, Utc::now().time());
691
692 let test_cases = vec![
693 ("0", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
694 ("0000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
695 ("0000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
696 ("0000000000000000000", Utc.ymd(1970, 1, 1).and_hms(0, 0, 0)),
697 ("-770172300", Utc.ymd(1945, 8, 5).and_hms(23, 15, 0)),
698 (
699 "1671673426.123456789",
700 Utc.ymd(2022, 12, 22).and_hms_nano(1, 43, 46, 123456768),
701 ),
702 ("1511648546", Utc.ymd(2017, 11, 25).and_hms(22, 22, 26)),
703 (
704 "1620036248.420",
705 Utc.ymd(2021, 5, 3).and_hms_milli(10, 4, 8, 420),
706 ),
707 (
708 "1620036248.717915136",
709 Utc.ymd(2021, 5, 3).and_hms_nano(10, 4, 8, 717915136),
710 ),
711 ];
712
713 for &(input, want) in test_cases.iter() {
714 assert_eq!(
715 parse.unix_timestamp(input).unwrap().unwrap(),
716 want,
717 "unix_timestamp/{}",
718 input
719 )
720 }
721 assert!(parse.unix_timestamp("15116").is_some());
722 assert!(
723 parse
724 .unix_timestamp("16200248727179150001620024872717915000") .is_some()
726 );
727 assert!(parse.unix_timestamp("not-a-ts").is_none());
728 }
729
730 #[test]
731 fn rfc3339() {
732 let parse = Parse::new(&Utc, Utc::now().time());
733
734 let test_cases = [
735 (
736 "2021-05-01T01:17:02.604456Z",
737 Utc.ymd(2021, 5, 1).and_hms_nano(1, 17, 2, 604456000),
738 ),
739 (
740 "2017-11-25T22:34:50Z",
741 Utc.ymd(2017, 11, 25).and_hms(22, 34, 50),
742 ),
743 ];
744
745 for &(input, want) in test_cases.iter() {
746 assert_eq!(
747 parse.rfc3339(input).unwrap().unwrap(),
748 want,
749 "rfc3339/{}",
750 input
751 )
752 }
753 assert!(parse.rfc3339("2017-11-25 22:34:50").is_none());
754 assert!(parse.rfc3339("not-date-time").is_none());
755 }
756
757 #[test]
758 fn rfc2822() {
759 let parse = Parse::new(&Utc, Utc::now().time());
760
761 let test_cases = [
762 (
763 "Wed, 02 Jun 2021 06:31:39 GMT",
764 Utc.ymd(2021, 6, 2).and_hms(6, 31, 39),
765 ),
766 (
767 "Wed, 02 Jun 2021 06:31:39 PDT",
768 Utc.ymd(2021, 6, 2).and_hms(13, 31, 39),
769 ),
770 ];
771
772 for &(input, want) in test_cases.iter() {
773 assert_eq!(
774 parse.rfc2822(input).unwrap().unwrap(),
775 want,
776 "rfc2822/{}",
777 input
778 )
779 }
780 assert!(parse.rfc2822("02 Jun 2021 06:31:39").is_none());
781 assert!(parse.rfc2822("not-date-time").is_none());
782 }
783
784 #[test]
785 fn ymd_hms() {
786 let parse = Parse::new(&Utc, Utc::now().time());
787
788 let test_cases = [
789 ("2021-04-30 21:14", Utc.ymd(2021, 4, 30).and_hms(21, 14, 0)),
790 (
791 "2021-04-30 21:14:10",
792 Utc.ymd(2021, 4, 30).and_hms(21, 14, 10),
793 ),
794 (
795 "2021-04-30 21:14:10.052282",
796 Utc.ymd(2021, 4, 30).and_hms_micro(21, 14, 10, 52282),
797 ),
798 (
799 "2014-04-26 05:24:37 PM",
800 Utc.ymd(2014, 4, 26).and_hms(17, 24, 37),
801 ),
802 (
803 "2014-04-26 17:24:37.123",
804 Utc.ymd(2014, 4, 26).and_hms_milli(17, 24, 37, 123),
805 ),
806 (
807 "2014-04-26 17:24:37.3186369",
808 Utc.ymd(2014, 4, 26).and_hms_nano(17, 24, 37, 318636900),
809 ),
810 (
811 "2012-08-03 18:31:59.257000000",
812 Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
813 ),
814 ];
815
816 for &(input, want) in test_cases.iter() {
817 assert_eq!(
818 parse.ymd_hms(input).unwrap().unwrap(),
819 want,
820 "ymd_hms/{}",
821 input
822 )
823 }
824 assert!(parse.ymd_hms("not-date-time").is_none());
825 }
826
827 #[test]
828 fn ymd_hms_z() {
829 let parse = Parse::new(&Utc, Utc::now().time());
830
831 let test_cases = [
832 (
833 "2017-11-25 13:31:15 PST",
834 Utc.ymd(2017, 11, 25).and_hms(21, 31, 15),
835 ),
836 (
837 "2017-11-25 13:31 PST",
838 Utc.ymd(2017, 11, 25).and_hms(21, 31, 0),
839 ),
840 (
841 "2014-12-16 06:20:00 UTC",
842 Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
843 ),
844 (
845 "2014-12-16 06:20:00 GMT",
846 Utc.ymd(2014, 12, 16).and_hms(6, 20, 0),
847 ),
848 (
849 "2014-04-26 13:13:43 +0800",
850 Utc.ymd(2014, 4, 26).and_hms(5, 13, 43),
851 ),
852 (
853 "2014-04-26 13:13:44 +09:00",
854 Utc.ymd(2014, 4, 26).and_hms(4, 13, 44),
855 ),
856 (
857 "2012-08-03 18:31:59.257000000 +0000",
858 Utc.ymd(2012, 8, 3).and_hms_nano(18, 31, 59, 257000000),
859 ),
860 (
861 "2015-09-30 18:48:56.35272715 UTC",
862 Utc.ymd(2015, 9, 30).and_hms_nano(18, 48, 56, 352727150),
863 ),
864 ];
865
866 for &(input, want) in test_cases.iter() {
867 assert_eq!(
868 parse.ymd_hms_z(input).unwrap().unwrap(),
869 want,
870 "ymd_hms_z/{}",
871 input
872 )
873 }
874 assert!(parse.ymd_hms_z("not-date-time").is_none());
875 assert!(parse.ymd_hms_z("2021-04-30 21:14").is_none()); assert!(parse.ymd_hms_z("2021-04-30X21:14Z").is_none()); assert!(parse.ymd_hms_z("2021-04-30 21:1XZ").is_none()); }
882
883 #[test]
884 fn ymd() {
885 let parse = Parse::new(&Utc, Utc::now().time());
886
887 let test_cases = [(
888 "2021-02-21",
889 Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
890 )];
891
892 for &(input, want) in test_cases.iter() {
893 assert_eq!(
894 parse
895 .ymd(input)
896 .unwrap()
897 .unwrap()
898 .trunc_subsecs(0)
899 .with_second(0)
900 .unwrap(),
901 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
902 "ymd/{}",
903 input
904 )
905 }
906 assert!(parse.ymd("not-date-time").is_none());
907 }
908
909 #[test]
910 fn ymd_z() {
911 let parse = Parse::new(&Utc, Utc::now().time());
912 let now_at_pst = Utc::now().with_timezone(&FixedOffset::west(8 * 3600));
913 let now_at_cst = Utc::now().with_timezone(&FixedOffset::east(8 * 3600));
914
915 let test_cases = [
916 (
917 "2021-02-21 PST",
918 FixedOffset::west(8 * 3600)
919 .ymd(2021, 2, 21)
920 .and_time(now_at_pst.time())
921 .map(|dt| dt.with_timezone(&Utc)),
922 ),
923 (
924 "2021-02-21 UTC",
925 FixedOffset::west(0)
926 .ymd(2021, 2, 21)
927 .and_time(Utc::now().time())
928 .map(|dt| dt.with_timezone(&Utc)),
929 ),
930 (
931 "2020-07-20+08:00",
932 FixedOffset::east(8 * 3600)
933 .ymd(2020, 7, 20)
934 .and_time(now_at_cst.time())
935 .map(|dt| dt.with_timezone(&Utc)),
936 ),
937 ];
938
939 for &(input, want) in test_cases.iter() {
940 assert_eq!(
941 parse
942 .ymd_z(input)
943 .unwrap()
944 .unwrap()
945 .trunc_subsecs(0)
946 .with_second(0)
947 .unwrap(),
948 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
949 "ymd_z/{}",
950 input
951 )
952 }
953 assert!(parse.ymd_z("not-date-time").is_none());
954 assert!(parse.ymd_z("2021-02-21").is_none()); assert!(parse.ymd_z("2021-02-21X").is_none()); }
958
959 #[test]
960 fn month_ymd() {
961 let parse = Parse::new(&Utc, Utc::now().time());
962
963 let test_cases = [(
964 "2021-Feb-21",
965 Utc.ymd(2021, 2, 21).and_time(Utc::now().time()),
966 )];
967
968 for &(input, want) in test_cases.iter() {
969 assert_eq!(
970 parse
971 .month_ymd(input)
972 .unwrap()
973 .unwrap()
974 .trunc_subsecs(0)
975 .with_second(0)
976 .unwrap(),
977 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
978 "month_ymd/{}",
979 input
980 )
981 }
982 assert!(parse.month_ymd("not-date-time").is_none());
983 }
984
985 #[test]
986 fn month_mdy_hms() {
987 let parse = Parse::new(&Utc, Utc::now().time());
988
989 let test_cases = [
990 (
991 "May 8, 2009 5:57:51 PM",
992 Utc.ymd(2009, 5, 8).and_hms(17, 57, 51),
993 ),
994 (
995 "September 17, 2012 10:09am",
996 Utc.ymd(2012, 9, 17).and_hms(10, 9, 0),
997 ),
998 (
999 "September 17, 2012, 10:10:09",
1000 Utc.ymd(2012, 9, 17).and_hms(10, 10, 9),
1001 ),
1002 ];
1003
1004 for &(input, want) in test_cases.iter() {
1005 assert_eq!(
1006 parse.month_mdy_hms(input).unwrap().unwrap(),
1007 want,
1008 "month_mdy_hms/{}",
1009 input
1010 )
1011 }
1012 assert!(parse.month_mdy_hms("not-date-time").is_none());
1013 }
1014
1015 #[test]
1016 fn month_mdy_hms_z() {
1017 let parse = Parse::new(&Utc, Utc::now().time());
1018
1019 let test_cases = [
1020 (
1021 "May 02, 2021 15:51:31 UTC",
1022 Utc.ymd(2021, 5, 2).and_hms(15, 51, 31),
1023 ),
1024 (
1025 "May 02, 2021 15:51 UTC",
1026 Utc.ymd(2021, 5, 2).and_hms(15, 51, 0),
1027 ),
1028 (
1029 "May 26, 2021, 12:49 AM PDT",
1030 Utc.ymd(2021, 5, 26).and_hms(7, 49, 0),
1031 ),
1032 (
1033 "September 17, 2012 at 10:09am PST",
1034 Utc.ymd(2012, 9, 17).and_hms(18, 9, 0),
1035 ),
1036 ];
1037
1038 for &(input, want) in test_cases.iter() {
1039 assert_eq!(
1040 parse.month_mdy_hms_z(input).unwrap().unwrap(),
1041 want,
1042 "month_mdy_hms_z/{}",
1043 input
1044 )
1045 }
1046 assert!(parse.month_mdy_hms_z("not-date-time").is_none());
1047 assert!(parse.month_mdy_hms_z("May 27, 02:45:27 XX PST").is_none()); assert!(parse.month_mdy_hms_z("May 27 1234 something PST").is_none()); }
1052
1053 #[test]
1054 fn month_mdy() {
1055 let parse = Parse::new(&Utc, Utc::now().time());
1056
1057 let test_cases = [
1058 (
1059 "May 25, 2021",
1060 Utc.ymd(2021, 5, 25).and_time(Utc::now().time()),
1061 ),
1062 (
1063 "oct 7, 1970",
1064 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1065 ),
1066 (
1067 "oct 7, 70",
1068 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1069 ),
1070 (
1071 "oct. 7, 1970",
1072 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1073 ),
1074 (
1075 "oct. 7, 70",
1076 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1077 ),
1078 (
1079 "October 7, 1970",
1080 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1081 ),
1082 ];
1083
1084 for &(input, want) in test_cases.iter() {
1085 assert_eq!(
1086 parse
1087 .month_mdy(input)
1088 .unwrap()
1089 .unwrap()
1090 .trunc_subsecs(0)
1091 .with_second(0)
1092 .unwrap(),
1093 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1094 "month_mdy/{}",
1095 input
1096 )
1097 }
1098 assert!(parse.month_mdy("not-date-time").is_none());
1099 }
1100
1101 #[test]
1102 fn month_dmy_hms() {
1103 let parse = Parse::new(&Utc, Utc::now().time());
1104
1105 let test_cases = [
1106 (
1107 "12 Feb 2006, 19:17",
1108 Utc.ymd(2006, 2, 12).and_hms(19, 17, 0),
1109 ),
1110 ("12 Feb 2006 19:17", Utc.ymd(2006, 2, 12).and_hms(19, 17, 0)),
1111 (
1112 "14 May 2019 19:11:40.164",
1113 Utc.ymd(2019, 5, 14).and_hms_milli(19, 11, 40, 164),
1114 ),
1115 ];
1116
1117 for &(input, want) in test_cases.iter() {
1118 assert_eq!(
1119 parse.month_dmy_hms(input).unwrap().unwrap(),
1120 want,
1121 "month_dmy_hms/{}",
1122 input
1123 )
1124 }
1125 assert!(parse.month_dmy_hms("not-date-time").is_none());
1126 }
1127
1128 #[test]
1129 fn month_dmy() {
1130 let parse = Parse::new(&Utc, Utc::now().time());
1131
1132 let test_cases = [
1133 ("7 oct 70", Utc.ymd(1970, 10, 7).and_time(Utc::now().time())),
1134 (
1135 "7 oct 1970",
1136 Utc.ymd(1970, 10, 7).and_time(Utc::now().time()),
1137 ),
1138 (
1139 "03 February 2013",
1140 Utc.ymd(2013, 2, 3).and_time(Utc::now().time()),
1141 ),
1142 (
1143 "1 July 2013",
1144 Utc.ymd(2013, 7, 1).and_time(Utc::now().time()),
1145 ),
1146 ];
1147
1148 for &(input, want) in test_cases.iter() {
1149 assert_eq!(
1150 parse
1151 .month_dmy(input)
1152 .unwrap()
1153 .unwrap()
1154 .trunc_subsecs(0)
1155 .with_second(0)
1156 .unwrap(),
1157 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1158 "month_dmy/{}",
1159 input
1160 )
1161 }
1162 assert!(parse.month_dmy("not-date-time").is_none());
1163 }
1164
1165 #[test]
1168 fn month_dmy_year_fast_path() {
1169 let parse = Parse::new(&Utc, Utc::now().time());
1170
1171 let four_digit = parse.month_dmy("14 May 2019").unwrap().unwrap();
1173 assert_eq!(four_digit.year(), 2019);
1174 assert_eq!(four_digit.month(), 5);
1175 assert_eq!(four_digit.day(), 14);
1176
1177 let two_digit = parse.month_dmy("14 May 19").unwrap().unwrap();
1180 assert_eq!(two_digit.year(), 2019);
1181 assert_eq!(two_digit.month(), 5);
1182 assert_eq!(two_digit.day(), 14);
1183 }
1184
1185 #[test]
1186 fn slash_mdy_hms() {
1187 let parse = Parse::new(&Utc, Utc::now().time());
1188
1189 let test_cases = vec![
1190 ("4/8/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1191 ("04/08/2014 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1192 ("4/8/14 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1193 ("04/2/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1194 ("8/8/1965 12:00:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1195 (
1196 "8/8/1965 01:00:01 PM",
1197 Utc.ymd(1965, 8, 8).and_hms(13, 0, 1),
1198 ),
1199 ("8/8/1965 01:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1200 ("8/8/1965 1:00 PM", Utc.ymd(1965, 8, 8).and_hms(13, 0, 0)),
1201 ("8/8/1965 12:00 AM", Utc.ymd(1965, 8, 8).and_hms(0, 0, 0)),
1202 ("4/02/2014 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1203 (
1204 "03/19/2012 10:11:59",
1205 Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1206 ),
1207 (
1208 "03/19/2012 10:11:59.3186369",
1209 Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1210 ),
1211 ];
1212
1213 for &(input, want) in test_cases.iter() {
1214 assert_eq!(
1215 parse.slash_mdy_hms(input).unwrap().unwrap(),
1216 want,
1217 "slash_mdy_hms/{}",
1218 input
1219 )
1220 }
1221 assert!(parse.slash_mdy_hms("not-date-time").is_none());
1222 }
1223
1224 #[test]
1225 fn slash_mdy() {
1226 let parse = Parse::new(&Utc, Utc::now().time());
1227
1228 let test_cases = [
1229 (
1230 "3/31/2014",
1231 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1232 ),
1233 (
1234 "03/31/2014",
1235 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1236 ),
1237 ("08/21/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1238 ("8/1/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1239 ];
1240
1241 for &(input, want) in test_cases.iter() {
1242 assert_eq!(
1243 parse
1244 .slash_mdy(input)
1245 .unwrap()
1246 .unwrap()
1247 .trunc_subsecs(0)
1248 .with_second(0)
1249 .unwrap(),
1250 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1251 "slash_mdy/{}",
1252 input
1253 )
1254 }
1255 assert!(parse.slash_mdy("not-date-time").is_none());
1256 }
1257
1258 #[test]
1259 fn slash_dmy() {
1260 let mut parse = Parse::new(&Utc, Utc::now().time());
1261
1262 let test_cases = [
1263 (
1264 "31/3/2014",
1265 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1266 ),
1267 (
1268 "13/11/2014",
1269 Utc.ymd(2014, 11, 13).and_time(Utc::now().time()),
1270 ),
1271 ("21/08/71", Utc.ymd(1971, 8, 21).and_time(Utc::now().time())),
1272 ("1/8/71", Utc.ymd(1971, 8, 1).and_time(Utc::now().time())),
1273 ];
1274
1275 for &(input, want) in test_cases.iter() {
1276 assert_eq!(
1277 parse
1278 .prefer_dmy(true)
1279 .slash_dmy(input)
1280 .unwrap()
1281 .unwrap()
1282 .trunc_subsecs(0)
1283 .with_second(0)
1284 .unwrap(),
1285 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1286 "slash_dmy/{}",
1287 input
1288 )
1289 }
1290 assert!(parse.slash_dmy("not-date-time").is_none());
1291 }
1292
1293 #[test]
1294 fn slash_ymd_hms() {
1295 let parse = Parse::new(&Utc, Utc::now().time());
1296
1297 let test_cases = [
1298 ("2014/4/8 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1299 ("2014/04/08 22:05", Utc.ymd(2014, 4, 8).and_hms(22, 5, 0)),
1300 ("2014/04/2 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1301 ("2014/4/02 03:00:51", Utc.ymd(2014, 4, 2).and_hms(3, 0, 51)),
1302 (
1303 "2012/03/19 10:11:59",
1304 Utc.ymd(2012, 3, 19).and_hms(10, 11, 59),
1305 ),
1306 (
1307 "2012/03/19 10:11:59.3186369",
1308 Utc.ymd(2012, 3, 19).and_hms_nano(10, 11, 59, 318636900),
1309 ),
1310 ];
1311
1312 for &(input, want) in test_cases.iter() {
1313 assert_eq!(
1314 parse.slash_ymd_hms(input).unwrap().unwrap(),
1315 want,
1316 "slash_ymd_hms/{}",
1317 input
1318 )
1319 }
1320 assert!(parse.slash_ymd_hms("not-date-time").is_none());
1321 }
1322
1323 #[test]
1324 fn slash_ymd() {
1325 let parse = Parse::new(&Utc, Utc::now().time());
1326
1327 let test_cases = [
1328 (
1329 "2014/3/31",
1330 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1331 ),
1332 (
1333 "2014/03/31",
1334 Utc.ymd(2014, 3, 31).and_time(Utc::now().time()),
1335 ),
1336 ];
1337
1338 for &(input, want) in test_cases.iter() {
1339 assert_eq!(
1340 parse
1341 .slash_ymd(input)
1342 .unwrap()
1343 .unwrap()
1344 .trunc_subsecs(0)
1345 .with_second(0)
1346 .unwrap(),
1347 want.unwrap().trunc_subsecs(0).with_second(0).unwrap(),
1348 "slash_ymd/{}",
1349 input
1350 )
1351 }
1352 assert!(parse.slash_ymd("not-date-time").is_none());
1353 }
1354}