#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Class {
White = 0,
NonWord = 1,
Delimiter = 2,
Lower = 3,
Upper = 4,
Number = 5,
}
const SCORE_MATCH: i32 = 16;
const SCORE_GAP_START: i32 = -3;
const SCORE_GAP_EXTENSION: i32 = -1;
const BONUS_BOUNDARY: i32 = SCORE_MATCH / 2;
const BONUS_NON_WORD: i32 = SCORE_MATCH / 2;
const BONUS_CAMEL_123: i32 = BONUS_BOUNDARY - 1;
const BONUS_CONSECUTIVE: i32 = -(SCORE_GAP_START + SCORE_GAP_EXTENSION);
const BONUS_FIRST_CHAR_MULTIPLIER: i32 = 2;
const BONUS_BOUNDARY_WHITE: i32 = BONUS_BOUNDARY;
const BONUS_BOUNDARY_DELIMITER: i32 = BONUS_BOUNDARY + 1;
const BONUS_FILENAME: i32 = BONUS_BOUNDARY - 2;
fn class_of(c: char) -> Class {
if c.is_whitespace() {
Class::White
} else if matches!(c, '/' | '\\' | ',' | ':' | ';' | '|') {
Class::Delimiter
} else if c.is_ascii_digit() {
Class::Number
} else if c.is_uppercase() {
Class::Upper
} else if c.is_lowercase() || c.is_alphabetic() {
Class::Lower
} else {
Class::NonWord
}
}
fn bonus_for(prev: Class, curr: Class) -> i32 {
if curr > Class::NonWord {
match prev {
Class::White => return BONUS_BOUNDARY_WHITE,
Class::Delimiter => return BONUS_BOUNDARY_DELIMITER,
Class::NonWord => return BONUS_BOUNDARY,
_ => {}
}
}
if (prev == Class::Lower && curr == Class::Upper)
|| (prev != Class::Number && curr == Class::Number)
{
return BONUS_CAMEL_123;
}
match curr {
Class::NonWord | Class::Delimiter => BONUS_NON_WORD,
Class::White => BONUS_BOUNDARY_WHITE,
_ => 0,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Match {
pub score: i32,
pub positions: Vec<usize>,
}
fn score_from(hay: &[char], lower: &[char], needle: &[char], start: usize) -> Option<Match> {
if lower[start] != needle[0] {
return None;
}
let mut positions = Vec::with_capacity(needle.len());
let mut score = 0i32;
let mut consecutive = 0usize;
let mut first_bonus = 0i32;
if !hay[start..].iter().any(|c| *c == '/' || *c == '\\') {
score += BONUS_FILENAME;
}
let mut prev_class = if start == 0 {
Class::White
} else {
class_of(hay[start - 1])
};
let mut previous: Option<usize> = None;
for (n, needle_char) in needle.iter().enumerate() {
let from = previous.map(|p| p + 1).unwrap_or(start);
let pos = (from..lower.len()).find(|i| lower[*i] == *needle_char)?;
let class = class_of(hay[pos]);
let gap = previous.map(|p| pos - p - 1).unwrap_or(0);
let bonus = if gap > 0 {
prev_class = class_of(hay[pos - 1]);
let b = bonus_for(prev_class, class);
score += SCORE_GAP_START + (gap as i32 - 1) * SCORE_GAP_EXTENSION;
consecutive = 0;
first_bonus = 0;
b
} else {
let b = bonus_for(prev_class, class);
if consecutive == 0 {
first_bonus = b;
b
} else {
if b >= BONUS_BOUNDARY && b > first_bonus {
first_bonus = b;
}
b.max(first_bonus).max(BONUS_CONSECUTIVE)
}
};
score += SCORE_MATCH
+ if n == 0 {
bonus * BONUS_FIRST_CHAR_MULTIPLIER
} else {
bonus
};
consecutive += 1;
prev_class = class;
previous = Some(pos);
positions.push(pos);
}
Some(Match { score, positions })
}
pub fn best_match(needle: &str, haystack: &str) -> Option<Match> {
if needle.is_empty() {
return Some(Match {
score: 0,
positions: Vec::new(),
});
}
let hay: Vec<char> = haystack.chars().collect();
let lower: Vec<char> = haystack.to_lowercase().chars().collect();
let needle: Vec<char> = needle.to_lowercase().chars().collect();
if lower.len() != hay.len() {
return simple_match(&hay, &needle);
}
if needle.len() > hay.len() {
return None;
}
if !subsequence(&lower, &needle) {
return None;
}
let mut best: Option<Match> = None;
for start in 0..hay.len() {
if lower[start] != needle[0] {
continue;
}
if let Some(candidate) = score_from(&hay, &lower, &needle, start) {
if best.as_ref().is_none_or(|b| candidate.score > b.score) {
best = Some(candidate);
}
} else {
break;
}
}
best
}
fn simple_match(hay: &[char], needle: &[char]) -> Option<Match> {
let mut positions = Vec::with_capacity(needle.len());
let mut hi = 0usize;
for nc in needle {
let found =
(hi..hay.len()).find(|i| hay[*i].to_lowercase().next().is_some_and(|c| c == *nc))?;
positions.push(found);
hi = found + 1;
}
Some(Match {
score: (positions.len() as i32) * SCORE_MATCH,
positions,
})
}
fn subsequence(lower: &[char], needle: &[char]) -> bool {
let mut hi = 0usize;
for nc in needle {
match (hi..lower.len()).find(|i| lower[*i] == *nc) {
Some(found) => hi = found + 1,
None => return false,
}
}
true
}
pub fn is_match(needle: &str, haystack: &str) -> bool {
if needle.is_empty() {
return true;
}
let mut chars = haystack.chars().flat_map(|c| c.to_lowercase());
'outer: for nc in needle.chars().flat_map(|c| c.to_lowercase()) {
for hc in chars.by_ref() {
if hc == nc {
continue 'outer;
}
}
return false;
}
true
}