use std::collections::HashSet;
pub const RELEVANCE_THRESHOLD: f64 = 0.70;
pub fn calculate_relevance(mr_title: &str, commit_msg: &str) -> f64 {
let extract_keywords = |text: &str| -> HashSet<String> {
text.to_lowercase()
.split(|c: char| !c.is_alphabetic())
.map(|s| s.to_string())
.filter(|s| s.len() > 2 && s != "feat" && s != "fix" && s != "refactor" && s != "chore")
.collect()
};
let mr_words = extract_keywords(mr_title);
let commit_words = extract_keywords(commit_msg);
if mr_words.is_empty() || commit_words.is_empty() {
return 0.0;
}
let common_words = mr_words.intersection(&commit_words).count();
let total_keywords_to_match = mr_words.len().min(commit_words.len());
if total_keywords_to_match == 0 {
return 0.0;
}
(common_words as f64) / (total_keywords_to_match as f64)
}
pub fn parse_duration_to_hours(input: &str) -> Result<f32, String> {
let s = input.trim().to_lowercase().replace(',', ".");
if let Some(h_pos) = s.find('h') {
let hours_part = &s[..h_pos];
let minutes_part = s[h_pos + 1..].trim_end_matches('m');
let hours: f32 = hours_part
.parse()
.map_err(|_| format!("Invalid hours in \"{}\"", input))?;
let minutes: f32 = if minutes_part.is_empty() {
0.0
} else {
minutes_part
.parse()
.map_err(|_| format!("Invalid minutes in \"{}\"", input))?
};
let total = hours + minutes / 60.0;
if total <= 0.0 {
return Err("Duration must be greater than zero".into());
}
return Ok(total);
}
if let Some(stripped) = s.strip_suffix('m') {
let minutes: f32 = stripped
.parse()
.map_err(|_| format!("Invalid minutes in \"{}\"", input))?;
let total = minutes / 60.0;
if total <= 0.0 {
return Err("Duration must be greater than zero".into());
}
return Ok(total);
}
if let Ok(minutes) = s.parse::<f32>() {
if minutes <= 0.0 {
return Err("Duration must be greater than zero".into());
}
return Ok(minutes / 60.0);
}
Err(format!(
"Unrecognised format \"{}\". Try: 1h30, 90m, 1.5h",
input
))
}