google_cloud_wkt/
duration.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15/// Well-known duration representation for Google APIs.
16///
17/// # Examples
18/// ```
19/// # use google_cloud_wkt::{Duration, DurationError};
20/// let d = Duration::try_from("12.34s")?;
21/// assert_eq!(d.seconds(), 12);
22/// assert_eq!(d.nanos(), 340_000_000);
23/// assert_eq!(d, Duration::new(12, 340_000_000)?);
24/// assert_eq!(d, Duration::clamp(12, 340_000_000));
25///
26/// # Ok::<(), DurationError>(())
27/// ```
28///
29/// A Duration represents a signed, fixed-length span of time represented
30/// as a count of seconds and fractions of seconds at nanosecond
31/// resolution. It is independent of any calendar and concepts like "day"
32/// or "month". It is related to [Timestamp](crate::Timestamp) in that the
33/// difference between two Timestamp values is a Duration and it can be added
34/// or subtracted from a Timestamp. Range is approximately +-10,000 years.
35///
36/// # JSON Mapping
37///
38/// In JSON format, the Duration type is encoded as a string rather than an
39/// object, where the string ends in the suffix "s" (indicating seconds) and
40/// is preceded by the number of seconds, with nanoseconds expressed as
41/// fractional seconds. For example, 3 seconds with 0 nanoseconds should be
42/// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should
43/// be expressed in JSON format as "3.000000001s", and 3 seconds and 1
44/// microsecond should be expressed in JSON format as "3.000001s".
45#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
46#[non_exhaustive]
47pub struct Duration {
48    /// Signed seconds of the span of time.
49    ///
50    /// Must be from -315,576,000,000 to +315,576,000,000 inclusive. Note: these
51    /// bounds are computed from:
52    ///     60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
53    seconds: i64,
54
55    /// Signed fractions of a second at nanosecond resolution of the span
56    /// of time.
57    ///
58    /// Durations less than one second are represented with a 0 `seconds` field
59    /// and a positive or negative `nanos` field. For durations
60    /// of one second or more, a non-zero value for the `nanos` field must be
61    /// of the same sign as the `seconds` field. Must be from -999,999,999
62    /// to +999,999,999 inclusive.
63    nanos: i32,
64}
65
66/// Represent failures in converting or creating [Duration] instances.
67///
68/// # Examples
69/// ```
70/// # use google_cloud_wkt::{Duration, DurationError};
71/// let duration = Duration::new(Duration::MAX_SECONDS + 2, 0);
72/// assert!(matches!(duration, Err(DurationError::OutOfRange)));
73///
74/// let duration = Duration::new(0, 1_500_000_000);
75/// assert!(matches!(duration, Err(DurationError::OutOfRange)));
76///
77/// let duration = Duration::new(120, -500_000_000);
78/// assert!(matches!(duration, Err(DurationError::MismatchedSigns)));
79///
80/// let ts = Duration::try_from("invalid");
81/// assert!(matches!(ts, Err(DurationError::Deserialize(_))));
82/// ```
83#[derive(thiserror::Error, Debug)]
84#[non_exhaustive]
85pub enum DurationError {
86    /// One of the components (seconds and/or nanoseconds) was out of range.
87    #[error("seconds and/or nanoseconds out of range")]
88    OutOfRange,
89
90    /// The sign of the seconds component does not match the sign of the nanoseconds component.
91    #[error("if seconds and nanoseconds are not zero, they must have the same sign")]
92    MismatchedSigns,
93
94    /// Cannot deserialize the duration.
95    #[error("cannot deserialize the duration: {0}")]
96    Deserialize(#[source] BoxedError),
97}
98
99type BoxedError = Box<dyn std::error::Error + Send + Sync>;
100type Error = DurationError;
101
102impl Duration {
103    const NS: i32 = 1_000_000_000;
104
105    /// The maximum value for the `seconds` component, approximately 10,000 years.
106    pub const MAX_SECONDS: i64 = 315_576_000_000;
107
108    /// The minimum value for the `seconds` component, approximately -10,000 years.
109    pub const MIN_SECONDS: i64 = -Self::MAX_SECONDS;
110
111    /// The maximum value for the `nanos` component.
112    pub const MAX_NANOS: i32 = Self::NS - 1;
113
114    /// The minimum value for the `nanos` component.
115    pub const MIN_NANOS: i32 = -Self::MAX_NANOS;
116
117    /// Creates a [Duration] from the seconds and nanoseconds component.
118    ///
119    /// # Examples
120    /// ```
121    /// # use google_cloud_wkt::{Duration, DurationError};
122    /// let d = Duration::new(12, 340_000_000)?;
123    /// assert_eq!(String::from(d), "12.34s");
124    ///
125    /// let d = Duration::new(-12, -340_000_000)?;
126    /// assert_eq!(String::from(d), "-12.34s");
127    /// # Ok::<(), DurationError>(())
128    /// ```
129    ///
130    /// # Examples: invalid inputs
131    /// ```
132    /// # use google_cloud_wkt::{Duration, DurationError};
133    /// let d = Duration::new(12, 2_000_000_000);
134    /// assert!(matches!(d, Err(DurationError::OutOfRange)));
135    ///
136    /// let d = Duration::new(-12, 340_000_000);
137    /// assert!(matches!(d, Err(DurationError::MismatchedSigns)));
138    /// # Ok::<(), DurationError>(())
139    /// ```
140    ///
141    /// This function validates the `seconds` and `nanos` components and returns
142    /// an error if either are out of range or their signs do not match.
143    /// Consider using [clamp()][Duration::clamp] to add nanoseconds to seconds
144    /// with carry.
145    ///
146    /// # Parameters
147    ///
148    /// * `seconds` - the seconds in the interval.
149    /// * `nanos` - the nanoseconds *added* to the interval.
150    pub fn new(seconds: i64, nanos: i32) -> Result<Self, Error> {
151        if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
152            return Err(Error::OutOfRange);
153        }
154        if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
155            return Err(Error::OutOfRange);
156        }
157        if (seconds != 0 && nanos != 0) && ((seconds < 0) != (nanos < 0)) {
158            return Err(Error::MismatchedSigns);
159        }
160        Ok(Self { seconds, nanos })
161    }
162
163    /// Create a normalized, clamped [Duration].
164    ///
165    /// # Examples
166    /// ```
167    /// # use google_cloud_wkt::{Duration, DurationError};
168    /// let d = Duration::clamp(12, 340_000_000);
169    /// assert_eq!(String::from(d), "12.34s");
170    /// let d = Duration::clamp(10, 2_000_000_000);
171    /// assert_eq!(String::from(d), "12s");
172    /// # Ok::<(), DurationError>(())
173    /// ```
174    ///
175    /// Durations must be in the [-10_000, +10_000] year range, the nanoseconds
176    /// field must be in the [-999_999_999, +999_999_999] range, and the seconds
177    /// and nanosecond fields must have the same sign. This function creates a
178    /// new [Duration] instance clamped to those ranges.
179    ///
180    /// The function effectively adds the nanoseconds part (with carry) to the
181    /// seconds part, with saturation.
182    ///
183    /// # Parameters
184    ///
185    /// * `seconds` - the seconds in the interval.
186    /// * `nanos` - the nanoseconds *added* to the interval.
187    pub fn clamp(seconds: i64, nanos: i32) -> Self {
188        let mut seconds = seconds;
189        seconds = seconds.saturating_add((nanos / Self::NS) as i64);
190        let mut nanos = nanos % Self::NS;
191        if seconds > 0 && nanos < 0 {
192            seconds = seconds.saturating_sub(1);
193            nanos += Self::NS;
194        } else if seconds < 0 && nanos > 0 {
195            seconds = seconds.saturating_add(1);
196            nanos = -(Self::NS - nanos);
197        }
198        if seconds > Self::MAX_SECONDS {
199            return Self {
200                seconds: Self::MAX_SECONDS,
201                nanos: 0,
202            };
203        }
204        if seconds < Self::MIN_SECONDS {
205            return Self {
206                seconds: Self::MIN_SECONDS,
207                nanos: 0,
208            };
209        }
210        Self { seconds, nanos }
211    }
212
213    /// Returns the seconds part of the duration.
214    ///
215    /// # Example
216    /// ```
217    /// # use google_cloud_wkt::Duration;
218    /// let d = Duration::clamp(12, 34);
219    /// assert_eq!(d.seconds(), 12);
220    /// ```
221    pub fn seconds(&self) -> i64 {
222        self.seconds
223    }
224
225    /// Returns the sub-second part of the duration.
226    ///
227    /// # Example
228    /// ```
229    /// # use google_cloud_wkt::Duration;
230    /// let d = Duration::clamp(12, 34);
231    /// assert_eq!(d.nanos(), 34);
232    /// ```
233    pub fn nanos(&self) -> i32 {
234        self.nanos
235    }
236}
237
238impl crate::message::Message for Duration {
239    fn typename() -> &'static str {
240        "type.googleapis.com/google.protobuf.Duration"
241    }
242
243    #[allow(private_interfaces)]
244    fn serializer() -> impl crate::message::MessageSerializer<Self> {
245        crate::message::ValueSerializer::<Self>::new()
246    }
247}
248
249/// Converts a [Duration] to its [String] representation.
250///
251/// # Example
252/// ```
253/// # use google_cloud_wkt::Duration;
254/// let d = Duration::clamp(12, 340_000_000);
255/// assert_eq!(String::from(d), "12.34s");
256/// ```
257impl From<Duration> for String {
258    fn from(duration: Duration) -> String {
259        let sign = if duration.seconds < 0 || duration.nanos < 0 {
260            "-"
261        } else {
262            ""
263        };
264        if duration.nanos == 0 {
265            return format!("{sign}{}s", duration.seconds.abs());
266        }
267        let ns = format!("{:09}", duration.nanos.abs());
268        format!(
269            "{sign}{}.{}s",
270            duration.seconds.abs(),
271            ns.trim_end_matches('0')
272        )
273    }
274}
275
276/// Converts the string representation of a duration to [Duration].
277///
278/// # Example
279/// ```
280/// # use google_cloud_wkt::{Duration, DurationError};
281/// let d = Duration::try_from("12.34s")?;
282/// assert_eq!(d.seconds(), 12);
283/// assert_eq!(d.nanos(), 340_000_000);
284/// # Ok::<(), DurationError>(())
285/// ```
286impl TryFrom<&str> for Duration {
287    type Error = DurationError;
288    fn try_from(value: &str) -> Result<Self, Self::Error> {
289        if !value.ends_with('s') {
290            return Err(DurationError::Deserialize("missing trailing 's'".into()));
291        }
292        let digits = &value[..(value.len() - 1)];
293        let (sign, digits) = if let Some(stripped) = digits.strip_prefix('-') {
294            (-1, stripped)
295        } else {
296            (1, &digits[0..])
297        };
298        let mut split = digits.splitn(2, '.');
299        let (seconds, nanos) = (split.next(), split.next());
300        let seconds = seconds
301            .map(str::parse::<i64>)
302            .transpose()
303            .map_err(|e| DurationError::Deserialize(e.into()))?
304            .unwrap_or(0);
305        let nanos = nanos
306            .map(|s| {
307                let pad = "000000000";
308                format!("{s}{}", &pad[s.len()..])
309            })
310            .map(|s| s.parse::<i32>())
311            .transpose()
312            .map_err(|e| DurationError::Deserialize(e.into()))?
313            .unwrap_or(0);
314
315        Duration::new(sign * seconds, sign as i32 * nanos)
316    }
317}
318
319/// Converts the string representation of a duration to [Duration].
320///
321/// # Example
322/// ```
323/// # use google_cloud_wkt::{Duration, DurationError};
324/// let s = "12.34s".to_string();
325/// let d = Duration::try_from(&s)?;
326/// assert_eq!(d.seconds(), 12);
327/// assert_eq!(d.nanos(), 340_000_000);
328/// # Ok::<(), DurationError>(())
329/// ```
330impl TryFrom<&String> for Duration {
331    type Error = DurationError;
332    fn try_from(value: &String) -> Result<Self, Self::Error> {
333        Duration::try_from(value.as_str())
334    }
335}
336
337/// Convert from [std::time::Duration] to [Duration].
338///
339/// # Example
340/// ```
341/// # use google_cloud_wkt::{Duration, DurationError};
342/// let d = Duration::try_from(std::time::Duration::from_secs(123))?;
343/// assert_eq!(d.seconds(), 123);
344/// assert_eq!(d.nanos(), 0);
345/// # Ok::<(), DurationError>(())
346/// ```
347impl TryFrom<std::time::Duration> for Duration {
348    type Error = DurationError;
349
350    fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
351        if value.as_secs() > (i64::MAX as u64) {
352            return Err(Error::OutOfRange);
353        }
354        assert!(value.as_secs() <= (i64::MAX as u64));
355        assert!(value.subsec_nanos() <= (i32::MAX as u32));
356        Self::new(value.as_secs() as i64, value.subsec_nanos() as i32)
357    }
358}
359
360/// Convert from [Duration] to [std::time::Duration].
361///
362/// Returns an error if `value` is negative, as `std::time::Duration` cannot
363/// represent negative durations.
364///
365/// # Example
366/// ```
367/// # use google_cloud_wkt::{Duration, DurationError};
368/// let d = Duration::new(12, 340_000_000)?;
369/// let duration = std::time::Duration::try_from(d)?;
370/// assert_eq!(duration.as_secs(), 12);
371/// assert_eq!(duration.subsec_nanos(), 340_000_000);
372/// # Ok::<(), DurationError>(())
373/// ```
374impl TryFrom<Duration> for std::time::Duration {
375    type Error = DurationError;
376
377    fn try_from(value: Duration) -> Result<Self, Self::Error> {
378        if value.seconds < 0 {
379            return Err(Error::OutOfRange);
380        }
381        if value.nanos < 0 {
382            return Err(Error::OutOfRange);
383        }
384        Ok(Self::new(value.seconds as u64, value.nanos as u32))
385    }
386}
387
388/// Convert from [time::Duration] to [Duration].
389///
390/// This conversion may fail if the [time::Duration] value is out of range.
391///
392/// # Example
393/// ```
394/// # use google_cloud_wkt::{Duration, DurationError};
395/// let d = Duration::try_from(time::Duration::new(12, 340_000_000))?;
396/// assert_eq!(d.seconds(), 12);
397/// assert_eq!(d.nanos(), 340_000_000);
398/// # Ok::<(), DurationError>(())
399/// ```
400#[cfg(feature = "time")]
401#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
402impl TryFrom<time::Duration> for Duration {
403    type Error = DurationError;
404
405    fn try_from(value: time::Duration) -> Result<Self, Self::Error> {
406        Self::new(value.whole_seconds(), value.subsec_nanoseconds())
407    }
408}
409
410/// Convert from [Duration] to [time::Duration].
411///
412/// This conversion is always safe because the range for [Duration] is
413/// guaranteed to fit into the destination type.
414///
415/// # Example
416/// ```
417/// # use google_cloud_wkt::{Duration, DurationError};
418/// let d = time::Duration::from(Duration::clamp(12, 340_000_000));
419/// assert_eq!(d.whole_seconds(), 12);
420/// assert_eq!(d.subsec_nanoseconds(), 340_000_000);
421/// # Ok::<(), DurationError>(())
422/// ```
423#[cfg(feature = "time")]
424#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
425impl From<Duration> for time::Duration {
426    fn from(value: Duration) -> Self {
427        Self::new(value.seconds(), value.nanos())
428    }
429}
430
431/// Converts from [chrono::Duration] to [Duration].
432///
433/// The conversion may fail if the input value is out of range.
434///
435/// # Example
436/// ```
437/// # use google_cloud_wkt::{Duration, DurationError};
438/// let d = Duration::try_from(chrono::Duration::new(12, 340_000_000).unwrap())?;
439/// assert_eq!(d.seconds(), 12);
440/// assert_eq!(d.nanos(), 340_000_000);
441/// # Ok::<(), DurationError>(())
442/// ```
443#[cfg(feature = "chrono")]
444#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
445impl TryFrom<chrono::Duration> for Duration {
446    type Error = DurationError;
447
448    fn try_from(value: chrono::Duration) -> Result<Self, Self::Error> {
449        Self::new(value.num_seconds(), value.subsec_nanos())
450    }
451}
452
453/// Converts from [Duration] to [chrono::Duration].
454///
455/// # Example
456/// ```
457/// # use google_cloud_wkt::{Duration, DurationError};
458/// let d = chrono::Duration::from(Duration::clamp(12, 340_000_000));
459/// assert_eq!(d.num_seconds(), 12);
460/// assert_eq!(d.subsec_nanos(), 340_000_000);
461/// # Ok::<(), DurationError>(())
462/// ```
463#[cfg(feature = "chrono")]
464#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
465impl From<Duration> for chrono::Duration {
466    fn from(value: Duration) -> Self {
467        Self::seconds(value.seconds) + Self::nanoseconds(value.nanos as i64)
468    }
469}
470
471/// Implement [`serde`](::serde) serialization for [Duration].
472#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
473impl serde::ser::Serialize for Duration {
474    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
475    where
476        S: serde::ser::Serializer,
477    {
478        let formatted = String::from(*self);
479        formatted.serialize(serializer)
480    }
481}
482
483struct DurationVisitor;
484
485impl serde::de::Visitor<'_> for DurationVisitor {
486    type Value = Duration;
487
488    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
489        formatter.write_str("a string with a duration in Google format ([sign]{seconds}.{nanos}s)")
490    }
491
492    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
493    where
494        E: serde::de::Error,
495    {
496        let d = Duration::try_from(value).map_err(E::custom)?;
497        Ok(d)
498    }
499}
500
501/// Implement [`serde`](::serde) deserialization for [`Duration`].
502#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
503impl<'de> serde::de::Deserialize<'de> for Duration {
504    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
505    where
506        D: serde::Deserializer<'de>,
507    {
508        deserializer.deserialize_str(DurationVisitor)
509    }
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use serde_json::json;
516    use test_case::test_case;
517    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
518
519    // Verify 0 converts as expected.
520    #[test]
521    fn zero() -> Result {
522        let proto = Duration {
523            seconds: 0,
524            nanos: 0,
525        };
526        let json = serde_json::to_value(proto)?;
527        let expected = json!(r#"0s"#);
528        assert_eq!(json, expected);
529        let roundtrip = serde_json::from_value::<Duration>(json)?;
530        assert_eq!(proto, roundtrip);
531        Ok(())
532    }
533
534    // Google assumes all minutes have 60 seconds. Leap seconds are handled via
535    // smearing.
536    const SECONDS_IN_DAY: i64 = 24 * 60 * 60;
537    // For the purposes of this Duration type, Google ignores the subtleties of
538    // leap years on multiples of 100 and 400.
539    const SECONDS_IN_YEAR: i64 = 365 * SECONDS_IN_DAY + SECONDS_IN_DAY / 4;
540
541    #[test_case(10_000 * SECONDS_IN_YEAR , 0 ; "exactly 10,000 years")]
542    #[test_case(- 10_000 * SECONDS_IN_YEAR , 0 ; "exactly negative 10,000 years")]
543    #[test_case(10_000 * SECONDS_IN_YEAR , 999_999_999 ; "exactly 10,000 years and 999,999,999 nanos"
544	)]
545    #[test_case(- 10_000 * SECONDS_IN_YEAR , -999_999_999 ; "exactly negative 10,000 years and 999,999,999 nanos"
546	)]
547    #[test_case(0, 999_999_999 ; "exactly 999,999,999 nanos")]
548    #[test_case(0 , -999_999_999 ; "exactly negative 999,999,999 nanos")]
549    fn edge_of_range(seconds: i64, nanos: i32) -> Result {
550        let d = Duration::new(seconds, nanos)?;
551        assert_eq!(seconds, d.seconds());
552        assert_eq!(nanos, d.nanos());
553        Ok(())
554    }
555
556    #[test_case(10_000 * SECONDS_IN_YEAR + 1, 0 ; "more seconds than in 10,000 years")]
557    #[test_case(- 10_000 * SECONDS_IN_YEAR - 1, 0 ; "more negative seconds than in -10,000 years")]
558    #[test_case(0, 1_000_000_000 ; "too many positive nanoseconds")]
559    #[test_case(0, -1_000_000_000 ; "too many negative nanoseconds")]
560    fn out_of_range(seconds: i64, nanos: i32) -> Result {
561        let d = Duration::new(seconds, nanos);
562        assert!(matches!(d, Err(Error::OutOfRange)), "{d:?}");
563        Ok(())
564    }
565
566    #[test_case(1 , -1 ; "mismatched sign case 1")]
567    #[test_case(-1 , 1 ; "mismatched sign case 2")]
568    fn mismatched_sign(seconds: i64, nanos: i32) -> Result {
569        let d = Duration::new(seconds, nanos);
570        assert!(matches!(d, Err(Error::MismatchedSigns)), "{d:?}");
571        Ok(())
572    }
573
574    #[test_case(20_000 * SECONDS_IN_YEAR, 0, 10_000 * SECONDS_IN_YEAR, 0 ; "too many positive seconds"
575	)]
576    #[test_case(-20_000 * SECONDS_IN_YEAR, 0, -10_000 * SECONDS_IN_YEAR, 0 ; "too many negative seconds"
577	)]
578    #[test_case(10_000 * SECONDS_IN_YEAR - 1, 1_999_999_999, 10_000 * SECONDS_IN_YEAR, 999_999_999 ; "upper edge of range"
579	)]
580    #[test_case(-10_000 * SECONDS_IN_YEAR + 1, -1_999_999_999, -10_000 * SECONDS_IN_YEAR, -999_999_999 ; "lower edge of range"
581	)]
582    #[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"
583	)]
584    #[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"
585	)]
586    #[test_case(0, 0, 0, 0 ; "all inputs are zero")]
587    #[test_case(1, 0, 1, 0 ; "positive seconds and zero nanos")]
588    #[test_case(1, 200_000, 1, 200_000 ; "positive seconds and nanos")]
589    #[test_case(-1, 0, -1, 0; "negative seconds and zero nanos")]
590    #[test_case(-1, -500_000_000, -1, -500_000_000; "negative seconds and nanos")]
591    #[test_case(2, -400_000_000, 1, 600_000_000; "positive seconds and negative nanos")]
592    #[test_case(-2, 400_000_000, -1, -600_000_000; "negative seconds and positive nanos")]
593    fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) -> Result {
594        let got = Duration::clamp(seconds, nanos);
595        let want = Duration {
596            seconds: want_seconds,
597            nanos: want_nanos,
598        };
599        assert_eq!(want, got);
600        Ok(())
601    }
602
603    // Verify durations can roundtrip from string -> struct -> string without loss.
604    #[test_case(0, 0, "0s" ; "zero")]
605    #[test_case(0, 2, "0.000000002s" ; "2ns")]
606    #[test_case(0, 200_000_000, "0.2s" ; "200ms")]
607    #[test_case(12, 0, "12s"; "round positive seconds")]
608    #[test_case(12, 123, "12.000000123s"; "positive seconds and nanos")]
609    #[test_case(12, 123_000, "12.000123s"; "positive seconds and micros")]
610    #[test_case(12, 123_000_000, "12.123s"; "positive seconds and millis")]
611    #[test_case(12, 123_456_789, "12.123456789s"; "positive seconds and full nanos")]
612    #[test_case(-12, -0, "-12s"; "round negative seconds")]
613    #[test_case(-12, -123, "-12.000000123s"; "negative seconds and nanos")]
614    #[test_case(-12, -123_000, "-12.000123s"; "negative seconds and micros")]
615    #[test_case(-12, -123_000_000, "-12.123s"; "negative seconds and millis")]
616    #[test_case(-12, -123_456_789, "-12.123456789s"; "negative seconds and full nanos")]
617    #[test_case(-10_000 * SECONDS_IN_YEAR, -999_999_999, "-315576000000.999999999s"; "range edge start"
618	)]
619    #[test_case(10_000 * SECONDS_IN_YEAR, 999_999_999, "315576000000.999999999s"; "range edge end")]
620    fn roundtrip(seconds: i64, nanos: i32, want: &str) -> Result {
621        let input = Duration::new(seconds, nanos)?;
622        let got = serde_json::to_value(input)?
623            .as_str()
624            .map(str::to_string)
625            .ok_or("cannot convert value to string")?;
626        assert_eq!(want, got);
627
628        let rt = serde_json::from_value::<Duration>(serde_json::Value::String(got))?;
629        assert_eq!(input, rt);
630        Ok(())
631    }
632
633    #[test_case("-315576000001s"; "range edge start")]
634    #[test_case("315576000001s"; "range edge end")]
635    fn deserialize_out_of_range(input: &str) -> Result {
636        let value = serde_json::to_value(input)?;
637        let got = serde_json::from_value::<Duration>(value);
638        assert!(got.is_err());
639        Ok(())
640    }
641
642    #[test_case(time::Duration::default(), Duration::default() ; "default")]
643    #[test_case(time::Duration::new(0, 0), Duration::new(0, 0).unwrap() ; "zero")]
644    #[test_case(time::Duration::new(10_000 * SECONDS_IN_YEAR , 0), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
645	)]
646    #[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"
647	)]
648    fn from_time_in_range(value: time::Duration, want: Duration) -> Result {
649        let got = Duration::try_from(value)?;
650        assert_eq!(got, want);
651        Ok(())
652    }
653
654    #[test_case(time::Duration::new(10_001 * SECONDS_IN_YEAR, 0) ; "above the range")]
655    #[test_case(time::Duration::new(-10_001 * SECONDS_IN_YEAR, 0) ; "below the range")]
656    fn from_time_out_of_range(value: time::Duration) {
657        let got = Duration::try_from(value);
658        assert!(matches!(got, Err(DurationError::OutOfRange)), "{got:?}");
659    }
660
661    #[test_case(Duration::default(), time::Duration::default() ; "default")]
662    #[test_case(Duration::new(0, 0).unwrap(), time::Duration::new(0, 0) ; "zero")]
663    #[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(10_000 * SECONDS_IN_YEAR, 0) ; "exactly 10,000 years"
664	)]
665    #[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"
666	)]
667    fn to_time_in_range(value: Duration, want: time::Duration) -> Result {
668        let got = time::Duration::from(value);
669        assert_eq!(got, want);
670        Ok(())
671    }
672
673    #[test_case("" ; "empty")]
674    #[test_case("1.0" ; "missing final s")]
675    #[test_case("1.2.3.4s" ; "too many periods")]
676    #[test_case("aaas" ; "not a number")]
677    #[test_case("aaaa.0s" ; "seconds are not a number [aaa]")]
678    #[test_case("1a.0s" ; "seconds are not a number [1a]")]
679    #[test_case("1.aaas" ; "nanos are not a number [aaa]")]
680    #[test_case("1.0as" ; "nanos are not a number [0a]")]
681    fn parse_detect_bad_input(input: &str) -> Result {
682        let got = Duration::try_from(input);
683        assert!(got.is_err());
684        let err = got.err().unwrap();
685        assert!(
686            matches!(err, DurationError::Deserialize(_)),
687            "unexpected error {err:?}"
688        );
689        Ok(())
690    }
691
692    #[test]
693    fn deserialize_unexpected_input_type() -> Result {
694        let got = serde_json::from_value::<Duration>(serde_json::json!({}));
695        assert!(got.is_err());
696        let msg = format!("{got:?}");
697        assert!(msg.contains("duration in Google format"), "message={msg}");
698        Ok(())
699    }
700
701    #[test_case(std::time::Duration::new(0, 0), Duration::clamp(0, 0))]
702    #[test_case(
703        std::time::Duration::new(0, 400_000_000),
704        Duration::clamp(0, 400_000_000)
705    )]
706    #[test_case(
707        std::time::Duration::new(1, 400_000_000),
708        Duration::clamp(1, 400_000_000)
709    )]
710    #[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))]
711    fn from_std_time_in_range(input: std::time::Duration, want: Duration) {
712        let got = Duration::try_from(input).unwrap();
713        assert_eq!(got, want);
714    }
715
716    #[test]
717    fn convert_from_string() -> Result {
718        let input = "12.750s".to_string();
719        let a = Duration::try_from(input.as_str())?;
720        let b = Duration::try_from(&input)?;
721        assert_eq!(a, b);
722        Ok(())
723    }
724
725    #[test_case(std::time::Duration::new(i64::MAX as u64, 0))]
726    #[test_case(std::time::Duration::new(i64::MAX as u64 + 10, 0))]
727    fn from_std_time_out_of_range(input: std::time::Duration) {
728        let got = Duration::try_from(input);
729        assert!(got.is_err(), "{got:?}");
730    }
731
732    #[test_case(chrono::Duration::default(), Duration::default() ; "default")]
733    #[test_case(chrono::Duration::new(0, 0).unwrap(), Duration::new(0, 0).unwrap() ; "zero")]
734    #[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"
735	)]
736    #[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"
737	)]
738    fn from_chrono_time_in_range(value: chrono::Duration, want: Duration) -> Result {
739        let got = Duration::try_from(value)?;
740        assert_eq!(got, want);
741        Ok(())
742    }
743
744    #[test_case(Duration::default(), chrono::Duration::default() ; "default")]
745    #[test_case(Duration::new(0, 0).unwrap(), chrono::Duration::new(0, 0).unwrap() ; "zero")]
746    #[test_case(Duration::new(0, 500_000).unwrap(), chrono::Duration::new(0, 500_000).unwrap() ; "500us")]
747    #[test_case(Duration::new(1, 400_000_000).unwrap(), chrono::Duration::new(1, 400_000_000).unwrap() ; "1.4s")]
748    #[test_case(Duration::new(0, -400_000_000).unwrap(), chrono::Duration::new(-1, 600_000_000).unwrap() ; "minus 0.4s")]
749    #[test_case(Duration::new(-1, -400_000_000).unwrap(), chrono::Duration::new(-2, 600_000_000).unwrap() ; "minus 1.4s")]
750    #[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"
751	)]
752    #[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"
753	)]
754    fn to_chrono_time_in_range(value: Duration, want: chrono::Duration) -> Result {
755        let got = chrono::Duration::from(value);
756        assert_eq!(got, want);
757        Ok(())
758    }
759
760    #[test_case(chrono::Duration::new(10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "above the range")]
761    #[test_case(chrono::Duration::new(-10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "below the range")]
762    fn from_chrono_time_out_of_range(value: chrono::Duration) {
763        let got = Duration::try_from(value);
764        assert!(matches!(got, Err(DurationError::OutOfRange)), "{got:?}");
765    }
766}