netspeed_cli/formatter/
estimates.rs1#![allow(
4 clippy::cast_precision_loss,
5 clippy::cast_possible_truncation,
6 clippy::cast_sign_loss
7)]
8
9use crate::common;
10use owo_colors::OwoColorize;
11
12struct Target {
15 name: &'static str,
16 required_mbps: f64,
17}
18
19const TARGETS: &[Target] = &[
20 Target {
21 name: "Video calls (1080p)",
22 required_mbps: 3.0,
23 },
24 Target {
25 name: "HD streaming",
26 required_mbps: 5.0,
27 },
28 Target {
29 name: "4K streaming",
30 required_mbps: 25.0,
31 },
32 Target {
33 name: "Cloud gaming",
34 required_mbps: 35.0,
35 },
36 Target {
37 name: "Large file transfers",
38 required_mbps: 100.0,
39 },
40];
41
42pub fn build_targets(download_bps: Option<f64>, nc: bool) -> String {
44 let Some(dl) = download_bps else {
45 return String::new();
46 };
47 let dl_mbps = dl / 1_000_000.0;
48
49 let mut lines = Vec::new();
50
51 if nc {
52 lines.push("\n USAGE CHECK".to_string());
53 } else {
54 lines.push(format!("\n {}", "USAGE CHECK".bold().underline()));
55 }
56
57 for target in TARGETS {
58 let met = dl_mbps >= target.required_mbps;
59 let ratio = dl_mbps / target.required_mbps;
60 let suffix = if ratio >= 10.0 {
61 format!("{:.0}x", ratio)
62 } else {
63 format!("{:.1}x", ratio)
64 };
65 if met {
66 let line = format!("{:<26} ✅ {} above", target.name, suffix);
67 if nc {
68 lines.push(format!(" {line}"));
69 } else {
70 lines.push(format!(" {}", line.green()));
71 }
72 } else {
73 let shortfall = target.required_mbps - dl_mbps;
74 let line = format!("{:<26} ❌ {:.1} Mb/s short", target.name, shortfall);
75 if nc {
76 lines.push(format!(" {line}"));
77 } else {
78 lines.push(format!(" {}", line.red()));
79 }
80 }
81 }
82
83 lines.join("\n")
84}
85
86pub fn format_targets(download_bps: Option<f64>, nc: bool) {
88 let output = build_targets(download_bps, nc);
89 if !output.is_empty() {
90 eprintln!("{}", output);
91 }
92}
93
94struct FileEstimate {
97 name: &'static str,
98 size_bytes: u64,
99}
100
101const ESTIMATES: &[FileEstimate] = &[
102 FileEstimate {
103 name: "MP3 song",
104 size_bytes: 5 * 1024 * 1024,
105 },
106 FileEstimate {
107 name: "HD photo",
108 size_bytes: 5 * 1024 * 1024,
109 },
110 FileEstimate {
111 name: "App install",
112 size_bytes: 100 * 1024 * 1024,
113 },
114 FileEstimate {
115 name: "HD movie (4 GB)",
116 size_bytes: 4 * 1024 * 1024 * 1024,
117 },
118 FileEstimate {
119 name: "4K movie (15 GB)",
120 size_bytes: 15 * 1024 * 1024 * 1024,
121 },
122 FileEstimate {
123 name: "Game install (50 GB)",
124 size_bytes: 50 * 1024 * 1024 * 1024,
125 },
126];
127
128fn format_time_estimate(secs: f64, _nc: bool) -> String {
129 if secs < 1.0 {
130 format!("{:.1}s", secs)
131 } else if secs < 60.0 {
132 format!("{secs:.0}s")
133 } else if secs < 3600.0 {
134 format!("{}m {:02}s", secs as u64 / 60, (secs % 60.0) as u64)
135 } else {
136 format!(
137 "{}h {:02}m",
138 secs as u64 / 3600,
139 ((secs % 3600.0) / 60.0) as u64
140 )
141 }
142}
143
144pub fn build_estimates(download_bps: Option<f64>, nc: bool) -> String {
146 let Some(dl) = download_bps else {
147 return String::new();
148 };
149 let dl_bytes_per_sec = dl / 8.0;
150
151 let mut lines = Vec::new();
152
153 if nc {
154 lines.push("\n ESTIMATES".to_string());
155 } else {
156 lines.push(format!("\n {}", "ESTIMATES".bold().underline()));
157 }
158
159 for file in ESTIMATES {
160 let secs = file.size_bytes as f64 / dl_bytes_per_sec;
161 let time_str = format_time_estimate(secs, nc);
162 let label = format!(
163 "{:<24} ~{time_str}",
164 format!(
165 "{} ({})",
166 file.name,
167 common::format_data_size(file.size_bytes)
168 )
169 );
170 if nc {
171 lines.push(format!(" {label}"));
172 } else {
173 lines.push(format!(" {}", label.green()));
174 }
175 }
176
177 lines.join("\n")
178}
179
180pub fn format_estimates(download_bps: Option<f64>, nc: bool) {
182 let output = build_estimates(download_bps, nc);
183 if !output.is_empty() {
184 eprintln!("{}", output);
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 #[test]
193 fn test_format_time_estimate() {
194 assert!(format_time_estimate(0.5, false).contains("0.5s"));
195 assert!(format_time_estimate(30.0, false).contains("30s"));
196 assert!(format_time_estimate(120.0, false).contains("2m"));
197 }
198}