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