1use hashbrown::HashMap;
2#[cfg(feature = "parallel")]
3use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};
4use smallvec::SmallVec;
5use std::collections::HashSet;
6use std::hash::{DefaultHasher, Hash, Hasher};
7use std::sync::Arc;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum SpaceHandling {
12 Strict,
14 IgnoreSpaces,
16 NormalizeSpaces,
18}
19
20#[derive(Debug, Clone)]
23pub struct WuManber {
24 patterns: Vec<Arc<String>>,
25 original_patterns: Vec<String>,
26 pattern_set: HashSet<Arc<String>>, min_len: usize,
28 block_size: usize,
29 shift_table: HashMap<u64, usize>,
30 hash_table: HashMap<u64, SmallVec<[usize; 4]>>,
31 space_handling: SpaceHandling,
32}
33
34impl WuManber {
35 pub fn new_chinese(patterns: Vec<String>) -> Self {
37 Self::new_with_space_handling(patterns, SpaceHandling::Strict)
38 }
39
40 pub fn new(patterns: Vec<String>, block_size: usize) -> Self {
42 Self::new_with_block_size_and_space_handling(patterns, block_size, SpaceHandling::Strict)
43 }
44
45 pub fn new_parallel(patterns: Vec<String>, block_size: usize) -> Self {
47 Self::new_parallel_with_space_handling(patterns, block_size, SpaceHandling::Strict)
48 }
49
50 pub fn new_with_space_handling(patterns: Vec<String>, space_handling: SpaceHandling) -> Self {
52 let block_size = Self::calculate_optimal_block_size(&patterns, space_handling);
53 Self::new_with_block_size_and_space_handling(patterns, block_size, space_handling)
54 }
55
56 pub fn new_with_block_size_and_space_handling(
58 patterns: Vec<String>,
59 block_size: usize,
60 space_handling: SpaceHandling,
61 ) -> Self {
62 Self::build_instance(patterns, block_size, false, space_handling)
63 }
64
65 pub fn new_parallel_with_space_handling(
67 patterns: Vec<String>,
68 block_size: usize,
69 space_handling: SpaceHandling,
70 ) -> Self {
71 Self::build_instance(patterns, block_size, true, space_handling)
72 }
73
74 fn calculate_optimal_block_size(patterns: &[String], space_handling: SpaceHandling) -> usize {
76 if patterns.is_empty() {
77 return 2;
78 }
79
80 let processed_patterns: Vec<String> =
81 patterns.iter().map(|p| Self::preprocess_pattern(p, space_handling)).filter(|p| !p.is_empty()).collect();
82
83 let min_len = processed_patterns.iter().map(|p| p.chars().count()).min().unwrap_or(2);
84
85 match min_len {
87 1 => 1,
88 2..=3 => 2,
89 4..=10 => 3,
90 _ => (min_len / 2).clamp(2, 4),
91 }
92 }
93
94 fn preprocess_pattern(pattern: &str, space_handling: SpaceHandling) -> String {
96 match space_handling {
97 SpaceHandling::Strict => pattern.to_string(),
98 SpaceHandling::IgnoreSpaces => pattern.chars().filter(|c| !c.is_whitespace()).collect(),
99 SpaceHandling::NormalizeSpaces => {
100 let mut result = String::new();
101 let mut prev_was_space = false;
102
103 for ch in pattern.chars() {
104 if ch.is_whitespace() {
105 if !prev_was_space {
106 result.push(' ');
107 prev_was_space = true;
108 }
109 } else {
110 result.push(ch);
111 prev_was_space = false;
112 }
113 }
114
115 result
116 }
117 }
118 }
119
120 fn preprocess_text(&self, text: &str) -> String {
122 Self::preprocess_pattern(text, self.space_handling)
123 }
124
125 fn empty() -> Self {
127 WuManber {
128 patterns: Vec::new(),
129 original_patterns: Vec::new(),
130 pattern_set: HashSet::new(),
131 min_len: 0,
132 block_size: 2,
133 shift_table: HashMap::new(),
134 hash_table: HashMap::new(),
135 space_handling: SpaceHandling::Strict,
136 }
137 }
138
139 fn build_instance(patterns: Vec<String>, block_size: usize, parallel: bool, space_handling: SpaceHandling) -> Self {
141 if patterns.is_empty() {
142 return Self::empty();
143 }
144
145 let original_patterns = patterns.clone();
147
148 let processed_patterns: Vec<String> =
150 patterns.iter().map(|p| Self::preprocess_pattern(p, space_handling)).filter(|p| !p.is_empty()).collect();
151
152 if processed_patterns.is_empty() {
153 return Self::empty();
154 }
155
156 let min_len = processed_patterns.iter().map(|p| p.chars().count()).min().unwrap_or(1);
157
158 let safe_block_size = block_size.min(min_len);
160
161 let patterns_arc: Vec<Arc<String>> = processed_patterns.into_iter().map(Arc::new).collect();
162 let pattern_set: HashSet<Arc<String>> = patterns_arc.iter().cloned().collect();
164 let mut instance = WuManber {
165 patterns: patterns_arc,
166 original_patterns,
167 pattern_set,
168 min_len,
169 block_size: safe_block_size,
170 shift_table: HashMap::new(),
171 hash_table: HashMap::new(),
172 space_handling,
173 };
174
175 #[cfg(feature = "parallel")]
178 {
179 if parallel {
180 instance.build_tables_parallel();
181 return instance;
182 }
183 }
184 #[cfg(not(feature = "parallel"))]
185 let _ = parallel;
186
187 instance.build_tables();
188
189 instance
190 }
191
192 fn build_tables(&mut self) {
194 self.build_shift_table();
195 self.build_hash_table();
196 }
197
198 #[cfg(feature = "parallel")]
200 fn build_tables_parallel(&mut self) {
201 self.build_shift_table_parallel();
202 self.build_hash_table_parallel();
203 }
204
205 fn build_shift_table(&mut self) {
207 for pattern in self.patterns.iter() {
208 let chars: Vec<char> = pattern.chars().collect();
209 let char_count = chars.len();
210
211 if char_count < self.block_size {
213 continue;
214 }
215
216 for i in 0..=(char_count - self.block_size) {
217 let block = self.extract_block_optimized(&chars, i);
218 let hash = Self::calculate_hash_fast(&block);
219 let shift = char_count - i - self.block_size;
220
221 self.shift_table.entry(hash).and_modify(|v| *v = (*v).min(shift)).or_insert(shift);
222 }
223 }
224 }
225
226 fn build_hash_table(&mut self) {
228 for (pattern_idx, pattern) in self.patterns.iter().enumerate() {
229 let chars: Vec<char> = pattern.chars().collect();
230 let char_count = chars.len();
231
232 if char_count >= self.block_size {
233 let start_pos = char_count - self.block_size;
234 let block = self.extract_block_optimized(&chars, start_pos);
235 let hash = Self::calculate_hash_fast(&block);
236
237 self.hash_table.entry(hash).or_default().push(pattern_idx);
238 }
239 }
240 }
241
242 #[cfg(feature = "parallel")]
244 fn build_shift_table_parallel(&mut self) {
245 let block_size = self.block_size;
246
247 let shift_entries: Vec<(u64, usize)> = self
248 .patterns
249 .par_iter()
250 .flat_map(|pattern| {
251 let chars: Vec<char> = pattern.chars().collect();
252 let char_count = chars.len();
253
254 if char_count < block_size {
255 return Vec::new();
256 }
257
258 (0..=(char_count - block_size))
259 .map(move |i| {
260 let block = chars[i..i + block_size].iter().collect::<String>();
261 let hash = Self::calculate_hash_fast(&block);
262 let shift = char_count - i - block_size;
263 (hash, shift)
264 })
265 .collect::<Vec<_>>()
266 })
267 .collect();
268
269 for (hash, shift) in shift_entries {
270 self.shift_table.entry(hash).and_modify(|v| *v = (*v).min(shift)).or_insert(shift);
271 }
272 }
273
274 #[cfg(feature = "parallel")]
276 fn build_hash_table_parallel(&mut self) {
277 let block_size = self.block_size;
278
279 let hash_entries: Vec<(u64, usize)> = self
280 .patterns
281 .par_iter()
282 .enumerate()
283 .filter_map(|(pattern_idx, pattern)| {
284 let chars: Vec<char> = pattern.chars().collect();
285 let char_count = chars.len();
286
287 if char_count >= block_size {
288 let start_pos = char_count - block_size;
289 let block = chars[start_pos..start_pos + block_size].iter().collect::<String>();
290 let hash = Self::calculate_hash_fast(&block);
291 Some((hash, pattern_idx))
292 } else {
293 None
294 }
295 })
296 .collect();
297
298 for (hash, pattern_idx) in hash_entries {
299 self.hash_table.entry(hash).or_insert_with(SmallVec::new).push(pattern_idx);
300 }
301 }
302
303 #[inline]
305 fn extract_block_optimized(&self, chars: &[char], start: usize) -> String {
306 chars[start..start + self.block_size].iter().collect()
307 }
308
309 #[inline]
311 fn calculate_hash_fast(s: &str) -> u64 {
312 let mut hasher = DefaultHasher::new();
313 s.hash(&mut hasher);
314 hasher.finish()
315 }
316
317 pub fn search(&self, text: &str) -> Option<Arc<String>> {
319 if self.patterns.is_empty() || text.is_empty() {
320 return None;
321 }
322
323 if self.space_handling != SpaceHandling::Strict {
324 return self.search_with_preprocessing(text);
325 }
326
327 let chars: Vec<char> = text.chars().collect();
328 let text_len = chars.len();
329
330 if text_len < self.min_len {
331 return None;
332 }
333
334 let mut pos = self.min_len - 1;
335
336 while pos < text_len {
337 if pos + 1 < self.block_size {
338 pos += 1;
339 continue;
340 }
341
342 let block_start = pos + 1 - self.block_size;
343 let block = self.extract_block_optimized(&chars, block_start);
344 let hash = Self::calculate_hash_fast(&block);
345
346 if let Some(&shift) = self.shift_table.get(&hash) {
347 if shift == 0 {
348 if let Some(pattern_indices) = self.hash_table.get(&hash)
349 && let Some(result) = self.verify_matches_arc(&chars, pos, pattern_indices)
350 {
351 return Some(result);
352 }
353
354 let prev_block_start = block_start.saturating_sub(1);
355 if prev_block_start < block_start {
356 let prev_block = self.extract_block_optimized(&chars, prev_block_start);
357 let prev_hash = Self::calculate_hash_fast(&prev_block);
358 if let Some(found) = self.hash_table.get(&prev_hash).and_then(|prev_pattern_indices| {
359 self.verify_matches_arc(&chars, pos - 1, prev_pattern_indices)
360 }) {
361 return Some(found);
362 }
363 }
364
365 pos += 1;
366 } else {
367 pos += shift;
368 }
369 } else {
370 pos += self.min_len.saturating_sub(self.block_size).saturating_add(1);
372 }
373 }
374
375 None
376 }
377
378 fn search_with_preprocessing(&self, text: &str) -> Option<Arc<String>> {
380 for (i, original_pattern) in self.original_patterns.iter().enumerate() {
381 let processed_text = self.preprocess_text(text);
382 let processed_pattern = Self::preprocess_pattern(original_pattern, self.space_handling);
383
384 if processed_text.contains(&processed_pattern) {
385 return self.patterns.get(i).cloned();
386 }
387 }
388 None
389 }
390
391 fn verify_matches_arc(&self, chars: &[char], pos: usize, pattern_indices: &[usize]) -> Option<Arc<String>> {
393 for &pattern_idx in pattern_indices {
394 if let Some(pattern) = self.patterns.get(pattern_idx) {
395 let pattern_chars: Vec<char> = pattern.chars().collect();
396 let pattern_len = pattern_chars.len();
397
398 if pos + 1 >= pattern_len {
399 let start = pos + 1 - pattern_len;
400 if chars[start..start + pattern_len] == pattern_chars[..] {
401 return Some(pattern.clone());
402 }
403 }
404 }
405 }
406 None
407 }
408
409 pub fn search_string(&self, text: &str) -> Option<String> {
411 if self.space_handling != SpaceHandling::Strict {
412 for original_pattern in &self.original_patterns {
414 let processed_text = self.preprocess_text(text);
415 let processed_pattern = Self::preprocess_pattern(original_pattern, self.space_handling);
416 if processed_text.contains(&processed_pattern) {
417 return Some(original_pattern.clone());
418 }
419 }
420 return None;
421 }
422
423 self.search(text).map(|arc| (*arc).clone())
424 }
425
426 pub fn search_all(&self, text: &str) -> Vec<Arc<String>> {
428 if self.patterns.is_empty() || text.is_empty() {
429 return Vec::new();
430 }
431 if self.space_handling != SpaceHandling::Strict {
432 return self.search_all_with_preprocessing(text);
433 }
434
435 let chars: Vec<char> = text.chars().collect();
436 let mut results = Vec::new();
437 let mut found_indices = HashSet::new();
438 self.scan_core(&chars, |pattern_idx, _start, _len| {
439 if found_indices.insert(pattern_idx) {
440 if let Some(pattern) = self.patterns.get(pattern_idx) {
441 results.push(pattern.clone());
442 }
443 }
444 });
445 results
446 }
447
448 fn scan_core(&self, chars: &[char], mut on_match: impl FnMut(usize, usize, usize)) {
455 let text_len = chars.len();
456 if text_len < self.min_len {
457 return;
458 }
459 let mut pos = self.min_len - 1;
460 while pos < text_len {
461 if pos + 1 < self.block_size {
462 pos += 1;
463 continue;
464 }
465
466 let block_start = pos + 1 - self.block_size;
467 let block = self.extract_block_optimized(chars, block_start);
468 let hash = Self::calculate_hash_fast(&block);
469
470 if let Some(&shift) = self.shift_table.get(&hash) {
471 if shift == 0 {
472 if let Some(pattern_indices) = self.hash_table.get(&hash) {
473 self.verify_and_emit(chars, pos, pattern_indices, &mut on_match);
474 }
475 let prev_block_start = block_start.saturating_sub(1);
477 if prev_block_start < block_start {
478 let prev_block = self.extract_block_optimized(chars, prev_block_start);
479 let prev_hash = Self::calculate_hash_fast(&prev_block);
480 if let Some(prev_pattern_indices) = self.hash_table.get(&prev_hash) {
481 self.verify_and_emit(chars, pos - 1, prev_pattern_indices, &mut on_match);
482 }
483 }
484 pos += 1;
485 } else {
486 pos += shift;
487 }
488 } else {
489 pos += self.min_len.saturating_sub(self.block_size).saturating_add(1);
490 }
491 }
492 }
493
494 fn verify_and_emit(
498 &self,
499 chars: &[char],
500 pos: usize,
501 pattern_indices: &[usize],
502 on_match: &mut impl FnMut(usize, usize, usize),
503 ) {
504 for &pattern_idx in pattern_indices {
505 if let Some(pattern) = self.patterns.get(pattern_idx) {
506 let pattern_chars: Vec<char> = pattern.chars().collect();
507 let pattern_len = pattern_chars.len();
508 if pos + 1 >= pattern_len {
509 let start = pos + 1 - pattern_len;
510 if chars[start..start + pattern_len] == pattern_chars[..] {
511 on_match(pattern_idx, start, pattern_len);
512 }
513 }
514 }
515 }
516 }
517
518 fn search_all_with_preprocessing(&self, text: &str) -> Vec<Arc<String>> {
520 let mut results = Vec::new();
521 let mut found_indices = HashSet::new();
522 let processed_text = self.preprocess_text(text);
523
524 for (i, original_pattern) in self.original_patterns.iter().enumerate() {
525 let processed_pattern = Self::preprocess_pattern(original_pattern, self.space_handling);
526
527 if processed_text.contains(&processed_pattern) && !found_indices.contains(&i) {
528 found_indices.insert(i);
529 if i < self.patterns.len() {
530 results.push(self.patterns[i].clone());
531 }
532 }
533 }
534
535 results
536 }
537
538 pub fn search_all_strings(&self, text: &str) -> Vec<String> {
540 self.search_all(text).iter().map(|arc| (**arc).clone()).collect()
541 }
542
543 pub fn replace_all(&self, text: &str, replacement: char) -> String {
547 let repl = replacement.to_string();
548 self.rebuild_matches(text, |start, end| {
549 let char_count = text[start..end].chars().count();
550 repl.repeat(char_count)
551 })
552 }
553
554 pub fn remove_all(&self, text: &str) -> String {
556 self.rebuild_matches(text, |_, _| String::new())
557 }
558
559 fn rebuild_matches(&self, text: &str, make_replacement: impl Fn(usize, usize) -> String) -> String {
564 let mut matches = self.find_matches(text);
565 matches.sort_by(|a, b| a.start.cmp(&b.start).then(b.end.cmp(&a.end)));
567
568 let mut out = String::with_capacity(text.len());
569 let mut cursor = 0usize;
570 for m in matches {
571 if m.start < cursor {
572 continue; }
574 out.push_str(&text[cursor..m.start]);
575 out.push_str(&make_replacement(m.start, m.end));
576 cursor = m.end;
577 }
578 out.push_str(&text[cursor..]);
579 out
580 }
581
582 pub fn find_matches(&self, text: &str) -> Vec<Match> {
590 if self.space_handling != SpaceHandling::Strict || self.patterns.is_empty() || text.is_empty() {
591 return self.find_matches_bruteforce(text);
592 }
593
594 let chars: Vec<char> = text.chars().collect();
595 let mut char_to_byte: Vec<usize> = text.char_indices().map(|(b, _)| b).collect();
598 char_to_byte.push(text.len());
599
600 let mut matches = Vec::new();
601 let mut seen = HashSet::new();
602 self.scan_core(&chars, |_pattern_idx, char_start, char_len| {
603 let char_end = char_start + char_len;
604 if seen.insert((char_start, char_end)) {
607 matches.push(Match { start: char_to_byte[char_start], end: char_to_byte[char_end] });
608 }
609 });
610 matches.sort_by_key(|m| m.start);
611 matches
612 }
613
614 fn find_matches_bruteforce(&self, text: &str) -> Vec<Match> {
616 let mut matches = Vec::new();
617 for pattern in &self.original_patterns {
618 let mut start = 0;
619 while let Some(pos) = text[start..].find(pattern) {
620 let absolute_start = start + pos;
621 let absolute_end = absolute_start + pattern.len();
622 matches.push(Match { start: absolute_start, end: absolute_end });
623 start = absolute_start + text[absolute_start..].chars().next().map_or(1, char::len_utf8);
624 }
625 }
626 matches.sort_by_key(|m| m.start);
627 matches
628 }
629
630 pub fn patterns(&self) -> &[Arc<String>] {
632 &self.patterns
633 }
634
635 pub fn patterns_strings(&self) -> Vec<String> {
637 self.original_patterns.clone()
638 }
639
640 pub fn contains_pattern(&self, pattern: &str) -> bool {
642 let target = Arc::new(pattern.to_string());
643 self.pattern_set.contains(&target)
644 }
645
646 pub fn space_handling(&self) -> SpaceHandling {
648 self.space_handling
649 }
650
651 pub fn memory_stats(&self) -> WuManberMemoryStats {
653 let patterns_memory = self.patterns.iter().map(|p| size_of::<Arc<String>>() + p.len()).sum::<usize>();
654
655 let shift_table_memory = self.shift_table.len() * (size_of::<u64>() + size_of::<usize>());
656 let hash_table_memory = self
657 .hash_table
658 .iter()
659 .map(|(_k, v)| size_of::<u64>() + size_of::<SmallVec<[usize; 4]>>() + v.len() * size_of::<usize>())
660 .sum::<usize>();
661
662 let pattern_set_memory = self.pattern_set.len() * size_of::<Arc<String>>();
663
664 let total_memory = patterns_memory + shift_table_memory + hash_table_memory + pattern_set_memory;
665
666 WuManberMemoryStats {
667 total_patterns: self.patterns.len(),
668 patterns_memory,
669 shift_table_memory,
670 hash_table_memory,
671 total_memory,
672 }
673 }
674
675 pub fn stats(&self) -> WuManberStats {
677 WuManberStats {
678 pattern_count: self.patterns.len(),
679 min_length: self.min_len,
680 block_size: self.block_size,
681 shift_table_size: self.shift_table.len(),
682 hash_table_size: self.hash_table.len(),
683 }
684 }
685}
686
687#[derive(Debug, Clone, Copy, PartialEq, Eq)]
689pub struct Match {
690 pub start: usize,
691 pub end: usize,
692}
693
694#[derive(Debug, Clone)]
696pub struct WuManberStats {
697 pub pattern_count: usize,
698 pub min_length: usize,
699 pub block_size: usize,
700 pub shift_table_size: usize,
701 pub hash_table_size: usize,
702}
703
704#[derive(Debug, Clone)]
706pub struct WuManberMemoryStats {
707 pub total_patterns: usize,
708 pub patterns_memory: usize,
709 pub shift_table_memory: usize,
710 pub hash_table_memory: usize,
711 pub total_memory: usize,
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717
718 #[test]
719 fn test_basic_matching() {
720 let wm =
721 WuManber::new_chinese(vec!["色情".to_string(), "赌博".to_string(), "诈骗".to_string(), "扯蛋".to_string()]);
722
723 assert_eq!(wm.search_string("正常内容"), None);
724 assert_eq!(wm.search_string("测试含有赌博内容"), Some("赌博".to_string()));
725 assert_eq!(wm.search_string("测试还有色情赌博图片"), Some("色情".to_string()));
726 assert_eq!(wm.search_string("测试有色情赌博图片"), Some("色情".to_string()));
727 assert_eq!(wm.search_string("还有诈骗色情测试"), Some("诈骗".to_string()));
728 }
729
730 #[test]
731 fn test_varied_length() {
732 let wm = WuManber::new(vec!["赌".to_string(), "赌博".to_string(), "赌博机".to_string()], 1);
733
734 assert_eq!(wm.search_string("赌"), Some("赌".to_string()));
735 assert_eq!(wm.search_string("赌博"), Some("赌".to_string())); assert_eq!(wm.search_string("赌博机"), Some("赌".to_string())); }
738
739 #[test]
740 fn test_space_handling_strategies() {
741 let patterns = vec!["hello world".to_string(), "test pattern".to_string()];
742
743 let wm_strict = WuManber::new_with_space_handling(patterns.clone(), SpaceHandling::Strict);
745 assert_eq!(wm_strict.search_string("hello world"), Some("hello world".to_string()));
746 assert_eq!(wm_strict.search_string("helloworld"), None);
747
748 let wm_ignore = WuManber::new_with_space_handling(patterns.clone(), SpaceHandling::IgnoreSpaces);
750 assert_eq!(wm_ignore.search_string("hello world"), Some("hello world".to_string()));
751 assert_eq!(wm_ignore.search_string("helloworld"), Some("hello world".to_string()));
752
753 let wm_normalize = WuManber::new_with_space_handling(patterns.clone(), SpaceHandling::NormalizeSpaces);
755 assert_eq!(wm_normalize.search_string("test pattern"), Some("test pattern".to_string()));
756 assert_eq!(wm_normalize.search_string("test pattern"), Some("test pattern".to_string()));
757 }
758
759 #[test]
760 fn test_mixed_patterns() {
761 let mixed_patterns =
762 vec!["关键词 2500".to_string(), "关键词 3000".to_string(), "test".to_string(), "hello world".to_string()];
763
764 let wm = WuManber::new_chinese(mixed_patterns);
765
766 assert_eq!(wm.search_string("包含关键词 2500 的文本"), Some("关键词 2500".to_string()));
767 assert_eq!(wm.search_string("包含关键词 3000 的文本"), Some("关键词 3000".to_string()));
768 assert_eq!(wm.search_string("this is test"), Some("test".to_string()));
769 assert_eq!(wm.search_string("say hello world"), Some("hello world".to_string()));
770 }
771
772 #[test]
773 fn test_complex_patterns_with_spaces() {
774 let complex_patterns = vec![
775 "Hello World".to_string(),
776 "你好 世界".to_string(),
777 "multiple spaces".to_string(),
778 "tab\there".to_string(),
779 "new\nline".to_string(),
780 ];
781
782 let wm = WuManber::new_chinese(complex_patterns);
783
784 assert_eq!(wm.search_string("Say Hello World"), Some("Hello World".to_string()));
785 assert_eq!(wm.search_string("说你好 世界"), Some("你好 世界".to_string()));
786 assert_eq!(wm.search_string("has multiple spaces here"), Some("multiple spaces".to_string()));
787 assert_eq!(wm.search_string("tab\there you go"), Some("tab\there".to_string()));
788 assert_eq!(wm.search_string("new\nline break"), Some("new\nline".to_string()));
789 }
790
791 #[test]
792 fn test_ignore_spaces_functionality() {
793 let patterns = vec!["关键词 2500".to_string(), "hello world".to_string()];
794 let wm = WuManber::new_with_space_handling(patterns, SpaceHandling::IgnoreSpaces);
795
796 assert_eq!(wm.search_string("关键词 2500"), Some("关键词 2500".to_string()));
798 assert_eq!(wm.search_string("关键词 2500"), Some("关键词 2500".to_string()));
799 assert_eq!(wm.search_string("关键词 2500"), Some("关键词 2500".to_string()));
800 assert_eq!(wm.search_string("helloworld"), Some("hello world".to_string()));
801 assert_eq!(wm.search_string("hello world"), Some("hello world".to_string()));
802 }
803
804 #[test]
805 fn test_parallel_performance() {
806 let patterns: Vec<String> = (0..5000).map(|i| format!("关键词 {i}")).collect();
807
808 let wm_seq = WuManber::new(patterns.clone(), 2);
809 let wm_par = WuManber::new_parallel(patterns, 2);
810
811 let text = "这里包含关键词 2500 和关键词 3000";
812
813 let result_seq = wm_seq.search_all_strings(text);
814 let result_par = wm_par.search_all_strings(text);
815
816 assert_eq!(result_seq.len(), result_par.len());
817 assert!(result_seq.contains(&"关键词 2500".to_string()));
818 assert!(result_seq.contains(&"关键词 3000".to_string()));
819
820 for item in &result_seq {
821 assert!(result_par.contains(item));
822 }
823 }
824
825 #[test]
826 fn test_search_all_functionality() {
827 let wm = WuManber::new_chinese(vec!["苹果".to_string(), "香蕉".to_string(), "橙子".to_string()]);
828
829 let text = "我喜欢吃苹果、香蕉和橙子";
830 let results = wm.search_all_strings(text);
831
832 assert_eq!(results.len(), 3);
833 assert!(results.contains(&"苹果".to_string()));
834 assert!(results.contains(&"香蕉".to_string()));
835 assert!(results.contains(&"橙子".to_string()));
836 }
837
838 #[test]
839 fn test_find_matches_multibyte_positions() {
840 let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
843
844 let text = "含有赌博内容";
845 let matches = wm.find_matches(text);
846 assert_eq!(matches.len(), 1);
847 assert_eq!(matches[0].start, 6); assert_eq!(matches[0].end, 12); assert_eq!(&text[matches[0].start..matches[0].end], "赌博");
850 }
851
852 #[test]
853 fn test_find_matches_multiple_multibyte_occurrences() {
854 let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
856 let text = "赌博和赌博";
857 let matches = wm.find_matches(text);
858 assert_eq!(matches.len(), 2);
859 for m in &matches {
860 assert_eq!(&text[m.start..m.end], "赌博");
861 }
862 }
863
864 #[test]
865 fn test_find_matches_mixed_ascii_and_multibyte() {
866 let wm = WuManber::new_chinese(vec!["ab".to_string(), "赌博".to_string()]);
868 let text = "x 赌博 y 赌博 z";
869 let matches = wm.find_matches(text);
870 assert_eq!(matches.len(), 2);
871 assert!(matches.iter().all(|m| &text[m.start..m.end] == "赌博"));
872 }
873
874 #[test]
875 fn test_find_matches_overlapping_patterns() {
876 let wm = WuManber::new_chinese(vec!["赌博".to_string(), "赌博机".to_string()]);
880 let text = "赌博机";
881 let matches = wm.find_matches(text);
882 assert_eq!(matches.len(), 2, "got {matches:?}");
883 assert!(matches.iter().any(|m| &text[m.start..m.end] == "赌博"));
884 assert!(matches.iter().any(|m| &text[m.start..m.end] == "赌博机"));
885 let mut spans: Vec<_> = matches.iter().map(|m| (m.start, m.end)).collect();
887 spans.sort();
888 let before = spans.len();
889 spans.dedup();
890 assert_eq!(spans.len(), before);
891 }
892
893 #[test]
894 fn test_find_matches_pattern_at_text_end() {
895 let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
898 let text = "前缀赌博";
899 let matches = wm.find_matches(text);
900 assert_eq!(matches.len(), 1);
901 assert_eq!(&text[matches[0].start..matches[0].end], "赌博");
902 assert_eq!(matches[0].end, text.len());
903 }
904
905 #[test]
906 fn test_replace_all_overlapping_leftmost_longest() {
907 let wm = WuManber::new_chinese(vec!["赌".to_string(), "赌博".to_string(), "赌博机".to_string()]);
909 assert_eq!(wm.replace_all("赌博机", '*'), "***");
910 assert_eq!(wm.remove_all("赌博机"), "");
911 }
912
913 #[test]
914 fn test_replace_all_multiple_distinct() {
915 let wm = WuManber::new_chinese(vec!["赌博".to_string(), "色情".to_string()]);
916 assert_eq!(wm.replace_all("赌博和色情", '*'), "**和**");
918 }
919
920 #[test]
921 fn test_replace_all_no_match() {
922 let wm = WuManber::new_chinese(vec!["赌博".to_string()]);
923 assert_eq!(wm.replace_all("正常内容", '*'), "正常内容");
924 assert_eq!(wm.remove_all("正常内容"), "正常内容");
925 }
926}