jiff/fmt/strtime/
parse.rs

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