rusty-pv 0.1.0

Pipe viewer — a Rust port of Andrew Wood's `pv(1)` with progress bar, ETA, rate display, token-bucket rate limiting, IEC/SI unit math, SIGWINCH-aware terminal redraw, SIGUSR1 size refresh, multi-instance cursor coordination, and a typed library API.
Documentation
//! Exponential moving average smoothing for displayed rate (FR-005, HINT-002).
//!
//! α = 0.3 is **fixed at v0.1.0** per FR-005 + Clarifications Q2; not user-
//! configurable via CLI or library in this version. Future revisions may
//! expose α via an additive `PvBuilder::ema_alpha(f64)` method without a MAJOR
//! semver bump.

/// EMA smoothing constant. **DO NOT** change this without a MAJOR semver bump
/// in rusty-pv — Strict-mode byte-equal compatibility with upstream's display
/// cadence depends on it.
pub const EMA_ALPHA: f64 = 0.3;

/// Exponential moving average over a stream of byte-rate samples.
///
/// On the first non-zero sample, the EMA is seeded directly with that sample
/// (no startup ramp). Subsequent samples are combined as
/// `new = α · sample + (1 - α) · old`.
#[derive(Debug, Clone, Copy)]
pub struct Ema {
    value: Option<f64>,
}

impl Ema {
    /// Construct an EMA with no samples yet.
    #[must_use]
    pub const fn new() -> Self {
        Ema { value: None }
    }

    /// Feed a new sample and return the updated EMA value.
    pub fn update(&mut self, sample: f64) -> f64 {
        let next = match self.value {
            None => sample,
            Some(prev) => EMA_ALPHA * sample + (1.0 - EMA_ALPHA) * prev,
        };
        self.value = Some(next);
        next
    }

    /// Current EMA value (`0.0` if no samples have been observed yet).
    #[must_use]
    pub fn current(&self) -> f64 {
        self.value.unwrap_or(0.0)
    }
}

impl Default for Ema {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn first_sample_seeds_directly() {
        let mut e = Ema::new();
        assert_eq!(e.update(100.0), 100.0);
    }

    #[test]
    fn converges_toward_steady_state() {
        let mut e = Ema::new();
        e.update(100.0);
        for _ in 0..50 {
            e.update(200.0);
        }
        assert!((e.current() - 200.0).abs() < 1.0);
    }

    #[test]
    fn alpha_is_locked_at_three_tenths() {
        // Sanity check the documented constant — this assertion must FAIL
        // (loudly) if a future PR changes the constant without bumping MAJOR.
        assert!((EMA_ALPHA - 0.3).abs() < f64::EPSILON);
    }
}