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/// # Examples
18/// ```
19/// # use google_cloud_wkt::{Timestamp, TimestampError};
20/// let ts = Timestamp::try_from("2025-05-16T09:46:12.500Z")?;
21/// assert_eq!(ts.seconds(), 1747388772);
22/// assert_eq!(ts.nanos(), 500_000_000);
23///
24/// assert_eq!(ts, Timestamp::new(1747388772, 500_000_000)?);
25/// assert_eq!(ts, Timestamp::clamp(1747388772, 500_000_000));
26/// # Ok::<(), TimestampError>(())
27/// ```
28///
29/// A Timestamp represents a point in time independent of any time zone or local
30/// calendar, encoded as a count of seconds and fractions of seconds at
31/// nanosecond resolution. The count is relative to an epoch at UTC midnight on
32/// January 1, 1970, in the proleptic Gregorian calendar which extends the
33/// Gregorian calendar backwards to year one.
34///
35/// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
36/// second table is needed for interpretation, using a [24-hour linear
37/// smear](https://developers.google.com/time/smear).
38///
39/// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
40/// restricting to that range, we ensure that we can convert to and from [RFC
41/// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
42///
43/// # JSON Mapping
44///
45/// In JSON format, the Timestamp type is encoded as a string in the
46/// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
47/// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
48/// where {year} is always expressed using four digits while {month}, {day},
49/// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
50/// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
51/// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
52/// is required.
53///
54/// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
55/// 01:30 UTC on January 15, 2017.
56///
57#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
58#[non_exhaustive]
59pub struct Timestamp {
60    /// Represents seconds of UTC time since Unix epoch
61    /// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
62    /// 9999-12-31T23:59:59Z inclusive.
63    seconds: i64,
64
65    /// Non-negative fractions of a second at nanosecond resolution. Negative
66    /// second values with fractions must still have non-negative nanos values
67    /// that count forward in time. Must be from 0 to 999,999,999
68    /// inclusive.
69    nanos: i32,
70}
71
72/// Represent failures in converting or creating [Timestamp] instances.
73///
74/// Examples
75/// ```
76/// # use google_cloud_wkt::{Timestamp, TimestampError};
77/// let ts = Timestamp::new(Timestamp::MAX_SECONDS + 2, 0);
78/// assert!(matches!(ts, Err(TimestampError::OutOfRange)));
79///
80/// let ts = Timestamp::new(0, 1_500_000_000);
81/// assert!(matches!(ts, Err(TimestampError::OutOfRange)));
82///
83/// let ts = Timestamp::try_from("invalid");
84/// assert!(matches!(ts, Err(TimestampError::Deserialize(_))));
85/// ```
86#[derive(thiserror::Error, Debug)]
87#[non_exhaustive]
88pub enum TimestampError {
89    /// One of the components (seconds and/or nanoseconds) was out of range.
90    #[error("seconds and/or nanoseconds out of range")]
91    OutOfRange,
92
93    /// There was a problem deserializing a timestamp.
94    #[error("cannot deserialize timestamp, source={0}")]
95    Deserialize(#[source] BoxedError),
96}
97
98type BoxedError = Box<dyn std::error::Error + Send + Sync>;
99type Error = TimestampError;
100
101impl Timestamp {
102    const NS: i32 = 1_000_000_000;
103
104    // Obtained via: `date +%s --date='0001-01-01T00:00:00Z'`
105    /// The minimum value for the `seconds` component. Corresponds to '0001-01-01T00:00:00Z'.
106    pub const MIN_SECONDS: i64 = -62135596800;
107
108    // Obtained via: `date +%s --date='9999-12-31T23:59:59Z'`
109    /// The maximum value for the `seconds` component. Corresponds to '9999-12-31T23:59:59Z'.
110    pub const MAX_SECONDS: i64 = 253402300799;
111
112    /// The minimum value for the `nanos` component.
113    pub const MIN_NANOS: i32 = 0;
114
115    /// The maximum value for the `nanos` component.
116    pub const MAX_NANOS: i32 = Self::NS - 1;
117
118    /// Creates a new [Timestamp] from the seconds and nanoseconds.
119    ///
120    /// If either value is out of range it returns an error.
121    ///
122    /// # Examples
123    /// ```
124    /// # use google_cloud_wkt::{Timestamp, TimestampError};
125    /// let ts = Timestamp::new(1747388772, 0)?;
126    /// assert_eq!(String::from(ts), "2025-05-16T09:46:12Z");
127    ///
128    /// let ts = Timestamp::new(1747388772, 2_000_000_000);
129    /// assert!(matches!(ts, Err(TimestampError::OutOfRange)));
130    /// # Ok::<(), TimestampError>(())
131    /// ```
132    ///
133    /// # Parameters
134    ///
135    /// * `seconds` - the seconds on the timestamp.
136    /// * `nanos` - the nanoseconds on the timestamp.
137    pub fn new(seconds: i64, nanos: i32) -> Result<Self, Error> {
138        if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
139            return Err(Error::OutOfRange);
140        }
141        if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
142            return Err(Error::OutOfRange);
143        }
144        Ok(Self { seconds, nanos })
145    }
146
147    /// Create a normalized, clamped [Timestamp].
148    ///
149    /// # Examples
150    /// ```
151    /// # use google_cloud_wkt::{Timestamp, TimestampError};
152    /// let ts = Timestamp::clamp(1747388772, 0);
153    /// assert_eq!(String::from(ts), "2025-05-16T09:46:12Z");
154    ///
155    /// let ts = Timestamp::clamp(1747388772, 2_000_000_000);
156    /// // extra nanoseconds are carried as seconds
157    /// assert_eq!(String::from(ts), "2025-05-16T09:46:14Z");
158    /// # Ok::<(), TimestampError>(())
159    /// ```
160    ///
161    /// Timestamps must be between 0001-01-01T00:00:00Z and
162    /// 9999-12-31T23:59:59.999999999Z, and the nanoseconds component must
163    /// always be in the range [0, 999_999_999]. This function creates a
164    /// new [Timestamp] instance clamped to those ranges.
165    ///
166    /// The function effectively adds the nanoseconds part (with carry) to the
167    /// seconds part, with saturation.
168    ///
169    /// # Parameters
170    ///
171    /// * `seconds` - the seconds on the timestamp.
172    /// * `nanos` - the nanoseconds added to the seconds.
173    pub fn clamp(seconds: i64, nanos: i32) -> Self {
174        let (seconds, nanos) = match nanos.cmp(&0_i32) {
175            std::cmp::Ordering::Equal => (seconds, nanos),
176            std::cmp::Ordering::Greater => (
177                seconds.saturating_add((nanos / Self::NS) as i64),
178                nanos % Self::NS,
179            ),
180            std::cmp::Ordering::Less => (
181                seconds.saturating_sub(1 - (nanos / Self::NS) as i64),
182                Self::NS + nanos % Self::NS,
183            ),
184        };
185        if seconds < Self::MIN_SECONDS {
186            return Self {
187                seconds: Self::MIN_SECONDS,
188                nanos: 0,
189            };
190        } else if seconds > Self::MAX_SECONDS {
191            return Self {
192                seconds: Self::MAX_SECONDS,
193                nanos: 0,
194            };
195        }
196        Self { seconds, nanos }
197    }
198
199    /// Represents seconds of UTC time since Unix epoch (1970-01-01T00:00:00Z).
200    ///
201    /// Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.
202    ///
203    /// # Examples
204    /// ```
205    /// # use google_cloud_wkt::{Timestamp, TimestampError};
206    /// let ts = Timestamp::new(120, 500_000_000)?;
207    /// assert_eq!(ts.seconds(), 120);
208    /// # Ok::<(), TimestampError>(())
209    /// ```
210    pub fn seconds(&self) -> i64 {
211        self.seconds
212    }
213
214    /// Non-negative fractions of a second at nanosecond resolution.
215    ///
216    /// Negative second values (before the Unix epoch) with fractions must still
217    /// have non-negative nanos values that count forward in time. Must be from
218    /// 0 to 999,999,999 inclusive.
219    ///
220    /// # Examples
221    /// ```
222    /// # use google_cloud_wkt::{Timestamp, TimestampError};
223    /// let ts = Timestamp::new(120, 500_000_000)?;
224    /// assert_eq!(ts.nanos(), 500_000_000);
225    /// # Ok::<(), TimestampError>(())
226    /// ```
227    pub fn nanos(&self) -> i32 {
228        self.nanos
229    }
230}
231
232impl crate::message::Message for Timestamp {
233    fn typename() -> &'static str {
234        "type.googleapis.com/google.protobuf.Timestamp"
235    }
236
237    #[allow(private_interfaces)]
238    fn serializer() -> impl crate::message::MessageSerializer<Self> {
239        crate::message::ValueSerializer::<Self>::new()
240    }
241}
242
243const NS: i128 = 1_000_000_000;
244
245/// Implement [`serde`](::serde) serialization for timestamps.
246#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
247impl serde::ser::Serialize for Timestamp {
248    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
249    where
250        S: serde::ser::Serializer,
251    {
252        String::from(*self).serialize(serializer)
253    }
254}
255
256struct TimestampVisitor;
257
258impl serde::de::Visitor<'_> for TimestampVisitor {
259    type Value = Timestamp;
260
261    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
262        formatter.write_str("a string with a timestamp in RFC 3339 format")
263    }
264
265    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
266    where
267        E: serde::de::Error,
268    {
269        Timestamp::try_from(value).map_err(E::custom)
270    }
271}
272
273/// Implement [`serde`](::serde) deserialization for timestamps.
274#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
275impl<'de> serde::de::Deserialize<'de> for Timestamp {
276    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
277    where
278        D: serde::Deserializer<'de>,
279    {
280        deserializer.deserialize_str(TimestampVisitor)
281    }
282}
283
284/// Convert from [time::OffsetDateTime] to [Timestamp].
285///
286/// This conversion may fail if the [time::OffsetDateTime] value is out of range.
287///
288/// # Examples
289/// ```
290/// # use google_cloud_wkt::Timestamp;
291/// use time::{macros::datetime, OffsetDateTime};
292/// let dt = datetime!(2025-05-16 09:46:12 UTC);
293/// let ts = Timestamp::try_from(dt)?;
294/// assert_eq!(String::from(ts), "2025-05-16T09:46:12Z");
295/// # Ok::<(), anyhow::Error>(())
296/// ```
297#[cfg(feature = "time")]
298#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
299impl TryFrom<time::OffsetDateTime> for Timestamp {
300    type Error = TimestampError;
301
302    fn try_from(value: time::OffsetDateTime) -> Result<Self, Self::Error> {
303        let seconds = value.unix_timestamp();
304        let nanos = (value.unix_timestamp_nanos() - seconds as i128 * NS) as i32;
305        Self::new(seconds, nanos)
306    }
307}
308
309/// Convert from [Timestamp] to [OffsetDateTime][time::OffsetDateTime]
310///
311/// This conversion may fail if the [Timestamp] value is out of range.
312///
313/// # Examples
314/// ```
315/// # use google_cloud_wkt::Timestamp;
316/// use time::{macros::datetime, OffsetDateTime};
317/// let ts = Timestamp::try_from("2025-05-16T09:46:12Z")?;
318/// let dt = OffsetDateTime::try_from(ts)?;
319/// assert_eq!(dt, datetime!(2025-05-16 09:46:12 UTC));
320/// # Ok::<(), anyhow::Error>(())
321/// ```
322#[cfg(feature = "time")]
323#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
324impl TryFrom<Timestamp> for time::OffsetDateTime {
325    type Error = time::error::ComponentRange;
326    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
327        let ts = time::OffsetDateTime::from_unix_timestamp(value.seconds())?;
328        Ok(ts + time::Duration::nanoseconds(value.nanos() as i64))
329    }
330}
331
332const EXPECT_OFFSET_DATE_TIME_CONVERTS: &str = concat!(
333    "converting Timestamp to time::OffsetDateTime should always succeed. ",
334    "The Timestamp values are always in range. ",
335    "If this is not the case, please file a bug at https://github.com/googleapis/google-cloud-rust/issues"
336);
337const EXPECT_TIMESTAMP_FORMAT_SUCCEEDS: &str = concat!(
338    "formatting a Timestamp using RFC-3339 should always succeed. ",
339    "The Timestamp values are always in range, and we use a well-known constant for the format specifier. ",
340    "If this is not the case, please file a bug at https://github.com/googleapis/google-cloud-rust/issues"
341);
342use time::format_description::well_known::Rfc3339;
343
344/// Converts a [Timestamp] to its [String] representation.
345///
346/// # Example
347/// ```
348/// # use google_cloud_wkt::{Timestamp, TimestampError};
349/// let ts = Timestamp::new(1747388772, 0)?;
350/// assert_eq!(String::from(ts), "2025-05-16T09:46:12Z");
351/// # Ok::<(), anyhow::Error>(())
352/// ```
353impl From<Timestamp> for String {
354    fn from(timestamp: Timestamp) -> Self {
355        let ts = time::OffsetDateTime::from_unix_timestamp_nanos(
356            timestamp.seconds as i128 * NS + timestamp.nanos as i128,
357        )
358        .expect(EXPECT_OFFSET_DATE_TIME_CONVERTS);
359        ts.format(&Rfc3339).expect(EXPECT_TIMESTAMP_FORMAT_SUCCEEDS)
360    }
361}
362
363/// Converts the string representation of a timestamp to [Timestamp].
364///
365/// # Example
366/// ```
367/// # use google_cloud_wkt::{Timestamp, TimestampError};
368/// let ts = Timestamp::try_from("2025-05-16T09:46:12.500Z")?;
369/// assert_eq!(ts.seconds(), 1747388772);
370/// assert_eq!(ts.nanos(), 500_000_000);
371/// # Ok::<(), anyhow::Error>(())
372/// ```
373impl TryFrom<&str> for Timestamp {
374    type Error = TimestampError;
375    fn try_from(value: &str) -> Result<Self, Self::Error> {
376        let odt = time::OffsetDateTime::parse(value, &Rfc3339)
377            .map_err(|e| TimestampError::Deserialize(e.into()))?;
378        let nanos_since_epoch = odt.unix_timestamp_nanos();
379        let seconds = (nanos_since_epoch / NS) as i64;
380        let nanos = (nanos_since_epoch % NS) as i32;
381        if nanos < 0 {
382            return Timestamp::new(seconds - 1, Self::NS + nanos);
383        }
384        Timestamp::new(seconds, nanos)
385    }
386}
387
388/// Converts the string representation of a timestamp to [Timestamp].
389///
390/// # Example
391/// ```
392/// # use google_cloud_wkt::{Timestamp, TimestampError};
393/// let s = "2025-05-16T09:46:12.500Z".to_string();
394/// let ts = Timestamp::try_from(&s)?;
395/// assert_eq!(ts.seconds(), 1747388772);
396/// assert_eq!(ts.nanos(), 500_000_000);
397/// # Ok::<(), anyhow::Error>(())
398/// ```
399impl TryFrom<&String> for Timestamp {
400    type Error = TimestampError;
401    fn try_from(value: &String) -> Result<Self, Self::Error> {
402        Timestamp::try_from(value.as_str())
403    }
404}
405
406/// Converts from [std::time::SystemTime] to [Timestamp].
407///
408/// This conversion may fail if the [std::time::SystemTime] value is out of
409/// range.
410///
411/// # Example
412/// ```
413/// # use std::time::SystemTime;
414/// # use google_cloud_wkt::{Timestamp, TimestampError};
415/// let ts = Timestamp::try_from(SystemTime::now())?;
416/// println!("now={ts:?}");
417/// # Ok::<(), anyhow::Error>(())
418/// ```
419impl TryFrom<std::time::SystemTime> for Timestamp {
420    type Error = TimestampError;
421    fn try_from(value: std::time::SystemTime) -> Result<Self, Self::Error> {
422        match value.duration_since(std::time::SystemTime::UNIX_EPOCH) {
423            Ok(d) => {
424                let s = d.as_secs();
425                if s > i64::MAX as u64 {
426                    return Err(TimestampError::OutOfRange);
427                }
428                Timestamp::new(s as i64, d.subsec_nanos() as i32)
429            }
430            Err(e) => {
431                // The error case essentially gives us a negative duration.
432                let d = e.duration();
433                let s = d.as_secs();
434                // We might add 1 to s later, but recall that `-i64::MIN == i64::MAX + 1`
435                if s > i64::MAX as u64 {
436                    return Err(TimestampError::OutOfRange);
437                }
438                let seconds = -(s as i64);
439                let nanos = d.subsec_nanos() as i32;
440                if nanos > 0 {
441                    return Timestamp::new(seconds - 1, Self::NS - nanos);
442                }
443                Timestamp::new(seconds, 0)
444            }
445        }
446    }
447}
448
449/// Converts from [chrono::DateTime] to [Timestamp].
450///
451/// This conversion may fail if the [chrono::DateTime] value is out of range.
452///
453/// # Example
454/// ```
455/// # use google_cloud_wkt::{Timestamp, TimestampError};
456/// use chrono::{DateTime, TimeZone, Utc};
457/// let date : DateTime<Utc> = Utc.with_ymd_and_hms(2025, 5, 16, 10, 15, 00).unwrap();
458/// let ts = Timestamp::try_from(date)?;
459/// assert_eq!(String::from(ts), "2025-05-16T10:15:00Z");
460/// # Ok::<(), anyhow::Error>(())
461/// ```
462#[cfg(feature = "chrono")]
463#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
464impl TryFrom<chrono::DateTime<chrono::Utc>> for Timestamp {
465    type Error = TimestampError;
466
467    fn try_from(value: chrono::DateTime<chrono::Utc>) -> Result<Self, Self::Error> {
468        assert!(value.timestamp_subsec_nanos() <= (i32::MAX as u32));
469        Timestamp::new(value.timestamp(), value.timestamp_subsec_nanos() as i32)
470    }
471}
472
473/// Converts from [Timestamp] to [chrono::DateTime].
474///
475/// # Example
476/// ```
477/// # use google_cloud_wkt::{Timestamp, TimestampError};
478/// use chrono::{DateTime, TimeZone, Utc};
479/// let ts = Timestamp::try_from("2025-05-16T10:15:00Z")?;
480/// let date = DateTime::try_from(ts)?;
481/// # Ok::<(), anyhow::Error>(())
482/// ```
483#[cfg(feature = "chrono")]
484#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
485impl TryFrom<Timestamp> for chrono::DateTime<chrono::Utc> {
486    type Error = TimestampError;
487    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
488        let ts = chrono::DateTime::from_timestamp(value.seconds, 0).unwrap();
489        Ok(ts + chrono::Duration::nanoseconds(value.nanos as i64))
490    }
491}
492
493/// Converts from [Timestamp] to [std::time::SystemTime].
494///
495/// This conversion may fail if the [std::time::SystemTime] value is out of
496/// range.
497///
498/// # Example
499/// ```
500/// # use std::time::SystemTime;
501/// # use google_cloud_wkt::{Timestamp, TimestampError};
502/// let ts = Timestamp::new(0, 0)?;
503/// let epoch = SystemTime::try_from(ts)?;
504/// assert_eq!(epoch, SystemTime::UNIX_EPOCH);
505/// # Ok::<(), anyhow::Error>(())
506/// ```
507impl TryFrom<Timestamp> for std::time::SystemTime {
508    type Error = TimestampError;
509    fn try_from(value: Timestamp) -> Result<std::time::SystemTime, Self::Error> {
510        let s = value.seconds();
511        let ts = if s >= 0 {
512            let d = std::time::Duration::from_secs(s as u64);
513            std::time::SystemTime::UNIX_EPOCH
514                .checked_add(d)
515                .ok_or_else(|| TimestampError::OutOfRange)?
516        } else {
517            let d = std::time::Duration::from_secs(-s as u64);
518            std::time::SystemTime::UNIX_EPOCH
519                .checked_sub(d)
520                .ok_or_else(|| TimestampError::OutOfRange)?
521        };
522        let d = std::time::Duration::from_nanos(value.nanos() as u64);
523        ts.checked_add(d).ok_or_else(|| TimestampError::OutOfRange)
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530    use serde_json::json;
531    use test_case::test_case;
532    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
533
534    // Verify the epoch converts as expected.
535    #[test]
536    fn unix_epoch() -> Result {
537        let proto = Timestamp::default();
538        let json = serde_json::to_value(proto)?;
539        let expected = json!("1970-01-01T00:00:00Z");
540        assert_eq!(json, expected);
541        let roundtrip = serde_json::from_value::<Timestamp>(json)?;
542        assert_eq!(proto, roundtrip);
543        Ok(())
544    }
545
546    fn get_seconds(input: &str) -> i64 {
547        let odt = time::OffsetDateTime::parse(input, &Rfc3339);
548        let odt = odt.unwrap();
549        odt.unix_timestamp()
550    }
551
552    fn get_min_seconds() -> i64 {
553        self::get_seconds("0001-01-01T00:00:00Z")
554    }
555
556    fn get_max_seconds() -> i64 {
557        self::get_seconds("9999-12-31T23:59:59Z")
558    }
559
560    #[test_case(get_min_seconds() - 1, 0; "seconds below range")]
561    #[test_case(get_max_seconds() + 1, 0; "seconds above range")]
562    #[test_case(0, -1; "nanos below range")]
563    #[test_case(0, 1_000_000_000; "nanos above range")]
564    fn new_out_of_range(seconds: i64, nanos: i32) -> Result {
565        let t = Timestamp::new(seconds, nanos);
566        assert!(matches!(t, Err(Error::OutOfRange)), "{t:?}");
567        Ok(())
568    }
569
570    #[test_case(0, 0, 0, 0; "zero")]
571    #[test_case(0, 1_234_567_890, 1, 234_567_890; "nanos overflow")]
572    #[test_case(0, 2_100_000_123, 2, 100_000_123; "nanos overflow x2")]
573    #[test_case(0, -1_400_000_000, -2, 600_000_000; "nanos underflow")]
574    #[test_case(0, -2_100_000_000, -3, 900_000_000; "nanos underflow x2")]
575    #[test_case(self::get_max_seconds() + 1, 0, get_max_seconds(), 0; "seconds over range")]
576    #[test_case(self::get_min_seconds() - 1, 0, get_min_seconds(), 0; "seconds below range")]
577    #[test_case(self::get_max_seconds() - 1, 2_000_000_001, get_max_seconds(), 0; "nanos overflow range"
578	)]
579    #[test_case(self::get_min_seconds() + 1, -1_500_000_000, get_min_seconds(), 0; "nanos underflow range"
580	)]
581    fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) {
582        let got = Timestamp::clamp(seconds, nanos);
583        let want = Timestamp {
584            seconds: want_seconds,
585            nanos: want_nanos,
586        };
587        assert_eq!(got, want);
588    }
589
590    // Verify timestamps can roundtrip from string -> struct -> string without loss.
591    #[test_case("0001-01-01T00:00:00.123456789Z")]
592    #[test_case("0001-01-01T00:00:00.123456Z")]
593    #[test_case("0001-01-01T00:00:00.123Z")]
594    #[test_case("0001-01-01T00:00:00Z")]
595    #[test_case("1960-01-01T00:00:00.123456789Z")]
596    #[test_case("1960-01-01T00:00:00.123456Z")]
597    #[test_case("1960-01-01T00:00:00.123Z")]
598    #[test_case("1960-01-01T00:00:00Z")]
599    #[test_case("1970-01-01T00:00:00.123456789Z")]
600    #[test_case("1970-01-01T00:00:00.123456Z")]
601    #[test_case("1970-01-01T00:00:00.123Z")]
602    #[test_case("1970-01-01T00:00:00Z")]
603    #[test_case("9999-12-31T23:59:59.999999999Z")]
604    #[test_case("9999-12-31T23:59:59.123456789Z")]
605    #[test_case("9999-12-31T23:59:59.123456Z")]
606    #[test_case("9999-12-31T23:59:59.123Z")]
607    #[test_case("2024-10-19T12:34:56Z")]
608    #[test_case("2024-10-19T12:34:56.789Z")]
609    #[test_case("2024-10-19T12:34:56.789123456Z")]
610    fn roundtrip(input: &str) -> Result {
611        let json = serde_json::Value::String(input.to_string());
612        let timestamp = serde_json::from_value::<Timestamp>(json)?;
613        let roundtrip = serde_json::to_string(&timestamp)?;
614        assert_eq!(
615            format!("\"{input}\""),
616            roundtrip,
617            "mismatched value for input={input}"
618        );
619        Ok(())
620    }
621
622    // Verify timestamps work for some well know times, including fractional
623    // seconds.
624    #[test_case(
625        "0001-01-01T00:00:00.123456789Z",
626        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_789)
627    )]
628    #[test_case(
629        "0001-01-01T00:00:00.123456Z",
630        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_000)
631    )]
632    #[test_case(
633        "0001-01-01T00:00:00.123Z",
634        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_000_000)
635    )]
636    #[test_case("0001-01-01T00:00:00Z", Timestamp::clamp(Timestamp::MIN_SECONDS, 0))]
637    #[test_case("1970-01-01T00:00:00.123456789Z", Timestamp::clamp(0, 123_456_789))]
638    #[test_case("1970-01-01T00:00:00.123456Z", Timestamp::clamp(0, 123_456_000))]
639    #[test_case("1970-01-01T00:00:00.123Z", Timestamp::clamp(0, 123_000_000))]
640    #[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0))]
641    #[test_case(
642        "9999-12-31T23:59:59.123456789Z",
643        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_789)
644    )]
645    #[test_case(
646        "9999-12-31T23:59:59.123456Z",
647        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_000)
648    )]
649    #[test_case(
650        "9999-12-31T23:59:59.123Z",
651        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_000_000)
652    )]
653    #[test_case("9999-12-31T23:59:59Z", Timestamp::clamp(Timestamp::MAX_SECONDS, 0))]
654    fn well_known(input: &str, want: Timestamp) -> Result {
655        let json = serde_json::Value::String(input.to_string());
656        let got = serde_json::from_value::<Timestamp>(json)?;
657        assert_eq!(want, got);
658        Ok(())
659    }
660
661    #[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0); "zulu offset")]
662    #[test_case("1970-01-01T00:00:00+02:00", Timestamp::clamp(-2 * 60 * 60, 0); "2h positive")]
663    #[test_case("1970-01-01T00:00:00+02:45", Timestamp::clamp(-2 * 60 * 60 - 45 * 60, 0); "2h45m positive"
664	)]
665    #[test_case("1970-01-01T00:00:00-02:00", Timestamp::clamp(2 * 60 * 60, 0); "2h negative")]
666    #[test_case("1970-01-01T00:00:00-02:45", Timestamp::clamp(2 * 60 * 60 + 45 * 60, 0); "2h45m negative"
667	)]
668    fn deserialize_offsets(input: &str, want: Timestamp) -> Result {
669        let json = serde_json::Value::String(input.to_string());
670        let got = serde_json::from_value::<Timestamp>(json)?;
671        assert_eq!(want, got);
672        Ok(())
673    }
674
675    #[test_case("0000-01-01T00:00:00Z"; "below range")]
676    #[test_case("10000-01-01T00:00:00Z"; "above range")]
677    fn deserialize_out_of_range(input: &str) -> Result {
678        let value = serde_json::to_value(input)?;
679        let got = serde_json::from_value::<Timestamp>(value);
680        assert!(got.is_err());
681        Ok(())
682    }
683
684    #[test]
685    fn deserialize_unexpected_input_type() -> Result {
686        let got = serde_json::from_value::<Timestamp>(serde_json::json!({}));
687        assert!(got.is_err());
688        let msg = format!("{got:?}");
689        assert!(msg.contains("RFC 3339"), "message={msg}");
690        Ok(())
691    }
692
693    #[serde_with::skip_serializing_none]
694    #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
695    #[serde(rename_all = "camelCase")]
696    struct Helper {
697        pub create_time: Option<Timestamp>,
698    }
699
700    #[test]
701    fn access() {
702        let ts = Timestamp::default();
703        assert_eq!(ts.nanos(), 0);
704        assert_eq!(ts.seconds(), 0);
705    }
706
707    #[test]
708    fn serialize_in_struct() -> Result {
709        let input = Helper {
710            ..Default::default()
711        };
712        let json = serde_json::to_value(input)?;
713        assert_eq!(json, json!({}));
714
715        let input = Helper {
716            create_time: Some(Timestamp::new(12, 345_678_900)?),
717        };
718
719        let json = serde_json::to_value(input)?;
720        assert_eq!(
721            json,
722            json!({ "createTime": "1970-01-01T00:00:12.3456789Z" })
723        );
724        Ok(())
725    }
726
727    #[test]
728    fn deserialize_in_struct() -> Result {
729        let input = json!({});
730        let want = Helper {
731            ..Default::default()
732        };
733        let got = serde_json::from_value::<Helper>(input)?;
734        assert_eq!(want, got);
735
736        let input = json!({ "createTime": "1970-01-01T00:00:12.3456789Z" });
737        let want = Helper {
738            create_time: Some(Timestamp::new(12, 345678900)?),
739        };
740        let got = serde_json::from_value::<Helper>(input)?;
741        assert_eq!(want, got);
742        Ok(())
743    }
744
745    #[test]
746    fn compare() -> Result {
747        let ts0 = Timestamp::default();
748        let ts1 = Timestamp::new(1, 100)?;
749        let ts2 = Timestamp::new(1, 200)?;
750        let ts3 = Timestamp::new(2, 0)?;
751        assert_eq!(ts0.partial_cmp(&ts0), Some(std::cmp::Ordering::Equal));
752        assert_eq!(ts0.partial_cmp(&ts1), Some(std::cmp::Ordering::Less));
753        assert_eq!(ts2.partial_cmp(&ts3), Some(std::cmp::Ordering::Less));
754        Ok(())
755    }
756
757    #[test]
758    fn convert_from_string() -> Result {
759        let input = "2025-05-16T18:00:00Z".to_string();
760        let a = Timestamp::try_from(input.as_str())?;
761        let b = Timestamp::try_from(&input)?;
762        assert_eq!(a, b);
763        Ok(())
764    }
765
766    #[test]
767    fn convert_from_time() -> Result {
768        let ts = time::OffsetDateTime::from_unix_timestamp(123)?
769            + time::Duration::nanoseconds(456789012);
770        let got = Timestamp::try_from(ts)?;
771        let want = Timestamp::new(123, 456789012)?;
772        assert_eq!(got, want);
773        Ok(())
774    }
775
776    #[test]
777    fn convert_to_time() -> Result {
778        let ts = Timestamp::new(123, 456789012)?;
779        let got = time::OffsetDateTime::try_from(ts)?;
780        let want = time::OffsetDateTime::from_unix_timestamp(123)?
781            + time::Duration::nanoseconds(456789012);
782        assert_eq!(got, want);
783        Ok(())
784    }
785
786    #[test]
787    fn convert_from_chrono_time() -> Result {
788        let ts = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
789        let got = Timestamp::try_from(ts)?;
790        let want = Timestamp::new(123, 456789012)?;
791        assert_eq!(got, want);
792        Ok(())
793    }
794
795    #[test]
796    fn convert_to_chrono_time() -> Result {
797        let ts = Timestamp::new(123, 456789012)?;
798        let got = chrono::DateTime::try_from(ts)?;
799        let want = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
800        assert_eq!(got, want);
801        Ok(())
802    }
803
804    #[test]
805    fn convert_from_std_time() -> Result {
806        let now = std::time::SystemTime::now();
807        let want = now.duration_since(std::time::SystemTime::UNIX_EPOCH)?;
808
809        let wkt = Timestamp::try_from(now)?;
810        assert_eq!(wkt.seconds(), want.as_secs() as i64);
811        assert_eq!(wkt.nanos(), want.subsec_nanos() as i32);
812
813        let past = std::time::SystemTime::UNIX_EPOCH
814            .checked_sub(std::time::Duration::from_secs(123))
815            .unwrap()
816            .checked_sub(std::time::Duration::from_nanos(100))
817            .unwrap();
818        let wkt = Timestamp::try_from(past)?;
819        assert_eq!(wkt.seconds(), -124);
820        assert_eq!(wkt.nanos(), Timestamp::NS - 100);
821        Ok(())
822    }
823
824    #[test]
825    fn convert_to_std_time() -> Result {
826        let ts = Timestamp::clamp(123, 456789000);
827        let got = std::time::SystemTime::try_from(ts)?;
828        let want = std::time::SystemTime::UNIX_EPOCH
829            .checked_add(std::time::Duration::from_secs(123))
830            .unwrap()
831            .checked_add(std::time::Duration::from_nanos(456789000))
832            .unwrap();
833        assert_eq!(got, want);
834
835        let ts = Timestamp::clamp(-123, -456789000);
836        let got = std::time::SystemTime::try_from(ts)?;
837        let want = std::time::SystemTime::UNIX_EPOCH
838            .checked_sub(std::time::Duration::from_secs(123))
839            .unwrap()
840            .checked_sub(std::time::Duration::from_nanos(456789000))
841            .unwrap();
842        assert_eq!(got, want);
843        Ok(())
844    }
845}