Skip to main content

max / multithreaded

3.8 KB · 141 lines History Blame Raw
1 //! Human-readable timestamp formatting for forum display.
2
3 use chrono::{DateTime, Utc};
4
5 /// Relative timestamp for thread listings: "just now", "3m ago", "2h ago", etc.
6 /// Falls back to "Jan 15" format for dates older than 7 days.
7 pub fn relative_timestamp(dt: DateTime<Utc>) -> String {
8 let now = Utc::now();
9 let delta = now.signed_duration_since(dt);
10
11 if delta.num_seconds() < 60 {
12 "just now".into()
13 } else if delta.num_minutes() < 60 {
14 format!("{}m ago", delta.num_minutes())
15 } else if delta.num_hours() < 24 {
16 format!("{}h ago", delta.num_hours())
17 } else if delta.num_days() < 7 {
18 format!("{}d ago", delta.num_days())
19 } else {
20 dt.format("%b %-d").to_string()
21 }
22 }
23
24 /// Absolute timestamp for post display: "2026-03-13 10:30".
25 pub fn post_timestamp(dt: DateTime<Utc>) -> String {
26 dt.format("%Y-%m-%d %H:%M").to_string()
27 }
28
29 #[cfg(test)]
30 mod tests {
31 use super::*;
32 use chrono::Duration;
33
34 #[test]
35 fn just_now() {
36 let dt = Utc::now() - Duration::seconds(30);
37 assert_eq!(relative_timestamp(dt), "just now");
38 }
39
40 #[test]
41 fn minutes_ago() {
42 let dt = Utc::now() - Duration::minutes(5);
43 assert_eq!(relative_timestamp(dt), "5m ago");
44 }
45
46 #[test]
47 fn hours_ago() {
48 let dt = Utc::now() - Duration::hours(3);
49 assert_eq!(relative_timestamp(dt), "3h ago");
50 }
51
52 #[test]
53 fn days_ago() {
54 let dt = Utc::now() - Duration::days(2);
55 assert_eq!(relative_timestamp(dt), "2d ago");
56 }
57
58 #[test]
59 fn older_than_week() {
60 let dt = Utc::now() - Duration::days(10);
61 let result = relative_timestamp(dt);
62 // Should be in "Jan 15" format
63 assert!(!result.contains("ago"), "expected date format, got: {result}");
64 }
65
66 #[test]
67 fn post_timestamp_format() {
68 let dt = chrono::NaiveDate::from_ymd_opt(2026, 3, 13)
69 .unwrap()
70 .and_hms_opt(10, 30, 0)
71 .unwrap()
72 .and_utc();
73 assert_eq!(post_timestamp(dt), "2026-03-13 10:30");
74 }
75
76 #[test]
77 fn boundary_59_seconds_is_just_now() {
78 let dt = Utc::now() - Duration::seconds(59);
79 assert_eq!(relative_timestamp(dt), "just now");
80 }
81
82 #[test]
83 fn boundary_60_seconds_is_1m() {
84 let dt = Utc::now() - Duration::seconds(60);
85 assert_eq!(relative_timestamp(dt), "1m ago");
86 }
87
88 #[test]
89 fn boundary_59_minutes_is_minutes() {
90 let dt = Utc::now() - Duration::minutes(59);
91 assert_eq!(relative_timestamp(dt), "59m ago");
92 }
93
94 #[test]
95 fn boundary_60_minutes_is_1h() {
96 let dt = Utc::now() - Duration::minutes(60);
97 assert_eq!(relative_timestamp(dt), "1h ago");
98 }
99
100 #[test]
101 fn boundary_23_hours_is_hours() {
102 let dt = Utc::now() - Duration::hours(23);
103 assert_eq!(relative_timestamp(dt), "23h ago");
104 }
105
106 #[test]
107 fn boundary_24_hours_is_1d() {
108 let dt = Utc::now() - Duration::hours(24);
109 assert_eq!(relative_timestamp(dt), "1d ago");
110 }
111
112 #[test]
113 fn boundary_6_days_is_days() {
114 let dt = Utc::now() - Duration::days(6);
115 assert_eq!(relative_timestamp(dt), "6d ago");
116 }
117
118 #[test]
119 fn boundary_7_days_is_date() {
120 let dt = Utc::now() - Duration::days(7);
121 let result = relative_timestamp(dt);
122 assert!(!result.contains("ago"), "expected date format, got: {result}");
123 }
124
125 #[test]
126 fn post_timestamp_midnight() {
127 let dt = chrono::NaiveDate::from_ymd_opt(2026, 1, 1)
128 .unwrap()
129 .and_hms_opt(0, 0, 0)
130 .unwrap()
131 .and_utc();
132 assert_eq!(post_timestamp(dt), "2026-01-01 00:00");
133 }
134
135 #[test]
136 fn zero_seconds_ago_is_just_now() {
137 let dt = Utc::now();
138 assert_eq!(relative_timestamp(dt), "just now");
139 }
140 }
141