1#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
35#[non_exhaustive]
36pub struct Duration {
37 seconds: i64,
43
44 nanos: i32,
53}
54
55#[derive(thiserror::Error, Debug, PartialEq)]
57pub enum DurationError {
58 #[error("seconds and/or nanoseconds out of range")]
60 OutOfRange(),
61
62 #[error("if seconds and nanoseconds are not zero, they must have the same sign")]
64 MismatchedSigns(),
65
66 #[error("cannot serialize the duration")]
68 Serializate(),
69
70 #[error("cannot deserialize the duration: {0:?}")]
72 Deserialize(String),
73}
74
75type Error = DurationError;
76
77impl Duration {
78 const NS: i32 = 1_000_000_000;
79
80 pub const MAX_SECONDS: i64 = 315_576_000_000;
82
83 pub const MIN_SECONDS: i64 = -Self::MAX_SECONDS;
85
86 pub const MAX_NANOS: i32 = Self::NS - 1;
88
89 pub const MIN_NANOS: i32 = -Self::MAX_NANOS;
91
92 pub fn new(seconds: i64, nanos: i32) -> Result<Self, Error> {
104 if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
105 return Err(Error::OutOfRange());
106 }
107 if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
108 return Err(Error::OutOfRange());
109 }
110 if (seconds != 0 && nanos != 0) && ((seconds < 0) != (nanos < 0)) {
111 return Err(Error::MismatchedSigns());
112 }
113 Ok(Self { seconds, nanos })
114 }
115
116 pub fn clamp(seconds: i64, nanos: i32) -> Self {
131 let mut seconds = seconds;
132 seconds = seconds.saturating_add((nanos / Self::NS) as i64);
133 let mut nanos = nanos % Self::NS;
134 if seconds > 0 && nanos < 0 {
135 seconds = seconds.saturating_sub(1);
136 nanos += Self::NS;
137 } else if seconds < 0 && nanos > 0 {
138 seconds = seconds.saturating_add(1);
139 nanos = -(Self::NS - nanos);
140 }
141 if seconds > Self::MAX_SECONDS {
142 return Self {
143 seconds: Self::MAX_SECONDS,
144 nanos: 0,
145 };
146 }
147 if seconds < Self::MIN_SECONDS {
148 return Self {
149 seconds: Self::MIN_SECONDS,
150 nanos: 0,
151 };
152 }
153 Self { seconds, nanos }
154 }
155
156 pub fn seconds(&self) -> i64 {
158 self.seconds
159 }
160
161 pub fn nanos(&self) -> i32 {
163 self.nanos
164 }
165}
166
167impl crate::message::Message for Duration {
168 fn typename() -> &'static str {
169 "type.googleapis.com/google.protobuf.Duration"
170 }
171 fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
172 crate::message::to_json_string(self)
173 }
174 fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
175 crate::message::from_value(map)
176 }
177}
178
179impl From<&Duration> for String {
181 fn from(duration: &Duration) -> String {
182 let sign = if duration.seconds < 0 || duration.nanos < 0 {
183 "-"
184 } else {
185 ""
186 };
187 if duration.nanos == 0 {
188 return format!("{sign}{}s", duration.seconds.abs());
189 }
190 let ns = format!("{:09}", duration.nanos.abs());
191 format!(
192 "{sign}{}.{}s",
193 duration.seconds.abs(),
194 ns.trim_end_matches('0')
195 )
196 }
197}
198
199impl TryFrom<&str> for Duration {
201 type Error = DurationError;
202 fn try_from(value: &str) -> Result<Self, Self::Error> {
203 if !value.ends_with('s') {
204 return Err(DurationError::Deserialize("missing trailing 's'".into()));
205 }
206 let digits = &value[..(value.len() - 1)];
207 let (sign, digits) = if let Some(stripped) = digits.strip_prefix('-') {
208 (-1, stripped)
209 } else {
210 (1, &digits[0..])
211 };
212 let mut split = digits.splitn(2, '.');
213 let (seconds, nanos) = (split.next(), split.next());
214 let seconds = seconds
215 .map(str::parse::<i64>)
216 .transpose()
217 .map_err(|e| DurationError::Deserialize(format!("{e}")))?
218 .unwrap_or(0);
219 let nanos = nanos
220 .map(|s| {
221 let pad = "000000000";
222 format!("{s}{}", &pad[s.len()..])
223 })
224 .map(|s| s.parse::<i32>())
225 .transpose()
226 .map_err(|e| DurationError::Deserialize(format!("{e}")))?
227 .unwrap_or(0);
228
229 Duration::new(sign * seconds, sign as i32 * nanos)
230 }
231}
232
233impl TryFrom<std::time::Duration> for Duration {
235 type Error = DurationError;
236
237 fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
238 if value.as_secs() > (i64::MAX as u64) {
239 return Err(Error::OutOfRange());
240 }
241 assert!(value.as_secs() <= (i64::MAX as u64));
242 assert!(value.subsec_nanos() <= (i32::MAX as u32));
243 Self::new(value.as_secs() as i64, value.subsec_nanos() as i32)
244 }
245}
246
247impl TryFrom<Duration> for std::time::Duration {
249 type Error = DurationError;
250
251 fn try_from(value: Duration) -> Result<Self, Self::Error> {
252 if value.seconds < 0 {
253 return Err(Error::OutOfRange());
254 }
255 if value.nanos < 0 {
256 return Err(Error::OutOfRange());
257 }
258 Ok(Self::new(value.seconds as u64, value.nanos as u32))
259 }
260}
261
262#[cfg(feature = "time")]
266impl TryFrom<time::Duration> for Duration {
267 type Error = DurationError;
268
269 fn try_from(value: time::Duration) -> Result<Self, Self::Error> {
270 Self::new(value.whole_seconds(), value.subsec_nanoseconds())
271 }
272}
273
274#[cfg(feature = "time")]
279impl From<Duration> for time::Duration {
280 fn from(value: Duration) -> Self {
281 Self::new(value.seconds(), value.nanos())
282 }
283}
284
285#[cfg(feature = "chrono")]
287impl TryFrom<chrono::Duration> for Duration {
288 type Error = DurationError;
289
290 fn try_from(value: chrono::Duration) -> Result<Self, Self::Error> {
291 Self::new(value.num_seconds(), value.subsec_nanos())
292 }
293}
294
295#[cfg(feature = "chrono")]
297impl From<Duration> for chrono::Duration {
298 fn from(value: Duration) -> Self {
299 Self::seconds(value.seconds) + Self::nanoseconds(value.nanos as i64)
300 }
301}
302
303impl serde::ser::Serialize for Duration {
305 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
306 where
307 S: serde::ser::Serializer,
308 {
309 let formatted = String::from(self);
310 formatted.serialize(serializer)
311 }
312}
313
314struct DurationVisitor;
315
316impl serde::de::Visitor<'_> for DurationVisitor {
317 type Value = Duration;
318
319 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
320 formatter.write_str("a string with a duration in Google format ([sign]{seconds}.{nanos}s)")
321 }
322
323 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
324 where
325 E: serde::de::Error,
326 {
327 let d = Duration::try_from(value).map_err(E::custom)?;
328 Ok(d)
329 }
330}
331
332impl<'de> serde::de::Deserialize<'de> for Duration {
334 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
335 where
336 D: serde::Deserializer<'de>,
337 {
338 deserializer.deserialize_str(DurationVisitor)
339 }
340}
341
342#[cfg(test)]
343mod test {
344 use super::*;
345 use serde_json::json;
346 use test_case::test_case;
347 type Result = std::result::Result<(), Box<dyn std::error::Error>>;
348
349 #[test]
351 fn zero() -> Result {
352 let proto = Duration {
353 seconds: 0,
354 nanos: 0,
355 };
356 let json = serde_json::to_value(&proto)?;
357 let expected = json!(r#"0s"#);
358 assert_eq!(json, expected);
359 let roundtrip = serde_json::from_value::<Duration>(json)?;
360 assert_eq!(proto, roundtrip);
361 Ok(())
362 }
363
364 const SECONDS_IN_DAY: i64 = 24 * 60 * 60;
367 const SECONDS_IN_YEAR: i64 = 365 * SECONDS_IN_DAY + SECONDS_IN_DAY / 4;
370
371 #[test_case(10_000 * SECONDS_IN_YEAR , 0 ; "exactly 10,000 years")]
372 #[test_case(- 10_000 * SECONDS_IN_YEAR , 0 ; "exactly negative 10,000 years")]
373 #[test_case(10_000 * SECONDS_IN_YEAR , 999_999_999 ; "exactly 10,000 years and 999,999,999 nanos"
374 )]
375 #[test_case(- 10_000 * SECONDS_IN_YEAR , -999_999_999 ; "exactly negative 10,000 years and 999,999,999 nanos"
376 )]
377 #[test_case(0, 999_999_999 ; "exactly 999,999,999 nanos")]
378 #[test_case(0 , -999_999_999 ; "exactly negative 999,999,999 nanos")]
379 fn edge_of_range(seconds: i64, nanos: i32) -> Result {
380 let d = Duration::new(seconds, nanos)?;
381 assert_eq!(seconds, d.seconds());
382 assert_eq!(nanos, d.nanos());
383 Ok(())
384 }
385
386 #[test_case(10_000 * SECONDS_IN_YEAR + 1, 0 ; "more seconds than in 10,000 years")]
387 #[test_case(- 10_000 * SECONDS_IN_YEAR - 1, 0 ; "more negative seconds than in -10,000 years")]
388 #[test_case(0, 1_000_000_000 ; "too many positive nanoseconds")]
389 #[test_case(0, -1_000_000_000 ; "too many negative nanoseconds")]
390 fn out_of_range(seconds: i64, nanos: i32) -> Result {
391 let d = Duration::new(seconds, nanos);
392 assert_eq!(d, Err(Error::OutOfRange()));
393 Ok(())
394 }
395
396 #[test_case(1 , -1 ; "mismatched sign case 1")]
397 #[test_case(-1 , 1 ; "mismatched sign case 2")]
398 fn mismatched_sign(seconds: i64, nanos: i32) -> Result {
399 let d = Duration::new(seconds, nanos);
400 assert_eq!(d, Err(Error::MismatchedSigns()));
401 Ok(())
402 }
403
404 #[test_case(20_000 * SECONDS_IN_YEAR, 0, 10_000 * SECONDS_IN_YEAR, 0 ; "too many positive seconds"
405 )]
406 #[test_case(-20_000 * SECONDS_IN_YEAR, 0, -10_000 * SECONDS_IN_YEAR, 0 ; "too many negative seconds"
407 )]
408 #[test_case(10_000 * SECONDS_IN_YEAR - 1, 1_999_999_999, 10_000 * SECONDS_IN_YEAR, 999_999_999 ; "upper edge of range"
409 )]
410 #[test_case(-10_000 * SECONDS_IN_YEAR + 1, -1_999_999_999, -10_000 * SECONDS_IN_YEAR, -999_999_999 ; "lower edge of range"
411 )]
412 #[test_case(10_000 * SECONDS_IN_YEAR - 1 , 2 * 1_000_000_000_i32, 10_000 * SECONDS_IN_YEAR, 0 ; "nanos push over 10,000 years"
413 )]
414 #[test_case(-10_000 * SECONDS_IN_YEAR + 1, -2 * 1_000_000_000_i32, -10_000 * SECONDS_IN_YEAR, 0 ; "one push under -10,000 years"
415 )]
416 #[test_case(0, 0, 0, 0 ; "all inputs are zero")]
417 #[test_case(1, 0, 1, 0 ; "positive seconds and zero nanos")]
418 #[test_case(1, 200_000, 1, 200_000 ; "positive seconds and nanos")]
419 #[test_case(-1, 0, -1, 0; "negative seconds and zero nanos")]
420 #[test_case(-1, -500_000_000, -1, -500_000_000; "negative seconds and nanos")]
421 #[test_case(2, -400_000_000, 1, 600_000_000; "positive seconds and negative nanos")]
422 #[test_case(-2, 400_000_000, -1, -600_000_000; "negative seconds and positive nanos")]
423 fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) -> Result {
424 let got = Duration::clamp(seconds, nanos);
425 let want = Duration {
426 seconds: want_seconds,
427 nanos: want_nanos,
428 };
429 assert_eq!(want, got);
430 Ok(())
431 }
432
433 #[test_case(0, 0, "0s" ; "zero")]
435 #[test_case(0, 2, "0.000000002s" ; "2ns")]
436 #[test_case(0, 200_000_000, "0.2s" ; "200ms")]
437 #[test_case(12, 0, "12s"; "round positive seconds")]
438 #[test_case(12, 123, "12.000000123s"; "positive seconds and nanos")]
439 #[test_case(12, 123_000, "12.000123s"; "positive seconds and micros")]
440 #[test_case(12, 123_000_000, "12.123s"; "positive seconds and millis")]
441 #[test_case(12, 123_456_789, "12.123456789s"; "positive seconds and full nanos")]
442 #[test_case(-12, -0, "-12s"; "round negative seconds")]
443 #[test_case(-12, -123, "-12.000000123s"; "negative seconds and nanos")]
444 #[test_case(-12, -123_000, "-12.000123s"; "negative seconds and micros")]
445 #[test_case(-12, -123_000_000, "-12.123s"; "negative seconds and millis")]
446 #[test_case(-12, -123_456_789, "-12.123456789s"; "negative seconds and full nanos")]
447 #[test_case(-10_000 * SECONDS_IN_YEAR, -999_999_999, "-315576000000.999999999s"; "range edge start"
448 )]
449 #[test_case(10_000 * SECONDS_IN_YEAR, 999_999_999, "315576000000.999999999s"; "range edge end")]
450 fn roundtrip(seconds: i64, nanos: i32, want: &str) -> Result {
451 let input = Duration::new(seconds, nanos)?;
452 let got = serde_json::to_value(&input)?
453 .as_str()
454 .map(str::to_string)
455 .ok_or("cannot convert value to string")?;
456 assert_eq!(want, got);
457
458 let rt = serde_json::from_value::<Duration>(serde_json::Value::String(got))?;
459 assert_eq!(input, rt);
460 Ok(())
461 }
462
463 #[test_case("-315576000001s"; "range edge start")]
464 #[test_case("315576000001s"; "range edge end")]
465 fn deserialize_out_of_range(input: &str) -> Result {
466 let value = serde_json::to_value(input)?;
467 let got = serde_json::from_value::<Duration>(value);
468 assert!(got.is_err());
469 Ok(())
470 }
471
472 #[test_case(time::Duration::default(), Duration::default() ; "default")]
473 #[test_case(time::Duration::new(0, 0), Duration::new(0, 0).unwrap() ; "zero")]
474 #[test_case(time::Duration::new(10_000 * SECONDS_IN_YEAR , 0), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
475 )]
476 #[test_case(time::Duration::new(-10_000 * SECONDS_IN_YEAR , 0), Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly negative 10,000 years"
477 )]
478 fn from_time_in_range(value: time::Duration, want: Duration) -> Result {
479 let got = Duration::try_from(value)?;
480 assert_eq!(got, want);
481 Ok(())
482 }
483
484 #[test_case(time::Duration::new(10_001 * SECONDS_IN_YEAR, 0) ; "above the range")]
485 #[test_case(time::Duration::new(-10_001 * SECONDS_IN_YEAR, 0) ; "below the range")]
486 fn from_time_out_of_range(value: time::Duration) {
487 let got = Duration::try_from(value);
488 assert_eq!(got, Err(DurationError::OutOfRange()));
489 }
490
491 #[test_case(Duration::default(), time::Duration::default() ; "default")]
492 #[test_case(Duration::new(0, 0).unwrap(), time::Duration::new(0, 0) ; "zero")]
493 #[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(10_000 * SECONDS_IN_YEAR, 0) ; "exactly 10,000 years"
494 )]
495 #[test_case(Duration::new(-10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(-10_000 * SECONDS_IN_YEAR, 0) ; "exactly negative 10,000 years"
496 )]
497 fn to_time_in_range(value: Duration, want: time::Duration) -> Result {
498 let got = time::Duration::from(value);
499 assert_eq!(got, want);
500 Ok(())
501 }
502
503 #[test_case("" ; "empty")]
504 #[test_case("1.0" ; "missing final s")]
505 #[test_case("1.2.3.4s" ; "too many periods")]
506 #[test_case("aaas" ; "not a number")]
507 #[test_case("aaaa.0s" ; "seconds are not a number [aaa]")]
508 #[test_case("1a.0s" ; "seconds are not a number [1a]")]
509 #[test_case("1.aaas" ; "nanos are not a number [aaa]")]
510 #[test_case("1.0as" ; "nanos are not a number [0a]")]
511 fn parse_detect_bad_input(input: &str) -> Result {
512 let got = Duration::try_from(input);
513 assert!(got.is_err());
514 let err = got.err().unwrap();
515 assert!(
516 matches!(err, DurationError::Deserialize(_)),
517 "unexpected error {err:?}"
518 );
519 Ok(())
520 }
521
522 #[test]
523 fn deserialize_unexpected_input_type() -> Result {
524 let got = serde_json::from_value::<Duration>(serde_json::json!({}));
525 assert!(got.is_err());
526 let msg = format!("{got:?}");
527 assert!(msg.contains("duration in Google format"), "message={}", msg);
528 Ok(())
529 }
530
531 #[test_case(std::time::Duration::new(0, 0), Duration::clamp(0, 0))]
532 #[test_case(
533 std::time::Duration::new(0, 400_000_000),
534 Duration::clamp(0, 400_000_000)
535 )]
536 #[test_case(
537 std::time::Duration::new(1, 400_000_000),
538 Duration::clamp(1, 400_000_000)
539 )]
540 #[test_case(std::time::Duration::new(10_000 * SECONDS_IN_YEAR as u64, 999_999_999), Duration::clamp(10_000 * SECONDS_IN_YEAR, 999_999_999))]
541 fn from_std_time_in_range(input: std::time::Duration, want: Duration) {
542 let got = Duration::try_from(input).unwrap();
543 assert_eq!(got, want);
544 }
545
546 #[test_case(std::time::Duration::new(i64::MAX as u64, 0))]
547 #[test_case(std::time::Duration::new(i64::MAX as u64 + 10, 0))]
548 fn from_std_time_out_of_range(input: std::time::Duration) {
549 let got = Duration::try_from(input);
550 assert!(got.is_err(), "{got:?}");
551 }
552
553 #[test_case(chrono::Duration::default(), Duration::default() ; "default")]
554 #[test_case(chrono::Duration::new(0, 0).unwrap(), Duration::new(0, 0).unwrap() ; "zero")]
555 #[test_case(chrono::Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap(), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
556 )]
557 #[test_case(chrono::Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap(), Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly negative 10,000 years"
558 )]
559 fn from_chrono_time_in_range(value: chrono::Duration, want: Duration) -> Result {
560 let got = Duration::try_from(value)?;
561 assert_eq!(got, want);
562 Ok(())
563 }
564
565 #[test_case(Duration::default(), chrono::Duration::default() ; "default")]
566 #[test_case(Duration::new(0, 0).unwrap(), chrono::Duration::new(0, 0).unwrap() ; "zero")]
567 #[test_case(Duration::new(0, 500_000).unwrap(), chrono::Duration::new(0, 500_000).unwrap() ; "500us")]
568 #[test_case(Duration::new(1, 400_000_000).unwrap(), chrono::Duration::new(1, 400_000_000).unwrap() ; "1.4s")]
569 #[test_case(Duration::new(0, -400_000_000).unwrap(), chrono::Duration::new(-1, 600_000_000).unwrap() ; "minus 0.4s")]
570 #[test_case(Duration::new(-1, -400_000_000).unwrap(), chrono::Duration::new(-2, 600_000_000).unwrap() ; "minus 1.4s")]
571 #[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), chrono::Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
572 )]
573 #[test_case(Duration::new(-10_000 * SECONDS_IN_YEAR , 0).unwrap(), chrono::Duration::new(-10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly negative 10,000 years"
574 )]
575 fn to_chrono_time_in_range(value: Duration, want: chrono::Duration) -> Result {
576 let got = chrono::Duration::from(value);
577 assert_eq!(got, want);
578 Ok(())
579 }
580
581 #[test_case(chrono::Duration::new(10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "above the range")]
582 #[test_case(chrono::Duration::new(-10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "below the range")]
583 fn from_chrono_time_out_of_range(value: chrono::Duration) {
584 let got = Duration::try_from(value);
585 assert_eq!(got, Err(DurationError::OutOfRange()));
586 }
587}