net_bytes/
download_acceleration.rs

1use std::time::Duration;
2
3use rust_decimal::{Decimal, MathematicalOps, dec};
4
5use crate::{FileSizeFormat, format_parts};
6
7/// Download acceleration in bytes per second squared
8///
9/// 下载加速度(单位:字节每秒平方)
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub struct DownloadAcceleration {
12    bytes_per_second_sq: Decimal,
13}
14
15impl DownloadAcceleration {
16    /// Create a new download acceleration from raw bytes per second squared
17    ///
18    /// # Parameters
19    /// - `bytes_per_second_sq`: Non-zero acceleration in bytes per second squared
20    ///
21    /// # Note
22    /// For negative acceleration, use `as u64` conversion of `i64` to ensure proper sign handling
23    ///
24    /// 从原始字节/秒²创建一个新的下载加速度实例
25    ///
26    /// # 参数
27    /// - `bytes_per_second_sq`: 非零的字节/秒²加速度值
28    ///
29    /// # 注意
30    /// 对于负加速度,使用 `i64` 的 `as u64` 转换,确保正确处理符号位
31    #[inline]
32    pub fn from_raw(bytes_per_second_sq: i64) -> Self {
33        Self {
34            bytes_per_second_sq: Decimal::from(bytes_per_second_sq),
35        }
36    }
37
38    /// Create a new download acceleration from speed change and time interval
39    ///
40    /// # Parameters
41    /// - `initial_speed`: Initial speed in bytes per second (can be zero)
42    /// - `final_speed`: Final speed in bytes per second (can be zero)
43    /// - `duration`: Time interval for the speed change
44    ///
45    /// 从下载速度变化量和时间间隔创建一个新的下载加速度实例
46    ///
47    /// # 参数
48    /// - `initial_speed`: 初始速度(字节/秒,可以为零)
49    /// - `final_speed`: 最终速度(字节/秒,可以为零)
50    /// - `duration`: 速度变化所用的时间
51    pub fn new(initial_speed: u64, final_speed: u64, duration: Duration) -> Self {
52        let seconds = Decimal::from(duration.as_secs())
53            + Decimal::from(duration.subsec_nanos()) / Decimal::from(1_000_000_000);
54        let speed_diff = Decimal::from(final_speed) - Decimal::from(initial_speed);
55        let bytes_per_second_sq = if seconds.is_zero() {
56            Decimal::ZERO
57        } else {
58            speed_diff / seconds
59        };
60
61        Self {
62            bytes_per_second_sq,
63        }
64    }
65
66    /// Get the acceleration in bytes per second squared as a `Decimal`
67    ///
68    /// 以 `Decimal` 的形式获取字节每秒平方
69    #[inline]
70    pub fn as_decimal(&self) -> Decimal {
71        self.bytes_per_second_sq
72    }
73
74    /// Get the acceleration in bytes per second squared as `i64` (floored)
75    ///
76    /// 以 `i64` 的形式获取字节每秒平方(向下取整)
77    #[inline]
78    pub fn as_i64(&self) -> i64 {
79        self.bytes_per_second_sq.floor().try_into().unwrap_or(0)
80    }
81
82    fn format_parts(
83        &self,
84        base: Decimal,
85        units: &'static [&'static str],
86    ) -> (String, &'static str) {
87        let mut value = Decimal::from(self.bytes_per_second_sq);
88        let is_negative = value.is_sign_negative();
89
90        if is_negative {
91            value.set_sign_positive(true);
92        }
93
94        let (formatted_value, unit) = format_parts(value, base, units);
95        let formatted_value = if is_negative {
96            format!("-{}", formatted_value)
97        } else {
98            formatted_value
99        };
100
101        (formatted_value, unit)
102    }
103
104    /// Fallback to linear prediction when quadratic solution is not applicable
105    ///
106    /// 当二次方程解不适用时,回退到线性预测
107    #[inline]
108    fn linear_prediction(current_speed: Decimal, remaining_bytes: Decimal) -> Option<Decimal> {
109        (current_speed > Decimal::ZERO).then(|| remaining_bytes / current_speed)
110    }
111
112    /// Predicts the time remaining (in seconds) for a download to complete
113    /// considering acceleration.
114    ///
115    /// # Parameters
116    /// - `current_speed`: Current download speed in bytes per second as Decimal
117    /// - `remaining_bytes`: Non-zero remaining bytes to download
118    ///
119    /// # Returns
120    /// - `Some(Decimal)`: Estimated time remaining in seconds
121    /// - `None`: If the time is infinite, or if the input is invalid (negative speed or overflow)
122    ///
123    /// # Behavior
124    /// - Returns `None` if `current_speed` is negative or zero
125    /// - Falls back to linear prediction if acceleration is negligible or arithmetic operations would overflow
126    /// - Handles edge cases gracefully without panicking
127    ///
128    /// 预测考虑加速度的下载剩余时间(秒)
129    ///
130    /// # 参数
131    /// - `current_speed`: 当前下载速度(字节/秒),使用 Decimal 类型
132    /// - `remaining_bytes`: 剩余要下载的字节数(非零)
133    ///
134    /// # 返回
135    /// - `Some(Decimal)`: 估计的剩余时间(秒)
136    /// - `None`: 如果时间为无限大,或输入无效(速度为负或溢出)
137    ///
138    /// # 行为
139    /// - 如果 `current_speed` 为负或零,返回 `None`
140    /// - 如果加速度可忽略或算术运算可能溢出,则回退到线性预测
141    /// - 优雅处理边界情况,不会导致程序崩溃
142    pub fn predict_eta(&self, current_speed: Decimal, remaining_bytes: u64) -> Option<Decimal> {
143        // Validate input
144        if !current_speed.is_sign_positive() {
145            return None;
146        }
147
148        let remaining_bytes_decimal = Decimal::from(remaining_bytes);
149        let accel = self.bytes_per_second_sq;
150
151        // Threshold for considering acceleration as negligible (0.1 B/s²)
152        const ACCEL_THRESHOLD: Decimal = dec!(0.1);
153
154        // If acceleration is negligible, use linear prediction
155        if accel.abs() < ACCEL_THRESHOLD {
156            return Self::linear_prediction(current_speed, remaining_bytes_decimal);
157        }
158
159        // Solve quadratic equation: 0.5*a*t² + v*t - s = 0
160        self.solve_quadratic_eta(current_speed, remaining_bytes_decimal, accel)
161    }
162
163    /// Solves the quadratic equation for ETA prediction
164    ///
165    /// 求解二次方程以预测 ETA
166    fn solve_quadratic_eta(
167        &self,
168        current_speed: Decimal,
169        remaining_bytes: Decimal,
170        accel: Decimal,
171    ) -> Option<Decimal> {
172        const HALF: Decimal = dec!(0.5);
173        const TWO: Decimal = dec!(2);
174
175        // Coefficients: a*t² + b*t + c = 0
176        let a = HALF * accel;
177        let b = current_speed;
178        let c = -remaining_bytes;
179
180        // Calculate discriminant: b² - 4ac
181        let discriminant = self.calculate_discriminant(a, b, c)?;
182
183        // If discriminant is negative, no real solution exists
184        if discriminant < Decimal::ZERO {
185            return Self::linear_prediction(current_speed, remaining_bytes);
186        }
187
188        // Calculate square root of discriminant
189        let sqrt_discriminant = discriminant.sqrt()?;
190        let two_a = TWO * a;
191
192        // Quadratic formula: t = (-b ± √discriminant) / (2a)
193        let t1 = (-b + sqrt_discriminant) / two_a;
194        let t2 = (-b - sqrt_discriminant) / two_a;
195
196        // Return the smallest positive root
197        match (t1.is_sign_positive(), t2.is_sign_positive()) {
198            (true, true) => Some(t1.min(t2)),
199            (true, false) => Some(t1),
200            (false, true) => Some(t2),
201            (false, false) => Self::linear_prediction(current_speed, remaining_bytes),
202        }
203    }
204
205    /// Calculates the discriminant of the quadratic equation
206    ///
207    /// 计算二次方程的判别式
208    #[inline]
209    fn calculate_discriminant(&self, a: Decimal, b: Decimal, c: Decimal) -> Option<Decimal> {
210        let b_squared = b.checked_mul(b)?;
211        let four_ac = dec!(4).checked_mul(a)?.checked_mul(c)?;
212        b_squared.checked_sub(four_ac)
213    }
214}
215
216impl FileSizeFormat for DownloadAcceleration {
217    /// Returns the formatted value and unit in SI (base-1000) standard
218    ///
219    /// 返回 SI (base-1000) 标准的 (formatted_value, unit)
220    fn get_si_parts(&self) -> (String, &'static str) {
221        const UNITS: &[&str] = &[
222            "B/s²", "KB/s²", "MB/s²", "GB/s²", "TB/s²", "PB/s²", "EB/s²", "ZB/s²", "YB/s²",
223        ];
224        self.format_parts(Decimal::from(1000), UNITS)
225    }
226
227    /// Returns the formatted value and unit in IEC (base-1024) standard
228    ///
229    /// 返回 IEC (base-1024) 标准的 (formatted_value, unit)
230    fn get_iec_parts(&self) -> (String, &'static str) {
231        const UNITS: &[&str] = &[
232            "B/s²", "KiB/s²", "MiB/s²", "GiB/s²", "TiB/s²", "PiB/s²", "EiB/s²", "ZiB/s²", "YiB/s²",
233        ];
234        self.format_parts(Decimal::from(1024), UNITS)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use rust_decimal::Decimal;
241
242    use super::DownloadAcceleration;
243    use crate::{FormattedValue, SizeStandard};
244    /// Helper function - SI standard
245    ///
246    /// 辅助函数 - SI 标准
247    fn format_test_si(accel: i64) -> String {
248        FormattedValue::new(DownloadAcceleration::from_raw(accel), SizeStandard::SI).to_string()
249    }
250
251    /// Helper function - IEC standard
252    ///
253    /// 辅助函数 - IEC 标准
254    fn format_test_iec(accel: i64) -> String {
255        FormattedValue::new(DownloadAcceleration::from_raw(accel), SizeStandard::IEC).to_string()
256    }
257
258    #[test]
259    fn test_predict_eta() {
260        // Test with positive acceleration
261        let acc = DownloadAcceleration::from_raw(1000); // 1000 B/s²
262
263        // Current speed: 100 B/s, remaining: 1000 bytes
264        // With acceleration, it will complete before reaching 10s (linear prediction)
265        let eta = acc
266            .predict_eta(Decimal::new(100, 0), 1000)
267            .expect("Should have a valid ETA");
268        assert!(eta < Decimal::new(10, 0)); // Should be less than linear prediction (10s)
269        assert!(eta > Decimal::ZERO);
270
271        // Test with negative acceleration (small deceleration)
272        let acc = DownloadAcceleration::from_raw(-50); // -50 B/s²
273
274        // Current speed: 200 B/s, remaining: 1000 bytes
275        // With small deceleration, it will take slightly longer than linear prediction (5s)
276        let eta = acc
277            .predict_eta(Decimal::new(200, 0), 1000)
278            .expect("Should have a valid ETA");
279        assert!(eta >= Decimal::new(5, 0)); // Should be at least linear prediction (5s)
280
281        // Test with zero acceleration (should match linear prediction)
282        let acc = DownloadAcceleration::from_raw(0);
283        let eta = acc
284            .predict_eta(Decimal::new(100, 0), 1000)
285            .expect("Should have a valid ETA");
286        assert_eq!(eta, Decimal::new(10, 0)); // 1000 / 100 = 10s
287
288        // Test with zero speed (should return None)
289        let eta = acc.predict_eta(Decimal::ZERO, 1000);
290        assert!(eta.is_none());
291
292        // Test with exact remaining bytes
293        let eta = acc
294            .predict_eta(Decimal::new(100, 0), 1000)
295            .expect("Should have a valid ETA");
296        assert_eq!(eta, Decimal::new(10, 0));
297    }
298
299    #[test]
300    fn test_predict_eta_edge_cases() {
301        // Test with very large acceleration
302        let acc = DownloadAcceleration::from_raw(1_000_000);
303        let eta = acc
304            .predict_eta(Decimal::new(1000, 0), 1_000_000)
305            .expect("Should have a valid ETA");
306        assert!(eta > Decimal::ZERO);
307        assert!(eta < Decimal::new(1000, 0)); // Should be much less than linear prediction
308
309        // Test with very small remaining bytes
310        let acc = DownloadAcceleration::from_raw(100);
311        let eta = acc
312            .predict_eta(Decimal::new(1000, 0), 1)
313            .expect("Should have a valid ETA");
314        assert!(eta > Decimal::ZERO);
315        assert!(eta < Decimal::new(1, 0));
316
317        // Test with negative acceleration that causes speed to become zero
318        let acc = DownloadAcceleration::from_raw(-100);
319        let eta = acc
320            .predict_eta(Decimal::new(500, 0), 2000)
321            .expect("Should have a valid ETA");
322        assert!(eta > Decimal::ZERO);
323
324        // Test with threshold acceleration (0.1 B/s²) - should use linear prediction
325        let acc = DownloadAcceleration::from_raw(0);
326        let eta_linear = acc
327            .predict_eta(Decimal::new(100, 0), 1000)
328            .expect("Should have a valid ETA");
329
330        let acc_threshold = DownloadAcceleration::from_raw(0);
331        let eta_threshold = acc_threshold
332            .predict_eta(Decimal::new(100, 0), 1000)
333            .expect("Should have a valid ETA");
334
335        assert_eq!(eta_linear, eta_threshold);
336    }
337
338    // --- Tests for SI (base-1000) standard ---
339    // --- SI (base-1000) 测试 ---
340    #[test]
341    fn test_si_acceleration() {
342        // Test positive acceleration
343        assert_eq!(format_test_si(512), "512.0 B/s²");
344        assert_eq!(format_test_si(1000), "1.00 KB/s²");
345        assert_eq!(format_test_si(1024), "1.02 KB/s²");
346
347        // Test negative acceleration
348        assert_eq!(format_test_si(-1500), "-1.50 KB/s²");
349    }
350
351    // --- Tests for IEC (base-1024) standard ---
352    // --- IEC (base-1024) 测试 ---
353    #[test]
354    fn test_iec_acceleration() {
355        // Test positive acceleration
356        assert_eq!(format_test_iec(1024), "1.00 KiB/s²");
357        assert_eq!(format_test_iec(1500), "1.46 KiB/s²");
358
359        // Test negative acceleration
360        assert_eq!(format_test_iec(-2048), "-2.00 KiB/s²");
361    }
362}