chie_shared/utils/
time.rs

1//! Time and timestamp utility functions.
2
3use chrono::Utc;
4
5/// Get current Unix timestamp in milliseconds.
6#[inline]
7pub fn now_ms() -> i64 {
8    Utc::now().timestamp_millis()
9}
10
11/// Get current Unix timestamp in seconds.
12#[inline]
13pub fn now_secs() -> i64 {
14    Utc::now().timestamp()
15}
16
17/// Convert milliseconds to seconds.
18#[inline]
19pub fn ms_to_secs(ms: i64) -> i64 {
20    ms / 1000
21}
22
23/// Convert seconds to milliseconds.
24#[inline]
25pub fn secs_to_ms(secs: i64) -> i64 {
26    secs * 1000
27}
28
29/// Check if a timestamp is within the valid range (not too old, not in future).
30///
31/// # Examples
32///
33/// ```
34/// use chie_shared::{now_ms, is_timestamp_valid};
35///
36/// // Recent timestamp is valid
37/// let recent = now_ms() - 1000; // 1 second ago
38/// assert!(is_timestamp_valid(recent, 5000)); // 5 second tolerance
39///
40/// // Old timestamp is invalid
41/// let old = now_ms() - 10000; // 10 seconds ago
42/// assert!(!is_timestamp_valid(old, 5000)); // Only 5 second tolerance
43///
44/// // Future timestamp is always invalid
45/// let future = now_ms() + 1000;
46/// assert!(!is_timestamp_valid(future, 5000));
47/// ```
48#[inline]
49pub fn is_timestamp_valid(timestamp_ms: i64, tolerance_ms: i64) -> bool {
50    let now = now_ms();
51    timestamp_ms <= now && (now - timestamp_ms) <= tolerance_ms
52}
53
54/// Format Unix timestamp (milliseconds) as human-readable string.
55/// Returns format like "2024-12-16 14:30:45 UTC".
56pub fn format_timestamp(timestamp_ms: i64) -> String {
57    use chrono::{DateTime, Utc};
58
59    if let Some(dt) = DateTime::<Utc>::from_timestamp(
60        timestamp_ms / 1000,
61        ((timestamp_ms % 1000) * 1_000_000) as u32,
62    ) {
63        dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
64    } else {
65        "Invalid timestamp".to_string()
66    }
67}
68
69/// Parse human-readable duration string to milliseconds.
70/// Supports formats like "1h30m", "45s", "2h", "500ms".
71/// Returns None if the format is invalid.
72///
73/// # Examples
74///
75/// ```
76/// use chie_shared::parse_duration_str;
77///
78/// // Parse various duration formats
79/// assert_eq!(parse_duration_str("500ms"), Some(500));
80/// assert_eq!(parse_duration_str("5s"), Some(5000));
81/// assert_eq!(parse_duration_str("2m"), Some(120_000));
82/// assert_eq!(parse_duration_str("1h"), Some(3_600_000));
83///
84/// // Combined durations
85/// assert_eq!(parse_duration_str("1h30m"), Some(5_400_000));
86/// assert_eq!(parse_duration_str("2h15m30s"), Some(8_130_000));
87///
88/// // Invalid formats return None
89/// assert_eq!(parse_duration_str("invalid"), None);
90/// assert_eq!(parse_duration_str("10"), None); // Missing unit
91/// ```
92pub fn parse_duration_str(s: &str) -> Option<u64> {
93    let s = s.trim().to_lowercase();
94    if s.is_empty() {
95        return None;
96    }
97
98    // Handle pure milliseconds (e.g., "500ms")
99    if s.ends_with("ms") {
100        return s.strip_suffix("ms")?.trim().parse().ok();
101    }
102
103    let mut total_ms = 0u64;
104    let mut current_num = String::new();
105
106    for ch in s.chars() {
107        if ch.is_ascii_digit() {
108            current_num.push(ch);
109        } else if !current_num.is_empty() {
110            let num: u64 = current_num.parse().ok()?;
111            current_num.clear();
112
113            let multiplier = match ch {
114                'd' => 24 * 60 * 60 * 1000, // days
115                'h' => 60 * 60 * 1000,      // hours
116                'm' => 60 * 1000,           // minutes
117                's' => 1000,                // seconds
118                _ => return None,
119            };
120
121            total_ms = total_ms.checked_add(num.checked_mul(multiplier)?)?;
122        }
123    }
124
125    if total_ms == 0 { None } else { Some(total_ms) }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_timestamp_conversion() {
134        let secs = 1000;
135        let ms = secs_to_ms(secs);
136        assert_eq!(ms, 1_000_000);
137        assert_eq!(ms_to_secs(ms), secs);
138    }
139
140    #[test]
141    fn test_timestamp_validation() {
142        let now = now_ms();
143        assert!(is_timestamp_valid(now, 1000));
144        assert!(is_timestamp_valid(now - 500, 1000));
145        assert!(!is_timestamp_valid(now + 1000, 1000));
146        assert!(!is_timestamp_valid(now - 2000, 1000));
147    }
148
149    #[test]
150    fn test_format_timestamp() {
151        // Test a known timestamp
152        let ts = 1_702_742_445_000_i64; // 2023-12-16 14:00:45
153        let formatted = format_timestamp(ts);
154        assert!(formatted.contains("2023-12-16"));
155        assert!(formatted.contains("UTC"));
156
157        // Test invalid timestamp
158        let invalid = format_timestamp(i64::MAX);
159        assert_eq!(invalid, "Invalid timestamp");
160    }
161
162    #[test]
163    fn test_parse_duration_str() {
164        assert_eq!(parse_duration_str("500ms"), Some(500));
165        assert_eq!(parse_duration_str("5s"), Some(5000));
166        assert_eq!(parse_duration_str("2m"), Some(120_000));
167        assert_eq!(parse_duration_str("1h"), Some(3_600_000));
168        assert_eq!(parse_duration_str("1d"), Some(86_400_000));
169        assert_eq!(parse_duration_str("1h30m"), Some(5_400_000));
170        assert_eq!(parse_duration_str("2h15m30s"), Some(8_130_000));
171        assert_eq!(parse_duration_str(""), None);
172        assert_eq!(parse_duration_str("invalid"), None);
173        assert_eq!(parse_duration_str("10"), None); // No unit
174    }
175}