1use chrono::DateTime;
5
6fn js_round(value: f64) -> i64 {
8 (value + 0.5).floor() as i64
9}
10
11fn js_to_fixed(value: f64, digits: u32) -> String {
13 let factor = 10f64.powi(digits as i32);
14 let scaled = js_round(value * factor) as f64 / factor;
15 format!("{scaled:.*}", digits as usize)
16}
17
18pub fn format_duration(duration_ms: i64) -> String {
19 if duration_ms < 1_000 {
20 return format!("{}ms", duration_ms.max(0));
21 }
22 let seconds = duration_ms as f64 / 1_000.0;
23 if seconds < 60.0 {
24 let digits = if seconds < 10.0 { 1 } else { 0 };
25 return format!("{}s", js_to_fixed(seconds, digits));
26 }
27 let minutes = (seconds / 60.0).floor() as i64;
28 let rest = js_round(seconds % 60.0);
29 format!("{minutes}m{rest:02}s")
30}
31
32pub fn parse_timestamp_ms(value: &str) -> Option<i64> {
35 DateTime::parse_from_rfc3339(value)
36 .ok()
37 .map(|value| value.timestamp_millis())
38}
39
40pub fn strip_ansi(text: &str) -> String {
42 let chars: Vec<char> = text.chars().collect();
43 let mut result = String::new();
44 let mut index = 0;
45 while index < chars.len() {
46 if chars[index] == '\u{1b}' && chars.get(index + 1) == Some(&'[') {
47 index += 2;
48 while index < chars.len() && !chars[index].is_ascii_alphabetic() {
49 index += 1;
50 }
51 index += 1;
52 continue;
53 }
54 result.push(chars[index]);
55 index += 1;
56 }
57 result
58}
59
60pub fn sanitize_text(text: &str) -> String {
64 let stripped = strip_ansi(text);
65 let mut result = String::new();
66 let mut pending_space = false;
67 for char in stripped.chars() {
68 if matches!(char, '\t' | '\n' | '\r') {
69 pending_space = true;
70 continue;
71 }
72 if pending_space {
73 result.push(' ');
74 pending_space = false;
75 }
76 if ('\u{0}'..='\u{1f}').contains(&char) || ('\u{7f}'..='\u{9f}').contains(&char) {
79 continue;
80 }
81 result.push(char);
82 }
83 if pending_space {
84 result.push(' ');
85 }
86 result
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn duration_matches_js() {
95 assert_eq!(format_duration(0), "0ms");
96 assert_eq!(format_duration(-5), "0ms");
97 assert_eq!(format_duration(999), "999ms");
98 assert_eq!(format_duration(1_000), "1.0s");
99 assert_eq!(format_duration(1_050), "1.1s"); assert_eq!(format_duration(5_000), "5.0s");
101 assert_eq!(format_duration(9_940), "9.9s");
102 assert_eq!(format_duration(10_500), "11s"); assert_eq!(format_duration(59_499), "59s");
104 assert_eq!(format_duration(60_000), "1m00s");
105 assert_eq!(format_duration(90_500), "1m31s"); assert_eq!(format_duration(3_599_000), "59m59s");
107 }
108
109 #[test]
110 fn sanitize_collapses_control_runs() {
111 assert_eq!(sanitize_text("a\n\nb\tc"), "a b c");
112 assert_eq!(sanitize_text("\u{1b}[31mred\u{1b}[0m"), "red");
113 assert_eq!(sanitize_text("bell\u{7}!"), "bell!");
114 assert_eq!(sanitize_text("a\u{9b}2Jb"), "a2Jb");
116 assert_eq!(sanitize_text("a\u{9d}52;xb"), "a52;xb");
117 assert_eq!(sanitize_text("del\u{7f}!"), "del!");
118 }
119}