pub fn starts_with_ascii_case_insensitive(text: &str, prefix: &str) -> bool {
prefix.is_ascii()
&& text.len() >= prefix.len()
&& text.as_bytes()[..prefix.len()]
.iter()
.zip(prefix.as_bytes())
.all(|(left, right)| left.eq_ignore_ascii_case(right))
}
pub fn strip_prefix_ascii_case_insensitive<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
starts_with_ascii_case_insensitive(text, prefix).then(|| &text[prefix.len()..])
}
pub fn find_ascii_case_insensitive(text: &str, needle: &str) -> Option<usize> {
find_ascii_case_insensitive_from(text, needle, 0)
}
pub fn rfind_ascii_case_insensitive(text: &str, needle: &str) -> Option<usize> {
if needle.is_empty() {
return Some(text.len());
}
if !needle.is_ascii() || needle.len() > text.len() {
return None;
}
let haystack = text.as_bytes();
let needle = needle.as_bytes();
(0..=haystack.len() - needle.len()).rev().find(|&position| {
haystack[position..position + needle.len()]
.iter()
.zip(needle)
.all(|(left, right)| left.eq_ignore_ascii_case(right))
})
}
pub fn find_ascii_case_insensitive_from(text: &str, needle: &str, start: usize) -> Option<usize> {
if needle.is_empty() {
return (start <= text.len()).then_some(start);
}
if !needle.is_ascii() || start > text.len() || needle.len() > text.len() - start {
return None;
}
let haystack = text.as_bytes();
let needle = needle.as_bytes();
(start..=haystack.len() - needle.len()).find(|&position| {
haystack[position..position + needle.len()]
.iter()
.zip(needle)
.all(|(left, right)| left.eq_ignore_ascii_case(right))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_ascii_without_mutating_unicode_input() {
let text = "ßSELECT ff from İstanbul";
let select = find_ascii_case_insensitive(text, "select").expect("ASCII match");
let from = rfind_ascii_case_insensitive(text, "FROM").expect("ASCII match");
assert_eq!(select, 2);
assert_eq!(from, 13);
assert_eq!(&text[select..select + 6], "SELECT");
assert_eq!(&text[from..from + 4], "from");
assert_eq!(find_ascii_case_insensitive(text, "istanbul"), None);
assert_eq!(find_ascii_case_insensitive(text, "ssselect"), None);
assert_eq!(find_ascii_case_insensitive(text, "ff"), None);
}
#[test]
fn rejects_non_ascii_needles() {
let text = "SELECT Straße";
assert!(!starts_with_ascii_case_insensitive(text, "ŚELECT"));
assert_eq!(strip_prefix_ascii_case_insensitive(text, "ŚELECT"), None);
assert_eq!(find_ascii_case_insensitive(text, "ß"), None);
assert_eq!(find_ascii_case_insensitive_from(text, "İ", 0), None);
assert_eq!(rfind_ascii_case_insensitive(text, "ff"), None);
}
#[test]
fn prefix_helpers_return_original_suffix() {
let text = "SeLeCt ß";
assert!(starts_with_ascii_case_insensitive(text, "select"));
assert_eq!(
strip_prefix_ascii_case_insensitive(text, "SELECT"),
Some(" ß")
);
assert_eq!(strip_prefix_ascii_case_insensitive(text, ""), Some(text));
assert!(starts_with_ascii_case_insensitive(text, ""));
assert!(!starts_with_ascii_case_insensitive(text, "select x"));
}
#[test]
fn empty_and_start_bounds_are_conventional() {
let text = "éX";
assert_eq!(find_ascii_case_insensitive(text, ""), Some(0));
assert_eq!(rfind_ascii_case_insensitive(text, ""), Some(text.len()));
assert_eq!(find_ascii_case_insensitive_from(text, "", 1), Some(1));
assert_eq!(
find_ascii_case_insensitive_from(text, "", text.len()),
Some(text.len())
);
assert_eq!(
find_ascii_case_insensitive_from(text, "", text.len() + 1),
None
);
assert_eq!(find_ascii_case_insensitive_from(text, "x", 1), Some(2));
assert_eq!(
find_ascii_case_insensitive_from(text, "x", text.len()),
None
);
}
}