use crate::error::{ProcessorError, Result};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};
use std::sync::{Arc, RwLock};
use tracing::{debug, trace, warn};
#[derive(Debug, thiserror::Error)]
pub enum NormalizationError {
#[error("invalid interval: {interval}ms, must be greater than 0")]
InvalidInterval { interval: i64 },
#[error("buffer overflow: attempted to add event when buffer is full (capacity: {capacity})")]
BufferOverflow { capacity: usize },
#[error("invalid timestamp: {timestamp}, reason: {reason}")]
InvalidTimestamp { timestamp: i64, reason: String },
#[error("insufficient data for {operation}: need at least {required} points, got {actual}")]
InsufficientData {
operation: String,
required: usize,
actual: usize,
},
#[error("configuration error: {0}")]
Configuration(String),
#[error("computation error: {0}")]
Computation(String),
}
impl From<NormalizationError> for ProcessorError {
fn from(err: NormalizationError) -> Self {
ProcessorError::Unexpected(err.to_string())
}
}
pub type NormalizationResult<T> = std::result::Result<T, NormalizationError>;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FillStrategy {
ForwardFill,
BackwardFill,
LinearInterpolate,
Zero,
Skip,
}
impl FillStrategy {
pub fn fill(
&self,
prev_value: Option<f64>,
next_value: Option<f64>,
prev_time: Option<DateTime<Utc>>,
next_time: Option<DateTime<Utc>>,
target_time: DateTime<Utc>,
) -> NormalizationResult<Option<f64>> {
match self {
FillStrategy::ForwardFill => Ok(prev_value),
FillStrategy::BackwardFill => Ok(next_value),
FillStrategy::LinearInterpolate => {
match (prev_value, next_value, prev_time, next_time) {
(Some(v1), Some(v2), Some(t1), Some(t2)) => {
let total_duration = (t2 - t1).num_milliseconds() as f64;
let elapsed_duration = (target_time - t1).num_milliseconds() as f64;
if total_duration == 0.0 {
Ok(Some((v1 + v2) / 2.0))
} else {
let ratio = elapsed_duration / total_duration;
let interpolated = v1 + ratio * (v2 - v1);
Ok(Some(interpolated))
}
}
(Some(v), None, _, _) => Ok(Some(v)), (None, Some(v), _, _) => Ok(Some(v)), (Some(v1), Some(v2), None, _) | (Some(v1), Some(v2), _, None) => Ok(Some((v1 + v2) / 2.0)), (None, None, _, _) => Ok(None), }
}
FillStrategy::Zero => Ok(Some(0.0)),
FillStrategy::Skip => Ok(None),
}
}
pub fn requires_future_data(&self) -> bool {
matches!(self, FillStrategy::BackwardFill | FillStrategy::LinearInterpolate)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NormalizationConfig {
pub interval_ms: Option<i64>,
pub fill_strategy: FillStrategy,
pub max_buffer_size: usize,
pub max_out_of_order_ms: i64,
pub min_samples_for_detection: usize,
pub interval_tolerance: f64,
pub warn_on_late_events: bool,
}
impl Default for NormalizationConfig {
fn default() -> Self {
Self {
interval_ms: None,
fill_strategy: FillStrategy::LinearInterpolate,
max_buffer_size: 1000,
max_out_of_order_ms: 60_000, min_samples_for_detection: 10,
interval_tolerance: 0.1, warn_on_late_events: true,
}
}
}
impl NormalizationConfig {
pub fn with_interval(interval_ms: i64) -> NormalizationResult<Self> {
if interval_ms <= 0 {
return Err(NormalizationError::InvalidInterval { interval: interval_ms });
}
Ok(Self {
interval_ms: Some(interval_ms),
..Default::default()
})
}
pub fn with_fill_strategy(mut self, strategy: FillStrategy) -> Self {
self.fill_strategy = strategy;
self
}
pub fn with_max_buffer_size(mut self, size: usize) -> Self {
self.max_buffer_size = size;
self
}
pub fn validate(&self) -> NormalizationResult<()> {
if let Some(interval) = self.interval_ms {
if interval <= 0 {
return Err(NormalizationError::InvalidInterval { interval });
}
}
if self.max_buffer_size == 0 {
return Err(NormalizationError::Configuration(
"max_buffer_size must be greater than 0".to_string(),
));
}
if self.max_out_of_order_ms < 0 {
return Err(NormalizationError::Configuration(
"max_out_of_order_ms must be non-negative".to_string(),
));
}
if self.interval_tolerance < 0.0 || self.interval_tolerance >= 1.0 {
return Err(NormalizationError::Configuration(
"interval_tolerance must be between 0.0 and 1.0".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeSeriesEvent {
pub timestamp: DateTime<Utc>,
pub value: f64,
pub metadata: Option<serde_json::Value>,
}
impl TimeSeriesEvent {
pub fn new(timestamp: DateTime<Utc>, value: f64) -> Self {
Self {
timestamp,
value,
metadata: None,
}
}
pub fn with_metadata(timestamp: DateTime<Utc>, value: f64, metadata: serde_json::Value) -> Self {
Self {
timestamp,
value,
metadata: Some(metadata),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NormalizationStats {
pub events_received: u64,
pub events_normalized: u64,
pub events_dropped_late: u64,
pub events_dropped_overflow: u64,
pub points_filled: u64,
pub points_skipped: u64,
pub current_buffer_size: usize,
pub max_buffer_size_reached: usize,
pub detected_interval_ms: Option<i64>,
pub total_processing_time_us: u64,
pub out_of_order_events: u64,
}
impl NormalizationStats {
pub fn fill_rate(&self) -> f64 {
if self.events_normalized > 0 {
self.points_filled as f64 / self.events_normalized as f64
} else {
0.0
}
}
pub fn drop_rate(&self) -> f64 {
if self.events_received > 0 {
(self.events_dropped_late + self.events_dropped_overflow) as f64
/ self.events_received as f64
} else {
0.0
}
}
pub fn avg_processing_time_us(&self) -> f64 {
if self.events_received > 0 {
self.total_processing_time_us as f64 / self.events_received as f64
} else {
0.0
}
}
}
#[derive(Clone)]
pub struct TimeSeriesNormalizer {
config: Arc<NormalizationConfig>,
state: Arc<RwLock<NormalizerState>>,
}
#[derive(Debug)]
struct NormalizerState {
buffer: BTreeMap<i64, f64>,
last_emitted_timestamp: Option<DateTime<Utc>>,
stats: NormalizationStats,
detected_interval: Option<i64>,
sample_timestamps: VecDeque<i64>,
}
impl TimeSeriesNormalizer {
pub fn new(config: NormalizationConfig) -> NormalizationResult<Self> {
config.validate()?;
let state = NormalizerState {
buffer: BTreeMap::new(),
last_emitted_timestamp: None,
stats: NormalizationStats::default(),
detected_interval: config.interval_ms,
sample_timestamps: VecDeque::with_capacity(config.min_samples_for_detection),
};
Ok(Self {
config: Arc::new(config),
state: Arc::new(RwLock::new(state)),
})
}
pub fn default_config() -> NormalizationResult<Self> {
Self::new(NormalizationConfig::default())
}
pub fn normalize(&self, event: TimeSeriesEvent) -> Result<Vec<TimeSeriesEvent>> {
let start_time = std::time::Instant::now();
let mut state = self.state.write().map_err(|e| {
ProcessorError::Unexpected(format!("Failed to acquire write lock: {}", e))
})?;
state.stats.events_received += 1;
let timestamp_ms = event.timestamp.timestamp_millis();
if let Some(last_emitted) = state.last_emitted_timestamp {
let late_by_ms = last_emitted.timestamp_millis() - timestamp_ms;
if late_by_ms > self.config.max_out_of_order_ms {
if self.config.warn_on_late_events {
warn!(
timestamp_ms = timestamp_ms,
late_by_ms = late_by_ms,
"Dropping late event"
);
}
state.stats.events_dropped_late += 1;
return Ok(Vec::new());
}
}
if state.detected_interval.is_none() {
self.update_interval_detection(&mut state, timestamp_ms);
}
if state.buffer.len() >= self.config.max_buffer_size {
warn!(
buffer_size = state.buffer.len(),
"Buffer overflow, dropping event"
);
state.stats.events_dropped_overflow += 1;
return Ok(Vec::new());
}
if let Some(last_emitted) = state.last_emitted_timestamp {
if timestamp_ms < last_emitted.timestamp_millis() {
state.stats.out_of_order_events += 1;
}
}
state.buffer.insert(timestamp_ms, event.value);
state.stats.current_buffer_size = state.buffer.len();
state.stats.max_buffer_size_reached =
state.stats.max_buffer_size_reached.max(state.buffer.len());
let result = self.emit_normalized_events(&mut state);
let elapsed = start_time.elapsed();
state.stats.total_processing_time_us += elapsed.as_micros() as u64;
result
}
pub fn normalize_batch(&self, events: Vec<TimeSeriesEvent>) -> Result<Vec<TimeSeriesEvent>> {
let start_time = std::time::Instant::now();
let mut state = self.state.write().map_err(|e| {
ProcessorError::Unexpected(format!("Failed to acquire write lock: {}", e))
})?;
let mut all_results = Vec::new();
for event in events {
state.stats.events_received += 1;
let timestamp_ms = event.timestamp.timestamp_millis();
if let Some(last_emitted) = state.last_emitted_timestamp {
let late_by_ms = last_emitted.timestamp_millis() - timestamp_ms;
if late_by_ms > self.config.max_out_of_order_ms {
if self.config.warn_on_late_events {
debug!(
timestamp_ms = timestamp_ms,
late_by_ms = late_by_ms,
"Dropping late event in batch"
);
}
state.stats.events_dropped_late += 1;
continue;
}
}
if state.detected_interval.is_none() {
self.update_interval_detection(&mut state, timestamp_ms);
}
if state.buffer.len() >= self.config.max_buffer_size {
state.stats.events_dropped_overflow += 1;
continue;
}
if let Some(last_emitted) = state.last_emitted_timestamp {
if timestamp_ms < last_emitted.timestamp_millis() {
state.stats.out_of_order_events += 1;
}
}
state.buffer.insert(timestamp_ms, event.value);
}
state.stats.current_buffer_size = state.buffer.len();
state.stats.max_buffer_size_reached =
state.stats.max_buffer_size_reached.max(state.buffer.len());
let result = self.emit_normalized_events(&mut state);
let elapsed = start_time.elapsed();
state.stats.total_processing_time_us += elapsed.as_micros() as u64;
match result {
Ok(events) => {
all_results.extend(events);
Ok(all_results)
}
Err(e) => Err(e),
}
}
pub fn flush(&self) -> Result<Vec<TimeSeriesEvent>> {
let mut state = self.state.write().map_err(|e| {
ProcessorError::Unexpected(format!("Failed to acquire write lock: {}", e))
})?;
let mut result = Vec::new();
while !state.buffer.is_empty() {
if let Some((×tamp_ms, &value)) = state.buffer.iter().next() {
let timestamp = DateTime::from_timestamp_millis(timestamp_ms)
.unwrap_or_else(|| Utc::now());
result.push(TimeSeriesEvent::new(timestamp, value));
state.buffer.remove(×tamp_ms);
state.last_emitted_timestamp = Some(timestamp);
state.stats.events_normalized += 1;
}
}
Ok(result)
}
pub fn stats(&self) -> Result<NormalizationStats> {
let state = self.state.read().map_err(|e| {
ProcessorError::Unexpected(format!("Failed to acquire read lock: {}", e))
})?;
Ok(state.stats.clone())
}
pub fn reset(&self) -> Result<()> {
let mut state = self.state.write().map_err(|e| {
ProcessorError::Unexpected(format!("Failed to acquire write lock: {}", e))
})?;
state.buffer.clear();
state.last_emitted_timestamp = None;
state.stats = NormalizationStats::default();
state.detected_interval = self.config.interval_ms;
state.sample_timestamps.clear();
Ok(())
}
pub fn interval_ms(&self) -> Result<Option<i64>> {
let state = self.state.read().map_err(|e| {
ProcessorError::Unexpected(format!("Failed to acquire read lock: {}", e))
})?;
Ok(state.detected_interval)
}
fn update_interval_detection(&self, state: &mut NormalizerState, timestamp_ms: i64) {
state.sample_timestamps.push_back(timestamp_ms);
while state.sample_timestamps.len() > self.config.min_samples_for_detection {
state.sample_timestamps.pop_front();
}
if state.sample_timestamps.len() >= self.config.min_samples_for_detection {
if let Some(interval) = self.detect_interval(&state.sample_timestamps) {
debug!(interval_ms = interval, "Detected time-series interval");
state.detected_interval = Some(interval);
state.stats.detected_interval_ms = Some(interval);
}
}
}
fn detect_interval(&self, timestamps: &VecDeque<i64>) -> Option<i64> {
if timestamps.len() < 2 {
return None;
}
let mut intervals = Vec::new();
for i in 1..timestamps.len() {
let interval = timestamps[i] - timestamps[i - 1];
if interval > 0 {
intervals.push(interval);
}
}
if intervals.is_empty() {
return None;
}
intervals.sort_unstable();
let median = if intervals.len() % 2 == 0 {
(intervals[intervals.len() / 2 - 1] + intervals[intervals.len() / 2]) / 2
} else {
intervals[intervals.len() / 2]
};
let tolerance = (median as f64 * self.config.interval_tolerance) as i64;
let consistent_count = intervals
.iter()
.filter(|&&i| (i - median).abs() <= tolerance)
.count();
let consistency_ratio = consistent_count as f64 / intervals.len() as f64;
if consistency_ratio >= 0.7 {
Some(median)
} else {
None
}
}
fn emit_normalized_events(&self, state: &mut NormalizerState) -> Result<Vec<TimeSeriesEvent>> {
let interval = match state.detected_interval {
Some(i) => i,
None => {
return Ok(Vec::new());
}
};
let mut result = Vec::new();
let buffer_to_keep = if self.config.fill_strategy.requires_future_data() {
2 } else {
1 };
if state.buffer.len() <= buffer_to_keep {
return Ok(result);
}
let timestamps: Vec<i64> = state.buffer.keys().copied().collect();
let emit_until_idx = timestamps.len() - buffer_to_keep;
for i in 0..emit_until_idx {
let timestamp_ms = timestamps[i];
let value = state.buffer[×tamp_ms];
let timestamp = DateTime::from_timestamp_millis(timestamp_ms)
.ok_or_else(|| {
NormalizationError::InvalidTimestamp {
timestamp: timestamp_ms,
reason: "invalid timestamp".to_string(),
}
})?;
if let Some(last_emitted) = state.last_emitted_timestamp {
let expected_next_ms = last_emitted.timestamp_millis() + interval;
let gap_ms = timestamp_ms - expected_next_ms;
if gap_ms > 0 {
let filled_events = self.fill_gap(
state,
last_emitted,
timestamp,
interval,
value,
i,
×tamps,
)?;
result.extend(filled_events);
}
}
result.push(TimeSeriesEvent::new(timestamp, value));
state.last_emitted_timestamp = Some(timestamp);
state.stats.events_normalized += 1;
state.buffer.remove(×tamp_ms);
}
Ok(result)
}
#[allow(clippy::too_many_arguments)]
fn fill_gap(
&self,
state: &mut NormalizerState,
last_timestamp: DateTime<Utc>,
next_timestamp: DateTime<Utc>,
interval: i64,
next_value: f64,
next_idx: usize,
all_timestamps: &[i64],
) -> Result<Vec<TimeSeriesEvent>> {
let mut result = Vec::new();
let prev_value = if next_idx > 0 {
Some(state.buffer[&all_timestamps[next_idx - 1]])
} else {
None
};
let mut current_ms = last_timestamp.timestamp_millis() + interval;
let next_ms = next_timestamp.timestamp_millis();
while current_ms < next_ms {
let current_timestamp = DateTime::from_timestamp_millis(current_ms)
.ok_or_else(|| NormalizationError::InvalidTimestamp {
timestamp: current_ms,
reason: "invalid timestamp during gap fill".to_string(),
})?;
let filled_value = self.config.fill_strategy.fill(
prev_value,
Some(next_value),
Some(last_timestamp),
Some(next_timestamp),
current_timestamp,
)?;
match filled_value {
Some(value) => {
result.push(TimeSeriesEvent::new(current_timestamp, value));
state.stats.points_filled += 1;
trace!(
timestamp_ms = current_ms,
value = value,
strategy = ?self.config.fill_strategy,
"Filled missing point"
);
}
None => {
state.stats.points_skipped += 1;
trace!(
timestamp_ms = current_ms,
"Skipped missing point"
);
}
}
current_ms += interval;
}
Ok(result)
}
}
pub struct NormalizerBuilder {
config: NormalizationConfig,
}
impl NormalizerBuilder {
pub fn new() -> Self {
Self {
config: NormalizationConfig::default(),
}
}
pub fn interval_ms(mut self, interval: i64) -> Self {
self.config.interval_ms = Some(interval);
self
}
pub fn fill_strategy(mut self, strategy: FillStrategy) -> Self {
self.config.fill_strategy = strategy;
self
}
pub fn max_buffer_size(mut self, size: usize) -> Self {
self.config.max_buffer_size = size;
self
}
pub fn max_out_of_order_ms(mut self, ms: i64) -> Self {
self.config.max_out_of_order_ms = ms;
self
}
pub fn min_samples_for_detection(mut self, count: usize) -> Self {
self.config.min_samples_for_detection = count;
self
}
pub fn interval_tolerance(mut self, tolerance: f64) -> Self {
self.config.interval_tolerance = tolerance;
self
}
pub fn warn_on_late_events(mut self, warn: bool) -> Self {
self.config.warn_on_late_events = warn;
self
}
pub fn build(self) -> NormalizationResult<TimeSeriesNormalizer> {
TimeSeriesNormalizer::new(self.config)
}
}
impl Default for NormalizerBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_event(timestamp_ms: i64, value: f64) -> TimeSeriesEvent {
let timestamp = DateTime::from_timestamp_millis(timestamp_ms).unwrap();
TimeSeriesEvent::new(timestamp, value)
}
#[test]
fn test_fill_strategy_forward_fill() {
let strategy = FillStrategy::ForwardFill;
let t1 = Utc::now();
let t2 = t1 + Duration::seconds(10);
let target = t1 + Duration::seconds(5);
let result = strategy
.fill(Some(10.0), Some(20.0), Some(t1), Some(t2), target)
.unwrap();
assert_eq!(result, Some(10.0));
}
#[test]
fn test_fill_strategy_backward_fill() {
let strategy = FillStrategy::BackwardFill;
let t1 = Utc::now();
let t2 = t1 + Duration::seconds(10);
let target = t1 + Duration::seconds(5);
let result = strategy
.fill(Some(10.0), Some(20.0), Some(t1), Some(t2), target)
.unwrap();
assert_eq!(result, Some(20.0));
}
#[test]
fn test_fill_strategy_linear_interpolate() {
let strategy = FillStrategy::LinearInterpolate;
let t1 = Utc::now();
let t2 = t1 + Duration::seconds(10);
let target = t1 + Duration::seconds(5);
let result = strategy
.fill(Some(10.0), Some(20.0), Some(t1), Some(t2), target)
.unwrap();
assert_eq!(result, Some(15.0)); }
#[test]
fn test_fill_strategy_zero() {
let strategy = FillStrategy::Zero;
let t1 = Utc::now();
let t2 = t1 + Duration::seconds(10);
let target = t1 + Duration::seconds(5);
let result = strategy
.fill(Some(10.0), Some(20.0), Some(t1), Some(t2), target)
.unwrap();
assert_eq!(result, Some(0.0));
}
#[test]
fn test_fill_strategy_skip() {
let strategy = FillStrategy::Skip;
let t1 = Utc::now();
let t2 = t1 + Duration::seconds(10);
let target = t1 + Duration::seconds(5);
let result = strategy
.fill(Some(10.0), Some(20.0), Some(t1), Some(t2), target)
.unwrap();
assert_eq!(result, None);
}
#[test]
fn test_config_validation() {
let config = NormalizationConfig::with_interval(1000).unwrap();
assert!(config.validate().is_ok());
let invalid_config = NormalizationConfig {
interval_ms: Some(-1000),
..Default::default()
};
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_normalizer_creation() {
let config = NormalizationConfig::with_interval(1000).unwrap();
let normalizer = TimeSeriesNormalizer::new(config);
assert!(normalizer.is_ok());
}
#[test]
fn test_normalizer_with_ordered_events() {
let config = NormalizationConfig::with_interval(1000)
.unwrap()
.with_fill_strategy(FillStrategy::Skip);
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
let events = vec![
create_event(base_time, 10.0),
create_event(base_time + 1000, 20.0),
create_event(base_time + 2000, 30.0),
];
let mut all_results = Vec::new();
for event in events {
let results = normalizer.normalize(event).unwrap();
all_results.extend(results);
}
assert!(all_results.len() >= 1);
}
#[test]
fn test_normalizer_with_gaps() {
let config = NormalizationConfig::with_interval(1000)
.unwrap()
.with_fill_strategy(FillStrategy::LinearInterpolate);
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
let events = vec![
create_event(base_time, 10.0),
create_event(base_time + 3000, 40.0), create_event(base_time + 4000, 50.0),
];
let mut all_results = Vec::new();
for event in events {
let results = normalizer.normalize(event).unwrap();
all_results.extend(results);
}
let final_results = normalizer.flush().unwrap();
all_results.extend(final_results);
assert!(all_results.len() >= 3);
}
#[test]
fn test_normalizer_batch_processing() {
let config = NormalizationConfig::with_interval(1000)
.unwrap()
.with_fill_strategy(FillStrategy::Skip);
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
let events = vec![
create_event(base_time, 10.0),
create_event(base_time + 1000, 20.0),
create_event(base_time + 2000, 30.0),
];
let results = normalizer.normalize_batch(events).unwrap();
assert!(!results.is_empty());
}
#[test]
fn test_normalizer_out_of_order_events() {
let config = NormalizationConfig::with_interval(1000)
.unwrap()
.with_fill_strategy(FillStrategy::Skip);
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
let events = vec![
create_event(base_time, 10.0),
create_event(base_time + 2000, 30.0),
create_event(base_time + 1000, 20.0), ];
let mut all_results = Vec::new();
for event in events {
let results = normalizer.normalize(event).unwrap();
all_results.extend(results);
}
let stats = normalizer.stats().unwrap();
assert_eq!(stats.out_of_order_events, 1);
}
#[test]
fn test_normalizer_late_events() {
let config = NormalizationConfig {
interval_ms: Some(1000),
max_out_of_order_ms: 2000,
warn_on_late_events: false,
..Default::default()
};
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
let events = vec![
create_event(base_time, 10.0),
create_event(base_time + 1000, 20.0),
create_event(base_time + 2000, 30.0),
create_event(base_time + 10000, 100.0), create_event(base_time + 500, 5.0), ];
for event in events {
let _ = normalizer.normalize(event);
}
let stats = normalizer.stats().unwrap();
assert!(stats.events_dropped_late > 0);
}
#[test]
fn test_normalizer_buffer_overflow() {
let config = NormalizationConfig {
interval_ms: Some(1000),
max_buffer_size: 3,
..Default::default()
};
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
for i in 0..10 {
let event = create_event(base_time + (i * 1000), i as f64);
let _ = normalizer.normalize(event);
}
let stats = normalizer.stats().unwrap();
assert!(stats.events_received == 10);
}
#[test]
fn test_normalizer_interval_detection() {
let config = NormalizationConfig {
interval_ms: None, min_samples_for_detection: 5,
..Default::default()
};
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
let interval = 5000_i64;
for i in 0..10 {
let event = create_event(base_time + (i * interval), i as f64);
let _ = normalizer.normalize(event);
}
let detected = normalizer.interval_ms().unwrap();
assert_eq!(detected, Some(interval));
}
#[test]
fn test_normalizer_reset() {
let config = NormalizationConfig::with_interval(1000).unwrap();
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let event = create_event(1000, 10.0);
let _ = normalizer.normalize(event);
let stats_before = normalizer.stats().unwrap();
assert!(stats_before.events_received > 0);
normalizer.reset().unwrap();
let stats_after = normalizer.stats().unwrap();
assert_eq!(stats_after.events_received, 0);
}
#[test]
fn test_normalizer_flush() {
let config = NormalizationConfig::with_interval(1000)
.unwrap()
.with_fill_strategy(FillStrategy::Skip);
let normalizer = TimeSeriesNormalizer::new(config).unwrap();
let base_time = 1000_i64;
let events = vec![
create_event(base_time, 10.0),
create_event(base_time + 1000, 20.0),
];
for event in events {
let _ = normalizer.normalize(event);
}
let flushed = normalizer.flush().unwrap();
assert!(!flushed.is_empty());
let stats = normalizer.stats().unwrap();
assert_eq!(stats.current_buffer_size, 0);
}
#[test]
fn test_builder_pattern() {
let normalizer = NormalizerBuilder::new()
.interval_ms(1000)
.fill_strategy(FillStrategy::ForwardFill)
.max_buffer_size(500)
.max_out_of_order_ms(30000)
.build();
assert!(normalizer.is_ok());
}
#[test]
fn test_stats_calculations() {
let mut stats = NormalizationStats {
events_received: 100,
events_normalized: 80,
points_filled: 20,
events_dropped_late: 5,
events_dropped_overflow: 5,
total_processing_time_us: 10000,
..Default::default()
};
assert_eq!(stats.fill_rate(), 0.25); assert_eq!(stats.drop_rate(), 0.10); assert_eq!(stats.avg_processing_time_us(), 100.0); }
#[test]
fn test_time_series_event_creation() {
let timestamp = Utc::now();
let event = TimeSeriesEvent::new(timestamp, 42.0);
assert_eq!(event.timestamp, timestamp);
assert_eq!(event.value, 42.0);
assert!(event.metadata.is_none());
let event_with_meta = TimeSeriesEvent::with_metadata(
timestamp,
42.0,
serde_json::json!({"key": "value"}),
);
assert!(event_with_meta.metadata.is_some());
}
#[test]
fn test_linear_interpolation_edge_cases() {
let strategy = FillStrategy::LinearInterpolate;
let t1 = Utc::now();
let result = strategy
.fill(None, Some(20.0), None, Some(t1), t1)
.unwrap();
assert_eq!(result, Some(20.0));
let result = strategy
.fill(Some(10.0), None, Some(t1), None, t1)
.unwrap();
assert_eq!(result, Some(10.0));
let result = strategy.fill(None, None, None, None, t1).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_concurrent_access() {
use std::thread;
let config = NormalizationConfig::with_interval(1000).unwrap();
let normalizer = Arc::new(TimeSeriesNormalizer::new(config).unwrap());
let mut handles = vec![];
for i in 0..4 {
let normalizer_clone = Arc::clone(&normalizer);
let handle = thread::spawn(move || {
let base_time = 1000 + (i * 10000);
for j in 0..10 {
let event = create_event(base_time + (j * 1000), (i * 10 + j) as f64);
let _ = normalizer_clone.normalize(event);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let stats = normalizer.stats().unwrap();
assert_eq!(stats.events_received, 40);
}
}