pub(crate) fn is_cjk(c: char) -> bool {
matches!(c as u32, 0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0xF900..=0xFAFF)
}
pub(crate) fn extract_keywords(text: &str) -> Vec<String> {
let mut keywords = Vec::new();
let mut cjk_run: Vec<char> = Vec::new();
let mut ascii_run = String::new();
let flush_cjk = |run: &mut Vec<char>, out: &mut Vec<String>| {
for w in run.windows(2) {
out.push(w.iter().collect());
}
run.clear();
};
let flush_ascii = |run: &mut String, out: &mut Vec<String>| {
if !run.is_empty() {
out.push(std::mem::take(run));
}
};
for c in text.chars() {
if is_cjk(c) {
flush_ascii(&mut ascii_run, &mut keywords);
cjk_run.push(c);
} else if c.is_ascii_alphanumeric() {
flush_cjk(&mut cjk_run, &mut keywords);
ascii_run.push(c);
} else {
flush_cjk(&mut cjk_run, &mut keywords);
flush_ascii(&mut ascii_run, &mut keywords);
}
}
flush_cjk(&mut cjk_run, &mut keywords);
flush_ascii(&mut ascii_run, &mut keywords);
keywords
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_keywords_cjk_bigrams() {
let kws = extract_keywords("安装配置指南");
assert_eq!(kws, vec!["安装", "装配", "配置", "置指", "指南"]);
}
#[test]
fn test_extract_keywords_mixed() {
let mixed = extract_keywords("支持 Setup v2 认证");
assert!(mixed.contains(&"Setup".to_string()));
assert!(mixed.contains(&"v2".to_string()));
assert!(mixed.contains(&"认证".to_string()));
}
#[test]
fn test_extract_keywords_empty() {
assert!(extract_keywords("").is_empty(), "空串返回空");
assert!(extract_keywords("!!!---").is_empty(), "纯标点无关键词返回空");
}
}