jiff/fmt/strtime/
parse.rs

1use core::fmt::Write;
2
3use crate::{
4    civil::Weekday,
5    error::{err, ErrorContext},
6    fmt::{
7        offset,
8        strtime::{BrokenDownTime, Extension, Flag, Meridiem},
9        Parsed,
10    },
11    util::{
12        escape, parse,
13        rangeint::{ri8, RFrom},
14        t::{self, C},
15    },
16    Error, Timestamp,
17};
18
19pub(super) struct Parser<'f, 'i, 't> {
20    pub(super) fmt: &'f [u8],
21    pub(super) inp: &'i [u8],
22    pub(super) tm: &'t mut BrokenDownTime,
23}
24
25impl<'f, 'i, 't> Parser<'f, 'i, 't> {
26    pub(super) fn parse(&mut self) -> Result<(), Error> {
27        while !self.fmt.is_empty() {
28            if self.f() != b'%' {
29                self.parse_literal()?;
30                continue;
31            }
32            if !self.bump_fmt() {
33                return Err(err!(
34                    "invalid format string, expected byte after '%', \
35                     but found end of format string",
36                ));
37            }
38            // We don't check this for `%.` since that currently always
39            // must lead to `%.f` which can actually parse the empty string!
40            if self.inp.is_empty() && self.f() != b'.' {
41                return Err(err!(
42                    "expected non-empty input for directive %{directive}, \
43                     but found end of input",
44                    directive = escape::Byte(self.f()),
45                ));
46            }
47            // Parse extensions like padding/case options and padding width.
48            let ext = self.parse_extension()?;
49            match self.f() {
50                b'%' => self.parse_percent().context("%% failed")?,
51                b'A' => self.parse_weekday_full().context("%A failed")?,
52                b'a' => self.parse_weekday_abbrev().context("%a failed")?,
53                b'B' => self.parse_month_name_full().context("%B failed")?,
54                b'b' => self.parse_month_name_abbrev().context("%b failed")?,
55                b'C' => self.parse_century(ext).context("%C failed")?,
56                b'D' => self.parse_american_date().context("%D failed")?,
57                b'd' => self.parse_day(ext).context("%d failed")?,
58                b'e' => self.parse_day(ext).context("%e failed")?,
59                b'F' => self.parse_iso_date().context("%F failed")?,
60                b'f' => self.parse_fractional(ext).context("%f failed")?,
61                b'G' => self.parse_iso_week_year(ext).context("%G failed")?,
62                b'g' => self.parse_iso_week_year2(ext).context("%g failed")?,
63                b'H' => self.parse_hour24(ext).context("%H failed")?,
64                b'h' => self.parse_month_name_abbrev().context("%h failed")?,
65                b'I' => self.parse_hour12(ext).context("%I failed")?,
66                b'j' => self.parse_day_of_year(ext).context("%j failed")?,
67                b'k' => self.parse_hour24(ext).context("%k failed")?,
68                b'l' => self.parse_hour12(ext).context("%l failed")?,
69                b'M' => self.parse_minute(ext).context("%M failed")?,
70                b'm' => self.parse_month(ext).context("%m failed")?,
71                b'N' => self.parse_fractional(ext).context("%N failed")?,
72                b'n' => self.parse_whitespace().context("%n failed")?,
73                b'P' => self.parse_ampm().context("%P failed")?,
74                b'p' => self.parse_ampm().context("%p failed")?,
75                b'Q' => match ext.colons {
76                    0 => self.parse_iana_nocolon().context("%Q failed")?,
77                    1 => self.parse_iana_colon().context("%:Q failed")?,
78                    _ => {
79                        return Err(err!(
80                            "invalid number of `:` in `%Q` directive"
81                        ))
82                    }
83                },
84                b'R' => self.parse_clock_nosecs().context("%R failed")?,
85                b'S' => self.parse_second(ext).context("%S failed")?,
86                b's' => self.parse_timestamp(ext).context("%s failed")?,
87                b'T' => self.parse_clock_secs().context("%T failed")?,
88                b't' => self.parse_whitespace().context("%t failed")?,
89                b'U' => self.parse_week_sun(ext).context("%U failed")?,
90                b'u' => self.parse_weekday_mon(ext).context("%u failed")?,
91                b'V' => self.parse_week_iso(ext).context("%V failed")?,
92                b'W' => self.parse_week_mon(ext).context("%W failed")?,
93                b'w' => self.parse_weekday_sun(ext).context("%w failed")?,
94                b'Y' => self.parse_year(ext).context("%Y failed")?,
95                b'y' => self.parse_year2(ext).context("%y failed")?,
96                b'z' => match ext.colons {
97                    0 => self.parse_offset_nocolon().context("%z failed")?,
98                    1 => self.parse_offset_colon().context("%:z failed")?,
99                    2 => self.parse_offset_colon2().context("%::z failed")?,
100                    3 => self.parse_offset_colon3().context("%:::z failed")?,
101                    _ => {
102                        return Err(err!(
103                            "invalid number of `:` in `%z` directive"
104                        ))
105                    }
106                },
107                b'c' => {
108                    return Err(err!("cannot parse locale date and time"));
109                }
110                b'r' => {
111                    return Err(err!(
112                        "cannot parse locale 12-hour clock time"
113                    ));
114                }
115                b'X' => {
116                    return Err(err!("cannot parse locale clock time"));
117                }
118                b'x' => {
119                    return Err(err!("cannot parse locale date"));
120                }
121                b'Z' => {
122                    return Err(err!("cannot parse time zone abbreviations"));
123                }
124                b'.' => {
125                    if !self.bump_fmt() {
126                        return Err(err!(
127                            "invalid format string, expected directive \
128                             after '%.'",
129                        ));
130                    }
131                    // Skip over any precision settings that might be here.
132                    // This is a specific special format supported by `%.f`.
133                    let (width, fmt) = Extension::parse_width(self.fmt)?;
134                    let ext = Extension { width, ..ext };
135                    self.fmt = fmt;
136                    match self.f() {
137                        b'f' => self
138                            .parse_dot_fractional(ext)
139                            .context("%.f failed")?,
140                        unk => {
141                            return Err(err!(
142                                "found unrecognized directive %{unk} \
143                                 following %.",
144                                unk = escape::Byte(unk),
145                            ));
146                        }
147                    }
148                }
149                unk => {
150                    return Err(err!(
151                        "found unrecognized directive %{unk}",
152                        unk = escape::Byte(unk),
153                    ));
154                }
155            }
156        }
157        Ok(())
158    }
159
160    /// Returns the byte at the current position of the format string.
161    ///
162    /// # Panics
163    ///
164    /// This panics when the entire format string has been consumed.
165    fn f(&self) -> u8 {
166        self.fmt[0]
167    }
168
169    /// Returns the byte at the current position of the input string.
170    ///
171    /// # Panics
172    ///
173    /// This panics when the entire input string has been consumed.
174    fn i(&self) -> u8 {
175        self.inp[0]
176    }
177
178    /// Bumps the position of the format string.
179    ///
180    /// This returns true in precisely the cases where `self.f()` will not
181    /// panic. i.e., When the end of the format string hasn't been reached yet.
182    fn bump_fmt(&mut self) -> bool {
183        self.fmt = &self.fmt[1..];
184        !self.fmt.is_empty()
185    }
186
187    /// Bumps the position of the input string.
188    ///
189    /// This returns true in precisely the cases where `self.i()` will not
190    /// panic. i.e., When the end of the input string hasn't been reached yet.
191    fn bump_input(&mut self) -> bool {
192        self.inp = &self.inp[1..];
193        !self.inp.is_empty()
194    }
195
196    /// Parses optional extensions before a specifier directive. That is, right
197    /// after the `%`. If any extensions are parsed, the parser is bumped
198    /// to the next byte. (If no next byte exists, then an error is returned.)
199    fn parse_extension(&mut self) -> Result<Extension, Error> {
200        let (flag, fmt) = Extension::parse_flag(self.fmt)?;
201        let (width, fmt) = Extension::parse_width(fmt)?;
202        let (colons, fmt) = Extension::parse_colons(fmt)?;
203        self.fmt = fmt;
204        Ok(Extension { flag, width, colons })
205    }
206
207    // We write out a parsing routine for each directive below. Each parsing
208    // routine assumes that the parser is positioned immediately after the
209    // `%` for the current directive, and that there is at least one unconsumed
210    // byte in the input.
211
212    /// Parses a literal from the input that matches the current byte in the
213    /// format string.
214    ///
215    /// This may consume multiple bytes from the input, for example, a single
216    /// whitespace byte in the format string can match zero or more whitespace
217    /// in the input.
218    fn parse_literal(&mut self) -> Result<(), Error> {
219        if self.f().is_ascii_whitespace() {
220            if !self.inp.is_empty() {
221                while self.i().is_ascii_whitespace() && self.bump_input() {}
222            }
223        } else if self.inp.is_empty() {
224            return Err(err!(
225                "expected to match literal byte {byte:?} from \
226                 format string, but found end of input",
227                byte = escape::Byte(self.fmt[0]),
228            ));
229        } else if self.f() != self.i() {
230            return Err(err!(
231                "expected to match literal byte {expect:?} from \
232                 format string, but found byte {found:?} in input",
233                expect = escape::Byte(self.f()),
234                found = escape::Byte(self.i()),
235            ));
236        } else {
237            self.bump_input();
238        }
239        self.bump_fmt();
240        Ok(())
241    }
242
243    /// Parses an arbitrary (zero or more) amount ASCII whitespace.
244    ///
245    /// This is for `%n` and `%t`.
246    fn parse_whitespace(&mut self) -> Result<(), Error> {
247        if !self.inp.is_empty() {
248            while self.i().is_ascii_whitespace() && self.bump_input() {}
249        }
250        self.bump_fmt();
251        Ok(())
252    }
253
254    /// Parses a literal '%' from the input.
255    fn parse_percent(&mut self) -> Result<(), Error> {
256        if self.i() != b'%' {
257            return Err(err!(
258                "expected '%' due to '%%' in format string, \
259                 but found {byte:?} in input",
260                byte = escape::Byte(self.inp[0]),
261            ));
262        }
263        self.bump_fmt();
264        self.bump_input();
265        Ok(())
266    }
267
268    /// Parses `%D`, which is equivalent to `%m/%d/%y`.
269    fn parse_american_date(&mut self) -> Result<(), Error> {
270        let mut p = Parser { fmt: b"%m/%d/%y", inp: self.inp, tm: self.tm };
271        p.parse()?;
272        self.inp = p.inp;
273        self.bump_fmt();
274        Ok(())
275    }
276
277    /// Parse `%p`, which indicates whether the time is AM or PM.
278    ///
279    /// This is generally only useful with `%I`. If, say, `%H` is used, then
280    /// the AM/PM moniker will be validated, but it doesn't actually influence
281    /// the clock time.
282    fn parse_ampm(&mut self) -> Result<(), Error> {
283        let (index, inp) = parse_ampm(self.inp)?;
284        self.inp = inp;
285
286        self.tm.meridiem = Some(match index {
287            0 => Meridiem::AM,
288            1 => Meridiem::PM,
289            // OK because 0 <= index <= 1.
290            index => unreachable!("unknown AM/PM index {index}"),
291        });
292        self.bump_fmt();
293        Ok(())
294    }
295
296    /// Parses `%T`, which is equivalent to `%H:%M:%S`.
297    fn parse_clock_secs(&mut self) -> Result<(), Error> {
298        let mut p = Parser { fmt: b"%H:%M:%S", inp: self.inp, tm: self.tm };
299        p.parse()?;
300        self.inp = p.inp;
301        self.bump_fmt();
302        Ok(())
303    }
304
305    /// Parses `%R`, which is equivalent to `%H:%M`.
306    fn parse_clock_nosecs(&mut self) -> Result<(), Error> {
307        let mut p = Parser { fmt: b"%H:%M", inp: self.inp, tm: self.tm };
308        p.parse()?;
309        self.inp = p.inp;
310        self.bump_fmt();
311        Ok(())
312    }
313
314    /// Parses `%d` and `%e`, which is equivalent to the day of the month.
315    ///
316    /// We merely require that it is in the range 1-31 here.
317    fn parse_day(&mut self, ext: Extension) -> Result<(), Error> {
318        let (day, inp) = ext
319            .parse_number(2, Flag::PadZero, self.inp)
320            .context("failed to parse day")?;
321        self.inp = inp;
322
323        let day =
324            t::Day::try_new("day", day).context("day number is invalid")?;
325        self.tm.day = Some(day);
326        self.bump_fmt();
327        Ok(())
328    }
329
330    /// Parses `%j`, which is equivalent to the day of the year.
331    ///
332    /// We merely require that it is in the range 1-366 here.
333    fn parse_day_of_year(&mut self, ext: Extension) -> Result<(), Error> {
334        let (day, inp) = ext
335            .parse_number(3, Flag::PadZero, self.inp)
336            .context("failed to parse day of year")?;
337        self.inp = inp;
338
339        let day = t::DayOfYear::try_new("day-of-year", day)
340            .context("day of year number is invalid")?;
341        self.tm.day_of_year = Some(day);
342        self.bump_fmt();
343        Ok(())
344    }
345
346    /// Parses `%H`, which is equivalent to the hour.
347    fn parse_hour24(&mut self, ext: Extension) -> Result<(), Error> {
348        let (hour, inp) = ext
349            .parse_number(2, Flag::PadZero, self.inp)
350            .context("failed to parse hour")?;
351        self.inp = inp;
352
353        let hour = t::Hour::try_new("hour", hour)
354            .context("hour number is invalid")?;
355        self.tm.hour = Some(hour);
356        self.bump_fmt();
357        Ok(())
358    }
359
360    /// Parses `%I`, which is equivalent to the hour on a 12-hour clock.
361    fn parse_hour12(&mut self, ext: Extension) -> Result<(), Error> {
362        type Hour12 = ri8<1, 12>;
363
364        let (hour, inp) = ext
365            .parse_number(2, Flag::PadZero, self.inp)
366            .context("failed to parse hour")?;
367        self.inp = inp;
368
369        let hour =
370            Hour12::try_new("hour", hour).context("hour number is invalid")?;
371        self.tm.hour = Some(t::Hour::rfrom(hour));
372        self.bump_fmt();
373        Ok(())
374    }
375
376    /// Parses `%F`, which is equivalent to `%Y-%m-%d`.
377    fn parse_iso_date(&mut self) -> Result<(), Error> {
378        let mut p = Parser { fmt: b"%Y-%m-%d", inp: self.inp, tm: self.tm };
379        p.parse()?;
380        self.inp = p.inp;
381        self.bump_fmt();
382        Ok(())
383    }
384
385    /// Parses `%M`, which is equivalent to the minute.
386    fn parse_minute(&mut self, ext: Extension) -> Result<(), Error> {
387        let (minute, inp) = ext
388            .parse_number(2, Flag::PadZero, self.inp)
389            .context("failed to parse minute")?;
390        self.inp = inp;
391
392        let minute = t::Minute::try_new("minute", minute)
393            .context("minute number is invalid")?;
394        self.tm.minute = Some(minute);
395        self.bump_fmt();
396        Ok(())
397    }
398
399    /// Parse `%Q`, which is the IANA time zone identifier or an offset without
400    /// colons.
401    fn parse_iana_nocolon(&mut self) -> Result<(), Error> {
402        #[cfg(not(feature = "alloc"))]
403        {
404            Err(err!(
405                "cannot parse `%Q` without Jiff's `alloc` feature enabled"
406            ))
407        }
408        #[cfg(feature = "alloc")]
409        {
410            use alloc::string::ToString;
411
412            if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
413                return self.parse_offset_nocolon();
414            }
415            let (iana, inp) = parse_iana(self.inp)?;
416            self.inp = inp;
417            self.tm.iana = Some(iana.to_string());
418            self.bump_fmt();
419            Ok(())
420        }
421    }
422
423    /// Parse `%:Q`, which is the IANA time zone identifier or an offset with
424    /// colons.
425    fn parse_iana_colon(&mut self) -> Result<(), Error> {
426        #[cfg(not(feature = "alloc"))]
427        {
428            Err(err!(
429                "cannot parse `%:Q` without Jiff's `alloc` feature enabled"
430            ))
431        }
432        #[cfg(feature = "alloc")]
433        {
434            use alloc::string::ToString;
435
436            if !self.inp.is_empty() && matches!(self.inp[0], b'+' | b'-') {
437                return self.parse_offset_colon();
438            }
439            let (iana, inp) = parse_iana(self.inp)?;
440            self.inp = inp;
441            self.tm.iana = Some(iana.to_string());
442            self.bump_fmt();
443            Ok(())
444        }
445    }
446
447    /// Parse `%z`, which is a time zone offset without colons that requires
448    /// a minutes component but will parse a second component if it's there.
449    fn parse_offset_nocolon(&mut self) -> Result<(), Error> {
450        static PARSER: offset::Parser = offset::Parser::new()
451            .zulu(false)
452            .require_minute(true)
453            .subminute(true)
454            .subsecond(false)
455            .colon(offset::Colon::Absent);
456
457        let Parsed { value, input } = PARSER.parse(self.inp)?;
458        self.tm.offset = Some(value.to_offset()?);
459        self.inp = input;
460        self.bump_fmt();
461
462        Ok(())
463    }
464
465    /// Parse `%:z`, which is a time zone offset with colons that requires
466    /// a minutes component but will parse a second component if it's there.
467    fn parse_offset_colon(&mut self) -> Result<(), Error> {
468        static PARSER: offset::Parser = offset::Parser::new()
469            .zulu(false)
470            .require_minute(true)
471            .subminute(true)
472            .subsecond(false)
473            .colon(offset::Colon::Required);
474
475        let Parsed { value, input } = PARSER.parse(self.inp)?;
476        self.tm.offset = Some(value.to_offset()?);
477        self.inp = input;
478        self.bump_fmt();
479
480        Ok(())
481    }
482
483    /// Parse `%::z`, which is a time zone offset with colons that requires
484    /// a seconds component.
485    fn parse_offset_colon2(&mut self) -> Result<(), Error> {
486        static PARSER: offset::Parser = offset::Parser::new()
487            .zulu(false)
488            .require_minute(true)
489            .require_second(true)
490            .subminute(true)
491            .subsecond(false)
492            .colon(offset::Colon::Required);
493
494        let Parsed { value, input } = PARSER.parse(self.inp)?;
495        self.tm.offset = Some(value.to_offset()?);
496        self.inp = input;
497        self.bump_fmt();
498
499        Ok(())
500    }
501
502    /// Parse `%:::z`, which is a time zone offset with colons that only
503    /// requires an hour component, but will parse minute/second components
504    /// if they are there.
505    fn parse_offset_colon3(&mut self) -> Result<(), Error> {
506        static PARSER: offset::Parser = offset::Parser::new()
507            .zulu(false)
508            .require_minute(false)
509            .require_second(false)
510            .subminute(true)
511            .subsecond(false)
512            .colon(offset::Colon::Required);
513
514        let Parsed { value, input } = PARSER.parse(self.inp)?;
515        self.tm.offset = Some(value.to_offset()?);
516        self.inp = input;
517        self.bump_fmt();
518
519        Ok(())
520    }
521
522    /// Parses `%S`, which is equivalent to the second.
523    fn parse_second(&mut self, ext: Extension) -> Result<(), Error> {
524        let (mut second, inp) = ext
525            .parse_number(2, Flag::PadZero, self.inp)
526            .context("failed to parse second")?;
527        self.inp = inp;
528
529        // As with other parses in Jiff, and like Temporal,
530        // we constrain `60` seconds to `59` because we don't
531        // support leap seconds.
532        if second == 60 {
533            second = 59;
534        }
535        let second = t::Second::try_new("second", second)
536            .context("second number is invalid")?;
537        self.tm.second = Some(second);
538        self.bump_fmt();
539        Ok(())
540    }
541
542    /// Parses `%s`, which is equivalent to a Unix timestamp.
543    fn parse_timestamp(&mut self, ext: Extension) -> Result<(), Error> {
544        let (sign, inp) = parse_optional_sign(self.inp);
545        let (timestamp, inp) = ext
546            // 19 comes from `i64::MAX.to_string().len()`.
547            .parse_number(19, Flag::PadSpace, inp)
548            .context("failed to parse Unix timestamp (in seconds)")?;
549        // I believe this error case is actually impossible. Since `timestamp`
550        // is guaranteed to be positive, and negating any positive `i64` will
551        // always result in a valid `i64`.
552        let timestamp = timestamp.checked_mul(sign).ok_or_else(|| {
553            err!(
554                "parsed Unix timestamp `{timestamp}` with a \
555                 leading `-` sign, which causes overflow",
556            )
557        })?;
558        let timestamp =
559            Timestamp::from_second(timestamp).with_context(|| {
560                err!(
561                    "parsed Unix timestamp `{timestamp}`, \
562                     but out of range of valid Jiff `Timestamp`",
563                )
564            })?;
565        self.inp = inp;
566        self.tm.timestamp = Some(timestamp);
567
568        self.bump_fmt();
569        Ok(())
570    }
571
572    /// Parses `%f` (or `%N`, which is an alias for `%9f`), which is equivalent
573    /// to a fractional second up to nanosecond precision. This must always
574    /// parse at least one decimal digit and does not parse any leading dot.
575    ///
576    /// At present, we don't use any flags/width/precision settings to
577    /// influence parsing. That is, `%3f` will parse the fractional component
578    /// in `0.123456789`.
579    fn parse_fractional(&mut self, _ext: Extension) -> Result<(), Error> {
580        let mkdigits = parse::slicer(self.inp);
581        while mkdigits(self.inp).len() < 9
582            && self.inp.first().map_or(false, u8::is_ascii_digit)
583        {
584            self.inp = &self.inp[1..];
585        }
586        let digits = mkdigits(self.inp);
587        if digits.is_empty() {
588            return Err(err!(
589                "expected at least one fractional decimal digit, \
590                 but did not find any",
591            ));
592        }
593        // I believe this error can never happen, since we know we have no more
594        // than 9 ASCII digits. Any sequence of 9 ASCII digits can be parsed
595        // into an `i64`.
596        let nanoseconds = parse::fraction(digits).map_err(|err| {
597            err!(
598                "failed to parse {digits:?} as fractional second component \
599                 (up to 9 digits, nanosecond precision): {err}",
600                digits = escape::Bytes(digits),
601            )
602        })?;
603        // I believe this is also impossible to fail, since the maximal
604        // fractional nanosecond is 999_999_999, and which also corresponds
605        // to the maximal expressible number with 9 ASCII digits. So every
606        // possible expressible value here is in range.
607        let nanoseconds =
608            t::SubsecNanosecond::try_new("nanoseconds", nanoseconds).map_err(
609                |err| err!("fractional nanoseconds are not valid: {err}"),
610            )?;
611        self.tm.subsec = Some(nanoseconds);
612        self.bump_fmt();
613        Ok(())
614    }
615
616    /// Parses `%f`, which is equivalent to a dot followed by a fractional
617    /// second up to nanosecond precision. Note that if there is no leading
618    /// dot, then this successfully parses the empty string.
619    fn parse_dot_fractional(&mut self, ext: Extension) -> Result<(), Error> {
620        if !self.inp.starts_with(b".") {
621            self.bump_fmt();
622            return Ok(());
623        }
624        self.inp = &self.inp[1..];
625        self.parse_fractional(ext)
626    }
627
628    /// Parses `%m`, which is equivalent to the month.
629    fn parse_month(&mut self, ext: Extension) -> Result<(), Error> {
630        let (month, inp) = ext
631            .parse_number(2, Flag::PadZero, self.inp)
632            .context("failed to parse month")?;
633        self.inp = inp;
634
635        let month = t::Month::try_new("month", month)
636            .context("month number is invalid")?;
637        self.tm.month = Some(month);
638        self.bump_fmt();
639        Ok(())
640    }
641
642    /// Parse `%b` or `%h`, which is an abbreviated month name.
643    fn parse_month_name_abbrev(&mut self) -> Result<(), Error> {
644        let (index, inp) = parse_month_name_abbrev(self.inp)?;
645        self.inp = inp;
646
647        // Both are OK because 0 <= index <= 11.
648        let index = i8::try_from(index).unwrap();
649        self.tm.month = Some(t::Month::new(index + 1).unwrap());
650        self.bump_fmt();
651        Ok(())
652    }
653
654    /// Parse `%B`, which is a full month name.
655    fn parse_month_name_full(&mut self) -> Result<(), Error> {
656        static CHOICES: &'static [&'static [u8]] = &[
657            b"January",
658            b"February",
659            b"March",
660            b"April",
661            b"May",
662            b"June",
663            b"July",
664            b"August",
665            b"September",
666            b"October",
667            b"November",
668            b"December",
669        ];
670
671        let (index, inp) = parse_choice(self.inp, CHOICES)
672            .context("unrecognized month name")?;
673        self.inp = inp;
674
675        // Both are OK because 0 <= index <= 11.
676        let index = i8::try_from(index).unwrap();
677        self.tm.month = Some(t::Month::new(index + 1).unwrap());
678        self.bump_fmt();
679        Ok(())
680    }
681
682    /// Parse `%a`, which is an abbreviated weekday.
683    fn parse_weekday_abbrev(&mut self) -> Result<(), Error> {
684        let (index, inp) = parse_weekday_abbrev(self.inp)?;
685        self.inp = inp;
686
687        // Both are OK because 0 <= index <= 6.
688        let index = i8::try_from(index).unwrap();
689        self.tm.weekday =
690            Some(Weekday::from_sunday_zero_offset(index).unwrap());
691        self.bump_fmt();
692        Ok(())
693    }
694
695    /// Parse `%A`, which is a full weekday name.
696    fn parse_weekday_full(&mut self) -> Result<(), Error> {
697        static CHOICES: &'static [&'static [u8]] = &[
698            b"Sunday",
699            b"Monday",
700            b"Tuesday",
701            b"Wednesday",
702            b"Thursday",
703            b"Friday",
704            b"Saturday",
705        ];
706
707        let (index, inp) = parse_choice(self.inp, CHOICES)
708            .context("unrecognized weekday abbreviation")?;
709        self.inp = inp;
710
711        // Both are OK because 0 <= index <= 6.
712        let index = i8::try_from(index).unwrap();
713        self.tm.weekday =
714            Some(Weekday::from_sunday_zero_offset(index).unwrap());
715        self.bump_fmt();
716        Ok(())
717    }
718
719    /// Parse `%u`, which is a weekday number with Monday being `1` and
720    /// Sunday being `7`.
721    fn parse_weekday_mon(&mut self, ext: Extension) -> Result<(), Error> {
722        let (weekday, inp) = ext
723            .parse_number(1, Flag::NoPad, self.inp)
724            .context("failed to parse weekday number")?;
725        self.inp = inp;
726
727        let weekday = i8::try_from(weekday).map_err(|_| {
728            err!("parsed weekday number `{weekday}` is invalid")
729        })?;
730        let weekday = Weekday::from_monday_one_offset(weekday)
731            .context("weekday number is invalid")?;
732        self.tm.weekday = Some(weekday);
733        self.bump_fmt();
734        Ok(())
735    }
736
737    /// Parse `%w`, which is a weekday number with Sunday being `0`.
738    fn parse_weekday_sun(&mut self, ext: Extension) -> Result<(), Error> {
739        let (weekday, inp) = ext
740            .parse_number(1, Flag::NoPad, self.inp)
741            .context("failed to parse weekday number")?;
742        self.inp = inp;
743
744        let weekday = i8::try_from(weekday).map_err(|_| {
745            err!("parsed weekday number `{weekday}` is invalid")
746        })?;
747        let weekday = Weekday::from_sunday_zero_offset(weekday)
748            .context("weekday number is invalid")?;
749        self.tm.weekday = Some(weekday);
750        self.bump_fmt();
751        Ok(())
752    }
753
754    /// Parse `%U`, which is a week number with Sunday being the first day
755    /// in the first week numbered `01`.
756    fn parse_week_sun(&mut self, ext: Extension) -> Result<(), Error> {
757        let (week, inp) = ext
758            .parse_number(2, Flag::PadZero, self.inp)
759            .context("failed to parse Sunday-based week number")?;
760        self.inp = inp;
761
762        let week = t::WeekNum::try_new("week", week)
763            .context("Sunday-based week number is invalid")?;
764        self.tm.week_sun = Some(week);
765        self.bump_fmt();
766        Ok(())
767    }
768
769    /// Parse `%V`, which is an ISO 8601 week number.
770    fn parse_week_iso(&mut self, ext: Extension) -> Result<(), Error> {
771        let (week, inp) = ext
772            .parse_number(2, Flag::PadZero, self.inp)
773            .context("failed to parse ISO 8601 week number")?;
774        self.inp = inp;
775
776        let week = t::ISOWeek::try_new("week", week)
777            .context("ISO 8601 week number is invalid")?;
778        self.tm.iso_week = Some(week);
779        self.bump_fmt();
780        Ok(())
781    }
782
783    /// Parse `%W`, which is a week number with Monday being the first day
784    /// in the first week numbered `01`.
785    fn parse_week_mon(&mut self, ext: Extension) -> Result<(), Error> {
786        let (week, inp) = ext
787            .parse_number(2, Flag::PadZero, self.inp)
788            .context("failed to parse Monday-based week number")?;
789        self.inp = inp;
790
791        let week = t::WeekNum::try_new("week", week)
792            .context("Monday-based week number is invalid")?;
793        self.tm.week_mon = Some(week);
794        self.bump_fmt();
795        Ok(())
796    }
797
798    /// Parses `%Y`, which we permit to be any year, including a negative year.
799    fn parse_year(&mut self, ext: Extension) -> Result<(), Error> {
800        let (sign, inp) = parse_optional_sign(self.inp);
801        let (year, inp) = ext
802            .parse_number(4, Flag::PadZero, inp)
803            .context("failed to parse year")?;
804        self.inp = inp;
805
806        // OK because sign=={1,-1} and year can't be bigger than 4 digits
807        // so overflow isn't possible.
808        let year = sign.checked_mul(year).unwrap();
809        let year = t::Year::try_new("year", year)
810            .context("year number is invalid")?;
811        self.tm.year = Some(year);
812        self.bump_fmt();
813        Ok(())
814    }
815
816    /// Parses `%y`, which is equivalent to a 2-digit year.
817    ///
818    /// The numbers 69-99 refer to 1969-1999, while 00-68 refer to 2000-2068.
819    fn parse_year2(&mut self, ext: Extension) -> Result<(), Error> {
820        type Year2Digit = ri8<0, 99>;
821
822        let (year, inp) = ext
823            .parse_number(2, Flag::PadZero, self.inp)
824            .context("failed to parse 2-digit year")?;
825        self.inp = inp;
826
827        let year = Year2Digit::try_new("year (2 digits)", year)
828            .context("year number is invalid")?;
829        let mut year = t::Year::rfrom(year);
830        if year <= C(68) {
831            year += C(2000);
832        } else {
833            year += C(1900);
834        }
835        self.tm.year = Some(year);
836        self.bump_fmt();
837        Ok(())
838    }
839
840    /// Parses `%C`, which we permit to just be a century, including a negative
841    /// century.
842    fn parse_century(&mut self, ext: Extension) -> Result<(), Error> {
843        let (sign, inp) = parse_optional_sign(self.inp);
844        let (century, inp) = ext
845            .parse_number(2, Flag::NoPad, inp)
846            .context("failed to parse century")?;
847        self.inp = inp;
848
849        if !(0 <= century && century <= 99) {
850            return Err(err!(
851                "century `{century}` is too big, must be in range 0-99",
852            ));
853        }
854
855        // OK because sign=={1,-1} and century can't be bigger than 2 digits
856        // so overflow isn't possible.
857        let century = sign.checked_mul(century).unwrap();
858        // Similarly, we have 64-bit integers here. Two digits multiplied by
859        // 100 will never overflow.
860        let year = century.checked_mul(100).unwrap();
861        // I believe the error condition here is impossible.
862        let year = t::Year::try_new("year", year)
863            .context("year number (from century) is invalid")?;
864        self.tm.year = Some(year);
865        self.bump_fmt();
866        Ok(())
867    }
868
869    /// Parses `%G`, which we permit to be any year, including a negative year.
870    fn parse_iso_week_year(&mut self, ext: Extension) -> Result<(), Error> {
871        let (sign, inp) = parse_optional_sign(self.inp);
872        let (year, inp) = ext
873            .parse_number(4, Flag::PadZero, inp)
874            .context("failed to parse ISO 8601 week-based year")?;
875        self.inp = inp;
876
877        // OK because sign=={1,-1} and year can't be bigger than 4 digits
878        // so overflow isn't possible.
879        let year = sign.checked_mul(year).unwrap();
880        let year = t::ISOYear::try_new("year", year)
881            .context("ISO 8601 week-based year number is invalid")?;
882        self.tm.iso_week_year = Some(year);
883        self.bump_fmt();
884        Ok(())
885    }
886
887    /// Parses `%g`, which is equivalent to a 2-digit ISO 8601 week-based year.
888    ///
889    /// The numbers 69-99 refer to 1969-1999, while 00-68 refer to 2000-2068.
890    fn parse_iso_week_year2(&mut self, ext: Extension) -> Result<(), Error> {
891        type Year2Digit = ri8<0, 99>;
892
893        let (year, inp) = ext
894            .parse_number(2, Flag::PadZero, self.inp)
895            .context("failed to parse 2-digit ISO 8601 week-based year")?;
896        self.inp = inp;
897
898        let year = Year2Digit::try_new("year (2 digits)", year)
899            .context("ISO 8601 week-based year number is invalid")?;
900        let mut year = t::ISOYear::rfrom(year);
901        if year <= C(68) {
902            year += C(2000);
903        } else {
904            year += C(1900);
905        }
906        self.tm.iso_week_year = Some(year);
907        self.bump_fmt();
908        Ok(())
909    }
910}
911
912impl Extension {
913    /// Parse an integer with the given default padding and flag settings.
914    ///
915    /// The default padding is usually 2 (4 for %Y) and the default flag is
916    /// usually Flag::PadZero (there are no cases where the default flag is
917    /// different at time of writing). But both the padding and the flag can be
918    /// overridden by the settings on this extension.
919    ///
920    /// Generally speaking, parsing ignores everything in an extension except
921    /// for padding. When padding is set, then parsing will limit itself to a
922    /// number of digits equal to the greater of the default padding size or
923    /// the configured padding size. This permits `%Y%m%d` to parse `20240730`
924    /// successfully, for example.
925    ///
926    /// The remaining input is returned. This returns an error if the given
927    /// input is empty.
928    #[cfg_attr(feature = "perf-inline", inline(always))]
929    fn parse_number<'i>(
930        &self,
931        default_pad_width: usize,
932        default_flag: Flag,
933        mut inp: &'i [u8],
934    ) -> Result<(i64, &'i [u8]), Error> {
935        let flag = self.flag.unwrap_or(default_flag);
936        let zero_pad_width = match flag {
937            Flag::PadSpace | Flag::NoPad => 0,
938            _ => self.width.map(usize::from).unwrap_or(default_pad_width),
939        };
940        let max_digits = default_pad_width.max(zero_pad_width);
941
942        // Strip and ignore any whitespace we might see here.
943        while inp.get(0).map_or(false, |b| b.is_ascii_whitespace()) {
944            inp = &inp[1..];
945        }
946        let mut digits = 0;
947        while digits < inp.len()
948            && digits < zero_pad_width
949            && inp[digits] == b'0'
950        {
951            digits += 1;
952        }
953        let mut n: i64 = 0;
954        while digits < inp.len()
955            && digits < max_digits
956            && inp[digits].is_ascii_digit()
957        {
958            let byte = inp[digits];
959            digits += 1;
960            // This is manually inlined from `crate::util::parse::i64` to avoid
961            // repeating this loop, and with some error cases removed since we
962            // know that `byte` is an ASCII digit.
963            let digit = i64::from(byte - b'0');
964            n = n
965                .checked_mul(10)
966                .and_then(|n| n.checked_add(digit))
967                .ok_or_else(|| {
968                    err!(
969                        "number '{}' too big to parse into 64-bit integer",
970                        escape::Bytes(&inp[..digits]),
971                    )
972                })?;
973        }
974        if digits == 0 {
975            return Err(err!("invalid number, no digits found"));
976        }
977        Ok((n, &inp[digits..]))
978    }
979}
980
981/// Parses an optional sign from the beginning of the input. If one isn't
982/// found, then the sign returned is positive.
983///
984/// This also returns the remaining unparsed input.
985#[cfg_attr(feature = "perf-inline", inline(always))]
986fn parse_optional_sign<'i>(input: &'i [u8]) -> (i64, &'i [u8]) {
987    if input.is_empty() {
988        (1, input)
989    } else if input[0] == b'-' {
990        (-1, &input[1..])
991    } else if input[0] == b'+' {
992        (1, &input[1..])
993    } else {
994        (1, input)
995    }
996}
997
998/// Parses the input such that, on success, the index of the first matching
999/// choice (via ASCII case insensitive comparisons) is returned, along with
1000/// any remaining unparsed input.
1001///
1002/// If no choice given is a prefix of the input, then an error is returned.
1003/// The error includes the possible allowed choices.
1004fn parse_choice<'i>(
1005    input: &'i [u8],
1006    choices: &[&'static [u8]],
1007) -> Result<(usize, &'i [u8]), Error> {
1008    for (i, choice) in choices.into_iter().enumerate() {
1009        if input.len() < choice.len() {
1010            continue;
1011        }
1012        let (candidate, input) = input.split_at(choice.len());
1013        if candidate.eq_ignore_ascii_case(choice) {
1014            return Ok((i, input));
1015        }
1016    }
1017    #[cfg(feature = "alloc")]
1018    {
1019        let mut err = alloc::format!(
1020            "failed to find expected choice at beginning of {input:?}, \
1021             available choices are: ",
1022            input = escape::Bytes(input),
1023        );
1024        for (i, choice) in choices.iter().enumerate() {
1025            if i > 0 {
1026                write!(err, ", ").unwrap();
1027            }
1028            write!(err, "{}", escape::Bytes(choice)).unwrap();
1029        }
1030        Err(Error::adhoc(err))
1031    }
1032    #[cfg(not(feature = "alloc"))]
1033    {
1034        Err(err!(
1035            "failed to find expected value from a set of allowed choices"
1036        ))
1037    }
1038}
1039
1040/// Like `parse_choice`, but specialized for AM/PM.
1041///
1042/// This exists because AM/PM is common and we can take advantage of the fact
1043/// that they are both exactly two bytes.
1044#[cfg_attr(feature = "perf-inline", inline(always))]
1045fn parse_ampm<'i>(input: &'i [u8]) -> Result<(usize, &'i [u8]), Error> {
1046    if input.len() < 2 {
1047        return Err(err!(
1048            "expected to find AM or PM, \
1049             but the remaining input, {input:?}, is too short \
1050             to contain one",
1051            input = escape::Bytes(input),
1052        ));
1053    }
1054    let (x, input) = input.split_at(2);
1055    let candidate = &[x[0].to_ascii_lowercase(), x[1].to_ascii_lowercase()];
1056    let index = match candidate {
1057        b"am" => 0,
1058        b"pm" => 1,
1059        _ => {
1060            return Err(err!(
1061                "expected to find AM or PM, but found \
1062                {candidate:?} instead",
1063                candidate = escape::Bytes(x),
1064            ))
1065        }
1066    };
1067    Ok((index, input))
1068}
1069
1070/// Like `parse_choice`, but specialized for weekday abbreviation.
1071///
1072/// This exists because weekday abbreviations are common and we can take
1073/// advantage of the fact that they are all exactly three bytes.
1074#[cfg_attr(feature = "perf-inline", inline(always))]
1075fn parse_weekday_abbrev<'i>(
1076    input: &'i [u8],
1077) -> Result<(usize, &'i [u8]), Error> {
1078    if input.len() < 3 {
1079        return Err(err!(
1080            "expected to find a weekday abbreviation, \
1081             but the remaining input, {input:?}, is too short \
1082             to contain one",
1083            input = escape::Bytes(input),
1084        ));
1085    }
1086    let (x, input) = input.split_at(3);
1087    let candidate = &[
1088        x[0].to_ascii_lowercase(),
1089        x[1].to_ascii_lowercase(),
1090        x[2].to_ascii_lowercase(),
1091    ];
1092    let index = match candidate {
1093        b"sun" => 0,
1094        b"mon" => 1,
1095        b"tue" => 2,
1096        b"wed" => 3,
1097        b"thu" => 4,
1098        b"fri" => 5,
1099        b"sat" => 6,
1100        _ => {
1101            return Err(err!(
1102                "expected to find weekday abbreviation, but found \
1103                {candidate:?} instead",
1104                candidate = escape::Bytes(x),
1105            ))
1106        }
1107    };
1108    Ok((index, input))
1109}
1110
1111/// Like `parse_choice`, but specialized for month name abbreviation.
1112///
1113/// This exists because month name abbreviations are common and we can take
1114/// advantage of the fact that they are all exactly three bytes.
1115#[cfg_attr(feature = "perf-inline", inline(always))]
1116fn parse_month_name_abbrev<'i>(
1117    input: &'i [u8],
1118) -> Result<(usize, &'i [u8]), Error> {
1119    if input.len() < 3 {
1120        return Err(err!(
1121            "expected to find a month name abbreviation, \
1122             but the remaining input, {input:?}, is too short \
1123             to contain one",
1124            input = escape::Bytes(input),
1125        ));
1126    }
1127    let (x, input) = input.split_at(3);
1128    let candidate = &[
1129        x[0].to_ascii_lowercase(),
1130        x[1].to_ascii_lowercase(),
1131        x[2].to_ascii_lowercase(),
1132    ];
1133    let index = match candidate {
1134        b"jan" => 0,
1135        b"feb" => 1,
1136        b"mar" => 2,
1137        b"apr" => 3,
1138        b"may" => 4,
1139        b"jun" => 5,
1140        b"jul" => 6,
1141        b"aug" => 7,
1142        b"sep" => 8,
1143        b"oct" => 9,
1144        b"nov" => 10,
1145        b"dec" => 11,
1146        _ => {
1147            return Err(err!(
1148                "expected to find month name abbreviation, but found \
1149                 {candidate:?} instead",
1150                candidate = escape::Bytes(x),
1151            ))
1152        }
1153    };
1154    Ok((index, input))
1155}
1156
1157#[cfg_attr(feature = "perf-inline", inline(always))]
1158fn parse_iana<'i>(input: &'i [u8]) -> Result<(&'i str, &'i [u8]), Error> {
1159    let mkiana = parse::slicer(input);
1160    let (_, mut input) = parse_iana_component(input)?;
1161    while input.starts_with(b"/") {
1162        input = &input[1..];
1163        let (_, unconsumed) = parse_iana_component(input)?;
1164        input = unconsumed;
1165    }
1166    // This is OK because all bytes in a IANA TZ annotation are guaranteed
1167    // to be ASCII, or else we wouldn't be here. If this turns out to be
1168    // a perf issue, we can do an unchecked conversion here. But I figured
1169    // it would be better to start conservative.
1170    let iana = core::str::from_utf8(mkiana(input)).expect("ASCII");
1171    Ok((iana, input))
1172}
1173
1174/// Parses a single IANA name component. That is, the thing that leads all IANA
1175/// time zone identifiers and the thing that must always come after a `/`. This
1176/// returns an error if no component could be found.
1177#[cfg_attr(feature = "perf-inline", inline(always))]
1178fn parse_iana_component<'i>(
1179    mut input: &'i [u8],
1180) -> Result<(&'i [u8], &'i [u8]), Error> {
1181    let mkname = parse::slicer(input);
1182    if input.is_empty() {
1183        return Err(err!(
1184            "expected the start of an IANA time zone identifier \
1185             name or component, but found end of input instead",
1186        ));
1187    }
1188    if !matches!(input[0], b'_' | b'.' | b'A'..=b'Z' | b'a'..=b'z') {
1189        return Err(err!(
1190            "expected the start of an IANA time zone identifier \
1191             name or component, but found {:?} instead",
1192            escape::Byte(input[0]),
1193        ));
1194    }
1195    input = &input[1..];
1196
1197    let is_iana_char = |byte| {
1198        matches!(
1199            byte,
1200            b'_' | b'.' | b'+' | b'-' | b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z',
1201        )
1202    };
1203    while !input.is_empty() && is_iana_char(input[0]) {
1204        input = &input[1..];
1205    }
1206    Ok((mkname(input), input))
1207}
1208
1209#[cfg(feature = "alloc")]
1210#[cfg(test)]
1211mod tests {
1212    use alloc::string::ToString;
1213
1214    use super::*;
1215
1216    #[test]
1217    fn ok_parse_zoned() {
1218        if crate::tz::db().is_definitively_empty() {
1219            return;
1220        }
1221
1222        let p = |fmt: &str, input: &str| {
1223            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1224                .unwrap()
1225                .to_zoned()
1226                .unwrap()
1227        };
1228
1229        insta::assert_debug_snapshot!(
1230            p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -0400"),
1231            @"2022-04-01T20:46:15-04:00[-04:00]",
1232        );
1233        insta::assert_debug_snapshot!(
1234            p("%h %d, %Y %H:%M:%S %Q", "Apr 1, 2022 20:46:15 -0400"),
1235            @"2022-04-01T20:46:15-04:00[-04:00]",
1236        );
1237        insta::assert_debug_snapshot!(
1238            p("%h %d, %Y %H:%M:%S [%Q]", "Apr 1, 2022 20:46:15 [America/New_York]"),
1239            @"2022-04-01T20:46:15-04:00[America/New_York]",
1240        );
1241        insta::assert_debug_snapshot!(
1242            p("%h %d, %Y %H:%M:%S %Q", "Apr 1, 2022 20:46:15 America/New_York"),
1243            @"2022-04-01T20:46:15-04:00[America/New_York]",
1244        );
1245        insta::assert_debug_snapshot!(
1246            p("%h %d, %Y %H:%M:%S %:z %:Q", "Apr 1, 2022 20:46:15 -08:00 -04:00"),
1247            @"2022-04-01T20:46:15-04:00[-04:00]",
1248        );
1249    }
1250
1251    #[test]
1252    fn ok_parse_timestamp() {
1253        let p = |fmt: &str, input: &str| {
1254            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1255                .unwrap()
1256                .to_timestamp()
1257                .unwrap()
1258        };
1259
1260        insta::assert_debug_snapshot!(
1261            p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -0400"),
1262            @"2022-04-02T00:46:15Z",
1263        );
1264        insta::assert_debug_snapshot!(
1265            p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 +0400"),
1266            @"2022-04-01T16:46:15Z",
1267        );
1268        insta::assert_debug_snapshot!(
1269            p("%h %d, %Y %H:%M:%S %z", "Apr 1, 2022 20:46:15 -040059"),
1270            @"2022-04-02T00:47:14Z",
1271        );
1272
1273        insta::assert_debug_snapshot!(
1274            p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 -04:00"),
1275            @"2022-04-02T00:46:15Z",
1276        );
1277        insta::assert_debug_snapshot!(
1278            p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 +04:00"),
1279            @"2022-04-01T16:46:15Z",
1280        );
1281        insta::assert_debug_snapshot!(
1282            p("%h %d, %Y %H:%M:%S %:z", "Apr 1, 2022 20:46:15 -04:00:59"),
1283            @"2022-04-02T00:47:14Z",
1284        );
1285
1286        insta::assert_debug_snapshot!(
1287            p("%s", "0"),
1288            @"1970-01-01T00:00:00Z",
1289        );
1290        insta::assert_debug_snapshot!(
1291            p("%s", "-0"),
1292            @"1970-01-01T00:00:00Z",
1293        );
1294        insta::assert_debug_snapshot!(
1295            p("%s", "-1"),
1296            @"1969-12-31T23:59:59Z",
1297        );
1298        insta::assert_debug_snapshot!(
1299            p("%s", "1"),
1300            @"1970-01-01T00:00:01Z",
1301        );
1302        insta::assert_debug_snapshot!(
1303            p("%s", "+1"),
1304            @"1970-01-01T00:00:01Z",
1305        );
1306        insta::assert_debug_snapshot!(
1307            p("%s", "1737396540"),
1308            @"2025-01-20T18:09:00Z",
1309        );
1310        insta::assert_debug_snapshot!(
1311            p("%s", "-377705023201"),
1312            @"-009999-01-02T01:59:59Z",
1313        );
1314        insta::assert_debug_snapshot!(
1315            p("%s", "253402207200"),
1316            @"9999-12-30T22:00:00Z",
1317        );
1318    }
1319
1320    #[test]
1321    fn ok_parse_datetime() {
1322        let p = |fmt: &str, input: &str| {
1323            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1324                .unwrap()
1325                .to_datetime()
1326                .unwrap()
1327        };
1328
1329        insta::assert_debug_snapshot!(
1330            p("%h %d, %Y %H:%M:%S", "Apr 1, 2022 20:46:15"),
1331            @"2022-04-01T20:46:15",
1332        );
1333        insta::assert_debug_snapshot!(
1334            p("%h %05d, %Y %H:%M:%S", "Apr 1, 2022 20:46:15"),
1335            @"2022-04-01T20:46:15",
1336        );
1337    }
1338
1339    #[test]
1340    fn ok_parse_date() {
1341        let p = |fmt: &str, input: &str| {
1342            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1343                .unwrap()
1344                .to_date()
1345                .unwrap()
1346        };
1347
1348        insta::assert_debug_snapshot!(
1349            p("%m/%d/%y", "1/1/99"),
1350            @"1999-01-01",
1351        );
1352        insta::assert_debug_snapshot!(
1353            p("%m/%d/%04y", "1/1/0099"),
1354            @"1999-01-01",
1355        );
1356        insta::assert_debug_snapshot!(
1357            p("%D", "1/1/99"),
1358            @"1999-01-01",
1359        );
1360        insta::assert_debug_snapshot!(
1361            p("%m/%d/%Y", "1/1/0099"),
1362            @"0099-01-01",
1363        );
1364        insta::assert_debug_snapshot!(
1365            p("%m/%d/%Y", "1/1/1999"),
1366            @"1999-01-01",
1367        );
1368        insta::assert_debug_snapshot!(
1369            p("%m/%d/%Y", "12/31/9999"),
1370            @"9999-12-31",
1371        );
1372        insta::assert_debug_snapshot!(
1373            p("%m/%d/%Y", "01/01/-9999"),
1374            @"-009999-01-01",
1375        );
1376        insta::assert_snapshot!(
1377            p("%a %m/%d/%Y", "sun 7/14/2024"),
1378            @"2024-07-14",
1379        );
1380        insta::assert_snapshot!(
1381            p("%A %m/%d/%Y", "sUnDaY 7/14/2024"),
1382            @"2024-07-14",
1383        );
1384        insta::assert_snapshot!(
1385            p("%b %d %Y", "Jul 14 2024"),
1386            @"2024-07-14",
1387        );
1388        insta::assert_snapshot!(
1389            p("%B %d, %Y", "July 14, 2024"),
1390            @"2024-07-14",
1391        );
1392        insta::assert_snapshot!(
1393            p("%A, %B %d, %Y", "Wednesday, dEcEmBeR 25, 2024"),
1394            @"2024-12-25",
1395        );
1396
1397        insta::assert_debug_snapshot!(
1398            p("%Y%m%d", "20240730"),
1399            @"2024-07-30",
1400        );
1401        insta::assert_debug_snapshot!(
1402            p("%Y%m%d", "09990730"),
1403            @"0999-07-30",
1404        );
1405        insta::assert_debug_snapshot!(
1406            p("%Y%m%d", "9990111"),
1407            @"9990-11-01",
1408        );
1409        insta::assert_debug_snapshot!(
1410            p("%3Y%m%d", "09990111"),
1411            @"0999-01-11",
1412        );
1413        insta::assert_debug_snapshot!(
1414            p("%5Y%m%d", "09990111"),
1415            @"9990-11-01",
1416        );
1417        insta::assert_debug_snapshot!(
1418            p("%5Y%m%d", "009990111"),
1419            @"0999-01-11",
1420        );
1421
1422        insta::assert_debug_snapshot!(
1423            p("%C-%m-%d", "20-07-01"),
1424            @"2000-07-01",
1425        );
1426        insta::assert_debug_snapshot!(
1427            p("%C-%m-%d", "-20-07-01"),
1428            @"-002000-07-01",
1429        );
1430        insta::assert_debug_snapshot!(
1431            p("%C-%m-%d", "9-07-01"),
1432            @"0900-07-01",
1433        );
1434        insta::assert_debug_snapshot!(
1435            p("%C-%m-%d", "-9-07-01"),
1436            @"-000900-07-01",
1437        );
1438        insta::assert_debug_snapshot!(
1439            p("%C-%m-%d", "09-07-01"),
1440            @"0900-07-01",
1441        );
1442        insta::assert_debug_snapshot!(
1443            p("%C-%m-%d", "-09-07-01"),
1444            @"-000900-07-01",
1445        );
1446        insta::assert_debug_snapshot!(
1447            p("%C-%m-%d", "0-07-01"),
1448            @"0000-07-01",
1449        );
1450        insta::assert_debug_snapshot!(
1451            p("%C-%m-%d", "-0-07-01"),
1452            @"0000-07-01",
1453        );
1454
1455        insta::assert_snapshot!(
1456            p("%u %m/%d/%Y", "7 7/14/2024"),
1457            @"2024-07-14",
1458        );
1459        insta::assert_snapshot!(
1460            p("%w %m/%d/%Y", "0 7/14/2024"),
1461            @"2024-07-14",
1462        );
1463
1464        insta::assert_snapshot!(
1465            p("%Y-%U-%u", "2025-00-6"),
1466            @"2025-01-04",
1467        );
1468        insta::assert_snapshot!(
1469            p("%Y-%U-%u", "2025-01-7"),
1470            @"2025-01-05",
1471        );
1472        insta::assert_snapshot!(
1473            p("%Y-%U-%u", "2025-01-1"),
1474            @"2025-01-06",
1475        );
1476
1477        insta::assert_snapshot!(
1478            p("%Y-%W-%u", "2025-00-6"),
1479            @"2025-01-04",
1480        );
1481        insta::assert_snapshot!(
1482            p("%Y-%W-%u", "2025-00-7"),
1483            @"2025-01-05",
1484        );
1485        insta::assert_snapshot!(
1486            p("%Y-%W-%u", "2025-01-1"),
1487            @"2025-01-06",
1488        );
1489        insta::assert_snapshot!(
1490            p("%Y-%W-%u", "2025-01-2"),
1491            @"2025-01-07",
1492        );
1493    }
1494
1495    #[test]
1496    fn ok_parse_time() {
1497        let p = |fmt: &str, input: &str| {
1498            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1499                .unwrap()
1500                .to_time()
1501                .unwrap()
1502        };
1503
1504        insta::assert_debug_snapshot!(
1505            p("%H:%M", "15:48"),
1506            @"15:48:00",
1507        );
1508        insta::assert_debug_snapshot!(
1509            p("%H:%M:%S", "15:48:59"),
1510            @"15:48:59",
1511        );
1512        insta::assert_debug_snapshot!(
1513            p("%H:%M:%S", "15:48:60"),
1514            @"15:48:59",
1515        );
1516        insta::assert_debug_snapshot!(
1517            p("%T", "15:48:59"),
1518            @"15:48:59",
1519        );
1520        insta::assert_debug_snapshot!(
1521            p("%R", "15:48"),
1522            @"15:48:00",
1523        );
1524
1525        insta::assert_debug_snapshot!(
1526            p("%H %p", "5 am"),
1527            @"05:00:00",
1528        );
1529        insta::assert_debug_snapshot!(
1530            p("%H%p", "5am"),
1531            @"05:00:00",
1532        );
1533        insta::assert_debug_snapshot!(
1534            p("%H%p", "11pm"),
1535            @"23:00:00",
1536        );
1537        insta::assert_debug_snapshot!(
1538            p("%I%p", "11pm"),
1539            @"23:00:00",
1540        );
1541        insta::assert_debug_snapshot!(
1542            p("%I%p", "12am"),
1543            @"00:00:00",
1544        );
1545        insta::assert_debug_snapshot!(
1546            p("%H%p", "23pm"),
1547            @"23:00:00",
1548        );
1549        insta::assert_debug_snapshot!(
1550            p("%H%p", "23am"),
1551            @"11:00:00",
1552        );
1553
1554        insta::assert_debug_snapshot!(
1555            p("%H:%M:%S%.f", "15:48:01.1"),
1556            @"15:48:01.1",
1557        );
1558        insta::assert_debug_snapshot!(
1559            p("%H:%M:%S%.255f", "15:48:01.1"),
1560            @"15:48:01.1",
1561        );
1562        insta::assert_debug_snapshot!(
1563            p("%H:%M:%S%255.255f", "15:48:01.1"),
1564            @"15:48:01.1",
1565        );
1566        insta::assert_debug_snapshot!(
1567            p("%H:%M:%S%.f", "15:48:01"),
1568            @"15:48:01",
1569        );
1570        insta::assert_debug_snapshot!(
1571            p("%H:%M:%S%.fa", "15:48:01a"),
1572            @"15:48:01",
1573        );
1574        insta::assert_debug_snapshot!(
1575            p("%H:%M:%S%.f", "15:48:01.123456789"),
1576            @"15:48:01.123456789",
1577        );
1578        insta::assert_debug_snapshot!(
1579            p("%H:%M:%S%.f", "15:48:01.000000001"),
1580            @"15:48:01.000000001",
1581        );
1582
1583        insta::assert_debug_snapshot!(
1584            p("%H:%M:%S.%f", "15:48:01.1"),
1585            @"15:48:01.1",
1586        );
1587        insta::assert_debug_snapshot!(
1588            p("%H:%M:%S.%3f", "15:48:01.123"),
1589            @"15:48:01.123",
1590        );
1591        insta::assert_debug_snapshot!(
1592            p("%H:%M:%S.%3f", "15:48:01.123456"),
1593            @"15:48:01.123456",
1594        );
1595
1596        insta::assert_debug_snapshot!(
1597            p("%H:%M:%S.%N", "15:48:01.1"),
1598            @"15:48:01.1",
1599        );
1600        insta::assert_debug_snapshot!(
1601            p("%H:%M:%S.%3N", "15:48:01.123"),
1602            @"15:48:01.123",
1603        );
1604        insta::assert_debug_snapshot!(
1605            p("%H:%M:%S.%3N", "15:48:01.123456"),
1606            @"15:48:01.123456",
1607        );
1608
1609        insta::assert_debug_snapshot!(
1610            p("%H", "09"),
1611            @"09:00:00",
1612        );
1613        insta::assert_debug_snapshot!(
1614            p("%H", " 9"),
1615            @"09:00:00",
1616        );
1617        insta::assert_debug_snapshot!(
1618            p("%H", "15"),
1619            @"15:00:00",
1620        );
1621        insta::assert_debug_snapshot!(
1622            p("%k", "09"),
1623            @"09:00:00",
1624        );
1625        insta::assert_debug_snapshot!(
1626            p("%k", " 9"),
1627            @"09:00:00",
1628        );
1629        insta::assert_debug_snapshot!(
1630            p("%k", "15"),
1631            @"15:00:00",
1632        );
1633
1634        insta::assert_debug_snapshot!(
1635            p("%I", "09"),
1636            @"09:00:00",
1637        );
1638        insta::assert_debug_snapshot!(
1639            p("%I", " 9"),
1640            @"09:00:00",
1641        );
1642        insta::assert_debug_snapshot!(
1643            p("%l", "09"),
1644            @"09:00:00",
1645        );
1646        insta::assert_debug_snapshot!(
1647            p("%l", " 9"),
1648            @"09:00:00",
1649        );
1650    }
1651
1652    #[test]
1653    fn ok_parse_whitespace() {
1654        let p = |fmt: &str, input: &str| {
1655            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1656                .unwrap()
1657                .to_time()
1658                .unwrap()
1659        };
1660
1661        insta::assert_debug_snapshot!(
1662            p("%H%M", "1548"),
1663            @"15:48:00",
1664        );
1665        insta::assert_debug_snapshot!(
1666            p("%H%M", "15\n48"),
1667            @"15:48:00",
1668        );
1669        insta::assert_debug_snapshot!(
1670            p("%H%M", "15\t48"),
1671            @"15:48:00",
1672        );
1673        insta::assert_debug_snapshot!(
1674            p("%H%n%M", "1548"),
1675            @"15:48:00",
1676        );
1677        insta::assert_debug_snapshot!(
1678            p("%H%n%M", "15\n48"),
1679            @"15:48:00",
1680        );
1681        insta::assert_debug_snapshot!(
1682            p("%H%n%M", "15\t48"),
1683            @"15:48:00",
1684        );
1685        insta::assert_debug_snapshot!(
1686            p("%H%t%M", "1548"),
1687            @"15:48:00",
1688        );
1689        insta::assert_debug_snapshot!(
1690            p("%H%t%M", "15\n48"),
1691            @"15:48:00",
1692        );
1693        insta::assert_debug_snapshot!(
1694            p("%H%t%M", "15\t48"),
1695            @"15:48:00",
1696        );
1697    }
1698
1699    #[test]
1700    fn ok_parse_offset() {
1701        let p = |fmt: &str, input: &str| {
1702            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1703                .unwrap()
1704                .to_offset()
1705                .unwrap()
1706        };
1707
1708        insta::assert_debug_snapshot!(
1709            p("%z", "+0530"),
1710            @"05:30:00",
1711        );
1712        insta::assert_debug_snapshot!(
1713            p("%z", "-0530"),
1714            @"-05:30:00",
1715        );
1716        insta::assert_debug_snapshot!(
1717            p("%z", "-0500"),
1718            @"-05:00:00",
1719        );
1720        insta::assert_debug_snapshot!(
1721            p("%z", "+053015"),
1722            @"05:30:15",
1723        );
1724        insta::assert_debug_snapshot!(
1725            p("%z", "+050015"),
1726            @"05:00:15",
1727        );
1728
1729        insta::assert_debug_snapshot!(
1730            p("%:z", "+05:30"),
1731            @"05:30:00",
1732        );
1733        insta::assert_debug_snapshot!(
1734            p("%:z", "-05:30"),
1735            @"-05:30:00",
1736        );
1737        insta::assert_debug_snapshot!(
1738            p("%:z", "-05:00"),
1739            @"-05:00:00",
1740        );
1741        insta::assert_debug_snapshot!(
1742            p("%:z", "+05:30:15"),
1743            @"05:30:15",
1744        );
1745        insta::assert_debug_snapshot!(
1746            p("%:z", "-05:00:15"),
1747            @"-05:00:15",
1748        );
1749
1750        insta::assert_debug_snapshot!(
1751            p("%::z", "+05:30:15"),
1752            @"05:30:15",
1753        );
1754        insta::assert_debug_snapshot!(
1755            p("%::z", "-05:30:15"),
1756            @"-05:30:15",
1757        );
1758        insta::assert_debug_snapshot!(
1759            p("%::z", "-05:00:00"),
1760            @"-05:00:00",
1761        );
1762        insta::assert_debug_snapshot!(
1763            p("%::z", "-05:00:15"),
1764            @"-05:00:15",
1765        );
1766
1767        insta::assert_debug_snapshot!(
1768            p("%:::z", "+05"),
1769            @"05:00:00",
1770        );
1771        insta::assert_debug_snapshot!(
1772            p("%:::z", "-05"),
1773            @"-05:00:00",
1774        );
1775        insta::assert_debug_snapshot!(
1776            p("%:::z", "+00"),
1777            @"00:00:00",
1778        );
1779        insta::assert_debug_snapshot!(
1780            p("%:::z", "-00"),
1781            @"00:00:00",
1782        );
1783        insta::assert_debug_snapshot!(
1784            p("%:::z", "+05:30"),
1785            @"05:30:00",
1786        );
1787        insta::assert_debug_snapshot!(
1788            p("%:::z", "-05:30"),
1789            @"-05:30:00",
1790        );
1791        insta::assert_debug_snapshot!(
1792            p("%:::z", "+05:30:15"),
1793            @"05:30:15",
1794        );
1795        insta::assert_debug_snapshot!(
1796            p("%:::z", "-05:30:15"),
1797            @"-05:30:15",
1798        );
1799        insta::assert_debug_snapshot!(
1800            p("%:::z", "-05:00:00"),
1801            @"-05:00:00",
1802        );
1803        insta::assert_debug_snapshot!(
1804            p("%:::z", "-05:00:15"),
1805            @"-05:00:15",
1806        );
1807    }
1808
1809    #[test]
1810    fn err_parse() {
1811        let p = |fmt: &str, input: &str| {
1812            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
1813                .unwrap_err()
1814                .to_string()
1815        };
1816
1817        insta::assert_snapshot!(
1818            p("%M", ""),
1819            @"strptime parsing failed: expected non-empty input for directive %M, but found end of input",
1820        );
1821        insta::assert_snapshot!(
1822            p("%M", "a"),
1823            @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found",
1824        );
1825        insta::assert_snapshot!(
1826            p("%M%S", "15"),
1827            @"strptime parsing failed: expected non-empty input for directive %S, but found end of input",
1828        );
1829        insta::assert_snapshot!(
1830            p("%M%a", "Sun"),
1831            @"strptime parsing failed: %M failed: failed to parse minute: invalid number, no digits found",
1832        );
1833
1834        insta::assert_snapshot!(
1835            p("%y", "999"),
1836            @r###"strptime expects to consume the entire input, but "9" remains unparsed"###,
1837        );
1838        insta::assert_snapshot!(
1839            p("%Y", "-10000"),
1840            @r###"strptime expects to consume the entire input, but "0" remains unparsed"###,
1841        );
1842        insta::assert_snapshot!(
1843            p("%Y", "10000"),
1844            @r###"strptime expects to consume the entire input, but "0" remains unparsed"###,
1845        );
1846        insta::assert_snapshot!(
1847            p("%A %m/%d/%y", "Mon 7/14/24"),
1848            @r#"strptime parsing failed: %A failed: unrecognized weekday abbreviation: failed to find expected choice at beginning of "Mon 7/14/24", available choices are: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday"#,
1849        );
1850        insta::assert_snapshot!(
1851            p("%b", "Bad"),
1852            @r###"strptime parsing failed: %b failed: expected to find month name abbreviation, but found "Bad" instead"###,
1853        );
1854        insta::assert_snapshot!(
1855            p("%h", "July"),
1856            @r###"strptime expects to consume the entire input, but "y" remains unparsed"###,
1857        );
1858        insta::assert_snapshot!(
1859            p("%B", "Jul"),
1860            @r###"strptime parsing failed: %B failed: unrecognized month name: failed to find expected choice at beginning of "Jul", available choices are: January, February, March, April, May, June, July, August, September, October, November, December"###,
1861        );
1862        insta::assert_snapshot!(
1863            p("%H", "24"),
1864            @"strptime parsing failed: %H failed: hour number is invalid: parameter 'hour' with value 24 is not in the required range of 0..=23",
1865        );
1866        insta::assert_snapshot!(
1867            p("%M", "60"),
1868            @"strptime parsing failed: %M failed: minute number is invalid: parameter 'minute' with value 60 is not in the required range of 0..=59",
1869        );
1870        insta::assert_snapshot!(
1871            p("%S", "61"),
1872            @"strptime parsing failed: %S failed: second number is invalid: parameter 'second' with value 61 is not in the required range of 0..=59",
1873        );
1874        insta::assert_snapshot!(
1875            p("%I", "0"),
1876            @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 0 is not in the required range of 1..=12",
1877        );
1878        insta::assert_snapshot!(
1879            p("%I", "13"),
1880            @"strptime parsing failed: %I failed: hour number is invalid: parameter 'hour' with value 13 is not in the required range of 1..=12",
1881        );
1882        insta::assert_snapshot!(
1883            p("%p", "aa"),
1884            @r###"strptime parsing failed: %p failed: expected to find AM or PM, but found "aa" instead"###,
1885        );
1886
1887        insta::assert_snapshot!(
1888            p("%_", " "),
1889            @r###"strptime parsing failed: expected to find specifier directive after flag "_", but found end of format string"###,
1890        );
1891        insta::assert_snapshot!(
1892            p("%-", " "),
1893            @r###"strptime parsing failed: expected to find specifier directive after flag "-", but found end of format string"###,
1894        );
1895        insta::assert_snapshot!(
1896            p("%0", " "),
1897            @r###"strptime parsing failed: expected to find specifier directive after flag "0", but found end of format string"###,
1898        );
1899        insta::assert_snapshot!(
1900            p("%^", " "),
1901            @r###"strptime parsing failed: expected to find specifier directive after flag "^", but found end of format string"###,
1902        );
1903        insta::assert_snapshot!(
1904            p("%#", " "),
1905            @r###"strptime parsing failed: expected to find specifier directive after flag "#", but found end of format string"###,
1906        );
1907        insta::assert_snapshot!(
1908            p("%_1", " "),
1909            @"strptime parsing failed: expected to find specifier directive after width 1, but found end of format string",
1910        );
1911        insta::assert_snapshot!(
1912            p("%_23", " "),
1913            @"strptime parsing failed: expected to find specifier directive after width 23, but found end of format string",
1914        );
1915
1916        insta::assert_snapshot!(
1917            p("%:", " "),
1918            @"strptime parsing failed: expected to find specifier directive after 1 colons, but found end of format string",
1919        );
1920
1921        insta::assert_snapshot!(
1922            p("%::", " "),
1923            @"strptime parsing failed: expected to find specifier directive after 2 colons, but found end of format string",
1924        );
1925
1926        insta::assert_snapshot!(
1927            p("%:::", " "),
1928            @"strptime parsing failed: expected to find specifier directive after 3 colons, but found end of format string",
1929        );
1930
1931        insta::assert_snapshot!(
1932            p("%H:%M:%S%.f", "15:59:01."),
1933            @"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any",
1934        );
1935        insta::assert_snapshot!(
1936            p("%H:%M:%S%.f", "15:59:01.a"),
1937            @"strptime parsing failed: %.f failed: expected at least one fractional decimal digit, but did not find any",
1938        );
1939        insta::assert_snapshot!(
1940            p("%H:%M:%S%.f", "15:59:01.1234567891"),
1941            @r###"strptime expects to consume the entire input, but "1" remains unparsed"###,
1942        );
1943        insta::assert_snapshot!(
1944            p("%H:%M:%S.%f", "15:59:01."),
1945            @"strptime parsing failed: expected non-empty input for directive %f, but found end of input",
1946        );
1947        insta::assert_snapshot!(
1948            p("%H:%M:%S.%f", "15:59:01"),
1949            @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###,
1950        );
1951        insta::assert_snapshot!(
1952            p("%H:%M:%S.%f", "15:59:01.a"),
1953            @"strptime parsing failed: %f failed: expected at least one fractional decimal digit, but did not find any",
1954        );
1955        insta::assert_snapshot!(
1956            p("%H:%M:%S.%N", "15:59:01."),
1957            @"strptime parsing failed: expected non-empty input for directive %N, but found end of input",
1958        );
1959        insta::assert_snapshot!(
1960            p("%H:%M:%S.%N", "15:59:01"),
1961            @r###"strptime parsing failed: expected to match literal byte "." from format string, but found end of input"###,
1962        );
1963        insta::assert_snapshot!(
1964            p("%H:%M:%S.%N", "15:59:01.a"),
1965            @"strptime parsing failed: %N failed: expected at least one fractional decimal digit, but did not find any",
1966        );
1967
1968        insta::assert_snapshot!(
1969            p("%Q", "+America/New_York"),
1970            @r#"strptime parsing failed: %Q failed: failed to parse hours in UTC numeric offset "+America/New_York": failed to parse "Am" as hours (a two digit integer): invalid digit, expected 0-9 but got A"#,
1971        );
1972        insta::assert_snapshot!(
1973            p("%Q", "-America/New_York"),
1974            @r#"strptime parsing failed: %Q failed: failed to parse hours in UTC numeric offset "-America/New_York": failed to parse "Am" as hours (a two digit integer): invalid digit, expected 0-9 but got A"#,
1975        );
1976        insta::assert_snapshot!(
1977            p("%:Q", "+0400"),
1978            @r#"strptime parsing failed: %:Q failed: parsed hour component of time zone offset from "+0400", but could not find required colon separator"#,
1979        );
1980        insta::assert_snapshot!(
1981            p("%Q", "+04:00"),
1982            @r#"strptime parsing failed: %Q failed: parsed hour component of time zone offset from "+04:00", but found colon after hours which is not allowed"#,
1983        );
1984        insta::assert_snapshot!(
1985            p("%Q", "America/"),
1986            @"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found end of input instead",
1987        );
1988        insta::assert_snapshot!(
1989            p("%Q", "America/+"),
1990            @r###"strptime parsing failed: %Q failed: expected the start of an IANA time zone identifier name or component, but found "+" instead"###,
1991        );
1992
1993        insta::assert_snapshot!(
1994            p("%s", "-377705023202"),
1995            @"strptime parsing failed: %s failed: parsed Unix timestamp `-377705023202`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value -377705023202 is not in the required range of -377705023201..=253402207200",
1996        );
1997        insta::assert_snapshot!(
1998            p("%s", "253402207201"),
1999            @"strptime parsing failed: %s failed: parsed Unix timestamp `253402207201`, but out of range of valid Jiff `Timestamp`: parameter 'second' with value 253402207201 is not in the required range of -377705023201..=253402207200",
2000        );
2001        insta::assert_snapshot!(
2002            p("%s", "-9999999999999999999"),
2003            @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer",
2004        );
2005        insta::assert_snapshot!(
2006            p("%s", "9999999999999999999"),
2007            @"strptime parsing failed: %s failed: failed to parse Unix timestamp (in seconds): number '9999999999999999999' too big to parse into 64-bit integer",
2008        );
2009
2010        insta::assert_snapshot!(
2011            p("%u", "0"),
2012            @"strptime parsing failed: %u failed: weekday number is invalid: parameter 'weekday' with value 0 is not in the required range of 1..=7",
2013        );
2014        insta::assert_snapshot!(
2015            p("%w", "7"),
2016            @"strptime parsing failed: %w failed: weekday number is invalid: parameter 'weekday' with value 7 is not in the required range of 0..=6",
2017        );
2018        insta::assert_snapshot!(
2019            p("%u", "128"),
2020            @r###"strptime expects to consume the entire input, but "28" remains unparsed"###,
2021        );
2022        insta::assert_snapshot!(
2023            p("%w", "128"),
2024            @r###"strptime expects to consume the entire input, but "28" remains unparsed"###,
2025        );
2026    }
2027
2028    #[test]
2029    fn err_parse_date() {
2030        let p = |fmt: &str, input: &str| {
2031            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
2032                .unwrap()
2033                .to_date()
2034                .unwrap_err()
2035                .to_string()
2036        };
2037
2038        insta::assert_snapshot!(
2039            p("%Y", "2024"),
2040            @"a month/day, day-of-year or week date must be present to create a date, but none were found",
2041        );
2042        insta::assert_snapshot!(
2043            p("%m", "7"),
2044            @"missing year, date cannot be created",
2045        );
2046        insta::assert_snapshot!(
2047            p("%d", "25"),
2048            @"missing year, date cannot be created",
2049        );
2050        insta::assert_snapshot!(
2051            p("%Y-%m", "2024-7"),
2052            @"a month/day, day-of-year or week date must be present to create a date, but none were found",
2053        );
2054        insta::assert_snapshot!(
2055            p("%Y-%d", "2024-25"),
2056            @"a month/day, day-of-year or week date must be present to create a date, but none were found",
2057        );
2058        insta::assert_snapshot!(
2059            p("%m-%d", "7-25"),
2060            @"missing year, date cannot be created",
2061        );
2062
2063        insta::assert_snapshot!(
2064            p("%m/%d/%y", "6/31/24"),
2065            @"invalid date: parameter 'day' with value 31 is not in the required range of 1..=30",
2066        );
2067        insta::assert_snapshot!(
2068            p("%m/%d/%y", "2/29/23"),
2069            @"invalid date: parameter 'day' with value 29 is not in the required range of 1..=28",
2070        );
2071        insta::assert_snapshot!(
2072            p("%a %m/%d/%y", "Mon 7/14/24"),
2073            @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14",
2074        );
2075        insta::assert_snapshot!(
2076            p("%A %m/%d/%y", "Monday 7/14/24"),
2077            @"parsed weekday Monday does not match weekday Sunday from parsed date 2024-07-14",
2078        );
2079
2080        insta::assert_snapshot!(
2081            p("%Y-%U-%u", "2025-00-2"),
2082            @"weekday `Tuesday` is not valid for Sunday based week number `0` in year `2025`",
2083        );
2084        insta::assert_snapshot!(
2085            p("%Y-%W-%u", "2025-00-2"),
2086            @"weekday `Tuesday` is not valid for Monday based week number `0` in year `2025`",
2087        );
2088    }
2089
2090    #[test]
2091    fn err_parse_time() {
2092        let p = |fmt: &str, input: &str| {
2093            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
2094                .unwrap()
2095                .to_time()
2096                .unwrap_err()
2097                .to_string()
2098        };
2099
2100        insta::assert_snapshot!(
2101            p("%M", "59"),
2102            @"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)",
2103        );
2104        insta::assert_snapshot!(
2105            p("%S", "59"),
2106            @"parsing format did not include hour directive, but did include second directive (cannot have smaller time units with bigger time units missing)",
2107        );
2108        insta::assert_snapshot!(
2109            p("%M:%S", "59:59"),
2110            @"parsing format did not include hour directive, but did include minute directive (cannot have smaller time units with bigger time units missing)",
2111        );
2112        insta::assert_snapshot!(
2113            p("%H:%S", "15:59"),
2114            @"parsing format did not include minute directive, but did include second directive (cannot have smaller time units with bigger time units missing)",
2115        );
2116    }
2117
2118    #[test]
2119    fn err_parse_offset() {
2120        let p = |fmt: &str, input: &str| {
2121            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
2122                .unwrap_err()
2123                .to_string()
2124        };
2125
2126        insta::assert_snapshot!(
2127            p("%z", "+05:30"),
2128            @r#"strptime parsing failed: %z failed: parsed hour component of time zone offset from "+05:30", but found colon after hours which is not allowed"#,
2129        );
2130        insta::assert_snapshot!(
2131            p("%:z", "+0530"),
2132            @r#"strptime parsing failed: %:z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#,
2133        );
2134        insta::assert_snapshot!(
2135            p("%::z", "+0530"),
2136            @r#"strptime parsing failed: %::z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#,
2137        );
2138        insta::assert_snapshot!(
2139            p("%:::z", "+0530"),
2140            @r#"strptime parsing failed: %:::z failed: parsed hour component of time zone offset from "+0530", but could not find required colon separator"#,
2141        );
2142
2143        insta::assert_snapshot!(
2144            p("%z", "+05"),
2145            @r#"strptime parsing failed: %z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#,
2146        );
2147        insta::assert_snapshot!(
2148            p("%:z", "+05"),
2149            @r#"strptime parsing failed: %:z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#,
2150        );
2151        insta::assert_snapshot!(
2152            p("%::z", "+05"),
2153            @r#"strptime parsing failed: %::z failed: parsed hour component of time zone offset from "+05", but could not find required minute component"#,
2154        );
2155        insta::assert_snapshot!(
2156            p("%::z", "+05:30"),
2157            @r#"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset from "+05:30", but could not find required second component"#,
2158        );
2159        insta::assert_snapshot!(
2160            p("%:::z", "+5"),
2161            @r#"strptime parsing failed: %:::z failed: failed to parse hours in UTC numeric offset "+5": expected two digit hour after sign, but found end of input"#,
2162        );
2163
2164        insta::assert_snapshot!(
2165            p("%z", "+0530:15"),
2166            @r#"strptime expects to consume the entire input, but ":15" remains unparsed"#,
2167        );
2168        insta::assert_snapshot!(
2169            p("%:z", "+05:3015"),
2170            @r#"strptime expects to consume the entire input, but "15" remains unparsed"#,
2171        );
2172        insta::assert_snapshot!(
2173            p("%::z", "+05:3015"),
2174            @r#"strptime parsing failed: %::z failed: parsed hour and minute components of time zone offset from "+05:3015", but could not find required second component"#,
2175        );
2176        insta::assert_snapshot!(
2177            p("%:::z", "+05:3015"),
2178            @r#"strptime expects to consume the entire input, but "15" remains unparsed"#,
2179        );
2180    }
2181
2182    /// Regression test for checked arithmetic panicking.
2183    ///
2184    /// Ref https://github.com/BurntSushi/jiff/issues/426
2185    #[test]
2186    fn err_parse_large_century() {
2187        let p = |fmt: &str, input: &str| {
2188            BrokenDownTime::parse_mono(fmt.as_bytes(), input.as_bytes())
2189                .unwrap_err()
2190                .to_string()
2191        };
2192
2193        insta::assert_snapshot!(
2194            p("%^50C%", "2000000000000000000#0077)()"),
2195            @"strptime parsing failed: %C failed: century `2000000000000000000` is too big, must be in range 0-99",
2196        );
2197    }
2198}