use std::collections::HashMap;
#[inline]
pub fn is_cjk_char(c: char) -> bool {
matches!(c,
'\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}' | '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{F900}'..='\u{FAFF}' | '\u{AC00}'..='\u{D7AF}' | '\u{20000}'..='\u{2A6DF}' )
}
pub fn contains_cjk(text: &str) -> bool {
let (cjk_count, char_count) = cjk_counts(text);
if char_count == 0 {
return false;
}
(cjk_count as f64 / char_count as f64) > 0.15
}
pub fn contains_cjk_f32(text: &str) -> bool {
let (cjk_count, char_count) = cjk_counts(text);
if char_count == 0 {
return false;
}
(cjk_count as f32 / char_count as f32) > 0.15
}
fn cjk_counts(text: &str) -> (usize, usize) {
text.chars().fold((0, 0), |(cjk, total), ch| {
(cjk + usize::from(is_cjk_char(ch)), total + 1)
})
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScriptProfile {
pub cjk_fraction: f64,
pub latin_fraction: f64,
pub char_count: usize,
}
impl ScriptProfile {
pub fn analyze(text: &str) -> Self {
let chars: Vec<char> = text.chars().collect();
let n = chars.len();
if n == 0 {
return Self {
cjk_fraction: 0.0,
latin_fraction: 0.0,
char_count: 0,
};
}
let cjk = chars.iter().filter(|&&c| is_cjk_char(c)).count();
let latin = chars.iter().filter(|&&c| c.is_ascii_alphabetic()).count();
Self {
cjk_fraction: cjk as f64 / n as f64,
latin_fraction: latin as f64 / n as f64,
char_count: n,
}
}
pub fn is_cjk_dominant(&self) -> bool {
self.cjk_fraction > 0.15
}
}
pub fn is_meaningful_query(query: &str) -> bool {
let trimmed = query.trim();
if trimmed.is_empty() {
return false;
}
let non_ws: Vec<char> = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
if non_ws.is_empty() {
return false;
}
if !non_ws.iter().any(|c| c.is_alphanumeric()) {
return false;
}
if non_ws.len() == 1 && non_ws[0].is_ascii_alphabetic() {
return false;
}
let total = non_ws.len();
if total > 1 {
let mut counts: HashMap<char, usize> = HashMap::new();
for c in &non_ws {
*counts.entry(*c).or_insert(0) += 1;
}
if let Some(&max_count) = counts.values().max() {
if max_count as f64 / total as f64 > 0.80 {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cjk_ideograph() {
assert!(is_cjk_char('使'));
assert!(is_cjk_char('用'));
assert!(is_cjk_char('进'));
}
#[test]
fn hiragana_katakana() {
assert!(is_cjk_char('あ'));
assert!(is_cjk_char('ア'));
}
#[test]
fn hangul() {
assert!(is_cjk_char('가'));
}
#[test]
fn latin_ascii_not_cjk() {
assert!(!is_cjk_char('a'));
assert!(!is_cjk_char('Z'));
assert!(!is_cjk_char('0'));
assert!(!is_cjk_char('-'));
}
#[test]
fn cjk_extension_a_boundary() {
assert!(is_cjk_char('\u{3400}')); assert!(is_cjk_char('\u{4DBF}')); assert!(!is_cjk_char('\u{33FF}')); assert!(!is_cjk_char('\u{4DC0}')); }
#[test]
fn unified_ideographs_boundary() {
assert!(is_cjk_char('\u{4E00}')); assert!(is_cjk_char('\u{9FFF}')); assert!(!is_cjk_char('\u{A000}')); }
#[test]
fn compatibility_ideographs_boundary() {
assert!(is_cjk_char('\u{F900}')); assert!(is_cjk_char('\u{FAFF}')); assert!(!is_cjk_char('\u{F8FF}')); assert!(!is_cjk_char('\u{FB00}')); }
#[test]
fn hiragana_boundary() {
assert!(is_cjk_char('\u{3040}')); assert!(is_cjk_char('\u{309F}')); assert!(!is_cjk_char('\u{303F}')); }
#[test]
fn katakana_boundary() {
assert!(is_cjk_char('\u{30A0}')); assert!(is_cjk_char('\u{30FF}')); assert!(!is_cjk_char('\u{3100}')); }
#[test]
fn hangul_boundary() {
assert!(is_cjk_char('\u{AC00}')); assert!(is_cjk_char('\u{D7AF}')); assert!(!is_cjk_char('\u{D7B0}')); }
#[test]
fn empty_string_no_cjk() {
assert!(!contains_cjk(""));
}
#[test]
fn all_latin_no_cjk() {
assert!(!contains_cjk("hello world"));
}
#[test]
fn all_cjk() {
assert!(contains_cjk("你好世界"));
}
#[test]
fn mixed_above_threshold() {
assert!(contains_cjk("abc你好"));
}
#[test]
fn mixed_below_threshold() {
assert!(!contains_cjk("abcdefghi你"));
}
#[test]
fn exactly_15_percent_is_false() {
let text = "你好世abcdefghijklmnopq"; assert_eq!(text.chars().count(), 20);
assert!(!contains_cjk(text));
}
#[test]
fn f32_classifier_preserves_strict_boundary_and_script_ranges() {
for (text, expected) in [
("", false),
("ordinary latin query", false),
("漢字かな한글", true),
("界界界abcdefghijklmnopq", false),
("界界界abcdefghijklmnopqr", false),
("界界界abcdefghijklmnop", true),
] {
assert_eq!(contains_cjk_f32(text), expected, "text={text:?}");
}
}
#[test]
fn profile_pure_latin() {
let p = ScriptProfile::analyze("hello");
assert_eq!(p.char_count, 5);
assert_eq!(p.cjk_fraction, 0.0);
assert_eq!(p.latin_fraction, 1.0);
assert!(!p.is_cjk_dominant());
}
#[test]
fn profile_pure_cjk() {
let p = ScriptProfile::analyze("你好");
assert_eq!(p.char_count, 2);
assert_eq!(p.cjk_fraction, 1.0);
assert_eq!(p.latin_fraction, 0.0);
assert!(p.is_cjk_dominant());
}
#[test]
fn profile_empty() {
let p = ScriptProfile::analyze("");
assert_eq!(p.char_count, 0);
assert_eq!(p.cjk_fraction, 0.0);
assert!(!p.is_cjk_dominant());
}
#[test]
fn profile_mixed() {
let p = ScriptProfile::analyze("hi你");
assert_eq!(p.char_count, 3);
assert!((p.cjk_fraction - 1.0 / 3.0).abs() < 1e-9);
assert!((p.latin_fraction - 2.0 / 3.0).abs() < 1e-9);
assert!(p.is_cjk_dominant());
}
#[test]
fn empty_not_meaningful() {
assert!(!is_meaningful_query(""));
assert!(!is_meaningful_query(" "));
}
#[test]
fn symbols_only_not_meaningful() {
assert!(!is_meaningful_query("!!!"));
assert!(!is_meaningful_query("@#$%"));
assert!(!is_meaningful_query("..."));
}
#[test]
fn single_latin_char_not_meaningful() {
assert!(!is_meaningful_query("a"));
assert!(!is_meaningful_query("Z"));
}
#[test]
fn repeated_char_gibberish_not_meaningful() {
assert!(!is_meaningful_query("aaaaaaa")); assert!(!is_meaningful_query("aaaaab")); }
#[test]
fn repeated_char_below_threshold_is_meaningful() {
assert!(is_meaningful_query("aaab")); }
#[test]
fn normal_queries_are_meaningful() {
assert!(is_meaningful_query("rust programming"));
assert!(is_meaningful_query("你好世界"));
assert!(is_meaningful_query("BM25"));
assert!(is_meaningful_query("ab"));
}
#[test]
fn single_digit_is_meaningful() {
assert!(is_meaningful_query("5"));
}
#[test]
fn unicode_symbol_only_queries_are_not_meaningful() {
assert!(!is_meaningful_query("\u{FF0C}")); assert!(!is_meaningful_query("\u{3001}")); assert!(!is_meaningful_query("\u{FF01}\u{FF1F}")); assert!(!is_meaningful_query("\u{1F600}"));
assert!(is_meaningful_query("\u{4F60}\u{597D}\u{4E16}\u{754C}")); assert!(is_meaningful_query("BM25"));
assert!(is_meaningful_query("5"));
}
}