time_fmt/
parse.rs

1use thiserror::Error;
2use time::{Date, Month, PrimitiveDateTime, Time, UtcOffset, Weekday};
3
4use crate::{parse::desc_parser::Collector, util};
5
6mod desc_parser;
7pub mod time_format_item;
8
9#[derive(Error, Debug, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum ParseError {
12    #[error("Unknown specifier `%{0}`")]
13    UnknownSpecifier(char),
14    #[error("Expected {0} but got a byte {1}")]
15    UnexpectedByte(&'static str, u8),
16    #[error("Expected {0} but reached to the end")]
17    UnexpectedEnd(&'static str),
18    #[error("Expected {0} but doesn't have a match")]
19    NotMatch(&'static str),
20    #[error("Out-of-range for {0} component")]
21    ComponentOutOfRange(&'static str),
22    #[error("Unconverted data remains: {0}")]
23    UnconvertedDataRemains(String),
24    #[error(transparent)]
25    ComponentRange(#[from] time::error::ComponentRange),
26}
27
28trait Nat: std::ops::Add<Output = Self> + std::ops::Mul<Output = Self>
29where
30    Self: Sized,
31{
32    const ZERO: Self;
33    const TEN: Self;
34    fn from_u8(_: u8) -> Self;
35}
36
37trait Int: Nat + std::ops::Neg<Output = Self> {}
38
39impl Nat for u8 {
40    const ZERO: Self = 0;
41    const TEN: Self = 10;
42    fn from_u8(v: u8) -> Self {
43        v
44    }
45}
46impl Nat for u16 {
47    const ZERO: Self = 0;
48    const TEN: Self = 10;
49    fn from_u8(v: u8) -> Self {
50        v as u16
51    }
52}
53impl Nat for u32 {
54    const ZERO: Self = 0;
55    const TEN: Self = 10;
56    fn from_u8(v: u8) -> Self {
57        v as u32
58    }
59}
60impl Nat for i16 {
61    const ZERO: Self = 0;
62    const TEN: Self = 10;
63    fn from_u8(v: u8) -> Self {
64        v as i16
65    }
66}
67impl Int for i16 {}
68impl Nat for i32 {
69    const ZERO: Self = 0;
70    const TEN: Self = 10;
71    fn from_u8(v: u8) -> Self {
72        v as i32
73    }
74}
75impl Int for i32 {}
76
77#[derive(Debug)]
78enum ParsingYear {
79    Unspecified,
80    Year(i32),
81    PrefixSuffix(i32, u8),
82}
83#[derive(Debug)]
84enum ParsingDayOfYear {
85    Unspecified,
86    MonthDay(Month, u8),
87    DayOfYear(u16),
88}
89#[derive(Debug)]
90enum ParsingHour {
91    Unspecified,
92    FullDay(u8),
93    HalfDay(u8, bool),
94}
95
96#[derive(Debug, PartialEq, Eq)]
97pub enum TimeZoneSpecifier<'a> {
98    Offset(UtcOffset),
99    Name(&'a str),
100}
101
102struct ParseCollector<'a> {
103    s: &'a str,
104    year: ParsingYear,
105    day: ParsingDayOfYear,
106    hour: ParsingHour,
107    minute: u8,
108    second: u8,
109    nanosecond: u32,
110    zone: Option<TimeZoneSpecifier<'a>>,
111}
112impl<'a> ParseCollector<'a> {
113    fn new(s: &'a str) -> Self {
114        Self {
115            s,
116            year: ParsingYear::Unspecified,
117            day: ParsingDayOfYear::Unspecified,
118            hour: ParsingHour::Unspecified,
119            minute: 0,
120            second: 0,
121            nanosecond: 0,
122            zone: None,
123        }
124    }
125
126    #[inline]
127    fn skip_whitespaces(&mut self) {
128        self.s = self.s.trim_start();
129    }
130
131    #[inline]
132    fn get_until_whitespace(&mut self) -> &'a str {
133        let pos = self.s.find(char::is_whitespace).unwrap_or(self.s.len());
134        let (res, rest) = self.s.split_at(pos);
135        self.s = rest;
136        res
137    }
138
139    #[inline]
140    fn peek_byte(&self) -> Option<u8> {
141        self.s.bytes().next()
142    }
143
144    /// Note: Need a change if pass max_len that makes us require checking for overflow.
145    #[inline]
146    fn parse_nat<N: Nat>(&mut self, min_len: usize, max_len: usize) -> Result<N, ParseError> {
147        if self.s.len() < min_len {
148            return Err(ParseError::UnexpectedEnd("digits"));
149        }
150        let bytes = self.s.as_bytes();
151        let max_len = max_len.min(bytes.len());
152        let mut res = N::ZERO;
153        let mut bytes_read = 0;
154        for &c in &bytes[..max_len] {
155            if (b'0'..=b'9').contains(&c) {
156                res = (res * N::TEN) + N::from_u8(c - b'0');
157            } else if bytes_read < min_len {
158                return Err(ParseError::UnexpectedByte("digits", c));
159            } else {
160                break;
161            }
162            bytes_read += 1;
163        }
164        self.s = &self.s[bytes_read..];
165        Ok(res)
166    }
167
168    /// Allows '+'/'-'.
169    /// Note: Need a change if pass max_len that makes us require checking for overflow.
170    #[inline]
171    fn parse_int<Z: Int>(&mut self, max_len: usize) -> Result<Z, ParseError> {
172        if self.s.is_empty() {
173            return Err(ParseError::UnexpectedEnd("digits"));
174        }
175        let max_len = max_len.min(self.s.len());
176        let mut res = Z::ZERO;
177        let mut bytes_read = 0;
178        let mut negate = false;
179        let mut had_digit = false;
180        for &c in &self.s.as_bytes()[..max_len] {
181            if (b'0'..=b'9').contains(&c) {
182                res = (res * Z::TEN) + Z::from_u8(c - b'0');
183                had_digit = true;
184            } else if bytes_read == 0 {
185                if c == b'+' {
186                    // skip it
187                } else if c == b'-' {
188                    negate = true;
189                } else {
190                    return Err(ParseError::UnexpectedByte("digits or sign", c));
191                }
192            } else if had_digit {
193                break;
194            } else {
195                return Err(ParseError::UnexpectedByte("digits", c));
196            }
197            bytes_read += 1;
198        }
199        self.s = &self.s[bytes_read..];
200        Ok(if negate { -res } else { res })
201    }
202
203    #[inline]
204    fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool {
205        self.s.len() >= prefix.len()
206            && self.s.is_char_boundary(prefix.len())
207            && self.s[..prefix.len()].eq_ignore_ascii_case(prefix)
208    }
209}
210
211impl<'a> Collector for ParseCollector<'a> {
212    type Output = (PrimitiveDateTime, Option<TimeZoneSpecifier<'a>>);
213    type Error = ParseError;
214
215    #[inline]
216    fn spaces(&mut self) -> Result<(), Self::Error> {
217        self.skip_whitespaces();
218        Ok(())
219    }
220
221    #[inline]
222    fn day_of_week_name(&mut self) -> Result<(), Self::Error> {
223        let mut weekday = Weekday::Monday;
224        for _i in 0..7 {
225            let short = util::weekday_short_str(weekday);
226            if self.starts_with_ignore_ascii_case(short) {
227                let long = util::weekday_long_str(weekday);
228                if self.starts_with_ignore_ascii_case(long) {
229                    self.s = &self.s[long.len()..];
230                } else {
231                    self.s = &self.s[short.len()..];
232                }
233                // Found match. Ignore it!
234                return Ok(());
235            }
236            weekday = weekday.next();
237        }
238        Err(Self::Error::NotMatch("day of week name"))
239    }
240
241    #[inline]
242    fn month_name(&mut self) -> Result<(), Self::Error> {
243        let mut month = Month::January;
244        for _i in 0..12 {
245            let short = util::month_short_str(month);
246            if self.starts_with_ignore_ascii_case(short) {
247                let long = util::month_long_str(month);
248                if self.starts_with_ignore_ascii_case(long) {
249                    self.s = &self.s[long.len()..];
250                } else {
251                    self.s = &self.s[short.len()..];
252                }
253                match &mut self.day {
254                    ParsingDayOfYear::Unspecified => {
255                        self.day = ParsingDayOfYear::MonthDay(month, 1)
256                    }
257                    ParsingDayOfYear::MonthDay(current, _) => *current = month,
258                    // Prefer day of year over (month, day)
259                    ParsingDayOfYear::DayOfYear(_) => {}
260                }
261                return Ok(());
262            }
263            month = month.next();
264        }
265        Err(Self::Error::NotMatch("month name"))
266    }
267
268    #[inline]
269    fn year_prefix(&mut self) -> Result<(), Self::Error> {
270        let prefix = self.parse_int(2)?;
271        match &mut self.year {
272            ParsingYear::Unspecified => self.year = ParsingYear::PrefixSuffix(prefix, 0),
273            // Prefer year over (year prefix, year suffix).
274            ParsingYear::Year(_) => {}
275            ParsingYear::PrefixSuffix(v, _) => *v = prefix,
276        }
277        Ok(())
278    }
279
280    #[inline]
281    fn day_of_month(&mut self) -> Result<(), Self::Error> {
282        let day = self.parse_nat(1, 2)?;
283        if (1..=31).contains(&day) {
284            match &mut self.day {
285                ParsingDayOfYear::Unspecified => {
286                    self.day = ParsingDayOfYear::MonthDay(Month::January, day)
287                }
288                ParsingDayOfYear::MonthDay(_, current) => *current = day,
289                // Prefer day of year over (month, day)
290                ParsingDayOfYear::DayOfYear(_) => {}
291            }
292            Ok(())
293        } else {
294            Err(Self::Error::ComponentOutOfRange("day-of-month"))
295        }
296    }
297
298    #[inline]
299    fn hour_of_day(&mut self) -> Result<(), Self::Error> {
300        let hour = self.parse_nat(1, 2)?;
301        if (0..24).contains(&hour) {
302            match &mut self.hour {
303                ParsingHour::Unspecified => self.hour = ParsingHour::FullDay(hour),
304                ParsingHour::FullDay(current) => *current = hour,
305                // Prefer full day over halfday + am/pm.
306                ParsingHour::HalfDay(_, _) => {}
307            }
308            Ok(())
309        } else {
310            Err(Self::Error::ComponentOutOfRange("hour-of-day"))
311        }
312    }
313
314    #[inline]
315    fn hour_of_day_12(&mut self) -> Result<(), Self::Error> {
316        let hour: u8 = self.parse_nat(1, 2)?;
317        if (1..=12).contains(&hour) {
318            let hour = hour % 12;
319            match &mut self.hour {
320                ParsingHour::Unspecified => self.hour = ParsingHour::HalfDay(hour, false),
321                // Prefer full day over halfday + am/pm.
322                ParsingHour::FullDay(_) => {}
323                ParsingHour::HalfDay(current, _) => *current = hour,
324            }
325            Ok(())
326        } else {
327            Err(Self::Error::ComponentOutOfRange("hour-of-half-day"))
328        }
329    }
330
331    #[inline]
332    fn day_of_year(&mut self) -> Result<(), Self::Error> {
333        let day = self.parse_nat(1, 3)?;
334        if (1..=366).contains(&day) {
335            // Prefer day of year over (month, day)
336            self.day = ParsingDayOfYear::DayOfYear(day);
337            Ok(())
338        } else {
339            Err(Self::Error::ComponentOutOfRange("day-of-year"))
340        }
341    }
342
343    #[inline]
344    fn month_of_year(&mut self) -> Result<(), Self::Error> {
345        let month = self.parse_nat(1, 2)?;
346        if (1..=12).contains(&month) {
347            let month = util::get_month(month).unwrap();
348            match &mut self.day {
349                ParsingDayOfYear::Unspecified => self.day = ParsingDayOfYear::MonthDay(month, 1),
350                ParsingDayOfYear::MonthDay(current, _) => *current = month,
351                // Prefer day of year over (month, day)
352                ParsingDayOfYear::DayOfYear(_) => {}
353            }
354            Ok(())
355        } else {
356            Err(Self::Error::ComponentOutOfRange("month"))
357        }
358    }
359
360    #[inline]
361    fn minute_of_hour(&mut self) -> Result<(), Self::Error> {
362        let minute = self.parse_nat(1, 2)?;
363        if (0..60).contains(&minute) {
364            self.minute = minute;
365            Ok(())
366        } else {
367            Err(Self::Error::ComponentOutOfRange("munute"))
368        }
369    }
370
371    #[inline]
372    fn ampm(&mut self) -> Result<(), Self::Error> {
373        for h in [0, 12] {
374            let s = util::ampm_lower(h);
375            if self.starts_with_ignore_ascii_case(s) {
376                match &mut self.hour {
377                    ParsingHour::Unspecified => self.hour = ParsingHour::HalfDay(0, h != 0),
378                    // Prefer full day over halfday + am/pm.
379                    ParsingHour::FullDay(_) => {}
380                    ParsingHour::HalfDay(_, current) => *current = h != 0,
381                }
382                // Consume AM/PM substring
383                self.s = &self.s[2..];
384                return Ok(());
385            }
386        }
387        Err(Self::Error::NotMatch("am/pm"))
388    }
389
390    #[inline]
391    fn second_of_minute(&mut self) -> Result<(), Self::Error> {
392        let second = self.parse_nat(1, 2)?;
393        if (0..61).contains(&second) {
394            self.second = second;
395            Ok(())
396        } else {
397            Err(Self::Error::ComponentOutOfRange("second"))
398        }
399    }
400
401    #[inline]
402    fn nanosecond_of_second(&mut self) -> Result<(), Self::Error> {
403        let input_length = self.s.len();
404        let nanosecond: u32 = self.parse_nat(1, 9)?;
405
406        let digits_consumed = input_length - self.s.len();
407        static SCALE: [u32; 10] = [
408            0,
409            100_000_000,
410            10_000_000,
411            1_000_000,
412            100_000,
413            10_000,
414            1_000,
415            100,
416            10,
417            1,
418        ];
419        self.nanosecond = nanosecond * SCALE[digits_consumed];
420
421        Ok(())
422    }
423
424    #[inline]
425    fn week_number_of_current_year_start_sunday(&mut self) -> Result<(), Self::Error> {
426        let w: u8 = self.parse_nat(1, 2)?;
427        if (0..=53).contains(&w) {
428            // Ignore it!
429            Ok(())
430        } else {
431            Err(Self::Error::ComponentOutOfRange("week-number"))
432        }
433    }
434
435    #[inline]
436    fn day_of_week_from_sunday_as_0(&mut self) -> Result<(), Self::Error> {
437        let w: u8 = self.parse_nat(1, 1)?;
438        if (0..7).contains(&w) {
439            // Ignore it!
440            Ok(())
441        } else {
442            Err(Self::Error::ComponentOutOfRange("day-of-week"))
443        }
444    }
445
446    #[inline]
447    fn week_number_of_current_year_start_monday(&mut self) -> Result<(), Self::Error> {
448        let w: u8 = self.parse_nat(1, 2)?;
449        if (0..=53).contains(&w) {
450            Ok(())
451        } else {
452            Err(Self::Error::ComponentOutOfRange("week-number"))
453        }
454    }
455
456    #[inline]
457    fn year_suffix(&mut self) -> Result<(), Self::Error> {
458        let y = self.parse_nat(1, 2)?;
459        if (0..100).contains(&y) {
460            match &mut self.year {
461                ParsingYear::Unspecified => {
462                    self.year = ParsingYear::PrefixSuffix(if y < 69 { 20 } else { 19 }, y)
463                }
464                // Prefer year over (year prefix, year suffix).
465                ParsingYear::Year(_) => {}
466                ParsingYear::PrefixSuffix(_, current) => *current = y,
467            }
468            Ok(())
469        } else {
470            Err(Self::Error::ComponentOutOfRange("year-suffix"))
471        }
472    }
473
474    #[inline]
475    fn year(&mut self) -> Result<(), Self::Error> {
476        let y = self.parse_int(4)?;
477        // Prefer year over (year prefix, year suffix).
478        self.year = ParsingYear::Year(y);
479        Ok(())
480    }
481
482    #[inline]
483    fn timezone(&mut self) -> Result<(), Self::Error> {
484        let negate = match self.peek_byte() {
485            Some(b'Z') => {
486                self.s = &self.s[1..]; // skip Z
487                self.zone = Some(TimeZoneSpecifier::Offset(UtcOffset::UTC));
488                return Ok(());
489            }
490            Some(c @ (b'+' | b'-')) => {
491                self.s = &self.s[1..]; // skip sign
492                c == b'-'
493            }
494            Some(b) => return Err(Self::Error::UnexpectedByte("+ or -", b)),
495            None => return Err(Self::Error::UnexpectedEnd("+ or -")),
496        };
497        let h: u8 = self.parse_nat(2, 2)?;
498        if self.peek_byte() == Some(b':') {
499            self.s = &self.s[1..]; // skip :
500        }
501        let m: u8 = self.parse_nat(2, 2)?;
502        let h: i8 = h
503            .try_into()
504            .map_err(|_| Self::Error::ComponentOutOfRange("offset-hour"))?;
505        let m: i8 = m
506            .try_into()
507            .map_err(|_| Self::Error::ComponentOutOfRange("offset-minute"))?;
508        let (h, m) = if negate { (-h, -m) } else { (h, m) };
509        self.zone = Some(TimeZoneSpecifier::Offset(UtcOffset::from_hms(h, m, 0)?));
510        Ok(())
511    }
512
513    #[inline]
514    fn timezone_name(&mut self) -> Result<(), Self::Error> {
515        let s = self.get_until_whitespace();
516        self.zone = Some(TimeZoneSpecifier::Name(s));
517        Ok(())
518    }
519
520    #[inline]
521    fn static_str(&mut self, s: &'static str) -> Result<(), Self::Error> {
522        if let Some(rest) = self.s.strip_prefix(s) {
523            self.s = rest;
524            Ok(())
525        } else {
526            Err(Self::Error::NotMatch(s))
527        }
528    }
529
530    #[inline]
531    fn literal(
532        &mut self,
533        lit: &str,
534        _fmt_span: impl std::slice::SliceIndex<[u8], Output = [u8]>,
535    ) -> Result<(), Self::Error> {
536        if let Some(rest) = self.s.strip_prefix(lit) {
537            self.s = rest;
538            Ok(())
539        } else {
540            Err(Self::Error::NotMatch("string literal"))
541        }
542    }
543
544    #[inline]
545    fn unknown(&mut self, specifier: char) -> Result<(), Self::Error> {
546        Err(Self::Error::UnknownSpecifier(specifier))
547    }
548
549    #[inline]
550    fn unconsumed_input(&self) -> Result<(), Self::Error> {
551        let unconsumed_input = self.s.to_string();
552        if unconsumed_input.len() > 0 {
553            Err(Self::Error::UnconvertedDataRemains(unconsumed_input))
554        } else {
555            Ok(())
556        }
557    }
558
559    #[inline]
560    fn output(self) -> Result<Self::Output, Self::Error> {
561        let year = match self.year {
562            ParsingYear::Unspecified => 1900,
563            ParsingYear::Year(y) => y,
564            ParsingYear::PrefixSuffix(p, s) => p
565                .checked_mul(100)
566                .and_then(|p| p.checked_add(s as i32))
567                .ok_or(Self::Error::ComponentOutOfRange("year"))?,
568        };
569        let date = match self.day {
570            ParsingDayOfYear::Unspecified => Date::from_ordinal_date(year, 1)?,
571            ParsingDayOfYear::MonthDay(month, day) => Date::from_calendar_date(year, month, day)?,
572            ParsingDayOfYear::DayOfYear(day) => Date::from_ordinal_date(year, day)?,
573        };
574        let hour = match self.hour {
575            ParsingHour::Unspecified => 0,
576            ParsingHour::FullDay(h) => h,
577            ParsingHour::HalfDay(h, ampm) => {
578                if ampm {
579                    h + 12
580                } else {
581                    h
582                }
583            }
584        };
585        let time = Time::from_hms_nano(hour, self.minute, self.second, self.nanosecond)?;
586        let zone = self.zone;
587        Ok((PrimitiveDateTime::new(date, time), zone))
588    }
589}
590
591pub fn parse_date_time_maybe_with_zone<'a>(
592    fmt: &str,
593    s: &'a str,
594) -> Result<(PrimitiveDateTime, Option<TimeZoneSpecifier<'a>>), ParseError> {
595    let collector = ParseCollector::new(s);
596    desc_parser::parse_format_specifications(fmt, collector, false)
597}
598
599pub fn parse_strict_date_time_maybe_with_zone<'a>(
600    fmt: &str,
601    s: &'a str,
602) -> Result<(PrimitiveDateTime, Option<TimeZoneSpecifier<'a>>), ParseError> {
603    let collector = ParseCollector::new(s);
604    desc_parser::parse_format_specifications(fmt, collector, true)
605}
606
607#[cfg(test)]
608mod tests {
609    use super::{
610        parse_date_time_maybe_with_zone, parse_strict_date_time_maybe_with_zone, ParseError,
611        TimeZoneSpecifier,
612    };
613    use time::macros::{datetime, offset};
614
615    #[test]
616    fn test_simple_parse() -> Result<(), super::ParseError> {
617        assert_eq!(
618            parse_date_time_maybe_with_zone("%a %A %a", "wED Wed weDnesDay")?,
619            (datetime!(1900-01-01 00:00:00), None)
620        );
621        assert_eq!(
622            parse_date_time_maybe_with_zone("%b %B %b", "feB FEb feburaRy")?,
623            (datetime!(1900-02-01 00:00:00), None)
624        );
625        assert_eq!(
626            parse_date_time_maybe_with_zone("%c", "Sun Mar  6 12:34:56 2022")?,
627            (datetime!(2022-03-06 12:34:56), None)
628        );
629        assert_eq!(
630            parse_date_time_maybe_with_zone("%C", "20")?,
631            (datetime!(2000-01-01 00:00:00), None)
632        );
633        assert_eq!(
634            parse_date_time_maybe_with_zone("%d", "5")?,
635            (datetime!(1900-01-05 00:00:00), None)
636        );
637        assert_eq!(
638            parse_date_time_maybe_with_zone("%e", "5")?,
639            (datetime!(1900-01-05 00:00:00), None)
640        );
641        assert_eq!(
642            parse_date_time_maybe_with_zone("%D", "3 /6/22")?,
643            (datetime!(2022-03-06 00:00:00), None)
644        );
645        assert_eq!(
646            parse_date_time_maybe_with_zone("%F", "2022-03-06")?,
647            (datetime!(2022-03-06 00:00:00), None)
648        );
649        assert_eq!(
650            parse_date_time_maybe_with_zone("%H", "2")?,
651            (datetime!(1900-01-01 2:00:00), None)
652        );
653        assert_eq!(
654            parse_date_time_maybe_with_zone("%k", "2")?,
655            (datetime!(1900-01-01 2:00:00), None)
656        );
657        assert_eq!(
658            parse_date_time_maybe_with_zone("%I", "2")?,
659            (datetime!(1900-01-01 2:00:00), None)
660        );
661        assert_eq!(
662            parse_date_time_maybe_with_zone("%l", "12")?,
663            (datetime!(1900-01-01 00:00:00), None)
664        );
665        assert_eq!(
666            parse_date_time_maybe_with_zone("%j", "38")?,
667            (datetime!(1900-02-07 00:00:00), None)
668        );
669        assert_eq!(
670            parse_date_time_maybe_with_zone("%m", "8")?,
671            (datetime!(1900-08-01 00:00:00), None)
672        );
673        assert_eq!(
674            parse_date_time_maybe_with_zone("%M", "8")?,
675            (datetime!(1900-01-01 00:08:00), None)
676        );
677        assert_eq!(
678            parse_date_time_maybe_with_zone("%n%t ", "   ")?,
679            (datetime!(1900-01-01 00:00:00), None)
680        );
681        assert_eq!(
682            parse_date_time_maybe_with_zone("%I %p", "12 AM")?,
683            (datetime!(1900-01-01 00:00:00), None)
684        );
685        assert_eq!(
686            parse_date_time_maybe_with_zone("%I %p", "1 AM")?,
687            (datetime!(1900-01-01 01:00:00), None)
688        );
689        assert_eq!(
690            parse_date_time_maybe_with_zone("%I %p", "1 pm")?,
691            (datetime!(1900-01-01 13:00:00), None)
692        );
693        assert_eq!(
694            parse_date_time_maybe_with_zone("%I %p", "12 pm")?,
695            (datetime!(1900-01-01 12:00:00), None)
696        );
697        assert_eq!(
698            parse_date_time_maybe_with_zone("%r", "12:34:56 PM")?,
699            (datetime!(1900-01-01 12:34:56), None)
700        );
701        assert_eq!(
702            parse_date_time_maybe_with_zone("%r", "12:34:56 AM")?,
703            (datetime!(1900-01-01 00:34:56), None)
704        );
705        assert_eq!(
706            parse_date_time_maybe_with_zone("%r %F", "12:34:56 AM 2022-03-06")?,
707            (datetime!(2022-03-06 00:34:56), None)
708        );
709        assert_eq!(
710            parse_date_time_maybe_with_zone("%R", "12: 4")?,
711            (datetime!(1900-01-01 12:04:00), None)
712        );
713        assert_eq!(
714            parse_date_time_maybe_with_zone("%S", "01")?,
715            (datetime!(1900-01-01 00:00:01), None)
716        );
717        assert_eq!(
718            parse_date_time_maybe_with_zone("%T", "01:23:45")?,
719            (datetime!(1900-01-01 01:23:45), None)
720        );
721        assert_eq!(
722            parse_date_time_maybe_with_zone("%T.%f", "01:23:45.123")?,
723            (datetime!(1900-01-01 01:23:45.123), None)
724        );
725        assert_eq!(
726            parse_date_time_maybe_with_zone("%T.%f", "01:23:45.12300")?,
727            (datetime!(1900-01-01 01:23:45.123), None)
728        );
729        assert_eq!(
730            parse_date_time_maybe_with_zone("%T.%f", "01:23:45.000001234")?,
731            (datetime!(1900-01-01 01:23:45.000001234), None)
732        );
733        assert_eq!(
734            parse_date_time_maybe_with_zone("%U", "1")?,
735            (datetime!(1900-01-01 00:00:00), None)
736        );
737        assert_eq!(
738            parse_date_time_maybe_with_zone("%w", "1")?,
739            (datetime!(1900-01-01 00:00:00), None)
740        );
741        assert_eq!(
742            parse_date_time_maybe_with_zone("%W", "1")?,
743            (datetime!(1900-01-01 00:00:00), None)
744        );
745        assert_eq!(
746            parse_date_time_maybe_with_zone("%x", "3/6/22")?,
747            (datetime!(2022-03-06 00:00:00), None)
748        );
749        assert_eq!(
750            parse_date_time_maybe_with_zone("%X", "12:34:5")?,
751            (datetime!(1900-01-01 12:34:05), None)
752        );
753        assert_eq!(
754            parse_date_time_maybe_with_zone("%y", "12")?,
755            (datetime!(2012-01-01 00:00:00), None)
756        );
757        assert_eq!(
758            parse_date_time_maybe_with_zone("%y", "70")?,
759            (datetime!(1970-01-01 00:00:00), None)
760        );
761        assert_eq!(
762            parse_date_time_maybe_with_zone("%Y", "70")?,
763            (datetime!(0070-01-01 00:00:00), None)
764        );
765        assert_eq!(
766            parse_date_time_maybe_with_zone("%C%y", "2022")?,
767            (datetime!(2022-01-01 00:00:00), None)
768        );
769        Ok(())
770    }
771
772    #[test]
773    fn test_strict() {
774        assert!(matches!(
775            parse_strict_date_time_maybe_with_zone("%F", "2022-03-06T12:34:56Z"),
776            Err(ParseError::UnconvertedDataRemains(_)),
777        ));
778
779        assert_eq!(
780            parse_strict_date_time_maybe_with_zone("%FT%TZ", "2022-03-06T12:34:56Z"),
781            Ok((datetime!(2022-03-06 12:34:56), None))
782        );
783    }
784
785    #[test]
786    fn test_zone() -> Result<(), super::ParseError> {
787        assert_eq!(
788            parse_date_time_maybe_with_zone("%FT%TZ", "2022-03-06T12:34:56Z")?,
789            (datetime!(2022-03-06 12:34:56), None)
790        );
791        assert_eq!(
792            parse_date_time_maybe_with_zone("%FT%T%z", "2022-03-06T12:34:56Z")?,
793            (
794                datetime!(2022-03-06 12:34:56),
795                Some(TimeZoneSpecifier::Offset(offset!(+00:00)))
796            )
797        );
798        assert_eq!(
799            parse_date_time_maybe_with_zone("%FT%T%Z", "2022-03-06T12:34:56Z")?,
800            (
801                datetime!(2022-03-06 12:34:56),
802                Some(TimeZoneSpecifier::Name("Z"))
803            )
804        );
805        assert_eq!(
806            parse_date_time_maybe_with_zone("%FT%T %z", "2022-03-06T12:34:56 -1234")?,
807            (
808                datetime!(2022-03-06 12:34:56),
809                Some(TimeZoneSpecifier::Offset(offset!(-12:34)))
810            )
811        );
812        assert_eq!(
813            parse_date_time_maybe_with_zone("%FT%T %Z", "2022-03-06T12:34:56 JST")?,
814            (
815                datetime!(2022-03-06 12:34:56),
816                Some(TimeZoneSpecifier::Name("JST"))
817            )
818        );
819        assert_eq!(
820            parse_date_time_maybe_with_zone("%FT%T %z", "2022-03-06T12:34:56 -12:34")?,
821            (
822                datetime!(2022-03-06 12:34:56),
823                Some(TimeZoneSpecifier::Offset(offset!(-12:34)))
824            )
825        );
826        assert!(parse_date_time_maybe_with_zone("%FT%T %z", "2022-03-06T12:34:56 12:34").is_err());
827        assert!(parse_date_time_maybe_with_zone("%FT%T %z", "2022-03-06T12:34:56 +2:34").is_err());
828        assert!(parse_date_time_maybe_with_zone("%FT%T %z", "2022-03-06T12:34:56 +234").is_err());
829        Ok(())
830    }
831}