Skip to main content

netspeed_cli/
common.rs

1//! Common shared utilities used across download, upload, formatting, and progress modules.
2//!
3//! This module consolidates duplicated functionality to follow DRY principles:
4//! - Bandwidth calculation
5//! - Stream count determination
6//! - Distance formatting
7//! - Data size formatting
8
9/// Calculate bandwidth in bits per second from bytes transferred and elapsed time.
10///
11/// # Examples
12///
13/// ```
14/// # use netspeed_cli::common::calculate_bandwidth;
15/// let bps = calculate_bandwidth(10_000_000, 2.0);
16/// assert_eq!(bps, 40_000_000.0);
17/// ```
18#[must_use]
19pub fn calculate_bandwidth(total_bytes: u64, elapsed_secs: f64) -> f64 {
20    if elapsed_secs > 0.0 {
21        (total_bytes as f64 * 8.0) / elapsed_secs
22    } else {
23        0.0
24    }
25}
26
27/// Determine number of concurrent streams based on single connection flag.
28///
29/// Returns 1 for single connection mode, 4 for multi-stream mode.
30///
31/// # Examples
32///
33/// ```
34/// # use netspeed_cli::common::determine_stream_count;
35/// assert_eq!(determine_stream_count(true), 1);
36/// assert_eq!(determine_stream_count(false), 4);
37/// ```
38#[must_use]
39pub fn determine_stream_count(single: bool) -> usize {
40    if single { 1 } else { 4 }
41}
42
43/// Format distance consistently: 1 decimal for < 100 km, 0 decimals for >= 100 km.
44///
45/// # Examples
46///
47/// ```
48/// # use netspeed_cli::common::format_distance;
49/// assert_eq!(format_distance(50.5), "50.5 km");
50/// assert_eq!(format_distance(150.5), "150 km");
51/// ```
52#[must_use]
53pub fn format_distance(km: f64) -> String {
54    if km < 100.0 {
55        format!("{km:.1} km")
56    } else {
57        format!("{km:.0} km")
58    }
59}
60
61/// Format byte count into a human-readable string (KB, MB, GB).
62///
63/// # Examples
64///
65/// ```
66/// # use netspeed_cli::common::format_data_size;
67/// assert!(format_data_size(512).contains("KB"));
68/// assert!(format_data_size(1_048_576).contains("MB"));
69/// assert!(format_data_size(1_073_741_824).contains("GB"));
70/// ```
71#[must_use]
72pub fn format_data_size(bytes: u64) -> String {
73    if bytes < 1024 * 1024 {
74        format!("{:.1} KB", bytes as f64 / 1024.0)
75    } else if bytes < 1024 * 1024 * 1024 {
76        format!("{:.1} MB", bytes as f64 / (1024.0 * 1024.0))
77    } else {
78        format!("{:.2} GB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
79    }
80}
81
82/// Validate an IPv4 address string.
83///
84/// # Examples
85///
86/// ```
87/// # use netspeed_cli::common::is_valid_ipv4;
88/// assert!(is_valid_ipv4("192.168.1.1"));
89/// assert!(!is_valid_ipv4("999.999.999.999"));
90/// ```
91#[must_use]
92pub fn is_valid_ipv4(s: &str) -> bool {
93    let parts: Vec<&str> = s.split('.').collect();
94    if parts.len() != 4 {
95        return false;
96    }
97    parts.iter().all(|p| p.parse::<u8>().is_ok())
98}
99
100/// Render a horizontal bar chart using Unicode block characters.
101///
102/// `value` and `max` define the proportion. `width` is the bar length in chars.
103/// Returns filled (`█`) and empty (`░`) segments. Pure text — callers apply
104/// color via `owo_colors` for consistency.
105///
106/// # Examples
107///
108/// ```
109/// # use netspeed_cli::common::bar_chart;
110/// let bar = bar_chart(50.0, 100.0, 10);
111/// assert_eq!(bar.chars().count(), 10);
112/// ```
113#[must_use]
114pub fn bar_chart(value: f64, max: f64, width: usize) -> String {
115    if max <= 0.0 || width == 0 {
116        return "░".repeat(width);
117    }
118    let pct = (value / max).clamp(0.0, 1.0);
119    let filled = (pct * width as f64).round() as usize;
120    let empty = width.saturating_sub(filled);
121    format!("{}{}", "█".repeat(filled), "░".repeat(empty))
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn test_calculate_bandwidth_normal() {
130        assert_eq!(calculate_bandwidth(10_000_000, 2.0), 40_000_000.0);
131    }
132
133    #[test]
134    fn test_calculate_bandwidth_zero_elapsed() {
135        assert_eq!(calculate_bandwidth(10_000_000, 0.0), 0.0);
136    }
137
138    #[test]
139    fn test_determine_stream_count_single() {
140        assert_eq!(determine_stream_count(true), 1);
141    }
142
143    #[test]
144    fn test_determine_stream_count_multi() {
145        assert_eq!(determine_stream_count(false), 4);
146    }
147
148    #[test]
149    fn test_format_distance_under_100() {
150        assert_eq!(format_distance(50.5), "50.5 km");
151        assert_eq!(format_distance(99.9), "99.9 km");
152    }
153
154    #[test]
155    fn test_format_distance_100_plus() {
156        assert_eq!(format_distance(100.0), "100 km");
157        assert_eq!(format_distance(150.5), "150 km");
158    }
159
160    #[test]
161    fn test_format_data_size_bytes() {
162        assert!(format_data_size(512).contains("KB"));
163    }
164
165    #[test]
166    fn test_format_data_size_kilobytes() {
167        assert!(format_data_size(500 * 1024).contains("KB"));
168    }
169
170    #[test]
171    fn test_format_data_size_megabytes() {
172        assert!(format_data_size(10 * 1024 * 1024).contains("MB"));
173    }
174
175    #[test]
176    fn test_format_data_size_gigabytes() {
177        assert!(format_data_size(4 * 1024 * 1024 * 1024).contains("GB"));
178    }
179
180    #[test]
181    fn test_is_valid_ipv4_valid() {
182        assert!(is_valid_ipv4("192.168.1.1"));
183        assert!(is_valid_ipv4("0.0.0.0"));
184        assert!(is_valid_ipv4("255.255.255.255"));
185    }
186
187    #[test]
188    fn test_is_valid_ipv4_invalid() {
189        assert!(!is_valid_ipv4("256.1.1.1"));
190        assert!(!is_valid_ipv4("1.2.3"));
191        assert!(!is_valid_ipv4("abc"));
192        assert!(!is_valid_ipv4(""));
193        assert!(!is_valid_ipv4("1.2.3.4.5"));
194    }
195
196    #[test]
197    fn test_bar_chart_half() {
198        let bar = bar_chart(50.0, 100.0, 10);
199        assert_eq!(bar.chars().count(), 10);
200        assert_eq!(bar, "█████░░░░░");
201    }
202
203    #[test]
204    fn test_bar_chart_full() {
205        let bar = bar_chart(100.0, 100.0, 10);
206        assert_eq!(bar.chars().count(), 10);
207        assert_eq!(bar, "██████████");
208    }
209
210    #[test]
211    fn test_bar_chart_empty_val() {
212        let bar = bar_chart(0.0, 100.0, 10);
213        assert_eq!(bar, "░░░░░░░░░░");
214    }
215
216    #[test]
217    fn test_bar_chart_zero_max() {
218        let bar = bar_chart(50.0, 0.0, 10);
219        assert_eq!(bar, "░░░░░░░░░░");
220    }
221
222    #[test]
223    fn test_bar_chart_zero_width() {
224        let bar = bar_chart(50.0, 100.0, 0);
225        assert_eq!(bar, "");
226    }
227
228    #[test]
229    fn test_bar_chart_over_max() {
230        let bar = bar_chart(150.0, 100.0, 10);
231        assert_eq!(bar, "██████████"); // clamped to 100%
232    }
233}