const HOUR: u64 = 3_600;
const DAY: u64 = 24 * HOUR;
const WEEK: u64 = 7 * DAY;
#[must_use]
pub fn recency_weight(age_secs: u64) -> f64 {
if age_secs < HOUR {
4.0
} else if age_secs < DAY {
2.0
} else if age_secs < WEEK {
0.5
} else {
0.25
}
}
#[must_use]
pub fn score(visits: u32, last_seen: u64, now: u64) -> f64 {
let age = now.saturating_sub(last_seen);
f64::from(visits) * recency_weight(age)
}
#[cfg(test)]
mod tests {
use super::*;
const NOW: u64 = 1_000_000_000;
#[test]
fn weight_buckets_step_down_by_age() {
assert_eq!(recency_weight(0), 4.0);
assert_eq!(recency_weight(HOUR - 1), 4.0);
assert_eq!(recency_weight(HOUR), 2.0);
assert_eq!(recency_weight(DAY - 1), 2.0);
assert_eq!(recency_weight(DAY), 0.5);
assert_eq!(recency_weight(WEEK - 1), 0.5);
assert_eq!(recency_weight(WEEK), 0.25);
assert_eq!(recency_weight(WEEK * 52), 0.25);
}
#[test]
fn recent_few_visits_beats_old_many_visits() {
let recent = score(2, NOW - 60, NOW); let stale = score(20, NOW - 2 * WEEK, NOW); assert!(recent > stale, "recent {recent} should beat stale {stale}");
}
#[test]
fn more_visits_wins_within_same_bucket() {
let a = score(3, NOW - 30, NOW);
let b = score(5, NOW - 30, NOW);
assert!(b > a);
}
#[test]
fn future_last_seen_saturates_to_freshest() {
let s = score(1, NOW + 500, NOW);
assert_eq!(s, 4.0);
}
#[test]
fn zero_visits_is_zero() {
assert_eq!(score(0, NOW - 10, NOW), 0.0);
}
#[test]
fn ordering_is_sane_across_buckets() {
let fresh_frequent = score(10, NOW - 10, NOW); let fresh_rare = score(1, NOW - 10, NOW); let old_frequent = score(10, NOW - 3 * DAY, NOW); let ancient_rare = score(1, NOW - 60 * DAY, NOW); assert!(fresh_frequent > old_frequent);
assert!(old_frequent > fresh_rare);
assert!(fresh_rare > ancient_rare);
}
}