jiff/fmt/temporal/
parser.rs

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