1use chrono::{DateTime, Local, Utc};
4use std::time::{Duration, SystemTime};
5
6pub struct TimeUtils;
8
9impl TimeUtils {
10 pub fn now_utc() -> DateTime<Utc> {
12 Utc::now()
13 }
14
15 pub fn now_local() -> DateTime<Local> {
17 Local::now()
18 }
19
20 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 pub fn format_timestamp(time: DateTime<Local>) -> String {
47 time.format("%Y-%m-%d %H:%M:%S").to_string()
48 }
49
50 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 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 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
87pub trait Elapsed {
89 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}