1use std::collections::VecDeque;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum ForecastMethod {
43 MovingAverage,
45 LinearRegression,
47 ExponentialSmoothing,
49}
50
51pub struct Forecaster {
53 method: ForecastMethod,
55 samples: VecDeque<f64>,
57 max_samples: usize,
59 smoothing_alpha: f64,
61}
62
63impl Forecaster {
64 #[must_use]
66 pub fn new(method: ForecastMethod) -> Self {
67 Self {
68 method,
69 samples: VecDeque::new(),
70 max_samples: 100,
71 smoothing_alpha: 0.3,
72 }
73 }
74
75 #[must_use]
77 pub fn with_config(method: ForecastMethod, max_samples: usize, smoothing_alpha: f64) -> Self {
78 Self {
79 method,
80 samples: VecDeque::with_capacity(max_samples),
81 max_samples,
82 smoothing_alpha: smoothing_alpha.clamp(0.0, 1.0),
83 }
84 }
85
86 pub fn add_sample(&mut self, value: f64) {
88 self.samples.push_back(value);
89
90 while self.samples.len() > self.max_samples {
92 self.samples.pop_front();
93 }
94 }
95
96 pub fn add_samples(&mut self, values: &[f64]) {
98 for &value in values {
99 self.add_sample(value);
100 }
101 }
102
103 #[must_use]
105 #[inline]
106 pub fn forecast(&self, periods: usize) -> Option<f64> {
107 if self.samples.is_empty() {
108 return None;
109 }
110
111 match self.method {
112 ForecastMethod::MovingAverage => self.forecast_moving_average(),
113 ForecastMethod::LinearRegression => self.forecast_linear_regression(periods),
114 ForecastMethod::ExponentialSmoothing => self.forecast_exponential_smoothing(),
115 }
116 }
117
118 #[must_use]
120 fn forecast_moving_average(&self) -> Option<f64> {
121 if self.samples.is_empty() {
122 return None;
123 }
124
125 let sum: f64 = self.samples.iter().sum();
126 Some(sum / self.samples.len() as f64)
127 }
128
129 #[must_use]
131 fn forecast_linear_regression(&self, periods: usize) -> Option<f64> {
132 if self.samples.len() < 2 {
133 return None;
134 }
135
136 let (slope, intercept) = self.calculate_linear_trend();
137 let next_x = self.samples.len() as f64 + periods as f64 - 1.0;
138 Some(slope * next_x + intercept)
139 }
140
141 #[must_use]
143 fn forecast_exponential_smoothing(&self) -> Option<f64> {
144 if self.samples.is_empty() {
145 return None;
146 }
147
148 let mut smoothed = self.samples[0];
149 for &value in self.samples.iter().skip(1) {
150 smoothed = self.smoothing_alpha * value + (1.0 - self.smoothing_alpha) * smoothed;
151 }
152
153 Some(smoothed)
154 }
155
156 #[must_use]
158 fn calculate_linear_trend(&self) -> (f64, f64) {
159 let n = self.samples.len() as f64;
160
161 let mean_x = (n - 1.0) / 2.0;
163 let mean_y: f64 = self.samples.iter().sum::<f64>() / n;
164
165 let mut numerator = 0.0;
167 let mut denominator = 0.0;
168
169 for (i, &y) in self.samples.iter().enumerate() {
170 let x = i as f64;
171 numerator += (x - mean_x) * (y - mean_y);
172 denominator += (x - mean_x) * (x - mean_x);
173 }
174
175 let slope = if denominator != 0.0 {
176 numerator / denominator
177 } else {
178 0.0
179 };
180
181 let intercept = mean_y - slope * mean_x;
182
183 (slope, intercept)
184 }
185
186 #[must_use]
188 #[inline]
189 pub fn growth_rate(&self) -> Option<f64> {
190 if self.samples.len() < 2 {
191 return None;
192 }
193
194 let (slope, _) = self.calculate_linear_trend();
195 Some(slope)
196 }
197
198 #[must_use]
200 pub fn time_to_capacity(&self, capacity: f64) -> Option<usize> {
201 if self.samples.is_empty() {
202 return None;
203 }
204
205 let current = self.samples.back()?;
206
207 if *current >= capacity {
208 return Some(0);
209 }
210
211 let growth = self.growth_rate()?;
212
213 if growth <= 0.0 {
214 return None; }
216
217 let periods = ((capacity - current) / growth).ceil() as usize;
218 Some(periods)
219 }
220
221 #[must_use]
225 pub fn confidence(&self) -> Option<f64> {
226 if self.samples.len() < 2 {
227 return None;
228 }
229
230 match self.method {
231 ForecastMethod::LinearRegression => self.calculate_r_squared(),
232 ForecastMethod::MovingAverage | ForecastMethod::ExponentialSmoothing => {
233 Some(0.5) }
235 }
236 }
237
238 #[must_use]
240 fn calculate_r_squared(&self) -> Option<f64> {
241 if self.samples.len() < 2 {
242 return None;
243 }
244
245 let (slope, intercept) = self.calculate_linear_trend();
246 let mean_y: f64 = self.samples.iter().sum::<f64>() / self.samples.len() as f64;
247
248 let mut ss_tot = 0.0;
249 let mut ss_res = 0.0;
250
251 for (i, &y) in self.samples.iter().enumerate() {
252 let x = i as f64;
253 let y_pred = slope * x + intercept;
254
255 ss_tot += (y - mean_y) * (y - mean_y);
256 ss_res += (y - y_pred) * (y - y_pred);
257 }
258
259 if ss_tot == 0.0 {
260 return Some(0.0);
261 }
262
263 Some(1.0 - (ss_res / ss_tot))
264 }
265
266 #[must_use]
268 #[inline]
269 pub fn is_anomalous(&self, threshold: f64) -> bool {
270 if self.samples.len() < 3 {
271 return false;
272 }
273
274 let latest = match self.samples.back() {
275 Some(&v) => v,
276 None => return false,
277 };
278
279 let mut temp_samples = self.samples.clone();
281 temp_samples.pop_back();
282
283 let temp_forecaster = Forecaster {
284 method: self.method,
285 samples: temp_samples,
286 max_samples: self.max_samples,
287 smoothing_alpha: self.smoothing_alpha,
288 };
289
290 let forecast = match temp_forecaster.forecast(1) {
291 Some(f) => f,
292 None => return false,
293 };
294
295 let deviation = (latest - forecast).abs();
296 let avg = temp_forecaster.forecast_moving_average().unwrap_or(latest);
297
298 if avg == 0.0 {
299 return false;
300 }
301
302 let relative_deviation = deviation / avg;
303 relative_deviation > threshold
304 }
305
306 #[must_use]
308 #[inline]
309 pub fn sample_count(&self) -> usize {
310 self.samples.len()
311 }
312
313 #[must_use]
315 #[inline]
316 pub fn latest_value(&self) -> Option<f64> {
317 self.samples.back().copied()
318 }
319
320 pub fn clear(&mut self) {
322 self.samples.clear();
323 }
324}
325
326#[derive(Debug, Clone)]
328pub struct CapacityForecast {
329 pub current_usage: f64,
331 pub total_capacity: f64,
333 pub forecasted_usage: f64,
335 pub periods_to_capacity: Option<usize>,
337 pub growth_rate: f64,
339 pub confidence: f64,
341}
342
343impl CapacityForecast {
344 #[must_use]
346 #[inline]
347 pub fn is_critical(&self, threshold_periods: usize) -> bool {
348 match self.periods_to_capacity {
349 Some(periods) => periods <= threshold_periods,
350 None => false,
351 }
352 }
353
354 #[must_use]
356 #[inline]
357 pub fn usage_percent(&self) -> f64 {
358 if self.total_capacity == 0.0 {
359 return 0.0;
360 }
361 (self.current_usage / self.total_capacity) * 100.0
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn test_moving_average_forecast() {
371 let mut forecaster = Forecaster::new(ForecastMethod::MovingAverage);
372 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
373
374 let forecast = forecaster.forecast(1);
375 assert_eq!(forecast, Some(25.0));
376 }
377
378 #[test]
379 fn test_linear_regression_forecast() {
380 let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
381 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
382
383 let forecast = forecaster.forecast(1);
384 assert!(forecast.is_some());
385 let value = forecast.unwrap();
387 assert!((value - 50.0).abs() < 1.0);
388 }
389
390 #[test]
391 fn test_growth_rate() {
392 let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
393 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
394
395 let growth = forecaster.growth_rate();
396 assert!(growth.is_some());
397 let rate = growth.unwrap();
399 assert!((rate - 10.0).abs() < 0.1);
400 }
401
402 #[test]
403 fn test_time_to_capacity() {
404 let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
405 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
406
407 let periods = forecaster.time_to_capacity(100.0);
408 assert!(periods.is_some());
409 assert_eq!(periods.unwrap(), 6);
411 }
412
413 #[test]
414 fn test_time_to_capacity_already_exceeded() {
415 let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
416 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
417
418 let periods = forecaster.time_to_capacity(30.0);
419 assert_eq!(periods, Some(0));
420 }
421
422 #[test]
423 fn test_exponential_smoothing() {
424 let mut forecaster = Forecaster::new(ForecastMethod::ExponentialSmoothing);
425 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
426
427 let forecast = forecaster.forecast(1);
428 assert!(forecast.is_some());
429 }
430
431 #[test]
432 fn test_confidence() {
433 let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
434 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
435
436 let confidence = forecaster.confidence();
437 assert!(confidence.is_some());
438 let conf = confidence.unwrap();
440 assert!(conf > 0.9);
441 }
442
443 #[test]
444 fn test_anomaly_detection() {
445 let mut forecaster = Forecaster::new(ForecastMethod::LinearRegression);
446 forecaster.add_samples(&[10.0, 20.0, 30.0, 40.0]);
447
448 assert!(!forecaster.is_anomalous(0.5));
450
451 forecaster.add_sample(100.0);
453 assert!(forecaster.is_anomalous(0.5));
454 }
455
456 #[test]
457 fn test_sample_management() {
458 let mut forecaster = Forecaster::new(ForecastMethod::MovingAverage);
459 assert_eq!(forecaster.sample_count(), 0);
460 assert!(forecaster.latest_value().is_none());
461
462 forecaster.add_sample(42.0);
463 assert_eq!(forecaster.sample_count(), 1);
464 assert_eq!(forecaster.latest_value(), Some(42.0));
465
466 forecaster.clear();
467 assert_eq!(forecaster.sample_count(), 0);
468 }
469
470 #[test]
471 fn test_capacity_forecast_critical() {
472 let forecast = CapacityForecast {
473 current_usage: 80.0,
474 total_capacity: 100.0,
475 forecasted_usage: 95.0,
476 periods_to_capacity: Some(3),
477 growth_rate: 5.0,
478 confidence: 0.9,
479 };
480
481 assert!(forecast.is_critical(5));
482 assert!(!forecast.is_critical(2));
483 assert_eq!(forecast.usage_percent(), 80.0);
484 }
485}