use chrono::{DateTime, Utc};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DecayFunction {
Exponential,
#[cfg_attr(not(test), allow(dead_code))]
Linear,
}
#[derive(Debug, Clone, Copy)]
pub struct DecayConfig {
pub function: DecayFunction,
pub lambda: f64,
pub offset_days: f64,
}
impl Default for DecayConfig {
fn default() -> Self {
Self {
function: DecayFunction::Exponential,
lambda: 1e-6,
offset_days: 0.0,
}
}
}
impl DecayFunction {
#[cfg(test)]
pub fn all() -> impl Iterator<Item = Self> {
[DecayFunction::Exponential, DecayFunction::Linear].into_iter()
}
}
impl DecayConfig {
pub fn new() -> Result<Self, String> {
let config = Self::default();
config.validate()?;
Ok(config)
}
fn validate(&self) -> Result<(), String> {
if self.lambda <= 0.0 {
return Err(format!(
"Invalid lambda: {} (must be positive)",
self.lambda
));
}
match self.function {
DecayFunction::Exponential => {
if self.lambda > 1e-3 {
return Err(format!(
"Exponential decay lambda {} is too large (max: 1e-3)",
self.lambda
));
}
if self.lambda < 1e-10 {
return Err(format!(
"Exponential decay lambda {} is too small (min: 1e-10)",
self.lambda
));
}
}
DecayFunction::Linear => {
if self.lambda > 100.0 {
return Err(format!(
"Linear decay lambda {} is too large (max: 100.0)",
self.lambda
));
}
if self.lambda < 1e-6 {
return Err(format!(
"Linear decay lambda {} is too small to be useful (min: 1e-6)",
self.lambda
));
}
}
}
if self.offset_days < 0.0 {
return Err(format!(
"Invalid offset_days: {} (must be >= 0)",
self.offset_days
));
}
Ok(())
}
pub fn calculate_decay(&self, created_at: &DateTime<Utc>) -> f64 {
let now = Utc::now();
let age = now.signed_duration_since(*created_at);
let age_seconds = age.num_seconds().max(0) as f64;
if age_seconds.is_nan() || age_seconds.is_infinite() {
return 0.0;
}
let offset_seconds = self.offset_days * 86400.0;
let effective_age = (age_seconds - offset_seconds).max(0.0);
match self.function {
DecayFunction::Exponential => {
let exponent = -self.lambda * effective_age;
if exponent < -700.0 {
return 0.0;
}
if exponent > 700.0 {
return 1.0;
}
exponent.exp()
}
DecayFunction::Linear => {
let decay_rate = self.lambda * effective_age / 86400.0;
(1.0 - decay_rate).clamp(0.0, 1.0)
}
}
}
}
pub fn apply_recency_weight(
similarity: f64,
created_at: &DateTime<Utc>,
recency_weight: f64,
config: &DecayConfig,
) -> f64 {
if recency_weight <= 0.0 {
return similarity;
}
let decay = config.calculate_decay(created_at);
(1.0 - recency_weight) * similarity + recency_weight * decay
}
pub fn validate_recency_weight(recency_weight: f64) -> Result<(), String> {
if !(0.0..=1.0).contains(&recency_weight) {
return Err(format!(
"Invalid recency weight: {} (must be between 0.0 and 1.0)",
recency_weight
));
}
Ok(())
}
#[cfg(test)]
#[path = "temporal_tests.rs"]
mod temporal_tests;