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