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