use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataPoint {
pub timestamp: DateTime<Utc>,
pub value: Decimal,
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeriesData {
pub points: Vec<DataPoint>,
pub interval_secs: Option<i64>,
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ForecastHorizon {
Periods(usize),
UntilDate(DateTime<Utc>),
Days(u64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TrendDirection {
Increasing,
Decreasing,
Stable,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SeasonalityPattern {
pub detected: bool,
pub period: Option<usize>,
pub strength: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastConfig {
pub horizon: ForecastHorizon,
pub confidence_level: f64,
pub include_trend: bool,
pub detect_seasonality: bool,
pub min_data_points: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForecastResult {
pub forecast: Vec<DataPoint>,
pub lower_bound: Vec<DataPoint>,
pub upper_bound: Vec<DataPoint>,
pub trend: TrendDirection,
pub seasonality: SeasonalityPattern,
pub model_name: String,
pub confidence_level: f64,
pub metrics: Option<super::metrics::ForecastMetrics>,
}
impl Default for ForecastConfig {
fn default() -> Self {
Self {
horizon: ForecastHorizon::Days(30),
confidence_level: 0.95,
include_trend: true,
detect_seasonality: true,
min_data_points: 7,
}
}
}
impl TimeSeriesData {
pub fn new(points: Vec<DataPoint>) -> Self {
Self {
points,
interval_secs: None,
metadata: None,
}
}
pub fn with_auto_interval(mut points: Vec<DataPoint>) -> Self {
points.sort_by_key(|p| p.timestamp);
let interval = if points.len() >= 2 {
let total_secs: i64 = points
.windows(2)
.map(|w| (w[1].timestamp - w[0].timestamp).num_seconds())
.sum();
Some(total_secs / (points.len() - 1) as i64)
} else {
None
};
Self {
points,
interval_secs: interval,
metadata: None,
}
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
pub fn values(&self) -> Vec<Decimal> {
self.points.iter().map(|p| p.value).collect()
}
pub fn values_f64(&self) -> Vec<f64> {
self.points
.iter()
.map(|p| p.value.to_string().parse::<f64>().unwrap_or(0.0))
.collect()
}
pub fn last(&self) -> Option<&DataPoint> {
self.points.last()
}
pub fn first(&self) -> Option<&DataPoint> {
self.points.first()
}
pub fn mean(&self) -> Option<Decimal> {
if self.is_empty() {
return None;
}
let sum: Decimal = self.points.iter().map(|p| p.value).sum();
Some(sum / Decimal::from(self.points.len()))
}
pub fn std_dev(&self) -> Option<f64> {
if self.points.len() < 2 {
return None;
}
let values = self.values_f64();
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values
.iter()
.map(|v| (v - mean).powi(2))
.sum::<f64>() / (values.len() - 1) as f64;
Some(variance.sqrt())
}
pub fn subset(&self, start_idx: usize, end_idx: usize) -> Self {
let points = self.points[start_idx..end_idx].to_vec();
Self {
points,
interval_secs: self.interval_secs,
metadata: self.metadata.clone(),
}
}
}
impl DataPoint {
pub fn new(timestamp: DateTime<Utc>, value: Decimal) -> Self {
Self {
timestamp,
value,
metadata: None,
}
}
pub fn with_metadata(
timestamp: DateTime<Utc>,
value: Decimal,
metadata: serde_json::Value,
) -> Self {
Self {
timestamp,
value,
metadata: Some(metadata),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
fn create_test_series(count: usize) -> TimeSeriesData {
let start = Utc::now();
let points: Vec<DataPoint> = (0..count)
.map(|i| DataPoint::new(
start + Duration::hours(i as i64),
Decimal::from(i),
))
.collect();
TimeSeriesData::with_auto_interval(points)
}
#[test]
fn test_time_series_creation() {
let series = create_test_series(10);
assert_eq!(series.len(), 10);
assert!(!series.is_empty());
assert!(series.interval_secs.is_some());
}
#[test]
fn test_time_series_values() {
let series = create_test_series(5);
let values = series.values();
assert_eq!(values.len(), 5);
assert_eq!(values[0], Decimal::from(0));
assert_eq!(values[4], Decimal::from(4));
}
#[test]
fn test_time_series_mean() {
let series = create_test_series(5);
let mean = series.mean().unwrap();
assert_eq!(mean, Decimal::from(2));
}
#[test]
fn test_time_series_std_dev() {
let series = create_test_series(5);
let std_dev = series.std_dev().unwrap();
assert!(std_dev > 1.4 && std_dev < 1.6);
}
#[test]
fn test_time_series_subset() {
let series = create_test_series(10);
let subset = series.subset(2, 7);
assert_eq!(subset.len(), 5);
assert_eq!(subset.values()[0], Decimal::from(2));
assert_eq!(subset.values()[4], Decimal::from(6));
}
#[test]
fn test_empty_time_series() {
let series = TimeSeriesData::new(vec![]);
assert!(series.is_empty());
assert_eq!(series.len(), 0);
assert!(series.mean().is_none());
assert!(series.std_dev().is_none());
}
}