use unicode_normalization::UnicodeNormalization;
use unicode_segmentation::UnicodeSegmentation;
fn nfkc_lower(s: &str) -> String {
let mut out = String::new();
for c in s.nfkc() {
match c {
'ß' | 'ẞ' => out.push_str("ss"),
_ => out.extend(c.to_lowercase()),
}
}
out
}
fn fold_query_units(query: &str) -> Vec<String> {
let mut out: Vec<String> = Vec::new();
for g in query.graphemes(true) {
let b = g.as_bytes();
if b.len() == 1 && b[0].is_ascii() {
out.push(((b[0] as char).to_ascii_lowercase()).to_string());
continue;
}
let folded: String = nfkc_lower(g);
for fg in folded.graphemes(true) {
if !fg.is_empty() {
out.push(fg.to_string());
}
}
}
out
}
fn fold_candidate_units(candidate: &str) -> (Vec<&str>, Vec<(String, usize)>) {
let cgs: Vec<&str> = candidate.graphemes(true).collect();
let mut units: Vec<(String, usize)> = Vec::new();
for (i, cg) in cgs.iter().enumerate() {
let b = cg.as_bytes();
if b.len() == 1 && b[0].is_ascii() {
units.push((((b[0] as char).to_ascii_lowercase()).to_string(), i));
continue;
}
let folded: String = nfkc_lower(cg);
for fg in folded.graphemes(true) {
if !fg.is_empty() {
units.push((fg.to_string(), i));
}
}
}
(cgs, units)
}
pub fn match_positions_graphemes(query: &str, candidate: &str) -> Option<Vec<usize>> {
let qu: Vec<String> = fold_query_units(query);
if qu.is_empty() {
return Some(Vec::new());
}
let (_cgs, cu) = fold_candidate_units(candidate);
let mut q_i: usize = 0;
let mut raw_positions: Vec<usize> = Vec::new();
for (unit, orig_i) in cu {
if q_i >= qu.len() {
break;
}
if unit == qu[q_i] {
raw_positions.push(orig_i);
q_i += 1;
if q_i == qu.len() {
break;
}
}
}
if q_i != qu.len() {
return None;
}
let mut positions: Vec<usize> = Vec::new();
for p in raw_positions {
if positions.last().copied() != Some(p) {
positions.push(p);
}
}
Some(positions)
}
pub fn fuzzy_match_positions_graphemes(query: &str, candidate: &str) -> Option<(Vec<usize>, i64)> {
fuzzy_match_positions_graphemes_v1(query, candidate)
}
pub fn fuzzy_match_positions_graphemes_latest(
query: &str,
candidate: &str,
) -> Option<(Vec<usize>, i64)> {
fuzzy_match_positions_graphemes_v1(query, candidate)
}
pub fn fuzzy_match_positions_graphemes_v1(
query: &str,
candidate: &str,
) -> Option<(Vec<usize>, i64)> {
let qu: Vec<String> = fold_query_units(query);
if qu.is_empty() {
return Some((Vec::new(), 0));
}
let (cgs, cu) = fold_candidate_units(candidate);
let mut q_i: usize = 0;
let mut raw_positions: Vec<usize> = Vec::new();
for (unit, orig_i) in cu {
if q_i >= qu.len() {
break;
}
if unit == qu[q_i] {
raw_positions.push(orig_i);
q_i += 1;
if q_i == qu.len() {
break;
}
}
}
if q_i != qu.len() {
return None;
}
let mut positions: Vec<usize> = Vec::new();
for p in raw_positions {
if positions.last().copied() != Some(p) {
positions.push(p);
}
}
let score = score_match_positions(&cgs, &positions);
Some((positions, score))
}
fn is_ascii_word_grapheme(g: &str) -> bool {
let b = g.as_bytes();
if b.len() != 1 {
return false;
}
matches!(b[0], b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'_')
}
fn is_word_boundary(cgs: &[&str], pos: usize) -> bool {
if pos == 0 {
return true;
}
let cur_word = is_ascii_word_grapheme(cgs[pos]);
if !cur_word {
return false;
}
let prev_word = is_ascii_word_grapheme(cgs[pos - 1]);
!prev_word
}
fn score_match_positions(cgs: &[&str], positions: &[usize]) -> i64 {
let mut score: i64 = 0;
score -= cgs.len() as i64;
score += 10 * positions.len() as i64;
if positions.first().copied() == Some(0) {
score += 30;
}
let mut prev: Option<usize> = None;
for &p in positions {
if is_word_boundary(cgs, p) {
score += 15;
}
if let Some(pp) = prev {
if p == pp + 1 {
score += 20;
} else if p > pp + 1 {
score -= (p - (pp + 1)) as i64;
}
}
prev = Some(p);
}
score
}