diff --git a/src/models/time_series.rs b/src/models/time_series.rs new file mode 100644 index 0000000..6d48d0a --- /dev/null +++ b/src/models/time_series.rs @@ -0,0 +1,159 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum TimeBucket { + Hour, + Day, + Week, + Month, +} + +impl TimeBucket { + pub fn as_str(&self) -> &str { + match self { + TimeBucket::Hour => "hour", + TimeBucket::Day => "day", + TimeBucket::Week => "week", + TimeBucket::Month => "month", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TimeSeriesData { + pub bucket: String, + pub bucket_type: String, + pub commits: usize, + pub lines_added: usize, + pub lines_removed: usize, + pub authors: usize, +} + +impl Default for TimeSeriesData { + fn default() -> Self { + Self { + bucket: String::new(), + bucket_type: String::new(), + commits: 0, + lines_added: 0, + lines_removed: 0, + authors: 0, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MovingAverage { + pub period: usize, + pub values: Vec, +} + +impl MovingAverage { + pub fn new(period: usize) -> Self { + Self { + period, + values: Vec::new(), + } + } + + pub fn calculate(&mut self, value: f64) -> f64 { + self.values.push(value); + if self.values.len() > self.period { + self.values.remove(0); + } + let sum: f64 = self.values.iter().sum(); + sum / self.values.len() as f64 + } + + pub fn is_ready(&self) -> bool { + self.values.len() >= self.period + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrendAnalysis { + pub slope: f64, + pub direction: TrendDirection, + pub strength: f64, + pub prediction: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TrendDirection { + Increasing, + Decreasing, + Stable, + Volatile, +} + +impl Default for TrendAnalysis { + fn default() -> Self { + Self { + slope: 0.0, + direction: TrendDirection::Stable, + strength: 0.0, + prediction: Vec::new(), + } + } +} + +pub fn calculate_trend(values: &[f64]) -> TrendAnalysis { + if values.len() < 2 { + return TrendAnalysis::default(); + } + + let n = values.len() as f64; + let sum_x = (0..values.len()).map(|x| x as f64).sum::(); + let sum_y = values.iter().sum::(); + let sum_xy = values + .iter() + .enumerate() + .map(|(x, y)| x as f64 * y) + .sum::(); + let sum_x2 = values + .iter() + .enumerate() + .map(|(x, _)| (x as f64).powi(2)) + .sum::(); + + let slope = if n * sum_x2 - sum_x * sum_x != 0.0 { + (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x) + } else { + 0.0 + }; + + let avg_y = sum_y / n; + let direction = if slope > 0.01 { + TrendDirection::Increasing + } else if slope < -0.01 { + TrendDirection::Decreasing + } else { + TrendDirection::Stable + }; + + let variance: f64 = values + .iter() + .map(|y| (y - avg_y).powi(2)) + .sum::() + / n; + let std_dev = variance.sqrt(); + let strength = if std_dev > 0.0 { + (slope.abs() / std_dev).min(1.0) + } else { + 0.0 + }; + + let prediction: Vec = (0..3) + .map(|i| { + let x = values.len() as f64 + i as f64; + slope * (x - sum_x / n) + avg_y + }) + .collect(); + + TrendAnalysis { + slope, + direction, + strength, + prediction, + } +}