prost_wkt_types/pbtime/
timestamp.rs

1use super::*;
2
3////////////////////////////////////////////////////////////////////////////////
4/// FROM prost-types/src/timestamp.rs
5////////////////////////////////////////////////////////////////////////////////
6
7impl Timestamp {
8    /// Normalizes the timestamp to a canonical format.
9    ///
10    /// Based on [`google::protobuf::util::CreateNormalized`][1].
11    ///
12    /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
13    pub fn normalize(&mut self) {
14        // Make sure nanos is in the range.
15        if self.nanos <= -NANOS_PER_SECOND || self.nanos >= NANOS_PER_SECOND {
16            if let Some(seconds) = self
17                .seconds
18                .checked_add((self.nanos / NANOS_PER_SECOND) as i64)
19            {
20                self.seconds = seconds;
21                self.nanos %= NANOS_PER_SECOND;
22            } else if self.nanos < 0 {
23                // Negative overflow! Set to the earliest normal value.
24                self.seconds = i64::MIN;
25                self.nanos = 0;
26            } else {
27                // Positive overflow! Set to the latest normal value.
28                self.seconds = i64::MAX;
29                self.nanos = 999_999_999;
30            }
31        }
32
33        // For Timestamp nanos should be in the range [0, 999999999].
34        if self.nanos < 0 {
35            if let Some(seconds) = self.seconds.checked_sub(1) {
36                self.seconds = seconds;
37                self.nanos += NANOS_PER_SECOND;
38            } else {
39                // Negative overflow! Set to the earliest normal value.
40                debug_assert_eq!(self.seconds, i64::MIN);
41                self.nanos = 0;
42            }
43        }
44
45        // TODO: should this be checked?
46        // debug_assert!(self.seconds >= -62_135_596_800 && self.seconds <= 253_402_300_799,
47        //               "invalid timestamp: {:?}", self);
48    }
49
50    /// Normalizes the timestamp to a canonical format, returning the original value if it cannot be
51    /// normalized.
52    ///
53    /// Normalization is based on [`google::protobuf::util::CreateNormalized`][1].
54    ///
55    /// [1]: https://github.com/google/protobuf/blob/v3.3.2/src/google/protobuf/util/time_util.cc#L59-L77
56    pub fn try_normalize(mut self) -> Result<Timestamp, Timestamp> {
57        let before = self;
58        self.normalize();
59        // If the seconds value has changed, and is either i64::MIN or i64::MAX, then the timestamp
60        // normalization overflowed.
61        if (self.seconds == i64::MAX || self.seconds == i64::MIN) && self.seconds != before.seconds
62        {
63            Err(before)
64        } else {
65            Ok(self)
66        }
67    }
68
69    /// Creates a new `Timestamp` at the start of the provided UTC date.
70    pub fn date(year: i64, month: u8, day: u8) -> Result<Timestamp, TimestampError> {
71        Timestamp::date_time_nanos(year, month, day, 0, 0, 0, 0)
72    }
73
74    /// Creates a new `Timestamp` instance with the provided UTC date and time.
75    pub fn date_time(
76        year: i64,
77        month: u8,
78        day: u8,
79        hour: u8,
80        minute: u8,
81        second: u8,
82    ) -> Result<Timestamp, TimestampError> {
83        Timestamp::date_time_nanos(year, month, day, hour, minute, second, 0)
84    }
85
86    /// Creates a new `Timestamp` instance with the provided UTC date and time.
87    pub fn date_time_nanos(
88        year: i64,
89        month: u8,
90        day: u8,
91        hour: u8,
92        minute: u8,
93        second: u8,
94        nanos: u32,
95    ) -> Result<Timestamp, TimestampError> {
96        let date_time = datetime::DateTime {
97            year,
98            month,
99            day,
100            hour,
101            minute,
102            second,
103            nanos,
104        };
105
106        Timestamp::try_from(date_time)
107    }
108}
109
110// impl Name for Timestamp {
111//     const PACKAGE: &'static str = PACKAGE;
112//     const NAME: &'static str = "Timestamp";
113
114//     fn type_url() -> String {
115//         type_url_for::<Self>()
116//     }
117// }
118
119/// Implements the unstable/naive version of `Eq`: a basic equality check on the internal fields of the `Timestamp`.
120/// This implies that `normalized_ts != non_normalized_ts` even if `normalized_ts == non_normalized_ts.normalized()`.
121#[cfg(feature = "std")]
122impl Eq for Timestamp {}
123
124#[cfg(feature = "std")]
125impl std::hash::Hash for Timestamp {
126    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
127        self.seconds.hash(state);
128        self.nanos.hash(state);
129    }
130}
131
132#[cfg(feature = "std")]
133impl From<std::time::SystemTime> for Timestamp {
134    fn from(system_time: std::time::SystemTime) -> Timestamp {
135        let (seconds, nanos) = match system_time.duration_since(std::time::UNIX_EPOCH) {
136            Ok(duration) => {
137                let seconds = i64::try_from(duration.as_secs()).unwrap();
138                (seconds, duration.subsec_nanos() as i32)
139            }
140            Err(error) => {
141                let duration = error.duration();
142                let seconds = i64::try_from(duration.as_secs()).unwrap();
143                let nanos = duration.subsec_nanos() as i32;
144                if nanos == 0 {
145                    (-seconds, 0)
146                } else {
147                    (-seconds - 1, 1_000_000_000 - nanos)
148                }
149            }
150        };
151        Timestamp { seconds, nanos }
152    }
153}
154
155/// A timestamp handling error.
156#[allow(clippy::derive_partial_eq_without_eq)]
157#[derive(Debug, PartialEq)]
158#[non_exhaustive]
159pub enum TimestampError {
160    /// Indicates that a [`Timestamp`] could not be converted to
161    /// [`SystemTime`][std::time::SystemTime] because it is out of range.
162    ///
163    /// The range of times that can be represented by `SystemTime` depends on the platform. All
164    /// `Timestamp`s are likely representable on 64-bit Unix-like platforms, but other platforms,
165    /// such as Windows and 32-bit Linux, may not be able to represent the full range of
166    /// `Timestamp`s.
167    OutOfSystemRange(Timestamp),
168
169    /// An error indicating failure to parse a timestamp in RFC-3339 format.
170    ParseFailure,
171
172    /// Indicates an error when constructing a timestamp due to invalid date or time data.
173    InvalidDateTime,
174}
175
176impl fmt::Display for TimestampError {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            TimestampError::OutOfSystemRange(timestamp) => {
180                write!(
181                    f,
182                    "{} is not representable as a `SystemTime` because it is out of range",
183                    timestamp
184                )
185            }
186            TimestampError::ParseFailure => {
187                write!(f, "failed to parse RFC-3339 formatted timestamp")
188            }
189            TimestampError::InvalidDateTime => {
190                write!(f, "invalid date or time")
191            }
192        }
193    }
194}
195
196#[cfg(feature = "std")]
197impl std::error::Error for TimestampError {}
198
199#[cfg(feature = "std")]
200impl TryFrom<Timestamp> for std::time::SystemTime {
201    type Error = TimestampError;
202
203    fn try_from(mut timestamp: Timestamp) -> Result<std::time::SystemTime, Self::Error> {
204        let orig_timestamp = timestamp;
205        timestamp.normalize();
206
207        let system_time = if timestamp.seconds >= 0 {
208            std::time::UNIX_EPOCH.checked_add(time::Duration::from_secs(timestamp.seconds as u64))
209        } else {
210            std::time::UNIX_EPOCH.checked_sub(time::Duration::from_secs(
211                timestamp
212                    .seconds
213                    .checked_neg()
214                    .ok_or(TimestampError::OutOfSystemRange(timestamp))? as u64,
215            ))
216        };
217
218        let system_time = system_time.and_then(|system_time| {
219            system_time.checked_add(time::Duration::from_nanos(timestamp.nanos as u64))
220        });
221
222        system_time.ok_or(TimestampError::OutOfSystemRange(orig_timestamp))
223    }
224}
225
226impl FromStr for Timestamp {
227    type Err = TimestampError;
228
229    fn from_str(s: &str) -> Result<Timestamp, TimestampError> {
230        datetime::parse_timestamp(s).ok_or(TimestampError::ParseFailure)
231    }
232}
233
234impl fmt::Display for Timestamp {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        datetime::DateTime::from(*self).fmt(f)
237    }
238}
239
240////////////////////////////////////////////////////////////////////////////////
241/// Chrono conversion
242////////////////////////////////////////////////////////////////////////////////
243
244/// Converts chrono's `NaiveDateTime` to `Timestamp`..
245impl From<NaiveDateTime> for Timestamp {
246    fn from(dt: NaiveDateTime) -> Self {
247        Timestamp {
248            seconds: dt.and_utc().timestamp(),
249            nanos: dt.and_utc().timestamp_subsec_nanos() as i32,
250        }
251    }
252}
253
254/// Converts chrono's `DateTime<UTtc>` to `Timestamp`
255impl From<DateTime<Utc>> for Timestamp {
256    fn from(dt: DateTime<Utc>) -> Self {
257        Timestamp {
258            seconds: dt.timestamp(),
259            nanos: dt.timestamp_subsec_nanos() as i32,
260        }
261    }
262}
263
264/// Converts proto timestamp to chrono's DateTime<Utc>
265impl From<Timestamp> for DateTime<Utc> {
266    fn from(val: Timestamp) -> Self {
267        let mut value = val;
268        // A call to `normalize` should capture all out-of-bound sitations hopefully
269        // ensuring a panic never happens! Ideally this implementation should be
270        // deprecated in favour of TryFrom but unfortunately having `TryFrom` along with
271        // `From` causes a conflict.
272        value.normalize();
273        DateTime::from_timestamp(value.seconds, value.nanos as u32)
274            .expect("invalid or out-of-range datetime")
275    }
276}
277
278impl Serialize for Timestamp {
279    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
280    where
281        S: Serializer,
282    {
283        let mut ts = Timestamp {
284            seconds: self.seconds,
285            nanos: self.nanos,
286        };
287        ts.normalize();
288        let dt: DateTime<Utc> = ts.try_into().map_err(serde::ser::Error::custom)?;
289        serializer.serialize_str(format!("{dt:?}").as_str())
290    }
291}
292
293impl<'de> Deserialize<'de> for Timestamp {
294    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
295    where
296        D: Deserializer<'de>,
297    {
298        struct TimestampVisitor;
299
300        impl<'de> Visitor<'de> for TimestampVisitor {
301            type Value = Timestamp;
302
303            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
304                formatter.write_str("Timestamp in RFC3339 format")
305            }
306
307            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
308            where
309                E: de::Error,
310            {
311                let utc: DateTime<Utc> = chrono::DateTime::from_str(value).map_err(|err| {
312                    serde::de::Error::custom(format!(
313                        "Failed to parse {value} as datetime: {err:?}"
314                    ))
315                })?;
316                let ts = Timestamp::from(utc);
317                Ok(ts)
318            }
319        }
320        deserializer.deserialize_str(TimestampVisitor)
321    }
322}
323