proto_types/timestamp/
timestamp_conversions.rs1#[cfg(feature = "chrono")]
2mod chrono_impls {
3 use chrono::{DateTime, FixedOffset, NaiveDateTime, Utc};
4
5 use crate::{Timestamp, timestamp::TimestampError};
6
7 impl From<DateTime<Utc>> for Timestamp {
8 #[inline]
9 fn from(datetime: DateTime<Utc>) -> Self {
10 let mut ts = Self {
11 seconds: datetime.timestamp(),
12 nanos: datetime.timestamp_subsec_nanos().cast_signed(),
14 };
15 ts.normalize();
16 ts
17 }
18 }
19
20 impl From<NaiveDateTime> for Timestamp {
21 #[inline]
22 fn from(datetime: NaiveDateTime) -> Self {
23 let mut ts = Self {
24 seconds: datetime.and_utc().timestamp(),
25 nanos: datetime
27 .and_utc()
28 .timestamp_subsec_nanos()
29 .cast_signed(),
30 };
31 ts.normalize();
32 ts
33 }
34 }
35
36 impl TryFrom<Timestamp> for DateTime<Utc> {
37 type Error = TimestampError;
38
39 #[inline]
40 fn try_from(mut timestamp: Timestamp) -> Result<Self, Self::Error> {
41 timestamp.normalize();
42
43 u32::try_from(timestamp.nanos)
44 .ok()
45 .and_then(|nanos| Self::from_timestamp(timestamp.seconds, nanos))
46 .ok_or(TimestampError::OutOfSystemRange(timestamp))
47 }
48 }
49
50 impl TryFrom<Timestamp> for NaiveDateTime {
51 type Error = TimestampError;
52
53 #[inline]
54 fn try_from(mut timestamp: Timestamp) -> Result<Self, Self::Error> {
55 timestamp.normalize();
56
57 u32::try_from(timestamp.nanos)
58 .ok()
59 .and_then(|nanos| DateTime::<Utc>::from_timestamp(timestamp.seconds, nanos))
60 .map(|d| d.naive_local())
61 .ok_or(TimestampError::OutOfSystemRange(timestamp))
62 }
63 }
64
65 impl TryFrom<Timestamp> for DateTime<FixedOffset> {
66 type Error = TimestampError;
67
68 #[inline]
69 fn try_from(mut timestamp: Timestamp) -> Result<Self, Self::Error> {
70 timestamp.normalize();
71
72 let chrono_utc: DateTime<Utc> = timestamp.try_into()?;
73
74 Ok(chrono_utc.into())
75 }
76 }
77
78 impl TryFrom<chrono::DateTime<chrono::FixedOffset>> for Timestamp {
79 type Error = TimestampError;
80
81 #[inline]
82 fn try_from(dt: chrono::DateTime<chrono::FixedOffset>) -> Result<Self, Self::Error> {
83 let seconds = dt.timestamp();
84 let nanos = dt
85 .timestamp_subsec_nanos()
86 .try_into()
87 .map_err(|_| TimestampError::InvalidDateTime)?;
88
89 Ok(Self { seconds, nanos })
90 }
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use crate::Timestamp;
97
98 use alloc::string::ToString;
99 use std::time::{SystemTime, UNIX_EPOCH};
100
101 fn ts(s: i64, n: i32) -> Timestamp {
102 Timestamp {
103 seconds: s,
104 nanos: n,
105 }
106 }
107
108 #[test]
111 fn test_system_time_epoch() {
112 let t: Timestamp = UNIX_EPOCH.into();
114 assert_eq!(t, ts(0, 0));
115
116 let sys: SystemTime = ts(0, 0).try_into().unwrap();
118 assert_eq!(sys, UNIX_EPOCH);
119 }
120
121 #[test]
122 fn test_system_time_roundtrip() {
123 let now = SystemTime::now();
124
125 let t: Timestamp = now.into();
127
128 let back: SystemTime = t
130 .try_into()
131 .expect("Timestamp should fit in SystemTime");
132
133 let diff = now
134 .duration_since(back)
135 .unwrap_or_else(|e| e.duration());
136 assert!(diff.as_nanos() < 1, "Roundtrip drifted significantly");
137 }
138
139 #[test]
140 fn test_system_time_pre_epoch() {
141 let pre_epoch = UNIX_EPOCH - core::time::Duration::from_secs(1);
143
144 let t: Timestamp = pre_epoch.into();
145 assert_eq!(t.seconds, -1);
146
147 let back: SystemTime = t.try_into().unwrap();
148 assert_eq!(back, pre_epoch);
149 }
150
151 #[test]
154 fn test_display_rfc3339() {
155 assert_eq!(ts(0, 0).to_string(), "1970-01-01T00:00:00Z");
157
158 assert_eq!(ts(0, 500_000_000).to_string(), "1970-01-01T00:00:00.5Z");
161
162 assert_eq!(ts(-1, 0).to_string(), "1969-12-31T23:59:59Z");
165 }
166
167 #[test]
168 fn test_from_str_rfc3339() {
169 use core::str::FromStr;
170
171 let t = Timestamp::from_str("1970-01-01T00:00:00Z").unwrap();
173 assert_eq!(t, ts(0, 0));
174
175 let t = Timestamp::from_str("1970-01-01T00:00:00.123456789Z").unwrap();
177 assert_eq!(t, ts(0, 123_456_789));
178 }
179
180 #[test]
181 fn test_string_roundtrip() {
182 let t = ts(1_600_000_000, 123_000_000);
183 let s = t.to_string();
184 let back: Timestamp = s.parse().unwrap();
185 assert_eq!(t, back);
186 }
187
188 #[cfg(feature = "chrono")]
191 mod chrono_tests {
192 use super::*;
193 use chrono::{NaiveDate, NaiveDateTime, TimeZone, Utc};
194
195 #[test]
196 fn test_chrono_utc_roundtrip() {
197 let dt = Utc
199 .with_ymd_and_hms(2024, 1, 1, 12, 0, 0)
200 .unwrap();
201
202 let t: Timestamp = dt.into();
204 assert_eq!(t.seconds, 1_704_110_400);
205 assert_eq!(t.nanos, 0);
206
207 let back: chrono::DateTime<Utc> = t.try_into().unwrap();
209 assert_eq!(dt, back);
210 }
211
212 #[test]
213 fn test_chrono_naive_utc_assumption() {
214 let naive = NaiveDate::from_ymd_opt(2024, 1, 1)
216 .unwrap()
217 .and_hms_opt(12, 0, 0)
218 .unwrap();
219
220 let t: Timestamp = naive.into();
221
222 assert_eq!(t.seconds, 1_704_110_400);
224
225 let back_naive: NaiveDateTime = t.try_into().unwrap();
227 assert_eq!(naive, back_naive);
228 }
229
230 #[test]
231 fn test_fixed_offset_conversions() {
232 use chrono::{FixedOffset, TimeZone};
233
234 let dt_offset = FixedOffset::east_opt(5 * 3600)
237 .unwrap()
238 .with_ymd_and_hms(2024, 1, 1, 12, 0, 0)
239 .unwrap();
240
241 let t: Timestamp = dt_offset
242 .try_into()
243 .expect("Should convert with normalization");
244
245 assert_eq!(t.seconds, 1_704_092_400);
249 }
250 }
251}