net-bytes 0.3.0

A Rust library for handling file sizes, download speeds, and download acceleration with support for both SI and IEC standards
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
use std::time::Duration;

use crate::{FileSizeFormat, format_parts_scaled_signed, SCALE_I128};

/// Download acceleration in bytes per second squared (fixed-point representation)
///
/// Internally stores `bytes_per_second_sq * SCALE` for precision.
/// Supports negative values for deceleration.
///
/// 下载加速度(单位:字节每秒平方,定点数表示)
///
/// 内部存储 `bytes_per_second_sq * SCALE` 以保持精度。
/// 支持负值表示减速。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct DownloadAcceleration {
    /// Scaled value: actual_bytes_per_second_sq * SCALE
    scaled_bps_sq: i128,
}

impl DownloadAcceleration {
    /// Create a new download acceleration from raw bytes per second squared
    ///
    /// # Parameters
    /// - `bytes_per_second_sq`: Non-zero acceleration in bytes per second squared
    ///
    /// # Note
    /// For negative acceleration, use `as u64` conversion of `i64` to ensure proper sign handling
    ///
    /// 从原始字节/秒²创建一个新的下载加速度实例
    ///
    /// # 参数
    /// - `bytes_per_second_sq`: 非零的字节/秒²加速度值
    ///
    /// # 注意
    /// 对于负加速度,使用 `i64` 的 `as u64` 转换,确保正确处理符号位
    #[inline]
    pub fn from_raw(bytes_per_second_sq: i64) -> Self {
        Self {
            scaled_bps_sq: (bytes_per_second_sq as i128) * SCALE_I128,
        }
    }

    /// Create a new download acceleration from speed change and time interval
    ///
    /// Uses pure integer arithmetic with nanosecond precision.
    ///
    /// # Parameters
    /// - `initial_speed`: Initial speed in bytes per second (can be zero)
    /// - `final_speed`: Final speed in bytes per second (can be zero)
    /// - `duration`: Time interval for the speed change
    ///
    /// 从下载速度变化量和时间间隔创建一个新的下载加速度实例
    ///
    /// 使用纯整数运算,保持纳秒级精度。
    ///
    /// # 参数
    /// - `initial_speed`: 初始速度(字节/秒,可以为零)
    /// - `final_speed`: 最终速度(字节/秒,可以为零)
    /// - `duration`: 速度变化所用的时间
    pub fn new(initial_speed: u64, final_speed: u64, duration: Duration) -> Self {
        let nanos = duration.as_nanos();
        if nanos == 0 {
            return Self { scaled_bps_sq: 0 };
        }

        // speed_diff = final_speed - initial_speed (can be negative)
        let speed_diff = final_speed as i128 - initial_speed as i128;
        
        // acceleration = speed_diff / seconds = speed_diff / (nanos / 1e9) = speed_diff * 1e9 / nanos
        // scaled_bps_sq = acceleration * SCALE = speed_diff * 1e9 * SCALE / nanos
        let scaled_bps_sq = speed_diff * SCALE_I128 * 1_000_000_000 / nanos as i128;

        Self { scaled_bps_sq }
    }

    /// Get the internal scaled value (for advanced usage)
    ///
    /// 获取内部缩放值(高级用途)
    #[inline]
    pub fn as_scaled(&self) -> i128 {
        self.scaled_bps_sq
    }

    /// Get the acceleration in bytes per second squared as `i64` (floored)
    ///
    /// 以 `i64` 的形式获取字节每秒平方(向下取整)
    #[inline]
    pub fn as_i64(&self) -> i64 {
        (self.scaled_bps_sq / SCALE_I128) as i64
    }

    /// Get the acceleration as f64 (for compatibility)
    ///
    /// 以 f64 的形式获取加速度(兼容用途)
    #[inline]
    pub fn as_f64(&self) -> f64 {
        self.scaled_bps_sq as f64 / SCALE_I128 as f64
    }

    /// Fallback to linear prediction when quadratic solution is not applicable
    ///
    /// Returns time in seconds as f64.
    ///
    /// 当二次方程解不适用时,回退到线性预测
    ///
    /// 返回秒数(f64)。
    #[inline]
    fn linear_prediction(current_speed_f64: f64, remaining_bytes: u64) -> Option<f64> {
        if current_speed_f64 > 0.0 {
            Some(remaining_bytes as f64 / current_speed_f64)
        } else {
            None
        }
    }

