use crate::google::protobuf::Duration;
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DurationChronoError {
#[error("nanos field has invalid value or sign mismatch with seconds")]
InvalidNanos,
#[error("duration is out of range for chrono::TimeDelta")]
Overflow,
}
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl From<chrono::TimeDelta> for Duration {
fn from(d: chrono::TimeDelta) -> Self {
Self {
seconds: d.num_seconds(),
nanos: d.subsec_nanos(),
..Default::default()
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl TryFrom<Duration> for chrono::TimeDelta {
type Error = DurationChronoError;
fn try_from(d: Duration) -> Result<Self, Self::Error> {
if d.nanos < -999_999_999 || d.nanos > 999_999_999 {
return Err(DurationChronoError::InvalidNanos);
}
let sign_mismatch = (d.seconds > 0 && d.nanos < 0) || (d.seconds < 0 && d.nanos > 0);
if sign_mismatch {
return Err(DurationChronoError::InvalidNanos);
}
let secs_part = Self::try_seconds(d.seconds).ok_or(DurationChronoError::Overflow)?;
let nanos_part = Self::nanoseconds(i64::from(d.nanos));
secs_part
.checked_add(&nanos_part)
.ok_or(DurationChronoError::Overflow)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn positive_roundtrip() {
let td = chrono::TimeDelta::new(300, 500_000_000).unwrap();
let proto: Duration = td.into();
assert_eq!(proto.seconds, 300);
assert_eq!(proto.nanos, 500_000_000);
let back: chrono::TimeDelta = proto.try_into().unwrap();
assert_eq!(back, td);
}
#[test]
fn zero_roundtrip() {
let td = chrono::TimeDelta::zero();
let proto: Duration = td.into();
assert_eq!(proto.seconds, 0);
assert_eq!(proto.nanos, 0);
let back: chrono::TimeDelta = proto.try_into().unwrap();
assert_eq!(back, td);
}
#[test]
fn negative_roundtrip() {
let td = chrono::TimeDelta::milliseconds(-1_500);
let proto: Duration = td.into();
assert_eq!(proto.seconds, -1);
assert_eq!(proto.nanos, -500_000_000);
let back: chrono::TimeDelta = proto.try_into().unwrap();
assert_eq!(back, td);
}
#[test]
fn sub_second_negative_roundtrip() {
let td = chrono::TimeDelta::nanoseconds(-500_000_000);
let proto: Duration = td.into();
assert_eq!(proto.seconds, 0);
assert_eq!(proto.nanos, -500_000_000);
let back: chrono::TimeDelta = proto.try_into().unwrap();
assert_eq!(back, td);
}
#[test]
fn invalid_nanos_rejected() {
let bad = Duration {
seconds: 1,
nanos: 1_000_000_000,
..Default::default()
};
let result: Result<chrono::TimeDelta, _> = bad.try_into();
assert_eq!(result, Err(DurationChronoError::InvalidNanos));
}
#[test]
fn nanos_i32_min_is_invalid() {
let bad = Duration {
seconds: 0,
nanos: i32::MIN,
..Default::default()
};
let result: Result<chrono::TimeDelta, _> = bad.try_into();
assert_eq!(result, Err(DurationChronoError::InvalidNanos));
}
#[test]
fn sign_mismatch_rejected() {
let bad = Duration {
seconds: 5,
nanos: -1,
..Default::default()
};
let result: Result<chrono::TimeDelta, _> = bad.try_into();
assert_eq!(result, Err(DurationChronoError::InvalidNanos));
let bad2 = Duration {
seconds: -5,
nanos: 1,
..Default::default()
};
let result2: Result<chrono::TimeDelta, _> = bad2.try_into();
assert_eq!(result2, Err(DurationChronoError::InvalidNanos));
}
#[test]
fn timedelta_extremes_roundtrip() {
let max = chrono::TimeDelta::milliseconds(i64::MAX);
let proto: Duration = max.into();
assert_eq!(proto.seconds, max.num_seconds());
assert_eq!(proto.nanos, max.subsec_nanos());
let back: chrono::TimeDelta = proto.try_into().unwrap();
assert_eq!(back, max);
let min = chrono::TimeDelta::milliseconds(-i64::MAX);
let proto_min: Duration = min.into();
let back_min: chrono::TimeDelta = proto_min.try_into().unwrap();
assert_eq!(back_min, min);
}
#[test]
fn nanos_addition_overflow_is_overflow() {
let boundary_secs = i64::MAX / 1_000;
let near_max = Duration {
seconds: boundary_secs,
nanos: 999_999_999,
..Default::default()
};
let result: Result<chrono::TimeDelta, _> = near_max.try_into();
assert_eq!(result, Err(DurationChronoError::Overflow));
let boundary_neg = -(i64::MAX / 1_000);
let near_min = Duration {
seconds: boundary_neg,
nanos: -999_999_999,
..Default::default()
};
let result_neg: Result<chrono::TimeDelta, _> = near_min.try_into();
assert_eq!(result_neg, Err(DurationChronoError::Overflow));
}
#[test]
fn out_of_range_seconds_is_overflow() {
let huge = Duration {
seconds: i64::MAX,
nanos: 0,
..Default::default()
};
let result: Result<chrono::TimeDelta, _> = huge.try_into();
assert_eq!(result, Err(DurationChronoError::Overflow));
let tiny = Duration {
seconds: i64::MIN,
nanos: 0,
..Default::default()
};
let result2: Result<chrono::TimeDelta, _> = tiny.try_into();
assert_eq!(result2, Err(DurationChronoError::Overflow));
}
}