1use bytesize::ByteSize;
2use fundu::DurationParser;
3use human_repr::{HumanCount, HumanDuration, HumanThroughput};
4use std::str::FromStr;
5
6#[derive(Debug, Clone)]
8pub enum Measurement {
9 Duration(std::time::Duration),
10 DataSize(u64), DataRate(f64), Count(f64), }
14
15pub fn parse_value_with_unit(value: f64, unit_str: &str) -> Result<Measurement, String> {
18 if let Ok(duration) = parse_duration(value, unit_str) {
20 return Ok(Measurement::Duration(duration));
21 }
22
23 if let Ok(size) = parse_data_size(value, unit_str) {
25 return Ok(Measurement::DataSize(size));
26 }
27
28 if unit_str.contains("/s") {
30 if let Ok(rate) = parse_data_rate(value, unit_str) {
31 return Ok(Measurement::DataRate(rate));
32 }
33 }
34
35 Ok(Measurement::Count(value))
37}
38
39#[must_use]
41pub fn format_measurement(measurement: Measurement) -> String {
42 match measurement {
43 Measurement::Duration(d) => d.human_duration().to_string(),
44 Measurement::DataSize(bytes) => bytes.human_count_bytes().to_string(),
45 Measurement::DataRate(bps) => bps.human_throughput_bytes().to_string(),
46 Measurement::Count(v) => format!("{:.3}", v),
47 }
48}
49
50fn parse_duration(value: f64, unit: &str) -> Result<std::time::Duration, String> {
52 let parser = DurationParser::with_all_time_units();
53 let inputs = [format!("{}{}", value, unit), format!("{} {}", value, unit)];
55
56 for input in &inputs {
57 if let Ok(fundu_duration) = parser.parse(input) {
58 if let Ok(duration) = fundu_duration.try_into() {
59 return Ok(duration);
60 }
61 }
62 }
63
64 Err(format!("Failed to parse duration: {} {}", value, unit))
65}
66
67fn parse_data_size(value: f64, unit: &str) -> Result<u64, String> {
69 let normalized_unit = match unit.to_lowercase().as_str() {
71 "byte" | "bytes" => "B",
72 "kilobyte" | "kilobytes" => "KB",
73 "megabyte" | "megabytes" => "MB",
74 "gigabyte" | "gigabytes" => "GB",
75 "kibibyte" | "kibibytes" => "KiB",
76 "mebibyte" | "mebibytes" => "MiB",
77 "gibibyte" | "gibibytes" => "GiB",
78 _ => unit, };
80
81 let inputs = [
83 format!("{}{}", value, normalized_unit),
84 format!("{} {}", value, normalized_unit),
85 ];
86
87 for input in &inputs {
88 if let Ok(bs) = ByteSize::from_str(input) {
89 return Ok(bs.as_u64());
90 }
91 }
92
93 Err(format!("Failed to parse data size: {} {}", value, unit))
94}
95
96fn parse_data_rate(value: f64, unit_with_rate: &str) -> Result<f64, String> {
98 let parts: Vec<&str> = unit_with_rate.split('/').collect();
99 if parts.len() != 2 || parts[1] != "s" {
100 return Err("Invalid rate format".to_string());
101 }
102
103 let multiplier = match parts[0].to_lowercase().as_str() {
104 "b" => 1.0,
105 "kb" => 1_000.0,
106 "mb" => 1_000_000.0,
107 "gb" => 1_000_000_000.0,
108 "kib" => 1_024.0,
109 "mib" => 1_048_576.0,
110 "gib" => 1_073_741_824.0,
111 _ => return Err(format!("Unknown unit: {}", parts[0])),
112 };
113
114 Ok(value * multiplier)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn test_parse_duration_units() {
123 let m = parse_value_with_unit(9000.0, "ms").unwrap();
125 assert_eq!(format_measurement(m), "9s");
126
127 let m = parse_value_with_unit(125000.0, "ms").unwrap();
129 let formatted = format_measurement(m);
130 assert!(formatted.contains("2:05"));
131 }
132
133 #[test]
134 fn test_parse_data_size_units() {
135 let m = parse_value_with_unit(9000.0, "KB").unwrap();
137 assert_eq!(format_measurement(m), "9MB");
138
139 let m = parse_value_with_unit(1500.0, "MB").unwrap();
141 assert_eq!(format_measurement(m), "1.5GB");
142 }
143
144 #[test]
145 fn test_parse_data_rate_units() {
146 let m = parse_value_with_unit(9000.0, "KB/s").unwrap();
148 assert_eq!(format_measurement(m), "9MB/s");
149 }
150
151 #[test]
152 fn test_parse_fallback_to_count() {
153 let m = parse_value_with_unit(42.5, "widgets").unwrap();
155 assert_eq!(format_measurement(m), "42.500");
156 }
157
158 #[test]
159 fn test_duration_milliseconds() {
160 let m = parse_value_with_unit(9000.0, "ms").unwrap();
161 assert_eq!(format_measurement(m), "9s");
162 }
163
164 #[test]
165 fn test_duration_seconds_to_minutes() {
166 let m = parse_value_with_unit(125.0, "s").unwrap();
167 let formatted = format_measurement(m);
168 assert!(formatted.contains("2:05"));
169 }
170
171 #[test]
172 fn test_data_size_kilobytes() {
173 let m = parse_value_with_unit(9000.0, "KB").unwrap();
174 assert_eq!(format_measurement(m), "9MB");
175 }
176
177 #[test]
178 fn test_data_rate_megabytes() {
179 let m = parse_value_with_unit(1500.0, "MB/s").unwrap();
180 assert_eq!(format_measurement(m), "1.5GB/s");
181 }
182
183 #[test]
184 fn test_unknown_unit_fallback() {
185 let m = parse_value_with_unit(42.5, "widgets").unwrap();
187 assert!(matches!(m, Measurement::Count(_)));
188 }
189
190 #[test]
191 fn test_nanoseconds() {
192 let m = parse_value_with_unit(1_000_000.0, "ns").unwrap();
193 let formatted = format_measurement(m);
194 assert!(formatted.contains("ms") || formatted.contains("1"));
196 }
197
198 #[test]
199 fn test_bytes() {
200 let m = parse_value_with_unit(1024.0, "B").unwrap();
201 let formatted = format_measurement(m);
202 assert!(formatted.contains("1") || formatted.contains("B"));
204 }
205
206 #[test]
207 fn test_gigabytes() {
208 let m = parse_value_with_unit(2.5, "GB").unwrap();
209 assert_eq!(format_measurement(m), "2.5GB");
210 }
211
212 #[test]
213 fn test_hours() {
214 let m = parse_value_with_unit(2.0, "h").unwrap();
215 let formatted = format_measurement(m);
216 assert!(formatted.contains("2:00") || formatted.contains("h"));
218 }
219
220 #[test]
221 fn test_zero_values() {
222 let m = parse_value_with_unit(0.0, "ms").unwrap();
223 let formatted = format_measurement(m);
224 assert!(formatted.contains("0"));
225 }
226
227 #[test]
228 fn test_small_durations() {
229 let m = parse_value_with_unit(500.0, "ns").unwrap();
230 let formatted = format_measurement(m);
231 assert!(formatted.contains("ns") || formatted.contains("500"));
232 }
233
234 #[test]
235 fn test_bytes_unit_normalization() {
236 let m = parse_value_with_unit(1000.0, "bytes").unwrap();
238 assert!(
239 matches!(m, Measurement::DataSize(_)),
240 "Should parse 'bytes' as DataSize, got: {:?}",
241 m
242 );
243 let formatted = format_measurement(m);
244 assert_eq!(formatted, "1kB");
246
247 let m = parse_value_with_unit(500.0, "byte").unwrap();
249 assert!(matches!(m, Measurement::DataSize(_)));
250 let formatted = format_measurement(m);
251 assert_eq!(formatted, "500B");
252
253 let m = parse_value_with_unit(5000.0, "kilobytes").unwrap();
255 assert!(matches!(m, Measurement::DataSize(_)));
256 assert_eq!(format_measurement(m), "5MB");
258
259 let m = parse_value_with_unit(2000.0, "megabytes").unwrap();
260 assert!(matches!(m, Measurement::DataSize(_)));
261 assert_eq!(format_measurement(m), "2GB");
263 }
264}