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