avila_telemetry/
time_series.rs

1//! Core time series data structure and operations
2
3use crate::{Result, TelemetryError};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7/// Represents a time series with optional timestamps
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct TimeSeries {
10    /// Data values
11    pub values: Vec<f64>,
12    /// Optional timestamps (if None, assumes regular intervals)
13    pub timestamps: Option<Vec<DateTime<Utc>>>,
14    /// Optional name/label for the series
15    pub name: Option<String>,
16}
17
18impl TimeSeries {
19    /// Create a new time series with just values
20    pub fn new(values: Vec<f64>) -> Self {
21        Self {
22            values,
23            timestamps: None,
24            name: None,
25        }
26    }
27
28    /// Create a time series with timestamps
29    pub fn with_timestamps(values: Vec<f64>, timestamps: Vec<DateTime<Utc>>) -> Result<Self> {
30        if values.len() != timestamps.len() {
31            return Err(TelemetryError::InvalidData(
32                "Values and timestamps must have the same length".to_string(),
33            ));
34        }
35        Ok(Self {
36            values,
37            timestamps: Some(timestamps),
38            name: None,
39        })
40    }
41
42    /// Set the name of the time series
43    pub fn with_name(mut self, name: impl Into<String>) -> Self {
44        self.name = Some(name.into());
45        self
46    }
47
48    /// Get the length of the time series
49    pub fn len(&self) -> usize {
50        self.values.len()
51    }
52
53    /// Check if the time series is empty
54    pub fn is_empty(&self) -> bool {
55        self.values.is_empty()
56    }
57
58    /// Calculate simple moving average
59    pub fn moving_average(&self, window: usize) -> Result<Vec<f64>> {
60        if window == 0 {
61            return Err(TelemetryError::InvalidParameter(
62                "Window size must be greater than 0".to_string(),
63            ));
64        }
65
66        if window > self.values.len() {
67            return Err(TelemetryError::InsufficientData(format!(
68                "Window size {} is larger than data length {}",
69                window,
70                self.values.len()
71            )));
72        }
73
74        let mut result = Vec::with_capacity(self.values.len() - window + 1);
75
76        for i in 0..=(self.values.len() - window) {
77            let sum: f64 = self.values[i..i + window].iter().sum();
78            result.push(sum / window as f64);
79        }
80
81        Ok(result)
82    }
83
84    /// Calculate exponential moving average
85    pub fn exponential_moving_average(&self, alpha: f64) -> Result<Vec<f64>> {
86        if alpha <= 0.0 || alpha > 1.0 {
87            return Err(TelemetryError::InvalidParameter(
88                "Alpha must be between 0 and 1".to_string(),
89            ));
90        }
91
92        if self.is_empty() {
93            return Err(TelemetryError::InsufficientData(
94                "Cannot calculate EMA on empty series".to_string(),
95            ));
96        }
97
98        let mut result = Vec::with_capacity(self.values.len());
99        result.push(self.values[0]);
100
101        for i in 1..self.values.len() {
102            let ema = alpha * self.values[i] + (1.0 - alpha) * result[i - 1];
103            result.push(ema);
104        }
105
106        Ok(result)
107    }
108
109    /// Calculate first difference
110    pub fn diff(&self) -> Vec<f64> {
111        if self.values.len() < 2 {
112            return Vec::new();
113        }
114
115        self.values.windows(2).map(|w| w[1] - w[0]).collect()
116    }
117
118    /// Calculate percentage change
119    pub fn pct_change(&self) -> Vec<f64> {
120        if self.values.len() < 2 {
121            return Vec::new();
122        }
123
124        self.values
125            .windows(2)
126            .map(|w| {
127                if w[0] == 0.0 {
128                    0.0
129                } else {
130                    (w[1] - w[0]) / w[0]
131                }
132            })
133            .collect()
134    }
135
136    /// Get a slice of the time series
137    pub fn slice(&self, start: usize, end: usize) -> Result<TimeSeries> {
138        if start >= end || end > self.values.len() {
139            return Err(TelemetryError::InvalidParameter(
140                "Invalid slice indices".to_string(),
141            ));
142        }
143
144        let values = self.values[start..end].to_vec();
145        let timestamps = self.timestamps.as_ref().map(|ts| ts[start..end].to_vec());
146
147        Ok(TimeSeries {
148            values,
149            timestamps,
150            name: self.name.clone(),
151        })
152    }
153
154    /// Calculate basic statistics
155    pub fn statistics(&self) -> Statistics {
156        Statistics::from_values(&self.values)
157    }
158}
159
160/// Basic statistics for a time series
161#[derive(Debug, Clone)]
162pub struct Statistics {
163    pub mean: f64,
164    pub median: f64,
165    pub std_dev: f64,
166    pub min: f64,
167    pub max: f64,
168    pub count: usize,
169}
170
171impl Statistics {
172    pub fn from_values(values: &[f64]) -> Self {
173        let count = values.len();
174
175        if count == 0 {
176            return Self {
177                mean: 0.0,
178                median: 0.0,
179                std_dev: 0.0,
180                min: 0.0,
181                max: 0.0,
182                count: 0,
183            };
184        }
185
186        let mean = values.iter().sum::<f64>() / count as f64;
187
188        let mut sorted = values.to_vec();
189        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
190
191        let median = if count.is_multiple_of(2) {
192            (sorted[count / 2 - 1] + sorted[count / 2]) / 2.0
193        } else {
194            sorted[count / 2]
195        };
196
197        let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / count as f64;
198
199        let std_dev = variance.sqrt();
200        let min = *sorted.first().unwrap();
201        let max = *sorted.last().unwrap();
202
203        Self {
204            mean,
205            median,
206            std_dev,
207            min,
208            max,
209            count,
210        }
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_moving_average() {
220        let ts = TimeSeries::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
221        let ma = ts.moving_average(3).unwrap();
222        assert_eq!(ma, vec![2.0, 3.0, 4.0]);
223    }
224
225    #[test]
226    fn test_ema() {
227        let ts = TimeSeries::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
228        let ema = ts.exponential_moving_average(0.5).unwrap();
229        assert_eq!(ema.len(), 5);
230    }
231
232    #[test]
233    fn test_statistics() {
234        let ts = TimeSeries::new(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
235        let stats = ts.statistics();
236        assert_eq!(stats.mean, 3.0);
237        assert_eq!(stats.median, 3.0);
238        assert_eq!(stats.min, 1.0);
239        assert_eq!(stats.max, 5.0);
240    }
241}