Skip to main content

proto_types/timestamp/
timestamp_impls.rs

1use crate::{Duration, Timestamp};
2
3#[cfg(not(feature = "chrono"))]
4impl crate::Timestamp {
5	/// Returns the timestamp in YYYY-MM-DD format.
6	/// The same method, with the `chrono` feature, allows for custom formatting.
7	pub fn format(&self) -> crate::String {
8		use crate::ToString;
9
10		self.to_string()
11	}
12}
13
14#[cfg(feature = "chrono")]
15mod chrono {
16	use chrono::Utc;
17
18	use crate::{String, Timestamp, ToString, timestamp::TimestampError};
19
20	impl Timestamp {
21		/// Converts this timestamp into a [`chrono::DateTime<Utc>`] struct and calls .format on it with the string argument being given.
22		pub fn format(&self, string: &str) -> Result<String, TimestampError> {
23			let chrono_timestamp: chrono::DateTime<Utc> = (*self).try_into()?;
24
25			Ok(chrono_timestamp.format(string).to_string())
26		}
27
28		/// Converts this [`Timestamp`] instance to chrono::[`DateTime`](::chrono::DateTime) with [`chrono::Utc`].
29		#[inline]
30		pub fn as_datetime_utc(&self) -> Result<chrono::DateTime<Utc>, TimestampError> {
31			(*self).try_into()
32		}
33	}
34}
35
36impl Timestamp {
37	/// Creates a new instance.
38	#[must_use]
39	#[inline]
40	pub const fn new(seconds: i64, nanos: i32) -> Self {
41		Self { seconds, nanos }
42	}
43
44	/// Creates a new [`Timestamp`] from the given number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
45	///
46	/// This function handles both positive (post-1970) and negative (pre-1970) timestamps.
47	/// It ensures that the resulting timestamp is **normalized** according to Protobuf specifications,
48	/// meaning the `nanos` field will always be non-negative (0 to 999,999,999), adjusting the `seconds`
49	/// field accordingly.
50	///
51	/// # Examples
52	///
53	/// ```
54	/// use proto_types::Timestamp;
55	///
56	/// // Post-1970 (1.5 seconds)
57	/// let ts = Timestamp::from_unix_millis(1_500);
58	/// assert_eq!(ts.seconds, 1);
59	/// assert_eq!(ts.nanos, 500_000_000);
60	///
61	/// // Pre-1970 (-100ms)
62	/// // Represented as: The second *before* epoch (-1), plus 900ms forward.
63	/// let ts = Timestamp::from_unix_millis(-100);
64	/// assert_eq!(ts.seconds, -1);
65	/// assert_eq!(ts.nanos, 900_000_000);
66	/// ```
67	#[must_use]
68	pub fn from_unix_millis(millis: i64) -> Self {
69		let seconds = millis / 1000;
70
71		// SAFETY: millis % 1000 is max 999. 999 * 1_000_000 fits in i32.
72		#[allow(clippy::cast_possible_truncation)]
73		let nanos = ((millis % 1000) * 1_000_000) as i32;
74
75		let ts = Self { seconds, nanos };
76
77		ts.normalized()
78	}
79
80	/// Calculates the total number of milliseconds since the Unix epoch (1970-01-01T00:00:00Z).
81	///
82	/// This method automatically normalizes the timestamp before calculation to ensure correctness
83	/// for timestamps with negative nanoseconds (pre-1970 representation) or denormalized values.
84	///
85	/// Returns `None` if the calculation would overflow the [`i64`] range,
86	/// or if the timestamp itself cannot be normalized.
87	///
88	/// # Examples
89	///
90	/// ```
91	/// use proto_types::Timestamp;
92	///
93	/// // 1.5 seconds -> 1500 ms
94	/// let ts = Timestamp { seconds: 1, nanos: 500_000_000 };
95	/// assert_eq!(ts.checked_total_i64_millis(), Some(1_500));
96	///
97	/// // Pre-1970: -1s + 900ms = -100ms
98	/// let ts = Timestamp { seconds: -1, nanos: 900_000_000 };
99	/// assert_eq!(ts.checked_total_i64_millis(), Some(-100));
100	///
101	/// // Overflow check
102	/// let ts = Timestamp { seconds: i64::MAX, nanos: 0 };
103	/// assert_eq!(ts.checked_total_i64_millis(), None);
104	/// ```
105	#[must_use]
106	pub fn checked_total_i64_millis(&self) -> Option<i64> {
107		let ts = (*self).try_normalize().ok()?;
108
109		let seconds_part = ts.seconds.checked_mul(1000)?;
110		let nanos_part = i64::from(ts.nanos / 1_000_000);
111
112		seconds_part.checked_add(nanos_part)
113	}
114}
115
116#[cfg(all(not(feature = "std"), feature = "chrono-wasm"))]
117impl Timestamp {
118	/// Returns the current timestamp.
119	#[must_use]
120	#[inline]
121	pub fn now() -> Self {
122		::chrono::Utc::now().into()
123	}
124}
125
126#[cfg(feature = "std")]
127impl Timestamp {
128	/// Returns the current timestamp.
129	#[must_use]
130	#[inline]
131	pub fn now() -> Self {
132		std::time::SystemTime::now().into()
133	}
134}
135
136#[cfg(any(feature = "std", feature = "chrono-wasm"))]
137impl Timestamp {
138	/// Checks whether the Timestamp instance is within the indicated range (positive or negative) from now.
139	#[must_use]
140	#[inline]
141	pub fn is_within_range_from_now(&self, range: Duration) -> bool {
142		let now = Self::now();
143
144		(now + range) >= *self && (now - range) <= *self
145	}
146
147	/// Checks whether the Timestamp instance is within the indicated range in the future.
148	#[must_use]
149	#[inline]
150	pub fn is_within_future_range(&self, range: Duration) -> bool {
151		let now = Self::now();
152		let max = now + range;
153
154		*self <= max && *self >= now
155	}
156
157	/// Checks whether the Timestamp instance is within the indicated range in the past.
158	#[must_use]
159	#[inline]
160	pub fn is_within_past_range(&self, range: Duration) -> bool {
161		let now = Self::now();
162		let min = now - range;
163
164		*self >= min && *self <= now
165	}
166
167	/// Returns `true` if the timestamp is in the future.
168	#[must_use]
169	#[inline]
170	pub fn is_future(&self) -> bool {
171		*self > Self::now()
172	}
173
174	/// Returns `true` if the timestamp is in the past.
175	#[must_use]
176	#[inline]
177	pub fn is_past(&self) -> bool {
178		*self < Self::now()
179	}
180}
181
182#[cfg(test)]
183mod tests {
184	use super::*;
185
186	fn offset_seconds(base: &Timestamp, s: i64) -> Timestamp {
187		Timestamp {
188			seconds: base.seconds + s,
189			nanos: base.nanos,
190		}
191	}
192
193	#[test]
194	fn test_is_future() {
195		let now = Timestamp::now();
196
197		let future_point = offset_seconds(&now, 5);
198		let past_point = offset_seconds(&now, -5);
199
200		assert!(future_point.is_future(), "T + 5s should be in the future");
201		assert!(
202			!past_point.is_future(),
203			"T - 5s should NOT be in the future"
204		);
205	}
206
207	#[test]
208	fn test_is_past() {
209		let now = Timestamp::now();
210
211		let future_point = offset_seconds(&now, 5);
212		let past_point = offset_seconds(&now, -5);
213
214		assert!(past_point.is_past(), "T - 5s should be in the past");
215		assert!(!future_point.is_past(), "T + 5s should NOT be in the past");
216	}
217
218	#[test]
219	fn test_is_within_future_range() {
220		let now = Timestamp::now();
221		let range = Duration::new(10, 0);
222
223		let inside = offset_seconds(&now, 5);
224		assert!(
225			inside.is_within_future_range(range),
226			"5s is within 10s range"
227		);
228
229		let outside_far = offset_seconds(&now, 15);
230		assert!(
231			!outside_far.is_within_future_range(range),
232			"15s is outside 10s range"
233		);
234
235		let outside_past = offset_seconds(&now, -1);
236		assert!(
237			!outside_past.is_within_future_range(range),
238			"Past value is not in future range"
239		);
240	}
241
242	#[test]
243	fn test_is_within_past_range() {
244		let now = Timestamp::now();
245		let range = Duration::new(10, 0);
246
247		let inside = offset_seconds(&now, -5);
248		assert!(
249			inside.is_within_past_range(range),
250			"-5s is within 10s past range"
251		);
252
253		let outside_old = offset_seconds(&now, -15);
254		assert!(
255			!outside_old.is_within_past_range(range),
256			"-15s is too old for 10s range"
257		);
258
259		let outside_future = offset_seconds(&now, 1);
260		assert!(
261			!outside_future.is_within_past_range(range),
262			"Future value is not in past range"
263		);
264	}
265
266	#[test]
267	fn test_from_unix_millis_positive() {
268		let ts = Timestamp::from_unix_millis(1_000);
269		assert_eq!(ts.seconds, 1);
270		assert_eq!(ts.nanos, 0);
271
272		let ts = Timestamp::from_unix_millis(1_500);
273		assert_eq!(ts.seconds, 1);
274		assert_eq!(ts.nanos, 500_000_000);
275	}
276
277	#[test]
278	fn test_from_unix_millis_zero() {
279		let ts = Timestamp::from_unix_millis(0);
280		assert_eq!(ts.seconds, 0);
281		assert_eq!(ts.nanos, 0);
282	}
283
284	#[test]
285	fn test_from_unix_millis_negative() {
286		let ts = Timestamp::from_unix_millis(-1);
287		assert_eq!(ts.seconds, -1);
288		assert_eq!(ts.nanos, 999_000_000);
289
290		let ts = Timestamp::from_unix_millis(-100);
291		assert_eq!(ts.seconds, -1);
292		assert_eq!(ts.nanos, 900_000_000);
293
294		let ts = Timestamp::from_unix_millis(-1_500);
295		assert_eq!(ts.seconds, -2);
296		assert_eq!(ts.nanos, 500_000_000);
297	}
298
299	#[test]
300	fn test_round_trip_millis() {
301		let inputs = std::vec![0, 100, -100, 1_500, -1_500, 999, -999];
302
303		for input in inputs {
304			let ts = Timestamp::from_unix_millis(input);
305			let result = ts.checked_total_i64_millis().unwrap();
306			assert_eq!(input, result, "Round trip failed for {input}ms");
307		}
308	}
309
310	#[test]
311	fn test_total_millis_basic() {
312		let ts = Timestamp {
313			seconds: 1,
314			nanos: 500_000_000,
315		};
316		assert_eq!(ts.checked_total_i64_millis(), Some(1_500));
317	}
318
319	#[test]
320	fn test_total_millis_negative_normalization() {
321		// Checks that normalize() is called internally before math happens
322		let ts = Timestamp {
323			seconds: -1,
324			nanos: 500_000_000,
325		};
326		assert_eq!(ts.checked_total_i64_millis(), Some(-500));
327	}
328
329	#[test]
330	fn test_total_millis_overflow() {
331		let ts = Timestamp {
332			seconds: i64::MAX,
333			nanos: 0,
334		};
335		assert_eq!(ts.checked_total_i64_millis(), None);
336
337		let ts = Timestamp {
338			seconds: i64::MIN,
339			nanos: 0,
340		};
341		assert_eq!(ts.checked_total_i64_millis(), None);
342	}
343
344	#[test]
345	fn test_total_millis_boundary() {
346		let max_safe_seconds = i64::MAX / 1000;
347
348		let ts = Timestamp {
349			seconds: max_safe_seconds,
350			nanos: 0,
351		};
352		assert!(ts.checked_total_i64_millis().is_some());
353
354		let ts_overflow = Timestamp {
355			seconds: max_safe_seconds + 1,
356			nanos: 0,
357		};
358		assert!(ts_overflow.checked_total_i64_millis().is_none());
359	}
360}