Skip to main content

proto_types/timestamp/
timestamp_conversions.rs

1#[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				// Safe casting as this value is limited by chrono
13				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				// Safe casting as this value is limited by chrono
26				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	// --- 1. SystemTime Conversions ---
109
110	#[test]
111	fn test_system_time_epoch() {
112		// 1. Exact Epoch
113		let t: Timestamp = UNIX_EPOCH.into();
114		assert_eq!(t, ts(0, 0));
115
116		// 2. Roundtrip Epoch
117		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		// SystemTime -> Timestamp
126		let t: Timestamp = now.into();
127
128		// Timestamp -> SystemTime
129		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		// 1969-12-31 23:59:59
142		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	// --- 2. String Parsing & Formatting (RFC 3339) ---
152
153	#[test]
154	fn test_display_rfc3339() {
155		// 1. Epoch
156		assert_eq!(ts(0, 0).to_string(), "1970-01-01T00:00:00Z");
157
158		// 2. Nanos (Standard trimming logic expected)
159		// 0.5s
160		assert_eq!(ts(0, 500_000_000).to_string(), "1970-01-01T00:00:00.5Z");
161
162		// 3. Pre-Epoch
163		// 1969-12-31T23:59:59Z
164		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		// 1. Basic
172		let t = Timestamp::from_str("1970-01-01T00:00:00Z").unwrap();
173		assert_eq!(t, ts(0, 0));
174
175		// 2. With Nanos
176		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	// --- 3. Chrono Integrations ---
189
190	#[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			// 2024-01-01 12:00:00 UTC
198			let dt = Utc
199				.with_ymd_and_hms(2024, 1, 1, 12, 0, 0)
200				.unwrap();
201
202			// Into Timestamp
203			let t: Timestamp = dt.into();
204			assert_eq!(t.seconds, 1_704_110_400);
205			assert_eq!(t.nanos, 0);
206
207			// Back to Chrono
208			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			// NaiveDateTime is assumed to be UTC when converting to Timestamp
215			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			// Should match the UTC seconds from above
223			assert_eq!(t.seconds, 1_704_110_400);
224
225			// Roundtrip back
226			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			// 1. Success: Offset +05:00
235			// 2024-01-01 12:00:00 +05:00 == 07:00:00 UTC
236			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			// 12:00 local - 5h offset = 07:00 UTC.
246			// Timestamp for 2024-01-01 07:00:00 UTC is 1704092400.
247			// (1704110400 is 12:00 UTC)
248			assert_eq!(t.seconds, 1_704_092_400);
249		}
250	}
251}