git_perf/
units.rs

1use bytesize::ByteSize;
2use fundu::DurationParser;
3use human_repr::{HumanCount, HumanDuration, HumanThroughput};
4use std::str::FromStr;
5
6/// Represents a parsed measurement with detected type
7#[derive(Debug, Clone)]
8pub enum Measurement {
9    Duration(std::time::Duration),
10    DataSize(u64), // bytes
11    DataRate(f64), // bytes per second
12    Count(f64),    // unitless or custom
13}
14
15/// Parse a numeric value with its unit string
16/// Tries different parsers until one succeeds
17pub fn parse_value_with_unit(value: f64, unit_str: &str) -> Result<Measurement, String> {
18    // Try duration parsing (ms, s, min, h, etc.)
19    if let Ok(duration) = parse_duration(value, unit_str) {
20        return Ok(Measurement::Duration(duration));
21    }
22
23    // Try data size parsing (B, KB, MB, GB, etc.)
24    if let Ok(size) = parse_data_size(value, unit_str) {
25        return Ok(Measurement::DataSize(size));
26    }
27
28    // Try data rate parsing (KB/s, MB/s, etc.)
29    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    // Fallback: treat as unitless count
36    Ok(Measurement::Count(value))
37}
38
39/// Format measurement with auto-scaling using human-repr
40#[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
50/// Helper: Parse duration from value + unit
51fn parse_duration(value: f64, unit: &str) -> Result<std::time::Duration, String> {
52    let parser = DurationParser::with_all_time_units();
53    // Try without space first (9000ms), then with space (9000 ms)
54    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
67/// Helper: Parse data size from value + unit
68fn parse_data_size(value: f64, unit: &str) -> Result<u64, String> {
69    // Normalize unit: "bytes" -> "B", "byte" -> "B"
70    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, // Keep original if not recognized
79    };
80
81    // Try various input formats
82    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
96/// Helper: Parse data rate from value + unit (e.g., KB/s, MB/s)
97fn 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        // 9000 ms → "9s"
124        let m = parse_value_with_unit(9000.0, "ms").unwrap();
125        assert_eq!(format_measurement(m), "9s");
126
127        // 125000 ms → "2:05.0"
128        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        // 9000 KB → "9MB"
136        let m = parse_value_with_unit(9000.0, "KB").unwrap();
137        assert_eq!(format_measurement(m), "9MB");
138
139        // 1500 MB → "1.5GB"
140        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        // 9000 KB/s → "9MB/s"
147        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        // Unknown unit → Count (no parsing error)
154        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        // Unknown units fallback to raw count
186        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        // 1,000,000 ns = 1 ms
195        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        // Should be formatted as bytes
203        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        // 2 hours should be formatted appropriately
217        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        // Test that "bytes" is normalized to "B" and parsed correctly
237        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        // human-repr auto-scales 1000B to 1kB
245        assert_eq!(formatted, "1kB");
246
247        // Test "byte" singular
248        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        // Test other long forms
254        let m = parse_value_with_unit(5000.0, "kilobytes").unwrap();
255        assert!(matches!(m, Measurement::DataSize(_)));
256        // 5000 KB = 5 MB
257        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        // 2000 MB = 2 GB
262        assert_eq!(format_measurement(m), "2GB");
263    }
264}