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