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///
16/// Well-known duration representation for Google APIs.
17///
18/// A Duration represents a signed, fixed-length span of time represented
19/// as a count of seconds and fractions of seconds at nanosecond
20/// resolution. It is independent of any calendar and concepts like "day"
21/// or "month". It is related to [Timestamp](crate::Timestamp) in that the
22/// difference between two Timestamp values is a Duration and it can be added
23/// or subtracted from a Timestamp. Range is approximately +-10,000 years.
24///
25/// # JSON Mapping
26///
27/// In JSON format, the Duration type is encoded as a string rather than an
28/// object, where the string ends in the suffix "s" (indicating seconds) and
29/// is preceded by the number of seconds, with nanoseconds expressed as
30/// fractional seconds. For example, 3 seconds with 0 nanoseconds should be
31/// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should
32/// be expressed in JSON format as "3.000000001s", and 3 seconds and 1
33/// microsecond should be expressed in JSON format as "3.000001s".
34#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
35#[non_exhaustive]
36pub struct Duration {
37    /// Signed seconds of the span of time.
38    ///
39    /// Must be from -315,576,000,000 to +315,576,000,000 inclusive. Note: these
40    /// bounds are computed from:
41    ///     60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
42    seconds: i64,
43
44    /// Signed fractions of a second at nanosecond resolution of the span
45    /// of time.
46    ///
47    /// Durations less than one second are represented with a 0 `seconds` field
48    /// and a positive or negative `nanos` field. For durations
49    /// of one second or more, a non-zero value for the `nanos` field must be
50    /// of the same sign as the `seconds` field. Must be from -999,999,999
51    /// to +999,999,999 inclusive.
52    nanos: i32,
53}
54
55/// Represent failures in converting or creating [Duration] instances.
56#[derive(thiserror::Error, Debug, PartialEq)]
57pub enum DurationError {
58    /// One of the components (seconds and/or nanoseconds) was out of range.
59    #[error("seconds and/or nanoseconds out of range")]
60    OutOfRange(),
61
62    /// The sign of the seconds component does not match the sign of the nanoseconds component.
63    #[error("if seconds and nanoseconds are not zero, they must have the same sign")]
64    MismatchedSigns(),
65
66    /// Cannot serialize the duration.
67    #[error("cannot serialize the duration")]
68    Serializate(),
69
70    /// Cannot deserialize the duration.
71    #[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    /// The maximum value for the `seconds` component, approximately 10,000 years.
81    pub const MAX_SECONDS: i64 = 315_576_000_000;
82
83    /// The minimum value for the `seconds` component, approximately -10,000 years.
84    pub const MIN_SECONDS: i64 = -Self::MAX_SECONDS;
85
86    /// The maximum value for the `nanos` component.
87    pub const MAX_NANOS: i32 = Self::NS - 1;
88
89    /// The minimum value for the `nanos` component.
90    pub const MIN_NANOS: i32 = -Self::MAX_NANOS;
91
92    /// Creates a [Duration] from the seconds and nanoseconds component.
93    ///
94    /// This function validates the `seconds` and `nanos` components and returns
95    /// an error if either are out of range or their signs do not match.
96    /// Consider using [clamp()][Duration::clamp] to add nanoseconds to seconds
97    /// with carry.
98    ///
99    /// # Arguments
100    ///
101    /// * `seconds` - the seconds in the interval.
102    /// * `nanos` - the nanoseconds *added* to the interval.
103    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    /// Create a normalized, clamped [Duration].
117    ///
118    /// Durations must be in the [-10_000, +10_000] year range, the nanoseconds
119    /// field must be in the [-999_999_999, +999_999_999] range, and the seconds
120    /// and nanosecond fields must have the same sign. This function creates a
121    /// new [Duration] instance clamped to those ranges.
122    ///
123    /// The function effectively adds the nanoseconds part (with carry) to the
124    /// seconds part, with saturation.
125    ///
126    /// # Arguments
127    ///
128    /// * `seconds` - the seconds in the interval.
129    /// * `nanos` - the nanoseconds *added* to the interval.
130    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    /// Returns the seconds part of the duration.
157    pub fn seconds(&self) -> i64 {
158        self.seconds
159    }
160
161    /// Returns the sub-second part of the duration.
162    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
179/// Converts a [Duration] to its [String] representation.
180impl std::convert::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        if duration.seconds == 0 {
191            let ns = format!("{:09}", duration.nanos.abs());
192            return format!("{sign}0.{}s", ns.trim_end_matches('0'));
193        }
194        format!(
195            "{sign}{}.{:09}s",
196            duration.seconds.abs(),
197            duration.nanos.abs()
198        )
199    }
200}
201
202/// Converts the [String] representation of a duration to [Duration].
203impl std::convert::TryFrom<&str> for Duration {
204    type Error = DurationError;
205    fn try_from(value: &str) -> Result<Self, Self::Error> {
206        if !value.ends_with('s') {
207            return Err(DurationError::Deserialize("missing trailing 's'".into()));
208        }
209        let digits = &value[..(value.len() - 1)];
210        let (sign, digits) = if let Some(stripped) = digits.strip_prefix('-') {
211            (-1, stripped)
212        } else {
213            (1, &digits[0..])
214        };
215        let mut split = digits.splitn(2, '.');
216        let (seconds, nanos) = (split.next(), split.next());
217        let seconds = seconds
218            .map(str::parse::<i64>)
219            .transpose()
220            .map_err(|e| DurationError::Deserialize(format!("{e}")))?
221            .unwrap_or(0);
222        let nanos = nanos
223            .map(|s| {
224                let pad = "000000000";
225                format!("{s}{}", &pad[s.len()..])
226            })
227            .map(|s| s.parse::<i32>())
228            .transpose()
229            .map_err(|e| DurationError::Deserialize(format!("{e}")))?
230            .unwrap_or(0);
231
232        Duration::new(sign * seconds, sign as i32 * nanos)
233    }
234}
235
236/// Convert from [std::time::Duration] to [Duration].
237impl std::convert::TryFrom<std::time::Duration> for Duration {
238    type Error = DurationError;
239
240    fn try_from(value: std::time::Duration) -> Result<Self, Self::Error> {
241        if value.as_secs() > (i64::MAX as u64) {
242            return Err(Error::OutOfRange());
243        }
244        assert!(value.subsec_nanos() <= (i32::MAX as u32));
245        Self::new(value.as_secs() as i64, value.subsec_nanos() as i32)
246    }
247}
248
249/// Convert from [Duration] to [std::time::Duration].
250impl std::convert::TryFrom<Duration> for std::time::Duration {
251    type Error = DurationError;
252
253    fn try_from(value: Duration) -> Result<Self, Self::Error> {
254        if value.seconds < 0 {
255            return Err(Error::OutOfRange());
256        }
257        if value.nanos < 0 {
258            return Err(Error::OutOfRange());
259        }
260        Ok(Self::new(value.seconds as u64, value.nanos as u32))
261    }
262}
263
264/// Convert from [time::Duration] to [Duration].
265///
266/// This conversion may fail if the [time::Duration] value is out of range.
267#[cfg(feature = "time")]
268impl std::convert::TryFrom<time::Duration> for Duration {
269    type Error = DurationError;
270
271    fn try_from(value: time::Duration) -> Result<Self, Self::Error> {
272        Self::new(value.whole_seconds(), value.subsec_nanoseconds())
273    }
274}
275
276/// Convert from [Duration] to [time::Duration].
277///
278/// This conversion is always safe because the range for [Duration] is
279/// guaranteed to fit into the destination type.
280#[cfg(feature = "time")]
281impl std::convert::From<Duration> for time::Duration {
282    fn from(value: Duration) -> Self {
283        Self::new(value.seconds(), value.nanos())
284    }
285}
286
287/// Converts from [chrono::Duration] to [Duration].
288#[cfg(feature = "chrono")]
289impl std::convert::TryFrom<chrono::Duration> for Duration {
290    type Error = DurationError;
291
292    fn try_from(value: chrono::Duration) -> Result<Self, Self::Error> {
293        Self::new(value.num_seconds(), value.subsec_nanos())
294    }
295}
296
297/// Converts from [Duration] to [chrono::Duration].
298#[cfg(feature = "chrono")]
299impl std::convert::From<Duration> for chrono::Duration {
300    fn from(value: Duration) -> Self {
301        Self::seconds(value.seconds) + Self::nanoseconds(value.nanos as i64)
302    }
303}
304
305/// Implement [`serde`](::serde) serialization for [Duration].
306impl serde::ser::Serialize for Duration {
307    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
308    where
309        S: serde::ser::Serializer,
310    {
311        let formatted = String::from(self);
312        formatted.serialize(serializer)
313    }
314}
315
316struct DurationVisitor;
317
318impl serde::de::Visitor<'_> for DurationVisitor {
319    type Value = Duration;
320
321    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
322        formatter.write_str("a string with a duration in Google format ([sign]{seconds}.{nanos}s)")
323    }
324
325    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
326    where
327        E: serde::de::Error,
328    {
329        let d = Duration::try_from(value).map_err(E::custom)?;
330        Ok(d)
331    }
332}
333
334/// Implement [`serde`](::serde) deserialization for [`Duration`].
335impl<'de> serde::de::Deserialize<'de> for Duration {
336    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
337    where
338        D: serde::Deserializer<'de>,
339    {
340        deserializer.deserialize_str(DurationVisitor)
341    }
342}
343
344#[cfg(test)]
345mod test {
346    use super::*;
347    use serde_json::json;
348    use test_case::test_case;
349    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
350
351    // Verify 0 converts as expected.
352    #[test]
353    fn zero() -> Result {
354        let proto = Duration {
355            seconds: 0,
356            nanos: 0,
357        };
358        let json = serde_json::to_value(&proto)?;
359        let expected = json!(r#"0s"#);
360        assert_eq!(json, expected);
361        let roundtrip = serde_json::from_value::<Duration>(json)?;
362        assert_eq!(proto, roundtrip);
363        Ok(())
364    }
365
366    // Google assumes all minutes have 60 seconds. Leap seconds are handled via
367    // smearing.
368    const SECONDS_IN_DAY: i64 = 24 * 60 * 60;
369    // For the purposes of this Duration type, Google ignores the subtleties of
370    // leap years on multiples of 100 and 400.
371    const SECONDS_IN_YEAR: i64 = 365 * SECONDS_IN_DAY + SECONDS_IN_DAY / 4;
372
373    #[test_case(10_000 * SECONDS_IN_YEAR , 0 ; "exactly 10,000 years")]
374    #[test_case(- 10_000 * SECONDS_IN_YEAR , 0 ; "exactly negative 10,000 years")]
375    #[test_case(10_000 * SECONDS_IN_YEAR , 999_999_999 ; "exactly 10,000 years and 999,999,999 nanos")]
376    #[test_case(- 10_000 * SECONDS_IN_YEAR , -999_999_999 ; "exactly negative 10,000 years and 999,999,999 nanos")]
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    #[test_case(-20_000 * SECONDS_IN_YEAR, 0, -10_000 * SECONDS_IN_YEAR, 0 ; "too many negative seconds")]
406    #[test_case(10_000 * SECONDS_IN_YEAR - 1, 1_999_999_999, 10_000 * SECONDS_IN_YEAR, 999_999_999 ; "upper edge of range")]
407    #[test_case(-10_000 * SECONDS_IN_YEAR + 1, -1_999_999_999, -10_000 * SECONDS_IN_YEAR, -999_999_999 ; "lower edge of range")]
408    #[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")]
409    #[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")]
410    #[test_case(0, 0, 0, 0 ; "all inputs are zero")]
411    #[test_case(1, 0, 1, 0 ; "positive seconds and zero nanos")]
412    #[test_case(1, 200_000, 1, 200_000 ; "positive seconds and nanos")]
413    #[test_case(-1, 0, -1, 0; "negative seconds and zero nanos")]
414    #[test_case(-1, -500_000_000, -1, -500_000_000; "negative seconds and nanos")]
415    #[test_case(2, -400_000_000, 1, 600_000_000; "positive seconds and negative nanos")]
416    #[test_case(-2, 400_000_000, -1, -600_000_000; "negative seconds and positive nanos")]
417    fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) -> Result {
418        let got = Duration::clamp(seconds, nanos);
419        let want = Duration {
420            seconds: want_seconds,
421            nanos: want_nanos,
422        };
423        assert_eq!(want, got);
424        Ok(())
425    }
426
427    // Verify durations can roundtrip from string -> struct -> string without loss.
428    #[test_case(0, 0, "0s" ; "zero")]
429    #[test_case(0, 2, "0.000000002s" ; "2ns")]
430    #[test_case(0, 200_000_000, "0.2s" ; "200ms")]
431    #[test_case(12, 0, "12s"; "round positive seconds")]
432    #[test_case(12, 123, "12.000000123s"; "positive seconds and nanos")]
433    #[test_case(12, 123_000, "12.000123000s"; "positive seconds and micros")]
434    #[test_case(12, 123_000_000, "12.123000000s"; "positive seconds and millis")]
435    #[test_case(12, 123_456_789, "12.123456789s"; "positive seconds and full nanos")]
436    #[test_case(-12, -0, "-12s"; "round negative seconds")]
437    #[test_case(-12, -123, "-12.000000123s"; "negative seconds and nanos")]
438    #[test_case(-12, -123_000, "-12.000123000s"; "negative seconds and micros")]
439    #[test_case(-12, -123_000_000, "-12.123000000s"; "negative seconds and millis")]
440    #[test_case(-12, -123_456_789, "-12.123456789s"; "negative seconds and full nanos")]
441    #[test_case(-10_000 * SECONDS_IN_YEAR, -999_999_999, "-315576000000.999999999s"; "range edge start")]
442    #[test_case(10_000 * SECONDS_IN_YEAR, 999_999_999, "315576000000.999999999s"; "range edge end")]
443    fn roundtrip(seconds: i64, nanos: i32, want: &str) -> Result {
444        let input = Duration::new(seconds, nanos)?;
445        let got = serde_json::to_value(&input)?
446            .as_str()
447            .map(str::to_string)
448            .ok_or("cannot convert value to string")?;
449        assert_eq!(want, got);
450
451        let rt = serde_json::from_value::<Duration>(serde_json::Value::String(got))?;
452        assert_eq!(input, rt);
453        Ok(())
454    }
455
456    #[test_case("-315576000001s"; "range edge start")]
457    #[test_case("315576000001s"; "range edge end")]
458    fn deserialize_out_of_range(input: &str) -> Result {
459        let value = serde_json::to_value(input)?;
460        let got = serde_json::from_value::<Duration>(value);
461        assert!(got.is_err());
462        Ok(())
463    }
464
465    #[test_case(time::Duration::default(), Duration::default() ; "default")]
466    #[test_case(time::Duration::new(0, 0), Duration::new(0, 0).unwrap() ; "zero")]
467    #[test_case(time::Duration::new(10_000 * SECONDS_IN_YEAR , 0), Duration::new(10_000 * SECONDS_IN_YEAR, 0).unwrap() ; "exactly 10,000 years")]
468    #[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")]
469    fn from_time_in_range(value: time::Duration, want: Duration) -> Result {
470        let got = Duration::try_from(value)?;
471        assert_eq!(got, want);
472        Ok(())
473    }
474
475    #[test_case(time::Duration::new(10_001 * SECONDS_IN_YEAR, 0) ; "above the range")]
476    #[test_case(time::Duration::new(-10_001 * SECONDS_IN_YEAR, 0) ; "below the range")]
477    fn from_time_out_of_range(value: time::Duration) {
478        let got = Duration::try_from(value);
479        assert_eq!(got, Err(DurationError::OutOfRange()));
480    }
481
482    #[test_case(Duration::default(), time::Duration::default() ; "default")]
483    #[test_case(Duration::new(0, 0).unwrap(), time::Duration::new(0, 0) ; "zero")]
484    #[test_case(Duration::new(10_000 * SECONDS_IN_YEAR , 0).unwrap(), time::Duration::new(10_000 * SECONDS_IN_YEAR, 0) ; "exactly 10,000 years")]
485    #[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")]
486    fn to_time_in_range(value: Duration, want: time::Duration) -> Result {
487        let got = time::Duration::from(value);
488        assert_eq!(got, want);
489        Ok(())
490    }
491
492    #[test_case("" ; "empty")]
493    #[test_case("1.0" ; "missing final s")]
494    #[test_case("1.2.3.4s" ; "too many periods")]
495    #[test_case("aaas" ; "not a number")]
496    #[test_case("aaaa.0s" ; "seconds are not a number [aaa]")]
497    #[test_case("1a.0s" ; "seconds are not a number [1a]")]
498    #[test_case("1.aaas" ; "nanos are not a number [aaa]")]
499    #[test_case("1.0as" ; "nanos are not a number [0a]")]
500    fn parse_detect_bad_input(input: &str) -> Result {
501        let got = Duration::try_from(input);
502        assert!(got.is_err());
503        let err = got.err().unwrap();
504        match err {
505            DurationError::Deserialize(_) => {
506                assert!(true)
507            }
508            _ => assert!(false, "unexpected error {err:?}"),
509        };
510        Ok(())
511    }
512
513    #[test]
514    fn deserialize_unexpected_input_type() -> Result {
515        let got = serde_json::from_value::<Duration>(serde_json::json!({}));
516        assert!(got.is_err());
517        let msg = format!("{got:?}");
518        assert!(msg.contains("duration in Google format"), "message={}", msg);
519        Ok(())
520    }
521
522    #[test_case(chrono::Duration::default(), Duration::default() ; "default")]
523    #[test_case(chrono::Duration::new(0, 0).unwrap(), Duration::new(0, 0).unwrap() ; "zero")]
524    #[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")]
525    #[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")]
526    fn from_chrono_time_in_range(value: chrono::Duration, want: Duration) -> Result {
527        let got = Duration::try_from(value)?;
528        assert_eq!(got, want);
529        Ok(())
530    }
531    #[test_case(Duration::default(), chrono::Duration::default() ; "default")]
532    #[test_case(Duration::new(0, 0).unwrap(), chrono::Duration::new(0, 0).unwrap() ; "zero")]
533    #[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")]
534    #[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")]
535    fn to_chrono_time_in_range(value: Duration, want: chrono::Duration) -> Result {
536        let got = chrono::Duration::from(value);
537        assert_eq!(got, want);
538        Ok(())
539    }
540
541    #[test_case(chrono::Duration::new(10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "above the range")]
542    #[test_case(chrono::Duration::new(-10_001 * SECONDS_IN_YEAR, 0).unwrap() ; "below the range")]
543    fn from_chrono_time_out_of_range(value: chrono::Duration) {
544        let got = Duration::try_from(value);
545        assert_eq!(got, Err(DurationError::OutOfRange()));
546    }
547}