netspeed_cli/formatter/
stability.rs1#![allow(
4 clippy::cast_precision_loss,
5 clippy::cast_possible_truncation,
6 clippy::cast_sign_loss
7)]
8
9use owo_colors::OwoColorize;
10
11#[must_use]
13pub fn compute_cv(speeds: &[f64]) -> f64 {
14 if speeds.is_empty() {
15 return 0.0;
16 }
17 let n = speeds.len() as f64;
18 let mean = speeds.iter().sum::<f64>() / n;
19 if mean <= 0.0 {
20 return 0.0;
21 }
22 let variance = speeds.iter().map(|s| (s - mean).powi(2)).sum::<f64>() / n;
23 let stddev = variance.sqrt();
24 (stddev / mean) * 100.0
25}
26
27pub fn format_stability_line(cv: f64, nc: bool) -> String {
28 let (color, label) = if cv < 5.0 {
29 ("green", "rock-solid")
30 } else if cv < 10.0 {
31 ("bright_green", "very stable")
32 } else if cv < 20.0 {
33 ("yellow", "moderate")
34 } else if cv < 35.0 {
35 ("bright_yellow", "variable")
36 } else {
37 ("red", "unstable")
38 };
39 let text = format!("±{cv:.0}% {label}");
40 if nc {
41 text
42 } else {
43 match color {
44 "green" => text.green().to_string(),
45 "bright_green" => text.bright_green().to_string(),
46 "yellow" => text.yellow().to_string(),
47 "bright_yellow" => text.bright_yellow().to_string(),
48 "red" => text.red().bold().to_string(),
49 _ => text.to_string(),
50 }
51 }
52}
53
54pub fn compute_percentiles(samples: &[f64]) -> Option<(f64, f64, f64)> {
55 let n = samples.len();
56 if n < 3 {
57 return None;
58 }
59 let mut data = samples.to_vec();
60 let p50_idx = n * 50 / 100;
61 let p95_idx = (n * 95 / 100).min(n - 1);
62 let p99_idx = (n * 99 / 100).min(n - 1);
63
64 data.select_nth_unstable_by(p99_idx, |a, b| {
66 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
67 });
68 let p99 = data[p99_idx];
69
70 data.select_nth_unstable_by(p95_idx, |a, b| {
72 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
73 });
74 let p95 = data[p95_idx];
75
76 data.select_nth_unstable_by(p50_idx, |a, b| {
78 a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
79 });
80 let p50 = data[p50_idx];
81
82 Some((p50, p95, p99))
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_compute_cv_constant() {
91 let speeds = vec![100.0, 100.0, 100.0];
92 assert_eq!(compute_cv(&speeds), 0.0);
93 }
94
95 #[test]
96 fn test_compute_cv_empty() {
97 assert_eq!(compute_cv(&[]), 0.0);
98 }
99
100 #[test]
101 fn test_compute_percentiles_basic() {
102 let samples: Vec<f64> = (1..=100).map(|x| x as f64).collect();
103 let result = compute_percentiles(&samples);
104 assert!(result.is_some());
105 let (p50, p95, p99) = result.unwrap();
106 assert!((p50 - 51.0).abs() < 1.0);
108 assert!((p95 - 96.0).abs() < 1.0);
109 assert!((p99 - 100.0).abs() < 1.0);
110 }
111
112 #[test]
113 fn test_compute_percentiles_too_few() {
114 assert!(compute_percentiles(&[1.0, 2.0]).is_none());
115 }
116}