google_cloud_wkt/
timestamp.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 point in time representation for Google APIs.
16///
17/// A Timestamp represents a point in time independent of any time zone or local
18/// calendar, encoded as a count of seconds and fractions of seconds at
19/// nanosecond resolution. The count is relative to an epoch at UTC midnight on
20/// January 1, 1970, in the proleptic Gregorian calendar which extends the
21/// Gregorian calendar backwards to year one.
22///
23/// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
24/// second table is needed for interpretation, using a [24-hour linear
25/// smear](https://developers.google.com/time/smear).
26///
27/// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
28/// restricting to that range, we ensure that we can convert to and from [RFC
29/// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
30///
31/// # JSON Mapping
32///
33/// In JSON format, the Timestamp type is encoded as a string in the
34/// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
35/// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
36/// where {year} is always expressed using four digits while {month}, {day},
37/// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
38/// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
39/// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
40/// is required.
41///
42/// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
43/// 01:30 UTC on January 15, 2017.
44///
45#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
46#[non_exhaustive]
47pub struct Timestamp {
48    /// Represents seconds of UTC time since Unix epoch
49    /// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
50    /// 9999-12-31T23:59:59Z inclusive.
51    seconds: i64,
52
53    /// Non-negative fractions of a second at nanosecond resolution. Negative
54    /// second values with fractions must still have non-negative nanos values
55    /// that count forward in time. Must be from 0 to 999,999,999
56    /// inclusive.
57    nanos: i32,
58}
59
60/// Represent failures in converting or creating [Timestamp] instances.
61#[derive(thiserror::Error, Debug, PartialEq)]
62pub enum TimestampError {
63    /// One of the components (seconds and/or nanoseconds) was out of range.
64    #[error("seconds and/or nanoseconds out of range")]
65    OutOfRange(),
66
67    #[error("cannot serialize timestamp: {0}")]
68    Serialize(String),
69
70    #[error("cannot deserialize timestamp: {0}")]
71    Deserialize(String),
72}
73
74type Error = TimestampError;
75
76impl Timestamp {
77    const NS: i32 = 1_000_000_000;
78
79    // Obtained via: `date +%s --date='0001-01-01T00:00:00Z'`
80    /// The minimum value for the `seconds` component. Corresponds to '0001-01-01T00:00:00Z'.
81    pub const MIN_SECONDS: i64 = -62135596800;
82
83    // Obtained via: `date +%s --date='9999-12-31T23:59:59Z'`
84    /// The maximum value for the `seconds` component. Corresponds to '9999-12-31T23:59:59Z'.
85    pub const MAX_SECONDS: i64 = 253402300799;
86
87    /// The minimum value for the `nanos` component.
88    pub const MIN_NANOS: i32 = 0;
89
90    /// The maximum value for the `nanos` component.
91    pub const MAX_NANOS: i32 = Self::NS - 1;
92
93    /// Creates a new [Timestamp] from the seconds and nanoseconds.
94    ///
95    /// If either value is out of range it returns an error.
96    ///
97    /// # Arguments
98    ///
99    /// * `seconds` - the seconds on the timestamp.
100    /// * `nanos` - the nanoseconds on the timestamp.
101    pub fn new(seconds: i64, nanos: i32) -> std::result::Result<Self, Error> {
102        if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
103            return Err(Error::OutOfRange());
104        }
105        if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
106            return Err(Error::OutOfRange());
107        }
108        Ok(Self { seconds, nanos })
109    }
110
111    /// Create a normalized, clamped [Timestamp].
112    ///
113    /// Timestamps must be between 0001-01-01T00:00:00Z and
114    /// 9999-12-31T23:59:59.999999999Z, and the nanoseconds component must
115    /// always be in the range [0, 999_999_999]. This function creates a
116    /// new [Timestamp] instance clamped to those ranges.
117    ///
118    /// The function effectively adds the nanoseconds part (with carry) to the
119    /// seconds part, with saturation.
120    ///
121    /// # Arguments
122    ///
123    /// * `seconds` - the seconds on the timestamp.
124    /// * `nanos` - the nanoseconds added to the seconds.
125    pub fn clamp(seconds: i64, nanos: i32) -> Self {
126        let (seconds, nanos) = match nanos.cmp(&0_i32) {
127            std::cmp::Ordering::Equal => (seconds, nanos),
128            std::cmp::Ordering::Greater => (
129                seconds.saturating_add((nanos / Self::NS) as i64),
130                nanos % Self::NS,
131            ),
132            std::cmp::Ordering::Less => (
133                seconds.saturating_sub(1 - (nanos / Self::NS) as i64),
134                Self::NS + nanos % Self::NS,
135            ),
136        };
137        if seconds < Self::MIN_SECONDS {
138            return Self {
139                seconds: Self::MIN_SECONDS,
140                nanos: 0,
141            };
142        } else if seconds > Self::MAX_SECONDS {
143            return Self {
144                seconds: Self::MAX_SECONDS,
145                nanos: 0,
146            };
147        }
148        Self { seconds, nanos }
149    }
150
151    /// Represents seconds of UTC time since Unix epoch (1970-01-01T00:00:00Z).
152    ///
153    /// Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.
154    pub fn seconds(&self) -> i64 {
155        self.seconds
156    }
157
158    /// Non-negative fractions of a second at nanosecond resolution.
159    ///
160    /// Negative second values (before the Unix epoch) with fractions must still
161    /// have non-negative nanos values that count forward in time. Must be from
162    /// 0 to 999,999,999 inclusive.
163    pub fn nanos(&self) -> i32 {
164        self.nanos
165    }
166}
167
168impl crate::message::Message for Timestamp {
169    fn typename() -> &'static str {
170        "type.googleapis.com/google.protobuf.Timestamp"
171    }
172    fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
173        crate::message::to_json_string(self)
174    }
175    fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
176        crate::message::from_value(map)
177    }
178}
179
180use time::format_description::well_known::Rfc3339;
181const NS: i128 = 1_000_000_000;
182
183/// Implement [`serde`](::serde) serialization for timestamps.
184impl serde::ser::Serialize for Timestamp {
185    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
186    where
187        S: serde::ser::Serializer,
188    {
189        use serde::ser::Error as _;
190        String::try_from(self)
191            .map_err(S::Error::custom)?
192            .serialize(serializer)
193    }
194}
195
196struct TimestampVisitor;
197
198impl serde::de::Visitor<'_> for TimestampVisitor {
199    type Value = Timestamp;
200
201    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
202        formatter.write_str("a string with a timestamp in RFC 3339 format")
203    }
204
205    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
206    where
207        E: serde::de::Error,
208    {
209        Timestamp::try_from(value).map_err(E::custom)
210    }
211}
212
213/// Implement [`serde`](::serde) deserialization for timestamps.
214impl<'de> serde::de::Deserialize<'de> for Timestamp {
215    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216    where
217        D: serde::Deserializer<'de>,
218    {
219        deserializer.deserialize_str(TimestampVisitor)
220    }
221}
222
223/// Convert from [time::OffsetDateTime] to [Timestamp].
224///
225/// This conversion may fail if the [time::OffsetDateTime] value is out of range.
226#[cfg(feature = "time")]
227impl TryFrom<time::OffsetDateTime> for Timestamp {
228    type Error = TimestampError;
229
230    fn try_from(value: time::OffsetDateTime) -> std::result::Result<Self, Self::Error> {
231        use time::convert::{Nanosecond, Second};
232
233        let seconds = value.unix_timestamp();
234        let nanos = (value.unix_timestamp_nanos()
235            - seconds as i128 * Nanosecond::per(Second) as i128) as i32;
236        Self::new(seconds, nanos)
237    }
238}
239
240/// Convert from [Timestamp] to [OffsetDateTime][time::OffsetDateTime]
241///
242/// This conversion may fail if the [Timestamp] value is out of range.
243#[cfg(feature = "time")]
244impl TryFrom<Timestamp> for time::OffsetDateTime {
245    type Error = time::error::ComponentRange;
246    fn try_from(value: Timestamp) -> std::result::Result<Self, Self::Error> {
247        let ts = time::OffsetDateTime::from_unix_timestamp(value.seconds())?;
248        Ok(ts + time::Duration::nanoseconds(value.nanos() as i64))
249    }
250}
251
252/// Converts a [Timestamp] to its [String] representation.
253impl TryFrom<&Timestamp> for String {
254    type Error = TimestampError;
255    fn try_from(timestamp: &Timestamp) -> std::result::Result<Self, Self::Error> {
256        let ts = time::OffsetDateTime::from_unix_timestamp_nanos(
257            timestamp.seconds as i128 * NS + timestamp.nanos as i128,
258        )
259        .map_err(|e| TimestampError::Serialize(format!("{e}")))?;
260        ts.format(&Rfc3339)
261            .map_err(|e| TimestampError::Serialize(format!("{e}")))
262    }
263}
264
265/// Converts the [String] representation of a timestamp to [Timestamp].
266impl TryFrom<&str> for Timestamp {
267    type Error = TimestampError;
268    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
269        let odt = time::OffsetDateTime::parse(value, &Rfc3339)
270            .map_err(|e| TimestampError::Deserialize(format!("{e}")))?;
271        let nanos_since_epoch = odt.unix_timestamp_nanos();
272        let seconds = (nanos_since_epoch / NS) as i64;
273        let nanos = (nanos_since_epoch % NS) as i32;
274        Timestamp::new(seconds, nanos)
275    }
276}
277
278/// Converts from [chrono::DateTime] to [Timestamp].
279///
280/// This conversion may fail if the [chrono::DateTime] value is out of range.
281#[cfg(feature = "chrono")]
282impl TryFrom<chrono::DateTime<chrono::Utc>> for Timestamp {
283    type Error = TimestampError;
284
285    fn try_from(value: chrono::DateTime<chrono::Utc>) -> std::result::Result<Self, Self::Error> {
286        assert!(value.timestamp_subsec_nanos() <= (i32::MAX as u32));
287        Timestamp::new(value.timestamp(), value.timestamp_subsec_nanos() as i32)
288    }
289}
290
291/// Converts from [Timestamp] to [chrono::DateTime].
292#[cfg(feature = "chrono")]
293impl TryFrom<Timestamp> for chrono::DateTime<chrono::Utc> {
294    type Error = TimestampError;
295    fn try_from(value: Timestamp) -> std::result::Result<Self, Self::Error> {
296        let ts = chrono::DateTime::from_timestamp(value.seconds, 0).unwrap();
297        Ok(ts + chrono::Duration::nanoseconds(value.nanos as i64))
298    }
299}
300
301#[cfg(test)]
302mod test {
303    use super::*;
304    use serde_json::json;
305    use test_case::test_case;
306    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
307
308    // Verify the epoch converts as expected.
309    #[test]
310    fn unix_epoch() -> Result {
311        let proto = Timestamp::default();
312        let json = serde_json::to_value(&proto)?;
313        let expected = json!("1970-01-01T00:00:00Z");
314        assert_eq!(json, expected);
315        let roundtrip = serde_json::from_value::<Timestamp>(json)?;
316        assert_eq!(proto, roundtrip);
317        Ok(())
318    }
319
320    fn get_seconds(input: &str) -> i64 {
321        let odt = time::OffsetDateTime::parse(input, &Rfc3339);
322        let odt = odt.unwrap();
323        odt.unix_timestamp()
324    }
325
326    fn get_min_seconds() -> i64 {
327        self::get_seconds("0001-01-01T00:00:00Z")
328    }
329
330    fn get_max_seconds() -> i64 {
331        self::get_seconds("9999-12-31T23:59:59Z")
332    }
333
334    #[test_case(get_min_seconds() - 1, 0; "seconds below range")]
335    #[test_case(get_max_seconds() + 1, 0; "seconds above range")]
336    #[test_case(0, -1; "nanos below range")]
337    #[test_case(0, 1_000_000_000; "nanos above range")]
338    fn new_out_of_range(seconds: i64, nanos: i32) -> Result {
339        let t = Timestamp::new(seconds, nanos);
340        assert_eq!(t, Err(Error::OutOfRange()));
341        Ok(())
342    }
343
344    #[test_case(0, 0, 0, 0; "zero")]
345    #[test_case(0, 1_234_567_890, 1, 234_567_890; "nanos overflow")]
346    #[test_case(0, -1_400_000_000, -2, 600_000_000; "nanos underflow")]
347    #[test_case(self::get_max_seconds() + 1, 0, get_max_seconds(), 0; "seconds over range")]
348    #[test_case(self::get_min_seconds() - 1, 0, get_min_seconds(), 0; "seconds below range")]
349    #[test_case(self::get_max_seconds() - 1, 2_000_000_001, get_max_seconds(), 0; "nanos overflow range")]
350    #[test_case(self::get_min_seconds() + 1, -1_500_000_000, get_min_seconds(), 0; "nanos underflow range")]
351    fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) {
352        let got = Timestamp::clamp(seconds, nanos);
353        let want = Timestamp {
354            seconds: want_seconds,
355            nanos: want_nanos,
356        };
357        assert_eq!(got, want);
358    }
359
360    // Verify timestamps can roundtrip from string -> struct -> string without loss.
361    #[test_case("0001-01-01T00:00:00Z")]
362    #[test_case("9999-12-31T23:59:59.999999999Z")]
363    #[test_case("2024-10-19T12:34:56.789Z")]
364    #[test_case("2024-10-19T12:34:56.789123456Z")]
365    fn roundtrip(input: &str) -> Result {
366        let json = serde_json::Value::String(input.to_string());
367        let timestamp = serde_json::from_value::<Timestamp>(json)?;
368        let roundtrip = serde_json::to_string(&timestamp)?;
369        assert_eq!(
370            format!("\"{input}\""),
371            roundtrip,
372            "mismatched value for input={input}"
373        );
374        Ok(())
375    }
376
377    #[test_case("0000-01-01T00:00:00Z"; "below range")]
378    #[test_case("10000-01-01T00:00:00Z"; "above range")]
379    fn deserialize_out_of_range(input: &str) -> Result {
380        let value = serde_json::to_value(input)?;
381        let got = serde_json::from_value::<Timestamp>(value);
382        assert!(got.is_err());
383        Ok(())
384    }
385
386    #[test]
387    fn deserialize_unexpected_input_type() -> Result {
388        let got = serde_json::from_value::<Timestamp>(serde_json::json!({}));
389        assert!(got.is_err());
390        let msg = format!("{got:?}");
391        assert!(msg.contains("RFC 3339"), "message={}", msg);
392        Ok(())
393    }
394}