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) -> 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) -> 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) -> 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) -> 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) -> 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) -> 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        if nanos < 0 {
275            return Timestamp::new(seconds - 1, Self::NS + nanos);
276        }
277        Timestamp::new(seconds, nanos)
278    }
279}
280
281/// Converts from [chrono::DateTime] to [Timestamp].
282///
283/// This conversion may fail if the [chrono::DateTime] value is out of range.
284#[cfg(feature = "chrono")]
285impl TryFrom<chrono::DateTime<chrono::Utc>> for Timestamp {
286    type Error = TimestampError;
287
288    fn try_from(value: chrono::DateTime<chrono::Utc>) -> Result<Self, Self::Error> {
289        assert!(value.timestamp_subsec_nanos() <= (i32::MAX as u32));
290        Timestamp::new(value.timestamp(), value.timestamp_subsec_nanos() as i32)
291    }
292}
293
294/// Converts from [Timestamp] to [chrono::DateTime].
295#[cfg(feature = "chrono")]
296impl TryFrom<Timestamp> for chrono::DateTime<chrono::Utc> {
297    type Error = TimestampError;
298    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
299        let ts = chrono::DateTime::from_timestamp(value.seconds, 0).unwrap();
300        Ok(ts + chrono::Duration::nanoseconds(value.nanos as i64))
301    }
302}
303
304#[cfg(test)]
305mod test {
306    use super::*;
307    use serde_json::json;
308    use test_case::test_case;
309    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
310
311    // Verify the epoch converts as expected.
312    #[test]
313    fn unix_epoch() -> Result {
314        let proto = Timestamp::default();
315        let json = serde_json::to_value(&proto)?;
316        let expected = json!("1970-01-01T00:00:00Z");
317        assert_eq!(json, expected);
318        let roundtrip = serde_json::from_value::<Timestamp>(json)?;
319        assert_eq!(proto, roundtrip);
320        Ok(())
321    }
322
323    fn get_seconds(input: &str) -> i64 {
324        let odt = time::OffsetDateTime::parse(input, &Rfc3339);
325        let odt = odt.unwrap();
326        odt.unix_timestamp()
327    }
328
329    fn get_min_seconds() -> i64 {
330        self::get_seconds("0001-01-01T00:00:00Z")
331    }
332
333    fn get_max_seconds() -> i64 {
334        self::get_seconds("9999-12-31T23:59:59Z")
335    }
336
337    #[test_case(get_min_seconds() - 1, 0; "seconds below range")]
338    #[test_case(get_max_seconds() + 1, 0; "seconds above range")]
339    #[test_case(0, -1; "nanos below range")]
340    #[test_case(0, 1_000_000_000; "nanos above range")]
341    fn new_out_of_range(seconds: i64, nanos: i32) -> Result {
342        let t = Timestamp::new(seconds, nanos);
343        assert_eq!(t, Err(Error::OutOfRange()));
344        Ok(())
345    }
346
347    #[test_case(0, 0, 0, 0; "zero")]
348    #[test_case(0, 1_234_567_890, 1, 234_567_890; "nanos overflow")]
349    #[test_case(0, 2_100_000_123, 2, 100_000_123; "nanos overflow x2")]
350    #[test_case(0, -1_400_000_000, -2, 600_000_000; "nanos underflow")]
351    #[test_case(0, -2_100_000_000, -3, 900_000_000; "nanos underflow x2")]
352    #[test_case(self::get_max_seconds() + 1, 0, get_max_seconds(), 0; "seconds over range")]
353    #[test_case(self::get_min_seconds() - 1, 0, get_min_seconds(), 0; "seconds below range")]
354    #[test_case(self::get_max_seconds() - 1, 2_000_000_001, get_max_seconds(), 0; "nanos overflow range"
355	)]
356    #[test_case(self::get_min_seconds() + 1, -1_500_000_000, get_min_seconds(), 0; "nanos underflow range"
357	)]
358    fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) {
359        let got = Timestamp::clamp(seconds, nanos);
360        let want = Timestamp {
361            seconds: want_seconds,
362            nanos: want_nanos,
363        };
364        assert_eq!(got, want);
365    }
366
367    // Verify timestamps can roundtrip from string -> struct -> string without loss.
368    #[test_case("0001-01-01T00:00:00.123456789Z")]
369    #[test_case("0001-01-01T00:00:00.123456Z")]
370    #[test_case("0001-01-01T00:00:00.123Z")]
371    #[test_case("0001-01-01T00:00:00Z")]
372    #[test_case("1960-01-01T00:00:00.123456789Z")]
373    #[test_case("1960-01-01T00:00:00.123456Z")]
374    #[test_case("1960-01-01T00:00:00.123Z")]
375    #[test_case("1960-01-01T00:00:00Z")]
376    #[test_case("1970-01-01T00:00:00.123456789Z")]
377    #[test_case("1970-01-01T00:00:00.123456Z")]
378    #[test_case("1970-01-01T00:00:00.123Z")]
379    #[test_case("1970-01-01T00:00:00Z")]
380    #[test_case("9999-12-31T23:59:59.999999999Z")]
381    #[test_case("9999-12-31T23:59:59.123456789Z")]
382    #[test_case("9999-12-31T23:59:59.123456Z")]
383    #[test_case("9999-12-31T23:59:59.123Z")]
384    #[test_case("2024-10-19T12:34:56Z")]
385    #[test_case("2024-10-19T12:34:56.789Z")]
386    #[test_case("2024-10-19T12:34:56.789123456Z")]
387    fn roundtrip(input: &str) -> Result {
388        let json = serde_json::Value::String(input.to_string());
389        let timestamp = serde_json::from_value::<Timestamp>(json)?;
390        let roundtrip = serde_json::to_string(&timestamp)?;
391        assert_eq!(
392            format!("\"{input}\""),
393            roundtrip,
394            "mismatched value for input={input}"
395        );
396        Ok(())
397    }
398
399    // Verify timestamps work for some well know times, including fractional
400    // seconds.
401    #[test_case(
402        "0001-01-01T00:00:00.123456789Z",
403        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_789)
404    )]
405    #[test_case(
406        "0001-01-01T00:00:00.123456Z",
407        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_000)
408    )]
409    #[test_case(
410        "0001-01-01T00:00:00.123Z",
411        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_000_000)
412    )]
413    #[test_case("0001-01-01T00:00:00Z", Timestamp::clamp(Timestamp::MIN_SECONDS, 0))]
414    #[test_case("1970-01-01T00:00:00.123456789Z", Timestamp::clamp(0, 123_456_789))]
415    #[test_case("1970-01-01T00:00:00.123456Z", Timestamp::clamp(0, 123_456_000))]
416    #[test_case("1970-01-01T00:00:00.123Z", Timestamp::clamp(0, 123_000_000))]
417    #[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0))]
418    #[test_case(
419        "9999-12-31T23:59:59.123456789Z",
420        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_789)
421    )]
422    #[test_case(
423        "9999-12-31T23:59:59.123456Z",
424        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_000)
425    )]
426    #[test_case(
427        "9999-12-31T23:59:59.123Z",
428        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_000_000)
429    )]
430    #[test_case("9999-12-31T23:59:59Z", Timestamp::clamp(Timestamp::MAX_SECONDS, 0))]
431    fn well_known(input: &str, want: Timestamp) -> Result {
432        let json = serde_json::Value::String(input.to_string());
433        let got = serde_json::from_value::<Timestamp>(json)?;
434        assert_eq!(want, got);
435        Ok(())
436    }
437
438    #[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0); "zulu offset")]
439    #[test_case("1970-01-01T00:00:00+02:00", Timestamp::clamp(-2 * 60 * 60, 0); "2h positive")]
440    #[test_case("1970-01-01T00:00:00+02:45", Timestamp::clamp(-2 * 60 * 60 - 45 * 60, 0); "2h45m positive"
441	)]
442    #[test_case("1970-01-01T00:00:00-02:00", Timestamp::clamp(2 * 60 * 60, 0); "2h negative")]
443    #[test_case("1970-01-01T00:00:00-02:45", Timestamp::clamp(2 * 60 * 60 + 45 * 60, 0); "2h45m negative"
444	)]
445    fn deserialize_offsets(input: &str, want: Timestamp) -> Result {
446        let json = serde_json::Value::String(input.to_string());
447        let got = serde_json::from_value::<Timestamp>(json)?;
448        assert_eq!(want, got);
449        Ok(())
450    }
451
452    #[test_case("0000-01-01T00:00:00Z"; "below range")]
453    #[test_case("10000-01-01T00:00:00Z"; "above range")]
454    fn deserialize_out_of_range(input: &str) -> Result {
455        let value = serde_json::to_value(input)?;
456        let got = serde_json::from_value::<Timestamp>(value);
457        assert!(got.is_err());
458        Ok(())
459    }
460
461    #[test]
462    fn deserialize_unexpected_input_type() -> Result {
463        let got = serde_json::from_value::<Timestamp>(serde_json::json!({}));
464        assert!(got.is_err());
465        let msg = format!("{got:?}");
466        assert!(msg.contains("RFC 3339"), "message={}", msg);
467        Ok(())
468    }
469
470    #[serde_with::skip_serializing_none]
471    #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
472    #[serde(rename_all = "camelCase")]
473    struct Helper {
474        pub create_time: Option<Timestamp>,
475    }
476
477    #[test]
478    fn access() {
479        let ts = Timestamp::default();
480        assert_eq!(ts.nanos(), 0);
481        assert_eq!(ts.seconds(), 0);
482    }
483
484    #[test]
485    fn serialize_in_struct() -> Result {
486        let input = Helper {
487            ..Default::default()
488        };
489        let json = serde_json::to_value(input)?;
490        assert_eq!(json, json!({}));
491
492        let input = Helper {
493            create_time: Some(Timestamp::new(12, 345_678_900)?),
494        };
495
496        let json = serde_json::to_value(input)?;
497        assert_eq!(
498            json,
499            json!({ "createTime": "1970-01-01T00:00:12.3456789Z" })
500        );
501        Ok(())
502    }
503
504    #[test]
505    fn deserialize_in_struct() -> Result {
506        let input = json!({});
507        let want = Helper {
508            ..Default::default()
509        };
510        let got = serde_json::from_value::<Helper>(input)?;
511        assert_eq!(want, got);
512
513        let input = json!({ "createTime": "1970-01-01T00:00:12.3456789Z" });
514        let want = Helper {
515            create_time: Some(Timestamp::new(12, 345678900)?),
516        };
517        let got = serde_json::from_value::<Helper>(input)?;
518        assert_eq!(want, got);
519        Ok(())
520    }
521
522    #[test]
523    fn compare() -> Result {
524        let ts0 = Timestamp::default();
525        let ts1 = Timestamp::new(1, 100)?;
526        let ts2 = Timestamp::new(1, 200)?;
527        let ts3 = Timestamp::new(2, 0)?;
528        assert_eq!(ts0.partial_cmp(&ts0), Some(std::cmp::Ordering::Equal));
529        assert_eq!(ts0.partial_cmp(&ts1), Some(std::cmp::Ordering::Less));
530        assert_eq!(ts2.partial_cmp(&ts3), Some(std::cmp::Ordering::Less));
531        Ok(())
532    }
533
534    #[test]
535    fn convert_from_time() -> Result {
536        let ts = time::OffsetDateTime::from_unix_timestamp(123)?
537            + time::Duration::nanoseconds(456789012);
538        let got = Timestamp::try_from(ts)?;
539        let want = Timestamp::new(123, 456789012)?;
540        assert_eq!(got, want);
541        Ok(())
542    }
543
544    #[test]
545    fn convert_to_time() -> Result {
546        let ts = Timestamp::new(123, 456789012)?;
547        let got = time::OffsetDateTime::try_from(ts)?;
548        let want = time::OffsetDateTime::from_unix_timestamp(123)?
549            + time::Duration::nanoseconds(456789012);
550        assert_eq!(got, want);
551        Ok(())
552    }
553
554    #[test]
555    fn convert_from_chrono_time() -> Result {
556        let ts = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
557        let got = Timestamp::try_from(ts)?;
558        let want = Timestamp::new(123, 456789012)?;
559        assert_eq!(got, want);
560        Ok(())
561    }
562
563    #[test]
564    fn convert_to_chrono_time() -> Result {
565        let ts = Timestamp::new(123, 456789012)?;
566        let got = chrono::DateTime::try_from(ts)?;
567        let want = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
568        assert_eq!(got, want);
569        Ok(())
570    }
571}