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
40pub fn format_measurement(measurement: Measurement) -> String {
41    match measurement {
42        Measurement::Duration(d) => d.human_duration().to_string(),
43        Measurement::DataSize(bytes) => bytes.human_count_bytes().to_string(),
44        Measurement::DataRate(bps) => bps.human_throughput_bytes().to_string(),
45        Measurement::Count(v) => format!("{:.3}", v),
46    }
47}
48
49/// Helper: Parse duration from value + unit
50fn parse_duration(value: f64, unit: &str) -> Result<std::time::Duration, String> {
51    let parser = DurationParser::with_all_time_units();
52    // Try without space first (9000ms), then with space (9000 ms)
53    let inputs = [format!("{}{}", value, unit), format!("{} {}", value, unit)];
54
55    for input in &inputs {
56        if let Ok(fundu_duration) = parser.parse(input) {
57            if let Ok(duration) = fundu_duration.try_into() {
58                return Ok(duration);
59            }
60        }
61    }
62
63    Err(format!("Failed to parse duration: {} {}", value, unit))
64}
65
66/// Helper: Parse data size from value + unit
67fn parse_data_size(value: f64, unit: &str) -> Result<u64, String> {
68    // Normalize unit: "bytes" -> "B", "byte" -> "B"
69    let normalized_unit = match unit.to_lowercase().as_str() {
70        "byte" | "bytes" => "B",
71        "kilobyte" | "kilobytes" => "KB",
72        "megabyte" | "megabytes" => "MB",
73        "gigabyte" | "gigabytes" => "GB",
74        "kibibyte" | "kibibytes" => "KiB",
75        "mebibyte" | "mebibytes" => "MiB",
76        "gibibyte" | "gibibytes" => "GiB",
77        _ => unit, // Keep original if not recognized
78    };
79
80    // Try various input formats
81    let inputs = [
82        format!("{}{}", value, normalized_unit),
83        format!("{} {}", value, normalized_unit),
84    ];
85
86    for input in &inputs {
87        if let Ok(bs) = ByteSize::from_str(input) {
88            return Ok(bs.as_u64());
89        }
90    }
91
92    Err(format!("Failed to parse data size: {} {}", value, unit))
93}
94
95/// Helper: Parse data rate from value + unit (e.g., KB/s, MB/s)
96fn parse_data_rate(value: f64, unit_with_rate: &str) -> Result<f64, String> {
97    let parts: Vec<&str> = unit_with_rate.split('/').collect();
98    if parts.len() != 2 || parts[1] != "s" {
99        return Err("Invalid rate format".to_string());
100    }
101
102    let multiplier = match parts[0].to_lowercase().as_str() {
103        "b" => 1.0,
104        "kb" => 1_000.0,
105        "mb" => 1_000_000.0,
106        "gb" => 1_000_000_000.0,
107        "kib" => 1_024.0,
108        "mib" => 1_048_576.0,
109        "gib" => 1_073_741_824.0,
110        _ => return Err(format!("Unknown unit: {}", parts[0])),
111    };
112
113    Ok(value * multiplier)
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_parse_duration_units() {
122        // 9000 ms → "9s"
123        let m = parse_value_with_unit(9000.0, "ms").unwrap();
124        assert_eq!(format_measurement(m), "9s");
125
126        // 125000 ms → "2:05.0"
127        let m = parse_value_with_unit(125000.0, "ms").unwrap();
128        let formatted = format_measurement(m);
129        assert!(formatted.contains("2:05"));
130    }
131
132    #[test]
133    fn test_parse_data_size_units() {
134        // 9000 KB → "9MB"
135        let m = parse_value_with_unit(9000.0, "KB").unwrap();
136        assert_eq!(format_measurement(m), "9MB");
137
138        // 1500 MB → "1.5GB"
139        let m = parse_value_with_unit(1500.0, "MB").unwrap();
140        assert_eq!(format_measurement(m), "1.5GB");
141    }
142
143    #[test]
144    fn test_parse_data_rate_units() {
145        // 9000 KB/s → "9MB/s"
146        let m = parse_value_with_unit(9000.0, "KB/s").unwrap();
147        assert_eq!(format_measurement(m), "9MB/s");
148    }
149
150    #[test]
151    fn test_parse_fallback_to_count() {
152        // Unknown unit → Count (no parsing error)
153        let m = parse_value_with_unit(42.5, "widgets").unwrap();
154        assert_eq!(format_measurement(m), "42.500");
155    }
156
157    #[test]
158    fn test_duration_milliseconds() {
159        let m = parse_value_with_unit(9000.0, "ms").unwrap();
160        assert_eq!(format_measurement(m), "9s");
161    }
162
163    #[test]
164    fn test_duration_seconds_to_minutes() {
165        let m = parse_value_with_unit(125.0, "s").unwrap();
166        let formatted = format_measurement(m);
167        assert!(formatted.contains("2:05"));
168    }
169
170    #[test]
171    fn test_data_size_kilobytes() {
172        let m = parse_value_with_unit(9000.0, "KB").unwrap();
173        assert_eq!(format_measurement(m), "9MB");
174    }
175
176    #[test]
177    fn test_data_rate_megabytes() {
178        let m = parse_value_with_unit(1500.0, "MB/s").unwrap();
179        assert_eq!(format_measurement(m), "1.5GB/s");
180    }
181
182    #[test]
183    fn test_unknown_unit_fallback() {
184        // Unknown units fallback to raw count
185        let m = parse_value_with_unit(42.5, "widgets").unwrap();
186        assert!(matches!(m, Measurement::Count(_)));
187    }
188
189    #[test]
190    fn test_nanoseconds() {
191        let m = parse_value_with_unit(1_000_000.0, "ns").unwrap();
192        let formatted = format_measurement(m);
193        // 1,000,000 ns = 1 ms
194        assert!(formatted.contains("ms") || formatted.contains("1"));
195    }
196
197    #[test]
198    fn test_bytes() {
199        let m = parse_value_with_unit(1024.0, "B").unwrap();
200        let formatted = format_measurement(m);
201        // Should be formatted as bytes
202        assert!(formatted.contains("1") || formatted.contains("B"));
203    }
204
205    #[test]
206    fn test_gigabytes() {
207        let m = parse_value_with_unit(2.5, "GB").unwrap();
208        assert_eq!(format_measurement(m), "2.5GB");
209    }
210
211    #[test]
212    fn test_hours() {
213        let m = parse_value_with_unit(2.0, "h").unwrap();
214        let formatted = format_measurement(m);
215        // 2 hours should be formatted appropriately
216        assert!(formatted.contains("2:00") || formatted.contains("h"));
217    }
218
219    #[test]
220    fn test_zero_values() {
221        let m = parse_value_with_unit(0.0, "ms").unwrap();
222        let formatted = format_measurement(m);
223        assert!(formatted.contains("0"));
224    }
225
226    #[test]
227    fn test_small_durations() {
228        let m = parse_value_with_unit(500.0, "ns").unwrap();
229        let formatted = format_measurement(m);
230        assert!(formatted.contains("ns") || formatted.contains("500"));
231    }
232
233    #[test]
234    fn test_bytes_unit_normalization() {
235        // Test that "bytes" is normalized to "B" and parsed correctly
236        let m = parse_value_with_unit(1000.0, "bytes").unwrap();
237        assert!(
238            matches!(m, Measurement::DataSize(_)),
239            "Should parse 'bytes' as DataSize, got: {:?}",
240            m
241        );
242        let formatted = format_measurement(m);
243        // human-repr auto-scales 1000B to 1kB
244        assert_eq!(formatted, "1kB");
245
246        // Test "byte" singular
247        let m = parse_value_with_unit(500.0, "byte").unwrap();
248        assert!(matches!(m, Measurement::DataSize(_)));
249        let formatted = format_measurement(m);
250        assert_eq!(formatted, "500B");
251
252        // Test other long forms
253        let m = parse_value_with_unit(5000.0, "kilobytes").unwrap();
254        assert!(matches!(m, Measurement::DataSize(_)));
255        // 5000 KB = 5 MB
256        assert_eq!(format_measurement(m), "5MB");
257
258        let m = parse_value_with_unit(2000.0, "megabytes").unwrap();
259        assert!(matches!(m, Measurement::DataSize(_)));
260        // 2000 MB = 2 GB
261        assert_eq!(format_measurement(m), "2GB");
262    }
263}