1use crate::{
2 error::{err, ErrorContext},
3 fmt::{
4 friendly::parser_label,
5 util::{
6 fractional_time_to_duration, fractional_time_to_span,
7 parse_temporal_fraction,
8 },
9 Parsed,
10 },
11 util::{escape, t},
12 Error, SignedDuration, Span, Unit,
13};
14
15#[derive(Clone, Debug, Default)]
80pub struct SpanParser {
81 _private: (),
82}
83
84impl SpanParser {
85 #[inline]
111 pub const fn new() -> SpanParser {
112 SpanParser { _private: () }
113 }
114
115 pub fn parse_span<I: AsRef<[u8]>>(&self, input: I) -> Result<Span, Error> {
184 let input = input.as_ref();
185 let parsed = self.parse_to_span(input).with_context(|| {
186 err!(
187 "failed to parse {input:?} in the \"friendly\" format",
188 input = escape::Bytes(input)
189 )
190 })?;
191 let span = parsed.into_full().with_context(|| {
192 err!(
193 "failed to parse {input:?} in the \"friendly\" format",
194 input = escape::Bytes(input)
195 )
196 })?;
197 Ok(span)
198 }
199
200 pub fn parse_duration<I: AsRef<[u8]>>(
236 &self,
237 input: I,
238 ) -> Result<SignedDuration, Error> {
239 let input = input.as_ref();
240 let parsed = self.parse_to_duration(input).with_context(|| {
241 err!(
242 "failed to parse {input:?} in the \"friendly\" format",
243 input = escape::Bytes(input)
244 )
245 })?;
246 let sdur = parsed.into_full().with_context(|| {
247 err!(
248 "failed to parse {input:?} in the \"friendly\" format",
249 input = escape::Bytes(input)
250 )
251 })?;
252 Ok(sdur)
253 }
254
255 #[inline(always)]
256 fn parse_to_span<'i>(
257 &self,
258 input: &'i [u8],
259 ) -> Result<Parsed<'i, Span>, Error> {
260 if input.is_empty() {
261 return Err(err!("an empty string is not a valid duration"));
262 }
263 let (sign, input) =
266 if !input.first().map_or(false, |&b| matches!(b, b'+' | b'-')) {
267 (None, input)
268 } else {
269 let Parsed { value: sign, input } =
270 self.parse_prefix_sign(input);
271 (sign, input)
272 };
273
274 let Parsed { value, input } = self.parse_unit_value(input)?;
275 let Some(first_unit_value) = value else {
276 return Err(err!(
277 "parsing a friendly duration requires it to start \
278 with a unit value (a decimal integer) after an \
279 optional sign, but no integer was found",
280 ));
281 };
282 let Parsed { value: span, input } =
283 self.parse_units_to_span(input, first_unit_value)?;
284
285 let (sign, input) = if !input.first().map_or(false, is_whitespace) {
288 (sign.unwrap_or(t::Sign::N::<1>()), input)
289 } else {
290 let parsed = self.parse_suffix_sign(sign, input)?;
291 (parsed.value, parsed.input)
292 };
293 Ok(Parsed { value: span * i64::from(sign.get()), input })
294 }
295
296 #[inline(always)]
297 fn parse_to_duration<'i>(
298 &self,
299 input: &'i [u8],
300 ) -> Result<Parsed<'i, SignedDuration>, Error> {
301 if input.is_empty() {
302 return Err(err!("an empty string is not a valid duration"));
303 }
304 let (sign, input) =
307 if !input.first().map_or(false, |&b| matches!(b, b'+' | b'-')) {
308 (None, input)
309 } else {
310 let Parsed { value: sign, input } =
311 self.parse_prefix_sign(input);
312 (sign, input)
313 };
314
315 let Parsed { value, input } = self.parse_unit_value(input)?;
316 let Some(first_unit_value) = value else {
317 return Err(err!(
318 "parsing a friendly duration requires it to start \
319 with a unit value (a decimal integer) after an \
320 optional sign, but no integer was found",
321 ));
322 };
323 let Parsed { value: mut sdur, input } =
324 self.parse_units_to_duration(input, first_unit_value)?;
325
326 let (sign, input) = if !input.first().map_or(false, is_whitespace) {
329 (sign.unwrap_or(t::Sign::N::<1>()), input)
330 } else {
331 let parsed = self.parse_suffix_sign(sign, input)?;
332 (parsed.value, parsed.input)
333 };
334 if sign < 0 {
335 sdur = -sdur;
336 }
337
338 Ok(Parsed { value: sdur, input })
339 }
340
341 #[inline(always)]
342 fn parse_units_to_span<'i>(
343 &self,
344 mut input: &'i [u8],
345 first_unit_value: t::NoUnits,
346 ) -> Result<Parsed<'i, Span>, Error> {
347 let mut parsed_any_after_comma = true;
348 let mut prev_unit: Option<Unit> = None;
349 let mut value = first_unit_value;
350 let mut span = Span::new();
351 loop {
352 let parsed = self.parse_hms_maybe(input, value)?;
353 input = parsed.input;
354 if let Some(hms) = parsed.value {
355 if let Some(prev_unit) = prev_unit {
356 if prev_unit <= Unit::Hour {
357 return Err(err!(
358 "found 'HH:MM:SS' after unit {prev_unit}, \
359 but 'HH:MM:SS' can only appear after \
360 years, months, weeks or days",
361 prev_unit = prev_unit.singular(),
362 ));
363 }
364 }
365 span = set_span_unit_value(Unit::Hour, hms.hour, span)?;
366 span = set_span_unit_value(Unit::Minute, hms.minute, span)?;
367 span = if let Some(fraction) = hms.fraction {
368 fractional_time_to_span(
369 Unit::Second,
370 hms.second,
371 fraction,
372 span,
373 )?
374 } else {
375 set_span_unit_value(Unit::Second, hms.second, span)?
376 };
377 break;
378 }
379
380 let fraction =
381 if input.first().map_or(false, |&b| b == b'.' || b == b',') {
382 let parsed = parse_temporal_fraction(input)?;
383 input = parsed.input;
384 parsed.value
385 } else {
386 None
387 };
388
389 input = self.parse_optional_whitespace(input).input;
391
392 let parsed = self.parse_unit_designator(input)?;
394 input = parsed.input;
395 let unit = parsed.value;
396
397 if input.first().map_or(false, |&b| b == b',') {
402 input = self.parse_optional_comma(input)?.input;
403 parsed_any_after_comma = false;
404 }
405
406 if let Some(prev_unit) = prev_unit {
407 if prev_unit <= unit {
408 return Err(err!(
409 "found value {value:?} with unit {unit} \
410 after unit {prev_unit}, but units must be \
411 written from largest to smallest \
412 (and they can't be repeated)",
413 unit = unit.singular(),
414 prev_unit = prev_unit.singular(),
415 ));
416 }
417 }
418 prev_unit = Some(unit);
419
420 if let Some(fraction) = fraction {
421 span = fractional_time_to_span(unit, value, fraction, span)?;
422 break;
426 } else {
427 span = set_span_unit_value(unit, value, span)?;
428 }
429
430 let after_whitespace = self.parse_optional_whitespace(input).input;
434 let parsed = self.parse_unit_value(after_whitespace)?;
435 value = match parsed.value {
436 None => break,
437 Some(value) => value,
438 };
439 input = parsed.input;
440 parsed_any_after_comma = true;
441 }
442 if !parsed_any_after_comma {
443 return Err(err!(
444 "found comma at the end of duration, \
445 but a comma indicates at least one more \
446 unit follows and none were found after \
447 {prev_unit}",
448 prev_unit = prev_unit.unwrap().plural(),
451 ));
452 }
453 Ok(Parsed { value: span, input })
454 }
455
456 #[inline(always)]
457 fn parse_units_to_duration<'i>(
458 &self,
459 mut input: &'i [u8],
460 first_unit_value: t::NoUnits,
461 ) -> Result<Parsed<'i, SignedDuration>, Error> {
462 let mut parsed_any_after_comma = true;
463 let mut prev_unit: Option<Unit> = None;
464 let mut value = first_unit_value;
465 let mut sdur = SignedDuration::ZERO;
466 loop {
467 let parsed = self.parse_hms_maybe(input, value)?;
468 input = parsed.input;
469 if let Some(hms) = parsed.value {
470 if let Some(prev_unit) = prev_unit {
471 if prev_unit <= Unit::Hour {
472 return Err(err!(
473 "found 'HH:MM:SS' after unit {prev_unit}, \
474 but 'HH:MM:SS' can only appear after \
475 years, months, weeks or days",
476 prev_unit = prev_unit.singular(),
477 ));
478 }
479 }
480 sdur = sdur
481 .checked_add(duration_unit_value(Unit::Hour, hms.hour)?)
482 .ok_or_else(|| {
483 err!(
484 "accumulated `SignedDuration` overflowed when \
485 adding {value} of unit hour",
486 )
487 })?;
488 sdur = sdur
489 .checked_add(duration_unit_value(
490 Unit::Minute,
491 hms.minute,
492 )?)
493 .ok_or_else(|| {
494 err!(
495 "accumulated `SignedDuration` overflowed when \
496 adding {value} of unit minute",
497 )
498 })?;
499 sdur = sdur
500 .checked_add(duration_unit_value(
501 Unit::Second,
502 hms.second,
503 )?)
504 .ok_or_else(|| {
505 err!(
506 "accumulated `SignedDuration` overflowed when \
507 adding {value} of unit second",
508 )
509 })?;
510 if let Some(f) = hms.fraction {
511 let f = fractional_time_to_duration(Unit::Second, f)?;
513 sdur = sdur.checked_add(f).ok_or_else(|| err!(""))?;
514 };
515 break;
516 }
517
518 let fraction =
519 if input.first().map_or(false, |&b| b == b'.' || b == b',') {
520 let parsed = parse_temporal_fraction(input)?;
521 input = parsed.input;
522 parsed.value
523 } else {
524 None
525 };
526
527 input = self.parse_optional_whitespace(input).input;
529
530 let parsed = self.parse_unit_designator(input)?;
532 input = parsed.input;
533 let unit = parsed.value;
534
535 if input.first().map_or(false, |&b| b == b',') {
540 input = self.parse_optional_comma(input)?.input;
541 parsed_any_after_comma = false;
542 }
543
544 if let Some(prev_unit) = prev_unit {
545 if prev_unit <= unit {
546 return Err(err!(
547 "found value {value:?} with unit {unit} \
548 after unit {prev_unit}, but units must be \
549 written from largest to smallest \
550 (and they can't be repeated)",
551 unit = unit.singular(),
552 prev_unit = prev_unit.singular(),
553 ));
554 }
555 }
556 prev_unit = Some(unit);
557
558 sdur = sdur
559 .checked_add(duration_unit_value(unit, value)?)
560 .ok_or_else(|| {
561 err!(
562 "accumulated `SignedDuration` overflowed when adding \
563 {value} of unit {unit}",
564 unit = unit.singular(),
565 )
566 })?;
567 if let Some(f) = fraction {
568 let f = fractional_time_to_duration(unit, f)?;
569 sdur = sdur.checked_add(f).ok_or_else(|| err!(""))?;
570 break;
574 }
575
576 let after_whitespace = self.parse_optional_whitespace(input).input;
580 let parsed = self.parse_unit_value(after_whitespace)?;
581 value = match parsed.value {
582 None => break,
583 Some(value) => value,
584 };
585 input = parsed.input;
586 parsed_any_after_comma = true;
587 }
588 if !parsed_any_after_comma {
589 return Err(err!(
590 "found comma at the end of duration, \
591 but a comma indicates at least one more \
592 unit follows and none were found after \
593 {prev_unit}",
594 prev_unit = prev_unit.unwrap().plural(),
597 ));
598 }
599 Ok(Parsed { value: sdur, input })
600 }
601
602 #[inline(always)]
608 fn parse_hms_maybe<'i>(
609 &self,
610 input: &'i [u8],
611 hour: t::NoUnits,
612 ) -> Result<Parsed<'i, Option<HMS>>, Error> {
613 if !input.first().map_or(false, |&b| b == b':') {
614 return Ok(Parsed { input, value: None });
615 }
616 let Parsed { input, value } = self.parse_hms(&input[1..], hour)?;
617 Ok(Parsed { input, value: Some(value) })
618 }
619
620 #[inline(never)]
630 fn parse_hms<'i>(
631 &self,
632 input: &'i [u8],
633 hour: t::NoUnits,
634 ) -> Result<Parsed<'i, HMS>, Error> {
635 let Parsed { input, value } = self.parse_unit_value(input)?;
636 let Some(minute) = value else {
637 return Err(err!(
638 "expected to parse minute in 'HH:MM:SS' format \
639 following parsed hour of {hour}",
640 ));
641 };
642 if !input.first().map_or(false, |&b| b == b':') {
643 return Err(err!(
644 "when parsing 'HH:MM:SS' format, expected to \
645 see a ':' after the parsed minute of {minute}",
646 ));
647 }
648 let input = &input[1..];
649 let Parsed { input, value } = self.parse_unit_value(input)?;
650 let Some(second) = value else {
651 return Err(err!(
652 "expected to parse second in 'HH:MM:SS' format \
653 following parsed minute of {minute}",
654 ));
655 };
656 let (fraction, input) =
657 if input.first().map_or(false, |&b| b == b'.' || b == b',') {
658 let parsed = parse_temporal_fraction(input)?;
659 (parsed.value, parsed.input)
660 } else {
661 (None, input)
662 };
663 let hms = HMS { hour, minute, second, fraction };
664 Ok(Parsed { input, value: hms })
665 }
666
667 #[inline(always)]
676 fn parse_unit_value<'i>(
677 &self,
678 mut input: &'i [u8],
679 ) -> Result<Parsed<'i, Option<t::NoUnits>>, Error> {
680 const MAX_I64_DIGITS: usize = 19;
682
683 let mut digit_count = 0;
684 let mut n: i64 = 0;
685 while digit_count <= MAX_I64_DIGITS
686 && input.get(digit_count).map_or(false, u8::is_ascii_digit)
687 {
688 let byte = input[digit_count];
689 digit_count += 1;
690
691 let digit = match byte.checked_sub(b'0') {
698 None => {
699 return Err(err!(
700 "invalid digit, expected 0-9 but got {}",
701 escape::Byte(byte),
702 ));
703 }
704 Some(digit) if digit > 9 => {
705 return Err(err!(
706 "invalid digit, expected 0-9 but got {}",
707 escape::Byte(byte),
708 ))
709 }
710 Some(digit) => {
711 debug_assert!((0..=9).contains(&digit));
712 i64::from(digit)
713 }
714 };
715 n = n
716 .checked_mul(10)
717 .and_then(|n| n.checked_add(digit))
718 .ok_or_else(|| {
719 err!(
720 "number '{}' too big to parse into 64-bit integer",
721 escape::Bytes(&input[..digit_count]),
722 )
723 })?;
724 }
725 if digit_count == 0 {
726 return Ok(Parsed { value: None, input });
727 }
728
729 input = &input[digit_count..];
730 let value = t::NoUnits::new(n).unwrap();
732 Ok(Parsed { value: Some(value), input })
733 }
734
735 #[inline(always)]
742 fn parse_unit_designator<'i>(
743 &self,
744 input: &'i [u8],
745 ) -> Result<Parsed<'i, Unit>, Error> {
746 let Some((unit, len)) = parser_label::find(input) else {
747 if input.is_empty() {
748 return Err(err!(
749 "expected to find unit designator suffix \
750 (e.g., 'years' or 'secs'), \
751 but found end of input",
752 ));
753 } else {
754 return Err(err!(
755 "expected to find unit designator suffix \
756 (e.g., 'years' or 'secs'), \
757 but found input beginning with {found:?} instead",
758 found = escape::Bytes(&input[..input.len().min(20)]),
759 ));
760 }
761 };
762 Ok(Parsed { value: unit, input: &input[len..] })
763 }
764
765 #[inline(never)]
770 fn parse_prefix_sign<'i>(
771 &self,
772 input: &'i [u8],
773 ) -> Parsed<'i, Option<t::Sign>> {
774 let Some(sign) = input.first().copied() else {
775 return Parsed { value: None, input };
776 };
777 let sign = if sign == b'+' {
778 t::Sign::N::<1>()
779 } else if sign == b'-' {
780 t::Sign::N::<-1>()
781 } else {
782 return Parsed { value: None, input };
783 };
784 Parsed { value: Some(sign), input: &input[1..] }
785 }
786
787 #[inline(never)]
801 fn parse_suffix_sign<'i>(
802 &self,
803 prefix_sign: Option<t::Sign>,
804 mut input: &'i [u8],
805 ) -> Result<Parsed<'i, t::Sign>, Error> {
806 if !input.first().map_or(false, is_whitespace) {
807 let sign = prefix_sign.unwrap_or(t::Sign::N::<1>());
808 return Ok(Parsed { value: sign, input });
809 }
810 input = self.parse_optional_whitespace(&input[1..]).input;
812 let (suffix_sign, input) = if input.starts_with(b"ago") {
813 (Some(t::Sign::N::<-1>()), &input[3..])
814 } else {
815 (None, input)
816 };
817 let sign = match (prefix_sign, suffix_sign) {
818 (Some(_), Some(_)) => {
819 return Err(err!(
820 "expected to find either a prefix sign (+/-) or \
821 a suffix sign (ago), but found both",
822 ))
823 }
824 (Some(sign), None) => sign,
825 (None, Some(sign)) => sign,
826 (None, None) => t::Sign::N::<1>(),
827 };
828 Ok(Parsed { value: sign, input })
829 }
830
831 #[inline(never)]
841 fn parse_optional_comma<'i>(
842 &self,
843 mut input: &'i [u8],
844 ) -> Result<Parsed<'i, ()>, Error> {
845 if !input.first().map_or(false, |&b| b == b',') {
846 return Ok(Parsed { value: (), input });
847 }
848 input = &input[1..];
849 if input.is_empty() {
850 return Err(err!(
851 "expected whitespace after comma, but found end of input"
852 ));
853 }
854 if !is_whitespace(&input[0]) {
855 return Err(err!(
856 "expected whitespace after comma, but found {found:?}",
857 found = escape::Byte(input[0]),
858 ));
859 }
860 Ok(Parsed { value: (), input: &input[1..] })
861 }
862
863 #[inline(always)]
865 fn parse_optional_whitespace<'i>(
866 &self,
867 mut input: &'i [u8],
868 ) -> Parsed<'i, ()> {
869 while input.first().map_or(false, is_whitespace) {
870 input = &input[1..];
871 }
872 Parsed { value: (), input }
873 }
874}
875
876#[derive(Debug)]
878struct HMS {
879 hour: t::NoUnits,
880 minute: t::NoUnits,
881 second: t::NoUnits,
882 fraction: Option<t::SubsecNanosecond>,
883}
884
885#[inline(always)]
890fn set_span_unit_value(
891 unit: Unit,
892 value: t::NoUnits,
893 mut span: Span,
894) -> Result<Span, Error> {
895 if unit <= Unit::Hour {
896 let result = span.try_units_ranged(unit, value).with_context(|| {
897 err!(
898 "failed to set value {value:?} \
899 as {unit} unit on span",
900 unit = Unit::from(unit).singular(),
901 )
902 });
903 span = match result {
908 Ok(span) => span,
909 Err(_) => fractional_time_to_span(
910 unit,
911 value,
912 t::SubsecNanosecond::N::<0>(),
913 span,
914 )?,
915 };
916 } else {
917 span = span.try_units_ranged(unit, value).with_context(|| {
918 err!(
919 "failed to set value {value:?} \
920 as {unit} unit on span",
921 unit = Unit::from(unit).singular(),
922 )
923 })?;
924 }
925 Ok(span)
926}
927
928#[inline(always)]
935fn duration_unit_value(
936 unit: Unit,
937 value: t::NoUnits,
938) -> Result<SignedDuration, Error> {
939 let sdur = match unit {
948 Unit::Hour => {
949 let seconds =
950 value.checked_mul(t::SECONDS_PER_HOUR).ok_or_else(|| {
951 err!("converting {value} hours to seconds overflows i64")
952 })?;
953 SignedDuration::from_secs(seconds.get())
954 }
955 Unit::Minute => {
956 let seconds = value.try_checked_mul(
957 "minutes-to-seconds",
958 t::SECONDS_PER_MINUTE,
959 )?;
960 SignedDuration::from_secs(seconds.get())
961 }
962 Unit::Second => SignedDuration::from_secs(value.get()),
963 Unit::Millisecond => SignedDuration::from_millis(value.get()),
964 Unit::Microsecond => SignedDuration::from_micros(value.get()),
965 Unit::Nanosecond => SignedDuration::from_nanos(value.get()),
966 unsupported => {
967 return Err(err!(
968 "parsing {unit} units into a `SignedDuration` is not supported \
969 (perhaps try parsing into a `Span` instead)",
970 unit = unsupported.singular(),
971 ));
972 }
973 };
974 Ok(sdur)
975}
976
977#[inline(always)]
979fn is_whitespace(byte: &u8) -> bool {
980 matches!(*byte, b' ' | b'\t' | b'\n' | b'\r' | b'\x0C')
981}
982
983#[cfg(test)]
984mod tests {
985 use super::*;
986
987 #[test]
988 fn parse_span_basic() {
989 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
990
991 insta::assert_snapshot!(p("5 years"), @"P5Y");
992 insta::assert_snapshot!(p("5 years 4 months"), @"P5Y4M");
993 insta::assert_snapshot!(p("5 years 4 months 3 hours"), @"P5Y4MT3H");
994 insta::assert_snapshot!(p("5 years, 4 months, 3 hours"), @"P5Y4MT3H");
995
996 insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
997 insta::assert_snapshot!(p("5 days 01:02:03"), @"P5DT1H2M3S");
998 insta::assert_snapshot!(p("5 days, 01:02:03"), @"P5DT1H2M3S");
1000 insta::assert_snapshot!(p("3yrs 5 days 01:02:03"), @"P3Y5DT1H2M3S");
1001 insta::assert_snapshot!(p("3yrs 5 days, 01:02:03"), @"P3Y5DT1H2M3S");
1002 insta::assert_snapshot!(
1003 p("3yrs 5 days, 01:02:03.123456789"),
1004 @"P3Y5DT1H2M3.123456789S",
1005 );
1006 insta::assert_snapshot!(p("999:999:999"), @"PT999H999M999S");
1007 }
1008
1009 #[test]
1010 fn parse_span_fractional() {
1011 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
1012
1013 insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
1014 insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
1015 insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
1016 insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
1017 insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
1018
1019 insta::assert_snapshot!(p("1d 1.5hrs"), @"P1DT1H30M");
1020 insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
1021 insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
1022 insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
1023 insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
1024
1025 insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
1026 }
1027
1028 #[test]
1029 fn parse_span_boundaries() {
1030 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
1031
1032 insta::assert_snapshot!(p("19998 years"), @"P19998Y");
1033 insta::assert_snapshot!(p("19998 years ago"), @"-P19998Y");
1034 insta::assert_snapshot!(p("239976 months"), @"P239976M");
1035 insta::assert_snapshot!(p("239976 months ago"), @"-P239976M");
1036 insta::assert_snapshot!(p("1043497 weeks"), @"P1043497W");
1037 insta::assert_snapshot!(p("1043497 weeks ago"), @"-P1043497W");
1038 insta::assert_snapshot!(p("7304484 days"), @"P7304484D");
1039 insta::assert_snapshot!(p("7304484 days ago"), @"-P7304484D");
1040 insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
1041 insta::assert_snapshot!(p("175307616 hours ago"), @"-PT175307616H");
1042 insta::assert_snapshot!(p("10518456960 minutes"), @"PT10518456960M");
1043 insta::assert_snapshot!(p("10518456960 minutes ago"), @"-PT10518456960M");
1044 insta::assert_snapshot!(p("631107417600 seconds"), @"PT631107417600S");
1045 insta::assert_snapshot!(p("631107417600 seconds ago"), @"-PT631107417600S");
1046 insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT631107417600S");
1047 insta::assert_snapshot!(p("631107417600000 milliseconds ago"), @"-PT631107417600S");
1048 insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT631107417600S");
1049 insta::assert_snapshot!(p("631107417600000000 microseconds ago"), @"-PT631107417600S");
1050 insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT9223372036.854775807S");
1051 insta::assert_snapshot!(p("9223372036854775807 nanoseconds ago"), @"-PT9223372036.854775807S");
1052
1053 insta::assert_snapshot!(p("175307617 hours"), @"PT175307616H60M");
1054 insta::assert_snapshot!(p("175307617 hours ago"), @"-PT175307616H60M");
1055 insta::assert_snapshot!(p("10518456961 minutes"), @"PT10518456960M60S");
1056 insta::assert_snapshot!(p("10518456961 minutes ago"), @"-PT10518456960M60S");
1057 insta::assert_snapshot!(p("631107417601 seconds"), @"PT631107417601S");
1058 insta::assert_snapshot!(p("631107417601 seconds ago"), @"-PT631107417601S");
1059 insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT631107417600.001S");
1060 insta::assert_snapshot!(p("631107417600001 milliseconds ago"), @"-PT631107417600.001S");
1061 insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT631107417600.000001S");
1062 insta::assert_snapshot!(p("631107417600000001 microseconds ago"), @"-PT631107417600.000001S");
1063 }
1066
1067 #[test]
1068 fn err_span_basic() {
1069 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
1070
1071 insta::assert_snapshot!(
1072 p(""),
1073 @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
1074 );
1075 insta::assert_snapshot!(
1076 p(" "),
1077 @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"###,
1078 );
1079 insta::assert_snapshot!(
1080 p("a"),
1081 @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"###,
1082 );
1083 insta::assert_snapshot!(
1084 p("2 months 1 year"),
1085 @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)"###,
1086 );
1087 insta::assert_snapshot!(
1088 p("1 year 1 mont"),
1089 @r###"failed to parse "1 year 1 mont" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "nt" remains (expected no unparsed input)"###,
1090 );
1091 insta::assert_snapshot!(
1092 p("2 months,"),
1093 @r###"failed to parse "2 months," in the "friendly" format: expected whitespace after comma, but found end of input"###,
1094 );
1095 insta::assert_snapshot!(
1096 p("2 months, "),
1097 @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 and none were found after months"###,
1098 );
1099 insta::assert_snapshot!(
1100 p("2 months ,"),
1101 @r###"failed to parse "2 months ," in the "friendly" format: parsed value 'P2M', but unparsed input "," remains (expected no unparsed input)"###,
1102 );
1103 }
1104
1105 #[test]
1106 fn err_span_sign() {
1107 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
1108
1109 insta::assert_snapshot!(
1110 p("1yago"),
1111 @r###"failed to parse "1yago" in the "friendly" format: parsed value 'P1Y', but unparsed input "ago" remains (expected no unparsed input)"###,
1112 );
1113 insta::assert_snapshot!(
1114 p("1 year 1 monthago"),
1115 @r###"failed to parse "1 year 1 monthago" in the "friendly" format: parsed value 'P1Y1M', but unparsed input "ago" remains (expected no unparsed input)"###,
1116 );
1117 insta::assert_snapshot!(
1118 p("+1 year 1 month ago"),
1119 @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"###,
1120 );
1121 insta::assert_snapshot!(
1122 p("-1 year 1 month ago"),
1123 @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"###,
1124 );
1125 }
1126
1127 #[test]
1128 fn err_span_overflow_fraction() {
1129 let p = |s: &str| SpanParser::new().parse_span(s).unwrap();
1130 let pe = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
1131
1132 insta::assert_snapshot!(
1133 pe("640330789636854776 micros"),
1137 @r###"failed to parse "640330789636854776 micros" in the "friendly" format: failed to set nanosecond value 9223372036854776000 on span determined from 640330789636854776.0: parameter 'nanoseconds' with value 9223372036854776000 is not in the required range of -9223372036854775807..=9223372036854775807"###,
1138 );
1139 insta::assert_snapshot!(
1141 p("640330789636854775 micros"),
1142 @"PT640330789636.854775S"
1143 );
1144
1145 insta::assert_snapshot!(
1146 pe("640330789636854775.808 micros"),
1150 @r###"failed to parse "640330789636854775.808 micros" in the "friendly" format: failed to set nanosecond value 9223372036854775808 on span determined from 640330789636854775.808000000: parameter 'nanoseconds' with value 9223372036854775808 is not in the required range of -9223372036854775807..=9223372036854775807"###,
1151 );
1152 insta::assert_snapshot!(
1154 p("640330789636854775.807 micros"),
1155 @"PT640330789636.854775807S"
1156 );
1157 }
1158
1159 #[test]
1160 fn err_span_overflow_units() {
1161 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
1162
1163 insta::assert_snapshot!(
1164 p("19999 years"),
1165 @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"###,
1166 );
1167 insta::assert_snapshot!(
1168 p("19999 years ago"),
1169 @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"###,
1170 );
1171
1172 insta::assert_snapshot!(
1173 p("239977 months"),
1174 @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"###,
1175 );
1176 insta::assert_snapshot!(
1177 p("239977 months ago"),
1178 @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"###,
1179 );
1180
1181 insta::assert_snapshot!(
1182 p("1043498 weeks"),
1183 @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"###,
1184 );
1185 insta::assert_snapshot!(
1186 p("1043498 weeks ago"),
1187 @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"###,
1188 );
1189
1190 insta::assert_snapshot!(
1191 p("7304485 days"),
1192 @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"###,
1193 );
1194 insta::assert_snapshot!(
1195 p("7304485 days ago"),
1196 @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"###,
1197 );
1198
1199 insta::assert_snapshot!(
1200 p("9223372036854775808 nanoseconds"),
1201 @r###"failed to parse "9223372036854775808 nanoseconds" in the "friendly" format: number '9223372036854775808' too big to parse into 64-bit integer"###,
1202 );
1203 insta::assert_snapshot!(
1204 p("9223372036854775808 nanoseconds ago"),
1205 @r###"failed to parse "9223372036854775808 nanoseconds ago" in the "friendly" format: number '9223372036854775808' too big to parse into 64-bit integer"###,
1206 );
1207 }
1208
1209 #[test]
1210 fn err_span_fraction() {
1211 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
1212
1213 insta::assert_snapshot!(
1214 p("1.5 years"),
1215 @r###"failed to parse "1.5 years" in the "friendly" format: fractional year units are not allowed"###,
1216 );
1217 insta::assert_snapshot!(
1218 p("1.5 nanos"),
1219 @r###"failed to parse "1.5 nanos" in the "friendly" format: fractional nanosecond units are not allowed"###,
1220 );
1221 }
1222
1223 #[test]
1224 fn err_span_hms() {
1225 let p = |s: &str| SpanParser::new().parse_span(s).unwrap_err();
1226
1227 insta::assert_snapshot!(
1228 p("05:"),
1229 @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
1230 );
1231 insta::assert_snapshot!(
1232 p("05:06"),
1233 @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"###,
1234 );
1235 insta::assert_snapshot!(
1236 p("05:06:"),
1237 @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
1238 );
1239 insta::assert_snapshot!(
1240 p("2 hours, 05:06:07"),
1241 @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"###,
1242 );
1243 }
1244
1245 #[test]
1246 fn parse_duration_basic() {
1247 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1248
1249 insta::assert_snapshot!(p("1 hour, 2 minutes, 3 seconds"), @"PT1H2M3S");
1250 insta::assert_snapshot!(p("01:02:03"), @"PT1H2M3S");
1251 insta::assert_snapshot!(p("999:999:999"), @"PT1015H55M39S");
1252 }
1253
1254 #[test]
1255 fn parse_duration_negate() {
1256 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1257 let perr = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1258
1259 insta::assert_snapshot!(
1260 p("9223372036854775807s"),
1261 @"PT2562047788015215H30M7S",
1262 );
1263 insta::assert_snapshot!(
1264 perr("9223372036854775808s"),
1265 @r###"failed to parse "9223372036854775808s" in the "friendly" format: number '9223372036854775808' too big to parse into 64-bit integer"###,
1266 );
1267 insta::assert_snapshot!(
1276 perr("-9223372036854775808s"),
1277 @r###"failed to parse "-9223372036854775808s" in the "friendly" format: number '9223372036854775808' too big to parse into 64-bit integer"###,
1278 );
1279 }
1280
1281 #[test]
1282 fn parse_duration_fractional() {
1283 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1284
1285 insta::assert_snapshot!(p("1.5hrs"), @"PT1H30M");
1286 insta::assert_snapshot!(p("1.5mins"), @"PT1M30S");
1287 insta::assert_snapshot!(p("1.5secs"), @"PT1.5S");
1288 insta::assert_snapshot!(p("1.5msecs"), @"PT0.0015S");
1289 insta::assert_snapshot!(p("1.5µsecs"), @"PT0.0000015S");
1290
1291 insta::assert_snapshot!(p("1h 1.5mins"), @"PT1H1M30S");
1292 insta::assert_snapshot!(p("1m 1.5secs"), @"PT1M1.5S");
1293 insta::assert_snapshot!(p("1s 1.5msecs"), @"PT1.0015S");
1294 insta::assert_snapshot!(p("1ms 1.5µsecs"), @"PT0.0010015S");
1295
1296 insta::assert_snapshot!(p("1s2000ms"), @"PT3S");
1297 }
1298
1299 #[test]
1300 fn parse_duration_boundaries() {
1301 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1302 let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1303
1304 insta::assert_snapshot!(p("175307616 hours"), @"PT175307616H");
1305 insta::assert_snapshot!(p("175307616 hours ago"), @"-PT175307616H");
1306 insta::assert_snapshot!(p("10518456960 minutes"), @"PT175307616H");
1307 insta::assert_snapshot!(p("10518456960 minutes ago"), @"-PT175307616H");
1308 insta::assert_snapshot!(p("631107417600 seconds"), @"PT175307616H");
1309 insta::assert_snapshot!(p("631107417600 seconds ago"), @"-PT175307616H");
1310 insta::assert_snapshot!(p("631107417600000 milliseconds"), @"PT175307616H");
1311 insta::assert_snapshot!(p("631107417600000 milliseconds ago"), @"-PT175307616H");
1312 insta::assert_snapshot!(p("631107417600000000 microseconds"), @"PT175307616H");
1313 insta::assert_snapshot!(p("631107417600000000 microseconds ago"), @"-PT175307616H");
1314 insta::assert_snapshot!(p("9223372036854775807 nanoseconds"), @"PT2562047H47M16.854775807S");
1315 insta::assert_snapshot!(p("9223372036854775807 nanoseconds ago"), @"-PT2562047H47M16.854775807S");
1316
1317 insta::assert_snapshot!(p("175307617 hours"), @"PT175307617H");
1318 insta::assert_snapshot!(p("175307617 hours ago"), @"-PT175307617H");
1319 insta::assert_snapshot!(p("10518456961 minutes"), @"PT175307616H1M");
1320 insta::assert_snapshot!(p("10518456961 minutes ago"), @"-PT175307616H1M");
1321 insta::assert_snapshot!(p("631107417601 seconds"), @"PT175307616H1S");
1322 insta::assert_snapshot!(p("631107417601 seconds ago"), @"-PT175307616H1S");
1323 insta::assert_snapshot!(p("631107417600001 milliseconds"), @"PT175307616H0.001S");
1324 insta::assert_snapshot!(p("631107417600001 milliseconds ago"), @"-PT175307616H0.001S");
1325 insta::assert_snapshot!(p("631107417600000001 microseconds"), @"PT175307616H0.000001S");
1326 insta::assert_snapshot!(p("631107417600000001 microseconds ago"), @"-PT175307616H0.000001S");
1327 insta::assert_snapshot!(p("2562047788015215hours"), @"PT2562047788015215H");
1334 insta::assert_snapshot!(p("-2562047788015215hours"), @"-PT2562047788015215H");
1335 insta::assert_snapshot!(
1336 pe("2562047788015216hrs"),
1337 @r###"failed to parse "2562047788015216hrs" in the "friendly" format: converting 2562047788015216 hours to seconds overflows i64"###,
1338 );
1339
1340 insta::assert_snapshot!(p("153722867280912930minutes"), @"PT2562047788015215H30M");
1341 insta::assert_snapshot!(p("153722867280912930minutes ago"), @"-PT2562047788015215H30M");
1342 insta::assert_snapshot!(
1343 pe("153722867280912931mins"),
1344 @r###"failed to parse "153722867280912931mins" in the "friendly" format: parameter 'minutes-to-seconds' with value 60 is not in the required range of -9223372036854775808..=9223372036854775807"###,
1345 );
1346
1347 insta::assert_snapshot!(p("9223372036854775807seconds"), @"PT2562047788015215H30M7S");
1348 insta::assert_snapshot!(p("-9223372036854775807seconds"), @"-PT2562047788015215H30M7S");
1349 insta::assert_snapshot!(
1350 pe("9223372036854775808s"),
1351 @r###"failed to parse "9223372036854775808s" in the "friendly" format: number '9223372036854775808' too big to parse into 64-bit integer"###,
1352 );
1353 insta::assert_snapshot!(
1354 pe("-9223372036854775808s"),
1355 @r###"failed to parse "-9223372036854775808s" in the "friendly" format: number '9223372036854775808' too big to parse into 64-bit integer"###,
1356 );
1357 }
1358
1359 #[test]
1360 fn err_duration_basic() {
1361 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1362
1363 insta::assert_snapshot!(
1364 p(""),
1365 @r###"failed to parse "" in the "friendly" format: an empty string is not a valid duration"###,
1366 );
1367 insta::assert_snapshot!(
1368 p(" "),
1369 @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"###,
1370 );
1371 insta::assert_snapshot!(
1372 p("5"),
1373 @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"###,
1374 );
1375 insta::assert_snapshot!(
1376 p("a"),
1377 @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"###,
1378 );
1379 insta::assert_snapshot!(
1380 p("2 minutes 1 hour"),
1381 @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)"###,
1382 );
1383 insta::assert_snapshot!(
1384 p("1 hour 1 minut"),
1385 @r###"failed to parse "1 hour 1 minut" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ut" remains (expected no unparsed input)"###,
1386 );
1387 insta::assert_snapshot!(
1388 p("2 minutes,"),
1389 @r###"failed to parse "2 minutes," in the "friendly" format: expected whitespace after comma, but found end of input"###,
1390 );
1391 insta::assert_snapshot!(
1392 p("2 minutes, "),
1393 @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 and none were found after minutes"###,
1394 );
1395 insta::assert_snapshot!(
1396 p("2 minutes ,"),
1397 @r###"failed to parse "2 minutes ," in the "friendly" format: parsed value 'PT2M', but unparsed input "," remains (expected no unparsed input)"###,
1398 );
1399 }
1400
1401 #[test]
1402 fn err_duration_sign() {
1403 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1404
1405 insta::assert_snapshot!(
1406 p("1hago"),
1407 @r###"failed to parse "1hago" in the "friendly" format: parsed value 'PT1H', but unparsed input "ago" remains (expected no unparsed input)"###,
1408 );
1409 insta::assert_snapshot!(
1410 p("1 hour 1 minuteago"),
1411 @r###"failed to parse "1 hour 1 minuteago" in the "friendly" format: parsed value 'PT1H1M', but unparsed input "ago" remains (expected no unparsed input)"###,
1412 );
1413 insta::assert_snapshot!(
1414 p("+1 hour 1 minute ago"),
1415 @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"###,
1416 );
1417 insta::assert_snapshot!(
1418 p("-1 hour 1 minute ago"),
1419 @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"###,
1420 );
1421 }
1422
1423 #[test]
1424 fn err_duration_overflow_fraction() {
1425 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap();
1426 let pe = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1427
1428 insta::assert_snapshot!(
1429 pe("9223372036854775808 micros"),
1432 @r###"failed to parse "9223372036854775808 micros" in the "friendly" format: number '9223372036854775808' too big to parse into 64-bit integer"###,
1433 );
1434 insta::assert_snapshot!(
1436 p("9223372036854775807 micros"),
1437 @"PT2562047788H54.775807S"
1438 );
1439 }
1440
1441 #[test]
1442 fn err_duration_fraction() {
1443 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1444
1445 insta::assert_snapshot!(
1446 p("1.5 nanos"),
1447 @r###"failed to parse "1.5 nanos" in the "friendly" format: fractional nanosecond units are not allowed"###,
1448 );
1449 }
1450
1451 #[test]
1452 fn err_duration_hms() {
1453 let p = |s: &str| SpanParser::new().parse_duration(s).unwrap_err();
1454
1455 insta::assert_snapshot!(
1456 p("05:"),
1457 @r###"failed to parse "05:" in the "friendly" format: expected to parse minute in 'HH:MM:SS' format following parsed hour of 5"###,
1458 );
1459 insta::assert_snapshot!(
1460 p("05:06"),
1461 @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"###,
1462 );
1463 insta::assert_snapshot!(
1464 p("05:06:"),
1465 @r###"failed to parse "05:06:" in the "friendly" format: expected to parse second in 'HH:MM:SS' format following parsed minute of 6"###,
1466 );
1467 insta::assert_snapshot!(
1468 p("2 hours, 05:06:07"),
1469 @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"###,
1470 );
1471 }
1472}