use std::time::{Duration, Instant};
#[derive(Debug)]
pub struct Estimator {
start_time: Instant,
last_update: Instant,
last_percent: f64,
smoothed_rate: Option<f64>,
alpha: f64,
}
impl Estimator {
pub fn new(alpha: f64) -> Self {
let now = Instant::now();
Self {
start_time: now,
last_update: now,
last_percent: 0.0,
smoothed_rate: None,
alpha: alpha.clamp(0.01, 0.99),
}
}
pub fn update(&mut self, percent: f64) {
let now = Instant::now();
let elapsed = now.duration_since(self.last_update).as_secs_f64();
if elapsed > 0.001 {
let delta_percent = percent - self.last_percent;
if delta_percent > 0.0 {
let current_rate = delta_percent / elapsed;
self.smoothed_rate = Some(match self.smoothed_rate {
Some(prev) => self.alpha * current_rate + (1.0 - self.alpha) * prev,
None => current_rate,
});
}
}
self.last_percent = percent;
self.last_update = now;
}
pub fn eta(&self) -> Option<Duration> {
let rate = self.smoothed_rate?;
if rate <= 0.0 {
return None;
}
let remaining = 100.0 - self.last_percent;
if remaining <= 0.0 {
return Some(Duration::ZERO);
}
let secs = remaining / rate;
if secs > 86400.0 {
return None;
}
Some(Duration::from_secs_f64(secs))
}
pub fn elapsed(&self) -> Duration {
self.last_update.duration_since(self.start_time)
}
pub fn rate(&self) -> Option<f64> {
self.smoothed_rate
}
pub fn eta_display(&self) -> String {
match self.eta() {
None => "--:--".to_string(),
Some(d) if d.is_zero() => "< 1s".to_string(),
Some(d) => {
let total_secs = d.as_secs();
if total_secs < 60 {
format!("{total_secs}s")
} else if total_secs < 3600 {
let m = total_secs / 60;
let s = total_secs % 60;
format!("{m}m {s:02}s")
} else {
let h = total_secs / 3600;
let m = (total_secs % 3600) / 60;
format!("{h}h {m:02}m")
}
}
}
}
}
impl Default for Estimator {
fn default() -> Self {
Self::new(0.3)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initial_eta_is_none() {
let e = Estimator::default();
assert!(e.eta().is_none());
}
#[test]
fn eta_display_no_data() {
let e = Estimator::default();
assert_eq!(e.eta_display(), "--:--");
}
#[test]
fn rate_starts_none() {
let e = Estimator::default();
assert!(e.rate().is_none());
}
#[test]
fn eta_display_format_seconds() {
let mut e = Estimator::new(1.0); e.smoothed_rate = Some(10.0); e.last_percent = 90.0;
let eta = e.eta().unwrap();
assert!(eta.as_secs() <= 1);
}
#[test]
fn alpha_clamped() {
let e = Estimator::new(5.0); assert!((e.alpha - 0.99).abs() < f64::EPSILON);
}
}