use std::collections::HashMap;
use matcher_rs::{ProcessType, SimpleMatcher, SimpleMatcherBuilder};
#[test]
fn test_and_requires_all_segments() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "hello&world")
.add_word(ProcessType::None, 2, "hello&world&hello")
.add_word(ProcessType::None, 3, "a&a&b")
.add_word(ProcessType::None, 4, "abc&bc&c")
.build()
.unwrap();
assert!(matcher.is_match("hello world"));
assert!(!matcher.is_match("hello"));
assert!(matcher.is_match("hello hello world"));
{
let m2 = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 2, "hello&world&hello")
.build()
.unwrap();
assert!(!m2.is_match("hello world"));
assert!(m2.is_match("hello hello world"));
}
assert!(matcher.is_match("a a b"));
assert!(!matcher.is_match("a b"));
assert!(matcher.is_match("abc"));
let pattern_10 = (0..10).map(|_| "a").collect::<Vec<_>>().join("&");
let m10 = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, &pattern_10)
.build()
.unwrap();
assert!(m10.is_match(&"a ".repeat(10)));
assert!(!m10.is_match(&"a ".repeat(9)));
}
#[test]
fn test_not_vetoes_match() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "hello~world")
.add_word(ProcessType::None, 2, "apple~pp")
.build()
.unwrap();
assert_eq!(matcher.process("hello").len(), 1);
assert_eq!(matcher.process("world hello").len(), 0);
assert!(!matcher.is_match("hello hello world"));
assert!(!matcher.is_match("apple"));
}
#[test]
fn test_not_veto_global_across_variants() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None | ProcessType::Delete, 1, "apple~pie")
.build()
.unwrap();
assert!(
!matcher.is_match("apple p.i.e"),
"Delete-transformed text contains 'pie' → NOT veto"
);
}
#[test]
fn test_pure_not_rules_dropped() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "hello")
.add_word(ProcessType::None, 2, "~bad")
.build()
.unwrap();
assert!(matcher.is_match("hello world"));
assert!(!matcher.is_match("good text"));
assert!(!matcher.is_match("bad text"));
assert_eq!(matcher.process("hello").len(), 1);
}
#[test]
fn test_and_not_combined() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "x&y&z~a~b")
.add_word(ProcessType::None, 2, "a&b~c&d")
.build()
.unwrap();
for text in ["x y z", "x z y", "y x z", "y z x", "z x y", "z y x"] {
assert!(matcher.is_match(text), "should match permutation '{text}'");
}
assert!(!matcher.is_match("x y z a"), "NOT 'a' vetoes");
assert!(!matcher.is_match("x y z b"), "NOT 'b' vetoes");
assert!(matcher.is_match("a b d"));
assert!(!matcher.is_match("a b c d"), "NOT 'c' vetoes");
}
#[test]
fn test_or_alternatives() {
let m1 = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "color|colour")
.add_word(ProcessType::None, 2, "a|b|c|d")
.build()
.unwrap();
assert!(m1.is_match("color"));
assert!(m1.is_match("colour"));
assert!(!m1.is_match("xyz"), "no alternative present");
for ch in ["a", "b", "c", "d"] {
assert!(m1.is_match(ch));
}
assert!(!m1.is_match("e"));
let m2 = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "a|") .add_word(ProcessType::None, 2, "|b") .add_word(ProcessType::None, 3, "c||d") .build()
.unwrap();
assert!(m2.is_match("a"));
assert!(m2.is_match("b"));
assert!(m2.is_match("c"));
assert!(m2.is_match("d"));
let m3 = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "|")
.add_word(ProcessType::None, 2, "||")
.add_word(ProcessType::None, 3, "hello")
.build()
.unwrap();
assert!(m3.is_match("hello"));
assert!(!m3.is_match("anything else"));
let m4 = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "a|a")
.build()
.unwrap();
assert!(m4.is_match("a"));
assert!(!m4.is_match("b"));
}
#[test]
fn test_or_with_and_and_not() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "color|colour&bright")
.add_word(ProcessType::None, 2, "hello~bad|evil")
.add_word(ProcessType::None, 3, "a|b&c|d~e|f")
.build()
.unwrap();
assert!(matcher.is_match("bright color"));
assert!(matcher.is_match("bright colour"));
assert!(!matcher.is_match("bright"));
assert!(!matcher.is_match("color"));
assert!(matcher.is_match("hello"));
assert!(!matcher.is_match("hello bad"));
assert!(!matcher.is_match("hello evil"));
assert!(matcher.is_match("a c"));
assert!(matcher.is_match("b d"));
assert!(!matcher.is_match("a"));
assert!(!matcher.is_match("a c e"), "NOT e vetoes");
assert!(!matcher.is_match("b d f"), "NOT f vetoes");
}
#[test]
fn test_or_result_preserves_original_word() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 42, "color|colour")
.build()
.unwrap();
let results = matcher.process("colour is nice");
assert_eq!(results.len(), 1);
assert_eq!(results[0].word_id, 42);
assert_eq!(results[0].word.as_ref(), "color|colour");
}
#[test]
fn test_or_shared_across_rules() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "cat|dog")
.add_word(ProcessType::None, 2, "dog|bird")
.build()
.unwrap();
assert_eq!(matcher.process("dog").len(), 2, "dog matches both rules");
let r1 = matcher.process("cat");
assert_eq!(r1.len(), 1);
assert_eq!(r1[0].word_id, 1);
let r2 = matcher.process("bird");
assert_eq!(r2.len(), 1);
assert_eq!(r2[0].word_id, 2);
}
#[test]
fn test_boundary_enforcement() {
let both = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, r"\bcat\b")
.build()
.unwrap();
let cases_both: &[(&str, bool)] = &[
("the cat sat", true),
("cat", true),
("cat ", true),
(" cat", true),
("cat!", true),
(",cat,", true),
("(cat)", true),
("cat.dog", true),
("concatenate", false),
("scat", false),
("cats", false),
("_cat_", false), ("my_cat_name", false),
];
for &(text, expected) in cases_both {
assert_eq!(both.is_match(text), expected, r"\bcat\b on {text:?}");
}
let left = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, r"\bcat")
.build()
.unwrap();
assert!(left.is_match("cat"));
assert!(left.is_match("cats"));
assert!(left.is_match("catch"));
assert!(!left.is_match("scat"));
assert!(!left.is_match("concatenate"));
let right = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, r"cat\b")
.build()
.unwrap();
assert!(right.is_match("cat"));
assert!(right.is_match("scat"));
assert!(!right.is_match("cats"));
assert!(!right.is_match("catch"));
}
#[test]
fn test_boundary_with_operators() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, r"\bcat\b&\bdog\b")
.add_word(ProcessType::None, 2, r"\bcat\b~\bcatch\b")
.add_word(ProcessType::None, 3, r"\bcolor\b|\bcolour\b")
.build()
.unwrap();
assert!(matcher.is_match("cat and dog"));
assert!(!matcher.is_match("cats and dogs"));
assert!(matcher.is_match("the cat"));
assert!(matcher.is_match("the cat catches fish")); assert!(!matcher.is_match("the cat catch"));
assert!(matcher.is_match("nice color"));
assert!(matcher.is_match("nice colour"));
assert!(!matcher.is_match("colorful"));
}
#[test]
fn test_boundary_mixed_with_non_boundary() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, r"\bcat\b")
.add_word(ProcessType::None, 2, "cat")
.build()
.unwrap();
let r1 = matcher.process("concatenate");
assert_eq!(r1.len(), 1);
assert_eq!(r1[0].word_id, 2);
assert_eq!(matcher.process("the cat").len(), 2);
}
#[test]
fn test_boundary_with_normalize() {
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::Normalize, 1, r"\bcat\b")
.build()
.unwrap();
assert!(matcher.is_match("the CAT sat"));
assert!(!matcher.is_match("CONCATENATE"));
}
#[test]
fn test_edge_case_patterns() {
let result = SimpleMatcher::new(&HashMap::from([(
ProcessType::None,
HashMap::from([(1, "&"), (2, "~"), (3, "&&"), (4, "~~"), (5, "&~&~")]),
)]));
assert!(result.is_err(), "operator-only patterns should be rejected");
let matcher = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "hello&") .add_word(ProcessType::None, 2, "hello~") .add_word(ProcessType::None, 3, "&world") .add_word(ProcessType::None, 4, "~world") .build()
.unwrap();
assert!(matcher.is_match("hello"));
assert!(matcher.is_match("world"));
let m_nul = SimpleMatcherBuilder::new()
.add_word(ProcessType::None, 1, "hello\0world")
.build()
.unwrap();
assert!(m_nul.is_match("hello\0world"));
assert!(!m_nul.is_match("hello world"));
assert!(!m_nul.is_match("helloworld"));
}