1#[derive(Clone, Debug, Default, PartialEq, PartialOrd)]
46#[non_exhaustive]
47pub struct Timestamp {
48 seconds: i64,
52
53 nanos: i32,
58}
59
60#[derive(thiserror::Error, Debug, PartialEq)]
62pub enum TimestampError {
63 #[error("seconds and/or nanoseconds out of range")]
65 OutOfRange(),
66
67 #[error("cannot serialize timestamp: {0}")]
68 Serialize(String),
69
70 #[error("cannot deserialize timestamp: {0}")]
71 Deserialize(String),
72}
73
74type Error = TimestampError;
75
76impl Timestamp {
77 const NS: i32 = 1_000_000_000;
78
79 pub const MIN_SECONDS: i64 = -62135596800;
82
83 pub const MAX_SECONDS: i64 = 253402300799;
86
87 pub const MIN_NANOS: i32 = 0;
89
90 pub const MAX_NANOS: i32 = Self::NS - 1;
92
93 pub fn new(seconds: i64, nanos: i32) -> std::result::Result<Self, Error> {
102 if !(Self::MIN_SECONDS..=Self::MAX_SECONDS).contains(&seconds) {
103 return Err(Error::OutOfRange());
104 }
105 if !(Self::MIN_NANOS..=Self::MAX_NANOS).contains(&nanos) {
106 return Err(Error::OutOfRange());
107 }
108 Ok(Self { seconds, nanos })
109 }
110
111 pub fn clamp(seconds: i64, nanos: i32) -> Self {
126 let (seconds, nanos) = match nanos.cmp(&0_i32) {
127 std::cmp::Ordering::Equal => (seconds, nanos),
128 std::cmp::Ordering::Greater => (
129 seconds.saturating_add((nanos / Self::NS) as i64),
130 nanos % Self::NS,
131 ),
132 std::cmp::Ordering::Less => (
133 seconds.saturating_sub(1 - (nanos / Self::NS) as i64),
134 Self::NS + nanos % Self::NS,
135 ),
136 };
137 if seconds < Self::MIN_SECONDS {
138 return Self {
139 seconds: Self::MIN_SECONDS,
140 nanos: 0,
141 };
142 } else if seconds > Self::MAX_SECONDS {
143 return Self {
144 seconds: Self::MAX_SECONDS,
145 nanos: 0,
146 };
147 }
148 Self { seconds, nanos }
149 }
150
151 pub fn seconds(&self) -> i64 {
155 self.seconds
156 }
157
158 pub fn nanos(&self) -> i32 {
164 self.nanos
165 }
166}
167
168impl crate::message::Message for Timestamp {
169 fn typename() -> &'static str {
170 "type.googleapis.com/google.protobuf.Timestamp"
171 }
172 fn to_map(&self) -> Result<crate::message::Map, crate::AnyError> {
173 crate::message::to_json_string(self)
174 }
175 fn from_map(map: &crate::message::Map) -> Result<Self, crate::AnyError> {
176 crate::message::from_value(map)
177 }
178}
179
180use time::format_description::well_known::Rfc3339;
181const NS: i128 = 1_000_000_000;
182
183impl serde::ser::Serialize for Timestamp {
185 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
186 where
187 S: serde::ser::Serializer,
188 {
189 use serde::ser::Error as _;
190 String::try_from(self)
191 .map_err(S::Error::custom)?
192 .serialize(serializer)
193 }
194}
195
196struct TimestampVisitor;
197
198impl serde::de::Visitor<'_> for TimestampVisitor {
199 type Value = Timestamp;
200
201 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
202 formatter.write_str("a string with a timestamp in RFC 3339 format")
203 }
204
205 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
206 where
207 E: serde::de::Error,
208 {
209 Timestamp::try_from(value).map_err(E::custom)
210 }
211}
212
213impl<'de> serde::de::Deserialize<'de> for Timestamp {
215 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
216 where
217 D: serde::Deserializer<'de>,
218 {
219 deserializer.deserialize_str(TimestampVisitor)
220 }
221}
222
223#[cfg(feature = "time")]
227impl TryFrom<time::OffsetDateTime> for Timestamp {
228 type Error = TimestampError;
229
230 fn try_from(value: time::OffsetDateTime) -> std::result::Result<Self, Self::Error> {
231 use time::convert::{Nanosecond, Second};
232
233 let seconds = value.unix_timestamp();
234 let nanos = (value.unix_timestamp_nanos()
235 - seconds as i128 * Nanosecond::per(Second) as i128) as i32;
236 Self::new(seconds, nanos)
237 }
238}
239
240#[cfg(feature = "time")]
244impl TryFrom<Timestamp> for time::OffsetDateTime {
245 type Error = time::error::ComponentRange;
246 fn try_from(value: Timestamp) -> std::result::Result<Self, Self::Error> {
247 let ts = time::OffsetDateTime::from_unix_timestamp(value.seconds())?;
248 Ok(ts + time::Duration::nanoseconds(value.nanos() as i64))
249 }
250}
251
252impl TryFrom<&Timestamp> for String {
254 type Error = TimestampError;
255 fn try_from(timestamp: &Timestamp) -> std::result::Result<Self, Self::Error> {
256 let ts = time::OffsetDateTime::from_unix_timestamp_nanos(
257 timestamp.seconds as i128 * NS + timestamp.nanos as i128,
258 )
259 .map_err(|e| TimestampError::Serialize(format!("{e}")))?;
260 ts.format(&Rfc3339)
261 .map_err(|e| TimestampError::Serialize(format!("{e}")))
262 }
263}
264
265impl TryFrom<&str> for Timestamp {
267 type Error = TimestampError;
268 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
269 let odt = time::OffsetDateTime::parse(value, &Rfc3339)
270 .map_err(|e| TimestampError::Deserialize(format!("{e}")))?;
271 let nanos_since_epoch = odt.unix_timestamp_nanos();
272 let seconds = (nanos_since_epoch / NS) as i64;
273 let nanos = (nanos_since_epoch % NS) as i32;
274 Timestamp::new(seconds, nanos)
275 }
276}
277
278#[cfg(feature = "chrono")]
282impl TryFrom<chrono::DateTime<chrono::Utc>> for Timestamp {
283 type Error = TimestampError;
284
285 fn try_from(value: chrono::DateTime<chrono::Utc>) -> std::result::Result<Self, Self::Error> {
286 assert!(value.timestamp_subsec_nanos() <= (i32::MAX as u32));
287 Timestamp::new(value.timestamp(), value.timestamp_subsec_nanos() as i32)
288 }
289}
290
291#[cfg(feature = "chrono")]
293impl TryFrom<Timestamp> for chrono::DateTime<chrono::Utc> {
294 type Error = TimestampError;
295 fn try_from(value: Timestamp) -> std::result::Result<Self, Self::Error> {
296 let ts = chrono::DateTime::from_timestamp(value.seconds, 0).unwrap();
297 Ok(ts + chrono::Duration::nanoseconds(value.nanos as i64))
298 }
299}
300
301#[cfg(test)]
302mod test {
303 use super::*;
304 use serde_json::json;
305 use test_case::test_case;
306 type Result = std::result::Result<(), Box<dyn std::error::Error>>;
307
308 #[test]
310 fn unix_epoch() -> Result {
311 let proto = Timestamp::default();
312 let json = serde_json::to_value(&proto)?;
313 let expected = json!("1970-01-01T00:00:00Z");
314 assert_eq!(json, expected);
315 let roundtrip = serde_json::from_value::<Timestamp>(json)?;
316 assert_eq!(proto, roundtrip);
317 Ok(())
318 }
319
320 fn get_seconds(input: &str) -> i64 {
321 let odt = time::OffsetDateTime::parse(input, &Rfc3339);
322 let odt = odt.unwrap();
323 odt.unix_timestamp()
324 }
325
326 fn get_min_seconds() -> i64 {
327 self::get_seconds("0001-01-01T00:00:00Z")
328 }
329
330 fn get_max_seconds() -> i64 {
331 self::get_seconds("9999-12-31T23:59:59Z")
332 }
333
334 #[test_case(get_min_seconds() - 1, 0; "seconds below range")]
335 #[test_case(get_max_seconds() + 1, 0; "seconds above range")]
336 #[test_case(0, -1; "nanos below range")]
337 #[test_case(0, 1_000_000_000; "nanos above range")]
338 fn new_out_of_range(seconds: i64, nanos: i32) -> Result {
339 let t = Timestamp::new(seconds, nanos);
340 assert_eq!(t, Err(Error::OutOfRange()));
341 Ok(())
342 }
343
344 #[test_case(0, 0, 0, 0; "zero")]
345 #[test_case(0, 1_234_567_890, 1, 234_567_890; "nanos overflow")]
346 #[test_case(0, -1_400_000_000, -2, 600_000_000; "nanos underflow")]
347 #[test_case(self::get_max_seconds() + 1, 0, get_max_seconds(), 0; "seconds over range")]
348 #[test_case(self::get_min_seconds() - 1, 0, get_min_seconds(), 0; "seconds below range")]
349 #[test_case(self::get_max_seconds() - 1, 2_000_000_001, get_max_seconds(), 0; "nanos overflow range")]
350 #[test_case(self::get_min_seconds() + 1, -1_500_000_000, get_min_seconds(), 0; "nanos underflow range")]
351 fn clamp(seconds: i64, nanos: i32, want_seconds: i64, want_nanos: i32) {
352 let got = Timestamp::clamp(seconds, nanos);
353 let want = Timestamp {
354 seconds: want_seconds,
355 nanos: want_nanos,
356 };
357 assert_eq!(got, want);
358 }
359
360 #[test_case("0001-01-01T00:00:00Z")]
362 #[test_case("9999-12-31T23:59:59.999999999Z")]
363 #[test_case("2024-10-19T12:34:56.789Z")]
364 #[test_case("2024-10-19T12:34:56.789123456Z")]
365 fn roundtrip(input: &str) -> Result {
366 let json = serde_json::Value::String(input.to_string());
367 let timestamp = serde_json::from_value::<Timestamp>(json)?;
368 let roundtrip = serde_json::to_string(×tamp)?;
369 assert_eq!(
370 format!("\"{input}\""),
371 roundtrip,
372 "mismatched value for input={input}"
373 );
374 Ok(())
375 }
376
377 #[test_case("0000-01-01T00:00:00Z"; "below range")]
378 #[test_case("10000-01-01T00:00:00Z"; "above range")]
379 fn deserialize_out_of_range(input: &str) -> Result {
380 let value = serde_json::to_value(input)?;
381 let got = serde_json::from_value::<Timestamp>(value);
382 assert!(got.is_err());
383 Ok(())
384 }
385
386 #[test]
387 fn deserialize_unexpected_input_type() -> Result {
388 let got = serde_json::from_value::<Timestamp>(serde_json::json!({}));
389 assert!(got.is_err());
390 let msg = format!("{got:?}");
391 assert!(msg.contains("RFC 3339"), "message={}", msg);
392 Ok(())
393 }
394}