use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct DecayParams {
pub rate: f64,
pub floor: f64,
}
impl Default for DecayParams {
fn default() -> Self {
Self {
rate: 0.01, floor: 0.1, }
}
}
impl DecayParams {
pub fn with_half_life(days: f64) -> Self {
let rate = 1.0 - 0.5_f64.powf(1.0 / days);
Self { rate, floor: 0.1 }
}
pub fn with_floor(mut self, floor: f64) -> Self {
self.floor = floor.clamp(0.0, 1.0);
self
}
}
pub fn apply_decay(
utility: f64,
created_at: DateTime<Utc>,
now: DateTime<Utc>,
params: &DecayParams,
) -> f64 {
let days_elapsed = (now - created_at).num_seconds() as f64 / 86400.0;
if days_elapsed <= 0.0 {
return utility;
}
let decay_factor = (1.0 - params.rate).powf(days_elapsed);
let decayed = utility * decay_factor;
decayed.max(params.floor)
}
#[allow(dead_code)]
pub fn decay_factor(days: f64, params: &DecayParams) -> f64 {
if days <= 0.0 {
1.0
} else {
(1.0 - params.rate).powf(days).max(params.floor)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeDelta as Duration;
#[test]
fn test_no_decay_for_new() {
let params = DecayParams::default();
let now = Utc::now();
let created = now;
let decayed = apply_decay(1.0, created, now, ¶ms);
assert!((decayed - 1.0).abs() < 0.001);
}
#[test]
fn test_decay_over_time() {
let params = DecayParams::default();
let now = Utc::now();
let one_week_ago = now - Duration::days(7);
let one_month_ago = now - Duration::days(30);
let week_decay = apply_decay(1.0, one_week_ago, now, ¶ms);
let month_decay = apply_decay(1.0, one_month_ago, now, ¶ms);
assert!(week_decay > month_decay);
assert!(week_decay < 1.0);
assert!(month_decay < 1.0);
}
#[test]
fn test_floor() {
let params = DecayParams {
rate: 0.1,
floor: 0.2,
};
let now = Utc::now();
let long_ago = now - Duration::days(365);
let decayed = apply_decay(1.0, long_ago, now, ¶ms);
assert!(decayed >= params.floor);
}
#[test]
fn test_half_life() {
let half_life_days = 30.0;
let params = DecayParams::with_half_life(half_life_days).with_floor(0.0);
let now = Utc::now();
let half_life_ago = now - Duration::days(half_life_days as i64);
let decayed = apply_decay(1.0, half_life_ago, now, ¶ms);
assert!((decayed - 0.5).abs() < 0.05);
}
#[test]
fn test_decay_factor() {
let params = DecayParams::default();
let factor_0 = decay_factor(0.0, ¶ms);
let factor_7 = decay_factor(7.0, ¶ms);
let factor_30 = decay_factor(30.0, ¶ms);
assert!((factor_0 - 1.0).abs() < 0.001);
assert!(factor_7 < 1.0);
assert!(factor_30 < factor_7);
}
#[test]
fn test_preserves_relative_utility() {
let params = DecayParams::default();
let now = Utc::now();
let week_ago = now - Duration::days(7);
let high_utility = apply_decay(1.0, week_ago, now, ¶ms);
let low_utility = apply_decay(0.5, week_ago, now, ¶ms);
assert!(high_utility > low_utility);
}
}