1#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
46#[non_exhaustive]
47pub struct Duration {
48 seconds: i64,
54
55 nanos: i32,
64}
65
66#[derive(thiserror::Error, Debug)]
84#[non_exhaustive]
85pub enum DurationError {
86 #[error("seconds and/or nanoseconds out of range")]
88 OutOfRange,
89
90 #[error("if seconds and nanoseconds are not zero, they must have the same sign")]
92 MismatchedSigns,
93
94 #[error("cannot deserialize the duration: {0}")]
96 Deserialize(#[source] BoxedError),
97}
98
99type BoxedError = Box<dyn std::error::Error + Send + Sync>;
100type Error = DurationError;
101
102impl Duration {
103 const NS: i32 = 1_000_000_000;
104
105 pub const MAX_SECONDS: i64 = 315_576_000_000;
107
108 pub const MIN_SECONDS: i64 = -Self::MAX_SECONDS;
110
111 pub const MAX_NANOS: i32 = Self::NS - 1;
113
114 pub const MIN_NANOS: i32 = -Self::MAX_NANOS;
116
117 pub fn new(seconds: i64, nanos: i32) -> Result<Self, Error> {
151 if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
152 return Err(Error::OutOfRange);
153 }
154 if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
155 return Err(Error::OutOfRange);
156 }
157 if (seconds != 0 && nanos != 0) && ((seconds < 0) != (nanos < 0)) {
158 return Err(Error::MismatchedSigns);
159 }
160 Ok(Self { seconds, nanos })
161 }
162
163 pub fn clamp(seconds: i64, nanos: i32) -> Self {
188 let mut seconds = seconds;
189 seconds = seconds.saturating_add((nanos / Self::NS) as i64);
190 let mut nanos = nanos % Self::NS;
191 if seconds > 0 && nanos < 0 {
192 seconds = seconds.saturating_sub(1);
193 nanos += Self::NS;
194 } else if seconds < 0 && nanos > 0 {
195 seconds = seconds.saturating_add(1);
196 nanos = -(Self::NS - nanos);
197 }
198 if seconds > Self::MAX_SECONDS {
199 return Self {
200 seconds: Self::MAX_SECONDS,
201 nanos: 0,
202 };
203 }
204 if seconds < Self::MIN_SECONDS {
205 return Self {
206 seconds: Self::MIN_SECONDS,
207 nanos: 0,
208 };
209 }
210 Self { seconds, nanos }
211 }
212
213 pub fn seconds(&self) -> i64 {
222 self.seconds
223 }
224
225 pub fn nanos(&self) -> i32 {
234 self.nanos
235 }
236}
237
238impl crate::message::Message for Duration {
239 fn typename() -> &'static str {
240 "type.googleapis.com/google.protobuf.Duration"
241 }
242
243 #[allow(private_interfaces)]
244 fn serializer() -> impl crate::message::MessageSerializer<Self> {
245 crate::message::ValueSerializer::<Self>::new()
246 }
247}
248
249impl From<Duration> for String {
258 fn from(duration: Duration) -> String {
259 let sign = if duration.seconds < 0 || duration.nanos < 0 {
260 "-"
261 } else {
262 ""
263 };
264 if duration.nanos == 0 {
265 return format!("{sign}{}s", duration.seconds.abs());
266 }
267 let ns = format!("{:09}", duration.nanos.abs());
268 format!(
269 "{sign}{}.{}s",
270 duration.seconds.abs(),
271 ns.trim_end_matches('0')
272 )
273 }
274}
275
276impl TryFrom<&str> for Duration {
287 type Error = DurationError;
288 fn try_from(value: &str) -> Result<Self, Self::Error> {
289 if !value.ends_with('s') {
290 return Err(DurationError::Deserialize("missing trailing 's'".into()));
291 }
292 let digits = &value[..(value.len() - 1)];
293 let (sign, digits) = if let Some(stripped) = digits.strip_prefix('-') {
294 (-1, stripped)
295 } else {
296 (1, &digits[0..])
297 };
298 let mut split = digits.splitn(2, '.');
299 let (seconds, nanos) = (split.next(), split.next());
300 let seconds = seconds
301 .map(str::parse::<i64>)
302 .transpose()
303 .map_err(|e| DurationError::Deserialize(e.into()))?
304 .unwrap_or(0);
305 let nanos = nanos
306 .map(|s| {
307 let pad = "000000000";
308 format!("{s}{}", &pad[s.len()..])
309 })
310 .map(|s| s.parse::<i32>())
311 .transpose()
312 .map_err(|e| DurationError::Deserialize(e.into()))?
313 .unwrap_or(0);
314
315 Duration::new(sign * seconds, sign as i32 * nanos)
316 }
317}
318
319impl TryFrom<&String> for Duration {
331 type Error = DurationError;
332 fn try_from(value: &String) -> Result<Self, Self::Error> {
333 Duration::try_from(value.as_str())
334 }
335}
336
337impl TryFrom<std::time::Duration> for Duration {
348 type Error = DurationError;
349
350 fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
351 if value.as_secs() > (i64::MAX as u64) {
352 return Err(Error::OutOfRange);
353 }
354 assert!(value.as_secs() <= (i64::MAX as u64));
355 assert!(value.subsec_nanos() <= (i32::MAX as u32));
356 Self::new(value.as_secs() as i64, value.subsec_nanos() as i32)
357 }
358}
359
360impl TryFrom<Duration> for std::time::Duration {
375 type Error = DurationError;
376
377 fn try_from(value: Duration) -> Result<Self, Self::Error> {
378 if value.seconds < 0 {
379 return Err(Error::OutOfRange);
380 }
381 if value.nanos < 0 {
382 return Err(Error::OutOfRange);
383 }
384 Ok(Self::new(value.seconds as u64, value.nanos as u32))
385 }
386}
387
388#[cfg(feature = "time")]
401#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
402impl TryFrom<time::Duration> for Duration {
403 type Error = DurationError;
404
405 fn try_from(value: time::Duration) -> Result<Self, Self::Error> {
406 Self::new(value.whole_seconds(), value.subsec_nanoseconds())
407 }
408}
409
410#[cfg(feature = "time")]
424#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
425impl From<Duration> for time::Duration {
426 fn from(value: Duration) -> Self {
427 Self::new(value.seconds(), value.nanos())
428 }
429}
430
431#[cfg(feature = "chrono")]
444#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
445impl TryFrom<chrono::Duration> for Duration {
446 type Error = DurationError;
447
448 fn try_from(value: chrono::Duration) -> Result<Self, Self::Error> {
449 Self::new(value.num_seconds(), value.subsec_nanos())
450 }
451}
452
453#[cfg(feature = "chrono")]
464#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
465impl From<Duration> for chrono::Duration {
466 fn from(value: Duration) -> Self {
467 Self::seconds(value.seconds) + Self::nanoseconds(value.nanos as i64)
468 }
469}
470
471#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
473impl serde::ser::Serialize for Duration {
474 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
475 where
476 S: serde::ser::Serializer,
477 {
478 let formatted = String::from(*self);
479 formatted.serialize(serializer)
480 }
481}
482
483struct DurationVisitor;
484
485impl serde::de::Visitor<'_> for DurationVisitor {
486 type Value = Duration;
487
488 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
489 formatter.write_str("a string with a duration in Google format ([sign]{seconds}.{nanos}s)")
490 }
491
492 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
493 where
494 E: serde::de::Error,
495 {
496 let d = Duration::try_from(value).map_err(E::custom)?;
497 Ok(d)
498 }
499}
500
501#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
503impl<'de> serde::de::Deserialize<'de> for Duration {
504 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
505 where
506 D: serde::Deserializer<'de>,
507 {
508 deserializer.deserialize_str(DurationVisitor)
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use super::*;
515 use serde_json::json;
516 use test_case::test_case;
517 type Result = std::result::Result<(), Box<dyn std::error::Error>>;
518
519 #[test]
521 fn zero() -> Result {
522 let proto = Duration {
523 seconds: 0,
524 nanos: 0,
525 };
526 let json = serde_json::to_value(proto)?;
527 let expected = json!(r#"0s"#);
528 assert_eq!(json, expected);
529 let roundtrip = serde_json::from_value::<Duration>(json)?;
530 assert_eq!(proto, roundtrip);
531 Ok(())
532 }
533
534 const SECONDS_IN_DAY: i64 = 24 * 60 * 60;
537 const SECONDS_IN_YEAR: i64 = 365 * SECONDS_IN_DAY + SECONDS_IN_DAY / 4;
540
541 #[test_case(10_000 * SECONDS_IN_YEAR , 0 ; "exactly 10,000 years")]
542 #[test_case(- 10_000 * SECONDS_IN_YEAR , 0 ; "exactly negative 10,000 years")]
543 #[test_case(10_000 * SECONDS_IN_YEAR , 999_999_999 ; "exactly 10,000 years and 999,999,999 nanos"
544 )]
545 #[test_case(- 10_000 * SECONDS_IN_YEAR , -999_999_999 ; "exactly negative 10,000 years and 999,999,999 nanos"
546 )]
547 #[test_case(0, 999_999_999 ; "exactly 999,999,999 nanos")]
548 #[test_case(0 , -999_999_999 ; "exactly negative 999,999,999 nanos")]
549 fn edge_of_range(seconds: i64, nanos: i32) -> Result {
550 let d = Duration::new(seconds, nanos)?;
551 assert_eq!(seconds, d.seconds());
552 assert_eq!(nanos, d.nanos());
553 Ok(())
554 }
555
556 #[test_case(10_000 * SECONDS_IN_YEAR + 1, 0 ; "more seconds than in 10,000 years")]
557 #[test_case(- 10_000 * SECONDS_IN_YEAR - 1, 0 ; "more negative seconds than in -10,000 years")]
558 #[test_case(0, 1_000_000_000 ; "too many positive nanoseconds")]
559 #[test_case(0, -1_000_000_000 ; "too many negative nanoseconds")]
560 fn out_of_range(seconds: i64, nanos: i32) -> Result {
561 let d = Duration::new(seconds, nanos);
562 assert!(matches!(d, Err(Error::OutOfRange)), "{d:?}");
563 Ok(())
564 }
565
566 #[test_case(1 , -1 ; "mismatched sign case 1")]
567 #[test_case(-1 , 1 ; "mismatched sign case 2")]
568 fn mismatched_sign(seconds: i64, nanos: i32) -> Result {
569 let d = Duration::new(seconds, nanos);
570 assert!(matches!(d, Err(Error::MismatchedSigns)), "{d:?}");
571 Ok(())
572 }
573
574 #[test_case(20_000 * SECONDS_IN_YEAR, 0, 10_000 * SECONDS_IN_YEAR, 0 ; "too many positive seconds"
575 )]
576 #[test_case(-20_000 * SECONDS_IN_YEAR, 0, -10_000 * SECONDS_IN_YEAR, 0 ; "too many negative seconds"
577 )]
578 #[test_case(10_000 * SECONDS_IN_YEAR - 1, 1_999_999_999, 10_000 * SECONDS_IN_YEAR, 999_999_999 ; "upper edge of range"
579 )]
580 #[test_case(-10_000 * SECONDS_IN_YEAR + 1, -1_999_999_999, -10_000 * SECONDS_IN_YEAR, -999_999_999 ; "lower edge of range"
581 )]
582 #[test_case(10_000 * SECONDS_IN_YEAR - 1 , 2 * 1_000_000_000_i32, 10_000 * SECONDS_IN_YEAR, 0 ; "nanos push over 10,000 years"
583 )]
584 #[test_case(-10_000 * SECONDS_IN_YEAR + 1, -2 * 1_000_000_000_i32, -10_000 * SECONDS_IN_YEAR, 0 ; "one push under -10,000 years"
585 )]
586 #[test_case(0, 0, 0, 0 ; "all inputs are zero")]
587 #[test_case(1, 0, 1, 0 ; "positive seconds and zero nanos")]
588 #[test_case(1, 200_000, 1, 200_000 ; "positive seconds and nanos")]
589 #[test_case(-1, 0, -1, 0; "negative seconds and zero nanos")]
590 #[test_case(-1, -500_000_000, -1, -500_000_000; "negative seconds and nanos")]
591 #[test_case(2, -400_000_000, 1, 600_000_000; "positive seconds and negative nanos")]
592 #[test_case(-2, 400_000_000, -1, -600_000_000; "negative seconds and positive nanos")]
593 fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) -> Result {
594 let got = Duration::clamp(seconds, nanos);
595 let want = Duration {
596 seconds: want_seconds,
597 nanos: want_nanos,
598 };
599 assert_eq!(want, got);
600 Ok(())
601 }
602
603 #[test_case(0, 0, "0s" ; "zero")]
605 #[test_case(0, 2, "0.000000002s" ; "2ns")]
606 #[test_case(0, 200_000_000, "0.2s" ; "200ms")]
607 #[test_case(12, 0, "12s"; "round positive seconds")]
608 #[test_case(12, 123, "12.000000123s"; "positive seconds and nanos")]
609 #[test_case(12, 123_000, "12.000123s"; "positive seconds and micros")]
610 #[test_case(12, 123_000_000, "12.123s"; "positive seconds and millis")]
611 #[test_case(12, 123_456_789, "12.123456789s"; "positive seconds and full nanos")]
612 #[test_case(-12, -0, "-12s"; "round negative seconds")]
613 #[test_case(-12, -123, "-12.000000123s"; "negative seconds and nanos")]
614 #[test_case(-12, -123_000, "-12.000123s"; "negative seconds and micros")]
615 #[test_case(-12, -123_000_000, "-12.123s"; "negative seconds and millis")]
616 #[test_case(-12, -123_456_789, "-12.123456789s"; "negative seconds and full nanos")]
617 #[test_case(-10_000 * SECONDS_IN_YEAR, -999_999_999, "-315576000000.999999999s"; "range edge start"
618 )]
619 #[test_case(10_000 * SECONDS_IN_YEAR, 999_999_999, "315576000000.999999999s"; "range edge end")]
620 fn roundtrip(seconds: i64, nanos: i32, want: &str) -> Result {
621 let input = Duration::new(seconds, nanos)?;
622 let got = serde_json::to_value(input)?
623 .as_str()
624 .map(str::to_string)
625 .ok_or("cannot convert value to string")?;
626 assert_eq!(want, got);
627
628 let rt = serde_json::from_value::<Duration>(serde_json::Value::String(got))?;
629 assert_eq!(input, rt);
630 Ok(())
631 }
632
633 #[test_case("-315576000001s"; "range edge start")]
634 #[test_case("315576000001s"; "range edge end")]
635 fn deserialize_out_of_range(input: &str) -> Result {
636 let value = serde_json::to_value(input)?;
637 let got = serde_json::from_value::<Duration>(value);
638 assert!(got.is_err());
639 Ok(())
640 }
641
642 #[test_case(time::Duration::default(), Duration::default() ; "default")]
643 #[test_case(time::Duration::new(0, 0), Duration::new(0, 0).unwrap() ; "zero")]
644 #[test_case(time::Duration::new(10_000 * SECONDS_IN_YEAR , 0), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
645 )]
646 #[test_case(time::Duration::new(-10_000 * SECONDS_IN_YEAR , 0), Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly negative 10,000 years"
647 )]
648 fn from_time_in_range(value: time::Duration, want: Duration) -> Result {
649 let got = Duration::try_from(value)?;
650 assert_eq!(got, want);
651 Ok(())
652 }
653
654 #[test_case(time::Duration::new(10_001 * SECONDS_IN_YEAR, 0) ; "above the range")]
655 #[test_case(time::Duration::new(-10_001 * SECONDS_IN_YEAR, 0) ; "below the range")]
656 fn from_time_out_of_range(value: time::Duration) {
657 let got = Duration::try_from(value);
658 assert!(matches!(got, Err(DurationError::OutOfRange)), "{got:?}");
659 }
660
661 #[test_case(Duration::default(), time::Duration::default() ; "default")]
662 #[test_case(Duration::new(0, 0).unwrap(), time::Duration::new(0, 0) ; "zero")]
663 #[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(10_000 * SECONDS_IN_YEAR, 0) ; "exactly 10,000 years"
664 )]
665 #[test_case(Duration::new(-10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(-10_000 * SECONDS_IN_YEAR, 0) ; "exactly negative 10,000 years"
666 )]
667 fn to_time_in_range(value: Duration, want: time::Duration) -> Result {
668 let got = time::Duration::from(value);
669 assert_eq!(got, want);
670 Ok(())
671 }
672
673 #[test_case("" ; "empty")]
674 #[test_case("1.0" ; "missing final s")]
675 #[test_case("1.2.3.4s" ; "too many periods")]
676 #[test_case("aaas" ; "not a number")]
677 #[test_case("aaaa.0s" ; "seconds are not a number [aaa]")]
678 #[test_case("1a.0s" ; "seconds are not a number [1a]")]
679 #[test_case("1.aaas" ; "nanos are not a number [aaa]")]
680 #[test_case("1.0as" ; "nanos are not a number [0a]")]
681 fn parse_detect_bad_input(input: &str) -> Result {
682 let got = Duration::try_from(input);
683 assert!(got.is_err());
684 let err = got.err().unwrap();
685 assert!(
686 matches!(err, DurationError::Deserialize(_)),
687 "unexpected error {err:?}"
688 );
689 Ok(())
690 }
691
692 #[test]
693 fn deserialize_unexpected_input_type() -> Result {
694 let got = serde_json::from_value::<Duration>(serde_json::json!({}));
695 assert!(got.is_err());
696 let msg = format!("{got:?}");
697 assert!(msg.contains("duration in Google format"), "message={msg}");
698 Ok(())
699 }
700
701 #[test_case(std::time::Duration::new(0, 0), Duration::clamp(0, 0))]
702 #[test_case(
703 std::time::Duration::new(0, 400_000_000),
704 Duration::clamp(0, 400_000_000)
705 )]
706 #[test_case(
707 std::time::Duration::new(1, 400_000_000),
708 Duration::clamp(1, 400_000_000)
709 )]
710 #[test_case(std::time::Duration::new(10_000 * SECONDS_IN_YEAR as u64, 999_999_999), Duration::clamp(10_000 * SECONDS_IN_YEAR, 999_999_999))]
711 fn from_std_time_in_range(input: std::time::Duration, want: Duration) {
712 let got = Duration::try_from(input).unwrap();
713 assert_eq!(got, want);
714 }
715
716 #[test]
717 fn convert_from_string() -> Result {
718 let input = "12.750s".to_string();
719 let a = Duration::try_from(input.as_str())?;
720 let b = Duration::try_from(&input)?;
721 assert_eq!(a, b);
722 Ok(())
723 }
724
725 #[test_case(std::time::Duration::new(i64::MAX as u64, 0))]
726 #[test_case(std::time::Duration::new(i64::MAX as u64 + 10, 0))]
727 fn from_std_time_out_of_range(input: std::time::Duration) {
728 let got = Duration::try_from(input);
729 assert!(got.is_err(), "{got:?}");
730 }
731
732 #[test_case(chrono::Duration::default(), Duration::default() ; "default")]
733 #[test_case(chrono::Duration::new(0, 0).unwrap(), Duration::new(0, 0).unwrap() ; "zero")]
734 #[test_case(chrono::Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap(), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
735 )]
736 #[test_case(chrono::Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap(), Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly negative 10,000 years"
737 )]
738 fn from_chrono_time_in_range(value: chrono::Duration, want: Duration) -> Result {
739 let got = Duration::try_from(value)?;
740 assert_eq!(got, want);
741 Ok(())
742 }
743
744 #[test_case(Duration::default(), chrono::Duration::default() ; "default")]
745 #[test_case(Duration::new(0, 0).unwrap(), chrono::Duration::new(0, 0).unwrap() ; "zero")]
746 #[test_case(Duration::new(0, 500_000).unwrap(), chrono::Duration::new(0, 500_000).unwrap() ; "500us")]
747 #[test_case(Duration::new(1, 400_000_000).unwrap(), chrono::Duration::new(1, 400_000_000).unwrap() ; "1.4s")]
748 #[test_case(Duration::new(0, -400_000_000).unwrap(), chrono::Duration::new(-1, 600_000_000).unwrap() ; "minus 0.4s")]
749 #[test_case(Duration::new(-1, -400_000_000).unwrap(), chrono::Duration::new(-2, 600_000_000).unwrap() ; "minus 1.4s")]
750 #[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), chrono::Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
751 )]
752 #[test_case(Duration::new(-10_000 * SECONDS_IN_YEAR , 0).unwrap(), chrono::Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly negative 10,000 years"
753 )]
754 fn to_chrono_time_in_range(value: Duration, want: chrono::Duration) -> Result {
755 let got = chrono::Duration::from(value);
756 assert_eq!(got, want);
757 Ok(())
758 }
759
760 #[test_case(chrono::Duration::new(10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "above the range")]
761 #[test_case(chrono::Duration::new(-10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "below the range")]
762 fn from_chrono_time_out_of_range(value: chrono::Duration) {
763 let got = Duration::try_from(value);
764 assert!(matches!(got, Err(DurationError::OutOfRange)), "{got:?}");
765 }
766}