#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
#[non_exhaustive]
pub struct Timestamp {
seconds: i64,
nanos: i32,
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum TimestampError {
#[error("seconds and/or nanoseconds out of range")]
OutOfRange,
#[error("cannot deserialize timestamp, source={0}")]
Deserialize(#[source] BoxedError),
}
type BoxedError = Box<dyn std::error::Error + Send + Sync>;
type Error = TimestampError;
impl Timestamp {
const NS: i32 = 1_000_000_000;
pub const MIN_SECONDS: i64 = -62135596800;
pub const MAX_SECONDS: i64 = 253402300799;
pub const MIN_NANOS: i32 = 0;
pub const MAX_NANOS: i32 = Self::NS - 1;
pub fn new(seconds: i64, nanos: i32) -> Result<Self, Error> {
if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
return Err(Error::OutOfRange);
}
if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
return Err(Error::OutOfRange);
}
Ok(Self { seconds, nanos })
}
pub fn clamp(seconds: i64, nanos: i32) -> Self {
let (seconds, nanos) = match nanos.cmp(&0_i32) {
std::cmp::Ordering::Equal => (seconds, nanos),
std::cmp::Ordering::Greater => (
seconds.saturating_add((nanos / Self::NS) as i64),
nanos % Self::NS,
),
std::cmp::Ordering::Less => (
seconds.saturating_sub(1 - (nanos / Self::NS) as i64),
Self::NS + nanos % Self::NS,
),
};
if seconds < Self::MIN_SECONDS {
return Self {
seconds: Self::MIN_SECONDS,
nanos: 0,
};
} else if seconds > Self::MAX_SECONDS {
return Self {
seconds: Self::MAX_SECONDS,
nanos: 0,
};
}
Self { seconds, nanos }
}
pub fn seconds(&self) -> i64 {
self.seconds
}
pub fn nanos(&self) -> i32 {
self.nanos
}
}
impl crate::message::Message for Timestamp {
fn typename() -> &'static str {
"type.googleapis.com/google.protobuf.Timestamp"
}
#[allow(private_interfaces)]
fn serializer() -> impl crate::message::MessageSerializer<Self> {
crate::message::ValueSerializer::<Self>::new()
}
}
const NS: i128 = 1_000_000_000;
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
impl serde::ser::Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
String::from(*self).serialize(serializer)
}
}
struct TimestampVisitor;
impl serde::de::Visitor<'_> for TimestampVisitor {
type Value = Timestamp;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string with a timestamp in RFC 3339 format")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Timestamp::try_from(value).map_err(E::custom)
}
}
#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
impl<'de> serde::de::Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_str(TimestampVisitor)
}
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<time::OffsetDateTime> for Timestamp {
type Error = TimestampError;
fn try_from(value: time::OffsetDateTime) -> Result<Self, Self::Error> {
let seconds = value.unix_timestamp();
let nanos = (value.unix_timestamp_nanos() - seconds as i128 * NS) as i32;
Self::new(seconds, nanos)
}
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<Timestamp> for time::OffsetDateTime {
type Error = time::error::ComponentRange;
fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
let ts = time::OffsetDateTime::from_unix_timestamp(value.seconds())?;
Ok(ts + time::Duration::nanoseconds(value.nanos() as i64))
}
}
const EXPECT_OFFSET_DATE_TIME_CONVERTS: &str = concat!(
"converting Timestamp to time::OffsetDateTime should always succeed. ",
"The Timestamp values are always in range. ",
"If this is not the case, please file a bug at https://github.com/googleapis/google-cloud-rust/issues"
);
const EXPECT_TIMESTAMP_FORMAT_SUCCEEDS: &str = concat!(
"formatting a Timestamp using RFC-3339 should always succeed. ",
"The Timestamp values are always in range, and we use a well-known constant for the format specifier. ",
"If this is not the case, please file a bug at https://github.com/googleapis/google-cloud-rust/issues"
);
use time::format_description::well_known::Rfc3339;
impl From<Timestamp> for String {
fn from(timestamp: Timestamp) -> Self {
let ts = time::OffsetDateTime::from_unix_timestamp_nanos(
timestamp.seconds as i128 * NS + timestamp.nanos as i128,
)
.expect(EXPECT_OFFSET_DATE_TIME_CONVERTS);
ts.format(&Rfc3339).expect(EXPECT_TIMESTAMP_FORMAT_SUCCEEDS)
}
}
impl TryFrom<&str> for Timestamp {
type Error = TimestampError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let odt = time::OffsetDateTime::parse(value, &Rfc3339)
.map_err(|e| TimestampError::Deserialize(e.into()))?;
let nanos_since_epoch = odt.unix_timestamp_nanos();
let seconds = (nanos_since_epoch / NS) as i64;
let nanos = (nanos_since_epoch % NS) as i32;
if nanos < 0 {
return Timestamp::new(seconds - 1, Self::NS + nanos);
}
Timestamp::new(seconds, nanos)
}
}
impl TryFrom<&String> for Timestamp {
type Error = TimestampError;
fn try_from(value: &String) -> Result<Self, Self::Error> {
Timestamp::try_from(value.as_str())
}
}
impl TryFrom<std::time::SystemTime> for Timestamp {
type Error = TimestampError;
fn try_from(value: std::time::SystemTime) -> Result<Self, Self::Error> {
match value.duration_since(std::time::SystemTime::UNIX_EPOCH) {
Ok(d) => {
let s = d.as_secs();
if s > i64::MAX as u64 {
return Err(TimestampError::OutOfRange);
}
Timestamp::new(s as i64, d.subsec_nanos() as i32)
}
Err(e) => {
let d = e.duration();
let s = d.as_secs();
if s > i64::MAX as u64 {
return Err(TimestampError::OutOfRange);
}
let seconds = -(s as i64);
let nanos = d.subsec_nanos() as i32;
if nanos > 0 {
return Timestamp::new(seconds - 1, Self::NS - nanos);
}
Timestamp::new(seconds, 0)
}
}
}
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl TryFrom<chrono::DateTime<chrono::Utc>> for Timestamp {
type Error = TimestampError;
fn try_from(value: chrono::DateTime<chrono::Utc>) -> Result<Self, Self::Error> {
assert!(value.timestamp_subsec_nanos() <= (i32::MAX as u32));
Timestamp::new(value.timestamp(), value.timestamp_subsec_nanos() as i32)
}
}
#[cfg(feature = "chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "chrono")))]
impl TryFrom<Timestamp> for chrono::DateTime<chrono::Utc> {
type Error = TimestampError;
fn try_from(value: Timestamp) -> Result<Self, Self::Error> {
let ts = chrono::DateTime::from_timestamp(value.seconds, 0).unwrap();
Ok(ts + chrono::Duration::nanoseconds(value.nanos as i64))
}
}
impl TryFrom<Timestamp> for std::time::SystemTime {
type Error = TimestampError;
fn try_from(value: Timestamp) -> Result<std::time::SystemTime, Self::Error> {
let s = value.seconds();
let ts = if s >= 0 {
let d = std::time::Duration::from_secs(s as u64);
std::time::SystemTime::UNIX_EPOCH
.checked_add(d)
.ok_or_else(|| TimestampError::OutOfRange)?
} else {
let d = std::time::Duration::from_secs(-s as u64);
std::time::SystemTime::UNIX_EPOCH
.checked_sub(d)
.ok_or_else(|| TimestampError::OutOfRange)?
};
let d = std::time::Duration::from_nanos(value.nanos() as u64);
ts.checked_add(d).ok_or_else(|| TimestampError::OutOfRange)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use test_case::test_case;
type Result = std::result::Result<(), Box<dyn std::error::Error>>;
#[test]
fn unix_epoch() -> Result {
let proto = Timestamp::default();
let json = serde_json::to_value(proto)?;
let expected = json!("1970-01-01T00:00:00Z");
assert_eq!(json, expected);
let roundtrip = serde_json::from_value::<Timestamp>(json)?;
assert_eq!(proto, roundtrip);
Ok(())
}
fn get_seconds(input: &str) -> i64 {
let odt = time::OffsetDateTime::parse(input, &Rfc3339);
let odt = odt.unwrap();
odt.unix_timestamp()
}
fn get_min_seconds() -> i64 {
self::get_seconds("0001-01-01T00:00:00Z")
}
fn get_max_seconds() -> i64 {
self::get_seconds("9999-12-31T23:59:59Z")
}
#[test_case(get_min_seconds() - 1, 0; "seconds below range")]
#[test_case(get_max_seconds() + 1, 0; "seconds above range")]
#[test_case(0, -1; "nanos below range")]
#[test_case(0, 1_000_000_000; "nanos above range")]
fn new_out_of_range(seconds: i64, nanos: i32) -> Result {
let t = Timestamp::new(seconds, nanos);
assert!(matches!(t, Err(Error::OutOfRange)), "{t:?}");
Ok(())
}
#[test_case(0, 0, 0, 0; "zero")]
#[test_case(0, 1_234_567_890, 1, 234_567_890; "nanos overflow")]
#[test_case(0, 2_100_000_123, 2, 100_000_123; "nanos overflow x2")]
#[test_case(0, -1_400_000_000, -2, 600_000_000; "nanos underflow")]
#[test_case(0, -2_100_000_000, -3, 900_000_000; "nanos underflow x2")]
#[test_case(self::get_max_seconds() + 1, 0, get_max_seconds(), 0; "seconds over range")]
#[test_case(self::get_min_seconds() - 1, 0, get_min_seconds(), 0; "seconds below range")]
#[test_case(self::get_max_seconds() - 1, 2_000_000_001, get_max_seconds(), 0; "nanos overflow range"
)]
#[test_case(self::get_min_seconds() + 1, -1_500_000_000, get_min_seconds(), 0; "nanos underflow range"
)]
fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) {
let got = Timestamp::clamp(seconds, nanos);
let want = Timestamp {
seconds: want_seconds,
nanos: want_nanos,
};
assert_eq!(got, want);
}
#[test_case("0001-01-01T00:00:00.123456789Z")]
#[test_case("0001-01-01T00:00:00.123456Z")]
#[test_case("0001-01-01T00:00:00.123Z")]
#[test_case("0001-01-01T00:00:00Z")]
#[test_case("1960-01-01T00:00:00.123456789Z")]
#[test_case("1960-01-01T00:00:00.123456Z")]
#[test_case("1960-01-01T00:00:00.123Z")]
#[test_case("1960-01-01T00:00:00Z")]
#[test_case("1970-01-01T00:00:00.123456789Z")]
#[test_case("1970-01-01T00:00:00.123456Z")]
#[test_case("1970-01-01T00:00:00.123Z")]
#[test_case("1970-01-01T00:00:00Z")]
#[test_case("9999-12-31T23:59:59.999999999Z")]
#[test_case("9999-12-31T23:59:59.123456789Z")]
#[test_case("9999-12-31T23:59:59.123456Z")]
#[test_case("9999-12-31T23:59:59.123Z")]
#[test_case("2024-10-19T12:34:56Z")]
#[test_case("2024-10-19T12:34:56.789Z")]
#[test_case("2024-10-19T12:34:56.789123456Z")]
fn roundtrip(input: &str) -> Result {
let json = serde_json::Value::String(input.to_string());
let timestamp = serde_json::from_value::<Timestamp>(json)?;
let roundtrip = serde_json::to_string(×tamp)?;
assert_eq!(
format!("\"{input}\""),
roundtrip,
"mismatched value for input={input}"
);
Ok(())
}
#[test_case(
"0001-01-01T00:00:00.123456789Z",
Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_789)
)]
#[test_case(
"0001-01-01T00:00:00.123456Z",
Timestamp::clamp(Timestamp::MIN_SECONDS, 123_456_000)
)]
#[test_case(
"0001-01-01T00:00:00.123Z",
Timestamp::clamp(Timestamp::MIN_SECONDS, 123_000_000)
)]
#[test_case("0001-01-01T00:00:00Z", Timestamp::clamp(Timestamp::MIN_SECONDS, 0))]
#[test_case("1970-01-01T00:00:00.123456789Z", Timestamp::clamp(0, 123_456_789))]
#[test_case("1970-01-01T00:00:00.123456Z", Timestamp::clamp(0, 123_456_000))]
#[test_case("1970-01-01T00:00:00.123Z", Timestamp::clamp(0, 123_000_000))]
#[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0))]
#[test_case(
"9999-12-31T23:59:59.123456789Z",
Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_789)
)]
#[test_case(
"9999-12-31T23:59:59.123456Z",
Timestamp::clamp(Timestamp::MAX_SECONDS, 123_456_000)
)]
#[test_case(
"9999-12-31T23:59:59.123Z",
Timestamp::clamp(Timestamp::MAX_SECONDS, 123_000_000)
)]
#[test_case("9999-12-31T23:59:59Z", Timestamp::clamp(Timestamp::MAX_SECONDS, 0))]
fn well_known(input: &str, want: Timestamp) -> Result {
let json = serde_json::Value::String(input.to_string());
let got = serde_json::from_value::<Timestamp>(json)?;
assert_eq!(want, got);
Ok(())
}
#[test_case("1970-01-01T00:00:00Z", Timestamp::clamp(0, 0); "zulu offset")]
#[test_case("1970-01-01T00:00:00+02:00", Timestamp::clamp(-2 * 60 * 60, 0); "2h positive")]
#[test_case("1970-01-01T00:00:00+02:45", Timestamp::clamp(-2 * 60 * 60 - 45 * 60, 0); "2h45m positive"
)]
#[test_case("1970-01-01T00:00:00-02:00", Timestamp::clamp(2 * 60 * 60, 0); "2h negative")]
#[test_case("1970-01-01T00:00:00-02:45", Timestamp::clamp(2 * 60 * 60 + 45 * 60, 0); "2h45m negative"
)]
fn deserialize_offsets(input: &str, want: Timestamp) -> Result {
let json = serde_json::Value::String(input.to_string());
let got = serde_json::from_value::<Timestamp>(json)?;
assert_eq!(want, got);
Ok(())
}
#[test_case("0000-01-01T00:00:00Z"; "below range")]
#[test_case("10000-01-01T00:00:00Z"; "above range")]
fn deserialize_out_of_range(input: &str) -> Result {
let value = serde_json::to_value(input)?;
let got = serde_json::from_value::<Timestamp>(value);
assert!(got.is_err());
Ok(())
}
#[test]
fn deserialize_unexpected_input_type() -> Result {
let got = serde_json::from_value::<Timestamp>(serde_json::json!({}));
assert!(got.is_err());
let msg = format!("{got:?}");
assert!(msg.contains("RFC 3339"), "message={msg}");
Ok(())
}
#[serde_with::skip_serializing_none]
#[derive(Clone, Debug, Default, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct Helper {
pub create_time: Option<Timestamp>,
}
#[test]
fn access() {
let ts = Timestamp::default();
assert_eq!(ts.nanos(), 0);
assert_eq!(ts.seconds(), 0);
}
#[test]
fn serialize_in_struct() -> Result {
let input = Helper {
..Default::default()
};
let json = serde_json::to_value(input)?;
assert_eq!(json, json!({}));
let input = Helper {
create_time: Some(Timestamp::new(12, 345_678_900)?),
};
let json = serde_json::to_value(input)?;
assert_eq!(
json,
json!({ "createTime": "1970-01-01T00:00:12.3456789Z" })
);
Ok(())
}
#[test]
fn deserialize_in_struct() -> Result {
let input = json!({});
let want = Helper {
..Default::default()
};
let got = serde_json::from_value::<Helper>(input)?;
assert_eq!(want, got);
let input = json!({ "createTime": "1970-01-01T00:00:12.3456789Z" });
let want = Helper {
create_time: Some(Timestamp::new(12, 345678900)?),
};
let got = serde_json::from_value::<Helper>(input)?;
assert_eq!(want, got);
Ok(())
}
#[test]
fn compare() -> Result {
let ts0 = Timestamp::default();
let ts1 = Timestamp::new(1, 100)?;
let ts2 = Timestamp::new(1, 200)?;
let ts3 = Timestamp::new(2, 0)?;
assert_eq!(ts0.partial_cmp(&ts0), Some(std::cmp::Ordering::Equal));
assert_eq!(ts0.partial_cmp(&ts1), Some(std::cmp::Ordering::Less));
assert_eq!(ts2.partial_cmp(&ts3), Some(std::cmp::Ordering::Less));
Ok(())
}
#[test]
fn convert_from_string() -> Result {
let input = "2025-05-16T18:00:00Z".to_string();
let a = Timestamp::try_from(input.as_str())?;
let b = Timestamp::try_from(&input)?;
assert_eq!(a, b);
Ok(())
}
#[test]
fn convert_from_time() -> Result {
let ts = time::OffsetDateTime::from_unix_timestamp(123)?
+ time::Duration::nanoseconds(456789012);
let got = Timestamp::try_from(ts)?;
let want = Timestamp::new(123, 456789012)?;
assert_eq!(got, want);
Ok(())
}
#[test]
fn convert_to_time() -> Result {
let ts = Timestamp::new(123, 456789012)?;
let got = time::OffsetDateTime::try_from(ts)?;
let want = time::OffsetDateTime::from_unix_timestamp(123)?
+ time::Duration::nanoseconds(456789012);
assert_eq!(got, want);
Ok(())
}
#[test]
fn convert_from_chrono_time() -> Result {
let ts = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
let got = Timestamp::try_from(ts)?;
let want = Timestamp::new(123, 456789012)?;
assert_eq!(got, want);
Ok(())
}
#[test]
fn convert_to_chrono_time() -> Result {
let ts = Timestamp::new(123, 456789012)?;
let got = chrono::DateTime::try_from(ts)?;
let want = chrono::DateTime::from_timestamp(123, 456789012).unwrap();
assert_eq!(got, want);
Ok(())
}
#[test]
fn convert_from_std_time() -> Result {
let now = std::time::SystemTime::now();
let want = now.duration_since(std::time::SystemTime::UNIX_EPOCH)?;
let wkt = Timestamp::try_from(now)?;
assert_eq!(wkt.seconds(), want.as_secs() as i64);
assert_eq!(wkt.nanos(), want.subsec_nanos() as i32);
let past = std::time::SystemTime::UNIX_EPOCH
.checked_sub(std::time::Duration::from_secs(123))
.unwrap()
.checked_sub(std::time::Duration::from_nanos(100))
.unwrap();
let wkt = Timestamp::try_from(past)?;
assert_eq!(wkt.seconds(), -124);
assert_eq!(wkt.nanos(), Timestamp::NS - 100);
Ok(())
}
#[test]
fn convert_to_std_time() -> Result {
let ts = Timestamp::clamp(123, 456789000);
let got = std::time::SystemTime::try_from(ts)?;
let want = std::time::SystemTime::UNIX_EPOCH
.checked_add(std::time::Duration::from_secs(123))
.unwrap()
.checked_add(std::time::Duration::from_nanos(456789000))
.unwrap();
assert_eq!(got, want);
let ts = Timestamp::clamp(-123, -456789000);
let got = std::time::SystemTime::try_from(ts)?;
let want = std::time::SystemTime::UNIX_EPOCH
.checked_sub(std::time::Duration::from_secs(123))
.unwrap()
.checked_sub(std::time::Duration::from_nanos(456789000))
.unwrap();
assert_eq!(got, want);
Ok(())
}
}