#[derive(Debug, Clone)]
pub struct HoltResult {
pub level: Vec<f64>,
pub trend: Vec<f64>,
pub fitted: Vec<f64>,
}
impl HoltResult {
pub fn forecast(&self, h: usize) -> f64 {
let last_l = *self.level.last().expect("level must be non-empty");
let last_t = *self.trend.last().expect("trend must be non-empty");
last_l + h as f64 * last_t
}
}
pub struct HoltLinear {
alpha: f64,
beta: f64,
}
impl HoltLinear {
pub fn new(alpha: f64, beta: f64) -> Option<Self> {
if !alpha.is_finite() || alpha <= 0.0 || alpha >= 1.0 {
return None;
}
if !beta.is_finite() || beta <= 0.0 || beta >= 1.0 {
return None;
}
Some(Self { alpha, beta })
}
pub fn alpha(&self) -> f64 {
self.alpha
}
pub fn beta(&self) -> f64 {
self.beta
}
pub fn smooth(&self, data: &[f64]) -> Option<HoltResult> {
if data.len() < 2 {
return None;
}
let n = data.len();
let mut level = Vec::with_capacity(n);
let mut trend = Vec::with_capacity(n);
let mut fitted = Vec::with_capacity(n);
let l0 = data[0];
let t0 = data[1] - data[0];
level.push(l0);
trend.push(t0);
fitted.push(l0);
for i in 1..n {
let l_prev = level[i - 1];
let t_prev = trend[i - 1];
let l = self.alpha * data[i] + (1.0 - self.alpha) * (l_prev + t_prev);
let t = self.beta * (l - l_prev) + (1.0 - self.beta) * t_prev;
fitted.push(l_prev + t_prev); level.push(l);
trend.push(t);
}
Some(HoltResult {
level,
trend,
fitted,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_holt_linear_trend() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let data = [10.0, 12.0, 14.0, 16.0, 18.0];
let result = holt.smooth(&data).unwrap();
let f1 = result.forecast(1);
assert!((f1 - 20.0).abs() < 2.0, "forecast(1) = {f1}, expected ~20");
}
#[test]
fn test_holt_constant_series() {
let holt = HoltLinear::new(0.3, 0.3).unwrap();
let data = [5.0; 20];
let result = holt.smooth(&data).unwrap();
let last_trend = *result.trend.last().unwrap();
assert!(
last_trend.abs() < 0.1,
"trend should be near 0, got {last_trend}"
);
}
#[test]
fn test_holt_forecast_multi_step() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let data = [10.0, 12.0, 14.0, 16.0, 18.0];
let result = holt.smooth(&data).unwrap();
let f1 = result.forecast(1);
let f3 = result.forecast(3);
let f5 = result.forecast(5);
assert!(f1 < f3);
assert!(f3 < f5);
}
#[test]
fn test_holt_minimum_data() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let result = holt.smooth(&[10.0, 15.0]).unwrap();
assert_eq!(result.level.len(), 2);
assert_eq!(result.fitted.len(), 2);
}
#[test]
fn test_holt_insufficient_data() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
assert!(holt.smooth(&[10.0]).is_none());
assert!(holt.smooth(&[]).is_none());
}
#[test]
fn test_holt_invalid_params() {
assert!(HoltLinear::new(0.0, 0.5).is_none());
assert!(HoltLinear::new(1.0, 0.5).is_none());
assert!(HoltLinear::new(0.5, 0.0).is_none());
assert!(HoltLinear::new(0.5, 1.0).is_none());
assert!(HoltLinear::new(f64::NAN, 0.5).is_none());
}
#[test]
fn test_holt_fitted_values() {
let holt = HoltLinear::new(0.5, 0.5).unwrap();
let data = [10.0, 12.0, 14.0, 16.0];
let result = holt.smooth(&data).unwrap();
assert_eq!(result.fitted.len(), data.len());
assert!((result.fitted[0] - 10.0).abs() < 1e-10);
}
}