1use std::collections::HashMap;
4
5#[inline]
9pub fn is_cjk_char(c: char) -> bool {
10 matches!(c,
11 '\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}' )
19}
20
21pub fn contains_cjk(text: &str) -> bool {
23 let chars: Vec<char> = text.chars().collect();
24 if chars.is_empty() {
25 return false;
26 }
27 let cjk_count = chars.iter().filter(|&&c| is_cjk_char(c)).count();
28 (cjk_count as f64 / chars.len() as f64) > 0.15
29}
30
31#[derive(Debug, Clone, PartialEq)]
35pub struct ScriptProfile {
36 pub cjk_fraction: f64,
38 pub latin_fraction: f64,
40 pub char_count: usize,
42}
43
44impl ScriptProfile {
45 pub fn analyze(text: &str) -> Self {
47 let chars: Vec<char> = text.chars().collect();
48 let n = chars.len();
49 if n == 0 {
50 return Self {
51 cjk_fraction: 0.0,
52 latin_fraction: 0.0,
53 char_count: 0,
54 };
55 }
56 let cjk = chars.iter().filter(|&&c| is_cjk_char(c)).count();
57 let latin = chars.iter().filter(|&&c| c.is_ascii_alphabetic()).count();
58 Self {
59 cjk_fraction: cjk as f64 / n as f64,
60 latin_fraction: latin as f64 / n as f64,
61 char_count: n,
62 }
63 }
64
65 pub fn is_cjk_dominant(&self) -> bool {
67 self.cjk_fraction > 0.15
68 }
69}
70
71pub fn is_meaningful_query(query: &str) -> bool {
76 let trimmed = query.trim();
77 if trimmed.is_empty() {
78 return false;
79 }
80
81 let non_ws: Vec<char> = trimmed.chars().filter(|c| !c.is_whitespace()).collect();
82 if non_ws.is_empty() {
83 return false;
84 }
85
86 if !non_ws.iter().any(|c| c.is_alphanumeric()) {
88 return false;
89 }
90
91 if non_ws.len() == 1 && non_ws[0].is_ascii_alphabetic() {
93 return false;
94 }
95
96 let total = non_ws.len();
99 if total > 1 {
100 let mut counts: HashMap<char, usize> = HashMap::new();
101 for c in &non_ws {
102 *counts.entry(*c).or_insert(0) += 1;
103 }
104 if let Some(&max_count) = counts.values().max() {
105 if max_count as f64 / total as f64 > 0.80 {
106 return false;
107 }
108 }
109 }
110
111 true
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn cjk_ideograph() {
120 assert!(is_cjk_char('使'));
121 assert!(is_cjk_char('用'));
122 assert!(is_cjk_char('进'));
123 }
124
125 #[test]
126 fn hiragana_katakana() {
127 assert!(is_cjk_char('あ'));
128 assert!(is_cjk_char('ア'));
129 }
130
131 #[test]
132 fn hangul() {
133 assert!(is_cjk_char('가'));
134 }
135
136 #[test]
137 fn latin_ascii_not_cjk() {
138 assert!(!is_cjk_char('a'));
139 assert!(!is_cjk_char('Z'));
140 assert!(!is_cjk_char('0'));
141 assert!(!is_cjk_char('-'));
142 }
143
144 #[test]
145 fn cjk_extension_a_boundary() {
146 assert!(is_cjk_char('\u{3400}')); assert!(is_cjk_char('\u{4DBF}')); assert!(!is_cjk_char('\u{33FF}')); assert!(!is_cjk_char('\u{4DC0}')); }
151
152 #[test]
153 fn unified_ideographs_boundary() {
154 assert!(is_cjk_char('\u{4E00}')); assert!(is_cjk_char('\u{9FFF}')); assert!(!is_cjk_char('\u{A000}')); }
158
159 #[test]
160 fn compatibility_ideographs_boundary() {
161 assert!(is_cjk_char('\u{F900}')); assert!(is_cjk_char('\u{FAFF}')); assert!(!is_cjk_char('\u{F8FF}')); assert!(!is_cjk_char('\u{FB00}')); }
166
167 #[test]
168 fn hiragana_boundary() {
169 assert!(is_cjk_char('\u{3040}')); assert!(is_cjk_char('\u{309F}')); assert!(!is_cjk_char('\u{303F}')); }
173
174 #[test]
175 fn katakana_boundary() {
176 assert!(is_cjk_char('\u{30A0}')); assert!(is_cjk_char('\u{30FF}')); assert!(!is_cjk_char('\u{3100}')); }
180
181 #[test]
182 fn hangul_boundary() {
183 assert!(is_cjk_char('\u{AC00}')); assert!(is_cjk_char('\u{D7AF}')); assert!(!is_cjk_char('\u{D7B0}')); }
187
188 #[test]
189 fn empty_string_no_cjk() {
190 assert!(!contains_cjk(""));
191 }
192
193 #[test]
194 fn all_latin_no_cjk() {
195 assert!(!contains_cjk("hello world"));
196 }
197
198 #[test]
199 fn all_cjk() {
200 assert!(contains_cjk("你好世界"));
201 }
202
203 #[test]
204 fn mixed_above_threshold() {
205 assert!(contains_cjk("abc你好"));
207 }
208
209 #[test]
210 fn mixed_below_threshold() {
211 assert!(!contains_cjk("abcdefghi你"));
213 }
214
215 #[test]
216 fn exactly_15_percent_is_false() {
217 let text = "你好世abcdefghijklmnopq"; assert_eq!(text.chars().count(), 20);
220 assert!(!contains_cjk(text));
221 }
222
223 #[test]
224 fn profile_pure_latin() {
225 let p = ScriptProfile::analyze("hello");
226 assert_eq!(p.char_count, 5);
227 assert_eq!(p.cjk_fraction, 0.0);
228 assert_eq!(p.latin_fraction, 1.0);
229 assert!(!p.is_cjk_dominant());
230 }
231
232 #[test]
233 fn profile_pure_cjk() {
234 let p = ScriptProfile::analyze("你好");
235 assert_eq!(p.char_count, 2);
236 assert_eq!(p.cjk_fraction, 1.0);
237 assert_eq!(p.latin_fraction, 0.0);
238 assert!(p.is_cjk_dominant());
239 }
240
241 #[test]
242 fn profile_empty() {
243 let p = ScriptProfile::analyze("");
244 assert_eq!(p.char_count, 0);
245 assert_eq!(p.cjk_fraction, 0.0);
246 assert!(!p.is_cjk_dominant());
247 }
248
249 #[test]
250 fn profile_mixed() {
251 let p = ScriptProfile::analyze("hi你");
253 assert_eq!(p.char_count, 3);
254 assert!((p.cjk_fraction - 1.0 / 3.0).abs() < 1e-9);
255 assert!((p.latin_fraction - 2.0 / 3.0).abs() < 1e-9);
256 assert!(p.is_cjk_dominant());
257 }
258
259 #[test]
260 fn empty_not_meaningful() {
261 assert!(!is_meaningful_query(""));
262 assert!(!is_meaningful_query(" "));
263 }
264
265 #[test]
266 fn symbols_only_not_meaningful() {
267 assert!(!is_meaningful_query("!!!"));
268 assert!(!is_meaningful_query("@#$%"));
269 assert!(!is_meaningful_query("..."));
270 }
271
272 #[test]
273 fn single_latin_char_not_meaningful() {
274 assert!(!is_meaningful_query("a"));
275 assert!(!is_meaningful_query("Z"));
276 }
277
278 #[test]
279 fn repeated_char_gibberish_not_meaningful() {
280 assert!(!is_meaningful_query("aaaaaaa")); assert!(!is_meaningful_query("aaaaab")); }
283
284 #[test]
285 fn repeated_char_below_threshold_is_meaningful() {
286 assert!(is_meaningful_query("aaab")); }
288
289 #[test]
290 fn normal_queries_are_meaningful() {
291 assert!(is_meaningful_query("rust programming"));
292 assert!(is_meaningful_query("你好世界"));
293 assert!(is_meaningful_query("BM25"));
294 assert!(is_meaningful_query("ab"));
295 }
296
297 #[test]
298 fn single_digit_is_meaningful() {
299 assert!(is_meaningful_query("5"));
301 }
302
303 #[test]
304 fn unicode_symbol_only_queries_are_not_meaningful() {
305 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"));
312 assert!(is_meaningful_query("5"));
313 }
314}