1use crate::types::TestResult;
4use owo_colors::OwoColorize;
5
6#[must_use]
7pub fn ping_rating(ping_ms: f64) -> &'static str {
8 if ping_ms < 10.0 {
9 "Excellent"
10 } else if ping_ms < 30.0 {
11 "Good"
12 } else if ping_ms < 60.0 {
13 "Fair"
14 } else if ping_ms < 100.0 {
15 "Poor"
16 } else {
17 "Bad"
18 }
19}
20
21#[must_use]
22pub fn speed_rating_mbps(mbps: f64) -> &'static str {
23 if mbps >= 500.0 {
24 "Excellent"
25 } else if mbps >= 200.0 {
26 "Great"
27 } else if mbps >= 100.0 {
28 "Good"
29 } else if mbps >= 50.0 {
30 "Fair"
31 } else if mbps >= 25.0 {
32 "Moderate"
33 } else if mbps >= 10.0 {
34 "Slow"
35 } else {
36 "Very Slow"
37 }
38}
39
40pub fn colorize_rating(rating: &str, nc: bool) -> String {
41 if nc {
42 rating.to_string()
43 } else {
44 match rating {
45 "Excellent" => format!("{}{}", "⚡ ".green().bold(), rating.green().bold()),
46 "Great" => format!("{}{}", "🔵 ".blue(), rating.blue()),
47 "Good" => format!("{}{}", "🟢 ".bright_green(), rating.bright_green()),
48 "Fair" => format!("{}{}", "🟡 ".yellow(), rating.yellow()),
49 "Moderate" => format!("{}{}", "🟠 ".bright_yellow(), rating.bright_yellow()),
50 "Poor" => format!("{}{}", "🔴 ".red(), rating.red()),
51 "Slow" => format!("{}{}", "🟤 ".bright_red(), rating.bright_red()),
52 "Very Slow" => format!("{}{}", "⚠️ ".red().bold(), rating.red().bold()),
53 _ => rating.to_string(),
54 }
55 }
56}
57
58struct SpeedComponents {
60 value: f64,
61 unit: &'static str,
62}
63
64fn speed_components(bps: f64, bytes: bool) -> SpeedComponents {
66 let divider = if bytes { 8.0 } else { 1.0 };
67 let unit = if bytes { "MB/s" } else { "Mb/s" };
68 let value = bps / divider / 1_000_000.0;
69 SpeedComponents { value, unit }
70}
71
72pub fn format_speed_colored(bps: f64, bytes: bool) -> String {
73 let SpeedComponents { value, unit } = speed_components(bps, bytes);
74 let mbps = bps / 1_000_000.0;
75 let rating = speed_rating_mbps(mbps);
76 match rating {
77 "Excellent" | "Great" => format!("{value:.2} {unit}").green().bold().to_string(),
78 "Good" => format!("{value:.2} {unit}").bright_green().to_string(),
79 "Fair" | "Moderate" => format!("{value:.2} {unit}").yellow().to_string(),
80 "Poor" | "Slow" | "Very Slow" => format!("{value:.2} {unit}").red().to_string(),
81 _ => format!("{value:.2} {unit}"),
82 }
83}
84
85pub fn format_speed_plain(bps: f64, bytes: bool) -> String {
86 let SpeedComponents { value, unit } = speed_components(bps, bytes);
87 format!("{value:.2} {unit}")
88}
89
90pub fn format_duration(secs: f64) -> String {
91 if secs < 60.0 {
92 format!("{secs:.1}s")
93 } else {
94 let mins = secs as u64 / 60;
95 let secs = secs % 60.0;
96 format!("{mins}m {secs:.0}s")
97 }
98}
99
100#[must_use]
101pub fn connection_rating(result: &TestResult) -> &'static str {
102 fn score_lower(value: f64, thresholds: [f64; 5]) -> f64 {
104 if value < thresholds[0] {
105 100.0
106 } else if value < thresholds[1] {
107 80.0
108 } else if value < thresholds[2] {
109 60.0
110 } else if value < thresholds[3] {
111 40.0
112 } else {
113 20.0
114 }
115 }
116
117 fn score_higher(mbps: f64, thresholds: [f64; 6]) -> f64 {
119 if mbps >= thresholds[0] {
120 100.0
121 } else if mbps >= thresholds[1] {
122 85.0
123 } else if mbps >= thresholds[2] {
124 70.0
125 } else if mbps >= thresholds[3] {
126 55.0
127 } else if mbps >= thresholds[4] {
128 40.0
129 } else if mbps >= thresholds[5] {
130 25.0
131 } else {
132 10.0
133 }
134 }
135
136 let mut score = 0.0;
137 let mut factors = 0.0;
138
139 if let Some(ping) = result.ping {
141 score += score_lower(ping, [10.0, 30.0, 60.0, 100.0, f64::MAX]);
142 factors += 1.0;
143 }
144
145 if let Some(jitter) = result.jitter {
147 score += score_lower(jitter, [2.0, 5.0, 10.0, 20.0, f64::MAX]);
148 factors += 1.0;
149 }
150
151 if let Some(dl) = result.download {
153 score += score_higher(dl / 1_000_000.0, [500.0, 200.0, 100.0, 50.0, 25.0, 10.0]);
154 factors += 1.0;
155 }
156
157 if let Some(ul) = result.upload {
159 score += score_higher(ul / 1_000_000.0, [500.0, 200.0, 100.0, 50.0, 25.0, 10.0]);
160 factors += 1.0;
161 }
162
163 if factors == 0.0 {
164 return "Unknown";
165 }
166
167 let avg = score / factors;
168 if avg >= 90.0 {
169 "Excellent"
170 } else if avg >= 75.0 {
171 "Great"
172 } else if avg >= 55.0 {
173 "Good"
174 } else if avg >= 40.0 {
175 "Fair"
176 } else if avg >= 25.0 {
177 "Moderate"
178 } else {
179 "Poor"
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
185pub enum BufferbloatGrade {
186 A,
187 B,
188 C,
189 D,
190 F,
191}
192
193impl BufferbloatGrade {
194 #[must_use]
195 pub fn as_str(&self) -> &'static str {
196 match self {
197 Self::A => "A",
198 Self::B => "B",
199 Self::C => "C",
200 Self::D => "D",
201 Self::F => "F",
202 }
203 }
204}
205
206#[must_use]
208pub fn bufferbloat_grade(load_latency: f64, idle_latency: f64) -> (BufferbloatGrade, f64) {
209 let added = if idle_latency > 0.0 {
210 load_latency - idle_latency
211 } else {
212 load_latency
213 };
214 let grade = if added < 5.0 {
215 BufferbloatGrade::A
216 } else if added < 20.0 {
217 BufferbloatGrade::B
218 } else if added < 50.0 {
219 BufferbloatGrade::C
220 } else if added < 100.0 {
221 BufferbloatGrade::D
222 } else {
223 BufferbloatGrade::F
224 };
225 (grade, added.max(0.0))
226}
227
228pub fn bufferbloat_colorized(grade: BufferbloatGrade, added_ms: f64, nc: bool) -> String {
229 if nc {
230 format!("{} ({added_ms:.0}ms)", grade.as_str())
231 } else {
232 let (color, bold) = match grade {
233 BufferbloatGrade::A => ("green", true),
234 BufferbloatGrade::B => ("bright_green", false),
235 BufferbloatGrade::C => ("yellow", false),
236 BufferbloatGrade::D => ("bright_yellow", false),
237 BufferbloatGrade::F => ("red", true),
238 };
239 let text = format!("{} ({added_ms:.0}ms added)", grade.as_str());
240 match (color, bold) {
241 ("green", true) => text.green().bold().to_string(),
242 ("bright_green", _) => text.bright_green().to_string(),
243 ("yellow", _) => text.yellow().to_string(),
244 ("bright_yellow", _) => text.bright_yellow().to_string(),
245 ("red", true) => text.red().bold().to_string(),
246 _ => text.dimmed().to_string(),
247 }
248 }
249}
250
251pub fn format_overall_rating(result: &TestResult, nc: bool) -> String {
252 let rating = connection_rating(result);
253 if nc {
254 format!(" Overall: {rating}")
255 } else {
256 let (icon, color) = match rating {
257 "Excellent" => ("⚡ ", "green"),
258 "Great" => ("🔵 ", "blue"),
259 "Good" => ("🟢 ", "bright_green"),
260 "Fair" => ("🟡 ", "yellow"),
261 "Moderate" => ("🟠 ", "bright_yellow"),
262 "Poor" => ("🔴 ", "red"),
263 _ => ("", ""),
264 };
265 let text = format!("{icon}{rating}");
266 let colored = match color {
267 "green" => text.green().bold().to_string(),
268 "blue" => text.blue().to_string(),
269 "bright_green" => text.bright_green().to_string(),
270 "yellow" => text.yellow().to_string(),
271 "bright_yellow" => text.bright_yellow().to_string(),
272 "red" => text.red().to_string(),
273 _ => text.dimmed().to_string(),
274 };
275 format!(" {} {colored}", "Overall:".dimmed())
276 }
277}
278
279pub fn degradation_str(lat_load: f64, idle_ping: Option<f64>, nc: bool) -> String {
280 let Some(idle) = idle_ping else {
281 return String::new();
282 };
283 if idle <= 0.0 {
284 return String::new();
285 }
286 let pct = ((lat_load / idle) - 1.0) * 100.0;
287 let (label, color) = if pct < 25.0 {
288 ("minimal", "green")
289 } else if pct < 50.0 {
290 ("moderate", "yellow")
291 } else {
292 ("significant", "red")
293 };
294 let text = format!("+{pct:.0}% ({label})");
295 if nc {
296 format!(" [{text:>8}]")
297 } else {
298 let colored = match color {
299 "green" => text.green().to_string(),
300 "yellow" => text.yellow().to_string(),
301 "red" => text.red().to_string(),
302 _ => text.dimmed().to_string(),
303 };
304 format!(" {colored}")
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_ping_rating() {
314 assert_eq!(ping_rating(5.0), "Excellent");
315 assert_eq!(ping_rating(20.0), "Good");
316 assert_eq!(ping_rating(50.0), "Fair");
317 assert_eq!(ping_rating(80.0), "Poor");
318 assert_eq!(ping_rating(150.0), "Bad");
319 }
320
321 #[test]
322 fn test_speed_rating() {
323 assert_eq!(speed_rating_mbps(600.0), "Excellent");
324 assert_eq!(speed_rating_mbps(150.0), "Good");
325 assert_eq!(speed_rating_mbps(5.0), "Very Slow");
326 }
327
328 #[test]
329 fn test_format_duration() {
330 assert_eq!(format_duration(30.0), "30.0s");
331 assert_eq!(format_duration(90.0), "1m 30s");
332 }
333}