Skip to main content

gitkraft_core/utils/
time.rs

1//! Time formatting and OID helpers.
2
3/// Return a human-friendly relative-time string for the given UTC timestamp.
4///
5/// # Examples
6///
7/// - `"just now"`
8/// - `"1 minute ago"` / `"5 minutes ago"`
9/// - `"1 hour ago"` / `"3 hours ago"`
10/// - `"1 day ago"` / `"2 days ago"`
11/// - `"1 week ago"` / `"3 weeks ago"`
12/// - `"1 month ago"` / `"6 months ago"`
13/// - `"1 year ago"` / `"2 years ago"`
14pub fn relative_time(dt: chrono::DateTime<chrono::Utc>) -> String {
15    let now = chrono::Utc::now();
16    let duration = now.signed_duration_since(dt);
17
18    let seconds = duration.num_seconds();
19    if seconds < 0 {
20        return "in the future".to_string();
21    }
22
23    let minutes = duration.num_minutes();
24    let hours = duration.num_hours();
25    let days = duration.num_days();
26    let weeks = days / 7;
27    let months = days / 30;
28    let years = days / 365;
29
30    if seconds < 60 {
31        "just now".to_string()
32    } else if minutes == 1 {
33        "1 minute ago".to_string()
34    } else if minutes < 60 {
35        format!("{minutes} minutes ago")
36    } else if hours == 1 {
37        "1 hour ago".to_string()
38    } else if hours < 24 {
39        format!("{hours} hours ago")
40    } else if days == 1 {
41        "1 day ago".to_string()
42    } else if days < 7 {
43        format!("{days} days ago")
44    } else if weeks == 1 {
45        "1 week ago".to_string()
46    } else if weeks < 5 {
47        format!("{weeks} weeks ago")
48    } else if months == 1 {
49        "1 month ago".to_string()
50    } else if months < 12 {
51        format!("{months} months ago")
52    } else if years == 1 {
53        "1 year ago".to_string()
54    } else {
55        format!("{years} years ago")
56    }
57}
58
59/// Format a `git2::Oid` as a full hex string.
60pub fn fmt_oid(oid: git2::Oid) -> String {
61    oid.to_string()
62}
63
64/// Format a `git2::Oid` as an abbreviated hex string (first 7 characters).
65pub fn short_oid(oid: git2::Oid) -> String {
66    let full = oid.to_string();
67    full[..7.min(full.len())].to_string()
68}
69
70/// Abbreviate a hex OID string to the first 7 characters.
71pub fn short_oid_str(oid: &str) -> &str {
72    match oid.char_indices().nth(7) {
73        Some((i, _)) => &oid[..i],
74        None => oid,
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use chrono::Utc;
82
83    #[test]
84    fn relative_time_just_now() {
85        let now = Utc::now();
86        assert_eq!(relative_time(now), "just now");
87    }
88
89    #[test]
90    fn relative_time_one_minute() {
91        let t = Utc::now() - chrono::Duration::minutes(1);
92        assert_eq!(relative_time(t), "1 minute ago");
93    }
94
95    #[test]
96    fn relative_time_minutes() {
97        let t = Utc::now() - chrono::Duration::minutes(5);
98        assert_eq!(relative_time(t), "5 minutes ago");
99    }
100
101    #[test]
102    fn relative_time_one_hour() {
103        let t = Utc::now() - chrono::Duration::hours(1);
104        assert_eq!(relative_time(t), "1 hour ago");
105    }
106
107    #[test]
108    fn relative_time_hours() {
109        let t = Utc::now() - chrono::Duration::hours(3);
110        assert_eq!(relative_time(t), "3 hours ago");
111    }
112
113    #[test]
114    fn relative_time_one_day() {
115        let t = Utc::now() - chrono::Duration::days(1);
116        assert_eq!(relative_time(t), "1 day ago");
117    }
118
119    #[test]
120    fn relative_time_days() {
121        let t = Utc::now() - chrono::Duration::days(4);
122        assert_eq!(relative_time(t), "4 days ago");
123    }
124
125    #[test]
126    fn relative_time_one_week() {
127        let t = Utc::now() - chrono::Duration::weeks(1);
128        assert_eq!(relative_time(t), "1 week ago");
129    }
130
131    #[test]
132    fn relative_time_weeks() {
133        let t = Utc::now() - chrono::Duration::weeks(3);
134        assert_eq!(relative_time(t), "3 weeks ago");
135    }
136
137    #[test]
138    fn relative_time_one_month() {
139        let t = Utc::now() - chrono::Duration::days(35);
140        assert_eq!(relative_time(t), "1 month ago");
141    }
142
143    #[test]
144    fn relative_time_months() {
145        let t = Utc::now() - chrono::Duration::days(180);
146        assert_eq!(relative_time(t), "6 months ago");
147    }
148
149    #[test]
150    fn relative_time_one_year() {
151        let t = Utc::now() - chrono::Duration::days(400);
152        assert_eq!(relative_time(t), "1 year ago");
153    }
154
155    #[test]
156    fn relative_time_years() {
157        let t = Utc::now() - chrono::Duration::days(800);
158        assert_eq!(relative_time(t), "2 years ago");
159    }
160
161    #[test]
162    fn relative_time_future() {
163        let t = Utc::now() + chrono::Duration::hours(1);
164        assert_eq!(relative_time(t), "in the future");
165    }
166
167    #[test]
168    fn fmt_oid_full() {
169        let oid = git2::Oid::from_str("abcdef1234567890abcdef1234567890abcdef12").unwrap();
170        assert_eq!(fmt_oid(oid), "abcdef1234567890abcdef1234567890abcdef12");
171    }
172
173    #[test]
174    fn short_oid_seven_chars() {
175        let oid = git2::Oid::from_str("abcdef1234567890abcdef1234567890abcdef12").unwrap();
176        assert_eq!(short_oid(oid), "abcdef1");
177    }
178
179    #[test]
180    fn short_oid_str_abbreviates() {
181        assert_eq!(short_oid_str("abcdef1234567890"), "abcdef1");
182    }
183
184    #[test]
185    fn short_oid_str_multibyte() {
186        assert_eq!(short_oid_str("\n\0\0O\0Oర"), "\n\0\0O\0Oర");
187        assert_eq!(short_oid_str("ఠఠఠఠఠఠఠఠ"), "ఠఠఠఠఠఠఠ");
188    }
189
190    #[test]
191    fn short_oid_str_short_input() {
192        assert_eq!(short_oid_str("abc"), "abc");
193    }
194}