pub fn score(haystack: &str, needle: &str) -> Option<(i64, Vec<usize>)> {
if needle.is_empty() {
return Some((0, Vec::new()));
}
if let Some(byte_idx) = haystack.find(needle) {
let start_char = haystack[..byte_idx].chars().count();
let needle_len = needle.chars().count();
let positions: Vec<usize> = (start_char..start_char + needle_len).collect();
let prev_ch = haystack[..byte_idx].chars().last();
let at_boundary = byte_idx == 0 || matches!(prev_ch, Some('/' | '_' | '-' | '.' | ' '));
let mut total: i64 = needle_len as i64;
if at_boundary {
total += 8;
}
total += (needle_len as i64 - 1).max(0) * 5; total += 100; total -= haystack.chars().count() as i64 / 8;
return Some((total, positions));
}
let mut needle_chars = needle.chars().peekable();
let mut total: i64 = 0;
let mut prev_match = false;
let mut positions: Vec<usize> = Vec::new();
let mut prev_ch: Option<char> = None;
for (ci, ch) in haystack.chars().enumerate() {
if let Some(&nc) = needle_chars.peek() {
if ch == nc {
if prev_match {
total += 5;
}
let at_boundary = prev_ch
.map(|p| matches!(p, '/' | '_' | '-' | '.' | ' '))
.unwrap_or(true);
if at_boundary {
total += 8;
}
total += 1;
prev_match = true;
positions.push(ci);
needle_chars.next();
} else {
prev_match = false;
}
}
prev_ch = Some(ch);
}
if needle_chars.peek().is_some() {
return None;
}
total -= haystack.chars().count() as i64 / 8;
Some((total, positions))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn score_subsequence_match() {
assert!(score("src/main.rs", "main").is_some());
assert!(score("src/main.rs", "smr").is_some());
assert!(score("src/main.rs", "xyz").is_none());
}
#[test]
fn score_word_boundary_beats_mid_word() {
let (a, _) = score("src/main.rs", "main").unwrap();
let (b, _) = score("src/domain.rs", "main").unwrap();
assert!(a > b);
}
#[test]
fn score_shorter_wins_on_ties() {
let (a, _) = score("a/b/foo.rs", "foo").unwrap();
let (b, _) = score("a/b/c/d/e/foo.rs", "foo").unwrap();
assert!(a > b);
}
#[test]
fn score_returns_match_positions() {
let (_, positions) = score("foo_bar", "fb").unwrap();
assert_eq!(positions, vec![0, 4]);
}
#[test]
fn score_match_positions_skip_unmatched() {
let (_, positions) = score("hello world", "hw").unwrap();
assert_eq!(positions, vec![0, 6]);
}
#[test]
fn substring_match_returns_contiguous_positions() {
let (_, positions) = score("/home/mxaddict/foo/main/lib.rs", "main").unwrap();
assert_eq!(positions, vec![19, 20, 21, 22]);
}
#[test]
fn substring_match_outranks_scattered_subsequence() {
let (a, _) = score("/p/main.rs", "main").unwrap();
let (b, _) = score("/m/a/i/extra/n.rs", "main").unwrap();
assert!(a > b, "contiguous {a} must beat scattered {b}");
}
}