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