use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use crate::context::Context;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalQuery {
pub reference_time: DateTime<Utc>,
pub max_age: Option<Duration>,
pub min_age: Option<Duration>,
pub window_start: Option<DateTime<Utc>>,
pub window_end: Option<DateTime<Utc>>,
pub apply_decay: bool,
pub decay_half_life_hours: f64,
}
impl Default for TemporalQuery {
fn default() -> Self {
Self {
reference_time: Utc::now(),
max_age: None,
min_age: None,
window_start: None,
window_end: None,
apply_decay: true,
decay_half_life_hours: 24.0, }
}
}
impl TemporalQuery {
pub fn new() -> Self {
Self::default()
}
pub fn with_max_age(mut self, hours: i64) -> Self {
self.max_age = Some(Duration::hours(hours));
self
}
pub fn with_min_age(mut self, hours: i64) -> Self {
self.min_age = Some(Duration::hours(hours));
self
}
pub fn with_window(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
self.window_start = Some(start);
self.window_end = Some(end);
self
}
pub fn recent(hours: i64) -> Self {
Self::new().with_max_age(hours)
}
pub fn today() -> Self {
let now = Utc::now();
let start_of_day = now.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
Self {
reference_time: now,
window_start: Some(start_of_day),
window_end: Some(now),
..Default::default()
}
}
pub fn this_week() -> Self {
Self::new().with_max_age(24 * 7)
}
pub fn matches(&self, ctx: &Context) -> bool {
let age = self.reference_time - ctx.created_at;
if let Some(max) = self.max_age {
if age > max {
return false;
}
}
if let Some(min) = self.min_age {
if age < min {
return false;
}
}
if let Some(start) = self.window_start {
if ctx.created_at < start {
return false;
}
}
if let Some(end) = self.window_end {
if ctx.created_at > end {
return false;
}
}
true
}
pub fn relevance_score(&self, ctx: &Context) -> f64 {
if !self.apply_decay {
return 1.0;
}
let age_hours = ctx.age_hours();
let decay_factor = 0.5_f64.powf(age_hours / self.decay_half_life_hours);
let importance = ctx.metadata.importance as f64;
0.7 * decay_factor + 0.3 * importance
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemporalStats {
pub count: usize,
pub oldest: Option<DateTime<Utc>>,
pub newest: Option<DateTime<Utc>>,
pub avg_age_hours: f64,
pub distribution: TimeDistribution,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TimeDistribution {
pub last_hour: usize,
pub last_day: usize,
pub last_week: usize,
pub last_month: usize,
pub older: usize,
}
impl TemporalStats {
pub fn from_contexts(contexts: &[Context]) -> Self {
if contexts.is_empty() {
return Self {
count: 0,
oldest: None,
newest: None,
avg_age_hours: 0.0,
distribution: TimeDistribution::default(),
};
}
let mut oldest: Option<DateTime<Utc>> = None;
let mut newest: Option<DateTime<Utc>> = None;
let mut total_age_hours = 0.0;
let mut distribution = TimeDistribution::default();
for ctx in contexts {
if oldest.map(|o| ctx.created_at < o).unwrap_or(true) {
oldest = Some(ctx.created_at);
}
if newest.map(|n| ctx.created_at > n).unwrap_or(true) {
newest = Some(ctx.created_at);
}
let age_hours = ctx.age_hours();
total_age_hours += age_hours;
if age_hours < 1.0 {
distribution.last_hour += 1;
} else if age_hours < 24.0 {
distribution.last_day += 1;
} else if age_hours < 24.0 * 7.0 {
distribution.last_week += 1;
} else if age_hours < 24.0 * 30.0 {
distribution.last_month += 1;
} else {
distribution.older += 1;
}
}
Self {
count: contexts.len(),
oldest,
newest,
avg_age_hours: total_age_hours / contexts.len() as f64,
distribution,
}
}
}
pub fn format_age(ctx: &Context) -> String {
let age_secs = ctx.age_seconds();
if age_secs < 60 {
format!("{}s ago", age_secs)
} else if age_secs < 3600 {
format!("{}m ago", age_secs / 60)
} else if age_secs < 86400 {
format!("{}h ago", age_secs / 3600)
} else {
format!("{}d ago", age_secs / 86400)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::ContextDomain;
#[test]
fn test_temporal_query_recent() {
let query = TemporalQuery::recent(24);
assert!(query.max_age.is_some());
}
#[test]
fn test_relevance_decay() {
let query = TemporalQuery::new();
let ctx = Context::new("Test", ContextDomain::General);
let score = query.relevance_score(&ctx);
assert!(score > 0.9);
}
#[test]
fn test_temporal_stats() {
let contexts = vec![
Context::new("Test 1", ContextDomain::General),
Context::new("Test 2", ContextDomain::Code),
];
let stats = TemporalStats::from_contexts(&contexts);
assert_eq!(stats.count, 2);
assert!(stats.avg_age_hours < 1.0); }
}