rucksack_lib/
time.rs

1use chrono::offset::Local;
2use chrono::{DateTime, TimeZone, Utc};
3
4pub fn simple_timestamp() -> String {
5    format_datetime(chrono::offset::Local::now())
6}
7
8pub fn format_datetime(dt: DateTime<Local>) -> String {
9    dt.format("%Y%m%d-%H%M%S").to_string()
10}
11
12pub fn now() -> String {
13    Local::now().to_rfc3339()
14}
15
16pub fn epoch_to_string(e: i64) -> String {
17    Utc.timestamp_millis_opt(e).unwrap().to_rfc3339()
18}
19
20pub fn epoch_zero() -> String {
21    epoch_to_string(0)
22}
23
24pub fn string_to_epoch(stamp: String) -> i64 {
25    match DateTime::parse_from_rfc3339(&stamp) {
26        Ok(dt) => dt.timestamp_millis(),
27        Err(e) => {
28            log::debug!("{:?}", e);
29            Local::now().timestamp_millis()
30        }
31    }
32}
33
34#[cfg(test)]
35mod tests {
36    use super::*;
37
38    #[test]
39    fn test_epoch_zero() {
40        assert_eq!(super::epoch_zero(), "1970-01-01T00:00:00+00:00");
41    }
42
43    #[test]
44    fn test_epoch_to_string_zero() {
45        assert_eq!(epoch_to_string(0), "1970-01-01T00:00:00+00:00");
46    }
47
48    #[test]
49    fn test_epoch_to_string_positive() {
50        // 1000 milliseconds = 1 second after epoch
51        assert_eq!(epoch_to_string(1000), "1970-01-01T00:00:01+00:00");
52    }
53
54    #[test]
55    fn test_epoch_to_string_large() {
56        // January 1, 2020, 00:00:00 UTC
57        let timestamp = 1577836800000i64;
58        let result = epoch_to_string(timestamp);
59        assert!(result.starts_with("2020-01-01"));
60    }
61
62    #[test]
63    fn test_simple_timestamp_format() {
64        let timestamp = simple_timestamp();
65        // Should match format: YYYYMMDD-HHMMSS
66        assert_eq!(timestamp.len(), 15, "Timestamp should be 15 characters");
67        assert!(timestamp.contains('-'), "Timestamp should contain hyphen");
68
69        // Check that it's all digits except the hyphen
70        let parts: Vec<&str> = timestamp.split('-').collect();
71        assert_eq!(parts.len(), 2);
72        assert_eq!(parts[0].len(), 8, "Date part should be 8 digits");
73        assert_eq!(parts[1].len(), 6, "Time part should be 6 digits");
74        assert!(parts[0].chars().all(|c| c.is_ascii_digit()));
75        assert!(parts[1].chars().all(|c| c.is_ascii_digit()));
76    }
77
78    #[test]
79    fn test_format_datetime() {
80        // Create a known datetime
81        let dt = Local.with_ymd_and_hms(2023, 12, 25, 14, 30, 45).unwrap();
82        let formatted = format_datetime(dt);
83        assert_eq!(formatted, "20231225-143045");
84    }
85
86    #[test]
87    fn test_format_datetime_midnight() {
88        let dt = Local.with_ymd_and_hms(2000, 1, 1, 0, 0, 0).unwrap();
89        let formatted = format_datetime(dt);
90        assert_eq!(formatted, "20000101-000000");
91    }
92
93    #[test]
94    fn test_format_datetime_end_of_day() {
95        let dt = Local.with_ymd_and_hms(2023, 12, 31, 23, 59, 59).unwrap();
96        let formatted = format_datetime(dt);
97        assert_eq!(formatted, "20231231-235959");
98    }
99
100    #[test]
101    fn test_now_format() {
102        let timestamp = now();
103        // Should be RFC3339 format, which includes 'T' and timezone
104        assert!(timestamp.contains('T'), "RFC3339 should contain 'T'");
105        assert!(
106            timestamp.contains('+') || timestamp.contains('Z') || timestamp.contains('-'),
107            "RFC3339 should contain timezone indicator"
108        );
109    }
110
111    #[test]
112    fn test_string_to_epoch_valid_rfc3339() {
113        let valid_stamp = "2020-01-01T00:00:00+00:00".to_string();
114        let epoch = string_to_epoch(valid_stamp);
115        assert_eq!(epoch, 1577836800000);
116    }
117
118    #[test]
119    fn test_string_to_epoch_valid_with_z() {
120        let valid_stamp = "2020-01-01T00:00:00Z".to_string();
121        let epoch = string_to_epoch(valid_stamp);
122        assert_eq!(epoch, 1577836800000);
123    }
124
125    #[test]
126    fn test_string_to_epoch_invalid_format() {
127        let invalid_stamp = "not a timestamp".to_string();
128        let epoch = string_to_epoch(invalid_stamp);
129        // Should return current time, which will be > 0
130        assert!(epoch > 0, "Invalid format should return current timestamp");
131    }
132
133    #[test]
134    fn test_string_to_epoch_empty() {
135        let empty_stamp = "".to_string();
136        let epoch = string_to_epoch(empty_stamp);
137        // Should return current time
138        assert!(epoch > 0);
139    }
140
141    #[test]
142    fn test_string_to_epoch_partial_date() {
143        let partial = "2020-01-01".to_string();
144        let epoch = string_to_epoch(partial);
145        // Incomplete RFC3339 should fall back to current time
146        assert!(epoch > 0);
147    }
148
149    #[test]
150    fn test_epoch_roundtrip() {
151        let original_epoch = 1609459200000i64; // 2021-01-01 00:00:00 UTC
152        let timestamp_str = epoch_to_string(original_epoch);
153        let recovered_epoch = string_to_epoch(timestamp_str);
154        // Allow small difference due to timezone conversions
155        let difference = (original_epoch - recovered_epoch).abs();
156        assert!(
157            difference < 86400000, // Less than 24 hours difference
158            "Roundtrip should preserve epoch within timezone tolerance"
159        );
160    }
161
162    #[test]
163    fn test_simple_timestamp_uniqueness() {
164        // Two timestamps taken immediately after each other should be very similar
165        let ts1 = simple_timestamp();
166        let ts2 = simple_timestamp();
167        // They should have the same date part at minimum
168        assert_eq!(&ts1[0..8], &ts2[0..8], "Timestamps should have same date");
169    }
170
171    #[test]
172    fn epoch_zero() {
173        // Keep original test name for backwards compatibility
174        assert_eq!(super::epoch_zero(), "1970-01-01T00:00:00+00:00");
175    }
176}