#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
#[non_exhaustive]
pub struct Duration {
seconds: i64,
nanos: i32,
}
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum DurationError {
#[error("seconds and/or nanoseconds out of range")]
OutOfRange(),
#[error("if seconds and nanoseconds are not zero, they must have the same sign")]
MismatchedSigns(),
#[error("cannot serialize the duration")]
Serializate(),
#[error("cannot deserialize the duration: {0:?}")]
Deserialize(String),
}
type Error = DurationError;
impl Duration {
const NS: i32 = 1_000_000_000;
pub const MAX_SECONDS: i64 = 315_576_000_000;
pub const MIN_SECONDS: i64 = -Self::MAX_SECONDS;
pub const MAX_NANOS: i32 = Self::NS - 1;
pub const MIN_NANOS: i32 = -Self::MAX_NANOS;
pub fn new(seconds: i64, nanos: i32) -> Result<Self, Error> {
if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
return Err(Error::OutOfRange());
}
if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
return Err(Error::OutOfRange());
}
if (seconds != 0 && nanos != 0) && ((seconds < 0) != (nanos < 0)) {
return Err(Error::MismatchedSigns());
}
Ok(Self { seconds, nanos })
}
pub fn clamp(seconds: i64, nanos: i32) -> Self {
let mut seconds = seconds;
seconds = seconds.saturating_add((nanos / Self::NS) as i64);
let mut nanos = nanos % Self::NS;
if seconds > 0 && nanos < 0 {
seconds = seconds.saturating_sub(1);
nanos += Self::NS;
} else if seconds < 0 && nanos > 0 {
seconds = seconds.saturating_add(1);
nanos = -(Self::NS - nanos);
}
if seconds > Self::MAX_SECONDS {
return Self {
seconds: Self::MAX_SECONDS,
nanos: 0,
};
}
if seconds < Self::MIN_SECONDS {
return Self {
seconds: Self::MIN_SECONDS,
nanos: 0,
};
}
Self { seconds, nanos }
}
pub fn seconds(&self) -> i64 {
self.seconds
}
pub fn nanos(&self) -> i32 {
self.nanos
}
}
impl crate::message::Message for Duration {
fn typename() -> &'static str {
"type.googleapis.com/google.protobuf.Duration"
}
}
impl std::convert::From<&Duration> for String {
fn from(duration: &Duration) -> String {
let sign = if duration.seconds < 0 || duration.nanos < 0 {
"-"
} else {
""
};
if duration.nanos == 0 {
return format!("{sign}{}s", duration.seconds.abs());
}
if duration.seconds == 0 {
let ns = format!("{:09}", duration.nanos.abs());
return format!("{sign}0.{}s", ns.trim_end_matches('0'));
}
format!(
"{sign}{}.{:09}s",
duration.seconds.abs(),
duration.nanos.abs()
)
}
}
impl std::convert::TryFrom<&str> for Duration {
type Error = DurationError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if !value.ends_with('s') {
return Err(DurationError::Deserialize("missing trailing 's'".into()));
}
let digits = &value[..(value.len() - 1)];
let (sign, digits) = if let Some(stripped) = digits.strip_prefix('-') {
(-1, stripped)
} else {
(1, &digits[0..])
};
let mut split = digits.splitn(2, '.');
let (seconds, nanos) = (split.next(), split.next());
let seconds = seconds
.map(str::parse::<i64>)
.transpose()
.map_err(|e| DurationError::Deserialize(format!("{e}")))?
.unwrap_or(0);
let nanos = nanos
.map(|s| {
let pad = "000000000";
format!("{s}{}", &pad[s.len()..])
})
.map(|s| s.parse::<i32>())
.transpose()
.map_err(|e| DurationError::Deserialize(format!("{e}")))?
.unwrap_or(0);
Duration::new(sign * seconds, sign as i32 * nanos)
}
}
impl std::convert::TryFrom<std::time::Duration> for Duration {
type Error = DurationError;
fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
if value.as_secs() > (i64::MAX as u64) {
return Err(Error::OutOfRange());
}
assert!(value.subsec_nanos() <= (i32::MAX as u32));
Self::new(value.as_secs() as i64, value.subsec_nanos() as i32)
}
}
impl std::convert::TryFrom<Duration> for std::time::Duration {
type Error = DurationError;
fn try_from(value: Duration) -> Result<Self, Self::Error> {
if value.seconds < 0 {
return Err(Error::OutOfRange());
}
if value.nanos < 0 {
return Err(Error::OutOfRange());
}
Ok(Self::new(value.seconds as u64, value.nanos as u32))
}
}
#[cfg(feature = "time")]
impl std::convert::TryFrom<time::Duration> for Duration {
type Error = DurationError;
fn try_from(value: time::Duration) -> Result<Self, Self::Error> {
Self::new(value.whole_seconds(), value.subsec_nanoseconds())
}
}
#[cfg(feature = "time")]
impl std::convert::From<Duration> for time::Duration {
fn from(value: Duration) -> Self {
Self::new(value.seconds(), value.nanos())
}
}
#[cfg(feature = "chrono")]
impl std::convert::TryFrom<chrono::Duration> for Duration {
type Error = DurationError;
fn try_from(value: chrono::Duration) -> Result<Self, Self::Error> {
Self::new(value.num_seconds(), value.subsec_nanos())
}
}
#[cfg(feature = "chrono")]
impl std::convert::From<Duration> for chrono::Duration {
fn from(value: Duration) -> Self {
Self::seconds(value.seconds) + Self::nanoseconds(value.nanos as i64)
}
}
impl serde::ser::Serialize for Duration {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
let formatted = String::from(self);
formatted.serialize(serializer)
}
}
struct DurationVisitor;
impl serde::de::Visitor<'_> for DurationVisitor {
type Value = Duration;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string with a duration in Google format ([sign]{seconds}.{nanos}s)")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let d = Duration::try_from(value).map_err(E::custom)?;
Ok(d)
}
}
impl<'de> serde::de::Deserialize<'de> for Duration {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(DurationVisitor)
}
}
#[cfg(test)]
mod test {
use super::*;
use serde_json::json;
use test_case::test_case;
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
#[test]
fn zero() -> Result {
let proto = Duration {
seconds: 0,
nanos: 0,
};
let json = serde_json::to_value(&proto)?;
let expected = json!(r#"0s"#);
assert_eq!(json, expected);
let roundtrip = serde_json::from_value::<Duration>(json)?;
assert_eq!(proto, roundtrip);
Ok(())
}
const SECONDS_IN_DAY: i64 = 24 * 60 * 60;
const SECONDS_IN_YEAR: i64 = 365 * SECONDS_IN_DAY + SECONDS_IN_DAY / 4;
#[test_case(10_000 * SECONDS_IN_YEAR , 0 ; "exactly 10,000 years")]
#[test_case(- 10_000 * SECONDS_IN_YEAR , 0 ; "exactly negative 10,000 years")]
#[test_case(10_000 * SECONDS_IN_YEAR , 999_999_999 ; "exactly 10,000 years and 999,999,999 nanos")]
#[test_case(- 10_000 * SECONDS_IN_YEAR , -999_999_999 ; "exactly negative 10,000 years and 999,999,999 nanos")]
#[test_case(0, 999_999_999 ; "exactly 999,999,999 nanos")]
#[test_case(0 , -999_999_999 ; "exactly negative 999,999,999 nanos")]
fn edge_of_range(seconds: i64, nanos: i32) -> Result {
let d = Duration::new(seconds, nanos)?;
assert_eq!(seconds, d.seconds());
assert_eq!(nanos, d.nanos());
Ok(())
}
#[test_case(10_000 * SECONDS_IN_YEAR + 1, 0 ; "more seconds than in 10,000 years")]
#[test_case(- 10_000 * SECONDS_IN_YEAR - 1, 0 ; "more negative seconds than in -10,000 years")]
#[test_case(0, 1_000_000_000 ; "too many positive nanoseconds")]
#[test_case(0, -1_000_000_000 ; "too many negative nanoseconds")]
fn out_of_range(seconds: i64, nanos: i32) -> Result {
let d = Duration::new(seconds, nanos);
assert_eq!(d, Err(Error::OutOfRange()));
Ok(())
}
#[test_case(1 , -1 ; "mismatched sign case 1")]
#[test_case(-1 , 1 ; "mismatched sign case 2")]
fn mismatched_sign(seconds: i64, nanos: i32) -> Result {
let d = Duration::new(seconds, nanos);
assert_eq!(d, Err(Error::MismatchedSigns()));
Ok(())
}
#[test_case(20_000 * SECONDS_IN_YEAR, 0, 10_000 * SECONDS_IN_YEAR, 0 ; "too many positive seconds")]
#[test_case(-20_000 * SECONDS_IN_YEAR, 0, -10_000 * SECONDS_IN_YEAR, 0 ; "too many negative seconds")]
#[test_case(10_000 * SECONDS_IN_YEAR - 1, 1_999_999_999, 10_000 * SECONDS_IN_YEAR, 999_999_999 ; "upper edge of range")]
#[test_case(-10_000 * SECONDS_IN_YEAR + 1, -1_999_999_999, -10_000 * SECONDS_IN_YEAR, -999_999_999 ; "lower edge of range")]
#[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")]
#[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")]
#[test_case(0, 0, 0, 0 ; "all inputs are zero")]
#[test_case(1, 0, 1, 0 ; "positive seconds and zero nanos")]
#[test_case(1, 200_000, 1, 200_000 ; "positive seconds and nanos")]
#[test_case(-1, 0, -1, 0; "negative seconds and zero nanos")]
#[test_case(-1, -500_000_000, -1, -500_000_000; "negative seconds and nanos")]
#[test_case(2, -400_000_000, 1, 600_000_000; "positive seconds and negative nanos")]
#[test_case(-2, 400_000_000, -1, -600_000_000; "negative seconds and positive nanos")]
fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) -> Result {
let got = Duration::clamp(seconds, nanos);
let want = Duration {
seconds: want_seconds,
nanos: want_nanos,
};
assert_eq!(want, got);
Ok(())
}
#[test_case(0, 0, "0s" ; "zero")]
#[test_case(0, 2, "0.000000002s" ; "2ns")]
#[test_case(0, 200_000_000, "0.2s" ; "200ms")]
#[test_case(12, 0, "12s"; "round positive seconds")]
#[test_case(12, 123, "12.000000123s"; "positive seconds and nanos")]
#[test_case(12, 123_000, "12.000123000s"; "positive seconds and micros")]
#[test_case(12, 123_000_000, "12.123000000s"; "positive seconds and millis")]
#[test_case(12, 123_456_789, "12.123456789s"; "positive seconds and full nanos")]
#[test_case(-12, -0, "-12s"; "round negative seconds")]
#[test_case(-12, -123, "-12.000000123s"; "negative seconds and nanos")]
#[test_case(-12, -123_000, "-12.000123000s"; "negative seconds and micros")]
#[test_case(-12, -123_000_000, "-12.123000000s"; "negative seconds and millis")]
#[test_case(-12, -123_456_789, "-12.123456789s"; "negative seconds and full nanos")]
#[test_case(-10_000 * SECONDS_IN_YEAR, -999_999_999, "-315576000000.999999999s"; "range edge start")]
#[test_case(10_000 * SECONDS_IN_YEAR, 999_999_999, "315576000000.999999999s"; "range edge end")]
fn roundtrip(seconds: i64, nanos: i32, want: &str) -> Result {
let input = Duration::new(seconds, nanos)?;
let got = serde_json::to_value(&input)?
.as_str()
.map(str::to_string)
.ok_or("cannot convert value to string")?;
assert_eq!(want, got);
let rt = serde_json::from_value::<Duration>(serde_json::Value::String(got))?;
assert_eq!(input, rt);
Ok(())
}
#[test_case("-315576000001s"; "range edge start")]
#[test_case("315576000001s"; "range edge end")]
fn deserialize_out_of_range(input: &str) -> Result {
let value = serde_json::to_value(input)?;
let got = serde_json::from_value::<Duration>(value);
assert!(got.is_err());
Ok(())
}
#[test_case(time::Duration::default(), Duration::default() ; "default")]
#[test_case(time::Duration::new(0, 0), Duration::new(0, 0).unwrap() ; "zero")]
#[test_case(time::Duration::new(10_000 * SECONDS_IN_YEAR , 0), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years")]
#[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")]
fn from_time_in_range(value: time::Duration, want: Duration) -> Result {
let got = Duration::try_from(value)?;
assert_eq!(got, want);
Ok(())
}
#[test_case(time::Duration::new(10_001 * SECONDS_IN_YEAR, 0) ; "above the range")]
#[test_case(time::Duration::new(-10_001 * SECONDS_IN_YEAR, 0) ; "below the range")]
fn from_time_out_of_range(value: time::Duration) {
let got = Duration::try_from(value);
assert_eq!(got, Err(DurationError::OutOfRange()));
}
#[test_case(Duration::default(), time::Duration::default() ; "default")]
#[test_case(Duration::new(0, 0).unwrap(), time::Duration::new(0, 0) ; "zero")]
#[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(10_000 * SECONDS_IN_YEAR, 0) ; "exactly 10,000 years")]
#[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")]
fn to_time_in_range(value: Duration, want: time::Duration) -> Result {
let got = time::Duration::from(value);
assert_eq!(got, want);
Ok(())
}
#[test_case("" ; "empty")]
#[test_case("1.0" ; "missing final s")]
#[test_case("1.2.3.4s" ; "too many periods")]
#[test_case("aaas" ; "not a number")]
#[test_case("aaaa.0s" ; "seconds are not a number [aaa]")]
#[test_case("1a.0s" ; "seconds are not a number [1a]")]
#[test_case("1.aaas" ; "nanos are not a number [aaa]")]
#[test_case("1.0as" ; "nanos are not a number [0a]")]
fn parse_detect_bad_input(input: &str) -> Result {
let got = Duration::try_from(input);
assert!(got.is_err());
let err = got.err().unwrap();
match err {
DurationError::Deserialize(_) => {
assert!(true)
}
_ => assert!(false, "unexpected error {err:?}"),
};
Ok(())
}
#[test]
fn deserialize_unexpected_input_type() -> Result {
let got = serde_json::from_value::<Duration>(serde_json::json!({}));
assert!(got.is_err());
let msg = format!("{got:?}");
assert!(msg.contains("duration in Google format"), "message={}", msg);
Ok(())
}
#[test_case(chrono::Duration::default(), Duration::default() ; "default")]
#[test_case(chrono::Duration::new(0, 0).unwrap(), Duration::new(0, 0).unwrap() ; "zero")]
#[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")]
#[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")]
fn from_chrono_time_in_range(value: chrono::Duration, want: Duration) -> Result {
let got = Duration::try_from(value)?;
assert_eq!(got, want);
Ok(())
}
#[test_case(Duration::default(), chrono::Duration::default() ; "default")]
#[test_case(Duration::new(0, 0).unwrap(), chrono::Duration::new(0, 0).unwrap() ; "zero")]
#[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")]
#[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")]
fn to_chrono_time_in_range(value: Duration, want: chrono::Duration) -> Result {
let got = chrono::Duration::from(value);
assert_eq!(got, want);
Ok(())
}
#[test_case(chrono::Duration::new(10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "above the range")]
#[test_case(chrono::Duration::new(-10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "below the range")]
fn from_chrono_time_out_of_range(value: chrono::Duration) {
let got = Duration::try_from(value);
assert_eq!(got, Err(DurationError::OutOfRange()));
}
}