net_bytes/download_acceleration.rs
1use std::time::Duration;
2
3use crate::{FileSizeFormat, format_parts_scaled_signed, SCALE_I128};
4
5/// Download acceleration in bytes per second squared (fixed-point representation)
6///
7/// Internally stores `bytes_per_second_sq * SCALE` for precision.
8/// Supports negative values for deceleration.
9///
10/// 下载加速度(单位:字节每秒平方,定点数表示)
11///
12/// 内部存储 `bytes_per_second_sq * SCALE` 以保持精度。
13/// 支持负值表示减速。
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub struct DownloadAcceleration {
16 /// Scaled value: actual_bytes_per_second_sq * SCALE
17 scaled_bps_sq: i128,
18}
19
20impl DownloadAcceleration {
21 /// Create a new download acceleration from raw bytes per second squared
22 ///
23 /// # Parameters
24 /// - `bytes_per_second_sq`: Non-zero acceleration in bytes per second squared
25 ///
26 /// # Note
27 /// For negative acceleration, use `as u64` conversion of `i64` to ensure proper sign handling
28 ///
29 /// 从原始字节/秒²创建一个新的下载加速度实例
30 ///
31 /// # 参数
32 /// - `bytes_per_second_sq`: 非零的字节/秒²加速度值
33 ///
34 /// # 注意
35 /// 对于负加速度,使用 `i64` 的 `as u64` 转换,确保正确处理符号位
36 #[inline]
37 pub fn from_raw(bytes_per_second_sq: i64) -> Self {
38 Self {
39 scaled_bps_sq: (bytes_per_second_sq as i128) * SCALE_I128,
40 }
41 }
42
43 /// Create a new download acceleration from speed change and time interval
44 ///
45 /// Uses pure integer arithmetic with nanosecond precision.
46 ///
47 /// # Parameters
48 /// - `initial_speed`: Initial speed in bytes per second (can be zero)
49 /// - `final_speed`: Final speed in bytes per second (can be zero)
50 /// - `duration`: Time interval for the speed change
51 ///
52 /// 从下载速度变化量和时间间隔创建一个新的下载加速度实例
53 ///
54 /// 使用纯整数运算,保持纳秒级精度。
55 ///
56 /// # 参数
57 /// - `initial_speed`: 初始速度(字节/秒,可以为零)
58 /// - `final_speed`: 最终速度(字节/秒,可以为零)
59 /// - `duration`: 速度变化所用的时间
60 pub fn new(initial_speed: u64, final_speed: u64, duration: Duration) -> Self {
61 let nanos = duration.as_nanos();
62 if nanos == 0 {
63 return Self { scaled_bps_sq: 0 };
64 }
65
66 // speed_diff = final_speed - initial_speed (can be negative)
67 let speed_diff = final_speed as i128 - initial_speed as i128;
68
69 // acceleration = speed_diff / seconds = speed_diff / (nanos / 1e9) = speed_diff * 1e9 / nanos
70 // scaled_bps_sq = acceleration * SCALE = speed_diff * 1e9 * SCALE / nanos
71 let scaled_bps_sq = speed_diff * SCALE_I128 * 1_000_000_000 / nanos as i128;
72
73 Self { scaled_bps_sq }
74 }
75
76 /// Get the internal scaled value (for advanced usage)
77 ///
78 /// 获取内部缩放值(高级用途)
79 #[inline]
80 pub fn as_scaled(&self) -> i128 {
81 self.scaled_bps_sq
82 }
83
84 /// Get the acceleration in bytes per second squared as `i64` (floored)
85 ///
86 /// 以 `i64` 的形式获取字节每秒平方(向下取整)
87 #[inline]
88 pub fn as_i64(&self) -> i64 {
89 (self.scaled_bps_sq / SCALE_I128) as i64
90 }
91
92 /// Get the acceleration as f64 (for compatibility)
93 ///
94 /// 以 f64 的形式获取加速度(兼容用途)
95 #[inline]
96 pub fn as_f64(&self) -> f64 {
97 self.scaled_bps_sq as f64 / SCALE_I128 as f64
98 }
99
100 /// Fallback to linear prediction when quadratic solution is not applicable
101 ///
102 /// Returns time in seconds as f64.
103 ///
104 /// 当二次方程解不适用时,回退到线性预测
105 ///
106 /// 返回秒数(f64)。
107 #[inline]
108 fn linear_prediction(current_speed_f64: f64, remaining_bytes: u64) -> Option<f64> {
109 if current_speed_f64 > 0.0 {
110 Some(remaining_bytes as f64 / current_speed_f64)
111 } else {
112 None
113 }
114 }
115
116 /// Predicts the time remaining (in seconds) for a download to complete
117 /// considering acceleration.
118 ///
119 /// # Parameters
120 /// - `current_speed`: Current download speed in bytes per second
121 /// - `remaining_bytes`: Non-zero remaining bytes to download
122 ///
123 /// # Returns
124 /// - `Some(f64)`: Estimated time remaining in seconds
125 /// - `None`: If the time is infinite, or if the input is invalid
126 ///
127 /// # Behavior
128 /// - Returns `None` if `current_speed` is zero
129 /// - Falls back to linear prediction if acceleration is negligible
130 /// - Handles edge cases gracefully without panicking
131 ///
132 /// 预测考虑加速度的下载剩余时间(秒)
133 ///
134 /// # 参数
135 /// - `current_speed`: 当前下载速度(字节/秒)
136 /// - `remaining_bytes`: 剩余要下载的字节数(非零)
137 ///
138 /// # 返回
139 /// - `Some(f64)`: 估计的剩余时间(秒)
140 /// - `None`: 如果时间为无限大,或输入无效
141 ///
142 /// # 行为
143 /// - 如果 `current_speed` 为零,返回 `None`
144 /// - 如果加速度可忽略,则回退到线性预测
145 /// - 优雅处理边界情况,不会导致程序崩溃
146 pub fn predict_eta(&self, current_speed: u64, remaining_bytes: u64) -> Option<f64> {
147 let current_speed_f64 = current_speed as f64;
148
149 // Validate input
150 if current_speed == 0 {
151 return None;
152 }
153
154 let accel_f64 = self.as_f64();
155
156 // Threshold for considering acceleration as negligible (0.1 B/s²)
157 const ACCEL_THRESHOLD: f64 = 0.1;
158
159 // If acceleration is negligible, use linear prediction
160 if accel_f64.abs() < ACCEL_THRESHOLD {
161 return Self::linear_prediction(current_speed_f64, remaining_bytes);
162 }
163
164 // Solve quadratic equation: 0.5*a*t² + v*t - s = 0
165 self.solve_quadratic_eta(current_speed_f64, remaining_bytes, accel_f64)
166 }
167
168 /// Solves the quadratic equation for ETA prediction
169 ///
170 /// 求解二次方程以预测 ETA
171 fn solve_quadratic_eta(
172 &self,
173 current_speed: f64,
174 remaining_bytes: u64,
175 accel: f64,
176 ) -> Option<f64> {
177 // Coefficients: a*t² + b*t + c = 0
178 let a = 0.5 * accel;
179 let b = current_speed;
180 let c = -(remaining_bytes as f64);
181
182 // Calculate discriminant: b² - 4ac
183 let discriminant = b * b - 4.0 * a * c;
184
185 // If discriminant is negative, no real solution exists
186 if discriminant < 0.0 {
187 return Self::linear_prediction(current_speed, remaining_bytes);
188 }
189
190 // Calculate square root of discriminant
191 let sqrt_discriminant = discriminant.sqrt();
192 let two_a = 2.0 * a;
193
194 // Quadratic formula: t = (-b ± √discriminant) / (2a)
195 let t1 = (-b + sqrt_discriminant) / two_a;
196 let t2 = (-b - sqrt_discriminant) / two_a;
197
198 // Return the smallest positive root
199 match (t1 > 0.0, t2 > 0.0) {
200 (true, true) => Some(t1.min(t2)),
201 (true, false) => Some(t1),
202 (false, true) => Some(t2),
203 (false, false) => Self::linear_prediction(current_speed, remaining_bytes),
204 }
205 }
206}
207
208impl FileSizeFormat for DownloadAcceleration {
209 /// Returns the formatted value and unit in SI (base-1000) standard
210 ///
211 /// 返回 SI (base-1000) 标准的 (formatted_value, unit)
212 fn get_si_parts(&self) -> (String, &'static str) {
213 const UNITS: &[&str] = &[
214 "B/s²", "KB/s²", "MB/s²", "GB/s²", "TB/s²", "PB/s²", "EB/s²", "ZB/s²", "YB/s²",
215 ];
216 format_parts_scaled_signed(self.scaled_bps_sq, 1000, UNITS)
217 }
218
219 /// Returns the formatted value and unit in IEC (base-1024) standard
220 ///
221 /// 返回 IEC (base-1024) 标准的 (formatted_value, unit)
222 fn get_iec_parts(&self) -> (String, &'static str) {
223 const UNITS: &[&str] = &[
224 "B/s²", "KiB/s²", "MiB/s²", "GiB/s²", "TiB/s²", "PiB/s²", "EiB/s²", "ZiB/s²", "YiB/s²",
225 ];
226 format_parts_scaled_signed(self.scaled_bps_sq, 1024, UNITS)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::DownloadAcceleration;
233 use crate::{FormattedValue, SizeStandard};
234
235 /// Helper function - SI standard
236 ///
237 /// 辅助函数 - SI 标准
238 fn format_test_si(accel: i64) -> String {
239 FormattedValue::new(DownloadAcceleration::from_raw(accel), SizeStandard::SI).to_string()
240 }
241
242 /// Helper function - IEC standard
243 ///
244 /// 辅助函数 - IEC 标准
245 fn format_test_iec(accel: i64) -> String {
246 FormattedValue::new(DownloadAcceleration::from_raw(accel), SizeStandard::IEC).to_string()
247 }
248
249 #[test]
250 fn test_predict_eta() {
251 // Test with positive acceleration
252 let acc = DownloadAcceleration::from_raw(1000); // 1000 B/s²
253
254 // Current speed: 100 B/s, remaining: 1000 bytes
255 // With acceleration, it will complete before reaching 10s (linear prediction)
256 let eta = acc
257 .predict_eta(100, 1000)
258 .expect("Should have a valid ETA");
259 assert!(eta < 10.0); // Should be less than linear prediction (10s)
260 assert!(eta > 0.0);
261
262 // Test with negative acceleration (small deceleration)
263 let acc = DownloadAcceleration::from_raw(-50); // -50 B/s²
264
265 // Current speed: 200 B/s, remaining: 1000 bytes
266 // With small deceleration, it will take slightly longer than linear prediction (5s)
267 let eta = acc
268 .predict_eta(200, 1000)
269 .expect("Should have a valid ETA");
270 assert!(eta >= 5.0); // Should be at least linear prediction (5s)
271
272 // Test with zero acceleration (should match linear prediction)
273 let acc = DownloadAcceleration::from_raw(0);
274 let eta = acc
275 .predict_eta(100, 1000)
276 .expect("Should have a valid ETA");
277 assert!((eta - 10.0).abs() < 0.001); // 1000 / 100 = 10s
278
279 // Test with zero speed (should return None)
280 let eta = acc.predict_eta(0, 1000);
281 assert!(eta.is_none());
282
283 // Test with exact remaining bytes
284 let eta = acc
285 .predict_eta(100, 1000)
286 .expect("Should have a valid ETA");
287 assert!((eta - 10.0).abs() < 0.001);
288 }
289
290 #[test]
291 fn test_predict_eta_edge_cases() {
292 // Test with very large acceleration
293 let acc = DownloadAcceleration::from_raw(1_000_000);
294 let eta = acc
295 .predict_eta(1000, 1_000_000)
296 .expect("Should have a valid ETA");
297 assert!(eta > 0.0);
298 assert!(eta < 1000.0); // Should be much less than linear prediction
299
300 // Test with very small remaining bytes
301 let acc = DownloadAcceleration::from_raw(100);
302 let eta = acc
303 .predict_eta(1000, 1)
304 .expect("Should have a valid ETA");
305 assert!(eta > 0.0);
306 assert!(eta < 1.0);
307
308 // Test with negative acceleration that causes speed to become zero
309 let acc = DownloadAcceleration::from_raw(-100);
310 let eta = acc
311 .predict_eta(500, 2000)
312 .expect("Should have a valid ETA");
313 assert!(eta > 0.0);
314
315 // Test with threshold acceleration (0.1 B/s²) - should use linear prediction
316 let acc = DownloadAcceleration::from_raw(0);
317 let eta_linear = acc
318 .predict_eta(100, 1000)
319 .expect("Should have a valid ETA");
320
321 let acc_threshold = DownloadAcceleration::from_raw(0);
322 let eta_threshold = acc_threshold
323 .predict_eta(100, 1000)
324 .expect("Should have a valid ETA");
325
326 assert!((eta_linear - eta_threshold).abs() < 0.001);
327 }
328
329 // --- Tests for SI (base-1000) standard ---
330 // --- SI (base-1000) 测试 ---
331 #[test]
332 fn test_si_acceleration() {
333 // Test positive acceleration
334 assert_eq!(format_test_si(512), "512.0 B/s²");
335 assert_eq!(format_test_si(1000), "1.00 KB/s²");
336 assert_eq!(format_test_si(1024), "1.02 KB/s²");
337
338 // Test negative acceleration
339 assert_eq!(format_test_si(-1500), "-1.50 KB/s²");
340 }
341
342 // --- Tests for IEC (base-1024) standard ---
343 // --- IEC (base-1024) 测试 ---
344 #[test]
345 fn test_iec_acceleration() {
346 // Test positive acceleration
347 assert_eq!(format_test_iec(1024), "1.00 KiB/s²");
348 assert_eq!(format_test_iec(1500), "1.46 KiB/s²");
349
350 // Test negative acceleration
351 assert_eq!(format_test_iec(-2048), "-2.00 KiB/s²");
352 }
353
354 // --- Tests for `new` function ---
355 // --- `new` 函数测试 ---
356 #[test]
357 fn test_new_basic() {
358 use std::time::Duration;
359
360 // Speed increases from 100 to 200 B/s in 1 second = 100 B/s²
361 let acc = DownloadAcceleration::new(100, 200, Duration::from_secs(1));
362 assert_eq!(acc.as_i64(), 100);
363
364 // Speed increases from 0 to 1000 B/s in 2 seconds = 500 B/s²
365 let acc = DownloadAcceleration::new(0, 1000, Duration::from_secs(2));
366 assert_eq!(acc.as_i64(), 500);
367
368 // Speed increases from 100 to 600 B/s in 0.5 seconds = 1000 B/s²
369 let acc = DownloadAcceleration::new(100, 600, Duration::from_millis(500));
370 assert_eq!(acc.as_i64(), 1000);
371 }
372
373 #[test]
374 fn test_new_negative_acceleration() {
375 use std::time::Duration;
376
377 // Speed decreases from 200 to 100 B/s in 1 second = -100 B/s²
378 let acc = DownloadAcceleration::new(200, 100, Duration::from_secs(1));
379 assert_eq!(acc.as_i64(), -100);
380
381 // Speed decreases from 1000 to 0 B/s in 2 seconds = -500 B/s²
382 let acc = DownloadAcceleration::new(1000, 0, Duration::from_secs(2));
383 assert_eq!(acc.as_i64(), -500);
384 }
385
386 #[test]
387 fn test_new_zero_acceleration() {
388 use std::time::Duration;
389
390 // Speed stays the same = 0 B/s²
391 let acc = DownloadAcceleration::new(100, 100, Duration::from_secs(1));
392 assert_eq!(acc.as_i64(), 0);
393 assert_eq!(acc.as_scaled(), 0);
394 }
395
396 #[test]
397 fn test_new_zero_duration() {
398 use std::time::Duration;
399
400 // Any speed change in 0 duration = 0 B/s² (prevent division by zero)
401 let acc = DownloadAcceleration::new(100, 200, Duration::ZERO);
402 assert_eq!(acc.as_i64(), 0);
403 assert_eq!(acc.as_scaled(), 0);
404 }
405
406 #[test]
407 fn test_new_with_subsec_nanos() {
408 use std::time::Duration;
409
410 // Speed increases from 0 to 1500 B/s in 1.5 seconds = 1000 B/s²
411 let acc = DownloadAcceleration::new(0, 1500, Duration::from_millis(1500));
412 assert_eq!(acc.as_i64(), 1000);
413
414 // Speed increases from 0 to 1000 B/s in 100ms = 10000 B/s²
415 let acc = DownloadAcceleration::new(0, 1000, Duration::from_millis(100));
416 assert_eq!(acc.as_i64(), 10000);
417 }
418
419 #[test]
420 fn test_new_large_values() {
421 use std::time::Duration;
422
423 // 1 GB/s increase in 1 second = 1 GB/s²
424 let acc = DownloadAcceleration::new(0, 1_000_000_000, Duration::from_secs(1));
425 assert_eq!(acc.as_i64(), 1_000_000_000);
426
427 // 10 GB/s increase in 10 seconds = 1 GB/s²
428 let acc = DownloadAcceleration::new(0, 10_000_000_000, Duration::from_secs(10));
429 assert_eq!(acc.as_i64(), 1_000_000_000);
430 }
431}