pub const MIN_WINDOW_SECS: i64 = 3_600;
#[must_use]
pub fn velocity_per_day(stars_prev: i64, stars_now: i64, window_secs: i64) -> f64 {
let secs = window_secs.max(MIN_WINDOW_SECS);
(stars_now - stars_prev) as f64 * 86_400.0 / secs as f64
}
#[must_use]
pub fn personal_score(velocity_per_day: f64, topic_overlap: usize, language_match: bool) -> f64 {
let overlap = f64::from(u16::try_from(topic_overlap).unwrap_or(u16::MAX)) * 2.0;
let language = if language_match { 3.0 } else { 0.0 };
let velocity = velocity_per_day.abs().ln_1p() / 4.0;
overlap + language + velocity
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use super::*;
#[test]
fn velocity_scales_with_window() {
assert!((velocity_per_day(0, 70, 86_400) - 70.0).abs() < 1e-9);
assert!((velocity_per_day(0, 70, 7 * 86_400) - 10.0).abs() < 1e-9);
}
#[test]
fn velocity_clamps_tiny_windows() {
let v = velocity_per_day(0, 7, 1);
assert!((v - 7.0 * 86_400.0 / MIN_WINDOW_SECS as f64).abs() < 1e-9);
}
#[test]
fn velocity_can_be_negative() {
assert!((velocity_per_day(100, 90, 86_400) + 10.0).abs() < 1e-9);
}
#[test]
fn personal_score_is_explained_by_parts() {
let base = personal_score(0.0, 0, false);
assert!((base - 0.0).abs() < 1e-9);
assert!((personal_score(0.0, 2, false) - 4.0).abs() < 1e-9);
assert!((personal_score(0.0, 0, true) - 3.0).abs() < 1e-9);
}
#[test]
fn personal_score_compresses_velocity() {
let v1 = personal_score(10.0, 0, false);
let v2 = personal_score(1_000.0, 0, false);
assert!(v2 < v1 * 100.0);
assert!(v2 > v1);
}
}