use chrono::{DateTime, Utc};
pub fn relevance_score(
weight: f64,
last_used: DateTime<Utc>,
usage_count: i32,
created_at: DateTime<Utc>,
) -> f64 {
let now = Utc::now();
let days_since_used = (now - last_used).num_seconds() as f64 / 86400.0;
let recency = (-0.693_f64 / 23.0 * days_since_used).exp();
let usage = (1.0 + usage_count.max(0) as f64).ln().max(0.1);
let age_days = (now - created_at).num_seconds() as f64 / 86400.0;
let maturity = (1.0 + age_days / 30.0).min(2.0);
weight * recency * usage * maturity
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_fresh_high_usage() {
let now = Utc::now();
let score = relevance_score(1.0, now, 10, now - Duration::days(60));
assert!(score > 4.0, "expected high score, got {score}");
}
#[test]
fn test_stale_low_usage() {
let now = Utc::now();
let score = relevance_score(0.5, now - Duration::days(90), 1, now - Duration::days(90));
assert!(score < 1.0, "expected low score, got {score}");
}
#[test]
fn test_zero_usage_has_minimum_score() {
let now = Utc::now();
let score = relevance_score(1.0, now, 0, now);
assert!(
score > 0.0,
"expected non-zero score for unused items, got {score}"
);
assert!(
score < 0.5,
"expected low score for unused items, got {score}"
);
}
#[test]
fn test_young_item_low_maturity() {
let now = Utc::now();
let score = relevance_score(1.0, now, 5, now);
let expected_approx = 1.0 * 1.0 * (1.0 + 5.0_f64).ln() * 1.0;
assert!(
(score - expected_approx).abs() < 0.01,
"expected ~{expected_approx}, got {score}"
);
}
#[test]
fn test_zero_weight_gives_zero_score() {
let now = Utc::now();
let score = relevance_score(0.0, now, 100, now - Duration::days(60));
assert!(
score.abs() < 0.001,
"zero weight should yield zero score, got {score}"
);
}
#[test]
fn test_future_last_used_date() {
let now = Utc::now();
let future = now + Duration::days(7);
let score = relevance_score(1.0, future, 5, now);
assert!(
score > 0.0,
"future last_used should still give positive score, got {score}"
);
let normal = relevance_score(1.0, now, 5, now);
assert!(
score > normal,
"future last_used should boost recency: {score} vs {normal}"
);
}
#[test]
fn test_very_old_item_maturity_capped() {
let now = Utc::now();
let score_365 = relevance_score(1.0, now, 5, now - Duration::days(365));
let score_60 = relevance_score(1.0, now, 5, now - Duration::days(60));
assert!(
(score_365 - score_60).abs() < 0.001,
"maturity should be capped at 2.0 for both: 365d={score_365}, 60d={score_60}"
);
}
#[test]
fn test_negative_usage_count() {
let now = Utc::now();
let score = relevance_score(1.0, now, -5, now);
assert!(score.is_finite(), "got {score}");
let expected = relevance_score(1.0, now, 0, now);
assert!(
(score - expected).abs() < f64::EPSILON,
"clamped -5 should equal 0: got {score}, expected {expected}"
);
}
}