1#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
58#[non_exhaustive]
59pub struct Timestamp {
60 seconds: i64,
64
65 nanos: i32,
70}
71
72#[derive(thiserror::Error, Debug)]
87#[non_exhaustive]
88pub enum TimestampError {
89 #[error("seconds and/or nanoseconds out of range")]
91 OutOfRange,
92
93 #[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 pub const MIN_SECONDS: i64 = -62135596800;
107
108 pub const MAX_SECONDS: i64 = 253402300799;
111
112 pub const MIN_NANOS: i32 = 0;
114
115 pub const MAX_NANOS: i32 = Self::NS - 1;
117
118 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 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 pub fn seconds(&self) -> i64 {
211 self.seconds
212 }
213
214 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#[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#[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#[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#[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
347impl 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
366impl 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
391impl 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#[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#[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 #[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 #[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(×tamp)?;
540 assert_eq!(
541 format!("\"{input}\""),
542 roundtrip,
543 "mismatched value for input={input}"
544 );
545 Ok(())
546 }
547
548 #[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}