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