Skip to main content

netspeed_cli/formatter/
estimates.rs

1//! Usage check targets and real-world download time estimates.
2
3#![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
12// ── Target benchmarks ────────────────────────────────────────────────
13
14struct 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
42/// Build target usage check output as a string.
43pub 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
86/// Format target usage check against download speed.
87pub 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
94// ── Real-world download estimates ──────────────────────────────────────
95
96struct 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
144/// Build real-world download time estimates as a string.
145pub 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
180/// Format real-world download time estimates.
181pub 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}