resharp 0.7.5

high-performance regex engine with intersection and complement operations
Documentation
use resharp::{Error, RegexSet};

fn cat(pats: &[&str], input: &str) -> Vec<(usize, usize, usize)> {
    let set = RegexSet::new(pats).unwrap();
    set.categorize_all(input.as_bytes())
        .unwrap()
        .iter()
        .map(|m| (m.start, m.end, m.tag))
        .collect()
}

#[test]
fn set_basic_categorize() {
    assert_eq!(
        cat(&["ab+", "a[a-z]*", "c", "[0-9]+", "[0-9]{2}"], "xx abbb c 12 zz 123 abz"),
        vec![(3, 7, 0), (8, 9, 2), (10, 12, 3), (16, 19, 3), (20, 23, 1)]
    );
}

#[test]
fn set_spans_equal_union_find_all() {
    let pats = ["ab+", "a[a-z]*", "c", "[0-9]+"];
    let set = RegexSet::new(pats).unwrap();
    let input = b"xx abbb c 12 zz 123 abz";
    let spans: Vec<_> = set.find_all(input).unwrap().iter().map(|m| (m.start, m.end)).collect();
    let cats: Vec<_> = set.categorize_all(input).unwrap().iter().map(|m| (m.start, m.end)).collect();
    assert_eq!(spans, cats);
    let union = resharp::Regex::new("ab+|a[a-z]*|c|[0-9]+").unwrap();
    let u: Vec<_> = union.find_all(input).unwrap().iter().map(|m| (m.start, m.end)).collect();
    assert_eq!(spans, u);
}

#[test]
fn set_tie_reports_lowest_index() {
    assert_eq!(cat(&["[a-z]+", "abc"], "abc"), vec![(0, 3, 0)]);
    assert_eq!(cat(&["abc", "[a-z]+"], "abc"), vec![(0, 3, 0)]);
    assert_eq!(cat(&["abc", "[a-z]+"], "abcd"), vec![(0, 4, 1)]);
}

#[test]
fn set_trailing_word_boundary() {
    assert_eq!(
        cat(&["[a-z]+\\b", "[0-9]+"], "foo bar1 7"),
        vec![(0, 3, 0), (7, 8, 1), (9, 10, 1)]
    );
    assert_eq!(cat(&["c\\b", "c"], "cc c"), vec![(0, 1, 1), (1, 2, 0), (3, 4, 0)]);
}

#[test]
fn set_trailing_lookahead_distinguishes_members() {
    assert_eq!(
        cat(&["a(?=x)", "a(?=y)", "a"], "ax ay az a"),
        vec![(0, 1, 0), (3, 4, 1), (6, 7, 2), (9, 10, 2)]
    );
}

#[test]
fn set_anchors() {
    assert_eq!(cat(&["\\Aa", "a$", "a"], "a a a"), vec![(0, 1, 0), (2, 3, 2), (4, 5, 1)]);
    assert_eq!(cat(&["\\Aa", "a$", "a"], "a"), vec![(0, 1, 0)]);
}

#[test]
fn set_empty_input_and_empty_match() {
    assert_eq!(cat(&["x", ""], ""), vec![(0, 0, 1)]);
    assert_eq!(cat(&["x", "y*"], ""), vec![(0, 0, 1)]);
    assert_eq!(cat(&["x"], ""), vec![]);
}

#[test]
fn set_many_members() {
    let pats: Vec<String> = (0..200).map(|i| format!("k{i}\\b")).collect();
    let set = RegexSet::new(&pats).unwrap();
    let m = set.categorize_all(b"k7 k199 k42x k0").unwrap();
    let got: Vec<_> = m.iter().map(|m| (m.start, m.end, m.tag)).collect();
    assert_eq!(got, vec![(0, 2, 7), (3, 7, 199), (13, 15, 0)]);
}

#[test]
fn set_rejects_lookbehind_and_captures() {
    assert!(matches!(RegexSet::new(["\\bc", "c"]), Err(Error::SetMember(0, _))));
    assert!(matches!(RegexSet::new(["c", "(?<=a)c"]), Err(Error::SetMember(1, _))));
    assert!(matches!(RegexSet::new(["(?<g>c)"]), Err(Error::SetMember(0, _))));
    assert!(matches!(RegexSet::new(["^c"]), Err(Error::SetMember(0, _))));
    assert!(RegexSet::new(["c\\b", "c(?=d)", "\\Ac", "c$"]).is_ok());
}

#[test]
fn set_empty_set() {
    let set = RegexSet::new(Vec::<&str>::new()).unwrap();
    assert!(set.is_empty());
    assert_eq!(set.categorize_all(b"abc").unwrap(), vec![]);
    assert_eq!(set.matched(b"abc").unwrap(), vec![]);
    assert_eq!(set.matched(b"").unwrap(), vec![]);
}

