const BUNDLED: &str = include_str!("../data/popular-actions.txt");
pub fn bundled_popular() -> Vec<String> {
BUNDLED
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(str::to_lowercase)
.collect()
}
pub fn similar_popular(owner_repo: &str, popular: &[String]) -> Option<String> {
let candidate = owner_repo.to_lowercase();
if popular.contains(&candidate) {
return None;
}
popular
.iter()
.find(|p| {
candidate.len().abs_diff(p.len()) <= 1 && osa_distance(&candidate, p) == 1
})
.cloned()
}
pub fn osa_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let (m, n) = (a.len(), b.len());
let mut d = vec![vec![0usize; n + 1]; m + 1];
for (i, row) in d.iter_mut().enumerate() {
row[0] = i;
}
for (j, cell) in d[0].iter_mut().enumerate() {
*cell = j;
}
for i in 1..=m {
for j in 1..=n {
let cost = usize::from(a[i - 1] != b[j - 1]);
d[i][j] = (d[i - 1][j] + 1)
.min(d[i][j - 1] + 1)
.min(d[i - 1][j - 1] + cost);
if i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1] {
d[i][j] = d[i][j].min(d[i - 2][j - 2] + 1);
}
}
}
d[m][n]
}
#[cfg(test)]
mod tests {
use super::{bundled_popular, osa_distance, similar_popular};
#[test]
fn transposition_counts_as_one() {
assert_eq!(osa_distance("aquasecurity", "aquasecurtiy"), 1);
assert_eq!(osa_distance("actions", "actoins"), 1);
assert_eq!(osa_distance("same", "same"), 0);
assert_eq!(osa_distance("checkout", "checkov"), 2);
}
#[test]
fn finds_one_edit_neighbors_but_not_exact_or_distant() {
let popular = bundled_popular();
assert_eq!(
similar_popular("aquasecurtiy/trivy-action", &popular).as_deref(),
Some("aquasecurity/trivy-action")
);
assert_eq!(
similar_popular("actions/checkoutt", &popular).as_deref(),
Some("actions/checkout")
);
assert_eq!(similar_popular("actions/checkout", &popular), None);
assert_eq!(similar_popular("mycorp/deploy-tool", &popular), None);
}
#[test]
fn bundled_list_has_no_internal_one_edit_collisions() {
let popular = bundled_popular();
for (i, a) in popular.iter().enumerate() {
for b in popular.iter().skip(i + 1) {
assert!(osa_distance(a, b) > 1, "목록 내부 충돌: {a} ↔ {b}");
}
}
}
}