string-patterns 0.4.0

Makes it easier to work with common string patterns and regular expressions in Rust, adding convenient regex match and replace methods (pattern_match and pattern_replace) to the standard String type as well to vectors of strings
Documentation
//! Demonstrates the actual payoff of routing PatternMatch through the shared regex cache:
//! applying the *same* pattern across many subjects (e.g. one validator applied to every row
//! of a batch) used to recompile the regex on every call to PatternMatch::pattern_match_ci;
//! now it compiles once and reuses the cached Regex. The "old" behaviour is reimplemented
//! inline below (a fresh regex::Regex::new per call) so this bench doesn't need an older
//! published version of this same crate to compare against.

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use string_patterns::PatternMatch;

mod old {
    pub fn pattern_match_ci(subject: &str, pattern: &str) -> bool {
        match regex::Regex::new(&format!("(?i){pattern}")) {
            Ok(re) => re.is_match(subject),
            Err(_) => false,
        }
    }
}

// Deterministic xorshift PRNG so benchmark inputs are reproducible without a `rand` dependency.
struct Xorshift(u64);
impl Xorshift {
    fn next_u64(&mut self) -> u64 {
        let mut x = self.0;
        x ^= x << 13;
        x ^= x >> 7;
        x ^= x << 17;
        self.0 = x;
        x
    }
    fn range(&mut self, n: usize) -> usize {
        (self.next_u64() % n as u64) as usize
    }
}

const NOISE_WORDS: &[&str] = &[
    "connection", "established", "user", "request", "processed", "timeout", "retry",
    "system", "memory", "usage", "cpu", "load", "average", "shutdown", "started",
    "completed", "elapsed", "bytes", "sent", "received", "session", "token", "cache",
];

fn rand_words(rng: &mut Xorshift, n: usize) -> String {
    (0..n)
        .map(|_| NOISE_WORDS[rng.range(NOISE_WORDS.len())])
        .collect::<Vec<_>>()
        .join(" ")
}

/// Roughly 1 in 5 rows actually matches "float...error", mirroring a validator that most
/// rows pass and a minority fail (or vice versa) in a real batch job.
fn gen_row(rng: &mut Xorshift) -> String {
    if rng.range(5) == 0 {
        format!("floating point error: {}", rand_words(rng, 4))
    } else {
        rand_words(rng, 6)
    }
}

fn synthetic_rows(n: usize) -> Vec<String> {
    let mut rng = Xorshift(0x9E3779B97F4A7C15);
    (0..n).map(|_| gen_row(&mut rng)).collect()
}

const PATTERN: &str = r"\bfloat(ing)?.*?error";

fn bench_same_pattern_across_many_rows(c: &mut Criterion) {
    let rows = synthetic_rows(2_000);
    let mut group = c.benchmark_group("same_pattern_across_many_rows");
    group.sample_size(50);
    group.bench_function("old_uncached", |b| {
        b.iter(|| {
            rows.iter()
                .filter(|row| old::pattern_match_ci(black_box(row.as_str()), black_box(PATTERN)))
                .count()
        })
    });
    group.bench_function("pattern_match_ci_now_cached", |b| {
        b.iter(|| {
            rows.iter()
                .filter(|row| black_box(row.as_str()).pattern_match_ci(black_box(PATTERN)))
                .count()
        })
    });
    group.finish();
}

criterion_group!(benches, bench_same_pattern_across_many_rows);
criterion_main!(benches);