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        use time::convert::{Nanosecond, Second};
304
305        let seconds = value.unix_timestamp();
306        let nanos = (value.unix_timestamp_nanos()
307            - seconds as i128 * Nanosecond::per(Second) as i128) as i32;
308        Self::new(seconds, nanos)
309    }
310}
311
312/// Convert from [Timestamp] to [OffsetDateTime][time::OffsetDateTime]
313///
314/// This conversion may fail if the [Timestamp] value is out of range.
315///
316/// # Examples
317/// ```
318/// # use google_cloud_wkt::Timestamp;
319/// use time::{macros::datetime, OffsetDateTime};
320/// let ts = Timestamp::try_from("2025-05-16T09:46:12Z")?;
321/// let dt = OffsetDateTime::try_from(ts)?;
322/// assert_eq!(dt, datetime!(2025-05-16 09:46:12 UTC));
323/// # Ok::<(), anyhow::Error>(())
324/// ```
325#[cfg(feature = "time")]
326#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
327impl TryFrom<Timestamp> for time::OffsetDateTime {
328    type Error = time::error::ComponentRange;
329    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
330        let ts = time::OffsetDateTime::from_unix_timestamp(value.seconds())?;
331        Ok(ts + time::Duration::nanoseconds(value.nanos() as i64))
332    }
333}
334
335const EXPECT_OFFSET_DATE_TIME_CONVERTS: &str = concat!(
336    "converting Timestamp to time::OffsetDateTime should always succeed. ",
337    "The Timestamp values are always in range. ",
338    "If this is not the case, please file a bug at https://github.com/googleapis/google-cloud-rust/issues"
339);
340const EXPECT_TIMESTAMP_FORMAT_SUCCEEDS: &str = concat!(
341    "formatting a Timestamp using RFC-3339 should always succeed. ",
342    "The Timestamp values are always in range, and we use a well-known constant for the format specifier. ",
343    "If this is not the case, please file a bug at https://github.com/googleapis/google-cloud-rust/issues"
344);
345use time::format_description::well_known::Rfc3339;
346
347/// Converts a [Timestamp] to its [String] representation.
348///
349/// # Example
350/// ```
351/// # use google_cloud_wkt::{Timestamp, TimestampError};
352/// let ts = Timestamp::new(1747388772, 0)?;
353/// assert_eq!(String::from(ts), "2025-05-16T09:46:12Z");
354/// # Ok::<(), anyhow::Error>(())
355/// ```
356impl From<Timestamp> for String {
357    fn from(timestamp: Timestamp) -> Self {
358        let ts = time::OffsetDateTime::from_unix_timestamp_nanos(
359            timestamp.seconds as i128 * NS + timestamp.nanos as i128,
360        )
361        .expect(EXPECT_OFFSET_DATE_TIME_CONVERTS);
362        ts.format(&Rfc3339).expect(EXPECT_TIMESTAMP_FORMAT_SUCCEEDS)
363    }
364}
365
366/// Converts the string representation of a timestamp to [Timestamp].
367///
368/// # Example
369/// ```
370/// # use google_cloud_wkt::{Timestamp, TimestampError};
371/// let ts = Timestamp::try_from("2025-05-16T09:46:12.500Z")?;
372/// assert_eq!(ts.seconds(), 1747388772);
373/// assert_eq!(ts.nanos(), 500_000_000);
374/// # Ok::<(), anyhow::Error>(())
375/// ```
376impl TryFrom<&str> for Timestamp {
377    type Error = TimestampError;
378    fn try_from(value: &str) -> Result<Self, Self::Error> {
379        let odt = time::OffsetDateTime::parse(value, &Rfc3339)
380            .map_err(|e| TimestampError::Deserialize(e.into()))?;
381        let nanos_since_epoch = odt.unix_timestamp_nanos();
382        let seconds = (nanos_since_epoch / NS) as i64;
383        let nanos = (nanos_since_epoch % NS) as i32;
384        if nanos < 0 {
385            return Timestamp::new(seconds - 1, Self::NS + nanos);
386        }
387        Timestamp::new(seconds, nanos)
388    }
389}
390
391/// Converts the string representation of a timestamp to [Timestamp].
392///
393/// # Example
394/// ```
395/// # use google_cloud_wkt::{Timestamp, TimestampError};
396/// let s = "2025-05-16T09:46:12.500Z".to_string();
397/// let ts = Timestamp::try_from(&s)?;
398/// assert_eq!(ts.seconds(), 1747388772);
399/// assert_eq!(ts.nanos(), 500_000_000);
400/// # Ok::<(), anyhow::Error>(())
401/// ```
402impl TryFrom<&String> for Timestamp {
403    type Error = TimestampError;
404    fn try_from(value: &String) -> Result<Self, Self::Error> {
405        Timestamp::try_from(value.as_str())
406    }
407}
408
409/// Converts from [chrono::DateTime] to [Timestamp].
410///
411/// This conversion may fail if the [chrono::DateTime] value is out of range.
412///
413/// # Example
414/// ```
415/// # use google_cloud_wkt::{Timestamp, TimestampError};
416/// use chrono::{DateTime, TimeZone, Utc};
417/// let date : DateTime<Utc> = Utc.with_ymd_and_hms(2025, 5, 16, 10, 15, 00).unwrap();
418/// let ts = Timestamp::try_from(date)?;
419/// assert_eq!(String::from(ts), "2025-05-16T10:15:00Z");
420/// # Ok::<(), anyhow::Error>(())
421/// ```
422#[cfg(feature = "chrono")]
423#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
424impl TryFrom<chrono::DateTime<chrono::Utc>> for Timestamp {
425    type Error = TimestampError;
426
427    fn try_from(value: chrono::DateTime<chrono::Utc>) -> Result<Self, Self::Error> {
428        assert!(value.timestamp_subsec_nanos() <= (i32::MAX as u32));
429        Timestamp::new(value.timestamp(), value.timestamp_subsec_nanos() as i32)
430    }
431}
432
433/// Converts from [Timestamp] to [chrono::DateTime].
434///
435/// # Example
436/// ```
437/// # use google_cloud_wkt::{Timestamp, TimestampError};
438/// use chrono::{DateTime, TimeZone, Utc};
439/// let ts = Timestamp::try_from("2025-05-16T10:15:00Z")?;
440/// let date = DateTime::try_from(ts)?;
441/// # Ok::<(), anyhow::Error>(())
442/// ```
443#[cfg(feature = "chrono")]
444#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
445impl TryFrom<Timestamp> for chrono::DateTime<chrono::Utc> {
446    type Error = TimestampError;
447    fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
448        let ts = chrono::DateTime::from_timestamp(value.seconds, 0).unwrap();
449        Ok(ts + chrono::Duration::nanoseconds(value.nanos as i64))
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456    use serde_json::json;
457    use test_case::test_case;
458    type Result = std::result::Result<(), Box<dyn std::error::Error>>;
459
460    // Verify the epoch converts as expected.
461    #[test]
462    fn unix_epoch() -> Result {
463        let proto = Timestamp::default();
464        let json = serde_json::to_value(proto)?;
465        let expected = json!("1970-01-01T00:00:00Z");
466        assert_eq!(json, expected);
467        let roundtrip = serde_json::from_value::<Timestamp>(json)?;
468        assert_eq!(proto, roundtrip);
469        Ok(())
470    }
471
472    fn get_seconds(input: &str) -> i64 {
473        let odt = time::OffsetDateTime::parse(input, &Rfc3339);
474        let odt = odt.unwrap();
475        odt.unix_timestamp()
476    }
477
478    fn get_min_seconds() -> i64 {
479        self::get_seconds("0001-01-01T00:00:00Z")
480    }
481
482    fn get_max_seconds() -> i64 {
483        self::get_seconds("9999-12-31T23:59:59Z")
484    }
485
486    #[test_case(get_min_seconds() - 1, 0; "seconds below range")]
487    #[test_case(get_max_seconds() + 1, 0; "seconds above range")]
488    #[test_case(0, -1; "nanos below range")]
489    #[test_case(0, 1_000_000_000; "nanos above range")]
490    fn new_out_of_range(seconds: i64, nanos: i32) -> Result {
491        let t = Timestamp::new(seconds, nanos);
492        assert!(matches!(t, Err(Error::OutOfRange)), "{t:?}");
493        Ok(())
494    }
495
496    #[test_case(0, 0, 0, 0; "zero")]
497    #[test_case(0, 1_234_567_890, 1, 234_567_890; "nanos overflow")]
498    #[test_case(0, 2_100_000_123, 2, 100_000_123; "nanos overflow x2")]
499    #[test_case(0, -1_400_000_000, -2, 600_000_000; "nanos underflow")]
500    #[test_case(0, -2_100_000_000, -3, 900_000_000; "nanos underflow x2")]
501    #[test_case(self::get_max_seconds() + 1, 0, get_max_seconds(), 0; "seconds over range")]
502    #[test_case(self::get_min_seconds() - 1, 0, get_min_seconds(), 0; "seconds below range")]
503    #[test_case(self::get_max_seconds() - 1, 2_000_000_001, get_max_seconds(), 0; "nanos overflow range"
504	)]
505    #[test_case(self::get_min_seconds() + 1, -1_500_000_000, get_min_seconds(), 0; "nanos underflow range"
506	)]
507    fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) {
508        let got = Timestamp::clamp(seconds, nanos);
509        let want = Timestamp {
510            seconds: want_seconds,
511            nanos: want_nanos,
512        };
513        assert_eq!(got, want);
514    }
515
516    // Verify timestamps can roundtrip from string -> struct -> string without loss.
517    #[test_case("0001-01-01T00:00:00.123456789Z")]
518    #[test_case("0001-01-01T00:00:00.123456Z")]
519    #[test_case("0001-01-01T00:00:00.123Z")]
520    #[test_case("0001-01-01T00:00:00Z")]
521    #[test_case("1960-01-01T00:00:00.123456789Z")]
522    #[test_case("1960-01-01T00:00:00.123456Z")]
523    #[test_case("1960-01-01T00:00:00.123Z")]
524    #[test_case("1960-01-01T00:00:00Z")]
525    #[test_case("1970-01-01T00:00:00.123456789Z")]
526    #[test_case("1970-01-01T00:00:00.123456Z")]
527    #[test_case("1970-01-01T00:00:00.123Z")]
528    #[test_case("1970-01-01T00:00:00Z")]
529    #[test_case("9999-12-31T23:59:59.999999999Z")]
530    #[test_case("9999-12-31T23:59:59.123456789Z")]
531    #[test_case("9999-12-31T23:59:59.123456Z")]
532    #[test_case("9999-12-31T23:59:59.123Z")]
533    #[test_case("2024-10-19T12:34:56Z")]
534    #[test_case("2024-10-19T12:34:56.789Z")]
535    #[test_case("2024-10-19T12:34:56.789123456Z")]
536    fn roundtrip(input: &str) -> Result {
537        let json = serde_json::Value::String(input.to_string());
538        let timestamp = serde_json::from_value::<Timestamp>(json)?;
539        let roundtrip = serde_json::to_string(&timestamp)?;
540        assert_eq!(
541            format!("\"{input}\""),
542            roundtrip,
543            "mismatched value for input={input}"
544        );
545        Ok(())
546    }
547
548    // Verify timestamps work for some well know times, including fractional
549    // seconds.
550    #[test_case(
551        "0001-01-01T00:00:00.123456789Z",
552        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_789)
553    )]
554    #[test_case(
555        "0001-01-01T00:00:00.123456Z",
556        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_000)
557    )]
558    #[test_case(
559        "0001-01-01T00:00:00.123Z",
560        Timestamp::clamp(Timestamp::MIN_SECONDS, 123_000_000)
561    )]
562    #[test_case("0001-01-01T00:00:00Z", Timestamp::clamp(Timestamp::MIN_SECONDS, 0))]
563    #[test_case("1970-01-01T00:00:00.123456789Z", Timestamp::clamp(0, 123_456_789))]
564    #[test_case("1970-01-01T00:00:00.123456Z", Timestamp::clamp(0, 123_456_000))]
565    #[test_case("1970-01-01T00:00:00.123Z", Timestamp::clamp(0, 123_000_000))]
566    #[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0))]
567    #[test_case(
568        "9999-12-31T23:59:59.123456789Z",
569        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_789)
570    )]
571    #[test_case(
572        "9999-12-31T23:59:59.123456Z",
573        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_000)
574    )]
575    #[test_case(
576        "9999-12-31T23:59:59.123Z",
577        Timestamp::clamp(Timestamp::MAX_SECONDS, 123_000_000)
578    )]
579    #[test_case("9999-12-31T23:59:59Z", Timestamp::clamp(Timestamp::MAX_SECONDS, 0))]
580    fn well_known(input: &str, want: Timestamp) -> Result {
581        let json = serde_json::Value::String(input.to_string());
582        let got = serde_json::from_value::<Timestamp>(json)?;
583        assert_eq!(want, got);
584        Ok(())
585    }
586
587    #[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0); "zulu offset")]
588    #[test_case("1970-01-01T00:00:00+02:00", Timestamp::clamp(-2 * 60 * 60, 0); "2h positive")]
589    #[test_case("1970-01-01T00:00:00+02:45", Timestamp::clamp(-2 * 60 * 60 - 45 * 60, 0); "2h45m positive"
590	)]
591    #[test_case("1970-01-01T00:00:00-02:00", Timestamp::clamp(2 * 60 * 60, 0); "2h negative")]
592    #[test_case("1970-01-01T00:00:00-02:45", Timestamp::clamp(2 * 60 * 60 + 45 * 60, 0); "2h45m negative"
593	)]
594    fn deserialize_offsets(input: &str, want: Timestamp) -> Result {
595        let json = serde_json::Value::String(input.to_string());
596        let got = serde_json::from_value::<Timestamp>(json)?;
597        assert_eq!(want, got);
598        Ok(())
599    }
600
601    #[test_case("0000-01-01T00:00:00Z"; "below range")]
602    #[test_case("10000-01-01T00:00:00Z"; "above range")]
603    fn deserialize_out_of_range(input: &str) -> Result {
604        let value = serde_json::to_value(input)?;
605        let got = serde_json::from_value::<Timestamp>(value);
606        assert!(got.is_err());
607        Ok(())
608    }
609
610    #[test]
611    fn deserialize_unexpected_input_type() -> Result {
612        let got = serde_json::from_value::<Timestamp>(serde_json::json!({}));
613        assert!(got.is_err());
614        let msg = format!("{got:?}");
615        assert!(msg.contains("RFC 3339"), "message={msg}");
616        Ok(())
617    }
618
619    #[serde_with::skip_serializing_none]
620    #[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
621    #[serde(rename_all = "camelCase")]
622    struct Helper {
623        pub create_time: Option<Timestamp>,
624    }
625
626    #[test]
627    fn access() {
628        let ts = Timestamp::default();
629        assert_eq!(ts.nanos(), 0);
630        assert_eq!(ts.seconds(), 0);
631    }
632
633    #[test]
634    fn serialize_in_struct() -> Result {
635        let input = Helper {
636            ..Default::default()
637        };
638        let json = serde_json::to_value(input)?;
639        assert_eq!(json, json!({}));
640
641        let input = Helper {
642            create_time: Some(Timestamp::new(12, 345_678_900)?),
643        };
644
645        let json = serde_json::to_value(input)?;
646        assert_eq!(
647            json,
648            json!({ "createTime": "1970-01-01T00:00:12.3456789Z" })
649        );
650        Ok(())
651    }
652
653    #[test]
654    fn deserialize_in_struct() -> Result {
655        let input = json!({});
656        let want = Helper {
657            ..Default::default()
658        };
659        let got = serde_json::from_value::<Helper>(input)?;
660        assert_eq!(want, got);
661
662        let input = json!({ "createTime": "1970-01-01T00:00:12.3456789Z" });
663        let want = Helper {
664            create_time: Some(Timestamp::new(12, 345678900)?),
665        };
666        let got = serde_json::from_value::<Helper>(input)?;
667        assert_eq!(want, got);
668        Ok(())
669    }
670
671    #[test]
672    fn compare() -> Result {
673        let ts0 = Timestamp::default();
674        let ts1 = Timestamp::new(1, 100)?;
675        let ts2 = Timestamp::new(1, 200)?;
676        let ts3 = Timestamp::new(2, 0)?;
677        assert_eq!(ts0.partial_cmp(&ts0), Some(std::cmp::Ordering::Equal));
678        assert_eq!(ts0.partial_cmp(&ts1), Some(std::cmp::Ordering::Less));
679        assert_eq!(ts2.partial_cmp(&ts3), Some(std::cmp::Ordering::Less));
680        Ok(())
681    }
682
683    #[test]
684    fn convert_from_string() -> Result {
685        let input = "2025-05-16T18:00:00Z".to_string();
686        let a = Timestamp::try_from(input.as_str())?;
687        let b = Timestamp::try_from(&input)?;
688        assert_eq!(a, b);
689        Ok(())
690    }
691
692    #[test]
693    fn convert_from_time() -> Result {
694        let ts = time::OffsetDateTime::from_unix_timestamp(123)?
695            + time::Duration::nanoseconds(456789012);
696        let got = Timestamp::try_from(ts)?;
697        let want = Timestamp::new(123, 456789012)?;
698        assert_eq!(got, want);
699        Ok(())
700    }
701
702    #[test]
703    fn convert_to_time() -> Result {
704        let ts = Timestamp::new(123, 456789012)?;
705        let got = time::OffsetDateTime::try_from(ts)?;
706        let want = time::OffsetDateTime::from_unix_timestamp(123)?
707            + time::Duration::nanoseconds(456789012);
708        assert_eq!(got, want);
709        Ok(())
710    }
711
712    #[test]
713    fn convert_from_chrono_time() -> Result {
714        let ts = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
715        let got = Timestamp::try_from(ts)?;
716        let want = Timestamp::new(123, 456789012)?;
717        assert_eq!(got, want);
718        Ok(())
719    }
720
721    #[test]
722    fn convert_to_chrono_time() -> Result {
723        let ts = Timestamp::new(123, 456789012)?;
724        let got = chrono::DateTime::try_from(ts)?;
725        let want = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
726        assert_eq!(got, want);
727        Ok(())
728    }
729}