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