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