High-performance implementation of plcbundle written in Rust
1//! Formatting helpers for bytes, durations, rates, and numbers used across CLI/server/library components
2// Shared formatting helpers used across CLI/server/library components.
3
4use chrono::Duration as ChronoDuration;
5use std::time::Duration as StdDuration;
6
7/// Format a byte count as a human-readable string (e.g. "1.23 MB").
8pub fn format_bytes(bytes: u64) -> String {
9 const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
10
11 let mut size = bytes as f64;
12 let mut unit_idx = 0usize;
13
14 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
15 size /= 1024.0;
16 unit_idx += 1;
17 }
18
19 if unit_idx == 0 {
20 format!("{} {}", bytes, UNITS[unit_idx])
21 } else {
22 format!("{:.2} {}", size, UNITS[unit_idx])
23 }
24}
25
26/// Format a byte count in compact ls/df style (e.g. "1.5K", "2.3M", "1.2G").
27/// Similar to `ls -h` or `df -h` output format.
28pub fn format_bytes_compact(bytes: u64) -> String {
29 const UNITS: [&str; 5] = ["", "K", "M", "G", "T"];
30
31 if bytes == 0 {
32 return "0".to_string();
33 }
34
35 let mut size = bytes as f64;
36 let mut unit_idx = 0usize;
37
38 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
39 size /= 1024.0;
40 unit_idx += 1;
41 }
42
43 if unit_idx == 0 {
44 format!("{}", bytes)
45 } else {
46 // Use 1 decimal place, but remove trailing zero
47 let formatted = format!("{:.1}", size);
48 let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
49 format!("{}{}", trimmed, UNITS[unit_idx])
50 }
51}
52
53/// Format an integer with thousands separators (e.g. 12_345 -> "12,345").
54pub fn format_number<T>(value: T) -> String
55where
56 T: std::fmt::Display,
57{
58 let s = value.to_string();
59 let mut result = String::with_capacity(s.len() + s.len() / 3);
60 for (idx, ch) in s.chars().rev().enumerate() {
61 if idx > 0 && idx % 3 == 0 {
62 result.push(',');
63 }
64 result.push(ch);
65 }
66 result.chars().rev().collect()
67}
68
69// Internal helpers for duration formatting
70
71/// Format seconds as verbose units (e.g. "2d 3h 10m 5s").
72fn format_seconds_verbose(seconds: u64) -> String {
73 let days = seconds / 86_400;
74 let hours = (seconds % 86_400) / 3_600;
75 let minutes = (seconds % 3_600) / 60;
76 let secs = seconds % 60;
77
78 if days > 0 {
79 format!("{}d {}h {}m {}s", days, hours, minutes, secs)
80 } else if hours > 0 {
81 format!("{}h {}m {}s", hours, minutes, secs)
82 } else if minutes > 0 {
83 format!("{}m {}s", minutes, secs)
84 } else {
85 format!("{}s", secs)
86 }
87}
88
89/// Format seconds as compact units (e.g. "5s", "3m", "4h", "2d").
90fn format_seconds_compact(seconds: u64) -> String {
91 if seconds < 60 {
92 format!("{}s", seconds)
93 } else if seconds < 3_600 {
94 format!("{}m", seconds / 60)
95 } else if seconds < 86_400 {
96 format!("{}h", seconds / 3_600)
97 } else if seconds < 31_536_000 {
98 format!("{}d", seconds / 86_400)
99 } else {
100 format!("{}y", seconds / 31_536_000)
101 }
102}
103
104// Public duration formatting functions
105
106/// Format a chrono duration using verbose units (e.g. "2d 3h 10m 5s").
107pub fn format_duration_verbose(duration: ChronoDuration) -> String {
108 let seconds = duration.num_seconds();
109 let sign = if seconds < 0 { "-" } else { "" };
110 let abs_seconds = seconds.unsigned_abs();
111 format!("{}{}", sign, format_seconds_verbose(abs_seconds))
112}
113
114/// Format a duration using compact units (e.g. "5s", "3m", "4h", "2d").
115pub fn format_duration_compact(duration: ChronoDuration) -> String {
116 let seconds = duration.num_seconds();
117 let sign = if seconds < 0 { "-" } else { "" };
118 let abs_seconds = seconds.unsigned_abs();
119 format!("{}{}", sign, format_seconds_compact(abs_seconds))
120}
121
122/// Format a std::time::Duration using compact units (e.g. "5s", "3m", "4h", "2d").
123pub fn format_std_duration(duration: StdDuration) -> String {
124 format_seconds_compact(duration.as_secs())
125}
126
127/// Format a std::time::Duration using verbose units (e.g. "2d 3h 10m 5s").
128pub fn format_std_duration_verbose(duration: StdDuration) -> String {
129 format_seconds_verbose(duration.as_secs())
130}
131
132/// Format a duration in milliseconds (e.g. "123ms", "1.234ms").
133pub fn format_std_duration_ms(duration: StdDuration) -> String {
134 let ms = duration.as_secs_f64() * 1000.0;
135 if ms < 100.0 {
136 format!("{:.3}ms", ms)
137 } else {
138 format!("{:.0}ms", ms)
139 }
140}
141
142/// Format a duration with auto-scaling units (μs/ms for < 1s, then s/m/h for longer).
143pub fn format_std_duration_auto(duration: StdDuration) -> String {
144 let secs = duration.as_secs_f64();
145 if secs < 0.001 {
146 format!("{:.0}μs", secs * 1_000_000.0)
147 } else if secs < 1.0 {
148 format!("{:.0}ms", secs * 1000.0)
149 } else if secs < 60.0 {
150 format!("{:.1}s", secs)
151 } else {
152 // Use HumanDuration for longer durations (handles m, h, etc.)
153 use indicatif::HumanDuration;
154 HumanDuration(duration).to_string()
155 }
156}
157
158/// Format a bytes-per-second rate as a human-readable string (e.g. "1.23 MB/sec").
159/// Takes bytes per second as a floating point number.
160pub fn format_bytes_per_sec(bytes_per_sec: f64) -> String {
161 const UNITS: [&str; 5] = ["B", "KB", "MB", "GB", "TB"];
162
163 let mut size = bytes_per_sec;
164 let mut unit_idx = 0usize;
165
166 while size >= 1024.0 && unit_idx < UNITS.len() - 1 {
167 size /= 1024.0;
168 unit_idx += 1;
169 }
170
171 if unit_idx == 0 {
172 format!("{:.1} {}/sec", bytes_per_sec, UNITS[unit_idx])
173 } else {
174 format!("{:.1} {}/sec", size, UNITS[unit_idx])
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use chrono::Duration as ChronoDuration;
182
183 #[test]
184 fn test_format_bytes() {
185 assert_eq!(format_bytes(0), "0 B");
186 assert_eq!(format_bytes(512), "512 B");
187 assert_eq!(format_bytes(1024), "1.00 KB");
188 assert_eq!(format_bytes(1536), "1.50 KB");
189 assert_eq!(format_bytes(1024 * 1024), "1.00 MB");
190 assert_eq!(format_bytes(1024 * 1024 * 1024), "1.00 GB");
191 }
192
193 #[test]
194 fn test_format_bytes_compact() {
195 assert_eq!(format_bytes_compact(0), "0");
196 assert_eq!(format_bytes_compact(512), "512");
197 assert_eq!(format_bytes_compact(1024), "1K");
198 assert_eq!(format_bytes_compact(1536), "1.5K");
199 assert_eq!(format_bytes_compact(1024 * 1024), "1M");
200 assert_eq!(format_bytes_compact(1024 * 1024 * 1024), "1G");
201 assert_eq!(format_bytes_compact(1536 * 1024), "1.5M");
202 }
203
204 #[test]
205 fn test_format_number() {
206 assert_eq!(format_number(0), "0");
207 assert_eq!(format_number(123), "123");
208 assert_eq!(format_number(1234), "1,234");
209 assert_eq!(format_number(12345), "12,345");
210 assert_eq!(format_number(123456), "123,456");
211 assert_eq!(format_number(1234567), "1,234,567");
212 assert_eq!(format_number(12345678), "12,345,678");
213 }
214
215 #[test]
216 fn test_format_std_duration() {
217 assert_eq!(format_std_duration(StdDuration::from_secs(0)), "0s");
218 assert_eq!(format_std_duration(StdDuration::from_secs(30)), "30s");
219 assert_eq!(format_std_duration(StdDuration::from_secs(60)), "1m");
220 assert_eq!(format_std_duration(StdDuration::from_secs(3600)), "1h");
221 assert_eq!(format_std_duration(StdDuration::from_secs(86400)), "1d");
222 assert_eq!(format_std_duration(StdDuration::from_secs(31536000)), "1y");
223 }
224
225 #[test]
226 fn test_format_std_duration_verbose() {
227 assert_eq!(format_std_duration_verbose(StdDuration::from_secs(0)), "0s");
228 assert_eq!(format_std_duration_verbose(StdDuration::from_secs(5)), "5s");
229 assert_eq!(
230 format_std_duration_verbose(StdDuration::from_secs(65)),
231 "1m 5s"
232 );
233 assert_eq!(
234 format_std_duration_verbose(StdDuration::from_secs(3665)),
235 "1h 1m 5s"
236 );
237 assert_eq!(
238 format_std_duration_verbose(StdDuration::from_secs(90065)),
239 "1d 1h 1m 5s"
240 );
241 }
242
243 #[test]
244 fn test_format_std_duration_ms() {
245 assert_eq!(
246 format_std_duration_ms(StdDuration::from_millis(0)),
247 "0.000ms"
248 );
249 assert_eq!(
250 format_std_duration_ms(StdDuration::from_millis(50)),
251 "50.000ms"
252 );
253 assert_eq!(
254 format_std_duration_ms(StdDuration::from_millis(100)),
255 "100ms"
256 );
257 assert_eq!(
258 format_std_duration_ms(StdDuration::from_millis(1234)),
259 "1234ms"
260 );
261 }
262
263 #[test]
264 fn test_format_duration_verbose() {
265 assert_eq!(format_duration_verbose(ChronoDuration::seconds(0)), "0s");
266 assert_eq!(
267 format_duration_verbose(ChronoDuration::seconds(65)),
268 "1m 5s"
269 );
270 assert_eq!(
271 format_duration_verbose(ChronoDuration::seconds(-65)),
272 "-1m 5s"
273 );
274 }
275
276 #[test]
277 fn test_format_duration_compact() {
278 assert_eq!(format_duration_compact(ChronoDuration::seconds(0)), "0s");
279 assert_eq!(format_duration_compact(ChronoDuration::seconds(30)), "30s");
280 assert_eq!(format_duration_compact(ChronoDuration::seconds(60)), "1m");
281 assert_eq!(format_duration_compact(ChronoDuration::seconds(-60)), "-1m");
282 }
283
284 #[test]
285 fn test_format_bytes_per_sec() {
286 assert_eq!(format_bytes_per_sec(0.0), "0.0 B/sec");
287 assert_eq!(format_bytes_per_sec(512.0), "512.0 B/sec");
288 assert_eq!(format_bytes_per_sec(1024.0), "1.0 KB/sec");
289 assert_eq!(format_bytes_per_sec(1536.0), "1.5 KB/sec");
290 assert_eq!(format_bytes_per_sec(1024.0 * 1024.0), "1.0 MB/sec");
291 }
292}