use rapidhash::RapidHashSet as HashSet;
pub const DEFAULT_STOP_WORDS: &[&str] = &[
"a", "is", "the", "an", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into",
"it", "no", "not", "of", "on", "or", "such", "that", "their", "then", "there", "these", "they",
"this", "to", "was", "will", "with",
];
const STACK_BUFFER_MAX_LEN: usize = 64;
#[inline]
pub fn tokenize_text(text: &str) -> Vec<String> {
tokenize_text_with_stopwords(text, None)
}
pub fn tokenize_text_with_stopwords(
text: &str,
stop_words: Option<&HashSet<String>>,
) -> Vec<String> {
let mut words = Vec::new();
let mut cur = String::with_capacity(16);
for ch in text.chars() {
if ch.is_alphanumeric() || ch == '_' {
cur.push(ch.to_ascii_lowercase());
} else if !cur.is_empty() {
if stop_words.is_none_or(|sw| !sw.contains(&cur)) {
words.push(cur);
cur = String::with_capacity(16);
} else {
cur.clear();
}
}
}
if !cur.is_empty() && stop_words.is_none_or(|sw| !sw.contains(&cur)) {
words.push(cur);
}
words
}
#[inline]
pub fn unescape_tag_string(s: &str) -> String {
if !s.contains('\\') {
return s.to_string();
}
let mut res = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(next_ch) = chars.next() {
res.push(next_ch);
}
} else {
res.push(ch);
}
}
res
}
pub fn tokenize_tags(text: &str, separator: char, case_sensitive: bool) -> Vec<String> {
let mut tags = Vec::new();
let mut cur = String::with_capacity(32);
let mut escaped = false;
for ch in text.chars() {
if escaped {
cur.push('\\');
cur.push(ch);
escaped = false;
continue;
}
if ch == '\\' {
escaped = true;
continue;
}
if ch == separator {
let trimmed = cur.trim().trim_matches('"').trim_matches('\'');
if !trimmed.is_empty() {
let unescaped = unescape_tag_string(trimmed);
tags.push(if case_sensitive {
unescaped
} else {
unescaped.to_lowercase()
});
}
cur.clear();
} else {
cur.push(ch);
}
}
if escaped {
cur.push('\\');
}
let trimmed = cur.trim().trim_matches('"').trim_matches('\'');
if !trimmed.is_empty() {
let unescaped = unescape_tag_string(trimmed);
tags.push(if case_sensitive {
unescaped
} else {
unescaped.to_lowercase()
});
}
tags
}
pub fn levenshtein_distance(s1: &str, s2: &str) -> usize {
if s1 == s2 {
return 0;
}
if s1.is_empty() {
return s2.chars().count();
}
if s2.is_empty() {
return s1.chars().count();
}
if s1.is_ascii() && s2.is_ascii() {
let b1 = s1.as_bytes();
let b2 = s2.as_bytes();
let (b1, b2) = if b1.len() < b2.len() {
(b2, b1)
} else {
(b1, b2)
};
let len1 = b1.len();
let len2 = b2.len();
if len2 <= STACK_BUFFER_MAX_LEN {
let mut prev = [0usize; STACK_BUFFER_MAX_LEN + 1];
let mut curr = [0usize; STACK_BUFFER_MAX_LEN + 1];
for (j, item) in prev.iter_mut().enumerate().take(len2 + 1) {
*item = j;
}
for i in 1..=len1 {
curr[0] = i;
for j in 1..=len2 {
let cost = usize::from(b1[i - 1] != b2[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
prev[..=len2].copy_from_slice(&curr[..=len2]);
}
return prev[len2];
}
let mut prev: Vec<usize> = (0..=len2).collect();
let mut curr = vec![0; len2 + 1];
for i in 1..=len1 {
curr[0] = i;
for j in 1..=len2 {
let cost = usize::from(b1[i - 1] != b2[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
prev.copy_from_slice(&curr);
}
return prev[len2];
}
let s1_chars: Vec<char> = s1.chars().collect();
let s2_chars: Vec<char> = s2.chars().collect();
let (s1_chars, s2_chars) = if s1_chars.len() < s2_chars.len() {
(s2_chars, s1_chars)
} else {
(s1_chars, s2_chars)
};
let len1 = s1_chars.len();
let len2 = s2_chars.len();
if len2 <= STACK_BUFFER_MAX_LEN {
let mut prev = [0usize; STACK_BUFFER_MAX_LEN + 1];
let mut curr = [0usize; STACK_BUFFER_MAX_LEN + 1];
for (j, item) in prev.iter_mut().enumerate().take(len2 + 1) {
*item = j;
}
for i in 1..=len1 {
curr[0] = i;
for j in 1..=len2 {
let cost = usize::from(s1_chars[i - 1] != s2_chars[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
prev[..=len2].copy_from_slice(&curr[..=len2]);
}
return prev[len2];
}
let mut prev: Vec<usize> = (0..=len2).collect();
let mut curr = vec![0; len2 + 1];
for i in 1..=len1 {
curr[0] = i;
for j in 1..=len2 {
let cost = usize::from(s1_chars[i - 1] != s2_chars[j - 1]);
curr[j] = (prev[j] + 1).min(curr[j - 1] + 1).min(prev[j - 1] + cost);
}
prev.copy_from_slice(&curr);
}
prev[len2]
}