High-performance implementation of plcbundle written in Rust
at main 292 lines 9.7 kB view raw
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}