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