jiff/fmt/temporal/
parser.rs

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