vtcode_commons/
message_timestamp.rs1use std::time::Duration;
11
12use chrono::{DateTime, Local};
13
14pub fn format_message_time(now: DateTime<Local>) -> String {
19 now.format("%-I:%M %p").to_string()
20}
21
22pub fn format_turn_elapsed(duration: Duration) -> String {
28 const MINUTE: u64 = 60;
29 const HOUR: u64 = 3600;
30 let seconds = duration.as_secs();
31 if seconds < MINUTE {
32 return format!("{seconds}s");
33 }
34 if seconds < HOUR {
35 let minutes = seconds / MINUTE;
36 let rest = seconds % MINUTE;
37 if rest == 0 {
38 return format!("{minutes}m");
39 }
40 return format!("{minutes}m {rest}s");
41 }
42 let hours = seconds / HOUR;
43 let minutes = (seconds % HOUR) / MINUTE;
44 if minutes == 0 {
45 return format!("{hours}h");
46 }
47 format!("{hours}h {minutes}m")
48}
49
50pub fn message_timestamp_footer(now: DateTime<Local>, elapsed: Option<Duration>) -> String {
57 match elapsed {
58 Some(duration) => format!("· {} · {}", format_message_time(now), format_turn_elapsed(duration)),
59 None => format!("· {}", format_message_time(now)),
60 }
61}
62
63pub fn current_message_timestamp_footer() -> String {
65 message_timestamp_footer(Local::now(), None)
66}
67
68pub fn current_message_timestamp_footer_with_elapsed(elapsed: Option<Duration>) -> String {
71 message_timestamp_footer(Local::now(), elapsed)
72}
73
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use chrono::TimeZone;
78
79 fn local_at(hour: u32, minute: u32) -> DateTime<Local> {
80 Local
81 .with_ymd_and_hms(2026, 11, 2, hour, minute, 0)
82 .single()
83 .expect("fixture timestamp must be unambiguous")
84 }
85
86 #[test]
87 fn formats_afternoon_without_leading_zero() {
88 assert_eq!(format_message_time(local_at(15, 12)), "3:12 PM");
89 }
90
91 #[test]
92 fn formats_single_digit_morning_hour() {
93 assert_eq!(format_message_time(local_at(9, 5)), "9:05 AM");
94 }
95
96 #[test]
97 fn formats_double_digit_morning_hour() {
98 assert_eq!(format_message_time(local_at(11, 55)), "11:55 AM");
99 }
100
101 #[test]
102 fn midnight_is_twelve_am_not_zero() {
103 assert_eq!(format_message_time(local_at(0, 5)), "12:05 AM");
104 }
105
106 #[test]
107 fn noon_is_twelve_pm_not_zero() {
108 assert_eq!(format_message_time(local_at(12, 0)), "12:00 PM");
109 }
110
111 #[test]
112 fn footer_prefixes_time_with_middot() {
113 assert_eq!(message_timestamp_footer(local_at(15, 12), None), "· 3:12 PM");
114 }
115
116 #[test]
117 fn footer_appends_elapsed_after_the_clock() {
118 assert_eq!(message_timestamp_footer(local_at(15, 12), Some(Duration::from_secs(804))), "· 3:12 PM · 13m 24s");
119 assert_eq!(message_timestamp_footer(local_at(15, 12), Some(Duration::from_secs(3900))), "· 3:12 PM · 1h 5m");
120 }
121
122 #[test]
123 fn current_footer_with_elapsed_keeps_one_chrome_unit() {
124 let footer = current_message_timestamp_footer_with_elapsed(Some(Duration::from_secs(59)));
125 let time = footer.strip_prefix("· ").expect("current footer must carry the middot");
126 let mut halves = time.split(" · ");
127 let clock = halves.next().expect("clock half must exist");
128 assert!(clock.contains(':'), "clock half must show clock time: {footer:?}");
129 assert!(clock.ends_with("AM") || clock.ends_with("PM"), "clock half must show meridiem: {footer:?}");
130 assert_eq!(halves.next(), Some("59s"), "elapsed half must follow the clock");
131 assert!(halves.next().is_none(), "footer must stay one clock plus one duration");
132 }
133
134 #[test]
135 fn elapsed_symbols_omit_zero_parts() {
136 assert_eq!(format_turn_elapsed(Duration::from_secs(0)), "0s");
137 assert_eq!(format_turn_elapsed(Duration::from_secs(59)), "59s");
138 assert_eq!(format_turn_elapsed(Duration::from_secs(60)), "1m");
139 assert_eq!(format_turn_elapsed(Duration::from_secs(90)), "1m 30s");
140 assert_eq!(format_turn_elapsed(Duration::from_secs(3600)), "1h");
141 assert_eq!(format_turn_elapsed(Duration::from_secs(3900)), "1h 5m");
142 }
143
144 #[test]
145 fn current_footer_has_footer_shape() {
146 let footer = current_message_timestamp_footer();
147 let time = footer.strip_prefix("· ").expect("current footer must carry the middot");
148 assert!(time.contains(':'), "current footer must show clock time: {footer:?}");
149 assert!(time.ends_with("AM") || time.ends_with("PM"), "current footer must show meridiem: {footer:?}");
150 }
151}