use chrono::{DateTime, Datelike, Timelike, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimeCrystal {
pub id: Uuid,
pub name: String,
pub period: TemporalPeriod,
pub occurrences: Vec<TemporalOccurrence>,
pub distribution: Vec<f32>,
pub confidence: f32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl TimeCrystal {
pub fn new(name: impl Into<String>, period: TemporalPeriod) -> Self {
let bins = period.bin_count();
let now = Utc::now();
Self {
id: Uuid::new_v4(),
name: name.into(),
period,
occurrences: Vec::new(),
distribution: vec![0.0; bins],
confidence: 0.0,
created_at: now,
updated_at: now,
}
}
pub fn record(&mut self, timestamp: DateTime<Utc>, value: f32) {
let bin = self.period.time_to_bin(×tamp);
self.occurrences.push(TemporalOccurrence {
timestamp,
bin,
value,
});
self.updated_at = Utc::now();
self.recompute_distribution();
}
pub fn record_now(&mut self, value: f32) {
self.record(Utc::now(), value);
}
pub fn predict(&self, timestamp: &DateTime<Utc>) -> f32 {
let bin = self.period.time_to_bin(timestamp);
self.distribution.get(bin).copied().unwrap_or(0.0)
}
pub fn predict_now(&self) -> f32 {
self.predict(&Utc::now())
}
pub fn best_time(&self) -> usize {
self.distribution
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map_or(0, |(i, _)| i)
}
pub fn is_anomalous(&self, timestamp: &DateTime<Utc>, value: f32, threshold: f32) -> bool {
let expected = self.predict(timestamp);
(value - expected).abs() > threshold
}
fn recompute_distribution(&mut self) {
let bins = self.period.bin_count();
let mut counts = vec![0.0f32; bins];
let mut totals = vec![0.0f32; bins];
for occ in &self.occurrences {
if occ.bin < bins {
counts[occ.bin] += 1.0;
totals[occ.bin] += occ.value;
}
}
for i in 0..bins {
self.distribution[i] = if counts[i] > 0.0 {
totals[i] / counts[i]
} else {
0.0
};
}
let total_samples: f32 = counts.iter().sum();
self.confidence = (total_samples / (bins as f32 * 3.0)).min(1.0);
}
pub fn period_description(&self) -> String {
match &self.period {
TemporalPeriod::Hourly => "hourly (by minute)".to_string(),
TemporalPeriod::Daily => "daily (by hour)".to_string(),
TemporalPeriod::Weekly => "weekly (by day)".to_string(),
TemporalPeriod::Monthly => "monthly (by day)".to_string(),
TemporalPeriod::Custom { name, .. } => format!("custom: {}", name),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalOccurrence {
pub timestamp: DateTime<Utc>,
pub bin: usize,
pub value: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TemporalPeriod {
Hourly,
Daily,
Weekly,
Monthly,
Custom {
name: String,
bins: usize,
duration_minutes: u64,
},
}
impl TemporalPeriod {
pub fn bin_count(&self) -> usize {
match self {
TemporalPeriod::Hourly => 60,
TemporalPeriod::Daily => 24,
TemporalPeriod::Weekly => 7,
TemporalPeriod::Monthly => 31,
TemporalPeriod::Custom { bins, .. } => *bins,
}
}
pub fn time_to_bin(&self, timestamp: &DateTime<Utc>) -> usize {
match self {
TemporalPeriod::Hourly => timestamp.minute() as usize,
TemporalPeriod::Daily => timestamp.hour() as usize,
TemporalPeriod::Weekly => timestamp.weekday().num_days_from_monday() as usize,
TemporalPeriod::Monthly => (timestamp.day() - 1) as usize,
TemporalPeriod::Custom {
duration_minutes,
bins,
..
} => {
let minutes_since_epoch = timestamp.timestamp() as u64 / 60;
((minutes_since_epoch % duration_minutes) * (*bins as u64) / duration_minutes)
as usize
}
}
}
pub fn bin_label(&self, bin: usize) -> String {
match self {
TemporalPeriod::Hourly => format!(":{:02}", bin),
TemporalPeriod::Daily => format!("{:02}:00", bin),
TemporalPeriod::Weekly => {
let day = match bin {
0 => "Monday",
1 => "Tuesday",
2 => "Wednesday",
3 => "Thursday",
4 => "Friday",
5 => "Saturday",
6 => "Sunday",
_ => "Unknown",
};
day.to_string()
}
TemporalPeriod::Monthly => format!("Day {}", bin + 1),
TemporalPeriod::Custom { name, .. } => format!("{} bin {}", name, bin),
}
}
}
pub struct TemporalMemory {
crystals: HashMap<String, TimeCrystal>,
}
impl TemporalMemory {
pub fn new() -> Self {
Self {
crystals: HashMap::new(),
}
}
pub fn get_or_create(&mut self, name: &str, period: TemporalPeriod) -> &mut TimeCrystal {
self.crystals
.entry(name.to_string())
.or_insert_with(|| TimeCrystal::new(name, period))
}
pub fn record(&mut self, pattern_name: &str, period: TemporalPeriod, value: f32) {
let crystal = self.get_or_create(pattern_name, period);
crystal.record_now(value);
}
pub fn predict(&self, pattern_name: &str) -> Option<f32> {
self.crystals
.get(pattern_name)
.map(TimeCrystal::predict_now)
}
pub fn list_patterns(&self) -> Vec<&str> {
self.crystals
.keys()
.map(std::string::String::as_str)
.collect()
}
pub fn get(&self, name: &str) -> Option<&TimeCrystal> {
self.crystals.get(name)
}
pub fn len(&self) -> usize {
self.crystals.len()
}
pub fn is_empty(&self) -> bool {
self.crystals.is_empty()
}
}
impl Default for TemporalMemory {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_daily_pattern() {
let mut crystal = TimeCrystal::new("coding_activity", TemporalPeriod::Daily);
for hour in 0..24 {
let value = if (9..17).contains(&hour) {
1.0 } else if !(6..22).contains(&hour) {
0.0 } else {
0.3 };
let timestamp = Utc::now().with_hour(hour).unwrap().with_minute(0).unwrap();
crystal.record(timestamp, value);
}
let work_hour = crystal.predict(&Utc::now().with_hour(10).unwrap().with_minute(0).unwrap());
let night_hour = crystal.predict(&Utc::now().with_hour(2).unwrap().with_minute(0).unwrap());
assert!(work_hour > night_hour);
assert!(work_hour >= 0.9);
let best = crystal.best_time();
assert!((9..17).contains(&best));
}
#[test]
fn test_weekly_pattern() {
let mut crystal = TimeCrystal::new("deploy_activity", TemporalPeriod::Weekly);
for day in 0..7 {
let value = if day == 1 || day == 3 { 1.0 } else { 0.0 };
let days_offset = day as i64 - Utc::now().weekday().num_days_from_monday() as i64;
let timestamp = Utc::now() + Duration::days(days_offset);
crystal.record(timestamp, value);
}
assert!(crystal.distribution[1] > 0.5); assert!(crystal.distribution[3] > 0.5); assert!(crystal.distribution[0] < 0.1); assert!(crystal.distribution[5] < 0.1); }
#[test]
fn test_anomaly_detection() {
let mut crystal = TimeCrystal::new("system_load", TemporalPeriod::Daily);
for _ in 0..10 {
for hour in 0..24 {
let normal_value = if (9..17).contains(&hour) { 0.8 } else { 0.1 };
let timestamp = Utc::now().with_hour(hour).unwrap().with_minute(0).unwrap();
crystal.record(timestamp, normal_value);
}
}
let night_time = Utc::now().with_hour(3).unwrap();
assert!(crystal.is_anomalous(&night_time, 0.9, 0.3));
let day_time = Utc::now().with_hour(10).unwrap();
assert!(!crystal.is_anomalous(&day_time, 0.8, 0.3));
}
#[test]
fn test_temporal_memory() {
let mut memory = TemporalMemory::new();
memory.record("coding", TemporalPeriod::Daily, 1.0);
memory.record("meetings", TemporalPeriod::Daily, 0.5);
memory.record("deploys", TemporalPeriod::Weekly, 1.0);
assert_eq!(memory.len(), 3);
assert!(memory.predict("coding").is_some());
assert!(memory.predict("unknown").is_none());
let patterns = memory.list_patterns();
assert!(patterns.contains(&"coding"));
assert!(patterns.contains(&"meetings"));
assert!(patterns.contains(&"deploys"));
}
}