fn member_matches_span(
    cache: &mut std::collections::HashMap<String, resharp::Regex>,
    pat: &str,
    input: &[u8],
    s: usize,
    e: usize,
) -> bool {
    let n = input.len() - e;
    let pat = match pat.strip_prefix("\\A") {
        Some(rest) if s == 0 => rest,
        Some(_) => return false,
        None => pat,
    };
    let anchored = format!("\\A_{{{s}}}(?:{pat})_{{{n}}}\\z");
    let re = cache
        .entry(anchored.clone())
        .or_insert_with(|| resharp::Regex::new(&anchored).unwrap_or_else(|e| panic!("{anchored}: {e}")));
    re.is_match(input).unwrap()
}

#[test]
fn set_crosscheck_against_anchored_members() {
    let pool = [
        "a", "ab", "b+", "a[ab]*", "ba", "a(?=b)", "b(?=a)", "b\\b", "a$", "\\Aab", "[ab]{2}",
        "a|bb", "b(?=b)", "(a|b)b", "a\\b", "b*a", "[ab]+\\b", "a(?!b)", "ab(?= )", " a",
    ];
    let inputs = ["ab ba bb a", "aabb ab", "b", "abab a", "ba ab ba", "bbb a b", "a", "ab", ""];
    let mut seed = 0x9E3779B9u64;
    let mut next = || {
        seed ^= seed << 13;
        seed ^= seed >> 7;
        seed ^= seed << 17;
        seed
    };
    let mut cache = std::collections::HashMap::new();
    for _ in 0..150 {
        let k = 1 + (next() % 5) as usize;
        let pats: Vec<&str> = (0..k).map(|_| pool[(next() % pool.len() as u64) as usize]).collect();
        let set = match RegexSet::new(&pats) {
            Ok(s) => s,
            Err(e) => panic!("pats={pats:?}: {e}"),
        };
        for input in inputs {
            let got = set.categorize_all(input.as_bytes()).unwrap();
            for m in got {
                let expect = pats
                    .iter()
                    .position(|p| member_matches_span(&mut cache, p, input.as_bytes(), m.start, m.end))
                    .unwrap_or_else(|| panic!("no member matches {:?} on {input:?} pats={pats:?}", (m.start, m.end)));
                assert_eq!(
                    m.tag, expect,
                    "pats={pats:?} input={input:?} span={:?}",
                    (m.start, m.end)
                );
            }
        }
    }
}

fn matched(pats: &[&str], input: &str) -> Vec<usize> {
    RegexSet::new(pats).unwrap().matched(input.as_bytes()).unwrap()
}

#[test]
fn set_matched_is_independent_of_union_spans() {
    assert_eq!(matched(&["[a-z]+", "abc"], "abc"), vec![0, 1]);
    assert_eq!(matched(&["ab", "b"], "ab"), vec![0, 1]);
    assert_eq!(matched(&["ab", "b", "x"], "ab"), vec![0, 1]);
    assert_eq!(matched(&["x", "y"], "ab"), vec![]);
    assert_eq!(matched(&["a(?=x)", "a(?=y)", "a\\b"], "ax a"), vec![0, 2]);
    assert_eq!(matched(&["\\Aa", "a$", "a"], "b a"), vec![1, 2]);
    assert_eq!(matched(&["x", ""], ""), vec![1]);
    assert_eq!(matched(&["x", "\\A"], "x"), vec![0, 1]);
    assert_eq!(matched(&["x", "\\z"], "y"), vec![1]);
}

#[test]
fn set_matched_crosscheck_against_members() {
    let pool = [
        "a", "ab", "b+", "a[ab]*", "ba", "a(?=b)", "b(?=a)", "b\\b", "a$", "\\Aab", "[ab]{2}",
        "a|bb", "b(?=b)", "a\\b", "b*a", "[ab]+\\b", "a(?!b)", "ab(?= )", " a", "\\z", "",
    ];
    let inputs = ["ab ba bb a", "aabb ab", "b", "abab a", "ba ab ba", "bbb a b", "a", "ab", ""];
    let singles: Vec<resharp::Regex> = pool.iter().map(|p| resharp::Regex::new(p).unwrap()).collect();
    let mut seed = 0x51ED270Bu64;
    let mut next = || {
        seed ^= seed << 13;
        seed ^= seed >> 7;
        seed ^= seed << 17;
        seed
    };
    for _ in 0..150 {
        let k = 1 + (next() % 5) as usize;
        let idx: Vec<usize> = (0..k).map(|_| (next() % pool.len() as u64) as usize).collect();
        let pats: Vec<&str> = idx.iter().map(|&i| pool[i]).collect();
        let set = RegexSet::new(&pats).unwrap();
        for input in inputs {
            let got = set.matched(input.as_bytes()).unwrap();
            let expect: Vec<usize> = idx
                .iter()
                .enumerate()
                .filter(|(_, &i)| singles[i].is_match(input.as_bytes()).unwrap())
                .map(|(j, _)| j)
                .collect();
            assert_eq!(got, expect, "pats={pats:?} input={input:?}");
        }
    }
}