pub const DEFAULT_LIKE_ESCAPE: char = '\\';
pub fn sql_like_match(input: &str, pattern: &str, case_insensitive: bool) -> bool {
sql_like_match_escaped(input, pattern, case_insensitive, Some(DEFAULT_LIKE_ESCAPE))
}
pub fn sql_like_match_escaped(
input: &str,
pattern: &str,
case_insensitive: bool,
escape: Option<char>,
) -> bool {
let (input, pattern) = if case_insensitive {
(input.to_lowercase(), pattern.to_lowercase())
} else {
(input.to_owned(), pattern.to_owned())
};
let input: Vec<char> = input.chars().collect();
let tokens = tokenize(&pattern, escape);
matches(&input, &tokens)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Token {
Char(char),
AnyOne,
AnyRun,
}
fn tokenize(pattern: &str, escape: Option<char>) -> Vec<Token> {
let mut tokens = Vec::with_capacity(pattern.len());
let mut chars = pattern.chars();
while let Some(c) = chars.next() {
let token = if Some(c) == escape {
Token::Char(chars.next().unwrap_or(c))
} else {
match c {
'%' => Token::AnyRun,
'_' => Token::AnyOne,
other => Token::Char(other),
}
};
tokens.push(token);
}
tokens
}
fn matches(input: &[char], tokens: &[Token]) -> bool {
let (mut i, mut t) = (0usize, 0usize);
let mut backtrack: Option<(usize, usize)> = None;
while i < input.len() {
match tokens.get(t) {
Some(Token::AnyRun) => {
backtrack = Some((t, i));
t += 1;
continue;
}
Some(Token::AnyOne) => {
i += 1;
t += 1;
continue;
}
Some(Token::Char(c)) if *c == input[i] => {
i += 1;
t += 1;
continue;
}
Some(Token::Char(_)) | None => {}
}
match backtrack {
Some((run_t, run_i)) => {
backtrack = Some((run_t, run_i + 1));
i = run_i + 1;
t = run_t + 1;
}
None => return false,
}
}
tokens[t..].iter().all(|token| *token == Token::AnyRun)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn like_basic() {
assert!(sql_like_match("hello world", "%world", false));
assert!(sql_like_match("hello world", "hello%", false));
assert!(!sql_like_match("hello world", "xyz%", false));
assert!(sql_like_match("", "%", false));
assert!(!sql_like_match("", "_", false));
}
#[test]
fn underscore_matches_one_character_not_one_byte() {
assert!(sql_like_match("é", "_", false));
assert!(sql_like_match("naïve", "na_ve", false));
assert!(!sql_like_match("naïve", "na__ve", false));
}
#[test]
fn escape_makes_wildcards_literal() {
assert!(sql_like_match("100%", "100\\%", false));
assert!(!sql_like_match("1000", "100\\%", false));
assert!(sql_like_match("a_b", "a\\_b", false));
assert!(!sql_like_match("axb", "a\\_b", false));
assert!(sql_like_match("a\\", "a\\", false));
assert!(sql_like_match_escaped("50!%", "50!!!%", false, Some('!')));
assert!(sql_like_match_escaped("a\\b", "a\\b", false, None));
}
#[test]
fn ilike_case_insensitive() {
assert!(sql_like_match("Hello", "hello", true));
assert!(sql_like_match("WORLD", "%world%", true));
assert!(!sql_like_match("WORLD", "%world%", false));
}
#[test]
fn ilike_folds_unicode_case() {
assert!(sql_like_match("ÉCOLE", "école", true));
assert!(sql_like_match("ΣΟΦΙΑ", "σοφια", true));
assert!(!sql_like_match("ÉCOLE", "école", false));
}
#[test]
fn backtracking_finds_a_later_match() {
assert!(sql_like_match("abcabd", "%abd", false));
assert!(sql_like_match("aaa", "%a%a", false));
assert!(!sql_like_match("abc", "%d%", false));
}
}