    /// Predicts the time remaining (in seconds) for a download to complete
    /// considering acceleration.
    ///
    /// # Parameters
    /// - `current_speed`: Current download speed in bytes per second
    /// - `remaining_bytes`: Non-zero remaining bytes to download
    ///
    /// # Returns
    /// - `Some(f64)`: Estimated time remaining in seconds
    /// - `None`: If the time is infinite, or if the input is invalid
    ///
    /// # Behavior
    /// - Returns `None` if `current_speed` is zero
    /// - Falls back to linear prediction if acceleration is negligible
    /// - Handles edge cases gracefully without panicking
    ///
    /// 预测考虑加速度的下载剩余时间(秒)
    ///
    /// # 参数
    /// - `current_speed`: 当前下载速度(字节/秒)
    /// - `remaining_bytes`: 剩余要下载的字节数(非零)
    ///
    /// # 返回
    /// - `Some(f64)`: 估计的剩余时间(秒)
    /// - `None`: 如果时间为无限大,或输入无效
    ///
    /// # 行为
    /// - 如果 `current_speed` 为零,返回 `None`
    /// - 如果加速度可忽略,则回退到线性预测
    /// - 优雅处理边界情况,不会导致程序崩溃
    pub fn predict_eta(&self, current_speed: u64, remaining_bytes: u64) -> Option<f64> {
        let current_speed_f64 = current_speed as f64;
        
        // Validate input
        if current_speed == 0 {
            return None;
        }

        let accel_f64 = self.as_f64();

        // Threshold for considering acceleration as negligible (0.1 B/s²)
        const ACCEL_THRESHOLD: f64 = 0.1;

        // If acceleration is negligible, use linear prediction
        if accel_f64.abs() < ACCEL_THRESHOLD {
            return Self::linear_prediction(current_speed_f64, remaining_bytes);
        }

        // Solve quadratic equation: 0.5*a*t² + v*t - s = 0
        self.solve_quadratic_eta(current_speed_f64, remaining_bytes, accel_f64)
    }

    /// Solves the quadratic equation for ETA prediction
    ///
    /// 求解二次方程以预测 ETA
    fn solve_quadratic_eta(
        &self,
        current_speed: f64,
        remaining_bytes: u64,
        accel: f64,
    ) -> Option<f64> {
        // Coefficients: a*t² + b*t + c = 0
        let a = 0.5 * accel;
        let b = current_speed;
        let c = -(remaining_bytes as f64);

        // Calculate discriminant: b² - 4ac
        let discriminant = b * b - 4.0 * a * c;

        // If discriminant is negative, no real solution exists
        if discriminant < 0.0 {
            return Self::linear_prediction(current_speed, remaining_bytes);
        }

        // Calculate square root of discriminant
        let sqrt_discriminant = discriminant.sqrt();
        let two_a = 2.0 * a;

        // Quadratic formula: t = (-b ± √discriminant) / (2a)
        let t1 = (-b + sqrt_discriminant) / two_a;
        let t2 = (-b - sqrt_discriminant) / two_a;

        // Return the smallest positive root
        match (t1 > 0.0, t2 > 0.0) {
            (true, true) => Some(t1.min(t2)),
            (true, false) => Some(t1),
            (false, true) => Some(t2),
            (false, false) => Self::linear_prediction(current_speed, remaining_bytes),
        }
    }
}

impl FileSizeFormat for DownloadAcceleration {
    /// Returns the formatted value and unit in SI (base-1000) standard
    ///
    /// 返回 SI (base-1000) 标准的 (formatted_value, unit)
    fn get_si_parts(&self) -> (String, &'static str) {
        const UNITS: &[&str] = &[
            "B/s²", "KB/s²", "MB/s²", "GB/s²", "TB/s²", "PB/s²", "EB/s²", "ZB/s²", "YB/s²",
        ];
        format_parts_scaled_signed(self.scaled_bps_sq, 1000, UNITS)
    }

