1use super::*;
2
3#[cfg(feature = "std")]
4impl std::hash::Hash for Duration {
5 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
6 self.seconds.hash(state);
7 self.nanos.hash(state);
8 }
9}
10
11impl Duration {
12 pub fn normalize(&mut self) {
18 if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND {
20 if let Some(seconds) = self
21 .seconds
22 .checked_add((self.nanos / NANOS_PER_SECOND) as i64)
23 {
24 self.seconds = seconds;
25 self.nanos %= NANOS_PER_SECOND;
26 } else if self.nanos < 0 {
27 self.seconds = i64::MIN;
29 self.nanos = -NANOS_MAX;
30 } else {
31 self.seconds = i64::MAX;
33 self.nanos = NANOS_MAX;
34 }
35 }
36
37 if self.seconds < 0 && self.nanos > 0 {
39 if let Some(seconds) = self.seconds.checked_add(1) {
40 self.seconds = seconds;
41 self.nanos -= NANOS_PER_SECOND;
42 } else {
43 debug_assert_eq!(self.seconds, i64::MAX);
45 self.nanos = NANOS_MAX;
46 }
47 } else if self.seconds > 0 && self.nanos < 0 {
48 if let Some(seconds) = self.seconds.checked_sub(1) {
49 self.seconds = seconds;
50 self.nanos += NANOS_PER_SECOND;
51 } else {
52 debug_assert_eq!(self.seconds, i64::MIN);
54 self.nanos = -NANOS_MAX;
55 }
56 }
57 }
61
62 pub fn normalized(&self) -> Self {
68 let mut result = *self;
69 result.normalize();
70 result
71 }
72}
73
74impl Name for Duration {
75 const PACKAGE: &'static str = PACKAGE;
76 const NAME: &'static str = "Duration";
77
78 fn type_url() -> String {
79 type_url_for::<Self>()
80 }
81}
82
83impl TryFrom<time::Duration> for Duration {
84 type Error = DurationError;
85
86 fn try_from(duration: time::Duration) -> Result<Duration, DurationError> {
88 let seconds = i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?;
89 let nanos = duration.subsec_nanos() as i32;
90
91 let duration = Duration { seconds, nanos };
92 Ok(duration.normalized())
93 }
94}
95
96impl TryFrom<Duration> for time::Duration {
97 type Error = DurationError;
98
99 fn try_from(mut duration: Duration) -> Result<time::Duration, DurationError> {
101 duration.normalize();
102 if duration.seconds >= 0 && duration.nanos >= 0 {
103 Ok(time::Duration::new(
104 duration.seconds as u64,
105 duration.nanos as u32,
106 ))
107 } else {
108 Err(DurationError::NegativeDuration(time::Duration::new(
109 (-duration.seconds) as u64,
110 (-duration.nanos) as u32,
111 )))
112 }
113 }
114}
115
116impl fmt::Display for Duration {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 let d = self.normalized();
119 if self.seconds < 0 || self.nanos < 0 {
120 write!(f, "-")?;
121 }
122 write!(f, "{}", d.seconds.abs())?;
123
124 let nanos = d.nanos.abs();
126 if nanos == 0 {
127 write!(f, "s")
128 } else if nanos % 1_000_000 == 0 {
129 write!(f, ".{:03}s", nanos / 1_000_000)
130 } else if nanos % 1_000 == 0 {
131 write!(f, ".{:06}s", nanos / 1_000)
132 } else {
133 write!(f, ".{:09}s", nanos)
134 }
135 }
136}
137
138#[derive(Debug, PartialEq)]
140#[non_exhaustive]
141pub enum DurationError {
142 ParseFailure,
148
149 NegativeDuration(time::Duration),
153
154 OutOfRange,
159}
160
161impl fmt::Display for DurationError {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 DurationError::ParseFailure => write!(f, "failed to parse duration"),
165 DurationError::NegativeDuration(duration) => {
166 write!(f, "failed to convert negative duration: {:?}", duration)
167 }
168 DurationError::OutOfRange => {
169 write!(f, "failed to convert duration out of range")
170 }
171 }
172 }
173}
174
175#[cfg(feature = "std")]
176impl std::error::Error for DurationError {}
177
178impl FromStr for Duration {
179 type Err = DurationError;
180
181 fn from_str(s: &str) -> Result<Duration, DurationError> {
182 datetime::parse_duration(s).ok_or(DurationError::ParseFailure)
183 }
184}
185
186#[cfg(kani)]
187mod proofs {
188 use super::*;
189
190 #[cfg(feature = "std")]
191 #[kani::proof]
192 fn check_duration_roundtrip() {
193 let seconds = kani::any();
194 let nanos = kani::any();
195 kani::assume(nanos < 1_000_000_000);
196 let std_duration = std::time::Duration::new(seconds, nanos);
197 let Ok(prost_duration) = Duration::try_from(std_duration) else {
198 return;
200 };
201 assert_eq!(
202 time::Duration::try_from(prost_duration).unwrap(),
203 std_duration
204 );
205
206 if std_duration != time::Duration::default() {
207 let neg_prost_duration = Duration {
208 seconds: -prost_duration.seconds,
209 nanos: -prost_duration.nanos,
210 };
211
212 assert!(matches!(
213 time::Duration::try_from(neg_prost_duration),
214 Err(DurationError::NegativeDuration(d)) if d == std_duration,
215 ))
216 }
217 }
218
219 #[cfg(feature = "std")]
220 #[kani::proof]
221 fn check_duration_roundtrip_nanos() {
222 let seconds = 0;
223 let nanos = kani::any();
224 let std_duration = std::time::Duration::new(seconds, nanos);
225 let Ok(prost_duration) = Duration::try_from(std_duration) else {
226 return;
228 };
229 assert_eq!(
230 time::Duration::try_from(prost_duration).unwrap(),
231 std_duration
232 );
233
234 if std_duration != time::Duration::default() {
235 let neg_prost_duration = Duration {
236 seconds: -prost_duration.seconds,
237 nanos: -prost_duration.nanos,
238 };
239
240 assert!(matches!(
241 time::Duration::try_from(neg_prost_duration),
242 Err(DurationError::NegativeDuration(d)) if d == std_duration,
243 ))
244 }
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[cfg(feature = "std")]
253 #[test]
254 fn test_duration_from_str() {
255 assert_eq!(
256 Duration::from_str("0s"),
257 Ok(Duration {
258 seconds: 0,
259 nanos: 0
260 })
261 );
262 assert_eq!(
263 Duration::from_str("123s"),
264 Ok(Duration {
265 seconds: 123,
266 nanos: 0
267 })
268 );
269 assert_eq!(
270 Duration::from_str("0.123s"),
271 Ok(Duration {
272 seconds: 0,
273 nanos: 123_000_000
274 })
275 );
276 assert_eq!(
277 Duration::from_str("-123s"),
278 Ok(Duration {
279 seconds: -123,
280 nanos: 0
281 })
282 );
283 assert_eq!(
284 Duration::from_str("-0.123s"),
285 Ok(Duration {
286 seconds: 0,
287 nanos: -123_000_000
288 })
289 );
290 assert_eq!(
291 Duration::from_str("22041211.6666666666666s"),
292 Ok(Duration {
293 seconds: 22041211,
294 nanos: 666_666_666
295 })
296 );
297 }
298
299 #[cfg(feature = "std")]
300 #[test]
301 fn test_format_duration() {
302 assert_eq!(
303 "0s",
304 Duration {
305 seconds: 0,
306 nanos: 0
307 }
308 .to_string()
309 );
310 assert_eq!(
311 "123s",
312 Duration {
313 seconds: 123,
314 nanos: 0
315 }
316 .to_string()
317 );
318 assert_eq!(
319 "0.123s",
320 Duration {
321 seconds: 0,
322 nanos: 123_000_000
323 }
324 .to_string()
325 );
326 assert_eq!(
327 "-123s",
328 Duration {
329 seconds: -123,
330 nanos: 0
331 }
332 .to_string()
333 );
334 assert_eq!(
335 "-0.123s",
336 Duration {
337 seconds: 0,
338 nanos: -123_000_000
339 }
340 .to_string()
341 );
342 }
343
344 #[cfg(feature = "std")]
345 #[test]
346 fn check_duration_try_from_negative_nanos() {
347 let seconds: u64 = 0;
348 let nanos: u32 = 1;
349 let std_duration = std::time::Duration::new(seconds, nanos);
350
351 let neg_prost_duration = Duration {
352 seconds: 0,
353 nanos: -1,
354 };
355
356 assert!(matches!(
357 time::Duration::try_from(neg_prost_duration),
358 Err(DurationError::NegativeDuration(d)) if d == std_duration,
359 ))
360 }
361
362 #[test]
363 fn check_duration_normalize() {
364 #[rustfmt::skip] let cases = [
366 (line!(), 0, 0, 0, 0),
369 (line!(), 1, 1, 1, 1),
370 (line!(), -1, -1, -1, -1),
371 (line!(), 0, 999_999_999, 0, 999_999_999),
372 (line!(), 0, -999_999_999, 0, -999_999_999),
373 (line!(), 0, 1_000_000_000, 1, 0),
374 (line!(), 0, -1_000_000_000, -1, 0),
375 (line!(), 0, 1_000_000_001, 1, 1),
376 (line!(), 0, -1_000_000_001, -1, -1),
377 (line!(), -1, 1, 0, -999_999_999),
378 (line!(), 1, -1, 0, 999_999_999),
379 (line!(), -1, 1_000_000_000, 0, 0),
380 (line!(), 1, -1_000_000_000, 0, 0),
381 (line!(), i64::MIN , 0, i64::MIN , 0),
382 (line!(), i64::MIN + 1, 0, i64::MIN + 1, 0),
383 (line!(), i64::MIN , 1, i64::MIN + 1, -999_999_999),
384 (line!(), i64::MIN , 1_000_000_000, i64::MIN + 1, 0),
385 (line!(), i64::MIN , -1_000_000_000, i64::MIN , -999_999_999),
386 (line!(), i64::MIN + 1, -1_000_000_000, i64::MIN , 0),
387 (line!(), i64::MIN + 2, -1_000_000_000, i64::MIN + 1, 0),
388 (line!(), i64::MIN , -1_999_999_998, i64::MIN , -999_999_999),
389 (line!(), i64::MIN + 1, -1_999_999_998, i64::MIN , -999_999_998),
390 (line!(), i64::MIN + 2, -1_999_999_998, i64::MIN + 1, -999_999_998),
391 (line!(), i64::MIN , -1_999_999_999, i64::MIN , -999_999_999),
392 (line!(), i64::MIN + 1, -1_999_999_999, i64::MIN , -999_999_999),
393 (line!(), i64::MIN + 2, -1_999_999_999, i64::MIN + 1, -999_999_999),
394 (line!(), i64::MIN , -2_000_000_000, i64::MIN , -999_999_999),
395 (line!(), i64::MIN + 1, -2_000_000_000, i64::MIN , -999_999_999),
396 (line!(), i64::MIN + 2, -2_000_000_000, i64::MIN , 0),
397 (line!(), i64::MIN , -999_999_998, i64::MIN , -999_999_998),
398 (line!(), i64::MIN + 1, -999_999_998, i64::MIN + 1, -999_999_998),
399 (line!(), i64::MAX , 0, i64::MAX , 0),
400 (line!(), i64::MAX - 1, 0, i64::MAX - 1, 0),
401 (line!(), i64::MAX , -1, i64::MAX - 1, 999_999_999),
402 (line!(), i64::MAX , 1_000_000_000, i64::MAX , 999_999_999),
403 (line!(), i64::MAX - 1, 1_000_000_000, i64::MAX , 0),
404 (line!(), i64::MAX - 2, 1_000_000_000, i64::MAX - 1, 0),
405 (line!(), i64::MAX , 1_999_999_998, i64::MAX , 999_999_999),
406 (line!(), i64::MAX - 1, 1_999_999_998, i64::MAX , 999_999_998),
407 (line!(), i64::MAX - 2, 1_999_999_998, i64::MAX - 1, 999_999_998),
408 (line!(), i64::MAX , 1_999_999_999, i64::MAX , 999_999_999),
409 (line!(), i64::MAX - 1, 1_999_999_999, i64::MAX , 999_999_999),
410 (line!(), i64::MAX - 2, 1_999_999_999, i64::MAX - 1, 999_999_999),
411 (line!(), i64::MAX , 2_000_000_000, i64::MAX , 999_999_999),
412 (line!(), i64::MAX - 1, 2_000_000_000, i64::MAX , 999_999_999),
413 (line!(), i64::MAX - 2, 2_000_000_000, i64::MAX , 0),
414 (line!(), i64::MAX , 999_999_998, i64::MAX , 999_999_998),
415 (line!(), i64::MAX - 1, 999_999_998, i64::MAX - 1, 999_999_998),
416 ];
417
418 for case in cases.iter() {
419 let test_duration = Duration {
420 seconds: case.1,
421 nanos: case.2,
422 };
423
424 assert_eq!(
425 test_duration.normalized(),
426 Duration {
427 seconds: case.3,
428 nanos: case.4,
429 },
430 "test case on line {} doesn't match",
431 case.0,
432 );
433 }
434 }
435}