prost_wkt_types/pbtime/
duration.rs

1use super::*;
2
3////////////////////////////////////////////////////////////////////////////////
4/// FROM prost-types/src/duration.rs
5////////////////////////////////////////////////////////////////////////////////
6
7#[cfg(feature = "std")]
8impl std::hash::Hash for Duration {
9    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
10        self.seconds.hash(state);
11        self.nanos.hash(state);
12    }
13}
14
15impl Duration {
16    /// Normalizes the duration to a canonical format.
17    ///
18    /// Based on [`google::protobuf::util::CreateNormalized`][1].
19    ///
20    /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L79-L100
21    pub fn normalize(&mut self) {
22        // Make sure nanos is in the range.
23        if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND {
24            if let Some(seconds) = self
25                .seconds
26                .checked_add((self.nanos / NANOS_PER_SECOND) as i64)
27            {
28                self.seconds = seconds;
29                self.nanos %= NANOS_PER_SECOND;
30            } else if self.nanos < 0 {
31                // Negative overflow! Set to the least normal value.
32                self.seconds = i64::MIN;
33                self.nanos = -NANOS_MAX;
34            } else {
35                // Positive overflow! Set to the greatest normal value.
36                self.seconds = i64::MAX;
37                self.nanos = NANOS_MAX;
38            }
39        }
40
41        // nanos should have the same sign as seconds.
42        if self.seconds < 0 && self.nanos > 0 {
43            if let Some(seconds) = self.seconds.checked_add(1) {
44                self.seconds = seconds;
45                self.nanos -= NANOS_PER_SECOND;
46            } else {
47                // Positive overflow! Set to the greatest normal value.
48                debug_assert_eq!(self.seconds, i64::MAX);
49                self.nanos = NANOS_MAX;
50            }
51        } else if self.seconds > 0 && self.nanos < 0 {
52            if let Some(seconds) = self.seconds.checked_sub(1) {
53                self.seconds = seconds;
54                self.nanos += NANOS_PER_SECOND;
55            } else {
56                // Negative overflow! Set to the least normal value.
57                debug_assert_eq!(self.seconds, i64::MIN);
58                self.nanos = -NANOS_MAX;
59            }
60        }
61        // TODO: should this be checked?
62        // debug_assert!(self.seconds >= -315_576_000_000 && self.seconds <= 315_576_000_000,
63        //               "invalid duration: {:?}", self);
64    }
65}
66
67// impl Name for Duration {
68//     const PACKAGE: &'static str = PACKAGE;
69//     const NAME: &'static str = "Duration";
70
71//     fn type_url() -> String {
72//         type_url_for::<Self>()
73//     }
74// }
75
76impl TryFrom<time::Duration> for Duration {
77    type Error = DurationError;
78
79    /// Converts a `std::time::Duration` to a `Duration`, failing if the duration is too large.
80    fn try_from(duration: time::Duration) -> Result<Duration, DurationError> {
81        let seconds = i64::try_from(duration.as_secs()).map_err(|_| DurationError::OutOfRange)?;
82        let nanos = duration.subsec_nanos() as i32;
83
84        let mut duration = Duration { seconds, nanos };
85        duration.normalize();
86        Ok(duration)
87    }
88}
89
90impl TryFrom<Duration> for time::Duration {
91    type Error = DurationError;
92
93    /// Converts a `Duration` to a `std::time::Duration`, failing if the duration is negative.
94    fn try_from(mut duration: Duration) -> Result<time::Duration, DurationError> {
95        duration.normalize();
96        if duration.seconds >= 0 && duration.nanos >= 0 {
97            Ok(time::Duration::new(
98                duration.seconds as u64,
99                duration.nanos as u32,
100            ))
101        } else {
102            Err(DurationError::NegativeDuration(time::Duration::new(
103                (-duration.seconds) as u64,
104                (-duration.nanos) as u32,
105            )))
106        }
107    }
108}
109
110impl fmt::Display for Duration {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        let mut d = *self;
113        d.normalize();
114        if self.seconds < 0 && self.nanos < 0 {
115            write!(f, "-")?;
116        }
117        write!(f, "{}", d.seconds.abs())?;
118
119        // Format subseconds to either nothing, millis, micros, or nanos.
120        let nanos = d.nanos.abs();
121        if nanos == 0 {
122            write!(f, "s")
123        } else if nanos % 1_000_000 == 0 {
124            write!(f, ".{:03}s", nanos / 1_000_000)
125        } else if nanos % 1_000 == 0 {
126            write!(f, ".{:06}s", nanos / 1_000)
127        } else {
128            write!(f, ".{:09}s", nanos)
129        }
130    }
131}
132
133/// A duration handling error.
134#[allow(clippy::derive_partial_eq_without_eq)]
135#[derive(Debug, PartialEq)]
136#[non_exhaustive]
137pub enum DurationError {
138    /// Indicates failure to parse a [`Duration`] from a string.
139    ///
140    /// The [`Duration`] string format is specified in the [Protobuf JSON mapping specification][1].
141    ///
142    /// [1]: https://developers.google.com/protocol-buffers/docs/proto3#json
143    ParseFailure,
144
145    /// Indicates failure to convert a `prost_types::Duration` to a `std::time::Duration` because
146    /// the duration is negative. The included `std::time::Duration` matches the magnitude of the
147    /// original negative `prost_types::Duration`.
148    NegativeDuration(time::Duration),
149
150    /// Indicates failure to convert a `std::time::Duration` to a `prost_types::Duration`.
151    ///
152    /// Converting a `std::time::Duration` to a `prost_types::Duration` fails if the magnitude
153    /// exceeds that representable by `prost_types::Duration`.
154    OutOfRange,
155}
156
157impl fmt::Display for DurationError {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            DurationError::ParseFailure => write!(f, "failed to parse duration"),
161            DurationError::NegativeDuration(duration) => {
162                write!(f, "failed to convert negative duration: {:?}", duration)
163            }
164            DurationError::OutOfRange => {
165                write!(f, "failed to convert duration out of range")
166            }
167        }
168    }
169}
170
171#[cfg(feature = "std")]
172impl std::error::Error for DurationError {}
173
174impl FromStr for Duration {
175    type Err = DurationError;
176
177    fn from_str(s: &str) -> Result<Duration, DurationError> {
178        datetime::parse_duration(s).ok_or(DurationError::ParseFailure)
179    }
180}
181
182////////////////////////////////////////////////////////////////////////////////
183/// Chrono conversion
184////////////////////////////////////////////////////////////////////////////////
185
186/// Converts proto duration to chrono's Duration
187impl From<Duration> for chrono::Duration {
188    fn from(val: Duration) -> Self {
189        let mut value = val;
190        // A call to `normalize` should capture all out-of-bound sitations hopefully
191        // ensuring a panic never happens! Ideally this implementation should be
192        // deprecated in favour of TryFrom but unfortunately having `TryFrom` along with
193        // `From` causes a conflict.        
194        value.normalize();
195        let s = chrono::TimeDelta::try_seconds(value.seconds).expect("invalid or out-of-range seconds");
196        let ns = chrono::Duration::nanoseconds(value.nanos as i64);
197        s + ns
198    }
199}
200
201/// Converts chrono Duration to proto duration
202impl From<chrono::Duration> for Duration {
203    fn from(val: chrono::Duration) -> Self {
204        Duration {
205            seconds: val.num_seconds(),
206            nanos: (val.num_nanoseconds().unwrap() % 1_000_000_000) as i32,
207        }
208    }
209}
210
211impl Serialize for Duration {
212    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
213    where
214        S: Serializer,
215    {
216        let seconds: f64 = self.seconds as f64 + self.nanos as f64 / NANOS_PER_SECOND as f64;
217        // Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix "s". Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix "s" is required.
218        // see: https://protobuf.dev/programming-guides/proto3/#json
219        //
220        // this code currently *always* serializes with 9 fractional digits.
221        serializer.serialize_str(&format!("{:.9}s", seconds))
222    }
223}
224
225impl<'de> Deserialize<'de> for Duration {
226    fn deserialize<D>(deserializer: D) -> Result<Duration, D::Error>
227    where
228        D: Deserializer<'de>,
229    {
230        struct DurationVisitor;
231
232        impl<'de> de::Visitor<'de> for DurationVisitor {
233            type Value = Duration;
234
235            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
236                formatter.write_str("A duration ending in 's'")
237            }
238
239            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
240            where
241                E: de::Error,
242            {
243                if !value.ends_with('s') {
244                    return Err(de::Error::custom("Duration should end with 's'"));
245                }
246
247                let duration_str = &value[..value.len() - 1]; // Remove 's' from the end
248
249                let mut parts = duration_str.split('.'); // Split seconds and fractional seconds
250
251                let seconds: i64 = parts
252                    .next()
253                    .ok_or_else(|| de::Error::custom("Missing seconds"))?
254                    .parse()
255                    .map_err(de::Error::custom)?;
256
257                let nanos: i32 = match parts.next() {
258                    Some(fraction) => {
259                        let fraction = format!("{:0<9}", fraction); // Pad fraction to nanoseconds
260                        let nanos = fraction.parse().map_err(de::Error::custom)?;
261                        if nanos < 0 || nanos >= NANOS_PER_SECOND as i32 {
262                            return Err(de::Error::custom(format!(
263                                "Fractional nanoseconds out of range: {}",
264                                nanos
265                            )));
266                        }
267                        nanos
268                    }
269                    None => 0,
270                };
271
272                Ok(Duration { seconds, nanos })
273            }
274        }
275
276        deserializer.deserialize_str(DurationVisitor)
277    }
278}
279
280
281