Skip to main content

fd_core/
time.rs

1//! Time utilities for FerrumDeck
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6/// Get current UTC timestamp
7pub fn now() -> DateTime<Utc> {
8    Utc::now()
9}
10
11/// Timestamp wrapper for consistent serialization
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(transparent)]
14pub struct Timestamp(DateTime<Utc>);
15
16impl Timestamp {
17    /// Create a new timestamp for now
18    pub fn now() -> Self {
19        Self(Utc::now())
20    }
21
22    /// Create from a DateTime
23    pub fn from_datetime(dt: DateTime<Utc>) -> Self {
24        Self(dt)
25    }
26
27    /// Get the inner DateTime
28    pub fn inner(&self) -> DateTime<Utc> {
29        self.0
30    }
31
32    /// Get milliseconds since epoch
33    pub fn millis(&self) -> i64 {
34        self.0.timestamp_millis()
35    }
36
37    /// Create from milliseconds since epoch
38    pub fn from_millis(millis: i64) -> Option<Self> {
39        DateTime::from_timestamp_millis(millis).map(Self)
40    }
41}
42
43impl Default for Timestamp {
44    fn default() -> Self {
45        Self::now()
46    }
47}
48
49impl std::fmt::Display for Timestamp {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        write!(f, "{}", self.0.to_rfc3339())
52    }
53}
54
55impl From<DateTime<Utc>> for Timestamp {
56    fn from(dt: DateTime<Utc>) -> Self {
57        Self(dt)
58    }
59}
60
61impl From<Timestamp> for DateTime<Utc> {
62    fn from(ts: Timestamp) -> Self {
63        ts.0
64    }
65}
66
67/// Duration in milliseconds (for budgets, timeouts)
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(transparent)]
70pub struct DurationMs(u64);
71
72impl DurationMs {
73    pub fn new(ms: u64) -> Self {
74        Self(ms)
75    }
76
77    pub fn from_secs(secs: u64) -> Self {
78        Self(secs * 1000)
79    }
80
81    pub fn from_mins(mins: u64) -> Self {
82        Self(mins * 60 * 1000)
83    }
84
85    pub fn as_millis(&self) -> u64 {
86        self.0
87    }
88
89    pub fn as_secs(&self) -> u64 {
90        self.0 / 1000
91    }
92
93    pub fn as_std(&self) -> std::time::Duration {
94        std::time::Duration::from_millis(self.0)
95    }
96}
97
98impl From<u64> for DurationMs {
99    fn from(ms: u64) -> Self {
100        Self(ms)
101    }
102}
103
104impl From<std::time::Duration> for DurationMs {
105    fn from(d: std::time::Duration) -> Self {
106        Self(d.as_millis() as u64)
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    // ============================================================
115    // CORE-TIME-001: Timestamp creation and manipulation
116    // ============================================================
117
118    #[test]
119    fn test_timestamp_now_creates_current_time() {
120        let before = Utc::now();
121        let ts = Timestamp::now();
122        let after = Utc::now();
123
124        assert!(ts.inner() >= before);
125        assert!(ts.inner() <= after);
126    }
127
128    #[test]
129    fn test_timestamp_from_datetime() {
130        let dt = Utc::now();
131        let ts = Timestamp::from_datetime(dt);
132        assert_eq!(ts.inner(), dt);
133    }
134
135    #[test]
136    fn test_timestamp_inner_returns_datetime() {
137        let dt = Utc::now();
138        let ts = Timestamp::from_datetime(dt);
139        assert_eq!(ts.inner(), dt);
140    }
141
142    #[test]
143    fn test_timestamp_millis_conversion() {
144        let ts = Timestamp::now();
145        let millis = ts.millis();
146        assert!(millis > 0);
147    }
148
149    #[test]
150    fn test_timestamp_from_millis() {
151        let ts = Timestamp::now();
152        let millis = ts.millis();
153        let ts2 = Timestamp::from_millis(millis).unwrap();
154        // Compare milliseconds since we lose sub-millisecond precision
155        assert_eq!(ts.millis(), ts2.millis());
156    }
157
158    #[test]
159    fn test_timestamp_from_invalid_millis_returns_none() {
160        // Very negative values might not be valid
161        let result = Timestamp::from_millis(i64::MIN);
162        // This depends on chrono's behavior for extreme values
163        // Just verify it doesn't panic
164        let _ = result;
165    }
166
167    #[test]
168    fn test_timestamp_default_creates_now() {
169        let before = Utc::now();
170        let ts = Timestamp::default();
171        let after = Utc::now();
172
173        assert!(ts.inner() >= before);
174        assert!(ts.inner() <= after);
175    }
176
177    // ============================================================
178    // CORE-TIME-002: Timestamp traits
179    // ============================================================
180
181    #[test]
182    fn test_timestamp_display_is_rfc3339() {
183        let ts = Timestamp::now();
184        let display = ts.to_string();
185        // RFC3339 format has 'T' separator and timezone
186        assert!(display.contains('T'));
187        assert!(display.ends_with('Z') || display.contains('+') || display.contains('-'));
188    }
189
190    #[test]
191    fn test_timestamp_from_datetime_trait() {
192        let dt = Utc::now();
193        let ts: Timestamp = dt.into();
194        assert_eq!(ts.inner(), dt);
195    }
196
197    #[test]
198    fn test_timestamp_into_datetime_trait() {
199        let ts = Timestamp::now();
200        let dt: DateTime<Utc> = ts.into();
201        assert_eq!(dt, ts.inner());
202    }
203
204    #[test]
205    #[allow(clippy::clone_on_copy)]
206    fn test_timestamp_is_cloneable() {
207        let ts = Timestamp::now();
208        let cloned = ts.clone();
209        assert_eq!(ts, cloned);
210    }
211
212    #[test]
213    fn test_timestamp_is_copyable() {
214        let ts = Timestamp::now();
215        let copied = ts;
216        assert_eq!(ts, copied);
217    }
218
219    #[test]
220    fn test_timestamp_equality() {
221        let dt = Utc::now();
222        let ts1 = Timestamp::from_datetime(dt);
223        let ts2 = Timestamp::from_datetime(dt);
224        assert_eq!(ts1, ts2);
225    }
226
227    #[test]
228    fn test_timestamp_is_debuggable() {
229        let ts = Timestamp::now();
230        let debug = format!("{:?}", ts);
231        assert!(debug.contains("Timestamp"));
232    }
233
234    // ============================================================
235    // CORE-TIME-003: Timestamp serialization
236    // ============================================================
237
238    #[test]
239    fn test_timestamp_serializes_to_string() {
240        let ts = Timestamp::now();
241        let json = serde_json::to_string(&ts).unwrap();
242        // Should serialize as a string (RFC3339)
243        assert!(json.starts_with('"'));
244        assert!(json.ends_with('"'));
245    }
246
247    #[test]
248    fn test_timestamp_deserializes_from_string() {
249        let json = r#""2024-01-15T10:30:00Z""#;
250        let ts: Timestamp = serde_json::from_str(json).unwrap();
251        assert_eq!(ts.inner().year(), 2024);
252        assert_eq!(ts.inner().month(), 1);
253        assert_eq!(ts.inner().day(), 15);
254    }
255
256    #[test]
257    fn test_timestamp_roundtrip_serialization() {
258        let ts = Timestamp::now();
259        let json = serde_json::to_string(&ts).unwrap();
260        let ts2: Timestamp = serde_json::from_str(&json).unwrap();
261        // Compare at millisecond precision
262        assert_eq!(ts.millis(), ts2.millis());
263    }
264
265    // ============================================================
266    // CORE-TIME-004: DurationMs creation
267    // ============================================================
268
269    #[test]
270    fn test_duration_ms_new() {
271        let d = DurationMs::new(5000);
272        assert_eq!(d.as_millis(), 5000);
273    }
274
275    #[test]
276    fn test_duration_ms_from_secs() {
277        let d = DurationMs::from_secs(5);
278        assert_eq!(d.as_millis(), 5000);
279    }
280
281    #[test]
282    fn test_duration_ms_from_mins() {
283        let d = DurationMs::from_mins(2);
284        assert_eq!(d.as_millis(), 120000);
285    }
286
287    #[test]
288    fn test_duration_ms_as_millis() {
289        let d = DurationMs::new(12345);
290        assert_eq!(d.as_millis(), 12345);
291    }
292
293    #[test]
294    fn test_duration_ms_as_secs() {
295        let d = DurationMs::new(5500);
296        assert_eq!(d.as_secs(), 5); // Truncates
297    }
298
299    #[test]
300    fn test_duration_ms_as_std() {
301        let d = DurationMs::new(5000);
302        let std_d = d.as_std();
303        assert_eq!(std_d.as_millis(), 5000);
304    }
305
306    // ============================================================
307    // CORE-TIME-005: DurationMs traits
308    // ============================================================
309
310    #[test]
311    fn test_duration_ms_from_u64() {
312        let d: DurationMs = 3000u64.into();
313        assert_eq!(d.as_millis(), 3000);
314    }
315
316    #[test]
317    fn test_duration_ms_from_std_duration() {
318        let std_d = std::time::Duration::from_millis(2500);
319        let d: DurationMs = std_d.into();
320        assert_eq!(d.as_millis(), 2500);
321    }
322
323    #[test]
324    #[allow(clippy::clone_on_copy)]
325    fn test_duration_ms_is_cloneable() {
326        let d = DurationMs::new(1000);
327        let cloned = d.clone();
328        assert_eq!(d, cloned);
329    }
330
331    #[test]
332    fn test_duration_ms_is_copyable() {
333        let d = DurationMs::new(1000);
334        let copied = d;
335        assert_eq!(d, copied);
336    }
337
338    #[test]
339    fn test_duration_ms_equality() {
340        let d1 = DurationMs::new(1000);
341        let d2 = DurationMs::new(1000);
342        assert_eq!(d1, d2);
343    }
344
345    #[test]
346    fn test_duration_ms_inequality() {
347        let d1 = DurationMs::new(1000);
348        let d2 = DurationMs::new(2000);
349        assert_ne!(d1, d2);
350    }
351
352    #[test]
353    fn test_duration_ms_is_debuggable() {
354        let d = DurationMs::new(1000);
355        let debug = format!("{:?}", d);
356        assert!(debug.contains("DurationMs"));
357    }
358
359    // ============================================================
360    // CORE-TIME-006: DurationMs serialization
361    // ============================================================
362
363    #[test]
364    fn test_duration_ms_serializes_to_number() {
365        let d = DurationMs::new(5000);
366        let json = serde_json::to_string(&d).unwrap();
367        assert_eq!(json, "5000");
368    }
369
370    #[test]
371    fn test_duration_ms_deserializes_from_number() {
372        let json = "3000";
373        let d: DurationMs = serde_json::from_str(json).unwrap();
374        assert_eq!(d.as_millis(), 3000);
375    }
376
377    #[test]
378    fn test_duration_ms_roundtrip_serialization() {
379        let d = DurationMs::new(7500);
380        let json = serde_json::to_string(&d).unwrap();
381        let d2: DurationMs = serde_json::from_str(&json).unwrap();
382        assert_eq!(d, d2);
383    }
384
385    // ============================================================
386    // CORE-TIME-007: now() function
387    // ============================================================
388
389    #[test]
390    fn test_now_returns_current_time() {
391        let before = Utc::now();
392        let result = now();
393        let after = Utc::now();
394
395        assert!(result >= before);
396        assert!(result <= after);
397    }
398
399    #[test]
400    fn test_now_is_monotonic() {
401        let t1 = now();
402        let t2 = now();
403        assert!(t2 >= t1);
404    }
405
406    // ============================================================
407    // CORE-TIME-008: Edge cases
408    // ============================================================
409
410    #[test]
411    fn test_duration_ms_zero() {
412        let d = DurationMs::new(0);
413        assert_eq!(d.as_millis(), 0);
414        assert_eq!(d.as_secs(), 0);
415    }
416
417    #[test]
418    fn test_duration_ms_large_value() {
419        let d = DurationMs::new(u64::MAX);
420        assert_eq!(d.as_millis(), u64::MAX);
421    }
422
423    #[test]
424    fn test_timestamp_epoch() {
425        let ts = Timestamp::from_millis(0).unwrap();
426        assert_eq!(ts.millis(), 0);
427    }
428
429    use chrono::Datelike;
430}