raz_common/
time.rs

1//! Time and duration utilities
2
3use chrono::{DateTime, Local, Utc};
4use std::time::{Duration, SystemTime};
5
6/// Time utilities for consistent time handling
7pub struct TimeUtils;
8
9impl TimeUtils {
10    /// Get current time in UTC
11    pub fn now_utc() -> DateTime<Utc> {
12        Utc::now()
13    }
14
15    /// Get current time in local timezone
16    pub fn now_local() -> DateTime<Local> {
17        Local::now()
18    }
19
20    /// Format a duration in a human-readable way
21    pub fn format_duration(duration: Duration) -> String {
22        let secs = duration.as_secs();
23
24        if secs < 60 {
25            format!("{secs}s")
26        } else if secs < 3600 {
27            let mins = secs / 60;
28            let secs = secs % 60;
29            if secs > 0 {
30                format!("{mins}m {secs}s")
31            } else {
32                format!("{mins}m")
33            }
34        } else {
35            let hours = secs / 3600;
36            let mins = (secs % 3600) / 60;
37            if mins > 0 {
38                format!("{hours}h {mins}m")
39            } else {
40                format!("{hours}h")
41            }
42        }
43    }
44
45    /// Format a timestamp in a consistent way
46    pub fn format_timestamp(time: DateTime<Local>) -> String {
47        time.format("%Y-%m-%d %H:%M:%S").to_string()
48    }
49
50    /// Format a timestamp as relative time (e.g., "2 hours ago")
51    pub fn format_relative(time: DateTime<Local>) -> String {
52        let now = Local::now();
53        let duration = now.signed_duration_since(time);
54
55        if duration.num_seconds() < 0 {
56            return "in the future".to_string();
57        }
58
59        if duration.num_seconds() < 60 {
60            "just now".to_string()
61        } else if duration.num_minutes() < 60 {
62            let mins = duration.num_minutes();
63            format!("{} minute{} ago", mins, if mins == 1 { "" } else { "s" })
64        } else if duration.num_hours() < 24 {
65            let hours = duration.num_hours();
66            format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" })
67        } else if duration.num_days() < 30 {
68            let days = duration.num_days();
69            format!("{} day{} ago", days, if days == 1 { "" } else { "s" })
70        } else {
71            time.format("%Y-%m-%d").to_string()
72        }
73    }
74
75    /// Convert SystemTime to DateTime<Local>
76    pub fn system_time_to_local(time: SystemTime) -> DateTime<Local> {
77        let datetime: DateTime<Utc> = time.into();
78        datetime.with_timezone(&Local)
79    }
80
81    /// Parse ISO 8601 timestamp
82    pub fn parse_iso8601(s: &str) -> Result<DateTime<Utc>, chrono::ParseError> {
83        DateTime::parse_from_rfc3339(s).map(|dt| dt.with_timezone(&Utc))
84    }
85}
86
87/// Extension trait for measuring elapsed time
88pub trait Elapsed {
89    /// Get elapsed time since this instant
90    fn elapsed_formatted(&self) -> String;
91}
92
93impl Elapsed for std::time::Instant {
94    fn elapsed_formatted(&self) -> String {
95        TimeUtils::format_duration(self.elapsed())
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn test_format_duration() {
105        assert_eq!(TimeUtils::format_duration(Duration::from_secs(45)), "45s");
106        assert_eq!(
107            TimeUtils::format_duration(Duration::from_secs(90)),
108            "1m 30s"
109        );
110        assert_eq!(TimeUtils::format_duration(Duration::from_secs(3600)), "1h");
111        assert_eq!(
112            TimeUtils::format_duration(Duration::from_secs(3750)),
113            "1h 2m"
114        );
115    }
116
117    #[test]
118    fn test_format_relative() {
119        let now = Local::now();
120        assert_eq!(TimeUtils::format_relative(now), "just now");
121
122        let two_hours_ago = now - chrono::Duration::hours(2);
123        assert_eq!(TimeUtils::format_relative(two_hours_ago), "2 hours ago");
124    }
125}