1use crate::{
2 error::{err, ErrorContext},
3 fmt::{
4 friendly::parser_label,
5 util::{parse_temporal_fraction, DurationUnits},
6 Parsed,
7 },
8 util::{c::Sign, escape, parse},
9 Error, SignedDuration, Span, Unit,
10};
11
12#[derive(Clone, Debug, Default)]
77pub struct SpanParser {
78 _private: (),
79}
80
81impl SpanParser {
82 #[inline]
108 pub const fn new() -> SpanParser {
109 SpanParser { _private: () }
110 }
111
112 #[inline]
181 pub fn parse_span<I: AsRef<[u8]>>(&self, input: I) -> Result<Span, Error> {
182 #[inline(never)]
183 fn imp(span_parser: &SpanParser, input: &[u8]) -> Result<Span, Error> {
184 let mut builder = DurationUnits::default();
185 let parsed = span_parser.parse(input, &mut builder)?;
186 let parsed = parsed.and_then(|_| builder.to_span())?;
187 parsed.into_full()
188 }
189
190 let input = input.as_ref();
191 imp(self, input).with_context(|| {
192 err!(
193 "failed to parse {input:?} in the \"friendly\" format",
194 input = escape::Bytes(input)
195 )
196 })
197 }
198
199 #[inline]
235 pub fn parse_duration<I: AsRef<[u8]>>(
236 &self,
237 input: I,
238 ) -> Result<SignedDuration, Error> {
239 #[inline(never)]
240 fn imp(
241 span_parser: &SpanParser,
242 input: &[u8],
243 ) -> Result<SignedDuration, Error> {
244 let mut builder = DurationUnits::default();
245 let parsed = span_parser.parse(input, &mut builder)?;
246 let parsed = parsed.and_then(|_| builder.to_signed_duration())?;
247 parsed.into_full()
248 }
249
250 let input = input.as_ref();
251 imp(self, input).with_context(|| {
252 err!(
253 "failed to parse {input:?} in the \"friendly\" format",
254 input = escape::Bytes(input)
255 )
256 })
257 }
258
259 #[inline]
297 pub fn parse_unsigned_duration<I: AsRef<[u8]>>(
298 &self,
299 input: I,
300 ) -> Result<core::time::Duration, Error> {
301 #[inline(never)]
302 fn imp(
303 span_parser: &SpanParser,
304 input: &[u8],
305 ) -> Result<core::time::Duration, Error> {
306 let mut builder = DurationUnits::default();
307 let parsed = span_parser.parse(input, &mut builder)?;
308 let parsed =
309 parsed.and_then(|_| builder.to_unsigned_duration())?;
310 let d = parsed.value;
311 parsed.into_full_with(format_args!("{d:?}"))
312 }
313
314 let input = input.as_ref();
315 imp(self, input).with_context(|| {
316 err!(
317 "failed to parse {input:?} in the \"friendly\" format",
318 input = escape::Bytes(input)
319 )
320 })
321 }
322
323 #[cfg_attr(feature = "perf-inline", inline(always))]
324 fn parse<'i>(
325 &self,
326 input: &'i [u8],
327 builder: &mut DurationUnits,
328 ) -> Result<Parsed<'i, ()>, Error> {
329 if input.is_empty() {
330 return Err(err!("an empty string is not a valid duration"));
331 }
332 let (sign, input) =
335 if !input.first().map_or(false, |&b| matches!(b, b'+' | b'-')) {
336 (None, input)
337 } else {
338 let Parsed { value: sign, input } =
339 self.parse_prefix_sign(input);
340 (sign, input)
341 };
342
343 let Parsed { value, input } = self.parse_unit_value(input)?;
344 let Some(first_unit_value) = value else {
345 return Err(err!(
346 "parsing a friendly duration requires it to start \
347 with a unit value (a decimal integer) after an \
348 optional sign, but no integer was found",
349 ));
350 };
351
352 let Parsed { input, .. } =
353 self.parse_duration_units(input, first_unit_value, builder)?;
354
355 let (sign, input) = if !input.first().map_or(false, is_whitespace) {
358 (sign.unwrap_or(Sign::Positive), input)
359 } else {
360 let parsed = self.parse_suffix_sign(sign, input)?;
361 (parsed.value, parsed.input)
362 };
363 builder.set_sign(sign);
364 Ok(Parsed { value: (), input })
365 }
366
367 #[cfg_attr(feature = "perf-inline", inline(always))]
368 fn parse_duration_units<'i>(
369 &self,
370 mut input: &'i [u8],
371 first_unit_value: u64,
372 builder: &mut DurationUnits,
373 ) -> Result<Parsed<'i, ()>, Error> {
374 let mut parsed_any_after_comma = true;
375 let mut value = first_unit_value;
376 loop {
377 let parsed = self.parse_hms_maybe(input, value)?;
378 input = parsed.input;
379 if let Some(hms) = parsed.value {
380 builder.set_hms(
381 hms.hour,
382 hms.minute,
383 hms.second,
384 hms.fraction,
385 )?;
386 break;
387 }
388
389 let fraction =
390 if input.first().map_or(false, |&b| b == b'.' || b == b',') {
391 let parsed = parse_temporal_fraction(input)?;
392 input = parsed.input;
393 parsed.value
394 } else {
395 None
396 };
397
398 input = self.parse_optional_whitespace(input).input;
400
401 let parsed = self.parse_unit_designator(input)?;
403 input = parsed.input;
404 let unit = parsed.value;
405
406 if input.first().map_or(false, |&b| b == b',') {
411 input = self.parse_optional_comma(input)?.input;
412 parsed_any_after_comma = false;
413 }
414
415 builder.set_unit_value(unit, value)?;
416 if let Some(fraction) = fraction {
417 builder.set_fraction(fraction)?;
418 break;
422 }
423
424 let after_whitespace = self.parse_optional_whitespace(input).input;
428 let parsed = self.parse_unit_value(after_whitespace)?;
429 value = match parsed.value {
430 None => break,
431 Some(value) => value,
432 };
433 input = parsed.input;
434 parsed_any_after_comma = true;
435 }
436 if !parsed_any_after_comma {
437 return Err(err!(
438 "found comma at the end of duration, \
439 but a comma indicates at least one more \
440 unit follows",
441 ));
442 }
443 Ok(Parsed { value: (), input })
444 }
445
446 #[cfg_attr(feature = "perf-inline", inline(always))]
452 fn parse_hms_maybe<'i>(
453 &self,
454 input: &'i [u8],
455 hour: u64,
456 ) -> Result<Parsed<'i, Option<HMS>>, Error> {
457 if !input.first().map_or(false, |&b| b == b':') {
458 return Ok(Parsed { input, value: None });
459 }
460 let Parsed { input, value } = self.parse_hms(&input[1..], hour)?;
461 Ok(Parsed { input, value: Some(value) })
462 }
463
464 #[inline(never)]
474 fn parse_hms<'i>(
475 &self,
476 input: &'i [u8],
477 hour: u64,
478 ) -> Result<Parsed<'i, HMS>, Error> {
479 let Parsed { input, value } = self.parse_unit_value(input)?;
480 let Some(minute) = value else {
481 return Err(err!(
482 "expected to parse minute in 'HH:MM:SS' format \
483 following parsed hour of {hour}",
484 ));
485 };
486 if !input.first().map_or(false, |&b| b == b':') {
487 return Err(err!(
488 "when parsing 'HH:MM:SS' format, expected to \
489 see a ':' after the parsed minute of {minute}",
490 ));
491 }
492 let input = &input[1..];
493 let Parsed { input, value } = self.parse_unit_value(input)?;
494 let Some(second) = value else {
495 return Err(err!(
496 "expected to parse second in 'HH:MM:SS' format \
497 following parsed minute of {minute}",
498 ));
499 };
500 let (fraction, input) =
501 if input.first().map_or(false, |&b| b == b'.' || b == b',') {
502 let parsed = parse_temporal_fraction(input)?;
503 (parsed.value, parsed.input)
504 } else {
505 (None, input)
506 };
507 let hms = HMS { hour, minute, second, fraction };
508 Ok(Parsed { input, value: hms })
509 }
510
511 #[cfg_attr(feature = "perf-inline", inline(always))]
524 fn parse_unit_value<'i>(
525 &self,
526 input: &'i [u8],
527 ) -> Result<Parsed<'i, Option<u64>>, Error> {
528 let (value, input) = parse::u64_prefix(input)?;
529 Ok(Parsed { value, input })
530 }
531
532 #[cfg_attr(feature = "perf-inline", inline(always))]
539 fn parse_unit_designator<'i>(
540 &self,
541 input: &'i [u8],
542 ) -> Result<Parsed<'i, Unit>, Error> {
543 let Some((unit, len)) = parser_label::find(input) else {
544 if input.is_empty() {
545 return Err(err!(
546 "expected to find unit designator suffix \
547 (e.g., 'years' or 'secs'), \
548 but found end of input",
549 ));
550 } else {
551 return Err(err!(
552 "expected to find unit designator suffix \
553 (e.g., 'years' or 'secs'), \
554 but found input beginning with {found:?} instead",
555 found = escape::Bytes(&input[..input.len().min(20)]),
556 ));
557 }
558 };
559 Ok(Parsed { value: unit, input: &input[len..] })
560 }
561
562 #[inline(never)]
567 fn parse_prefix_sign<'i>(
568 &self,
569 input: &'i [u8],
570 ) -> Parsed<'i, Option<Sign>> {
571 let Some(sign) = input.first().copied() else {
572 return Parsed { value: None, input };
573 };
574 let sign = if sign == b'+' {
575 Sign::Positive
576 } else if sign == b'-' {
577 Sign::Negative
578 } else {
579 return Parsed { value: None, input };
580 };
581 Parsed { value: Some(sign), input: &input[1..] }
582 }
583
584 #[inline(never)]
598 fn parse_suffix_sign<'i>(
599 &self,
600 prefix_sign: Option<Sign>,
601 mut input: &'i [u8],
602 ) -> Result<Parsed<'i, Sign>, Error> {
603 if !input.first().map_or(false, is_whitespace) {
604 let sign = prefix_sign.unwrap_or(Sign::Positive);
605 return Ok(Parsed { value: sign, input });
606 }
607 input = self.parse_optional_whitespace(&input[1..]).input;
609 let (suffix_sign, input) = if input.starts_with(b"ago") {
610 (Some(Sign::Negative), &input[3..])
611 } else {
612 (None, input)
613 };
614 let sign = match (prefix_sign, suffix_sign) {
615 (Some(_), Some(_)) => {
616 return Err(err!(
617 "expected to find either a prefix sign (+/-) or \
618 a suffix sign (ago), but found both",
619 ))
620 }
621 (Some(sign), None) => sign,
622 (None, Some(sign)) => sign,
623 (None, None) => Sign::Positive,
624 };
625 Ok(Parsed { value: sign, input })
626 }
627
628 #[inline(never)]
638 fn parse_optional_comma<'i>(
639 &self,
640 mut input: &'i [u8],
641 ) -> Result<Parsed<'i, ()>, Error> {
642 if !input.first().map_or(false, |&b| b == b',') {
643 return Ok(Parsed { value: (), input });
644 }
645 input = &input[1..];
646 if input.is_empty() {
647 return Err(err!(
648 "expected whitespace after comma, but found end of input"
649 ));
650 }
651 if !is_whitespace(&input[0]) {
652 return Err(err!(
653 "expected whitespace after comma, but found {found:?}",
654 found = escape::Byte(input[0]),
655 ));
656 }
657 Ok(Parsed { value: (), input: &input[1..] })
658 }
659
660 #[cfg_attr(feature = "perf-inline", inline(always))]
662 fn parse_optional_whitespace<'i>(
663 &self,
664 mut input: &'i [u8],
665 ) -> Parsed<'i, ()> {
666 while input.first().map_or(false, is_whitespace) {
667 input = &input[1..];
668 }
669 Parsed { value: (), input }
670 }
671}
672
673#[derive(Debug)]
675struct HMS {
676 hour: u64,
677 minute: u64,
678 second: u64,
679 fraction: Option<u32>,
680}
681
682#[cfg_attr(feature = "perf-inline", inline(always))]
684fn is_whitespace(byte: &u8) -> bool {
685 matches!(*byte, b' ' | b'\t' | b'\n' | b'\r' | b'\x0C')
686}
687
688#[cfg(feature = "alloc")]
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn parse_span_basic() {
695 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
696
697 insta::assert_snapshot!(p("5 years"), @"P5Y");
698 insta::assert_snapshot!(p("5 years 4 months"), @"P5Y4M");
699 insta::assert_snapshot!(p("5 years 4 months 3 hours"), @"P5Y4MT3H");
700 insta::assert_snapshot!(p("5 years, 4 months, 3 hours"), @"P5Y4MT3H");
701
702 insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
703 insta::assert_snapshot!(p("5 days 01:02:03"), @"P5DT1H2M3S");
704 insta::assert_snapshot!(p("5 days, 01:02:03"), @"P5DT1H2M3S");
706 insta::assert_snapshot!(p("3yrs 5 days 01:02:03"), @"P3Y5DT1H2M3S");
707 insta::assert_snapshot!(p("3yrs 5 days, 01:02:03"), @"P3Y5DT1H2M3S");
708 insta::assert_snapshot!(
709 p("3yrs 5 days, 01:02:03.123456789"),
710 @"P3Y5DT1H2M3.123456789S",
711 );
712 insta::assert_snapshot!(p("999:999:999"), @"PT999H999M999S");
713 }
714
715 #[test]
716 fn parse_span_fractional() {
717 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
718
719 insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
720 insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
721 insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
722 insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
723 insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
724
725 insta::assert_snapshot!(p("1d 1.5hrs"), @"P1DT1H30M");
726 insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
727 insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
728 insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
729 insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
730
731 insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
732 }
733
734 #[test]
735 fn parse_span_boundaries() {
736 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
737
738 insta::assert_snapshot!(p("19998 years"), @"P19998Y");
739 insta::assert_snapshot!(p("19998 years ago"), @"-P19998Y");
740 insta::assert_snapshot!(p("239976 months"), @"P239976M");
741 insta::assert_snapshot!(p("239976 months ago"), @"-P239976M");
742 insta::assert_snapshot!(p("1043497 weeks"), @"P1043497W");
743 insta::assert_snapshot!(p("1043497 weeks ago"), @"-P1043497W");
744 insta::assert_snapshot!(p("7304484 days"), @"P7304484D");
745 insta::assert_snapshot!(p("7304484 days ago"), @"-P7304484D");
746 insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
747 insta::assert_snapshot!(p("175307616 hours ago"), @"-PT175307616H");
748 insta::assert_snapshot!(p("10518456960 minutes"), @"PT10518456960M");
749 insta::assert_snapshot!(p("10518456960 minutes ago"), @"-PT10518456960M");
750 insta::assert_snapshot!(p("631107417600 seconds"), @"PT631107417600S");
751 insta::assert_snapshot!(p("631107417600 seconds ago"), @"-PT631107417600S");
752 insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT631107417600S");
753 insta::assert_snapshot!(p("631107417600000 milliseconds ago"), @"-PT631107417600S");
754 insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT631107417600S");
755 insta::assert_snapshot!(p("631107417600000000 microseconds ago"), @"-PT631107417600S");
756 insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT9223372036.854775807S");
757 insta::assert_snapshot!(p("9223372036854775807 nanoseconds ago"), @"-PT9223372036.854775807S");
758
759 insta::assert_snapshot!(p("175307617 hours"), @"PT175307616H60M");
760 insta::assert_snapshot!(p("175307617 hours ago"), @"-PT175307616H60M");
761 insta::assert_snapshot!(p("10518456961 minutes"), @"PT10518456960M60S");
762 insta::assert_snapshot!(p("10518456961 minutes ago"), @"-PT10518456960M60S");
763 insta::assert_snapshot!(p("631107417601 seconds"), @"PT631107417601S");
764 insta::assert_snapshot!(p("631107417601 seconds ago"), @"-PT631107417601S");
765 insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT631107417600.001S");
766 insta::assert_snapshot!(p("631107417600001 milliseconds ago"), @"-PT631107417600.001S");
767 insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT631107417600.000001S");
768 insta::assert_snapshot!(p("631107417600000001 microseconds ago"), @"-PT631107417600.000001S");
769 }
772
773 #[test]
774 fn err_span_basic() {
775 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
776
777 insta::assert_snapshot!(
778 p(""),
779 @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
780 );
781 insta::assert_snapshot!(
782 p(" "),
783 @r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
784 );
785 insta::assert_snapshot!(
786 p("a"),
787 @r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
788 );
789 insta::assert_snapshot!(
790 p("2 months 1 year"),
791 @r###"failed to parse "2 months 1 year" in the "friendly" format: found value 1 with unit year after unit month, but units must be written from largest to smallest (and they can't be repeated)"###,
792 );
793 insta::assert_snapshot!(
794 p("1 year 1 mont"),
795 @r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###,
796 );
797 insta::assert_snapshot!(
798 p("2 months,"),
799 @r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###,
800 );
801 insta::assert_snapshot!(
802 p("2 months, "),
803 @r#"failed to parse "2 months, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
804 );
805 insta::assert_snapshot!(
806 p("2 months ,"),
807 @r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###,
808 );
809 }
810
811 #[test]
812 fn err_span_sign() {
813 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
814
815 insta::assert_snapshot!(
816 p("1yago"),
817 @r###"failed to parse "1yago" in the "friendly" format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"###,
818 );
819 insta::assert_snapshot!(
820 p("1 year 1 monthago"),
821 @r###"failed to parse "1 year 1 monthago" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"###,
822 );
823 insta::assert_snapshot!(
824 p("+1 year 1 month ago"),
825 @r###"failed to parse "+1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
826 );
827 insta::assert_snapshot!(
828 p("-1 year 1 month ago"),
829 @r###"failed to parse "-1 year 1 month ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
830 );
831 }
832
833 #[test]
834 fn err_span_overflow_fraction() {
835 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
836 let pe = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
837
838 insta::assert_snapshot!(
839 pe("640330789636854776 micros"),
843 @r#"failed to parse "640330789636854776 micros" in the "friendly" format: failed to set value 640330789636854776 as microsecond unit on span: failed to set nanosecond value 9223372036854776000 (it overflows `i64`) on span determined from 640330789636854776.0"#,
844 );
845 insta::assert_snapshot!(
847 p("640330789636854775 micros"),
848 @"PT640330789636.854775S"
849 );
850
851 insta::assert_snapshot!(
852 pe("640330789636854775.808 micros"),
856 @r#"failed to parse "640330789636854775.808 micros" in the "friendly" format: failed to set nanosecond value 9223372036854775808 (it overflows `i64`) on span determined from 640330789636854775.808000000"#,
857 );
858 insta::assert_snapshot!(
860 p("640330789636854775.807 micros"),
861 @"PT640330789636.854775807S"
862 );
863 }
864
865 #[test]
866 fn err_span_overflow_units() {
867 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
868
869 insta::assert_snapshot!(
870 p("19999 years"),
871 @r###"failed to parse "19999 years" in the "friendly" format: failed to set value 19999 as year unit on span: parameter 'years' with value 19999 is not in the required range of -19998..=19998"###,
872 );
873 insta::assert_snapshot!(
874 p("19999 years ago"),
875 @r#"failed to parse "19999 years ago" in the "friendly" format: failed to set value -19999 as year unit on span: parameter 'years' with value -19999 is not in the required range of -19998..=19998"#,
876 );
877
878 insta::assert_snapshot!(
879 p("239977 months"),
880 @r###"failed to parse "239977 months" in the "friendly" format: failed to set value 239977 as month unit on span: parameter 'months' with value 239977 is not in the required range of -239976..=239976"###,
881 );
882 insta::assert_snapshot!(
883 p("239977 months ago"),
884 @r#"failed to parse "239977 months ago" in the "friendly" format: failed to set value -239977 as month unit on span: parameter 'months' with value -239977 is not in the required range of -239976..=239976"#,
885 );
886
887 insta::assert_snapshot!(
888 p("1043498 weeks"),
889 @r###"failed to parse "1043498 weeks" in the "friendly" format: failed to set value 1043498 as week unit on span: parameter 'weeks' with value 1043498 is not in the required range of -1043497..=1043497"###,
890 );
891 insta::assert_snapshot!(
892 p("1043498 weeks ago"),
893 @r#"failed to parse "1043498 weeks ago" in the "friendly" format: failed to set value -1043498 as week unit on span: parameter 'weeks' with value -1043498 is not in the required range of -1043497..=1043497"#,
894 );
895
896 insta::assert_snapshot!(
897 p("7304485 days"),
898 @r###"failed to parse "7304485 days" in the "friendly" format: failed to set value 7304485 as day unit on span: parameter 'days' with value 7304485 is not in the required range of -7304484..=7304484"###,
899 );
900 insta::assert_snapshot!(
901 p("7304485 days ago"),
902 @r#"failed to parse "7304485 days ago" in the "friendly" format: failed to set value -7304485 as day unit on span: parameter 'days' with value -7304485 is not in the required range of -7304484..=7304484"#,
903 );
904
905 insta::assert_snapshot!(
906 p("9223372036854775808 nanoseconds"),
907 @r#"failed to parse "9223372036854775808 nanoseconds" in the "friendly" format: `9223372036854775808` nanoseconds is too big (or small) to fit into a signed 64-bit integer"#,
908 );
909 insta::assert_snapshot!(
910 p("9223372036854775808 nanoseconds ago"),
911 @r#"failed to parse "9223372036854775808 nanoseconds ago" in the "friendly" format: failed to set value -9223372036854775808 as nanosecond unit on span: parameter 'nanoseconds' with value -9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"#,
912 );
913 }
914
915 #[test]
916 fn err_span_fraction() {
917 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
918
919 insta::assert_snapshot!(
920 p("1.5 years"),
921 @r#"failed to parse "1.5 years" in the "friendly" format: fractional years are not supported"#,
922 );
923 insta::assert_snapshot!(
924 p("1.5 nanos"),
925 @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
926 );
927 }
928
929 #[test]
930 fn err_span_hms() {
931 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
932
933 insta::assert_snapshot!(
934 p("05:"),
935 @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
936 );
937 insta::assert_snapshot!(
938 p("05:06"),
939 @r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
940 );
941 insta::assert_snapshot!(
942 p("05:06:"),
943 @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
944 );
945 insta::assert_snapshot!(
946 p("2 hours, 05:06:07"),
947 @r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
948 );
949 }
950
951 #[test]
952 fn parse_signed_duration_basic() {
953 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
954
955 insta::assert_snapshot!(p("1 hour, 2 minutes, 3 seconds"), @"PT1H2M3S");
956 insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
957 insta::assert_snapshot!(p("999:999:999"), @"PT1015H55M39S");
958 }
959
960 #[test]
961 fn parse_signed_duration_negate() {
962 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
963 let perr = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
964
965 insta::assert_snapshot!(
966 p("9223372036854775807s"),
967 @"PT2562047788015215H30M7S",
968 );
969 insta::assert_snapshot!(
970 perr("9223372036854775808s"),
971 @r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
972 );
973 insta::assert_snapshot!(
974 p("-9223372036854775808s"),
975 @"-PT2562047788015215H30M8S",
976 );
977 }
978
979 #[test]
980 fn parse_signed_duration_fractional() {
981 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
982
983 insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
984 insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
985 insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
986 insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
987 insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
988
989 insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
990 insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
991 insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
992 insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
993
994 insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
995 }
996
997 #[test]
998 fn parse_signed_duration_boundaries() {
999 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1000 let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1001
1002 insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
1003 insta::assert_snapshot!(p("175307616 hours ago"), @"-PT175307616H");
1004 insta::assert_snapshot!(p("10518456960 minutes"), @"PT175307616H");
1005 insta::assert_snapshot!(p("10518456960 minutes ago"), @"-PT175307616H");
1006 insta::assert_snapshot!(p("631107417600 seconds"), @"PT175307616H");
1007 insta::assert_snapshot!(p("631107417600 seconds ago"), @"-PT175307616H");
1008 insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT175307616H");
1009 insta::assert_snapshot!(p("631107417600000 milliseconds ago"), @"-PT175307616H");
1010 insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT175307616H");
1011 insta::assert_snapshot!(p("631107417600000000 microseconds ago"), @"-PT175307616H");
1012 insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT2562047H47M16.854775807S");
1013 insta::assert_snapshot!(p("9223372036854775807 nanoseconds ago"), @"-PT2562047H47M16.854775807S");
1014
1015 insta::assert_snapshot!(p("175307617 hours"), @"PT175307617H");
1016 insta::assert_snapshot!(p("175307617 hours ago"), @"-PT175307617H");
1017 insta::assert_snapshot!(p("10518456961 minutes"), @"PT175307616H1M");
1018 insta::assert_snapshot!(p("10518456961 minutes ago"), @"-PT175307616H1M");
1019 insta::assert_snapshot!(p("631107417601 seconds"), @"PT175307616H1S");
1020 insta::assert_snapshot!(p("631107417601 seconds ago"), @"-PT175307616H1S");
1021 insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT175307616H0.001S");
1022 insta::assert_snapshot!(p("631107417600001 milliseconds ago"), @"-PT175307616H0.001S");
1023 insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT175307616H0.000001S");
1024 insta::assert_snapshot!(p("631107417600000001 microseconds ago"), @"-PT175307616H0.000001S");
1025 insta::assert_snapshot!(p("2562047788015215hours"), @"PT2562047788015215H");
1032 insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H");
1033 insta::assert_snapshot!(
1034 pe("2562047788015216hrs"),
1035 @r#"failed to parse "2562047788015216hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 2562047788015216 of unit hour"#,
1036 );
1037
1038 insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M");
1039 insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M");
1040 insta::assert_snapshot!(
1041 pe("153722867280912931mins"),
1042 @r#"failed to parse "153722867280912931mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 153722867280912931 of unit minute"#,
1043 );
1044
1045 insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S");
1046 insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S");
1047 insta::assert_snapshot!(
1048 pe("9223372036854775808s"),
1049 @r#"failed to parse "9223372036854775808s" in the "friendly" format: `9223372036854775808` seconds is too big (or small) to fit into a signed 64-bit integer"#,
1050 );
1051 insta::assert_snapshot!(
1052 p("-9223372036854775808s"),
1053 @"-PT2562047788015215H30M8S",
1054 );
1055 }
1056
1057 #[test]
1058 fn err_signed_duration_basic() {
1059 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1060
1061 insta::assert_snapshot!(
1062 p(""),
1063 @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
1064 );
1065 insta::assert_snapshot!(
1066 p(" "),
1067 @r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
1068 );
1069 insta::assert_snapshot!(
1070 p("5"),
1071 @r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
1072 );
1073 insta::assert_snapshot!(
1074 p("a"),
1075 @r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
1076 );
1077 insta::assert_snapshot!(
1078 p("2 minutes 1 hour"),
1079 @r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
1080 );
1081 insta::assert_snapshot!(
1082 p("1 hour 1 minut"),
1083 @r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###,
1084 );
1085 insta::assert_snapshot!(
1086 p("2 minutes,"),
1087 @r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
1088 );
1089 insta::assert_snapshot!(
1090 p("2 minutes, "),
1091 @r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
1092 );
1093 insta::assert_snapshot!(
1094 p("2 minutes ,"),
1095 @r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###,
1096 );
1097 }
1098
1099 #[test]
1100 fn err_signed_duration_sign() {
1101 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1102
1103 insta::assert_snapshot!(
1104 p("1hago"),
1105 @r###"failed to parse "1hago" in the "friendly" format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"###,
1106 );
1107 insta::assert_snapshot!(
1108 p("1 hour 1 minuteago"),
1109 @r###"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"###,
1110 );
1111 insta::assert_snapshot!(
1112 p("+1 hour 1 minute ago"),
1113 @r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
1114 );
1115 insta::assert_snapshot!(
1116 p("-1 hour 1 minute ago"),
1117 @r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
1118 );
1119 }
1120
1121 #[test]
1122 fn err_signed_duration_overflow_fraction() {
1123 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1124 let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1125
1126 insta::assert_snapshot!(
1127 pe("9223372036854775808 micros"),
1130 @r#"failed to parse "9223372036854775808 micros" in the "friendly" format: `9223372036854775808` microseconds is too big (or small) to fit into a signed 64-bit integer"#,
1131 );
1132 insta::assert_snapshot!(
1134 p("9223372036854775807 micros"),
1135 @"PT2562047788H54.775807S"
1136 );
1137 }
1138
1139 #[test]
1140 fn err_signed_duration_fraction() {
1141 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1142
1143 insta::assert_snapshot!(
1144 p("1.5 nanos"),
1145 @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
1146 );
1147 }
1148
1149 #[test]
1150 fn err_signed_duration_hms() {
1151 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1152
1153 insta::assert_snapshot!(
1154 p("05:"),
1155 @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
1156 );
1157 insta::assert_snapshot!(
1158 p("05:06"),
1159 @r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
1160 );
1161 insta::assert_snapshot!(
1162 p("05:06:"),
1163 @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
1164 );
1165 insta::assert_snapshot!(
1166 p("2 hours, 05:06:07"),
1167 @r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
1168 );
1169 }
1170
1171 #[test]
1172 fn parse_unsigned_duration_basic() {
1173 let p = |s: &str| {
1174 let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1175 crate::fmt::temporal::SpanPrinter::new()
1176 .unsigned_duration_to_string(&dur)
1177 };
1178
1179 insta::assert_snapshot!(
1180 p("1 hour, 2 minutes, 3 seconds"),
1181 @"PT1H2M3S",
1182 );
1183 insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
1184 insta::assert_snapshot!(p("999:999:999"), @"PT1015H55M39S");
1185 insta::assert_snapshot!(
1186 p("+1hr"),
1187 @"PT1H",
1188 );
1189 }
1190
1191 #[test]
1192 fn parse_unsigned_duration_negate() {
1193 let p = |s: &str| {
1194 let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1195 crate::fmt::temporal::SpanPrinter::new()
1196 .unsigned_duration_to_string(&dur)
1197 };
1198 let perr = |s: &str| {
1199 SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1200 };
1201
1202 insta::assert_snapshot!(
1203 p("18446744073709551615s"),
1204 @"PT5124095576030431H15S",
1205 );
1206 insta::assert_snapshot!(
1207 perr("18446744073709551616s"),
1208 @r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
1209 );
1210 insta::assert_snapshot!(
1211 perr("-1s"),
1212 @r#"failed to parse "-1s" in the "friendly" format: cannot parse negative duration into unsigned `std::time::Duration`"#,
1213 );
1214 }
1215
1216 #[test]
1217 fn parse_unsigned_duration_fractional() {
1218 let p = |s: &str| {
1219 let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1220 crate::fmt::temporal::SpanPrinter::new()
1221 .unsigned_duration_to_string(&dur)
1222 };
1223
1224 insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
1225 insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
1226 insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
1227 insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
1228 insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
1229
1230 insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
1231 insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
1232 insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
1233 insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
1234
1235 insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
1236 }
1237
1238 #[test]
1239 fn parse_unsigned_duration_boundaries() {
1240 let p = |s: &str| {
1241 let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1242 crate::fmt::temporal::SpanPrinter::new()
1243 .unsigned_duration_to_string(&dur)
1244 };
1245 let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1246
1247 insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
1248 insta::assert_snapshot!(p("10518456960 minutes"), @"PT175307616H");
1249 insta::assert_snapshot!(p("631107417600 seconds"), @"PT175307616H");
1250 insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT175307616H");
1251 insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT175307616H");
1252 insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT2562047H47M16.854775807S");
1253
1254 insta::assert_snapshot!(p("175307617 hours"), @"PT175307617H");
1255 insta::assert_snapshot!(p("10518456961 minutes"), @"PT175307616H1M");
1256 insta::assert_snapshot!(p("631107417601 seconds"), @"PT175307616H1S");
1257 insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT175307616H0.001S");
1258 insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT175307616H0.000001S");
1259
1260 insta::assert_snapshot!(p("5124095576030431hours"), @"PT5124095576030431H");
1264 insta::assert_snapshot!(
1265 pe("5124095576030432hrs"),
1266 @r#"failed to parse "5124095576030432hrs" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 5124095576030432 of unit hour"#,
1267 );
1268
1269 insta::assert_snapshot!(p("307445734561825860minutes"), @"PT5124095576030431H");
1270 insta::assert_snapshot!(
1271 pe("307445734561825861mins"),
1272 @r#"failed to parse "307445734561825861mins" in the "friendly" format: accumulated `SignedDuration` of `0s` overflowed when adding 307445734561825861 of unit minute"#,
1273 );
1274
1275 insta::assert_snapshot!(p("18446744073709551615seconds"), @"PT5124095576030431H15S");
1276 insta::assert_snapshot!(
1277 pe("18446744073709551616s"),
1278 @r#"failed to parse "18446744073709551616s" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
1279 );
1280 }
1281
1282 #[test]
1283 fn err_unsigned_duration_basic() {
1284 let p = |s: &str| {
1285 SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1286 };
1287
1288 insta::assert_snapshot!(
1289 p(""),
1290 @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
1291 );
1292 insta::assert_snapshot!(
1293 p(" "),
1294 @r###"failed to parse " " in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
1295 );
1296 insta::assert_snapshot!(
1297 p("5"),
1298 @r###"failed to parse "5" in the "friendly" format: expected to find unit designator suffix (e.g., 'years' or 'secs'), but found end of input"###,
1299 );
1300 insta::assert_snapshot!(
1301 p("a"),
1302 @r###"failed to parse "a" in the "friendly" format: parsing a friendly duration requires it to start with a unit value (a decimal integer) after an optional sign, but no integer was found"###,
1303 );
1304 insta::assert_snapshot!(
1305 p("2 minutes 1 hour"),
1306 @r###"failed to parse "2 minutes 1 hour" in the "friendly" format: found value 1 with unit hour after unit minute, but units must be written from largest to smallest (and they can't be repeated)"###,
1307 );
1308 insta::assert_snapshot!(
1309 p("1 hour 1 minut"),
1310 @r#"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value '3660s', but unparsed input "ut" remains (expected no unparsed input)"#,
1311 );
1312 insta::assert_snapshot!(
1313 p("2 minutes,"),
1314 @r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
1315 );
1316 insta::assert_snapshot!(
1317 p("2 minutes, "),
1318 @r#"failed to parse "2 minutes, " in the "friendly" format: found comma at the end of duration, but a comma indicates at least one more unit follows"#,
1319 );
1320 insta::assert_snapshot!(
1321 p("2 minutes ,"),
1322 @r#"failed to parse "2 minutes ," in the "friendly" format: parsed value '120s', but unparsed input "," remains (expected no unparsed input)"#,
1323 );
1324 }
1325
1326 #[test]
1327 fn err_unsigned_duration_sign() {
1328 let p = |s: &str| {
1329 SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1330 };
1331
1332 insta::assert_snapshot!(
1333 p("1hago"),
1334 @r#"failed to parse "1hago" in the "friendly" format: parsed value '3600s', but unparsed input "ago" remains (expected no unparsed input)"#,
1335 );
1336 insta::assert_snapshot!(
1337 p("1 hour 1 minuteago"),
1338 @r#"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value '3660s', but unparsed input "ago" remains (expected no unparsed input)"#,
1339 );
1340 insta::assert_snapshot!(
1341 p("+1 hour 1 minute ago"),
1342 @r###"failed to parse "+1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
1343 );
1344 insta::assert_snapshot!(
1345 p("-1 hour 1 minute ago"),
1346 @r###"failed to parse "-1 hour 1 minute ago" in the "friendly" format: expected to find either a prefix sign (+/-) or a suffix sign (ago), but found both"###,
1347 );
1348 }
1349
1350 #[test]
1351 fn err_unsigned_duration_overflow_fraction() {
1352 let p = |s: &str| {
1353 let dur = SpanParser::new().parse_unsigned_duration(s).unwrap();
1354 crate::fmt::temporal::SpanPrinter::new()
1355 .unsigned_duration_to_string(&dur)
1356 };
1357 let pe = |s: &str| {
1358 SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1359 };
1360
1361 insta::assert_snapshot!(
1362 pe("18446744073709551616 micros"),
1365 @r#"failed to parse "18446744073709551616 micros" in the "friendly" format: number `18446744073709551616` too big to parse into 64-bit integer"#,
1366 );
1367 insta::assert_snapshot!(
1369 p("18446744073709551615 micros"),
1370 @"PT5124095576H1M49.551615S"
1371 );
1372 }
1373
1374 #[test]
1375 fn err_unsigned_duration_fraction() {
1376 let p = |s: &str| {
1377 SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1378 };
1379
1380 insta::assert_snapshot!(
1381 p("1.5 nanos"),
1382 @r#"failed to parse "1.5 nanos" in the "friendly" format: fractional nanoseconds are not supported"#,
1383 );
1384 }
1385
1386 #[test]
1387 fn err_unsigned_duration_hms() {
1388 let p = |s: &str| {
1389 SpanParser::new().parse_unsigned_duration(s).unwrap_err()
1390 };
1391
1392 insta::assert_snapshot!(
1393 p("05:"),
1394 @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
1395 );
1396 insta::assert_snapshot!(
1397 p("05:06"),
1398 @r###"failed to parse "05:06" in the "friendly" format: when parsing 'HH:MM:SS' format, expected to see a ':' after the parsed minute of 6"###,
1399 );
1400 insta::assert_snapshot!(
1401 p("05:06:"),
1402 @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
1403 );
1404 insta::assert_snapshot!(
1405 p("2 hours, 05:06:07"),
1406 @r#"failed to parse "2 hours, 05:06:07" in the "friendly" format: found `HH:MM:SS` after unit hour, but `HH:MM:SS` can only appear after years, months, weeks or days"#,
1407 );
1408 }
1409}