    /// Returns the formatted value and unit in IEC (base-1024) standard
    ///
    /// 返回 IEC (base-1024) 标准的 (formatted_value, unit)
    fn get_iec_parts(&self) -> (String, &'static str) {
        const UNITS: &[&str] = &[
            "B/s²", "KiB/s²", "MiB/s²", "GiB/s²", "TiB/s²", "PiB/s²", "EiB/s²", "ZiB/s²", "YiB/s²",
        ];
        format_parts_scaled_signed(self.scaled_bps_sq, 1024, UNITS)
    }
}

#[cfg(test)]
mod tests {
    use super::DownloadAcceleration;
    use crate::{FormattedValue, SizeStandard};

    /// Helper function - SI standard
    ///
    /// 辅助函数 - SI 标准
    fn format_test_si(accel: i64) -> String {
        FormattedValue::new(DownloadAcceleration::from_raw(accel), SizeStandard::SI).to_string()
    }

    /// Helper function - IEC standard
    ///
    /// 辅助函数 - IEC 标准
    fn format_test_iec(accel: i64) -> String {
        FormattedValue::new(DownloadAcceleration::from_raw(accel), SizeStandard::IEC).to_string()
    }

    #[test]
    fn test_predict_eta() {
        // Test with positive acceleration
        let acc = DownloadAcceleration::from_raw(1000); // 1000 B/s²

        // Current speed: 100 B/s, remaining: 1000 bytes
        // With acceleration, it will complete before reaching 10s (linear prediction)
        let eta = acc
            .predict_eta(100, 1000)
            .expect("Should have a valid ETA");
        assert!(eta < 10.0); // Should be less than linear prediction (10s)
        assert!(eta > 0.0);

        // Test with negative acceleration (small deceleration)
        let acc = DownloadAcceleration::from_raw(-50); // -50 B/s²

        // Current speed: 200 B/s, remaining: 1000 bytes
        // With small deceleration, it will take slightly longer than linear prediction (5s)
        let eta = acc
            .predict_eta(200, 1000)
            .expect("Should have a valid ETA");
        assert!(eta >= 5.0); // Should be at least linear prediction (5s)

        // Test with zero acceleration (should match linear prediction)
        let acc = DownloadAcceleration::from_raw(0);
        let eta = acc
            .predict_eta(100, 1000)
            .expect("Should have a valid ETA");
        assert!((eta - 10.0).abs() < 0.001); // 1000 / 100 = 10s

        // Test with zero speed (should return None)
        let eta = acc.predict_eta(0, 1000);
        assert!(eta.is_none());

        // Test with exact remaining bytes
        let eta = acc
            .predict_eta(100, 1000)
            .expect("Should have a valid ETA");
        assert!((eta - 10.0).abs() < 0.001);
    }

    #[test]
    fn test_predict_eta_edge_cases() {
        // Test with very large acceleration
        let acc = DownloadAcceleration::from_raw(1_000_000);
        let eta = acc
            .predict_eta(1000, 1_000_000)
            .expect("Should have a valid ETA");
        assert!(eta > 0.0);
        assert!(eta < 1000.0); // Should be much less than linear prediction

        // Test with very small remaining bytes
        let acc = DownloadAcceleration::from_raw(100);
        let eta = acc
            .predict_eta(1000, 1)
            .expect("Should have a valid ETA");
        assert!(eta > 0.0);
        assert!(eta < 1.0);

        // Test with negative acceleration that causes speed to become zero
        let acc = DownloadAcceleration::from_raw(-100);
        let eta = acc
            .predict_eta(500, 2000)
            .expect("Should have a valid ETA");
        assert!(eta > 0.0);

        // Test with threshold acceleration (0.1 B/s²) - should use linear prediction
        let acc = DownloadAcceleration::from_raw(0);
        let eta_linear = acc
            .predict_eta(100, 1000)
            .expect("Should have a valid ETA");

        let acc_threshold = DownloadAcceleration::from_raw(0);
        let eta_threshold = acc_threshold
            .predict_eta(100, 1000)
            .expect("Should have a valid ETA");

        assert!((eta_linear - eta_threshold).abs() < 0.001);
    }

