use crate::UtilsError;
use chrono::{DateTime, Local, TimeZone, Utc};
use scirs2_core::ndarray::{Array1, Array2, Axis};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Timestamp {
pub timestamp: i64, }
impl Timestamp {
pub fn from_millis(millis: i64) -> Self {
Self { timestamp: millis }
}
pub fn from_secs(secs: i64) -> Self {
Self {
timestamp: secs * 1000,
}
}
pub fn now() -> Self {
let duration = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::from_secs(0));
Self::from_millis(duration.as_millis() as i64)
}
pub fn as_millis(&self) -> i64 {
self.timestamp
}
pub fn as_secs(&self) -> i64 {
self.timestamp / 1000
}
pub fn to_datetime_utc(&self) -> DateTime<Utc> {
let naive = DateTime::from_timestamp_millis(self.timestamp)
.unwrap_or_default()
.naive_utc();
DateTime::from_naive_utc_and_offset(naive, Utc)
}
pub fn add_duration(&self, duration: Duration) -> Self {
Self::from_millis(self.timestamp + duration.as_millis() as i64)
}
pub fn sub_duration(&self, duration: Duration) -> Self {
Self::from_millis(self.timestamp - duration.as_millis() as i64)
}
}
#[derive(Debug, Clone)]
pub struct TimeSeriesPoint<T> {
pub timestamp: Timestamp,
pub value: T,
}
impl<T> TimeSeriesPoint<T> {
pub fn new(timestamp: Timestamp, value: T) -> Self {
Self { timestamp, value }
}
}
#[derive(Debug, Clone)]
pub struct TimeSeries<T> {
data: BTreeMap<Timestamp, T>,
metadata: HashMap<String, String>,
}
impl<T: Clone> Default for TimeSeries<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone> TimeSeries<T> {
pub fn new() -> Self {
Self {
data: BTreeMap::new(),
metadata: HashMap::new(),
}
}
pub fn from_vecs(timestamps: Vec<Timestamp>, values: Vec<T>) -> Result<Self, UtilsError> {
if timestamps.len() != values.len() {
return Err(UtilsError::ShapeMismatch {
expected: vec![timestamps.len()],
actual: vec![values.len()],
});
}
let mut ts = Self::new();
for (timestamp, value) in timestamps.into_iter().zip(values) {
ts.insert(timestamp, value);
}
Ok(ts)
}
pub fn insert(&mut self, timestamp: Timestamp, value: T) {
self.data.insert(timestamp, value);
}
pub fn get(&self, timestamp: &Timestamp) -> Option<&T> {
self.data.get(timestamp)
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn first_timestamp(&self) -> Option<Timestamp> {
self.data.keys().next().copied()
}
pub fn last_timestamp(&self) -> Option<Timestamp> {
self.data.keys().next_back().copied()
}
pub fn range(&self, start: Timestamp, end: Timestamp) -> Vec<TimeSeriesPoint<T>> {
self.data
.range(start..=end)
.map(|(×tamp, value)| TimeSeriesPoint::new(timestamp, value.clone()))
.collect()
}
pub fn timestamps(&self) -> Vec<Timestamp> {
self.data.keys().copied().collect()
}
pub fn values(&self) -> Vec<T> {
self.data.values().cloned().collect()
}
pub fn set_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
}
pub fn get_metadata(&self, key: &str) -> Option<&String> {
self.metadata.get(key)
}
pub fn resample(
&self,
interval: Duration,
aggregation: AggregationMethod,
) -> Result<TimeSeries<f64>, UtilsError>
where
T: Into<f64> + Copy,
{
if self.is_empty() {
return Ok(TimeSeries::new());
}
let start = self.first_timestamp().expect("operation should succeed");
let end = self.last_timestamp().expect("operation should succeed");
let mut resampled = TimeSeries::new();
let mut current = start;
while current.timestamp <= end.timestamp {
let window_end = current.add_duration(interval);
let window_data: Vec<f64> = self
.range(current, window_end)
.into_iter()
.map(|point| point.value.into())
.collect();
if !window_data.is_empty() {
let aggregated = match aggregation {
AggregationMethod::Mean => {
window_data.iter().sum::<f64>() / window_data.len() as f64
}
AggregationMethod::Sum => window_data.iter().sum(),
AggregationMethod::Min => {
window_data.iter().fold(f64::INFINITY, |a, &b| a.min(b))
}
AggregationMethod::Max => {
window_data.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b))
}
AggregationMethod::First => window_data[0],
AggregationMethod::Last => window_data[window_data.len() - 1],
};
resampled.insert(current, aggregated);
}
current = current.add_duration(interval);
}
Ok(resampled)
}
}
#[derive(Debug, Clone, Copy)]
pub enum AggregationMethod {
Mean,
Sum,
Min,
Max,
First,
Last,
}
#[derive(Debug, Clone)]
pub struct SlidingWindow<T> {
window_size: Duration,
data: VecDeque<TimeSeriesPoint<T>>,
}
impl<T: Clone> SlidingWindow<T> {
pub fn new(window_size: Duration) -> Self {
Self {
window_size,
data: VecDeque::new(),
}
}
pub fn add(&mut self, point: TimeSeriesPoint<T>) {
self.data.push_back(point.clone());
let cutoff = point.timestamp.sub_duration(self.window_size);
while let Some(front) = self.data.front() {
if front.timestamp < cutoff {
self.data.pop_front();
} else {
break;
}
}
}
pub fn current_window(&self) -> Vec<TimeSeriesPoint<T>> {
self.data.iter().cloned().collect()
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn compute_stats(&self) -> WindowStats
where
T: Into<f64> + Copy,
{
if self.is_empty() {
return WindowStats::default();
}
let values: Vec<f64> = self.data.iter().map(|p| p.value.into()).collect();
let n = values.len() as f64;
let sum = values.iter().sum::<f64>();
let mean = sum / n;
let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / n;
let std_dev = variance.sqrt();
let min = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
WindowStats {
count: values.len(),
mean,
std_dev,
min,
max,
sum,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct WindowStats {
pub count: usize,
pub mean: f64,
pub std_dev: f64,
pub min: f64,
pub max: f64,
pub sum: f64,
}
pub struct TemporalIndex {
index: BTreeMap<Timestamp, Vec<usize>>,
}
impl Default for TemporalIndex {
fn default() -> Self {
Self::new()
}
}
impl TemporalIndex {
pub fn new() -> Self {
Self {
index: BTreeMap::new(),
}
}
pub fn add_entry(&mut self, timestamp: Timestamp, id: usize) {
self.index.entry(timestamp).or_default().push(id);
}
pub fn find_range(&self, start: Timestamp, end: Timestamp) -> Vec<usize> {
self.index
.range(start..=end)
.flat_map(|(_, ids)| ids.iter().copied())
.collect()
}
pub fn find_before(&self, timestamp: Timestamp) -> Vec<usize> {
self.index
.range(..timestamp)
.flat_map(|(_, ids)| ids.iter().copied())
.collect()
}
pub fn find_after(&self, timestamp: Timestamp) -> Vec<usize> {
self.index
.range((
std::ops::Bound::Excluded(timestamp),
std::ops::Bound::Unbounded,
))
.flat_map(|(_, ids)| ids.iter().copied())
.collect()
}
}
pub struct TimeZoneUtils;
impl TimeZoneUtils {
pub fn convert_timezone(
timestamp: Timestamp,
from_tz: &str,
to_tz: &str,
) -> Result<Timestamp, UtilsError> {
let datetime_utc = timestamp.to_datetime_utc();
match (from_tz, to_tz) {
("UTC", "Local") => {
let local = Local.from_utc_datetime(&datetime_utc.naive_utc());
Ok(Timestamp::from_millis(local.timestamp_millis()))
}
("Local", "UTC") => {
Ok(timestamp) }
_ => Ok(timestamp), }
}
pub fn start_of_day(timestamp: Timestamp) -> Timestamp {
let datetime = timestamp.to_datetime_utc();
let start_of_day = datetime
.date_naive()
.and_hms_opt(0, 0, 0)
.expect("operation should succeed");
let start_of_day_utc: DateTime<Utc> =
DateTime::from_naive_utc_and_offset(start_of_day, Utc);
Timestamp::from_millis(start_of_day_utc.timestamp_millis())
}
pub fn end_of_day(timestamp: Timestamp) -> Timestamp {
let datetime = timestamp.to_datetime_utc();
let end_of_day = datetime
.date_naive()
.and_hms_opt(23, 59, 59)
.expect("operation should succeed");
let end_of_day_utc: DateTime<Utc> = DateTime::from_naive_utc_and_offset(end_of_day, Utc);
Timestamp::from_millis(end_of_day_utc.timestamp_millis())
}
}
pub struct TemporalAggregator;
impl TemporalAggregator {
pub fn aggregate_by_period<T>(
time_series: &TimeSeries<T>,
period: Duration,
aggregation: AggregationMethod,
) -> Result<TimeSeries<f64>, UtilsError>
where
T: Into<f64> + Copy,
{
time_series.resample(period, aggregation)
}
pub fn rolling_statistics<T>(
data: &[TimeSeriesPoint<T>],
window_size: Duration,
) -> Vec<WindowStats>
where
T: Into<f64> + Copy + Clone,
{
let mut results = Vec::new();
let mut window = SlidingWindow::new(window_size);
for point in data {
window.add(point.clone());
results.push(window.compute_stats());
}
results
}
pub fn detect_trend<T>(data: &[TimeSeriesPoint<T>], window_size: usize) -> Vec<TrendDirection>
where
T: Into<f64> + Copy,
{
if data.len() < window_size * 2 {
return vec![TrendDirection::Stable; data.len()];
}
let mut trends = Vec::new();
let values: Vec<f64> = data.iter().map(|p| p.value.into()).collect();
for i in window_size..(values.len() - window_size) {
let before: f64 = values[(i - window_size)..i].iter().sum::<f64>() / window_size as f64;
let after: f64 =
values[(i + 1)..(i + 1 + window_size)].iter().sum::<f64>() / window_size as f64;
let trend = if after > before * 1.05 {
TrendDirection::Increasing
} else if after < before * 0.95 {
TrendDirection::Decreasing
} else {
TrendDirection::Stable
};
trends.push(trend);
}
let mut result = vec![TrendDirection::Stable; window_size];
result.extend(trends);
result.extend(vec![TrendDirection::Stable; window_size]);
result
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TrendDirection {
Increasing,
Decreasing,
Stable,
}
pub struct LagFeatureGenerator;
impl LagFeatureGenerator {
pub fn generate_lag_features(
data: &Array1<f64>,
lags: &[usize],
) -> Result<Array2<f64>, UtilsError> {
if data.is_empty() || lags.is_empty() {
return Err(UtilsError::EmptyInput);
}
let max_lag = *lags.iter().max().expect("operation should succeed");
if data.len() <= max_lag {
return Err(UtilsError::InsufficientData {
min: max_lag + 1,
actual: data.len(),
});
}
let n_samples = data.len() - max_lag;
let n_features = lags.len() + 1; let mut features = Array2::zeros((n_samples, n_features));
for (i, mut row) in features.axis_iter_mut(Axis(0)).enumerate() {
let idx = i + max_lag;
row[0] = data[idx];
for (j, &lag) in lags.iter().enumerate() {
row[j + 1] = data[idx - lag];
}
}
Ok(features)
}
pub fn generate_diff_features(
data: &Array1<f64>,
orders: &[usize],
) -> Result<Array2<f64>, UtilsError> {
if data.is_empty() || orders.is_empty() {
return Err(UtilsError::EmptyInput);
}
let max_order = *orders.iter().max().expect("operation should succeed");
if data.len() <= max_order {
return Err(UtilsError::InsufficientData {
min: max_order + 1,
actual: data.len(),
});
}
let n_samples = data.len() - max_order;
let n_features = orders.len();
let mut features = Array2::zeros((n_samples, n_features));
for (j, &order) in orders.iter().enumerate() {
let mut diff_data = data.to_owned();
for _ in 0..order {
let mut new_diff = Array1::zeros(diff_data.len() - 1);
for i in 0..new_diff.len() {
new_diff[i] = diff_data[i + 1] - diff_data[i];
}
diff_data = new_diff;
}
for i in 0..n_samples {
features[(i, j)] = diff_data[i];
}
}
Ok(features)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_timestamp_creation() {
let ts1 = Timestamp::from_secs(1000);
let ts2 = Timestamp::from_millis(1_000_000);
assert_eq!(ts1.as_secs(), 1000);
assert_eq!(ts2.as_millis(), 1_000_000);
assert_eq!(ts1, ts2);
}
#[test]
fn test_time_series_basic_operations() {
let mut ts = TimeSeries::new();
let ts1 = Timestamp::from_secs(100);
let ts2 = Timestamp::from_secs(200);
ts.insert(ts1, 10.0);
ts.insert(ts2, 20.0);
assert_eq!(ts.len(), 2);
assert_eq!(ts.get(&ts1), Some(&10.0));
assert_eq!(ts.first_timestamp(), Some(ts1));
assert_eq!(ts.last_timestamp(), Some(ts2));
}
#[test]
fn test_sliding_window() {
let mut window = SlidingWindow::new(Duration::from_secs(10));
let base_time = Timestamp::from_secs(100);
window.add(TimeSeriesPoint::new(base_time, 1.0));
window.add(TimeSeriesPoint::new(
base_time.add_duration(Duration::from_secs(5)),
2.0,
));
window.add(TimeSeriesPoint::new(
base_time.add_duration(Duration::from_secs(15)),
3.0,
));
assert_eq!(window.len(), 2); let stats = window.compute_stats();
assert_eq!(stats.count, 2);
assert_eq!(stats.mean, 2.5);
}
#[test]
fn test_temporal_index() {
let mut index = TemporalIndex::new();
let ts1 = Timestamp::from_secs(100);
let ts2 = Timestamp::from_secs(200);
let ts3 = Timestamp::from_secs(300);
index.add_entry(ts1, 1);
index.add_entry(ts2, 2);
index.add_entry(ts3, 3);
let range_results = index.find_range(ts1, ts2);
assert_eq!(range_results, vec![1, 2]);
let before_results = index.find_before(ts2);
assert_eq!(before_results, vec![1]);
}
#[test]
fn test_lag_feature_generation() {
let data = Array1::from(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
let lags = vec![1, 2];
let features = LagFeatureGenerator::generate_lag_features(&data, &lags)
.expect("operation should succeed");
assert_eq!(features.shape(), &[3, 3]); assert_eq!(features[(0, 0)], 3.0); assert_eq!(features[(0, 1)], 2.0); assert_eq!(features[(0, 2)], 1.0); }
#[test]
fn test_diff_features() {
let data = Array1::from(vec![1.0, 3.0, 6.0, 10.0, 15.0]);
let orders = vec![1, 2];
let features = LagFeatureGenerator::generate_diff_features(&data, &orders)
.expect("operation should succeed");
assert_eq!(features.shape(), &[3, 2]); assert_eq!(features[(0, 0)], 2.0); assert_eq!(features[(0, 1)], 1.0); }
#[test]
fn test_time_series_resampling() {
let timestamps = vec![
Timestamp::from_secs(0),
Timestamp::from_secs(1),
Timestamp::from_secs(2),
Timestamp::from_secs(3),
];
let values = vec![1.0, 2.0, 3.0, 4.0];
let ts = TimeSeries::from_vecs(timestamps, values).expect("operation should succeed");
let resampled = ts
.resample(Duration::from_secs(2), AggregationMethod::Mean)
.expect("operation should succeed");
assert!(!resampled.is_empty());
}
}