Skip to main content

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                if s.is_empty() || !s.chars().all(|c| c.is_ascii_digit()) {
308                    return Err(DurationError::Deserialize(
309                        format!("nanos are not a number [{s}]").into(),
310                    ));
311                }
312                let len = s.len();
313                let (digits, power) = if len > 9 { (&s[..9], 0) } else { (s, 9 - len) };
314                let mut val = digits
315                    .parse::<i32>()
316                    .map_err(|e| DurationError::Deserialize(e.into()))?;
317                if power > 0 {
318                    val *= 10_i32.pow(power as u32)
319                }
320                Ok(val)
321            })
322            .transpose()?
323            .unwrap_or(0);
324
325        Duration::new(sign * seconds, sign as i32 * nanos)
326    }
327}
328
329/// Converts the string representation of a duration to [Duration].
330///
331/// # Example
332/// ```
333/// # use google_cloud_wkt::{Duration, DurationError};
334/// let s = "12.34s".to_string();
335/// let d = Duration::try_from(&s)?;
336/// assert_eq!(d.seconds(), 12);
337/// assert_eq!(d.nanos(), 340_000_000);
338/// # Ok::<(), DurationError>(())
339/// ```
340impl TryFrom<&String> for Duration {
341    type Error = DurationError;
342    fn try_from(value: &String) -> Result<Self, Self::Error> {
343        Duration::try_from(value.as_str())
344    }
345}
346
347/// Convert from [std::time::Duration] to [Duration].
348///
349/// # Example
350/// ```
351/// # use google_cloud_wkt::{Duration, DurationError};
352/// let d = Duration::try_from(std::time::Duration::from_secs(123))?;
353/// assert_eq!(d.seconds(), 123);
354/// assert_eq!(d.nanos(), 0);
355/// # Ok::<(), DurationError>(())
356/// ```
357impl TryFrom<std::time::Duration> for Duration {
358    type Error = DurationError;
359
360    fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
361        if value.as_secs() > (i64::MAX as u64) {
362            return Err(Error::OutOfRange);
363        }
364        assert!(value.as_secs() <= (i64::MAX as u64));
365        assert!(value.subsec_nanos() <= (i32::MAX as u32));
366        Self::new(value.as_secs() as i64, value.subsec_nanos() as i32)
367    }
368}
369
370/// Convert from [Duration] to [std::time::Duration].
371///
372/// Returns an error if `value` is negative, as `std::time::Duration` cannot
373/// represent negative durations.
374///
375/// # Example
376/// ```
377/// # use google_cloud_wkt::{Duration, DurationError};
378/// let d = Duration::new(12, 340_000_000)?;
379/// let duration = std::time::Duration::try_from(d)?;
380/// assert_eq!(duration.as_secs(), 12);
381/// assert_eq!(duration.subsec_nanos(), 340_000_000);
382/// # Ok::<(), DurationError>(())
383/// ```
384impl TryFrom<Duration> for std::time::Duration {
385    type Error = DurationError;
386
387    fn try_from(value: Duration) -> Result<Self, Self::Error> {
388        if value.seconds < 0 {
389            return Err(Error::OutOfRange);
390        }
391        if value.nanos < 0 {
392            return Err(Error::OutOfRange);
393        }
394        Ok(Self::new(value.seconds as u64, value.nanos as u32))
395    }
396}
397
398/// Convert from [time::Duration] to [Duration].
399///
400/// This conversion may fail if the [time::Duration] value is out of range.
401///
402/// # Example
403/// ```
404/// # use google_cloud_wkt::{Duration, DurationError};
405/// let d = Duration::try_from(time::Duration::new(12, 340_000_000))?;
406/// assert_eq!(d.seconds(), 12);
407/// assert_eq!(d.nanos(), 340_000_000);
408/// # Ok::<(), DurationError>(())
409/// ```
410#[cfg(feature = "time")]
411#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
412impl TryFrom<time::Duration> for Duration {
413    type Error = DurationError;
414
415    fn try_from(value: time::Duration) -> Result<Self, Self::Error> {
416        Self::new(value.whole_seconds(), value.subsec_nanoseconds())
417    }
418}
419
420/// Convert from [Duration] to [time::Duration].
421///
422/// This conversion is always safe because the range for [Duration] is
423/// guaranteed to fit into the destination type.
424///
425/// # Example
426/// ```
427/// # use google_cloud_wkt::{Duration, DurationError};
428/// let d = time::Duration::from(Duration::clamp(12, 340_000_000));
429/// assert_eq!(d.whole_seconds(), 12);
430/// assert_eq!(d.subsec_nanoseconds(), 340_000_000);
431/// # Ok::<(), DurationError>(())
432/// ```
433#[cfg(feature = "time")]
434#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
435impl From<Duration> for time::Duration {
436    fn from(value: Duration) -> Self {
437        Self::new(value.seconds(), value.nanos())
438    }
439}
440
441/// Converts from [chrono::Duration] to [Duration].
442///
443/// The conversion may fail if the input value is out of range.
444///
445/// # Example
446/// ```
447/// # use google_cloud_wkt::{Duration, DurationError};
448/// let d = Duration::try_from(chrono::Duration::new(12, 340_000_000).unwrap())?;
449/// assert_eq!(d.seconds(), 12);
450/// assert_eq!(d.nanos(), 340_000_000);
451/// # Ok::<(), DurationError>(())
452/// ```
453#[cfg(feature = "chrono")]
454#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
455impl TryFrom<chrono::Duration> for Duration {
456    type Error = DurationError;
457
458    fn try_from(value: chrono::Duration) -> Result<Self, Self::Error> {
459        Self::new(value.num_seconds(), value.subsec_nanos())
460    }
461}
462
463/// Converts from [Duration] to [chrono::Duration].
464///
465/// # Example
466/// ```
467/// # use google_cloud_wkt::{Duration, DurationError};
468/// let d = chrono::Duration::from(Duration::clamp(12, 340_000_000));
469/// assert_eq!(d.num_seconds(), 12);
470/// assert_eq!(d.subsec_nanos(), 340_000_000);
471/// # Ok::<(), DurationError>(())
472/// ```
473#[cfg(feature = "chrono")]
474#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
475impl From<Duration> for chrono::Duration {
476    fn from(value: Duration) -> Self {
477        Self::seconds(value.seconds) + Self::nanoseconds(value.nanos as i64)
478    }
479}
480
481/// Implement [`serde`](::serde) serialization for [Duration].
482#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
483impl serde::ser::Serialize for Duration {
484    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
485    where
486        S: serde::ser::Serializer,
487    {
488        let formatted = String::from(*self);
489        formatted.serialize(serializer)
490    }
491}
492
493struct DurationVisitor;
494
495impl serde::de::Visitor<'_> for DurationVisitor {
496    type Value = Duration;
497
498    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
499        formatter.write_str("a string with a duration in Google format ([sign]{seconds}.{nanos}s)")
500    }
501
502    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
503    where
504        E: serde::de::Error,
505    {
506        let d = Duration::try_from(value).map_err(E::custom)?;
507        Ok(d)
508    }
509}
510
511/// Implement [`serde`](::serde) deserialization for [`Duration`].
512#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
513impl<'de> serde::de::Deserialize<'de> for Duration {
514    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
515    where
516        D: serde::Deserializer<'de>,
517    {
518        deserializer.deserialize_str(DurationVisitor)
519    }
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525    use serde_json::json;
526    use test_case::test_case;
527    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
528
529    // Verify 0 converts as expected.
530    #[test]
531    fn zero() -> Result {
532        let proto = Duration {
533            seconds: 0,
534            nanos: 0,
535        };
536        let json = serde_json::to_value(proto)?;
537        let expected = json!(r#"0s"#);
538        assert_eq!(json, expected);
539        let roundtrip = serde_json::from_value::<Duration>(json)?;
540        assert_eq!(proto, roundtrip);
541        Ok(())
542    }
543
544    // Google assumes all minutes have 60 seconds. Leap seconds are handled via
545    // smearing.
546    const SECONDS_IN_DAY: i64 = 24 * 60 * 60;
547    // For the purposes of this Duration type, Google ignores the subtleties of
548    // leap years on multiples of 100 and 400.
549    const SECONDS_IN_YEAR: i64 = 365 * SECONDS_IN_DAY + SECONDS_IN_DAY / 4;
550
551    #[test_case(10_000 * SECONDS_IN_YEAR , 0 ; "exactly 10,000 years")]
552    #[test_case(- 10_000 * SECONDS_IN_YEAR , 0 ; "exactly negative 10,000 years")]
553    #[test_case(10_000 * SECONDS_IN_YEAR , 999_999_999 ; "exactly 10,000 years and 999,999,999 nanos"
554	)]
555    #[test_case(- 10_000 * SECONDS_IN_YEAR , -999_999_999 ; "exactly negative 10,000 years and 999,999,999 nanos"
556	)]
557    #[test_case(0, 999_999_999 ; "exactly 999,999,999 nanos")]
558    #[test_case(0 , -999_999_999 ; "exactly negative 999,999,999 nanos")]
559    fn edge_of_range(seconds: i64, nanos: i32) -> Result {
560        let d = Duration::new(seconds, nanos)?;
561        assert_eq!(seconds, d.seconds());
562        assert_eq!(nanos, d.nanos());
563        Ok(())
564    }
565
566    #[test_case(10_000 * SECONDS_IN_YEAR + 1, 0 ; "more seconds than in 10,000 years")]
567    #[test_case(- 10_000 * SECONDS_IN_YEAR - 1, 0 ; "more negative seconds than in -10,000 years")]
568    #[test_case(0, 1_000_000_000 ; "too many positive nanoseconds")]
569    #[test_case(0, -1_000_000_000 ; "too many negative nanoseconds")]
570    fn out_of_range(seconds: i64, nanos: i32) -> Result {
571        let d = Duration::new(seconds, nanos);
572        assert!(matches!(d, Err(Error::OutOfRange)), "{d:?}");
573        Ok(())
574    }
575
576    #[test_case(1 , -1 ; "mismatched sign case 1")]
577    #[test_case(-1 , 1 ; "mismatched sign case 2")]
578    fn mismatched_sign(seconds: i64, nanos: i32) -> Result {
579        let d = Duration::new(seconds, nanos);
580        assert!(matches!(d, Err(Error::MismatchedSigns)), "{d:?}");
581        Ok(())
582    }
583
584    #[test_case(20_000 * SECONDS_IN_YEAR, 0, 10_000 * SECONDS_IN_YEAR, 0 ; "too many positive seconds"
585	)]
586    #[test_case(-20_000 * SECONDS_IN_YEAR, 0, -10_000 * SECONDS_IN_YEAR, 0 ; "too many negative seconds"
587	)]
588    #[test_case(10_000 * SECONDS_IN_YEAR - 1, 1_999_999_999, 10_000 * SECONDS_IN_YEAR, 999_999_999 ; "upper edge of range"
589	)]
590    #[test_case(-10_000 * SECONDS_IN_YEAR + 1, -1_999_999_999, -10_000 * SECONDS_IN_YEAR, -999_999_999 ; "lower edge of range"
591	)]
592    #[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"
593	)]
594    #[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"
595	)]
596    #[test_case(0, 0, 0, 0 ; "all inputs are zero")]
597    #[test_case(1, 0, 1, 0 ; "positive seconds and zero nanos")]
598    #[test_case(1, 200_000, 1, 200_000 ; "positive seconds and nanos")]
599    #[test_case(-1, 0, -1, 0; "negative seconds and zero nanos")]
600    #[test_case(-1, -500_000_000, -1, -500_000_000; "negative seconds and nanos")]
601    #[test_case(2, -400_000_000, 1, 600_000_000; "positive seconds and negative nanos")]
602    #[test_case(-2, 400_000_000, -1, -600_000_000; "negative seconds and positive nanos")]
603    fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) -> Result {
604        let got = Duration::clamp(seconds, nanos);
605        let want = Duration {
606            seconds: want_seconds,
607            nanos: want_nanos,
608        };
609        assert_eq!(want, got);
610        Ok(())
611    }
612
613    // Verify durations can roundtrip from string -> struct -> string without loss.
614    #[test_case(0, 0, "0s" ; "zero")]
615    #[test_case(0, 2, "0.000000002s" ; "2ns")]
616    #[test_case(0, 200_000_000, "0.2s" ; "200ms")]
617    #[test_case(12, 0, "12s"; "round positive seconds")]
618    #[test_case(12, 123, "12.000000123s"; "positive seconds and nanos")]
619    #[test_case(12, 123_000, "12.000123s"; "positive seconds and micros")]
620    #[test_case(12, 123_000_000, "12.123s"; "positive seconds and millis")]
621    #[test_case(12, 123_456_789, "12.123456789s"; "positive seconds and full nanos")]
622    #[test_case(-12, -0, "-12s"; "round negative seconds")]
623    #[test_case(-12, -123, "-12.000000123s"; "negative seconds and nanos")]
624    #[test_case(-12, -123_000, "-12.000123s"; "negative seconds and micros")]
625    #[test_case(-12, -123_000_000, "-12.123s"; "negative seconds and millis")]
626    #[test_case(-12, -123_456_789, "-12.123456789s"; "negative seconds and full nanos")]
627    #[test_case(-10_000 * SECONDS_IN_YEAR, -999_999_999, "-315576000000.999999999s"; "range edge start"
628	)]
629    #[test_case(10_000 * SECONDS_IN_YEAR, 999_999_999, "315576000000.999999999s"; "range edge end")]
630    fn roundtrip(seconds: i64, nanos: i32, want: &str) -> Result {
631        let input = Duration::new(seconds, nanos)?;
632        let got = serde_json::to_value(input)?
633            .as_str()
634            .map(str::to_string)
635            .ok_or("cannot convert value to string")?;
636        assert_eq!(want, got);
637
638        let rt = serde_json::from_value::<Duration>(serde_json::Value::String(got))?;
639        assert_eq!(input, rt);
640        Ok(())
641    }
642
643    #[test_case("-315576000001s"; "range edge start")]
644    #[test_case("315576000001s"; "range edge end")]
645    fn deserialize_out_of_range(input: &str) -> Result {
646        let value = serde_json::to_value(input)?;
647        let got = serde_json::from_value::<Duration>(value);
648        assert!(got.is_err(), "{got:?}");
649        Ok(())
650    }
651
652    #[test_case(time::Duration::default(), Duration::default() ; "default")]
653    #[test_case(time::Duration::new(0, 0), Duration::new(0, 0).unwrap() ; "zero")]
654    #[test_case(time::Duration::new(10_000 * SECONDS_IN_YEAR , 0), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years"
655	)]
656    #[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"
657	)]
658    fn from_time_in_range(value: time::Duration, want: Duration) -> Result {
659        let got = Duration::try_from(value)?;
660        assert_eq!(got, want);
661        Ok(())
662    }
663
664    #[test_case(time::Duration::new(10_001 * SECONDS_IN_YEAR, 0) ; "above the range")]
665    #[test_case(time::Duration::new(-10_001 * SECONDS_IN_YEAR, 0) ; "below the range")]
666    fn from_time_out_of_range(value: time::Duration) {
667        let got = Duration::try_from(value);
668        assert!(matches!(got, Err(DurationError::OutOfRange)), "{got:?}");
669    }
670
671    #[test_case(Duration::default(), time::Duration::default() ; "default")]
672    #[test_case(Duration::new(0, 0).unwrap(), time::Duration::new(0, 0) ; "zero")]
673    #[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(10_000 * SECONDS_IN_YEAR, 0) ; "exactly 10,000 years"
674	)]
675    #[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"
676	)]
677    fn to_time_in_range(value: Duration, want: time::Duration) -> Result {
678        let got = time::Duration::from(value);
679        assert_eq!(got, want);
680        Ok(())
681    }
682
683    #[test_case("" ; "empty")]
684    #[test_case("1.0" ; "missing final s")]
685    #[test_case("1.2.3.4s" ; "too many periods")]
686    #[test_case("aaas" ; "not a number")]
687    #[test_case("aaaa.0s" ; "seconds are not a number [aaa]")]
688    #[test_case("1a.0s" ; "seconds are not a number [1a]")]
689    #[test_case("1.aaas" ; "nanos are not a number [aaa]")]
690    #[test_case("1.0as" ; "nanos are not a number [0a]")]
691    #[test_case("1.1234567890as" ; "nanos with trailing chars [1234567890a]")]
692    #[test_case("1.s" ; "empty nanos")]
693    fn parse_detect_bad_input(input: &str) -> Result {
694        let got = Duration::try_from(input);
695        assert!(got.is_err(), "{got:?}");
696        let err = got.err().unwrap();
697        assert!(
698            matches!(err, DurationError::Deserialize(_)),
699            "unexpected error {err:?}"
700        );
701        Ok(())
702    }
703
704    #[test]
705    fn fractional_seconds_exceed_9_digits() -> Result {
706        let d = Duration::try_from("1.1234567890s")?;
707        assert_eq!(d, Duration::new(1, 123_456_789)?);
708
709        let d = Duration::try_from("1.123456789012s")?;
710        assert_eq!(d, Duration::new(1, 123_456_789)?);
711
712        let d: Duration = serde_json::from_str(r#""1.1234567890s""#)?;
713        assert_eq!(d, Duration::new(1, 123_456_789)?);
714
715        let d: Duration = serde_json::from_str(r#""1.123456789012s""#)?;
716        assert_eq!(d, Duration::new(1, 123_456_789)?);
717
718        let d = Duration::try_from("-1.1234567890s")?;
719        assert_eq!(d, Duration::new(-1, -123_456_789)?);
720
721        let d: Duration = serde_json::from_str(r#""-1.123456789012s""#)?;
722        assert_eq!(d, Duration::new(-1, -123_456_789)?);
723        Ok(())
724    }
725
726    #[test]
727    fn deserialize_unexpected_input_type() -> Result {
728        let got = serde_json::from_value::<Duration>(serde_json::json!({}));
729        assert!(got.is_err(), "{got:?}");
730        let msg = format!("{got:?}");
731        assert!(msg.contains("duration in Google format"), "message={msg}");
732        Ok(())
733    }
734
735    #[test_case(std::time::Duration::new(0, 0), Duration::clamp(0, 0))]
736    #[test_case(
737        std::time::Duration::new(0, 400_000_000),
738        Duration::clamp(0, 400_000_000)
739    )]
740    #[test_case(
741        std::time::Duration::new(1, 400_000_000),
742        Duration::clamp(1, 400_000_000)
743    )]
744    #[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))]
745    fn from_std_time_in_range(input: std::time::Duration, want: Duration) {
746        let got = Duration::try_from(input).unwrap();
747        assert_eq!(got, want);
748    }
749
750    #[test]
751    fn convert_from_string() -> Result {
752        let input = "12.750s".to_string();
753        let a = Duration::try_from(input.as_str())?;
754        let b = Duration::try_from(&input)?;
755        assert_eq!(a, b);
756        Ok(())
757    }
758
759    #[test_case(std::time::Duration::new(i64::MAX as u64, 0))]
760    #[test_case(std::time::Duration::new(i64::MAX as u64 + 10, 0))]
761    fn from_std_time_out_of_range(input: std::time::Duration) {
762        let got = Duration::try_from(input);
763        assert!(got.is_err(), "{got:?}");
764    }
765
766    #[test_case(chrono::Duration::default(), Duration::default() ; "default")]
767    #[test_case(chrono::Duration::new(0, 0).unwrap(), Duration::new(0, 0).unwrap() ; "zero")]
768    #[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"
769	)]
770    #[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"
771	)]
772    fn from_chrono_time_in_range(value: chrono::Duration, want: Duration) -> Result {
773        let got = Duration::try_from(value)?;
774        assert_eq!(got, want);
775        Ok(())
776    }
777
778    #[test_case(Duration::default(), chrono::Duration::default() ; "default")]
779    #[test_case(Duration::new(0, 0).unwrap(), chrono::Duration::new(0, 0).unwrap() ; "zero")]
780    #[test_case(Duration::new(0, 500_000).unwrap(), chrono::Duration::new(0, 500_000).unwrap() ; "500us")]
781    #[test_case(Duration::new(1, 400_000_000).unwrap(), chrono::Duration::new(1, 400_000_000).unwrap() ; "1.4s")]
782    #[test_case(Duration::new(0, -400_000_000).unwrap(), chrono::Duration::new(-1, 600_000_000).unwrap() ; "minus 0.4s")]
783    #[test_case(Duration::new(-1, -400_000_000).unwrap(), chrono::Duration::new(-2, 600_000_000).unwrap() ; "minus 1.4s")]
784    #[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"
785	)]
786    #[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"
787	)]
788    fn to_chrono_time_in_range(value: Duration, want: chrono::Duration) -> Result {
789        let got = chrono::Duration::from(value);
790        assert_eq!(got, want);
791        Ok(())
792    }
793
794    #[test_case(chrono::Duration::new(10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "above the range")]
795    #[test_case(chrono::Duration::new(-10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "below the range")]
796    fn from_chrono_time_out_of_range(value: chrono::Duration) {
797        let got = Duration::try_from(value);
798        assert!(matches!(got, Err(DurationError::OutOfRange)), "{got:?}");
799    }
800}