use crate::math::MulAdd;
#[inline]
pub(crate) const fn next_power_of_two_minus_one(n: u64) -> u64 {
if n == 0 {
return 0;
}
let v = n + 1;
let p = v.next_power_of_two();
p - 1
}
#[inline]
pub(crate) const fn log2_of_span_plus_one(span: u64) -> u32 {
(span + 1).trailing_zeros()
}
#[derive(Debug, Clone)]
pub struct EmaF64 {
alpha: f64,
one_minus_alpha: f64,
value: f64,
count: u64,
min_samples: u64,
}
#[derive(Debug, Clone)]
pub struct EmaF64Builder {
alpha: Option<f64>,
min_samples: u64,
seed: Option<f64>,
}
impl EmaF64 {
#[inline]
#[must_use]
pub fn builder() -> EmaF64Builder {
EmaF64Builder {
alpha: None,
min_samples: 1,
seed: None,
}
}
#[inline]
pub fn update(&mut self, sample: f64) -> Result<Option<f64>, crate::DataError> {
check_finite!(sample);
self.count += 1;
if self.count == 1 {
self.value = sample;
} else {
self.value = self.alpha.fma(sample, self.one_minus_alpha * self.value);
}
if self.count >= self.min_samples {
Ok(Some(self.value))
} else {
Ok(None)
}
}
#[inline]
#[must_use]
pub fn value(&self) -> Option<f64> {
if self.count >= self.min_samples {
Some(self.value)
} else {
None
}
}
#[inline]
#[must_use]
pub fn alpha(&self) -> f64 {
self.alpha
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.count
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.count >= self.min_samples
}
#[inline]
pub fn reset(&mut self) {
self.value = 0.0;
self.count = 0;
}
#[inline]
pub fn reconfigure_alpha(&mut self, alpha: f64) -> Result<(), crate::ConfigError> {
if !(alpha > 0.0 && alpha < 1.0) {
return Err(crate::ConfigError::Invalid("EMA alpha must be in (0, 1)"));
}
self.alpha = alpha;
self.one_minus_alpha = 1.0 - alpha;
Ok(())
}
}
impl EmaF64Builder {
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: f64) -> Self {
self.alpha = Some(alpha);
self
}
#[inline]
#[must_use]
#[cfg(any(feature = "std", feature = "libm"))]
pub fn halflife(mut self, halflife: f64) -> Self {
let ln2 = core::f64::consts::LN_2;
let alpha = 1.0 - crate::math::exp(-ln2 / halflife);
self.alpha = Some(alpha);
self
}
#[inline]
#[must_use]
pub fn span(mut self, n: u64) -> Self {
let alpha = 2.0 / (n as f64 + 1.0);
self.alpha = Some(alpha);
self
}
#[inline]
#[must_use]
pub fn min_samples(mut self, min: u64) -> Self {
self.min_samples = min;
self
}
#[inline]
#[must_use]
pub fn seed(mut self, value: f64) -> Self {
self.seed = Some(value);
self
}
#[inline]
pub fn build(self) -> Result<EmaF64, crate::ConfigError> {
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
if !(alpha > 0.0 && alpha < 1.0) {
return Err(crate::ConfigError::Invalid("EMA alpha must be in (0, 1)"));
}
let (value, count) = self
.seed
.map_or((0.0, 0), |seed_val| (seed_val, self.min_samples));
Ok(EmaF64 {
alpha,
one_minus_alpha: 1.0 - alpha,
value,
count,
min_samples: self.min_samples,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn first_sample_initializes() {
let mut ema = EmaF64::builder().alpha(0.5).build().unwrap();
assert_eq!(ema.update(100.0).unwrap(), Some(100.0));
assert_eq!(ema.value(), Some(100.0));
}
#[test]
fn convergence_toward_constant() {
let mut ema = EmaF64::builder().alpha(0.1).build().unwrap();
ema.update(0.0).unwrap();
for _ in 0..1000 {
ema.update(100.0).unwrap();
}
let val = ema.value().unwrap();
assert!(
(val - 100.0).abs() < 0.01,
"EMA should converge to 100, got {val}"
);
}
#[test]
fn higher_alpha_reacts_faster() {
let mut fast = EmaF64::builder().alpha(0.9).build().unwrap();
let mut slow = EmaF64::builder().alpha(0.1).build().unwrap();
fast.update(0.0).unwrap();
slow.update(0.0).unwrap();
fast.update(100.0).unwrap();
slow.update(100.0).unwrap();
let fast_val = fast.value().unwrap();
let slow_val = slow.value().unwrap();
assert!(
fast_val > slow_val,
"fast ({fast_val}) should react more than slow ({slow_val})"
);
}
#[test]
fn priming_behavior() {
let mut ema = EmaF64::builder().alpha(0.5).min_samples(5).build().unwrap();
for i in 1..5 {
assert_eq!(
ema.update(100.0).unwrap(),
None,
"sample {i} should not be primed"
);
assert!(!ema.is_primed());
}
assert!(ema.update(100.0).unwrap().is_some());
assert!(ema.is_primed());
}
#[test]
fn reset_clears_state() {
let mut ema = EmaF64::builder().alpha(0.5).build().unwrap();
ema.update(100.0).unwrap();
ema.update(200.0).unwrap();
ema.reset();
assert_eq!(ema.count(), 0);
assert_eq!(ema.value(), None);
assert_eq!(ema.update(50.0).unwrap(), Some(50.0));
}
#[test]
fn span_computes_alpha() {
let ema = EmaF64::builder().span(19).build().unwrap();
assert!((ema.alpha() - 0.1).abs() < 1e-10);
}
#[test]
fn halflife_computes_alpha() {
let ema = EmaF64::builder().halflife(1.0).build().unwrap();
assert!((ema.alpha() - 0.5).abs() < 1e-10);
}
#[test]
fn errors_without_alpha() {
let result = EmaF64::builder().build();
assert!(matches!(result, Err(crate::ConfigError::Missing("alpha"))));
}
#[test]
fn errors_on_alpha_zero() {
let result = EmaF64::builder().alpha(0.0).build();
assert!(matches!(result, Err(crate::ConfigError::Invalid(_))));
}
#[test]
fn errors_on_alpha_one() {
let result = EmaF64::builder().alpha(1.0).build();
assert!(matches!(result, Err(crate::ConfigError::Invalid(_))));
}
#[test]
#[allow(clippy::float_cmp)]
fn float_reconfigure_alpha_preserves_value() {
let mut ema = EmaF64::builder().alpha(0.5).build().unwrap();
ema.update(100.0).unwrap();
ema.update(200.0).unwrap();
let val_before = ema.value().unwrap();
let count_before = ema.count();
ema.reconfigure_alpha(0.9).unwrap();
assert!((ema.alpha() - 0.9).abs() < 1e-10);
assert_eq!(ema.value().unwrap(), val_before);
assert_eq!(ema.count(), count_before);
}
#[test]
fn float_reconfigure_alpha_validates() {
let mut ema = EmaF64::builder().alpha(0.5).build().unwrap();
assert!(ema.reconfigure_alpha(0.0).is_err());
assert!(ema.reconfigure_alpha(1.0).is_err());
assert!(ema.reconfigure_alpha(-0.1).is_err());
}
#[test]
fn rejects_nan_and_inf() {
let mut ema = EmaF64::builder().alpha(0.5).build().unwrap();
assert!(matches!(
ema.update(f64::NAN),
Err(crate::DataError::NotANumber)
));
assert!(matches!(
ema.update(f64::INFINITY),
Err(crate::DataError::Infinite)
));
assert!(matches!(
ema.update(f64::NEG_INFINITY),
Err(crate::DataError::Infinite)
));
assert_eq!(ema.count(), 0);
}
}