    // --- Tests for SI (base-1000) standard ---
    // --- SI (base-1000) 测试 ---
    #[test]
    fn test_si_acceleration() {
        // Test positive acceleration
        assert_eq!(format_test_si(512), "512.0 B/s²");
        assert_eq!(format_test_si(1000), "1.00 KB/s²");
        assert_eq!(format_test_si(1024), "1.02 KB/s²");

        // Test negative acceleration
        assert_eq!(format_test_si(-1500), "-1.50 KB/s²");
    }

    // --- Tests for IEC (base-1024) standard ---
    // --- IEC (base-1024) 测试 ---
    #[test]
    fn test_iec_acceleration() {
        // Test positive acceleration
        assert_eq!(format_test_iec(1024), "1.00 KiB/s²");
        assert_eq!(format_test_iec(1500), "1.46 KiB/s²");

        // Test negative acceleration
        assert_eq!(format_test_iec(-2048), "-2.00 KiB/s²");
    }

    // --- Tests for `new` function ---
    // --- `new` 函数测试 ---
    #[test]
    fn test_new_basic() {
        use std::time::Duration;

        // Speed increases from 100 to 200 B/s in 1 second = 100 B/s²
        let acc = DownloadAcceleration::new(100, 200, Duration::from_secs(1));
        assert_eq!(acc.as_i64(), 100);

        // Speed increases from 0 to 1000 B/s in 2 seconds = 500 B/s²
        let acc = DownloadAcceleration::new(0, 1000, Duration::from_secs(2));
        assert_eq!(acc.as_i64(), 500);

        // Speed increases from 100 to 600 B/s in 0.5 seconds = 1000 B/s²
        let acc = DownloadAcceleration::new(100, 600, Duration::from_millis(500));
        assert_eq!(acc.as_i64(), 1000);
    }

    #[test]
    fn test_new_negative_acceleration() {
        use std::time::Duration;

        // Speed decreases from 200 to 100 B/s in 1 second = -100 B/s²
        let acc = DownloadAcceleration::new(200, 100, Duration::from_secs(1));
        assert_eq!(acc.as_i64(), -100);

        // Speed decreases from 1000 to 0 B/s in 2 seconds = -500 B/s²
        let acc = DownloadAcceleration::new(1000, 0, Duration::from_secs(2));
        assert_eq!(acc.as_i64(), -500);
    }

    #[test]
    fn test_new_zero_acceleration() {
        use std::time::Duration;

        // Speed stays the same = 0 B/s²
        let acc = DownloadAcceleration::new(100, 100, Duration::from_secs(1));
        assert_eq!(acc.as_i64(), 0);
        assert_eq!(acc.as_scaled(), 0);
    }

    #[test]
    fn test_new_zero_duration() {
        use std::time::Duration;

        // Any speed change in 0 duration = 0 B/s² (prevent division by zero)
        let acc = DownloadAcceleration::new(100, 200, Duration::ZERO);
        assert_eq!(acc.as_i64(), 0);
        assert_eq!(acc.as_scaled(), 0);
    }

    #[test]
    fn test_new_with_subsec_nanos() {
        use std::time::Duration;

        // Speed increases from 0 to 1500 B/s in 1.5 seconds = 1000 B/s²
        let acc = DownloadAcceleration::new(0, 1500, Duration::from_millis(1500));
        assert_eq!(acc.as_i64(), 1000);

        // Speed increases from 0 to 1000 B/s in 100ms = 10000 B/s²
        let acc = DownloadAcceleration::new(0, 1000, Duration::from_millis(100));
        assert_eq!(acc.as_i64(), 10000);
    }

    #[test]
    fn test_new_large_values() {
        use std::time::Duration;

        // 1 GB/s increase in 1 second = 1 GB/s²
        let acc = DownloadAcceleration::new(0, 1_000_000_000, Duration::from_secs(1));
        assert_eq!(acc.as_i64(), 1_000_000_000);

        // 10 GB/s increase in 10 seconds = 1 GB/s²
        let acc = DownloadAcceleration::new(0, 10_000_000_000, Duration::from_secs(10));
        assert_eq!(acc.as_i64(), 1_000_000_000);
    }
}