#[derive(Debug, Clone)]
pub(super) struct AsciiInsensitiveNeedle {
lowercased: String,
is_ascii: bool,
}
impl AsciiInsensitiveNeedle {
pub(super) fn new(needle: impl Into<String>) -> Self {
let lowercased = needle.into().to_lowercase();
let is_ascii = lowercased.is_ascii();
Self {
lowercased,
is_ascii,
}
}
pub(super) fn matches(&self, haystack: &str) -> bool {
if self.is_ascii {
let h = haystack.as_bytes();
let n = self.lowercased.as_bytes();
if n.is_empty() {
return true;
}
if h.len() < n.len() {
return false;
}
h.windows(n.len()).any(|w| w.eq_ignore_ascii_case(n))
} else {
haystack.to_lowercase().contains(&self.lowercased)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_legacy_to_lowercase_contains() {
let cases: &[(&str, &str)] = &[
("GROCERY", "Grocery shopping list"),
("grocery", "Grocery shopping list"),
("Grocery", "grocery shopping list"),
("xyz", "Grocery shopping list"),
("", "anything"),
("longer than haystack", "short"),
("a", ""),
("hello", "héllo world"),
("world", "héllo world"),
("CAFÉ", "let's grab café tonight"),
("café", "let's grab CAFÉ tonight"),
("naïve", "a NAÏVE approach"),
("Ω", "math symbols: Ω ω"),
("DEPLOY", "Deploy to production"),
];
for (needle, haystack) in cases {
let reference = haystack.to_lowercase().contains(&needle.to_lowercase());
let actual = AsciiInsensitiveNeedle::new(*needle).matches(haystack);
assert_eq!(
actual, reference,
"AsciiInsensitiveNeedle({needle:?}).matches({haystack:?}) diverged from legacy",
);
}
}
}