Skip to main content

proto_types/duration/
base.rs

1// Partially taken from (prost-types)[https://github.com/tokio-rs/prost/blob/master/prost-types/src/duration.rs]
2use super::super::*;
3use crate::constants::{NANOS_PER_SECOND, PACKAGE_PREFIX, TIME_NANOS_MAX};
4
5impl Duration {
6	/// Normalizes the duration to a canonical format.
7	///
8	/// Based on [`google::protobuf::util::CreateNormalized`][1].
9	///
10	/// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100
11	pub fn normalize(&mut self) {
12		// Make sure nanos is in the range.
13		if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND {
14			if let Some(seconds) = self
15				.seconds
16				.checked_add(i64::from(self.nanos / NANOS_PER_SECOND))
17			{
18				self.seconds = seconds;
19
20				self.nanos %= NANOS_PER_SECOND;
21			} else if self.nanos < 0 {
22				// Negative overflow! Set to the least normal value.
23
24				self.seconds = i64::MIN;
25
26				self.nanos = -TIME_NANOS_MAX;
27			} else {
28				// Positive overflow! Set to the greatest normal value.
29
30				self.seconds = i64::MAX;
31
32				self.nanos = TIME_NANOS_MAX;
33			}
34		}
35
36		// nanos should have the same sign as seconds.
37
38		if self.seconds < 0 && self.nanos > 0 {
39			if let Some(seconds) = self.seconds.checked_add(1) {
40				self.seconds = seconds;
41
42				self.nanos -= NANOS_PER_SECOND;
43			} else {
44				// Positive overflow! Set to the greatest normal value.
45
46				debug_assert_eq!(self.seconds, i64::MAX);
47
48				self.nanos = TIME_NANOS_MAX;
49			}
50		} else if self.seconds > 0 && self.nanos < 0 {
51			if let Some(seconds) = self.seconds.checked_sub(1) {
52				self.seconds = seconds;
53
54				self.nanos += NANOS_PER_SECOND;
55			} else {
56				// Negative overflow! Set to the least normal value.
57
58				debug_assert_eq!(self.seconds, i64::MIN);
59
60				self.nanos = -TIME_NANOS_MAX;
61			}
62		}
63
64		// TODO: should this be checked?
65
66		// debug_assert!(self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000,
67
68		//               "invalid duration: {:?}", self);
69	}
70
71	/// Returns a normalized copy of the duration to a canonical format.
72	///
73	/// Based on [`google::protobuf::util::CreateNormalized`][1].
74	///
75	/// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100
76	#[must_use]
77	pub fn normalized(&self) -> Self {
78		let mut result = *self;
79
80		result.normalize();
81
82		result
83	}
84}
85
86impl Name for Duration {
87	const PACKAGE: &'static str = PACKAGE_PREFIX;
88
89	const NAME: &'static str = "Duration";
90
91	fn type_url() -> String {
92		type_url_for::<Self>()
93	}
94}
95
96impl TryFrom<time::Duration> for Duration {
97	type Error = DurationError;
98
99	/// Converts a `std::time::Duration` to a `Duration`, failing if the duration is too large.
100	fn try_from(duration: time::Duration) -> Result<Self, DurationError> {
101		let seconds = i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?;
102
103		let nanos: i32 = duration
104			.subsec_nanos()
105			.try_into()
106			.map_err(|_| DurationError::OutOfRange)?;
107
108		let duration = Self { seconds, nanos };
109
110		Ok(duration.normalized())
111	}
112}
113
114impl TryFrom<Duration> for time::Duration {
115	type Error = DurationError;
116
117	/// Converts a `Duration` to a `std::time::Duration`, failing if the duration is negative.
118	fn try_from(mut duration: Duration) -> Result<Self, DurationError> {
119		duration.normalize();
120
121		if duration.seconds >= 0 && duration.nanos >= 0 {
122			Ok(Self::new(
123				duration
124					.seconds
125					.try_into()
126					.map_err(|_| DurationError::OutOfRange)?,
127				duration
128					.nanos
129					.try_into()
130					.map_err(|_| DurationError::OutOfRange)?,
131			))
132		} else {
133			Err(DurationError::NegativeDuration(Self::new(
134				(-duration.seconds)
135					.try_into()
136					.map_err(|_| DurationError::OutOfRange)?,
137				(-duration.nanos)
138					.try_into()
139					.map_err(|_| DurationError::OutOfRange)?,
140			)))
141		}
142	}
143}
144
145/// A duration handling error.
146#[derive(Debug, PartialEq, Eq, Clone)]
147#[non_exhaustive]
148pub enum DurationError {
149	/// Indicates failure to parse a [`Duration`] from a string.
150	///
151	/// The [`Duration`] string format is specified in the [Protobuf JSON mapping specification][1].
152	///
153	/// [1]: https://developers.google.com/protocol-buffers/docs/proto3#json
154	ParseFailure,
155
156	/// Indicates failure to convert a `prost_types::Duration` to a `std::time::Duration` because
157	/// the duration is negative. The included `std::time::Duration` matches the magnitude of the
158	/// original negative `prost_types::Duration`.
159	NegativeDuration(time::Duration),
160
161	/// Indicates failure to convert a `std::time::Duration` to a `prost_types::Duration`.
162	///
163	/// Converting a `std::time::Duration` to a `prost_types::Duration` fails if the magnitude
164	/// exceeds that representable by `prost_types::Duration`.
165	OutOfRange,
166}
167
168impl fmt::Display for DurationError {
169	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170		match self {
171			Self::ParseFailure => write!(f, "failed to parse duration"),
172
173			Self::NegativeDuration(duration) => {
174				write!(f, "failed to convert negative duration: {duration:?}")
175			}
176
177			Self::OutOfRange => {
178				write!(f, "failed to convert duration out of range")
179			}
180		}
181	}
182}
183
184impl core::error::Error for DurationError {}
185
186impl FromStr for Duration {
187	type Err = DurationError;
188
189	fn from_str(s: &str) -> Result<Self, DurationError> {
190		datetime_internal::parse_duration(s).ok_or(DurationError::ParseFailure)
191	}
192}
193
194#[cfg(feature = "chrono")]
195mod chrono {
196
197	use crate::{Duration, duration::DurationError};
198
199	impl From<::chrono::TimeDelta> for Duration {
200		#[inline]
201		fn from(value: ::chrono::TimeDelta) -> Self {
202			let mut result = Self {
203				seconds: value.num_seconds(),
204
205				nanos: value.subsec_nanos(),
206			};
207
208			result.normalize();
209
210			result
211		}
212	}
213
214	impl TryFrom<Duration> for ::chrono::TimeDelta {
215		type Error = DurationError;
216
217		fn try_from(mut value: Duration) -> Result<Self, DurationError> {
218			value.normalize();
219
220			let seconds = Self::try_seconds(value.seconds).ok_or(DurationError::OutOfRange)?;
221
222			let nanos = Self::nanoseconds(value.nanos.into());
223
224			seconds
225				.checked_add(&nanos)
226				.ok_or(DurationError::OutOfRange)
227		}
228	}
229}