use chrono::{DateTime, Utc};
pub fn keyword_relevance(tweet_text: &str, keywords: &[String], max_score: f32) -> f32 {
if keywords.is_empty() {
return 0.0;
}
let text_lower = tweet_text.to_lowercase();
let mut matched_weight: f32 = 0.0;
let mut max_possible_weight: f32 = 0.0;
for keyword in keywords {
let weight = if keyword.contains(' ') { 2.0 } else { 1.0 };
max_possible_weight += weight;
if text_lower.contains(&keyword.to_lowercase()) {
matched_weight += weight;
}
}
if max_possible_weight == 0.0 {
return 0.0;
}
let score = (matched_weight / max_possible_weight) * max_score;
score.clamp(0.0, max_score)
}
pub fn follower_score(follower_count: u64, max_score: f32) -> f32 {
if follower_count == 0 {
return 0.0;
}
let log_val = (follower_count.max(1) as f64).log10();
let score = (log_val / 5.0) * max_score as f64;
(score as f32).clamp(0.0, max_score)
}
pub fn recency_score_at(tweet_created_at: &str, max_score: f32, now: DateTime<Utc>) -> f32 {
let created_at = match tweet_created_at.parse::<DateTime<Utc>>() {
Ok(dt) => dt,
Err(_) => {
tracing::warn!(
timestamp = tweet_created_at,
"Failed to parse tweet timestamp for recency scoring"
);
return 0.0;
}
};
let age_minutes = (now - created_at).num_minutes().max(0) as f64;
let fraction = if age_minutes <= 5.0 {
1.0
} else if age_minutes <= 30.0 {
let t = (age_minutes - 5.0) / 25.0;
1.0 - t * 0.2
} else if age_minutes <= 60.0 {
let t = (age_minutes - 30.0) / 30.0;
0.8 - t * 0.3
} else if age_minutes <= 360.0 {
let t = (age_minutes - 60.0) / 300.0;
0.5 - t * 0.25
} else {
0.0
};
(fraction as f32 * max_score).clamp(0.0, max_score)
}
pub fn recency_score(tweet_created_at: &str, max_score: f32) -> f32 {
recency_score_at(tweet_created_at, max_score, Utc::now())
}
pub fn reply_count_score(reply_count: u64, max_score: f32) -> f32 {
if reply_count >= 20 {
return 0.0;
}
let fraction = 1.0 - (reply_count as f64 / 20.0);
(fraction as f32 * max_score).clamp(0.0, max_score)
}
pub fn targeted_follower_score(follower_count: u64, max_score: f32) -> f32 {
if follower_count == 0 {
return 0.0;
}
let fraction = if follower_count < 100 {
follower_count as f64 / 200.0
} else if follower_count < 1_000 {
0.5 + (follower_count as f64 - 100.0) / 1_800.0
} else if follower_count <= 10_000 {
1.0
} else if follower_count <= 100_000 {
let t = (follower_count as f64 - 10_000.0) / 90_000.0;
1.0 - t * 0.75
} else {
0.25
};
(fraction as f32 * max_score).clamp(0.0, max_score)
}
pub fn content_type_score(has_media: bool, is_quote_tweet: bool, max_score: f32) -> f32 {
if has_media || is_quote_tweet {
0.0
} else {
max_score
}
}
pub fn engagement_rate(
likes: u64,
retweets: u64,
replies: u64,
follower_count: u64,
max_score: f32,
) -> f32 {
let total_engagement = (likes + retweets + replies) as f64;
let followers = follower_count.max(1) as f64;
let rate = total_engagement / followers;
let score = (rate / 0.05).min(1.0) * max_score as f64;
(score as f32).clamp(0.0, max_score)
}