smelt_memory/utility/
decay.rs1use chrono::{DateTime, Utc};
7
8#[derive(Debug, Clone)]
10pub struct DecayParams {
11 pub rate: f64,
14
15 pub floor: f64,
17}
18
19impl Default for DecayParams {
20 fn default() -> Self {
21 Self {
22 rate: 0.01, floor: 0.1, }
25 }
26}
27
28impl DecayParams {
29 pub fn with_half_life(days: f64) -> Self {
31 let rate = 1.0 - 0.5_f64.powf(1.0 / days);
35 Self { rate, floor: 0.1 }
36 }
37
38 pub fn with_floor(mut self, floor: f64) -> Self {
40 self.floor = floor.clamp(0.0, 1.0);
41 self
42 }
43}
44
45pub fn apply_decay(
56 utility: f64,
57 created_at: DateTime<Utc>,
58 now: DateTime<Utc>,
59 params: &DecayParams,
60) -> f64 {
61 let days_elapsed = (now - created_at).num_seconds() as f64 / 86400.0;
62
63 if days_elapsed <= 0.0 {
64 return utility;
65 }
66
67 let decay_factor = (1.0 - params.rate).powf(days_elapsed);
69 let decayed = utility * decay_factor;
70
71 decayed.max(params.floor)
73}
74
75#[allow(dead_code)]
77pub fn decay_factor(days: f64, params: &DecayParams) -> f64 {
78 if days <= 0.0 {
79 1.0
80 } else {
81 (1.0 - params.rate).powf(days).max(params.floor)
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use chrono::TimeDelta as Duration;
89
90 #[test]
91 fn test_no_decay_for_new() {
92 let params = DecayParams::default();
93 let now = Utc::now();
94 let created = now;
95
96 let decayed = apply_decay(1.0, created, now, ¶ms);
97 assert!((decayed - 1.0).abs() < 0.001);
98 }
99
100 #[test]
101 fn test_decay_over_time() {
102 let params = DecayParams::default();
103 let now = Utc::now();
104 let one_week_ago = now - Duration::days(7);
105 let one_month_ago = now - Duration::days(30);
106
107 let week_decay = apply_decay(1.0, one_week_ago, now, ¶ms);
108 let month_decay = apply_decay(1.0, one_month_ago, now, ¶ms);
109
110 assert!(week_decay > month_decay);
112 assert!(week_decay < 1.0);
114 assert!(month_decay < 1.0);
115 }
116
117 #[test]
118 fn test_floor() {
119 let params = DecayParams {
120 rate: 0.1,
121 floor: 0.2,
122 };
123 let now = Utc::now();
124 let long_ago = now - Duration::days(365);
125
126 let decayed = apply_decay(1.0, long_ago, now, ¶ms);
127 assert!(decayed >= params.floor);
128 }
129
130 #[test]
131 fn test_half_life() {
132 let half_life_days = 30.0;
133 let params = DecayParams::with_half_life(half_life_days).with_floor(0.0);
134 let now = Utc::now();
135 let half_life_ago = now - Duration::days(half_life_days as i64);
136
137 let decayed = apply_decay(1.0, half_life_ago, now, ¶ms);
138 assert!((decayed - 0.5).abs() < 0.05);
140 }
141
142 #[test]
143 fn test_decay_factor() {
144 let params = DecayParams::default();
145
146 let factor_0 = decay_factor(0.0, ¶ms);
147 let factor_7 = decay_factor(7.0, ¶ms);
148 let factor_30 = decay_factor(30.0, ¶ms);
149
150 assert!((factor_0 - 1.0).abs() < 0.001);
151 assert!(factor_7 < 1.0);
152 assert!(factor_30 < factor_7);
153 }
154
155 #[test]
156 fn test_preserves_relative_utility() {
157 let params = DecayParams::default();
158 let now = Utc::now();
159 let week_ago = now - Duration::days(7);
160
161 let high_utility = apply_decay(1.0, week_ago, now, ¶ms);
162 let low_utility = apply_decay(0.5, week_ago, now, ¶ms);
163
164 assert!(high_utility > low_utility);
166 }
167}