Skip to main content

vct_core/utils/
time.rs

1//! Timestamp parsing helpers for the ISO-8601 / RFC 3339 strings that the
2//! provider session logs embed in each record.
3
4use chrono::{DateTime, SecondsFormat, Utc};
5
6/// Current UTC time as RFC3339 with nanoseconds and a `Z` suffix
7/// (e.g. `2026-07-07T05:34:50.563606999Z`).
8///
9/// Matches the format Codex writes for `auth.json`'s `last_refresh` and the
10/// stamp the quota version caches / self `version.json` record use.
11pub fn now_rfc3339_utc_nanos() -> String {
12    Utc::now().to_rfc3339_opts(SecondsFormat::Nanos, true)
13}
14
15/// Parses an ISO-8601 / RFC 3339 timestamp into Unix milliseconds.
16///
17/// RFC 3339 is tried first (the common case, with timezone offset or `Z`),
18/// then a small set of explicit UTC fallback patterns covering 3-digit,
19/// arbitrary-precision, and zero-fraction sub-seconds. Surrounding
20/// whitespace is *not* tolerated.
21///
22/// Returns `0` for an empty string or any input that matches none of the
23/// accepted formats; callers treat `0` as "unknown time" rather than the
24/// Unix epoch.
25///
26/// # Examples
27///
28/// ```
29/// use vct_core::utils::parse_iso_timestamp;
30///
31/// assert_eq!(parse_iso_timestamp("1970-01-01T00:00:01Z"), 1_000);
32/// assert_eq!(parse_iso_timestamp(""), 0);
33/// assert_eq!(parse_iso_timestamp("not a timestamp"), 0);
34/// ```
35pub fn parse_iso_timestamp(ts: &str) -> i64 {
36    if ts.is_empty() {
37        return 0;
38    }
39
40    // Try RFC3339 first (most common format)
41    if let Ok(dt) = DateTime::parse_from_rfc3339(ts) {
42        return dt.timestamp_millis();
43    }
44
45    // Try other formats
46    let formats = [
47        "%Y-%m-%dT%H:%M:%S%.3fZ",
48        "%Y-%m-%dT%H:%M:%S%.fZ",
49        "%Y-%m-%dT%H:%M:%SZ",
50    ];
51
52    for format in &formats {
53        if let Ok(dt) = DateTime::parse_from_str(ts, format) {
54            return dt.timestamp_millis();
55        }
56    }
57
58    0
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn test_parse_iso_timestamp_rfc3339() {
67        // Test parsing RFC3339 format (most common)
68        let ts = "2024-01-15T10:30:45.123Z";
69        let result = parse_iso_timestamp(ts);
70        assert!(result > 0);
71
72        // Should parse to a valid timestamp (2024)
73        assert!(result > 1_700_000_000_000); // After 2023
74        assert!(result < 1_800_000_000_000); // Before 2027
75    }
76
77    #[test]
78    fn test_parse_iso_timestamp_with_timezone() {
79        // Test parsing with timezone offset
80        let ts = "2024-01-15T10:30:45.123+08:00";
81        let result = parse_iso_timestamp(ts);
82        assert!(result > 0);
83    }
84
85    #[test]
86    fn test_parse_iso_timestamp_no_millis() {
87        // Test parsing without milliseconds
88        let ts = "2024-01-15T10:30:45Z";
89        let result = parse_iso_timestamp(ts);
90        assert!(result > 0);
91    }
92
93    #[test]
94    fn test_parse_iso_timestamp_fallback_formats() {
95        // Test fallback format with milliseconds
96        let ts1 = "2024-01-15T10:30:45.123Z";
97        let result1 = parse_iso_timestamp(ts1);
98        assert!(result1 > 0);
99
100        // Test fallback format with fractional seconds
101        let ts2 = "2024-01-15T10:30:45.123456Z";
102        let result2 = parse_iso_timestamp(ts2);
103        assert!(result2 > 0);
104
105        // Test fallback format without fractional seconds
106        let ts3 = "2024-01-15T10:30:45Z";
107        let result3 = parse_iso_timestamp(ts3);
108        assert!(result3 > 0);
109    }
110
111    #[test]
112    fn test_parse_iso_timestamp_empty() {
113        // Test parsing empty string
114        let result = parse_iso_timestamp("");
115        assert_eq!(result, 0);
116    }
117
118    #[test]
119    fn test_parse_iso_timestamp_invalid() {
120        // Test parsing invalid format
121        let result = parse_iso_timestamp("not a timestamp");
122        assert_eq!(result, 0);
123
124        let result = parse_iso_timestamp("2024-13-45");
125        assert_eq!(result, 0);
126
127        let result = parse_iso_timestamp("invalid-date-time");
128        assert_eq!(result, 0);
129    }
130
131    #[test]
132    fn test_parse_iso_timestamp_different_years() {
133        // Test different years to ensure parsing is consistent
134        let ts_2020 = "2020-06-15T12:00:00Z";
135        let ts_2024 = "2024-06-15T12:00:00Z";
136
137        let result_2020 = parse_iso_timestamp(ts_2020);
138        let result_2024 = parse_iso_timestamp(ts_2024);
139
140        assert!(result_2020 > 0);
141        assert!(result_2024 > 0);
142        assert!(result_2024 > result_2020);
143    }
144
145    #[test]
146    fn test_parse_iso_timestamp_milliseconds_precision() {
147        // Test that milliseconds are preserved
148        let ts1 = "2024-01-15T10:30:45.000Z";
149        let ts2 = "2024-01-15T10:30:45.999Z";
150
151        let result1 = parse_iso_timestamp(ts1);
152        let result2 = parse_iso_timestamp(ts2);
153
154        assert!(result1 > 0);
155        assert!(result2 > 0);
156        // Should be ~999ms apart
157        assert!(result2 > result1);
158        assert!(result2 - result1 < 1000);
159    }
160
161    #[test]
162    fn test_parse_iso_timestamp_same_time() {
163        // Test parsing the same timestamp twice
164        let ts = "2024-01-15T10:30:45.123Z";
165        let result1 = parse_iso_timestamp(ts);
166        let result2 = parse_iso_timestamp(ts);
167
168        assert_eq!(result1, result2);
169    }
170
171    #[test]
172    fn test_parse_iso_timestamp_edge_cases() {
173        // Test edge cases
174
175        // Beginning of year
176        let ts1 = "2024-01-01T00:00:00Z";
177        let result1 = parse_iso_timestamp(ts1);
178        assert!(result1 > 0);
179
180        // End of year
181        let ts2 = "2024-12-31T23:59:59Z";
182        let result2 = parse_iso_timestamp(ts2);
183        assert!(result2 > 0);
184        assert!(result2 > result1);
185
186        // Leap year day
187        let ts3 = "2024-02-29T12:00:00Z";
188        let result3 = parse_iso_timestamp(ts3);
189        assert!(result3 > 0);
190    }
191
192    #[test]
193    fn test_parse_iso_timestamp_negative_timezone() {
194        // Test parsing with negative timezone offset
195        let ts = "2024-01-15T10:30:45.123-05:00";
196        let result = parse_iso_timestamp(ts);
197        assert!(result > 0);
198    }
199
200    #[test]
201    fn test_parse_iso_timestamp_midnight() {
202        // Test midnight timestamps
203        let ts = "2024-01-15T00:00:00.000Z";
204        let result = parse_iso_timestamp(ts);
205        assert!(result > 0);
206    }
207
208    #[test]
209    fn test_parse_iso_timestamp_noon() {
210        // Test noon timestamps
211        let ts = "2024-01-15T12:00:00.000Z";
212        let result = parse_iso_timestamp(ts);
213        assert!(result > 0);
214    }
215
216    #[test]
217    fn test_parse_iso_timestamp_whitespace() {
218        // Test that whitespace is not tolerated
219        let result = parse_iso_timestamp(" 2024-01-15T10:30:45Z ");
220        assert_eq!(result, 0);
221    }
222
223    #[test]
224    fn test_parse_iso_timestamp_partial() {
225        // Test partial timestamps (invalid)
226        let result = parse_iso_timestamp("2024-01-15");
227        assert_eq!(result, 0);
228
229        let result = parse_iso_timestamp("2024-01-15T10:30");
230        assert_eq!(result, 0);
231    }
232
233    #[test]
234    fn test_parse_iso_timestamp_ordering() {
235        // Test that timestamps maintain proper ordering
236        let timestamps = [
237            "2024-01-15T10:00:00Z",
238            "2024-01-15T11:00:00Z",
239            "2024-01-15T12:00:00Z",
240            "2024-01-15T13:00:00Z",
241        ];
242
243        let results: Vec<i64> = timestamps
244            .iter()
245            .map(|ts| parse_iso_timestamp(ts))
246            .collect();
247
248        // All should be non-zero
249        assert!(results.iter().all(|&r| r > 0));
250
251        // Should be in ascending order
252        for i in 1..results.len() {
253            assert!(results[i] > results[i - 1]);
254        }
255    }
256}