jiff/fmt/temporal/
parser.rs

1use crate::{
2    civil::{Date, DateTime, Time},
3    error::{err, Error, ErrorContext},
4    fmt::{
5        offset::{self, ParsedOffset},
6        rfc9557::{self, ParsedAnnotations},
7        temporal::Pieces,
8        util::{
9            fractional_time_to_duration, fractional_time_to_span,
10            parse_temporal_fraction,
11        },
12        Parsed,
13    },
14    span::Span,
15    tz::{
16        AmbiguousZoned, Disambiguation, Offset, OffsetConflict, TimeZone,
17        TimeZoneDatabase,
18    },
19    util::{
20        escape, parse,
21        t::{self, C},
22    },
23    SignedDuration, Timestamp, Unit, Zoned,
24};
25
26/// The datetime components parsed from a string.
27#[derive(Debug)]
28pub(super) struct ParsedDateTime<'i> {
29    /// The original input that the datetime was parsed from.
30    input: escape::Bytes<'i>,
31    /// A required civil date.
32    date: ParsedDate<'i>,
33    /// An optional civil time.
34    time: Option<ParsedTime<'i>>,
35    /// An optional UTC offset.
36    offset: Option<ParsedOffset>,
37    /// An optional RFC 9557 annotations parsed.
38    ///
39    /// An empty `ParsedAnnotations` is valid and possible, so this bakes
40    /// optionality into the type and doesn't need to be an `Option` itself.
41    annotations: ParsedAnnotations<'i>,
42}
43
44impl<'i> ParsedDateTime<'i> {
45    #[cfg_attr(feature = "perf-inline", inline(always))]
46    pub(super) fn to_pieces(&self) -> Result<Pieces<'i>, Error> {
47        let mut pieces = Pieces::from(self.date.date);
48        if let Some(ref time) = self.time {
49            pieces = pieces.with_time(time.time);
50        }
51        if let Some(ref offset) = self.offset {
52            pieces = pieces.with_offset(offset.to_pieces_offset()?);
53        }
54        if let Some(ann) = self.annotations.to_time_zone_annotation()? {
55            pieces = pieces.with_time_zone_annotation(ann);
56        }
57        Ok(pieces)
58    }
59
60    #[cfg_attr(feature = "perf-inline", inline(always))]
61    pub(super) fn to_zoned(
62        &self,
63        db: &TimeZoneDatabase,
64        offset_conflict: OffsetConflict,
65        disambiguation: Disambiguation,
66    ) -> Result<Zoned, Error> {
67        self.to_ambiguous_zoned(db, offset_conflict)?
68            .disambiguate(disambiguation)
69    }
70
71    #[cfg_attr(feature = "perf-inline", inline(always))]
72    pub(super) fn to_ambiguous_zoned(
73        &self,
74        db: &TimeZoneDatabase,
75        offset_conflict: OffsetConflict,
76    ) -> Result<AmbiguousZoned, Error> {
77        let time = self.time.as_ref().map_or(Time::midnight(), |p| p.time);
78        let dt = DateTime::from_parts(self.date.date, time);
79
80        // We always require a time zone when parsing a zoned instant.
81        let tz_annotation =
82            self.annotations.to_time_zone_annotation()?.ok_or_else(|| {
83                err!(
84                    "failed to find time zone in square brackets \
85                     in {:?}, which is required for parsing a zoned instant",
86                    self.input,
87                )
88            })?;
89        let tz = tz_annotation.to_time_zone_with(db)?;
90
91        // If there's no offset, then our only choice, regardless of conflict
92        // resolution preference, is to use the time zone. That is, there is no
93        // possible conflict.
94        let Some(ref parsed_offset) = self.offset else {
95            return Ok(tz.into_ambiguous_zoned(dt));
96        };
97        if parsed_offset.is_zulu() {
98            // When `Z` is used, that means the offset to local time is not
99            // known. In this case, there really can't be a conflict because
100            // there is an explicit acknowledgment that the offset could be
101            // anything. So we just always accept `Z` as if it were `UTC` and
102            // respect that. If we didn't have this special check, we'd fall
103            // below and the `Z` would just be treated as `+00:00`, which would
104            // likely result in `OffsetConflict::Reject` raising an error.
105            // (Unless the actual correct offset at the time is `+00:00` for
106            // the time zone parsed.)
107            return OffsetConflict::AlwaysOffset
108                .resolve(dt, Offset::UTC, tz)
109                .with_context(|| {
110                    err!("parsing {input:?} failed", input = self.input)
111                });
112        }
113        let offset = parsed_offset.to_offset()?;
114        let is_equal = |parsed: Offset, candidate: Offset| {
115            // If they're equal down to the second, then no amount of rounding
116            // or whatever should change that.
117            if parsed == candidate {
118                return true;
119            }
120            // If the candidate offset we're considering is a whole minute,
121            // then we never need rounding.
122            if candidate.part_seconds_ranged() == C(0) {
123                return parsed == candidate;
124            }
125            let Ok(candidate) = candidate.round(Unit::Minute) else {
126                // This is a degenerate case and this is the only sensible
127                // thing to do.
128                return parsed == candidate;
129            };
130            parsed == candidate
131        };
132        offset_conflict.resolve_with(dt, offset, tz, is_equal).with_context(
133            || err!("parsing {input:?} failed", input = self.input),
134        )
135    }
136
137    #[cfg_attr(feature = "perf-inline", inline(always))]
138    pub(super) fn to_timestamp(&self) -> Result<Timestamp, Error> {
139        let time = self.time.as_ref().map(|p| p.time).ok_or_else(|| {
140            err!(
141                "failed to find time component in {:?}, \
142                 which is required for parsing a timestamp",
143                self.input,
144            )
145        })?;
146        let parsed_offset = self.offset.as_ref().ok_or_else(|| {
147            err!(
148                "failed to find offset component in {:?}, \
149                 which is required for parsing a timestamp",
150                self.input,
151            )
152        })?;
153        let offset = parsed_offset.to_offset()?;
154        let dt = DateTime::from_parts(self.date.date, time);
155        let timestamp = offset.to_timestamp(dt).with_context(|| {
156            err!(
157                "failed to convert civil datetime to timestamp \
158                 with offset {offset}",
159            )
160        })?;
161        Ok(timestamp)
162    }
163
164    #[cfg_attr(feature = "perf-inline", inline(always))]
165    pub(super) fn to_datetime(&self) -> Result<DateTime, Error> {
166        if self.offset.as_ref().map_or(false, |o| o.is_zulu()) {
167            return Err(err!(
168                "cannot parse civil date from string with a Zulu \
169                 offset, parse as a `Timestamp` and convert to a civil \
170                 datetime instead",
171            ));
172        }
173        Ok(DateTime::from_parts(self.date.date, self.time()))
174    }
175
176    #[cfg_attr(feature = "perf-inline", inline(always))]
177    pub(super) fn to_date(&self) -> Result<Date, Error> {
178        if self.offset.as_ref().map_or(false, |o| o.is_zulu()) {
179            return Err(err!(
180                "cannot parse civil date from string with a Zulu \
181                 offset, parse as a `Timestamp` and convert to a civil \
182                 date instead",
183            ));
184        }
185        Ok(self.date.date)
186    }
187
188    #[cfg_attr(feature = "perf-inline", inline(always))]
189    fn time(&self) -> Time {
190        self.time.as_ref().map(|p| p.time).unwrap_or(Time::midnight())
191    }
192}
193
194impl<'i> core::fmt::Display for ParsedDateTime<'i> {
195    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
196        core::fmt::Display::fmt(&self.input, f)
197    }
198}
199
200/// The result of parsing a Gregorian calendar civil date.
201#[derive(Debug)]
202pub(super) struct ParsedDate<'i> {
203    /// The original input that the date was parsed from.
204    input: escape::Bytes<'i>,
205    /// The actual parsed date.
206    date: Date,
207}
208
209impl<'i> core::fmt::Display for ParsedDate<'i> {
210    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
211        core::fmt::Display::fmt(&self.input, f)
212    }
213}
214
215/// The result of parsing a 24-hour civil time.
216#[derive(Debug)]
217pub(super) struct ParsedTime<'i> {
218    /// The original input that the time was parsed from.
219    input: escape::Bytes<'i>,
220    /// The actual parsed time.
221    time: Time,
222    /// Whether the time was parsed in extended format or not.
223    extended: bool,
224}
225
226impl<'i> ParsedTime<'i> {
227    pub(super) fn to_time(&self) -> Time {
228        self.time
229    }
230}
231
232impl<'i> core::fmt::Display for ParsedTime<'i> {
233    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
234        core::fmt::Display::fmt(&self.input, f)
235    }
236}
237
238#[derive(Debug)]
239pub(super) struct ParsedTimeZone<'i> {
240    /// The original input that the time zone was parsed from.
241    input: escape::Bytes<'i>,
242    /// The kind of time zone parsed.
243    kind: ParsedTimeZoneKind<'i>,
244}
245
246impl<'i> core::fmt::Display for ParsedTimeZone<'i> {
247    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
248        core::fmt::Display::fmt(&self.input, f)
249    }
250}
251
252#[derive(Debug)]
253pub(super) enum ParsedTimeZoneKind<'i> {
254    Named(&'i str),
255    Offset(ParsedOffset),
256    #[cfg(feature = "alloc")]
257    Posix(crate::tz::posix::PosixTimeZoneOwned),
258}
259
260impl<'i> ParsedTimeZone<'i> {
261    pub(super) fn into_time_zone(
262        self,
263        db: &TimeZoneDatabase,
264    ) -> Result<TimeZone, Error> {
265        match self.kind {
266            ParsedTimeZoneKind::Named(iana_name) => {
267                let tz = db.get(iana_name).with_context(|| {
268                    err!(
269                        "parsed apparent IANA time zone identifier \
270                         {iana_name} from {input}, but the tzdb lookup \
271                         failed",
272                        input = self.input,
273                    )
274                })?;
275                Ok(tz)
276            }
277            ParsedTimeZoneKind::Offset(poff) => {
278                let offset = poff.to_offset().with_context(|| {
279                    err!(
280                        "offset successfully parsed from {input}, \
281                         but failed to convert to numeric `Offset`",
282                        input = self.input,
283                    )
284                })?;
285                Ok(TimeZone::fixed(offset))
286            }
287            #[cfg(feature = "alloc")]
288            ParsedTimeZoneKind::Posix(posix_tz) => {
289                Ok(TimeZone::from_posix_tz(posix_tz))
290            }
291        }
292    }
293}
294
295/// A parser for Temporal datetimes.
296#[derive(Debug)]
297pub(super) struct DateTimeParser {
298    /// There are currently no configuration options for this parser.
299    _priv: (),
300}
301
302impl DateTimeParser {
303    /// Create a new Temporal datetime parser with the default configuration.
304    pub(super) const fn new() -> DateTimeParser {
305        DateTimeParser { _priv: () }
306    }
307
308    // TemporalDateTimeString[Zoned] :::
309    //   AnnotatedDateTime[?Zoned]
310    //
311    // AnnotatedDateTime[Zoned] :::
312    //   [~Zoned] DateTime TimeZoneAnnotation[opt] Annotations[opt]
313    //   [+Zoned] DateTime TimeZoneAnnotation Annotations[opt]
314    //
315    // DateTime :::
316    //   Date
317    //   Date DateTimeSeparator TimeSpec DateTimeUTCOffset[opt]
318    #[cfg_attr(feature = "perf-inline", inline(always))]
319    pub(super) fn parse_temporal_datetime<'i>(
320        &self,
321        input: &'i [u8],
322    ) -> Result<Parsed<'i, ParsedDateTime<'i>>, Error> {
323        let mkslice = parse::slicer(input);
324        let Parsed { value: date, input } = self.parse_date_spec(input)?;
325        if input.is_empty() {
326            let value = ParsedDateTime {
327                input: escape::Bytes(mkslice(input)),
328                date,
329                time: None,
330                offset: None,
331                annotations: ParsedAnnotations::none(),
332            };
333            return Ok(Parsed { value, input });
334        }
335        let (time, offset, input) = if !matches!(input[0], b' ' | b'T' | b't')
336        {
337            (None, None, input)
338        } else {
339            let input = &input[1..];
340            // If there's a separator, then we must parse a time and we are
341            // *allowed* to parse an offset. But without a separator, we don't
342            // support offsets. Just annotations (which are parsed below).
343            let Parsed { value: time, input } = self.parse_time_spec(input)?;
344            let Parsed { value: offset, input } = self.parse_offset(input)?;
345            (Some(time), offset, input)
346        };
347        let Parsed { value: annotations, input } =
348            self.parse_annotations(input)?;
349        let value = ParsedDateTime {
350            input: escape::Bytes(mkslice(input)),
351            date,
352            time,
353            offset,
354            annotations,
355        };
356        Ok(Parsed { value, input })
357    }
358
359    // TemporalTimeString :::
360    //   AnnotatedTime
361    //   AnnotatedDateTimeTimeRequired
362    //
363    // AnnotatedTime :::
364    //   TimeDesignator TimeSpec
365    //                  DateTimeUTCOffset[opt]
366    //                  TimeZoneAnnotation[opt]
367    //                  Annotations[opt]
368    //   TimeSpecWithOptionalOffsetNotAmbiguous TimeZoneAnnotation[opt]
369    //                                          Annotations[opt]
370    //
371    // TimeSpecWithOptionalOffsetNotAmbiguous :::
372    //   TimeSpec DateTimeUTCOffsetopt (but not one of ValidMonthDay or DateSpecYearMonth)
373    //
374    // TimeDesignator ::: one of
375    //   T t
376    #[cfg_attr(feature = "perf-inline", inline(always))]
377    pub(super) fn parse_temporal_time<'i>(
378        &self,
379        mut input: &'i [u8],
380    ) -> Result<Parsed<'i, ParsedTime<'i>>, Error> {
381        let mkslice = parse::slicer(input);
382
383        if input.starts_with(b"T") || input.starts_with(b"t") {
384            input = &input[1..];
385            let Parsed { value: time, input } = self.parse_time_spec(input)?;
386            let Parsed { value: offset, input } = self.parse_offset(input)?;
387            if offset.map_or(false, |o| o.is_zulu()) {
388                return Err(err!(
389                    "cannot parse civil time from string with a Zulu \
390                     offset, parse as a `Timestamp` and convert to a civil \
391                     time instead",
392                ));
393            }
394            let Parsed { input, .. } = self.parse_annotations(input)?;
395            return Ok(Parsed { value: time, input });
396        }
397        // We now look for a full datetime and extract the time from that.
398        // We do this before looking for a non-T time-only component because
399        // otherwise things like `2024-06-01T01:02:03` end up having `2024-06`
400        // parsed as a `HHMM-OFFSET` time, and then result in an "ambiguous"
401        // error.
402        //
403        // This is largely a result of us trying to parse a time off of the
404        // beginning of the input without assuming that the time must consume
405        // the entire input.
406        if let Ok(parsed) = self.parse_temporal_datetime(input) {
407            let Parsed { value: dt, input } = parsed;
408            if dt.offset.map_or(false, |o| o.is_zulu()) {
409                return Err(err!(
410                    "cannot parse plain time from full datetime string with a \
411                     Zulu offset, parse as a `Timestamp` and convert to a \
412                     plain time instead",
413                ));
414            }
415            let Some(time) = dt.time else {
416                return Err(err!(
417                    "successfully parsed date from {parsed:?}, but \
418                     no time component was found",
419                    parsed = dt.input,
420                ));
421            };
422            return Ok(Parsed { value: time, input });
423        }
424
425        // At this point, we look for something that is a time that doesn't
426        // start with a `T`. We need to check that it isn't ambiguous with a
427        // possible date.
428        let Parsed { value: time, input } = self.parse_time_spec(input)?;
429        let Parsed { value: offset, input } = self.parse_offset(input)?;
430        if offset.map_or(false, |o| o.is_zulu()) {
431            return Err(err!(
432                "cannot parse plain time from string with a Zulu \
433                 offset, parse as a `Timestamp` and convert to a plain \
434                 time instead",
435            ));
436        }
437        // The possible ambiguities occur with the time AND the
438        // optional offset, so try to parse what we have so far as
439        // either a "month-day" or a "year-month." If either succeeds,
440        // then the time is ambiguous and we can report an error.
441        //
442        // ... but this can only happen when the time was parsed in
443        // "basic" mode. i.e., without the `:` separators.
444        if !time.extended {
445            let possibly_ambiguous = mkslice(input);
446            if self.parse_month_day(possibly_ambiguous).is_ok() {
447                return Err(err!(
448                    "parsed time from {parsed:?} is ambiguous \
449                             with a month-day date",
450                    parsed = escape::Bytes(possibly_ambiguous),
451                ));
452            }
453            if self.parse_year_month(possibly_ambiguous).is_ok() {
454                return Err(err!(
455                    "parsed time from {parsed:?} is ambiguous \
456                             with a year-month date",
457                    parsed = escape::Bytes(possibly_ambiguous),
458                ));
459            }
460        }
461        // OK... carry on.
462        let Parsed { input, .. } = self.parse_annotations(input)?;
463        Ok(Parsed { value: time, input })
464    }
465
466    #[cfg_attr(feature = "perf-inline", inline(always))]
467    pub(super) fn parse_time_zone<'i>(
468        &self,
469        mut input: &'i [u8],
470    ) -> Result<Parsed<'i, ParsedTimeZone<'i>>, Error> {
471        let Some(first) = input.first().copied() else {
472            return Err(err!("an empty string is not a valid time zone"));
473        };
474        let original = escape::Bytes(input);
475        if matches!(first, b'+' | b'-') {
476            static P: offset::Parser = offset::Parser::new()
477                .zulu(false)
478                .subminute(true)
479                .subsecond(false);
480            let Parsed { value: offset, input } = P.parse(input)?;
481            let kind = ParsedTimeZoneKind::Offset(offset);
482            let value = ParsedTimeZone { input: original, kind };
483            return Ok(Parsed { value, input });
484        }
485
486        // Creates a "named" parsed time zone, generally meant to
487        // be an IANA time zone identifier. We do this in a couple
488        // different cases below, hence the helper function.
489        let mknamed = |consumed, remaining| {
490            let Ok(tzid) = core::str::from_utf8(consumed) else {
491                return Err(err!(
492                    "found plausible IANA time zone identifier \
493                     {input:?}, but it is not valid UTF-8",
494                    input = escape::Bytes(consumed),
495                ));
496            };
497            let kind = ParsedTimeZoneKind::Named(tzid);
498            let value = ParsedTimeZone { input: original, kind };
499            Ok(Parsed { value, input: remaining })
500        };
501        // This part get tricky. The common case is absolutely an IANA time
502        // zone identifer. So we try to parse something that looks like an IANA
503        // tz id.
504        //
505        // In theory, IANA tz ids can never be valid POSIX TZ strings, since
506        // POSIX TZ strings minimally require an offset in them (e.g., `EST5`)
507        // and IANA tz ids aren't supposed to contain numbers. But there are
508        // some legacy IANA tz ids (`EST5EDT`) that do contain numbers.
509        //
510        // However, the legacy IANA tz ids, like `EST5EDT`, are pretty much
511        // nonsense as POSIX TZ strings since there is no DST transition rule.
512        // So in cases of nonsense tz ids, we assume they are IANA tz ids.
513        let mkconsumed = parse::slicer(input);
514        let mut saw_number = false;
515        loop {
516            let Some(byte) = input.first().copied() else { break };
517            if byte.is_ascii_whitespace() {
518                break;
519            }
520            saw_number = saw_number || byte.is_ascii_digit();
521            input = &input[1..];
522        }
523        let consumed = mkconsumed(input);
524        if !saw_number {
525            return mknamed(consumed, input);
526        }
527        #[cfg(not(feature = "alloc"))]
528        {
529            Err(err!(
530                "cannot parsed time zones other than fixed offsets \
531                 without the `alloc` crate feature enabled",
532            ))
533        }
534        #[cfg(feature = "alloc")]
535        {
536            use crate::tz::posix::PosixTimeZone;
537
538            match PosixTimeZone::parse_prefix(consumed) {
539                Ok((posix_tz, input)) => {
540                    let kind = ParsedTimeZoneKind::Posix(posix_tz);
541                    let value = ParsedTimeZone { input: original, kind };
542                    Ok(Parsed { value, input })
543                }
544                // We get here for invalid POSIX tz strings, or even if
545                // they are technically valid according to POSIX but not
546                // "reasonable", i.e., `EST5EDT`. Which in that case would
547                // end up doing an IANA tz lookup. (And it might hit because
548                // `EST5EDT` is a legacy IANA tz id. Lol.)
549                Err(_) => mknamed(consumed, input),
550            }
551        }
552    }
553
554    // Date :::
555    //   DateYear - DateMonth - DateDay
556    //   DateYear DateMonth DateDay
557    #[cfg_attr(feature = "perf-inline", inline(always))]
558    fn parse_date_spec<'i>(
559        &self,
560        input: &'i [u8],
561    ) -> Result<Parsed<'i, ParsedDate<'i>>, Error> {
562        let mkslice = parse::slicer(input);
563        let original = escape::Bytes(input);
564
565        // Parse year component.
566        let Parsed { value: year, input } =
567            self.parse_year(input).with_context(|| {
568                err!("failed to parse year in date {original:?}")
569            })?;
570        let extended = input.starts_with(b"-");
571
572        // Parse optional separator.
573        let Parsed { input, .. } = self
574            .parse_date_separator(input, extended)
575            .context("failed to parse separator after year")?;
576
577        // Parse month component.
578        let Parsed { value: month, input } =
579            self.parse_month(input).with_context(|| {
580                err!("failed to parse month in date {original:?}")
581            })?;
582
583        // Parse optional separator.
584        let Parsed { input, .. } = self
585            .parse_date_separator(input, extended)
586            .context("failed to parse separator after month")?;
587
588        // Parse day component.
589        let Parsed { value: day, input } =
590            self.parse_day(input).with_context(|| {
591                err!("failed to parse day in date {original:?}")
592            })?;
593
594        let date = Date::new_ranged(year, month, day).with_context(|| {
595            err!("date parsed from {original:?} is not valid")
596        })?;
597        let value = ParsedDate { input: escape::Bytes(mkslice(input)), date };
598        Ok(Parsed { value, input })
599    }
600
601    // TimeSpec :::
602    //   TimeHour
603    //   TimeHour : TimeMinute
604    //   TimeHour TimeMinute
605    //   TimeHour : TimeMinute : TimeSecond TimeFraction[opt]
606    //   TimeHour TimeMinute TimeSecond TimeFraction[opt]
607    #[cfg_attr(feature = "perf-inline", inline(always))]
608    fn parse_time_spec<'i>(
609        &self,
610        input: &'i [u8],
611    ) -> Result<Parsed<'i, ParsedTime<'i>>, Error> {
612        let mkslice = parse::slicer(input);
613        let original = escape::Bytes(input);
614
615        // Parse hour component.
616        let Parsed { value: hour, input } =
617            self.parse_hour(input).with_context(|| {
618                err!("failed to parse hour in time {original:?}")
619            })?;
620        let extended = input.starts_with(b":");
621
622        // Parse optional minute component.
623        let Parsed { value: has_minute, input } =
624            self.parse_time_separator(input, extended);
625        if !has_minute {
626            let time = Time::new_ranged(
627                hour,
628                t::Minute::N::<0>(),
629                t::Second::N::<0>(),
630                t::SubsecNanosecond::N::<0>(),
631            );
632            let value = ParsedTime {
633                input: escape::Bytes(mkslice(input)),
634                time,
635                extended,
636            };
637            return Ok(Parsed { value, input });
638        }
639        let Parsed { value: minute, input } =
640            self.parse_minute(input).with_context(|| {
641                err!("failed to parse minute in time {original:?}")
642            })?;
643
644        // Parse optional second component.
645        let Parsed { value: has_second, input } =
646            self.parse_time_separator(input, extended);
647        if !has_second {
648            let time = Time::new_ranged(
649                hour,
650                minute,
651                t::Second::N::<0>(),
652                t::SubsecNanosecond::N::<0>(),
653            );
654            let value = ParsedTime {
655                input: escape::Bytes(mkslice(input)),
656                time,
657                extended,
658            };
659            return Ok(Parsed { value, input });
660        }
661        let Parsed { value: second, input } =
662            self.parse_second(input).with_context(|| {
663                err!("failed to parse second in time {original:?}")
664            })?;
665
666        // Parse an optional fractional component.
667        let Parsed { value: nanosecond, input } =
668            parse_temporal_fraction(input).with_context(|| {
669                err!(
670                    "failed to parse fractional nanoseconds \
671                     in time {original:?}",
672                )
673            })?;
674
675        let time = Time::new_ranged(
676            hour,
677            minute,
678            second,
679            nanosecond.unwrap_or(t::SubsecNanosecond::N::<0>()),
680        );
681        let value = ParsedTime {
682            input: escape::Bytes(mkslice(input)),
683            time,
684            extended,
685        };
686        Ok(Parsed { value, input })
687    }
688
689    // ValidMonthDay :::
690    //   DateMonth -[opt] 0 NonZeroDigit
691    //   DateMonth -[opt] 1 DecimalDigit
692    //   DateMonth -[opt] 2 DecimalDigit
693    //   DateMonth -[opt] 30 but not one of 0230 or 02-30
694    //   DateMonthWithThirtyOneDays -opt 31
695    //
696    // DateMonthWithThirtyOneDays ::: one of
697    //   01 03 05 07 08 10 12
698    //
699    // NOTE: Jiff doesn't have a "month-day" type, but we still have a parsing
700    // function for it so that we can detect ambiguous time strings.
701    #[cfg_attr(feature = "perf-inline", inline(always))]
702    fn parse_month_day<'i>(
703        &self,
704        input: &'i [u8],
705    ) -> Result<Parsed<'i, ()>, Error> {
706        let original = escape::Bytes(input);
707
708        // Parse month component.
709        let Parsed { value: month, mut input } =
710            self.parse_month(input).with_context(|| {
711                err!("failed to parse month in month-day {original:?}")
712            })?;
713
714        // Skip over optional separator.
715        if input.starts_with(b"-") {
716            input = &input[1..];
717        }
718
719        // Parse day component.
720        let Parsed { value: day, input } =
721            self.parse_day(input).with_context(|| {
722                err!("failed to parse day in month-day {original:?}")
723            })?;
724
725        // Check that the month-day is valid. Since Temporal's month-day
726        // permits 02-29, we use a leap year. The error message here is
727        // probably confusing, but these errors should never be exposed to the
728        // user.
729        let year = t::Year::N::<2024>();
730        let _ = Date::new_ranged(year, month, day).with_context(|| {
731            err!("month-day parsed from {original:?} is not valid")
732        })?;
733
734        // We have a valid year-month. But we don't return it because we just
735        // need to check validity.
736        Ok(Parsed { value: (), input })
737    }
738
739    // DateSpecYearMonth :::
740    //   DateYear -[opt] DateMonth
741    //
742    // NOTE: Jiff doesn't have a "year-month" type, but we still have a parsing
743    // function for it so that we can detect ambiguous time strings.
744    #[cfg_attr(feature = "perf-inline", inline(always))]
745    fn parse_year_month<'i>(
746        &self,
747        input: &'i [u8],
748    ) -> Result<Parsed<'i, ()>, Error> {
749        let original = escape::Bytes(input);
750
751        // Parse year component.
752        let Parsed { value: year, mut input } =
753            self.parse_year(input).with_context(|| {
754                err!("failed to parse year in date {original:?}")
755            })?;
756
757        // Skip over optional separator.
758        if input.starts_with(b"-") {
759            input = &input[1..];
760        }
761
762        // Parse month component.
763        let Parsed { value: month, input } =
764            self.parse_month(input).with_context(|| {
765                err!("failed to parse month in month-day {original:?}")
766            })?;
767
768        // Check that the year-month is valid. We just use a day of 1, since
769        // every month in every year must have a day 1.
770        let day = t::Day::N::<1>();
771        let _ = Date::new_ranged(year, month, day).with_context(|| {
772            err!("year-month parsed from {original:?} is not valid")
773        })?;
774
775        // We have a valid year-month. But we don't return it because we just
776        // need to check validity.
777        Ok(Parsed { value: (), input })
778    }
779
780    // DateYear :::
781    //   DecimalDigit DecimalDigit DecimalDigit DecimalDigit
782    //   TemporalSign DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit DecimalDigit
783    //
784    // NOTE: I don't really like the fact that in order to write a negative
785    // year, you need to use the six digit variant. Like, why not allow
786    // `-0001`? I'm not sure why, so for Chesterton's fence reasons, I'm
787    // sticking with the Temporal spec. But I may loosen this in the future. We
788    // should be careful not to introduce any possible ambiguities, though, I
789    // don't think there are any?
790    #[cfg_attr(feature = "perf-inline", inline(always))]
791    fn parse_year<'i>(
792        &self,
793        input: &'i [u8],
794    ) -> Result<Parsed<'i, t::Year>, Error> {
795        let Parsed { value: sign, input } = self.parse_year_sign(input);
796        if let Some(sign) = sign {
797            let (year, input) = parse::split(input, 6).ok_or_else(|| {
798                err!(
799                    "expected six digit year (because of a leading sign), \
800                     but found end of input",
801                )
802            })?;
803            let year = parse::i64(year).with_context(|| {
804                err!(
805                    "failed to parse {year:?} as year (a six digit integer)",
806                    year = escape::Bytes(year),
807                )
808            })?;
809            let year =
810                t::Year::try_new("year", year).context("year is not valid")?;
811            if year == C(0) && sign < C(0) {
812                return Err(err!(
813                    "year zero must be written without a sign or a \
814                     positive sign, but not a negative sign",
815                ));
816            }
817            Ok(Parsed { value: year * sign, input })
818        } else {
819            let (year, input) = parse::split(input, 4).ok_or_else(|| {
820                err!(
821                    "expected four digit year (or leading sign for \
822                     six digit year), but found end of input",
823                )
824            })?;
825            let year = parse::i64(year).with_context(|| {
826                err!(
827                    "failed to parse {year:?} as year (a four digit integer)",
828                    year = escape::Bytes(year),
829                )
830            })?;
831            let year =
832                t::Year::try_new("year", year).context("year is not valid")?;
833            Ok(Parsed { value: year, input })
834        }
835    }
836
837    // DateMonth :::
838    //   0 NonZeroDigit
839    //   10
840    //   11
841    //   12
842    #[cfg_attr(feature = "perf-inline", inline(always))]
843    fn parse_month<'i>(
844        &self,
845        input: &'i [u8],
846    ) -> Result<Parsed<'i, t::Month>, Error> {
847        let (month, input) = parse::split(input, 2).ok_or_else(|| {
848            err!("expected two digit month, but found end of input")
849        })?;
850        let month = parse::i64(month).with_context(|| {
851            err!(
852                "failed to parse {month:?} as month (a two digit integer)",
853                month = escape::Bytes(month),
854            )
855        })?;
856        let month =
857            t::Month::try_new("month", month).context("month is not valid")?;
858        Ok(Parsed { value: month, input })
859    }
860
861    // DateDay :::
862    //   0 NonZeroDigit
863    //   1 DecimalDigit
864    //   2 DecimalDigit
865    //   30
866    //   31
867    #[cfg_attr(feature = "perf-inline", inline(always))]
868    fn parse_day<'i>(
869        &self,
870        input: &'i [u8],
871    ) -> Result<Parsed<'i, t::Day>, Error> {
872        let (day, input) = parse::split(input, 2).ok_or_else(|| {
873            err!("expected two digit day, but found end of input")
874        })?;
875        let day = parse::i64(day).with_context(|| {
876            err!(
877                "failed to parse {day:?} as day (a two digit integer)",
878                day = escape::Bytes(day),
879            )
880        })?;
881        let day = t::Day::try_new("day", day).context("day is not valid")?;
882        Ok(Parsed { value: day, input })
883    }
884
885    // TimeHour :::
886    //   Hour
887    //
888    // Hour :::
889    //   0 DecimalDigit
890    //   1 DecimalDigit
891    //   20
892    //   21
893    //   22
894    //   23
895    #[cfg_attr(feature = "perf-inline", inline(always))]
896    fn parse_hour<'i>(
897        &self,
898        input: &'i [u8],
899    ) -> Result<Parsed<'i, t::Hour>, Error> {
900        let (hour, input) = parse::split(input, 2).ok_or_else(|| {
901            err!("expected two digit hour, but found end of input")
902        })?;
903        let hour = parse::i64(hour).with_context(|| {
904            err!(
905                "failed to parse {hour:?} as hour (a two digit integer)",
906                hour = escape::Bytes(hour),
907            )
908        })?;
909        let hour =
910            t::Hour::try_new("hour", hour).context("hour is not valid")?;
911        Ok(Parsed { value: hour, input })
912    }
913
914    // TimeMinute :::
915    //   MinuteSecond
916    //
917    // MinuteSecond :::
918    //   0 DecimalDigit
919    //   1 DecimalDigit
920    //   2 DecimalDigit
921    //   3 DecimalDigit
922    //   4 DecimalDigit
923    //   5 DecimalDigit
924    #[cfg_attr(feature = "perf-inline", inline(always))]
925    fn parse_minute<'i>(
926        &self,
927        input: &'i [u8],
928    ) -> Result<Parsed<'i, t::Minute>, Error> {
929        let (minute, input) = parse::split(input, 2).ok_or_else(|| {
930            err!("expected two digit minute, but found end of input")
931        })?;
932        let minute = parse::i64(minute).with_context(|| {
933            err!(
934                "failed to parse {minute:?} as minute (a two digit integer)",
935                minute = escape::Bytes(minute),
936            )
937        })?;
938        let minute = t::Minute::try_new("minute", minute)
939            .context("minute is not valid")?;
940        Ok(Parsed { value: minute, input })
941    }
942
943    // TimeSecond :::
944    //   MinuteSecond
945    //   60
946    //
947    // MinuteSecond :::
948    //   0 DecimalDigit
949    //   1 DecimalDigit
950    //   2 DecimalDigit
951    //   3 DecimalDigit
952    //   4 DecimalDigit
953    //   5 DecimalDigit
954    #[cfg_attr(feature = "perf-inline", inline(always))]
955    fn parse_second<'i>(
956        &self,
957        input: &'i [u8],
958    ) -> Result<Parsed<'i, t::Second>, Error> {
959        let (second, input) = parse::split(input, 2).ok_or_else(|| {
960            err!("expected two digit second, but found end of input",)
961        })?;
962        let mut second = parse::i64(second).with_context(|| {
963            err!(
964                "failed to parse {second:?} as second (a two digit integer)",
965                second = escape::Bytes(second),
966            )
967        })?;
968        // NOTE: I believe Temporal allows one to make this configurable. That
969        // is, to reject it. But for now, we just always clamp a leap second.
970        if second == 60 {
971            second = 59;
972        }
973        let second = t::Second::try_new("second", second)
974            .context("second is not valid")?;
975        Ok(Parsed { value: second, input })
976    }
977
978    #[cfg_attr(feature = "perf-inline", inline(always))]
979    fn parse_offset<'i>(
980        &self,
981        input: &'i [u8],
982    ) -> Result<Parsed<'i, Option<ParsedOffset>>, Error> {
983        const P: offset::Parser =
984            offset::Parser::new().zulu(true).subminute(true);
985        P.parse_optional(input)
986    }
987
988    #[cfg_attr(feature = "perf-inline", inline(always))]
989    fn parse_annotations<'i>(
990        &self,
991        input: &'i [u8],
992    ) -> Result<Parsed<'i, ParsedAnnotations<'i>>, Error> {
993        const P: rfc9557::Parser = rfc9557::Parser::new();
994        if input.is_empty() || input[0] != b'[' {
995            let value = ParsedAnnotations::none();
996            return Ok(Parsed { input, value });
997        }
998        P.parse(input)
999    }
1000
1001    /// Parses the separator that is expected to appear between
1002    /// date components.
1003    ///
1004    /// When in extended mode, a `-` is expected. When not in extended mode,
1005    /// no input is consumed and this routine never fails.
1006    #[cfg_attr(feature = "perf-inline", inline(always))]
1007    fn parse_date_separator<'i>(
1008        &self,
1009        mut input: &'i [u8],
1010        extended: bool,
1011    ) -> Result<Parsed<'i, ()>, Error> {
1012        if !extended {
1013            // If we see a '-' when not in extended mode, then we can report
1014            // a better error message than, e.g., "-3 isn't a valid day."
1015            if input.starts_with(b"-") {
1016                return Err(err!(
1017                    "expected no separator after month since none was \
1018                     found after the year, but found a '-' separator",
1019                ));
1020            }
1021            return Ok(Parsed { value: (), input });
1022        }
1023        if input.is_empty() {
1024            return Err(err!(
1025                "expected '-' separator, but found end of input"
1026            ));
1027        }
1028        if input[0] != b'-' {
1029            return Err(err!(
1030                "expected '-' separator, but found {found:?} instead",
1031                found = escape::Byte(input[0]),
1032            ));
1033        }
1034        input = &input[1..];
1035        Ok(Parsed { value: (), input })
1036    }
1037
1038    /// Parses the separator that is expected to appear between time
1039    /// components. When `true` is returned, we expect to parse the next
1040    /// component. When `false` is returned, then no separator was found and
1041    /// there is no expectation of finding another component.
1042    ///
1043    /// When in extended mode, true is returned if and only if a separator is
1044    /// found.
1045    ///
1046    /// When in basic mode (not extended), then a subsequent component is only
1047    /// expected when `input` begins with two ASCII digits.
1048    #[cfg_attr(feature = "perf-inline", inline(always))]
1049    fn parse_time_separator<'i>(
1050        &self,
1051        mut input: &'i [u8],
1052        extended: bool,
1053    ) -> Parsed<'i, bool> {
1054        if !extended {
1055            let expected =
1056                input.len() >= 2 && input[..2].iter().all(u8::is_ascii_digit);
1057            return Parsed { value: expected, input };
1058        }
1059        let is_separator = input.get(0).map_or(false, |&b| b == b':');
1060        if is_separator {
1061            input = &input[1..];
1062        }
1063        Parsed { value: is_separator, input }
1064    }
1065
1066    // TemporalSign :::
1067    //   ASCIISign
1068    //   <MINUS>
1069    //
1070    // ASCIISign ::: one of
1071    //   + -
1072    //
1073    // NOTE: We specifically only support ASCII signs. I think Temporal needs
1074    // to support `<MINUS>` because of other things in ECMA script that
1075    // require it?[1]
1076    //
1077    // [1]: https://github.com/tc39/proposal-temporal/issues/2843
1078    #[cfg_attr(feature = "perf-inline", inline(always))]
1079    fn parse_year_sign<'i>(
1080        &self,
1081        mut input: &'i [u8],
1082    ) -> Parsed<'i, Option<t::Sign>> {
1083        let Some(sign) = input.get(0).copied() else {
1084            return Parsed { value: None, input };
1085        };
1086        let sign = if sign == b'+' {
1087            t::Sign::N::<1>()
1088        } else if sign == b'-' {
1089            t::Sign::N::<-1>()
1090        } else {
1091            return Parsed { value: None, input };
1092        };
1093        input = &input[1..];
1094        Parsed { value: Some(sign), input }
1095    }
1096}
1097
1098/// A parser for Temporal spans.
1099///
1100/// Note that in Temporal, a "span" is called a "duration."
1101#[derive(Debug)]
1102pub(super) struct SpanParser {
1103    /// There are currently no configuration options for this parser.
1104    _priv: (),
1105}
1106
1107impl SpanParser {
1108    /// Create a new Temporal span parser with the default configuration.
1109    pub(super) const fn new() -> SpanParser {
1110        SpanParser { _priv: () }
1111    }
1112
1113    #[cfg_attr(feature = "perf-inline", inline(always))]
1114    pub(super) fn parse_temporal_duration<'i>(
1115        &self,
1116        input: &'i [u8],
1117    ) -> Result<Parsed<'i, Span>, Error> {
1118        self.parse_span(input).context(
1119            "failed to parse ISO 8601 \
1120             duration string into `Span`",
1121        )
1122    }
1123
1124    #[cfg_attr(feature = "perf-inline", inline(always))]
1125    pub(super) fn parse_signed_duration<'i>(
1126        &self,
1127        input: &'i [u8],
1128    ) -> Result<Parsed<'i, SignedDuration>, Error> {
1129        self.parse_duration(input).context(
1130            "failed to parse ISO 8601 \
1131             duration string into `SignedDuration`",
1132        )
1133    }
1134
1135    #[cfg_attr(feature = "perf-inline", inline(always))]
1136    fn parse_span<'i>(
1137        &self,
1138        input: &'i [u8],
1139    ) -> Result<Parsed<'i, Span>, Error> {
1140        let original = escape::Bytes(input);
1141        let Parsed { value: sign, input } = self.parse_sign(input);
1142        let Parsed { input, .. } = self.parse_duration_designator(input)?;
1143        let Parsed { value: (mut span, parsed_any_date), input } =
1144            self.parse_date_units(input, Span::new())?;
1145        let Parsed { value: has_time, mut input } =
1146            self.parse_time_designator(input);
1147        if has_time {
1148            let parsed = self.parse_time_units(input, span)?;
1149            input = parsed.input;
1150
1151            let (time_span, parsed_any_time) = parsed.value;
1152            if !parsed_any_time {
1153                return Err(err!(
1154                    "found a time designator (T or t) in an ISO 8601 \
1155                     duration string in {original:?}, but did not find \
1156                     any time units",
1157                ));
1158            }
1159            span = time_span;
1160        } else if !parsed_any_date {
1161            return Err(err!(
1162                "found the start of a ISO 8601 duration string \
1163                 in {original:?}, but did not find any units",
1164            ));
1165        }
1166        if sign < C(0) {
1167            span = span.negate();
1168        }
1169        Ok(Parsed { value: span, input })
1170    }
1171
1172    #[cfg_attr(feature = "perf-inline", inline(always))]
1173    fn parse_duration<'i>(
1174        &self,
1175        input: &'i [u8],
1176    ) -> Result<Parsed<'i, SignedDuration>, Error> {
1177        let Parsed { value: sign, input } = self.parse_sign(input);
1178        let Parsed { input, .. } = self.parse_duration_designator(input)?;
1179        let Parsed { value: has_time, input } =
1180            self.parse_time_designator(input);
1181        if !has_time {
1182            return Err(err!(
1183                "parsing ISO 8601 duration into SignedDuration requires \
1184                 that the duration contain a time component and no \
1185                 components of days or greater",
1186            ));
1187        }
1188        let Parsed { value: dur, input } =
1189            self.parse_time_units_duration(input, sign == C(-1))?;
1190        Ok(Parsed { value: dur, input })
1191    }
1192
1193    /// Parses consecutive date units from an ISO 8601 duration string into the
1194    /// span given.
1195    ///
1196    /// If 1 or more units were found, then `true` is also returned. Otherwise,
1197    /// `false` indicates that no units were parsed. (Which the caller may want
1198    /// to treat as an error.)
1199    #[cfg_attr(feature = "perf-inline", inline(always))]
1200    fn parse_date_units<'i>(
1201        &self,
1202        mut input: &'i [u8],
1203        mut span: Span,
1204    ) -> Result<Parsed<'i, (Span, bool)>, Error> {
1205        let mut parsed_any = false;
1206        let mut prev_unit: Option<Unit> = None;
1207        loop {
1208            let parsed = self.parse_unit_value(input)?;
1209            input = parsed.input;
1210            let Some(value) = parsed.value else { break };
1211
1212            let parsed = self.parse_unit_date_designator(input)?;
1213            input = parsed.input;
1214            let unit = parsed.value;
1215
1216            if let Some(prev_unit) = prev_unit {
1217                if prev_unit <= unit {
1218                    return Err(err!(
1219                        "found value {value:?} with unit {unit} \
1220                         after unit {prev_unit}, but units must be \
1221                         written from largest to smallest \
1222                         (and they can't be repeated)",
1223                        unit = unit.singular(),
1224                        prev_unit = prev_unit.singular(),
1225                    ));
1226                }
1227            }
1228            prev_unit = Some(unit);
1229            span = span.try_units_ranged(unit, value).with_context(|| {
1230                err!(
1231                    "failed to set value {value:?} as {unit} unit on span",
1232                    unit = Unit::from(unit).singular(),
1233                )
1234            })?;
1235            parsed_any = true;
1236        }
1237        Ok(Parsed { value: (span, parsed_any), input })
1238    }
1239
1240    /// Parses consecutive time units from an ISO 8601 duration string into the
1241    /// span given.
1242    ///
1243    /// If 1 or more units were found, then `true` is also returned. Otherwise,
1244    /// `false` indicates that no units were parsed. (Which the caller may want
1245    /// to treat as an error.)
1246    #[cfg_attr(feature = "perf-inline", inline(always))]
1247    fn parse_time_units<'i>(
1248        &self,
1249        mut input: &'i [u8],
1250        mut span: Span,
1251    ) -> Result<Parsed<'i, (Span, bool)>, Error> {
1252        let mut parsed_any = false;
1253        let mut prev_unit: Option<Unit> = None;
1254        loop {
1255            let parsed = self.parse_unit_value(input)?;
1256            input = parsed.input;
1257            let Some(value) = parsed.value else { break };
1258
1259            let parsed = parse_temporal_fraction(input)?;
1260            input = parsed.input;
1261            let fraction = parsed.value;
1262
1263            let parsed = self.parse_unit_time_designator(input)?;
1264            input = parsed.input;
1265            let unit = parsed.value;
1266
1267            if let Some(prev_unit) = prev_unit {
1268                if prev_unit <= unit {
1269                    return Err(err!(
1270                        "found value {value:?} with unit {unit} \
1271                         after unit {prev_unit}, but units must be \
1272                         written from largest to smallest \
1273                         (and they can't be repeated)",
1274                        unit = unit.singular(),
1275                        prev_unit = prev_unit.singular(),
1276                    ));
1277                }
1278            }
1279            prev_unit = Some(unit);
1280            parsed_any = true;
1281
1282            if let Some(fraction) = fraction {
1283                span = fractional_time_to_span(unit, value, fraction, span)?;
1284                // Once we see a fraction, we are done. We don't permit parsing
1285                // any more units. That is, a fraction can only occur on the
1286                // lowest unit of time.
1287                break;
1288            } else {
1289                let result =
1290                    span.try_units_ranged(unit, value).with_context(|| {
1291                        err!(
1292                            "failed to set value {value:?} \
1293                             as {unit} unit on span",
1294                            unit = Unit::from(unit).singular(),
1295                        )
1296                    });
1297                // This is annoying, but because we can write out a larger
1298                // number of hours/minutes/seconds than what we actually
1299                // support, we need to be prepared to parse an unbalanced span
1300                // if our time units are too big here. This entire dance is
1301                // because ISO 8601 requires fractional seconds to represent
1302                // milli-, micro- and nano-seconds. This means that spans
1303                // cannot retain their full fidelity when roundtripping through
1304                // ISO 8601. However, it is guaranteed that their total elapsed
1305                // time represented will never change.
1306                span = match result {
1307                    Ok(span) => span,
1308                    Err(_) => fractional_time_to_span(
1309                        unit,
1310                        value,
1311                        t::SubsecNanosecond::N::<0>(),
1312                        span,
1313                    )?,
1314                };
1315            }
1316        }
1317        Ok(Parsed { value: (span, parsed_any), input })
1318    }
1319
1320    /// Parses consecutive time units from an ISO 8601 duration string into
1321    /// a Jiff signed duration.
1322    ///
1323    /// If no time units are found, then this returns an error.
1324    #[cfg_attr(feature = "perf-inline", inline(always))]
1325    fn parse_time_units_duration<'i>(
1326        &self,
1327        mut input: &'i [u8],
1328        negative: bool,
1329    ) -> Result<Parsed<'i, SignedDuration>, Error> {
1330        let mut parsed_any = false;
1331        let mut prev_unit: Option<Unit> = None;
1332        let mut dur = SignedDuration::ZERO;
1333
1334        loop {
1335            let parsed = self.parse_unit_value(input)?;
1336            input = parsed.input;
1337            let Some(value) = parsed.value else { break };
1338
1339            let parsed = parse_temporal_fraction(input)?;
1340            input = parsed.input;
1341            let fraction = parsed.value;
1342
1343            let parsed = self.parse_unit_time_designator(input)?;
1344            input = parsed.input;
1345            let unit = parsed.value;
1346
1347            if let Some(prev_unit) = prev_unit {
1348                if prev_unit <= unit {
1349                    return Err(err!(
1350                        "found value {value:?} with unit {unit} \
1351                         after unit {prev_unit}, but units must be \
1352                         written from largest to smallest \
1353                         (and they can't be repeated)",
1354                        unit = unit.singular(),
1355                        prev_unit = prev_unit.singular(),
1356                    ));
1357                }
1358            }
1359            prev_unit = Some(unit);
1360            parsed_any = true;
1361
1362            // Convert our parsed unit into a number of seconds.
1363            let unit_secs = match unit {
1364                Unit::Second => value.get(),
1365                Unit::Minute => {
1366                    let mins = value.get();
1367                    mins.checked_mul(60).ok_or_else(|| {
1368                        err!(
1369                            "minute units {mins} overflowed i64 when \
1370                             converted to seconds"
1371                        )
1372                    })?
1373                }
1374                Unit::Hour => {
1375                    let hours = value.get();
1376                    hours.checked_mul(3_600).ok_or_else(|| {
1377                        err!(
1378                            "hour units {hours} overflowed i64 when \
1379                             converted to seconds"
1380                        )
1381                    })?
1382                }
1383                // Guaranteed not to be here since `parse_unit_time_designator`
1384                // always returns hours, minutes or seconds.
1385                _ => unreachable!(),
1386            };
1387            // Never panics since nanos==0.
1388            let unit_dur = SignedDuration::new(unit_secs, 0);
1389            // And now try to add it to our existing duration.
1390            let result = if negative {
1391                dur.checked_sub(unit_dur)
1392            } else {
1393                dur.checked_add(unit_dur)
1394            };
1395            dur = result.ok_or_else(|| {
1396                err!(
1397                    "adding value {value} from unit {unit} overflowed \
1398                     signed duration {dur:?}",
1399                    unit = unit.singular(),
1400                )
1401            })?;
1402
1403            if let Some(fraction) = fraction {
1404                let fraction_dur =
1405                    fractional_time_to_duration(unit, fraction)?;
1406                let result = if negative {
1407                    dur.checked_sub(fraction_dur)
1408                } else {
1409                    dur.checked_add(fraction_dur)
1410                };
1411                dur = result.ok_or_else(|| {
1412                    err!(
1413                        "adding fractional duration {fraction_dur:?} \
1414                         from unit {unit} to {dur:?} overflowed \
1415                         signed duration limits",
1416                        unit = unit.singular(),
1417                    )
1418                })?;
1419                // Once we see a fraction, we are done. We don't permit parsing
1420                // any more units. That is, a fraction can only occur on the
1421                // lowest unit of time.
1422                break;
1423            }
1424        }
1425        if !parsed_any {
1426            return Err(err!(
1427                "expected at least one unit of time (hours, minutes or \
1428                 seconds) in ISO 8601 duration when parsing into a \
1429                 `SignedDuration`",
1430            ));
1431        }
1432        Ok(Parsed { value: dur, input })
1433    }
1434
1435    #[cfg_attr(feature = "perf-inline", inline(always))]
1436    fn parse_unit_value<'i>(
1437        &self,
1438        mut input: &'i [u8],
1439    ) -> Result<Parsed<'i, Option<t::NoUnits>>, Error> {
1440        // Discovered via `i64::MAX.to_string().len()`.
1441        const MAX_I64_DIGITS: usize = 19;
1442
1443        let mkdigits = parse::slicer(input);
1444        while mkdigits(input).len() <= MAX_I64_DIGITS
1445            && input.first().map_or(false, u8::is_ascii_digit)
1446        {
1447            input = &input[1..];
1448        }
1449        let digits = mkdigits(input);
1450        if digits.is_empty() {
1451            return Ok(Parsed { value: None, input });
1452        }
1453        let value = parse::i64(digits).with_context(|| {
1454            err!(
1455                "failed to parse {digits:?} as 64-bit signed integer",
1456                digits = escape::Bytes(digits),
1457            )
1458        })?;
1459        // OK because t::NoUnits permits all possible i64 values.
1460        let value = t::NoUnits::new(value).unwrap();
1461        Ok(Parsed { value: Some(value), input })
1462    }
1463
1464    #[cfg_attr(feature = "perf-inline", inline(always))]
1465    fn parse_unit_date_designator<'i>(
1466        &self,
1467        input: &'i [u8],
1468    ) -> Result<Parsed<'i, Unit>, Error> {
1469        if input.is_empty() {
1470            return Err(err!(
1471                "expected to find date unit designator suffix \
1472                 (Y, M, W or D), but found end of input",
1473            ));
1474        }
1475        let unit = match input[0] {
1476            b'Y' | b'y' => Unit::Year,
1477            b'M' | b'm' => Unit::Month,
1478            b'W' | b'w' => Unit::Week,
1479            b'D' | b'd' => Unit::Day,
1480            unknown => {
1481                return Err(err!(
1482                    "expected to find date unit designator suffix \
1483                     (Y, M, W or D), but found {found:?} instead",
1484                    found = escape::Byte(unknown),
1485                ));
1486            }
1487        };
1488        Ok(Parsed { value: unit, input: &input[1..] })
1489    }
1490
1491    #[cfg_attr(feature = "perf-inline", inline(always))]
1492    fn parse_unit_time_designator<'i>(
1493        &self,
1494        input: &'i [u8],
1495    ) -> Result<Parsed<'i, Unit>, Error> {
1496        if input.is_empty() {
1497            return Err(err!(
1498                "expected to find time unit designator suffix \
1499                 (H, M or S), but found end of input",
1500            ));
1501        }
1502        let unit = match input[0] {
1503            b'H' | b'h' => Unit::Hour,
1504            b'M' | b'm' => Unit::Minute,
1505            b'S' | b's' => Unit::Second,
1506            unknown => {
1507                return Err(err!(
1508                    "expected to find time unit designator suffix \
1509                     (H, M or S), but found {found:?} instead",
1510                    found = escape::Byte(unknown),
1511                ));
1512            }
1513        };
1514        Ok(Parsed { value: unit, input: &input[1..] })
1515    }
1516
1517    // DurationDesignator ::: one of
1518    //   P p
1519    #[cfg_attr(feature = "perf-inline", inline(always))]
1520    fn parse_duration_designator<'i>(
1521        &self,
1522        input: &'i [u8],
1523    ) -> Result<Parsed<'i, ()>, Error> {
1524        if input.is_empty() {
1525            return Err(err!(
1526                "expected to find duration beginning with 'P' or 'p', \
1527                 but found end of input",
1528            ));
1529        }
1530        if !matches!(input[0], b'P' | b'p') {
1531            return Err(err!(
1532                "expected 'P' or 'p' prefix to begin duration, \
1533                 but found {found:?} instead",
1534                found = escape::Byte(input[0]),
1535            ));
1536        }
1537        Ok(Parsed { value: (), input: &input[1..] })
1538    }
1539
1540    // TimeDesignator ::: one of
1541    //   T t
1542    #[cfg_attr(feature = "perf-inline", inline(always))]
1543    fn parse_time_designator<'i>(&self, input: &'i [u8]) -> Parsed<'i, bool> {
1544        if input.is_empty() || !matches!(input[0], b'T' | b't') {
1545            return Parsed { value: false, input };
1546        }
1547        Parsed { value: true, input: &input[1..] }
1548    }
1549
1550    // TemporalSign :::
1551    //   ASCIISign
1552    //   <MINUS>
1553    //
1554    // NOTE: Like with other things with signs, we don't support the Unicode
1555    // <MINUS> sign. Just ASCII.
1556    #[cfg_attr(feature = "perf-inline", inline(always))]
1557    fn parse_sign<'i>(&self, input: &'i [u8]) -> Parsed<'i, t::Sign> {
1558        let Some(sign) = input.get(0).copied() else {
1559            return Parsed { value: t::Sign::N::<1>(), input };
1560        };
1561        let sign = if sign == b'+' {
1562            t::Sign::N::<1>()
1563        } else if sign == b'-' {
1564            t::Sign::N::<-1>()
1565        } else {
1566            return Parsed { value: t::Sign::N::<1>(), input };
1567        };
1568        Parsed { value: sign, input: &input[1..] }
1569    }
1570}
1571
1572#[cfg(feature = "alloc")]
1573#[cfg(test)]
1574mod tests {
1575    use super::*;
1576
1577    #[test]
1578    fn ok_signed_duration() {
1579        let p =
1580            |input| SpanParser::new().parse_signed_duration(input).unwrap();
1581
1582        insta::assert_debug_snapshot!(p(b"PT0s"), @r###"
1583        Parsed {
1584            value: 0s,
1585            input: "",
1586        }
1587        "###);
1588        insta::assert_debug_snapshot!(p(b"PT0.000000001s"), @r###"
1589        Parsed {
1590            value: 1ns,
1591            input: "",
1592        }
1593        "###);
1594        insta::assert_debug_snapshot!(p(b"PT1s"), @r###"
1595        Parsed {
1596            value: 1s,
1597            input: "",
1598        }
1599        "###);
1600        insta::assert_debug_snapshot!(p(b"PT59s"), @r###"
1601        Parsed {
1602            value: 59s,
1603            input: "",
1604        }
1605        "###);
1606        insta::assert_debug_snapshot!(p(b"PT60s"), @r#"
1607        Parsed {
1608            value: 60s,
1609            input: "",
1610        }
1611        "#);
1612        insta::assert_debug_snapshot!(p(b"PT1m"), @r#"
1613        Parsed {
1614            value: 60s,
1615            input: "",
1616        }
1617        "#);
1618        insta::assert_debug_snapshot!(p(b"PT1m0.000000001s"), @r#"
1619        Parsed {
1620            value: 60s 1ns,
1621            input: "",
1622        }
1623        "#);
1624        insta::assert_debug_snapshot!(p(b"PT1.25m"), @r#"
1625        Parsed {
1626            value: 75s,
1627            input: "",
1628        }
1629        "#);
1630        insta::assert_debug_snapshot!(p(b"PT1h"), @r#"
1631        Parsed {
1632            value: 3600s,
1633            input: "",
1634        }
1635        "#);
1636        insta::assert_debug_snapshot!(p(b"PT1h0.000000001s"), @r#"
1637        Parsed {
1638            value: 3600s 1ns,
1639            input: "",
1640        }
1641        "#);
1642        insta::assert_debug_snapshot!(p(b"PT1.25h"), @r#"
1643        Parsed {
1644            value: 4500s,
1645            input: "",
1646        }
1647        "#);
1648
1649        insta::assert_debug_snapshot!(p(b"-PT2562047788015215h30m8.999999999s"), @r#"
1650        Parsed {
1651            value: -9223372036854775808s 999999999ns,
1652            input: "",
1653        }
1654        "#);
1655        insta::assert_debug_snapshot!(p(b"PT2562047788015215h30m7.999999999s"), @r#"
1656        Parsed {
1657            value: 9223372036854775807s 999999999ns,
1658            input: "",
1659        }
1660        "#);
1661    }
1662
1663    #[test]
1664    fn err_signed_duration() {
1665        let p = |input| {
1666            SpanParser::new().parse_signed_duration(input).unwrap_err()
1667        };
1668
1669        insta::assert_snapshot!(
1670            p(b"P0d"),
1671            @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater",
1672        );
1673        insta::assert_snapshot!(
1674            p(b"PT0d"),
1675            @r###"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find time unit designator suffix (H, M or S), but found "d" instead"###,
1676        );
1677        insta::assert_snapshot!(
1678            p(b"P0dT1s"),
1679            @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater",
1680        );
1681
1682        insta::assert_snapshot!(
1683            p(b""),
1684            @"failed to parse ISO 8601 duration string into `SignedDuration`: expected to find duration beginning with 'P' or 'p', but found end of input",
1685        );
1686        insta::assert_snapshot!(
1687            p(b"P"),
1688            @"failed to parse ISO 8601 duration string into `SignedDuration`: parsing ISO 8601 duration into SignedDuration requires that the duration contain a time component and no components of days or greater",
1689        );
1690        insta::assert_snapshot!(
1691            p(b"PT"),
1692            @"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`",
1693        );
1694        insta::assert_snapshot!(
1695            p(b"PTs"),
1696            @"failed to parse ISO 8601 duration string into `SignedDuration`: expected at least one unit of time (hours, minutes or seconds) in ISO 8601 duration when parsing into a `SignedDuration`",
1697        );
1698
1699        insta::assert_snapshot!(
1700            p(b"PT1s1m"),
1701            @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit minute after unit second, but units must be written from largest to smallest (and they can't be repeated)",
1702        );
1703        insta::assert_snapshot!(
1704            p(b"PT1s1h"),
1705            @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit second, but units must be written from largest to smallest (and they can't be repeated)",
1706        );
1707        insta::assert_snapshot!(
1708            p(b"PT1m1h"),
1709            @"failed to parse ISO 8601 duration string into `SignedDuration`: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)",
1710        );
1711
1712        insta::assert_snapshot!(
1713            p(b"-PT9223372036854775809s"),
1714            @r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775809" as 64-bit signed integer: number '9223372036854775809' too big to parse into 64-bit integer"###,
1715        );
1716        insta::assert_snapshot!(
1717            p(b"PT9223372036854775808s"),
1718            @r###"failed to parse ISO 8601 duration string into `SignedDuration`: failed to parse "9223372036854775808" as 64-bit signed integer: number '9223372036854775808' too big to parse into 64-bit integer"###,
1719        );
1720
1721        insta::assert_snapshot!(
1722            p(b"PT1m9223372036854775807s"),
1723            @"failed to parse ISO 8601 duration string into `SignedDuration`: adding value 9223372036854775807 from unit second overflowed signed duration 1m",
1724        );
1725        insta::assert_snapshot!(
1726            p(b"PT2562047788015215.6h"),
1727            @"failed to parse ISO 8601 duration string into `SignedDuration`: adding fractional duration 36m from unit hour to 2562047788015215h overflowed signed duration limits",
1728        );
1729    }
1730
1731    #[test]
1732    fn ok_temporal_duration_basic() {
1733        let p =
1734            |input| SpanParser::new().parse_temporal_duration(input).unwrap();
1735
1736        insta::assert_debug_snapshot!(p(b"P5d"), @r###"
1737        Parsed {
1738            value: 5d,
1739            input: "",
1740        }
1741        "###);
1742        insta::assert_debug_snapshot!(p(b"-P5d"), @r###"
1743        Parsed {
1744            value: 5d ago,
1745            input: "",
1746        }
1747        "###);
1748        insta::assert_debug_snapshot!(p(b"+P5d"), @r###"
1749        Parsed {
1750            value: 5d,
1751            input: "",
1752        }
1753        "###);
1754        insta::assert_debug_snapshot!(p(b"P5DT1s"), @r###"
1755        Parsed {
1756            value: 5d 1s,
1757            input: "",
1758        }
1759        "###);
1760        insta::assert_debug_snapshot!(p(b"PT1S"), @r###"
1761        Parsed {
1762            value: 1s,
1763            input: "",
1764        }
1765        "###);
1766        insta::assert_debug_snapshot!(p(b"PT0S"), @r###"
1767        Parsed {
1768            value: 0s,
1769            input: "",
1770        }
1771        "###);
1772        insta::assert_debug_snapshot!(p(b"P0Y"), @r###"
1773        Parsed {
1774            value: 0s,
1775            input: "",
1776        }
1777        "###);
1778        insta::assert_debug_snapshot!(p(b"P1Y1M1W1DT1H1M1S"), @r###"
1779        Parsed {
1780            value: 1y 1mo 1w 1d 1h 1m 1s,
1781            input: "",
1782        }
1783        "###);
1784        insta::assert_debug_snapshot!(p(b"P1y1m1w1dT1h1m1s"), @r###"
1785        Parsed {
1786            value: 1y 1mo 1w 1d 1h 1m 1s,
1787            input: "",
1788        }
1789        "###);
1790    }
1791
1792    #[test]
1793    fn ok_temporal_duration_fractional() {
1794        let p =
1795            |input| SpanParser::new().parse_temporal_duration(input).unwrap();
1796
1797        insta::assert_debug_snapshot!(p(b"PT0.5h"), @r###"
1798        Parsed {
1799            value: 30m,
1800            input: "",
1801        }
1802        "###);
1803        insta::assert_debug_snapshot!(p(b"PT0.123456789h"), @r###"
1804        Parsed {
1805            value: 7m 24s 444ms 440µs 400ns,
1806            input: "",
1807        }
1808        "###);
1809        insta::assert_debug_snapshot!(p(b"PT1.123456789h"), @r###"
1810        Parsed {
1811            value: 1h 7m 24s 444ms 440µs 400ns,
1812            input: "",
1813        }
1814        "###);
1815
1816        insta::assert_debug_snapshot!(p(b"PT0.5m"), @r###"
1817        Parsed {
1818            value: 30s,
1819            input: "",
1820        }
1821        "###);
1822        insta::assert_debug_snapshot!(p(b"PT0.123456789m"), @r###"
1823        Parsed {
1824            value: 7s 407ms 407µs 340ns,
1825            input: "",
1826        }
1827        "###);
1828        insta::assert_debug_snapshot!(p(b"PT1.123456789m"), @r###"
1829        Parsed {
1830            value: 1m 7s 407ms 407µs 340ns,
1831            input: "",
1832        }
1833        "###);
1834
1835        insta::assert_debug_snapshot!(p(b"PT0.5s"), @r###"
1836        Parsed {
1837            value: 500ms,
1838            input: "",
1839        }
1840        "###);
1841        insta::assert_debug_snapshot!(p(b"PT0.123456789s"), @r###"
1842        Parsed {
1843            value: 123ms 456µs 789ns,
1844            input: "",
1845        }
1846        "###);
1847        insta::assert_debug_snapshot!(p(b"PT1.123456789s"), @r###"
1848        Parsed {
1849            value: 1s 123ms 456µs 789ns,
1850            input: "",
1851        }
1852        "###);
1853
1854        // The tests below all have a whole second value that exceeds the
1855        // maximum allowed seconds in a span. But they should still parse
1856        // correctly by spilling over into milliseconds, microseconds and
1857        // nanoseconds.
1858        insta::assert_debug_snapshot!(p(b"PT1902545624836.854775807s"), @r###"
1859        Parsed {
1860            value: 631107417600s 631107417600000ms 631107417600000000µs 9223372036854775807ns,
1861            input: "",
1862        }
1863        "###);
1864        insta::assert_debug_snapshot!(p(b"PT175307616h10518456960m640330789636.854775807s"), @r###"
1865        Parsed {
1866            value: 175307616h 10518456960m 631107417600s 9223372036854ms 775µs 807ns,
1867            input: "",
1868        }
1869        "###);
1870        insta::assert_debug_snapshot!(p(b"-PT1902545624836.854775807s"), @r###"
1871        Parsed {
1872            value: 631107417600s 631107417600000ms 631107417600000000µs 9223372036854775807ns ago,
1873            input: "",
1874        }
1875        "###);
1876        insta::assert_debug_snapshot!(p(b"-PT175307616h10518456960m640330789636.854775807s"), @r###"
1877        Parsed {
1878            value: 175307616h 10518456960m 631107417600s 9223372036854ms 775µs 807ns ago,
1879            input: "",
1880        }
1881        "###);
1882    }
1883
1884    #[test]
1885    fn ok_temporal_duration_unbalanced() {
1886        let p =
1887            |input| SpanParser::new().parse_temporal_duration(input).unwrap();
1888
1889        insta::assert_debug_snapshot!(
1890            p(b"PT175307616h10518456960m1774446656760s"), @r###"
1891        Parsed {
1892            value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231821560000000µs,
1893            input: "",
1894        }
1895        "###);
1896        insta::assert_debug_snapshot!(
1897            p(b"Pt843517082H"), @r###"
1898        Parsed {
1899            value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231824800000000µs,
1900            input: "",
1901        }
1902        "###);
1903        insta::assert_debug_snapshot!(
1904            p(b"Pt843517081H"), @r###"
1905        Parsed {
1906            value: 175307616h 10518456960m 631107417600s 631107417600000ms 512231821200000000µs,
1907            input: "",
1908        }
1909        "###);
1910    }
1911
1912    #[test]
1913    fn ok_temporal_datetime_basic() {
1914        let p = |input| {
1915            DateTimeParser::new().parse_temporal_datetime(input).unwrap()
1916        };
1917
1918        insta::assert_debug_snapshot!(p(b"2024-06-01"), @r###"
1919        Parsed {
1920            value: ParsedDateTime {
1921                input: "2024-06-01",
1922                date: ParsedDate {
1923                    input: "2024-06-01",
1924                    date: 2024-06-01,
1925                },
1926                time: None,
1927                offset: None,
1928                annotations: ParsedAnnotations {
1929                    input: "",
1930                    time_zone: None,
1931                },
1932            },
1933            input: "",
1934        }
1935        "###);
1936        insta::assert_debug_snapshot!(p(b"2024-06-01[America/New_York]"), @r###"
1937        Parsed {
1938            value: ParsedDateTime {
1939                input: "2024-06-01[America/New_York]",
1940                date: ParsedDate {
1941                    input: "2024-06-01",
1942                    date: 2024-06-01,
1943                },
1944                time: None,
1945                offset: None,
1946                annotations: ParsedAnnotations {
1947                    input: "[America/New_York]",
1948                    time_zone: Some(
1949                        Named {
1950                            critical: false,
1951                            name: "America/New_York",
1952                        },
1953                    ),
1954                },
1955            },
1956            input: "",
1957        }
1958        "###);
1959        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03"), @r###"
1960        Parsed {
1961            value: ParsedDateTime {
1962                input: "2024-06-01T01:02:03",
1963                date: ParsedDate {
1964                    input: "2024-06-01",
1965                    date: 2024-06-01,
1966                },
1967                time: Some(
1968                    ParsedTime {
1969                        input: "01:02:03",
1970                        time: 01:02:03,
1971                        extended: true,
1972                    },
1973                ),
1974                offset: None,
1975                annotations: ParsedAnnotations {
1976                    input: "",
1977                    time_zone: None,
1978                },
1979            },
1980            input: "",
1981        }
1982        "###);
1983        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05"), @r###"
1984        Parsed {
1985            value: ParsedDateTime {
1986                input: "2024-06-01T01:02:03-05",
1987                date: ParsedDate {
1988                    input: "2024-06-01",
1989                    date: 2024-06-01,
1990                },
1991                time: Some(
1992                    ParsedTime {
1993                        input: "01:02:03",
1994                        time: 01:02:03,
1995                        extended: true,
1996                    },
1997                ),
1998                offset: Some(
1999                    ParsedOffset {
2000                        kind: Numeric(
2001                            -05,
2002                        ),
2003                    },
2004                ),
2005                annotations: ParsedAnnotations {
2006                    input: "",
2007                    time_zone: None,
2008                },
2009            },
2010            input: "",
2011        }
2012        "###);
2013        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-05[America/New_York]"), @r###"
2014        Parsed {
2015            value: ParsedDateTime {
2016                input: "2024-06-01T01:02:03-05[America/New_York]",
2017                date: ParsedDate {
2018                    input: "2024-06-01",
2019                    date: 2024-06-01,
2020                },
2021                time: Some(
2022                    ParsedTime {
2023                        input: "01:02:03",
2024                        time: 01:02:03,
2025                        extended: true,
2026                    },
2027                ),
2028                offset: Some(
2029                    ParsedOffset {
2030                        kind: Numeric(
2031                            -05,
2032                        ),
2033                    },
2034                ),
2035                annotations: ParsedAnnotations {
2036                    input: "[America/New_York]",
2037                    time_zone: Some(
2038                        Named {
2039                            critical: false,
2040                            name: "America/New_York",
2041                        },
2042                    ),
2043                },
2044            },
2045            input: "",
2046        }
2047        "###);
2048        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03Z[America/New_York]"), @r###"
2049        Parsed {
2050            value: ParsedDateTime {
2051                input: "2024-06-01T01:02:03Z[America/New_York]",
2052                date: ParsedDate {
2053                    input: "2024-06-01",
2054                    date: 2024-06-01,
2055                },
2056                time: Some(
2057                    ParsedTime {
2058                        input: "01:02:03",
2059                        time: 01:02:03,
2060                        extended: true,
2061                    },
2062                ),
2063                offset: Some(
2064                    ParsedOffset {
2065                        kind: Zulu,
2066                    },
2067                ),
2068                annotations: ParsedAnnotations {
2069                    input: "[America/New_York]",
2070                    time_zone: Some(
2071                        Named {
2072                            critical: false,
2073                            name: "America/New_York",
2074                        },
2075                    ),
2076                },
2077            },
2078            input: "",
2079        }
2080        "###);
2081        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03-01[America/New_York]"), @r###"
2082        Parsed {
2083            value: ParsedDateTime {
2084                input: "2024-06-01T01:02:03-01[America/New_York]",
2085                date: ParsedDate {
2086                    input: "2024-06-01",
2087                    date: 2024-06-01,
2088                },
2089                time: Some(
2090                    ParsedTime {
2091                        input: "01:02:03",
2092                        time: 01:02:03,
2093                        extended: true,
2094                    },
2095                ),
2096                offset: Some(
2097                    ParsedOffset {
2098                        kind: Numeric(
2099                            -01,
2100                        ),
2101                    },
2102                ),
2103                annotations: ParsedAnnotations {
2104                    input: "[America/New_York]",
2105                    time_zone: Some(
2106                        Named {
2107                            critical: false,
2108                            name: "America/New_York",
2109                        },
2110                    ),
2111                },
2112            },
2113            input: "",
2114        }
2115        "###);
2116    }
2117
2118    #[test]
2119    fn ok_temporal_datetime_incomplete() {
2120        let p = |input| {
2121            DateTimeParser::new().parse_temporal_datetime(input).unwrap()
2122        };
2123
2124        insta::assert_debug_snapshot!(p(b"2024-06-01T01"), @r###"
2125        Parsed {
2126            value: ParsedDateTime {
2127                input: "2024-06-01T01",
2128                date: ParsedDate {
2129                    input: "2024-06-01",
2130                    date: 2024-06-01,
2131                },
2132                time: Some(
2133                    ParsedTime {
2134                        input: "01",
2135                        time: 01:00:00,
2136                        extended: false,
2137                    },
2138                ),
2139                offset: None,
2140                annotations: ParsedAnnotations {
2141                    input: "",
2142                    time_zone: None,
2143                },
2144            },
2145            input: "",
2146        }
2147        "###);
2148        insta::assert_debug_snapshot!(p(b"2024-06-01T0102"), @r###"
2149        Parsed {
2150            value: ParsedDateTime {
2151                input: "2024-06-01T0102",
2152                date: ParsedDate {
2153                    input: "2024-06-01",
2154                    date: 2024-06-01,
2155                },
2156                time: Some(
2157                    ParsedTime {
2158                        input: "0102",
2159                        time: 01:02:00,
2160                        extended: false,
2161                    },
2162                ),
2163                offset: None,
2164                annotations: ParsedAnnotations {
2165                    input: "",
2166                    time_zone: None,
2167                },
2168            },
2169            input: "",
2170        }
2171        "###);
2172        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02"), @r###"
2173        Parsed {
2174            value: ParsedDateTime {
2175                input: "2024-06-01T01:02",
2176                date: ParsedDate {
2177                    input: "2024-06-01",
2178                    date: 2024-06-01,
2179                },
2180                time: Some(
2181                    ParsedTime {
2182                        input: "01:02",
2183                        time: 01:02:00,
2184                        extended: true,
2185                    },
2186                ),
2187                offset: None,
2188                annotations: ParsedAnnotations {
2189                    input: "",
2190                    time_zone: None,
2191                },
2192            },
2193            input: "",
2194        }
2195        "###);
2196    }
2197
2198    #[test]
2199    fn ok_temporal_datetime_separator() {
2200        let p = |input| {
2201            DateTimeParser::new().parse_temporal_datetime(input).unwrap()
2202        };
2203
2204        insta::assert_debug_snapshot!(p(b"2024-06-01t01:02:03"), @r###"
2205        Parsed {
2206            value: ParsedDateTime {
2207                input: "2024-06-01t01:02:03",
2208                date: ParsedDate {
2209                    input: "2024-06-01",
2210                    date: 2024-06-01,
2211                },
2212                time: Some(
2213                    ParsedTime {
2214                        input: "01:02:03",
2215                        time: 01:02:03,
2216                        extended: true,
2217                    },
2218                ),
2219                offset: None,
2220                annotations: ParsedAnnotations {
2221                    input: "",
2222                    time_zone: None,
2223                },
2224            },
2225            input: "",
2226        }
2227        "###);
2228        insta::assert_debug_snapshot!(p(b"2024-06-01 01:02:03"), @r###"
2229        Parsed {
2230            value: ParsedDateTime {
2231                input: "2024-06-01 01:02:03",
2232                date: ParsedDate {
2233                    input: "2024-06-01",
2234                    date: 2024-06-01,
2235                },
2236                time: Some(
2237                    ParsedTime {
2238                        input: "01:02:03",
2239                        time: 01:02:03,
2240                        extended: true,
2241                    },
2242                ),
2243                offset: None,
2244                annotations: ParsedAnnotations {
2245                    input: "",
2246                    time_zone: None,
2247                },
2248            },
2249            input: "",
2250        }
2251        "###);
2252    }
2253
2254    #[test]
2255    fn ok_temporal_time_basic() {
2256        let p =
2257            |input| DateTimeParser::new().parse_temporal_time(input).unwrap();
2258
2259        insta::assert_debug_snapshot!(p(b"01:02:03"), @r###"
2260        Parsed {
2261            value: ParsedTime {
2262                input: "01:02:03",
2263                time: 01:02:03,
2264                extended: true,
2265            },
2266            input: "",
2267        }
2268        "###);
2269        insta::assert_debug_snapshot!(p(b"130113"), @r###"
2270        Parsed {
2271            value: ParsedTime {
2272                input: "130113",
2273                time: 13:01:13,
2274                extended: false,
2275            },
2276            input: "",
2277        }
2278        "###);
2279        insta::assert_debug_snapshot!(p(b"T01:02:03"), @r###"
2280        Parsed {
2281            value: ParsedTime {
2282                input: "01:02:03",
2283                time: 01:02:03,
2284                extended: true,
2285            },
2286            input: "",
2287        }
2288        "###);
2289        insta::assert_debug_snapshot!(p(b"T010203"), @r###"
2290        Parsed {
2291            value: ParsedTime {
2292                input: "010203",
2293                time: 01:02:03,
2294                extended: false,
2295            },
2296            input: "",
2297        }
2298        "###);
2299    }
2300
2301    #[test]
2302    fn ok_temporal_time_from_full_datetime() {
2303        let p =
2304            |input| DateTimeParser::new().parse_temporal_time(input).unwrap();
2305
2306        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03"), @r###"
2307        Parsed {
2308            value: ParsedTime {
2309                input: "01:02:03",
2310                time: 01:02:03,
2311                extended: true,
2312            },
2313            input: "",
2314        }
2315        "###);
2316        insta::assert_debug_snapshot!(p(b"2024-06-01T01:02:03.123"), @r###"
2317        Parsed {
2318            value: ParsedTime {
2319                input: "01:02:03.123",
2320                time: 01:02:03.123,
2321                extended: true,
2322            },
2323            input: "",
2324        }
2325        "###);
2326        insta::assert_debug_snapshot!(p(b"2024-06-01T01"), @r###"
2327        Parsed {
2328            value: ParsedTime {
2329                input: "01",
2330                time: 01:00:00,
2331                extended: false,
2332            },
2333            input: "",
2334        }
2335        "###);
2336        insta::assert_debug_snapshot!(p(b"2024-06-01T0102"), @r###"
2337        Parsed {
2338            value: ParsedTime {
2339                input: "0102",
2340                time: 01:02:00,
2341                extended: false,
2342            },
2343            input: "",
2344        }
2345        "###);
2346        insta::assert_debug_snapshot!(p(b"2024-06-01T010203"), @r###"
2347        Parsed {
2348            value: ParsedTime {
2349                input: "010203",
2350                time: 01:02:03,
2351                extended: false,
2352            },
2353            input: "",
2354        }
2355        "###);
2356        insta::assert_debug_snapshot!(p(b"2024-06-01T010203-05"), @r###"
2357        Parsed {
2358            value: ParsedTime {
2359                input: "010203",
2360                time: 01:02:03,
2361                extended: false,
2362            },
2363            input: "",
2364        }
2365        "###);
2366        insta::assert_debug_snapshot!(
2367            p(b"2024-06-01T010203-05[America/New_York]"), @r###"
2368        Parsed {
2369            value: ParsedTime {
2370                input: "010203",
2371                time: 01:02:03,
2372                extended: false,
2373            },
2374            input: "",
2375        }
2376        "###);
2377        insta::assert_debug_snapshot!(
2378            p(b"2024-06-01T010203[America/New_York]"), @r###"
2379        Parsed {
2380            value: ParsedTime {
2381                input: "010203",
2382                time: 01:02:03,
2383                extended: false,
2384            },
2385            input: "",
2386        }
2387        "###);
2388    }
2389
2390    #[test]
2391    fn err_temporal_time_ambiguous() {
2392        let p = |input| {
2393            DateTimeParser::new().parse_temporal_time(input).unwrap_err()
2394        };
2395
2396        insta::assert_snapshot!(
2397            p(b"010203"),
2398            @r###"parsed time from "010203" is ambiguous with a month-day date"###,
2399        );
2400        insta::assert_snapshot!(
2401            p(b"130112"),
2402            @r###"parsed time from "130112" is ambiguous with a year-month date"###,
2403        );
2404    }
2405
2406    #[test]
2407    fn err_temporal_time_missing_time() {
2408        let p = |input| {
2409            DateTimeParser::new().parse_temporal_time(input).unwrap_err()
2410        };
2411
2412        insta::assert_snapshot!(
2413            p(b"2024-06-01[America/New_York]"),
2414            @r###"successfully parsed date from "2024-06-01[America/New_York]", but no time component was found"###,
2415        );
2416        // 2099 is not a valid time, but 2099-12-01 is a valid date, so this
2417        // carves a path where a full datetime parse is OK, but a basic
2418        // time-only parse is not.
2419        insta::assert_snapshot!(
2420            p(b"2099-12-01[America/New_York]"),
2421            @r###"successfully parsed date from "2099-12-01[America/New_York]", but no time component was found"###,
2422        );
2423        // Like above, but this time we use an invalid date. As a result, we
2424        // get an error reported not on the invalid date, but on how it is an
2425        // invalid time. (Because we're asking for a time here.)
2426        insta::assert_snapshot!(
2427            p(b"2099-13-01[America/New_York]"),
2428            @r###"failed to parse minute in time "2099-13-01[America/New_York]": minute is not valid: parameter 'minute' with value 99 is not in the required range of 0..=59"###,
2429        );
2430    }
2431
2432    #[test]
2433    fn err_temporal_time_zulu() {
2434        let p = |input| {
2435            DateTimeParser::new().parse_temporal_time(input).unwrap_err()
2436        };
2437
2438        insta::assert_snapshot!(
2439            p(b"T00:00:00Z"),
2440            @"cannot parse civil time from string with a Zulu offset, parse as a `Timestamp` and convert to a civil time instead",
2441        );
2442        insta::assert_snapshot!(
2443            p(b"00:00:00Z"),
2444            @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead",
2445        );
2446        insta::assert_snapshot!(
2447            p(b"000000Z"),
2448            @"cannot parse plain time from string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead",
2449        );
2450        insta::assert_snapshot!(
2451            p(b"2099-12-01T00:00:00Z"),
2452            @"cannot parse plain time from full datetime string with a Zulu offset, parse as a `Timestamp` and convert to a plain time instead",
2453        );
2454    }
2455
2456    #[test]
2457    fn ok_date_basic() {
2458        let p = |input| DateTimeParser::new().parse_date_spec(input).unwrap();
2459
2460        insta::assert_debug_snapshot!(p(b"2010-03-14"), @r###"
2461        Parsed {
2462            value: ParsedDate {
2463                input: "2010-03-14",
2464                date: 2010-03-14,
2465            },
2466            input: "",
2467        }
2468        "###);
2469        insta::assert_debug_snapshot!(p(b"20100314"), @r###"
2470        Parsed {
2471            value: ParsedDate {
2472                input: "20100314",
2473                date: 2010-03-14,
2474            },
2475            input: "",
2476        }
2477        "###);
2478        insta::assert_debug_snapshot!(p(b"2010-03-14T01:02:03"), @r###"
2479        Parsed {
2480            value: ParsedDate {
2481                input: "2010-03-14",
2482                date: 2010-03-14,
2483            },
2484            input: "T01:02:03",
2485        }
2486        "###);
2487        insta::assert_debug_snapshot!(p(b"-009999-03-14"), @r###"
2488        Parsed {
2489            value: ParsedDate {
2490                input: "-009999-03-14",
2491                date: -009999-03-14,
2492            },
2493            input: "",
2494        }
2495        "###);
2496        insta::assert_debug_snapshot!(p(b"+009999-03-14"), @r###"
2497        Parsed {
2498            value: ParsedDate {
2499                input: "+009999-03-14",
2500                date: 9999-03-14,
2501            },
2502            input: "",
2503        }
2504        "###);
2505    }
2506
2507    #[test]
2508    fn err_date_empty() {
2509        insta::assert_snapshot!(
2510            DateTimeParser::new().parse_date_spec(b"").unwrap_err(),
2511            @r###"failed to parse year in date "": expected four digit year (or leading sign for six digit year), but found end of input"###,
2512        );
2513    }
2514
2515    #[test]
2516    fn err_date_year() {
2517        insta::assert_snapshot!(
2518            DateTimeParser::new().parse_date_spec(b"123").unwrap_err(),
2519            @r###"failed to parse year in date "123": expected four digit year (or leading sign for six digit year), but found end of input"###,
2520        );
2521        insta::assert_snapshot!(
2522            DateTimeParser::new().parse_date_spec(b"123a").unwrap_err(),
2523            @r###"failed to parse year in date "123a": failed to parse "123a" as year (a four digit integer): invalid digit, expected 0-9 but got a"###,
2524        );
2525
2526        insta::assert_snapshot!(
2527            DateTimeParser::new().parse_date_spec(b"-9999").unwrap_err(),
2528            @r###"failed to parse year in date "-9999": expected six digit year (because of a leading sign), but found end of input"###,
2529        );
2530        insta::assert_snapshot!(
2531            DateTimeParser::new().parse_date_spec(b"+9999").unwrap_err(),
2532            @r###"failed to parse year in date "+9999": expected six digit year (because of a leading sign), but found end of input"###,
2533        );
2534        insta::assert_snapshot!(
2535            DateTimeParser::new().parse_date_spec(b"-99999").unwrap_err(),
2536            @r###"failed to parse year in date "-99999": expected six digit year (because of a leading sign), but found end of input"###,
2537        );
2538        insta::assert_snapshot!(
2539            DateTimeParser::new().parse_date_spec(b"+99999").unwrap_err(),
2540            @r###"failed to parse year in date "+99999": expected six digit year (because of a leading sign), but found end of input"###,
2541        );
2542        insta::assert_snapshot!(
2543            DateTimeParser::new().parse_date_spec(b"-99999a").unwrap_err(),
2544            @r###"failed to parse year in date "-99999a": failed to parse "99999a" as year (a six digit integer): invalid digit, expected 0-9 but got a"###,
2545        );
2546        insta::assert_snapshot!(
2547            DateTimeParser::new().parse_date_spec(b"+999999").unwrap_err(),
2548            @r###"failed to parse year in date "+999999": year is not valid: parameter 'year' with value 999999 is not in the required range of -9999..=9999"###,
2549        );
2550        insta::assert_snapshot!(
2551            DateTimeParser::new().parse_date_spec(b"-010000").unwrap_err(),
2552            @r###"failed to parse year in date "-010000": year is not valid: parameter 'year' with value 10000 is not in the required range of -9999..=9999"###,
2553        );
2554    }
2555
2556    #[test]
2557    fn err_date_month() {
2558        insta::assert_snapshot!(
2559            DateTimeParser::new().parse_date_spec(b"2024-").unwrap_err(),
2560            @r###"failed to parse month in date "2024-": expected two digit month, but found end of input"###,
2561        );
2562        insta::assert_snapshot!(
2563            DateTimeParser::new().parse_date_spec(b"2024").unwrap_err(),
2564            @r###"failed to parse month in date "2024": expected two digit month, but found end of input"###,
2565        );
2566        insta::assert_snapshot!(
2567            DateTimeParser::new().parse_date_spec(b"2024-13-01").unwrap_err(),
2568            @r###"failed to parse month in date "2024-13-01": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"###,
2569        );
2570        insta::assert_snapshot!(
2571            DateTimeParser::new().parse_date_spec(b"20241301").unwrap_err(),
2572            @r###"failed to parse month in date "20241301": month is not valid: parameter 'month' with value 13 is not in the required range of 1..=12"###,
2573        );
2574    }
2575
2576    #[test]
2577    fn err_date_day() {
2578        insta::assert_snapshot!(
2579            DateTimeParser::new().parse_date_spec(b"2024-12-").unwrap_err(),
2580            @r###"failed to parse day in date "2024-12-": expected two digit day, but found end of input"###,
2581        );
2582        insta::assert_snapshot!(
2583            DateTimeParser::new().parse_date_spec(b"202412").unwrap_err(),
2584            @r###"failed to parse day in date "202412": expected two digit day, but found end of input"###,
2585        );
2586        insta::assert_snapshot!(
2587            DateTimeParser::new().parse_date_spec(b"2024-12-40").unwrap_err(),
2588            @r###"failed to parse day in date "2024-12-40": day is not valid: parameter 'day' with value 40 is not in the required range of 1..=31"###,
2589        );
2590        insta::assert_snapshot!(
2591            DateTimeParser::new().parse_date_spec(b"2024-11-31").unwrap_err(),
2592            @r###"date parsed from "2024-11-31" is not valid: parameter 'day' with value 31 is not in the required range of 1..=30"###,
2593        );
2594        insta::assert_snapshot!(
2595            DateTimeParser::new().parse_date_spec(b"2024-02-30").unwrap_err(),
2596            @r###"date parsed from "2024-02-30" is not valid: parameter 'day' with value 30 is not in the required range of 1..=29"###,
2597        );
2598        insta::assert_snapshot!(
2599            DateTimeParser::new().parse_date_spec(b"2023-02-29").unwrap_err(),
2600            @r###"date parsed from "2023-02-29" is not valid: parameter 'day' with value 29 is not in the required range of 1..=28"###,
2601        );
2602    }
2603
2604    #[test]
2605    fn err_date_separator() {
2606        insta::assert_snapshot!(
2607            DateTimeParser::new().parse_date_spec(b"2024-1231").unwrap_err(),
2608            @r###"failed to parse separator after month: expected '-' separator, but found "3" instead"###,
2609        );
2610        insta::assert_snapshot!(
2611            DateTimeParser::new().parse_date_spec(b"202412-31").unwrap_err(),
2612            @"failed to parse separator after month: expected no separator after month since none was found after the year, but found a '-' separator",
2613        );
2614    }
2615
2616    #[test]
2617    fn ok_time_basic() {
2618        let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
2619
2620        insta::assert_debug_snapshot!(p(b"01:02:03"), @r###"
2621        Parsed {
2622            value: ParsedTime {
2623                input: "01:02:03",
2624                time: 01:02:03,
2625                extended: true,
2626            },
2627            input: "",
2628        }
2629        "###);
2630        insta::assert_debug_snapshot!(p(b"010203"), @r###"
2631        Parsed {
2632            value: ParsedTime {
2633                input: "010203",
2634                time: 01:02:03,
2635                extended: false,
2636            },
2637            input: "",
2638        }
2639        "###);
2640    }
2641
2642    #[test]
2643    fn ok_time_fractional() {
2644        let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
2645
2646        insta::assert_debug_snapshot!(p(b"01:02:03.123456789"), @r###"
2647        Parsed {
2648            value: ParsedTime {
2649                input: "01:02:03.123456789",
2650                time: 01:02:03.123456789,
2651                extended: true,
2652            },
2653            input: "",
2654        }
2655        "###);
2656        insta::assert_debug_snapshot!(p(b"010203.123456789"), @r###"
2657        Parsed {
2658            value: ParsedTime {
2659                input: "010203.123456789",
2660                time: 01:02:03.123456789,
2661                extended: false,
2662            },
2663            input: "",
2664        }
2665        "###);
2666
2667        insta::assert_debug_snapshot!(p(b"01:02:03.9"), @r###"
2668        Parsed {
2669            value: ParsedTime {
2670                input: "01:02:03.9",
2671                time: 01:02:03.9,
2672                extended: true,
2673            },
2674            input: "",
2675        }
2676        "###);
2677    }
2678
2679    #[test]
2680    fn ok_time_no_fractional() {
2681        let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
2682
2683        insta::assert_debug_snapshot!(p(b"01:02.123456789"), @r###"
2684        Parsed {
2685            value: ParsedTime {
2686                input: "01:02",
2687                time: 01:02:00,
2688                extended: true,
2689            },
2690            input: ".123456789",
2691        }
2692        "###);
2693    }
2694
2695    #[test]
2696    fn ok_time_leap() {
2697        let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
2698
2699        insta::assert_debug_snapshot!(p(b"01:02:60"), @r###"
2700        Parsed {
2701            value: ParsedTime {
2702                input: "01:02:60",
2703                time: 01:02:59,
2704                extended: true,
2705            },
2706            input: "",
2707        }
2708        "###);
2709    }
2710
2711    #[test]
2712    fn ok_time_mixed_format() {
2713        let p = |input| DateTimeParser::new().parse_time_spec(input).unwrap();
2714
2715        insta::assert_debug_snapshot!(p(b"01:0203"), @r###"
2716        Parsed {
2717            value: ParsedTime {
2718                input: "01:02",
2719                time: 01:02:00,
2720                extended: true,
2721            },
2722            input: "03",
2723        }
2724        "###);
2725        insta::assert_debug_snapshot!(p(b"0102:03"), @r###"
2726        Parsed {
2727            value: ParsedTime {
2728                input: "0102",
2729                time: 01:02:00,
2730                extended: false,
2731            },
2732            input: ":03",
2733        }
2734        "###);
2735    }
2736
2737    #[test]
2738    fn err_time_empty() {
2739        insta::assert_snapshot!(
2740            DateTimeParser::new().parse_time_spec(b"").unwrap_err(),
2741            @r###"failed to parse hour in time "": expected two digit hour, but found end of input"###,
2742        );
2743    }
2744
2745    #[test]
2746    fn err_time_hour() {
2747        insta::assert_snapshot!(
2748            DateTimeParser::new().parse_time_spec(b"a").unwrap_err(),
2749            @r###"failed to parse hour in time "a": expected two digit hour, but found end of input"###,
2750        );
2751        insta::assert_snapshot!(
2752            DateTimeParser::new().parse_time_spec(b"1a").unwrap_err(),
2753            @r###"failed to parse hour in time "1a": failed to parse "1a" as hour (a two digit integer): invalid digit, expected 0-9 but got a"###,
2754        );
2755        insta::assert_snapshot!(
2756            DateTimeParser::new().parse_time_spec(b"24").unwrap_err(),
2757            @r###"failed to parse hour in time "24": hour is not valid: parameter 'hour' with value 24 is not in the required range of 0..=23"###,
2758        );
2759    }
2760
2761    #[test]
2762    fn err_time_minute() {
2763        insta::assert_snapshot!(
2764            DateTimeParser::new().parse_time_spec(b"01:").unwrap_err(),
2765            @r###"failed to parse minute in time "01:": expected two digit minute, but found end of input"###,
2766        );
2767        insta::assert_snapshot!(
2768            DateTimeParser::new().parse_time_spec(b"01:a").unwrap_err(),
2769            @r###"failed to parse minute in time "01:a": expected two digit minute, but found end of input"###,
2770        );
2771        insta::assert_snapshot!(
2772            DateTimeParser::new().parse_time_spec(b"01:1a").unwrap_err(),
2773            @r###"failed to parse minute in time "01:1a": failed to parse "1a" as minute (a two digit integer): invalid digit, expected 0-9 but got a"###,
2774        );
2775        insta::assert_snapshot!(
2776            DateTimeParser::new().parse_time_spec(b"01:60").unwrap_err(),
2777            @r###"failed to parse minute in time "01:60": minute is not valid: parameter 'minute' with value 60 is not in the required range of 0..=59"###,
2778        );
2779    }
2780
2781    #[test]
2782    fn err_time_second() {
2783        insta::assert_snapshot!(
2784            DateTimeParser::new().parse_time_spec(b"01:02:").unwrap_err(),
2785            @r###"failed to parse second in time "01:02:": expected two digit second, but found end of input"###,
2786        );
2787        insta::assert_snapshot!(
2788            DateTimeParser::new().parse_time_spec(b"01:02:a").unwrap_err(),
2789            @r###"failed to parse second in time "01:02:a": expected two digit second, but found end of input"###,
2790        );
2791        insta::assert_snapshot!(
2792            DateTimeParser::new().parse_time_spec(b"01:02:1a").unwrap_err(),
2793            @r###"failed to parse second in time "01:02:1a": failed to parse "1a" as second (a two digit integer): invalid digit, expected 0-9 but got a"###,
2794        );
2795        insta::assert_snapshot!(
2796            DateTimeParser::new().parse_time_spec(b"01:02:61").unwrap_err(),
2797            @r###"failed to parse second in time "01:02:61": second is not valid: parameter 'second' with value 61 is not in the required range of 0..=59"###,
2798        );
2799    }
2800
2801    #[test]
2802    fn err_time_fractional() {
2803        insta::assert_snapshot!(
2804            DateTimeParser::new().parse_time_spec(b"01:02:03.").unwrap_err(),
2805            @r###"failed to parse fractional nanoseconds in time "01:02:03.": found decimal after seconds component, but did not find any decimal digits after decimal"###,
2806        );
2807        insta::assert_snapshot!(
2808            DateTimeParser::new().parse_time_spec(b"01:02:03.a").unwrap_err(),
2809            @r###"failed to parse fractional nanoseconds in time "01:02:03.a": found decimal after seconds component, but did not find any decimal digits after decimal"###,
2810        );
2811    }
2812}