use std::collections::VecDeque;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForecastMethod {
MovingAverage,
LinearRegression,
ExponentialSmoothing,
}
pub struct Forecaster {
method: ForecastMethod,
samples: VecDeque<f64>,
max_samples: usize,
smoothing_alpha: f64,
}
impl Forecaster {
#[must_use]
pub fn new(method: ForecastMethod) -> Self {
Self {
method,
samples: VecDeque::new(),
max_samples: 100,
smoothing_alpha: 0.3,
}
}
#[must_use]
pub fn with_config(method: ForecastMethod, max_samples: usize, smoothing_alpha: f64) -> Self {
Self {
method,
samples: VecDeque::with_capacity(max_samples),
max_samples,
smoothing_alpha: smoothing_alpha.clamp(0.0, 1.0),
}
}
pub fn add_sample(&mut self, value: f64) {
self.samples.push_back(value);
while self.samples.len() > self.max_samples {
self.samples.pop_front();
}
}
pub fn add_samples(&mut self, values: &[f64]) {
for &value in values {
self.add_sample(value);
}
}
#[must_use]
#[inline]
pub fn forecast(&self, periods: usize) -> Option<f64> {
if self.samples.is_empty() {
return None;
}
match self.method {
ForecastMethod::MovingAverage => self.forecast_moving_average(),
ForecastMethod::LinearRegression => self.forecast_linear_regression(periods),
ForecastMethod::ExponentialSmoothing => self.forecast_exponential_smoothing(),
}
}
#[must_use]
fn forecast_moving_average(&self) -> Option<f64> {
if self.samples.is_empty() {
return None;
}
let sum: f64 = self.samples.iter().sum();
Some(sum / self.samples.len() as f64)
}
#[must_use]
fn forecast_linear_regression(&self, periods: usize) -> Option<f64> {
if self.samples.len() < 2 {
return None;
}
let (slope, intercept) = self.calculate_linear_trend();
let next_x = self.samples.len() as f64 + periods as f64 - 1.0;
Some(slope * next_x + intercept)
}
#[must_use]
fn forecast_exponential_smoothing(&self) -> Option<f64> {
if self.samples.is_empty() {
return None;
}
let mut smoothed = self.samples[0];
for &value in self.samples.iter().skip(1) {
smoothed = self.smoothing_alpha * value + (1.0 - self.smoothing_alpha) * smoothed;
}
Some(smoothed)
}
#[must_use]
fn calculate_linear_trend(&self) -> (f64, f64) {
let n = self.samples.len() as f64;
let mean_x = (n - 1.0) / 2.0;
let mean_y: f64 = self.samples.iter().sum::<f64>() / n;
let mut numerator = 0.0;
let mut denominator = 0.0;
for (i, &y) in self.samples.iter().enumerate() {
let x = i as f64;
numerator += (x - mean_x) * (y - mean_y);
denominator += (x - mean_x) * (x - mean_x);
}
let slope = if denominator != 0.0 {
numerator / denominator
} else {
0.0
};
let intercept = mean_y - slope * mean_x;
(slope, intercept)
}
#[must_use]
#[inline]
pub fn growth_rate(&self) -> Option<f64> {
if self.samples.len() < 2 {
return None;
}
let (slope, _) = self.calculate_linear_trend();
Some(slope)
}
#[must_use]
pub fn time_to_capacity(&self, capacity: f64) -> Option<usize> {
if self.samples.is_empty() {
return None;
}
let current = self.samples.back()?;
if *current >= capacity {
return Some(0);
}
let growth = self.growth_rate()?;
if growth <= 0.0 {
return None; }
let periods = ((capacity - current) / growth).ceil() as usize;
Some(periods)
}
#[must_use]
pub fn confidence(&self) -> Option<f64> {
if self.samples.len() < 2 {
return None;
}
match self.method {
ForecastMethod::LinearRegression => self.calculate_r_squared(),
ForecastMethod::MovingAverage | ForecastMethod::ExponentialSmoothing => {
Some(0.5) }
}
}
#[must_use]
fn calculate_r_squared(&self) -> Option<f64> {
if self.samples.len() < 2 {
return None;
}
let (slope, intercept) = self.calculate_linear_trend();
let mean_y: f64 = self.samples.iter().sum::<f64>() / self.samples.len() as f64;
let mut ss_tot = 0.0;
let mut ss_res = 0.0;
for (i, &y) in self.samples.iter().enumerate() {
let x = i as f64;
let y_pred = slope * x + intercept;
ss_tot += (y - mean_y) * (y - mean_y);
ss_res += (y - y_pred) * (y - y_pred);
}
if ss_tot == 0.0 {
return Some(0.0);
}
Some(1.0 - (ss_res / ss_tot))
}
#[must_use]
#[inline]
pub fn is_anomalous(&self, threshold: f64) -> bool {
if self.samples.len() < 3 {
return false;
}
let latest = match self.samples.back() {
Some(&v) => v,
None => return false,
};
let mut temp_samples = self.samples.clone();
temp_samples.pop_back();
let temp_forecaster = Forecaster {
method: self.method,
samples: temp_samples,
max_samples: self.max_samples,
smoothing_alpha: self.smoothing_alpha,
};
let forecast = match temp_forecaster.forecast(1) {
Some(f) => f,
None => return false,
};
let deviation = (latest - forecast).abs();
let avg = temp_forecaster.forecast_moving_average().unwrap_or(latest);
if avg == 0.0 {
return false;
}
let relative_deviation = deviation / avg;
relative_deviation > threshold
}
#[must_use]
#[inline]
pub fn sample_count(&self) -> usize {
self.samples.len()
}
#[must_use]
#[inline]
pub fn latest_value(&self) -> Option<f64> {
self.samples.back().copied()
}
pub fn clear(&mut self) {
self.samples.clear();
}
}
#[derive(Debug, Clone)]
pub struct CapacityForecast {
pub current_usage: f64,
pub total_capacity: f64,
pub forecasted_usage: f64,
pub periods_to_capacity: Option<usize>,
pub growth_rate: f64,
pub confidence: f64,
}
impl CapacityForecast {
#[must_use]
#[inline]
pub fn is_critical(&self, threshold_periods: usize) -> bool {
match self.periods_to_capacity {
Some(periods) => periods <= threshold_periods,
None => false,
}
}
#[must_use]
#[inline]
pub fn usage_percent(&self) -> f64 {
if self.total_capacity == 0.0 {
return 0.0;
}
(self.current_usage / self.total_capacity) * 100.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_moving_average_forecast() {
let mut forecaster = Forecaster::new(ForecastMethod::MovingAverage);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
let forecast = forecaster.forecast(1);
assert_eq!(forecast, Some(25.0));
}
#[test]
fn test_linear_regression_forecast() {
let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
let forecast = forecaster.forecast(1);
assert!(forecast.is_some());
let value = forecast.unwrap();
assert!((value - 50.0).abs() < 1.0);
}
#[test]
fn test_growth_rate() {
let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
let growth = forecaster.growth_rate();
assert!(growth.is_some());
let rate = growth.unwrap();
assert!((rate - 10.0).abs() < 0.1);
}
#[test]
fn test_time_to_capacity() {
let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
let periods = forecaster.time_to_capacity(100.0);
assert!(periods.is_some());
assert_eq!(periods.unwrap(), 6);
}
#[test]
fn test_time_to_capacity_already_exceeded() {
let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
let periods = forecaster.time_to_capacity(30.0);
assert_eq!(periods, Some(0));
}
#[test]
fn test_exponential_smoothing() {
let mut forecaster = Forecaster::new(ForecastMethod::ExponentialSmoothing);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
let forecast = forecaster.forecast(1);
assert!(forecast.is_some());
}
#[test]
fn test_confidence() {
let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
let confidence = forecaster.confidence();
assert!(confidence.is_some());
let conf = confidence.unwrap();
assert!(conf > 0.9);
}
#[test]
fn test_anomaly_detection() {
let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
assert!(!forecaster.is_anomalous(0.5));
forecaster.add_sample(100.0);
assert!(forecaster.is_anomalous(0.5));
}
#[test]
fn test_sample_management() {
let mut forecaster = Forecaster::new(ForecastMethod::MovingAverage);
assert_eq!(forecaster.sample_count(), 0);
assert!(forecaster.latest_value().is_none());
forecaster.add_sample(42.0);
assert_eq!(forecaster.sample_count(), 1);
assert_eq!(forecaster.latest_value(), Some(42.0));
forecaster.clear();
assert_eq!(forecaster.sample_count(), 0);
}
#[test]
fn test_capacity_forecast_critical() {
let forecast = CapacityForecast {
current_usage: 80.0,
total_capacity: 100.0,
forecasted_usage: 95.0,
periods_to_capacity: Some(3),
growth_rate: 5.0,
confidence: 0.9,
};
assert!(forecast.is_critical(5));
assert!(!forecast.is_critical(2));
assert_eq!(forecast.usage_percent(), 80.0);
}
}