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 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#[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
344impl 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
363impl 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
388impl 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
406impl 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 let d = e.duration();
433 let s = d.as_secs();
434 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#[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#[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
493impl 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 #[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 #[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(×tamp)?;
614 assert_eq!(
615 format!("\"{input}\""),
616 roundtrip,
617 "mismatched value for input={input}"
618 );
619 Ok(())
620 }
621
622 #[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}