1use super::*;
2
3impl Duration {
4 pub fn normalize(&mut self) {
10 if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND {
12 if let Some(seconds) = self
13 .seconds
14 .checked_add((self.nanos / NANOS_PER_SECOND) as i64)
15 {
16 self.seconds = seconds;
17 self.nanos %= NANOS_PER_SECOND;
18 } else if self.nanos < 0 {
19 self.seconds = i64::MIN;
21 self.nanos = -NANOS_MAX;
22 } else {
23 self.seconds = i64::MAX;
25 self.nanos = NANOS_MAX;
26 }
27 }
28
29 if self.seconds < 0 && self.nanos > 0 {
31 if let Some(seconds) = self.seconds.checked_add(1) {
32 self.seconds = seconds;
33 self.nanos -= NANOS_PER_SECOND;
34 } else {
35 debug_assert_eq!(self.seconds, i64::MAX);
37 self.nanos = NANOS_MAX;
38 }
39 } else if self.seconds > 0 && self.nanos < 0 {
40 if let Some(seconds) = self.seconds.checked_sub(1) {
41 self.seconds = seconds;
42 self.nanos += NANOS_PER_SECOND;
43 } else {
44 debug_assert_eq!(self.seconds, i64::MIN);
46 self.nanos = -NANOS_MAX;
47 }
48 }
49 }
53
54 pub fn normalized(&self) -> Self {
60 let mut result = *self;
61 result.normalize();
62 result
63 }
64}
65
66impl Name for Duration {
67 const PACKAGE: &'static str = PACKAGE;
68 const NAME: &'static str = "Duration";
69
70 fn type_url() -> String {
71 type_url_for::<Self>()
72 }
73}
74
75impl TryFrom<time::Duration> for Duration {
76 type Error = DurationError;
77
78 fn try_from(duration: time::Duration) -> Result<Duration, DurationError> {
80 let seconds = i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?;
81 let nanos = duration.subsec_nanos() as i32;
82
83 let duration = Duration { seconds, nanos };
84 Ok(duration.normalized())
85 }
86}
87
88impl TryFrom<Duration> for time::Duration {
89 type Error = DurationError;
90
91 fn try_from(mut duration: Duration) -> Result<time::Duration, DurationError> {
93 duration.normalize();
94 if duration.seconds >= 0 && duration.nanos >= 0 {
95 Ok(time::Duration::new(
96 duration.seconds as u64,
97 duration.nanos as u32,
98 ))
99 } else {
100 Err(DurationError::NegativeDuration(time::Duration::new(
101 (-duration.seconds) as u64,
102 (-duration.nanos) as u32,
103 )))
104 }
105 }
106}
107
108impl fmt::Display for Duration {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 let d = self.normalized();
111 if self.seconds < 0 || self.nanos < 0 {
112 write!(f, "-")?;
113 }
114 write!(f, "{}", d.seconds.abs())?;
115
116 let nanos = d.nanos.abs();
118 if nanos == 0 {
119 write!(f, "s")
120 } else if nanos % 1_000_000 == 0 {
121 write!(f, ".{:03}s", nanos / 1_000_000)
122 } else if nanos % 1_000 == 0 {
123 write!(f, ".{:06}s", nanos / 1_000)
124 } else {
125 write!(f, ".{nanos:09}s")
126 }
127 }
128}
129
130#[derive(Debug, PartialEq)]
132#[non_exhaustive]
133pub enum DurationError {
134 ParseFailure,
140
141 NegativeDuration(time::Duration),
145
146 OutOfRange,
151}
152
153impl fmt::Display for DurationError {
154 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155 match self {
156 DurationError::ParseFailure => write!(f, "failed to parse duration"),
157 DurationError::NegativeDuration(duration) => {
158 write!(f, "failed to convert negative duration: {duration:?}")
159 }
160 DurationError::OutOfRange => {
161 write!(f, "failed to convert duration out of range")
162 }
163 }
164 }
165}
166
167impl core::error::Error for DurationError {}
168
169impl FromStr for Duration {
170 type Err = DurationError;
171
172 fn from_str(s: &str) -> Result<Duration, DurationError> {
173 datetime::parse_duration(s).ok_or(DurationError::ParseFailure)
174 }
175}
176
177#[cfg(feature = "chrono")]
178mod chrono {
179 use ::chrono::TimeDelta;
180
181 use super::*;
182
183 impl From<::chrono::TimeDelta> for Duration {
184 fn from(value: ::chrono::TimeDelta) -> Self {
185 let mut result = Self {
186 seconds: value.num_seconds(),
187 nanos: value.subsec_nanos(),
188 };
189 result.normalize();
190 result
191 }
192 }
193
194 impl TryFrom<Duration> for ::chrono::TimeDelta {
195 type Error = DurationError;
196
197 fn try_from(mut value: Duration) -> Result<TimeDelta, duration::DurationError> {
198 value.normalize();
199 let seconds = TimeDelta::try_seconds(value.seconds).ok_or(DurationError::OutOfRange)?;
200 let nanos = TimeDelta::nanoseconds(value.nanos.into());
201 seconds.checked_add(&nanos).ok_or(DurationError::OutOfRange)
202 }
203 }
204}
205
206#[cfg(kani)]
207mod proofs {
208 use super::*;
209
210 #[cfg(feature = "std")]
211 #[kani::proof]
212 fn check_duration_std_roundtrip() {
213 let seconds = kani::any();
214 let nanos = kani::any();
215 kani::assume(nanos < 1_000_000_000);
216 let std_duration = std::time::Duration::new(seconds, nanos);
217 let Ok(prost_duration) = Duration::try_from(std_duration) else {
218 return;
220 };
221 assert_eq!(
222 time::Duration::try_from(prost_duration).unwrap(),
223 std_duration
224 );
225
226 if std_duration != time::Duration::default() {
227 let neg_prost_duration = Duration {
228 seconds: -prost_duration.seconds,
229 nanos: -prost_duration.nanos,
230 };
231
232 assert!(matches!(
233 time::Duration::try_from(neg_prost_duration),
234 Err(DurationError::NegativeDuration(d)) if d == std_duration,
235 ))
236 }
237 }
238
239 #[cfg(feature = "std")]
240 #[kani::proof]
241 fn check_duration_std_roundtrip_nanos() {
242 let seconds = 0;
243 let nanos = kani::any();
244 let std_duration = std::time::Duration::new(seconds, nanos);
245 let Ok(prost_duration) = Duration::try_from(std_duration) else {
246 return;
248 };
249 assert_eq!(
250 time::Duration::try_from(prost_duration).unwrap(),
251 std_duration
252 );
253
254 if std_duration != time::Duration::default() {
255 let neg_prost_duration = Duration {
256 seconds: -prost_duration.seconds,
257 nanos: -prost_duration.nanos,
258 };
259
260 assert!(matches!(
261 time::Duration::try_from(neg_prost_duration),
262 Err(DurationError::NegativeDuration(d)) if d == std_duration,
263 ))
264 }
265 }
266
267 #[cfg(feature = "chrono")]
268 #[kani::proof]
269 fn check_duration_chrono_roundtrip() {
270 let seconds = kani::any();
271 let nanos = kani::any();
272 let prost_duration = Duration { seconds, nanos };
273 match ::chrono::TimeDelta::try_from(prost_duration) {
274 Err(DurationError::OutOfRange) => {
275 return;
277 }
278 Err(err) => {
279 panic!("Unexpected error: {err}")
280 }
281 Ok(chrono_duration) => {
282 let mut normalized_prost_duration = prost_duration;
283 normalized_prost_duration.normalize();
284 assert_eq!(
285 Duration::try_from(chrono_duration).unwrap(),
286 normalized_prost_duration
287 );
288 }
289 }
290 }
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[cfg(feature = "std")]
298 #[test]
299 fn test_duration_from_str() {
300 assert_eq!(
301 Duration::from_str("0s"),
302 Ok(Duration {
303 seconds: 0,
304 nanos: 0
305 })
306 );
307 assert_eq!(
308 Duration::from_str("123s"),
309 Ok(Duration {
310 seconds: 123,
311 nanos: 0
312 })
313 );
314 assert_eq!(
315 Duration::from_str("0.123s"),
316 Ok(Duration {
317 seconds: 0,
318 nanos: 123_000_000
319 })
320 );
321 assert_eq!(
322 Duration::from_str("-123s"),
323 Ok(Duration {
324 seconds: -123,
325 nanos: 0
326 })
327 );
328 assert_eq!(
329 Duration::from_str("-0.123s"),
330 Ok(Duration {
331 seconds: 0,
332 nanos: -123_000_000
333 })
334 );
335 assert_eq!(
336 Duration::from_str("22041211.6666666666666s"),
337 Ok(Duration {
338 seconds: 22041211,
339 nanos: 666_666_666
340 })
341 );
342 }
343
344 #[cfg(feature = "std")]
345 #[test]
346 fn test_format_duration() {
347 assert_eq!(
348 "0s",
349 Duration {
350 seconds: 0,
351 nanos: 0
352 }
353 .to_string()
354 );
355 assert_eq!(
356 "123s",
357 Duration {
358 seconds: 123,
359 nanos: 0
360 }
361 .to_string()
362 );
363 assert_eq!(
364 "0.123s",
365 Duration {
366 seconds: 0,
367 nanos: 123_000_000
368 }
369 .to_string()
370 );
371 assert_eq!(
372 "-123s",
373 Duration {
374 seconds: -123,
375 nanos: 0
376 }
377 .to_string()
378 );
379 assert_eq!(
380 "-0.123s",
381 Duration {
382 seconds: 0,
383 nanos: -123_000_000
384 }
385 .to_string()
386 );
387 }
388
389 #[cfg(feature = "std")]
390 #[test]
391 fn check_duration_try_from_negative_nanos() {
392 let seconds: u64 = 0;
393 let nanos: u32 = 1;
394 let std_duration = std::time::Duration::new(seconds, nanos);
395
396 let neg_prost_duration = Duration {
397 seconds: 0,
398 nanos: -1,
399 };
400
401 assert!(matches!(
402 time::Duration::try_from(neg_prost_duration),
403 Err(DurationError::NegativeDuration(d)) if d == std_duration,
404 ))
405 }
406
407 #[test]
408 fn check_duration_normalize() {
409 #[rustfmt::skip] let cases = [
411 (line!(), 0, 0, 0, 0),
414 (line!(), 1, 1, 1, 1),
415 (line!(), -1, -1, -1, -1),
416 (line!(), 0, 999_999_999, 0, 999_999_999),
417 (line!(), 0, -999_999_999, 0, -999_999_999),
418 (line!(), 0, 1_000_000_000, 1, 0),
419 (line!(), 0, -1_000_000_000, -1, 0),
420 (line!(), 0, 1_000_000_001, 1, 1),
421 (line!(), 0, -1_000_000_001, -1, -1),
422 (line!(), -1, 1, 0, -999_999_999),
423 (line!(), 1, -1, 0, 999_999_999),
424 (line!(), -1, 1_000_000_000, 0, 0),
425 (line!(), 1, -1_000_000_000, 0, 0),
426 (line!(), i64::MIN , 0, i64::MIN , 0),
427 (line!(), i64::MIN + 1, 0, i64::MIN + 1, 0),
428 (line!(), i64::MIN , 1, i64::MIN + 1, -999_999_999),
429 (line!(), i64::MIN , 1_000_000_000, i64::MIN + 1, 0),
430 (line!(), i64::MIN , -1_000_000_000, i64::MIN , -999_999_999),
431 (line!(), i64::MIN + 1, -1_000_000_000, i64::MIN , 0),
432 (line!(), i64::MIN + 2, -1_000_000_000, i64::MIN + 1, 0),
433 (line!(), i64::MIN , -1_999_999_998, i64::MIN , -999_999_999),
434 (line!(), i64::MIN + 1, -1_999_999_998, i64::MIN , -999_999_998),
435 (line!(), i64::MIN + 2, -1_999_999_998, i64::MIN + 1, -999_999_998),
436 (line!(), i64::MIN , -1_999_999_999, i64::MIN , -999_999_999),
437 (line!(), i64::MIN + 1, -1_999_999_999, i64::MIN , -999_999_999),
438 (line!(), i64::MIN + 2, -1_999_999_999, i64::MIN + 1, -999_999_999),
439 (line!(), i64::MIN , -2_000_000_000, i64::MIN , -999_999_999),
440 (line!(), i64::MIN + 1, -2_000_000_000, i64::MIN , -999_999_999),
441 (line!(), i64::MIN + 2, -2_000_000_000, i64::MIN , 0),
442 (line!(), i64::MIN , -999_999_998, i64::MIN , -999_999_998),
443 (line!(), i64::MIN + 1, -999_999_998, i64::MIN + 1, -999_999_998),
444 (line!(), i64::MAX , 0, i64::MAX , 0),
445 (line!(), i64::MAX - 1, 0, i64::MAX - 1, 0),
446 (line!(), i64::MAX , -1, i64::MAX - 1, 999_999_999),
447 (line!(), i64::MAX , 1_000_000_000, i64::MAX , 999_999_999),
448 (line!(), i64::MAX - 1, 1_000_000_000, i64::MAX , 0),
449 (line!(), i64::MAX - 2, 1_000_000_000, i64::MAX - 1, 0),
450 (line!(), i64::MAX , 1_999_999_998, i64::MAX , 999_999_999),
451 (line!(), i64::MAX - 1, 1_999_999_998, i64::MAX , 999_999_998),
452 (line!(), i64::MAX - 2, 1_999_999_998, i64::MAX - 1, 999_999_998),
453 (line!(), i64::MAX , 1_999_999_999, i64::MAX , 999_999_999),
454 (line!(), i64::MAX - 1, 1_999_999_999, i64::MAX , 999_999_999),
455 (line!(), i64::MAX - 2, 1_999_999_999, i64::MAX - 1, 999_999_999),
456 (line!(), i64::MAX , 2_000_000_000, i64::MAX , 999_999_999),
457 (line!(), i64::MAX - 1, 2_000_000_000, i64::MAX , 999_999_999),
458 (line!(), i64::MAX - 2, 2_000_000_000, i64::MAX , 0),
459 (line!(), i64::MAX , 999_999_998, i64::MAX , 999_999_998),
460 (line!(), i64::MAX - 1, 999_999_998, i64::MAX - 1, 999_999_998),
461 ];
462
463 for case in cases.iter() {
464 let test_duration = Duration {
465 seconds: case.1,
466 nanos: case.2,
467 };
468
469 assert_eq!(
470 test_duration.normalized(),
471 Duration {
472 seconds: case.3,
473 nanos: case.4,
474 },
475 "test case on line {} doesn't match",
476 case.0,
477 );
478 }
479 }
480}