proto_types/timestamp/
timestamp_impls.rs1use crate::{Duration, Timestamp};
2
3#[cfg(not(feature = "chrono"))]
4impl crate::Timestamp {
5 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 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 #[inline]
30 pub fn as_datetime_utc(&self) -> Result<chrono::DateTime<Utc>, TimestampError> {
31 (*self).try_into()
32 }
33 }
34}
35
36impl Timestamp {
37 #[must_use]
39 #[inline]
40 pub const fn new(seconds: i64, nanos: i32) -> Self {
41 Self { seconds, nanos }
42 }
43
44 #[must_use]
68 pub fn from_unix_millis(millis: i64) -> Self {
69 let seconds = millis / 1000;
70
71 #[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 #[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 #[must_use]
120 #[inline]
121 pub fn now() -> Self {
122 ::chrono::Utc::now().into()
123 }
124}
125
126#[cfg(feature = "std")]
127impl Timestamp {
128 #[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 #[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 #[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 #[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 #[must_use]
169 #[inline]
170 pub fn is_future(&self) -> bool {
171 *self > Self::now()
172 }
173
174 #[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 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}