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, ".{:09}s", nanos)
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
167#[cfg(feature = "std")]
168impl std::error::Error for DurationError {}
169
170impl FromStr for Duration {
171 type Err = DurationError;
172
173 fn from_str(s: &str) -> Result<Duration, DurationError> {
174 datetime::parse_duration(s).ok_or(DurationError::ParseFailure)
175 }
176}
177
178#[cfg(feature = "chrono")]
179mod chrono {
180 use ::chrono::TimeDelta;
181
182 use super::*;
183
184 impl From<::chrono::TimeDelta> for Duration {
185 fn from(value: ::chrono::TimeDelta) -> Self {
186 let mut result = Self {
187 seconds: value.num_seconds(),
188 nanos: value.subsec_nanos(),
189 };
190 result.normalize();
191 result
192 }
193 }
194
195 impl TryFrom<Duration> for ::chrono::TimeDelta {
196 type Error = DurationError;
197
198 fn try_from(mut value: Duration) -> Result<TimeDelta, duration::DurationError> {
199 value.normalize();
200 let seconds = TimeDelta::try_seconds(value.seconds).ok_or(DurationError::OutOfRange)?;
201 let nanos = TimeDelta::nanoseconds(value.nanos.into());
202 seconds.checked_add(&nanos).ok_or(DurationError::OutOfRange)
203 }
204 }
205}
206
207#[cfg(kani)]
208mod proofs {
209 use super::*;
210
211 #[cfg(feature = "std")]
212 #[kani::proof]
213 fn check_duration_std_roundtrip() {
214 let seconds = kani::any();
215 let nanos = kani::any();
216 kani::assume(nanos < 1_000_000_000);
217 let std_duration = std::time::Duration::new(seconds, nanos);
218 let Ok(prost_duration) = Duration::try_from(std_duration) else {
219 return;
221 };
222 assert_eq!(
223 time::Duration::try_from(prost_duration).unwrap(),
224 std_duration
225 );
226
227 if std_duration != time::Duration::default() {
228 let neg_prost_duration = Duration {
229 seconds: -prost_duration.seconds,
230 nanos: -prost_duration.nanos,
231 };
232
233 assert!(matches!(
234 time::Duration::try_from(neg_prost_duration),
235 Err(DurationError::NegativeDuration(d)) if d == std_duration,
236 ))
237 }
238 }
239
240 #[cfg(feature = "std")]
241 #[kani::proof]
242 fn check_duration_std_roundtrip_nanos() {
243 let seconds = 0;
244 let nanos = kani::any();
245 let std_duration = std::time::Duration::new(seconds, nanos);
246 let Ok(prost_duration) = Duration::try_from(std_duration) else {
247 return;
249 };
250 assert_eq!(
251 time::Duration::try_from(prost_duration).unwrap(),
252 std_duration
253 );
254
255 if std_duration != time::Duration::default() {
256 let neg_prost_duration = Duration {
257 seconds: -prost_duration.seconds,
258 nanos: -prost_duration.nanos,
259 };
260
261 assert!(matches!(
262 time::Duration::try_from(neg_prost_duration),
263 Err(DurationError::NegativeDuration(d)) if d == std_duration,
264 ))
265 }
266 }
267
268 #[cfg(feature = "chrono")]
269 #[kani::proof]
270 fn check_duration_chrono_roundtrip() {
271 let seconds = kani::any();
272 let nanos = kani::any();
273 let prost_duration = Duration { seconds, nanos };
274 match ::chrono::TimeDelta::try_from(prost_duration) {
275 Err(DurationError::OutOfRange) => {
276 return;
278 }
279 Err(err) => {
280 panic!("Unexpected error: {err}")
281 }
282 Ok(chrono_duration) => {
283 let mut normalized_prost_duration = prost_duration;
284 normalized_prost_duration.normalize();
285 assert_eq!(
286 Duration::try_from(chrono_duration).unwrap(),
287 normalized_prost_duration
288 );
289 }
290 }
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[cfg(feature = "std")]
299 #[test]
300 fn test_duration_from_str() {
301 assert_eq!(
302 Duration::from_str("0s"),
303 Ok(Duration {
304 seconds: 0,
305 nanos: 0
306 })
307 );
308 assert_eq!(
309 Duration::from_str("123s"),
310 Ok(Duration {
311 seconds: 123,
312 nanos: 0
313 })
314 );
315 assert_eq!(
316 Duration::from_str("0.123s"),
317 Ok(Duration {
318 seconds: 0,
319 nanos: 123_000_000
320 })
321 );
322 assert_eq!(
323 Duration::from_str("-123s"),
324 Ok(Duration {
325 seconds: -123,
326 nanos: 0
327 })
328 );
329 assert_eq!(
330 Duration::from_str("-0.123s"),
331 Ok(Duration {
332 seconds: 0,
333 nanos: -123_000_000
334 })
335 );
336 assert_eq!(
337 Duration::from_str("22041211.6666666666666s"),
338 Ok(Duration {
339 seconds: 22041211,
340 nanos: 666_666_666
341 })
342 );
343 }
344
345 #[cfg(feature = "std")]
346 #[test]
347 fn test_format_duration() {
348 assert_eq!(
349 "0s",
350 Duration {
351 seconds: 0,
352 nanos: 0
353 }
354 .to_string()
355 );
356 assert_eq!(
357 "123s",
358 Duration {
359 seconds: 123,
360 nanos: 0
361 }
362 .to_string()
363 );
364 assert_eq!(
365 "0.123s",
366 Duration {
367 seconds: 0,
368 nanos: 123_000_000
369 }
370 .to_string()
371 );
372 assert_eq!(
373 "-123s",
374 Duration {
375 seconds: -123,
376 nanos: 0
377 }
378 .to_string()
379 );
380 assert_eq!(
381 "-0.123s",
382 Duration {
383 seconds: 0,
384 nanos: -123_000_000
385 }
386 .to_string()
387 );
388 }
389
390 #[cfg(feature = "std")]
391 #[test]
392 fn check_duration_try_from_negative_nanos() {
393 let seconds: u64 = 0;
394 let nanos: u32 = 1;
395 let std_duration = std::time::Duration::new(seconds, nanos);
396
397 let neg_prost_duration = Duration {
398 seconds: 0,
399 nanos: -1,
400 };
401
402 assert!(matches!(
403 time::Duration::try_from(neg_prost_duration),
404 Err(DurationError::NegativeDuration(d)) if d == std_duration,
405 ))
406 }
407
408 #[test]
409 fn check_duration_normalize() {
410 #[rustfmt::skip] let cases = [
412 (line!(), 0, 0, 0, 0),
415 (line!(), 1, 1, 1, 1),
416 (line!(), -1, -1, -1, -1),
417 (line!(), 0, 999_999_999, 0, 999_999_999),
418 (line!(), 0, -999_999_999, 0, -999_999_999),
419 (line!(), 0, 1_000_000_000, 1, 0),
420 (line!(), 0, -1_000_000_000, -1, 0),
421 (line!(), 0, 1_000_000_001, 1, 1),
422 (line!(), 0, -1_000_000_001, -1, -1),
423 (line!(), -1, 1, 0, -999_999_999),
424 (line!(), 1, -1, 0, 999_999_999),
425 (line!(), -1, 1_000_000_000, 0, 0),
426 (line!(), 1, -1_000_000_000, 0, 0),
427 (line!(), i64::MIN , 0, i64::MIN , 0),
428 (line!(), i64::MIN + 1, 0, i64::MIN + 1, 0),
429 (line!(), i64::MIN , 1, i64::MIN + 1, -999_999_999),
430 (line!(), i64::MIN , 1_000_000_000, i64::MIN + 1, 0),
431 (line!(), i64::MIN , -1_000_000_000, i64::MIN , -999_999_999),
432 (line!(), i64::MIN + 1, -1_000_000_000, i64::MIN , 0),
433 (line!(), i64::MIN + 2, -1_000_000_000, i64::MIN + 1, 0),
434 (line!(), i64::MIN , -1_999_999_998, i64::MIN , -999_999_999),
435 (line!(), i64::MIN + 1, -1_999_999_998, i64::MIN , -999_999_998),
436 (line!(), i64::MIN + 2, -1_999_999_998, i64::MIN + 1, -999_999_998),
437 (line!(), i64::MIN , -1_999_999_999, i64::MIN , -999_999_999),
438 (line!(), i64::MIN + 1, -1_999_999_999, i64::MIN , -999_999_999),
439 (line!(), i64::MIN + 2, -1_999_999_999, i64::MIN + 1, -999_999_999),
440 (line!(), i64::MIN , -2_000_000_000, i64::MIN , -999_999_999),
441 (line!(), i64::MIN + 1, -2_000_000_000, i64::MIN , -999_999_999),
442 (line!(), i64::MIN + 2, -2_000_000_000, i64::MIN , 0),
443 (line!(), i64::MIN , -999_999_998, i64::MIN , -999_999_998),
444 (line!(), i64::MIN + 1, -999_999_998, i64::MIN + 1, -999_999_998),
445 (line!(), i64::MAX , 0, i64::MAX , 0),
446 (line!(), i64::MAX - 1, 0, i64::MAX - 1, 0),
447 (line!(), i64::MAX , -1, i64::MAX - 1, 999_999_999),
448 (line!(), i64::MAX , 1_000_000_000, i64::MAX , 999_999_999),
449 (line!(), i64::MAX - 1, 1_000_000_000, i64::MAX , 0),
450 (line!(), i64::MAX - 2, 1_000_000_000, i64::MAX - 1, 0),
451 (line!(), i64::MAX , 1_999_999_998, i64::MAX , 999_999_999),
452 (line!(), i64::MAX - 1, 1_999_999_998, i64::MAX , 999_999_998),
453 (line!(), i64::MAX - 2, 1_999_999_998, i64::MAX - 1, 999_999_998),
454 (line!(), i64::MAX , 1_999_999_999, i64::MAX , 999_999_999),
455 (line!(), i64::MAX - 1, 1_999_999_999, i64::MAX , 999_999_999),
456 (line!(), i64::MAX - 2, 1_999_999_999, i64::MAX - 1, 999_999_999),
457 (line!(), i64::MAX , 2_000_000_000, i64::MAX , 999_999_999),
458 (line!(), i64::MAX - 1, 2_000_000_000, i64::MAX , 999_999_999),
459 (line!(), i64::MAX - 2, 2_000_000_000, i64::MAX , 0),
460 (line!(), i64::MAX , 999_999_998, i64::MAX , 999_999_998),
461 (line!(), i64::MAX - 1, 999_999_998, i64::MAX - 1, 999_999_998),
462 ];
463
464 for case in cases.iter() {
465 let test_duration = Duration {
466 seconds: case.1,
467 nanos: case.2,
468 };
469
470 assert_eq!(
471 test_duration.normalized(),
472 Duration {
473 seconds: case.3,
474 nanos: case.4,
475 },
476 "test case on line {} doesn't match",
477 case.0,
478 );
479 }
480 }
481}