#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Query {
normalized: String,
terms: Vec<String>,
}
impl Query {
pub fn new(input: &str) -> Self {
let normalized = normalize_query(input);
let terms = parse_terms(&normalized);
Self { normalized, terms }
}
pub fn normalized(&self) -> &str {
&self.normalized
}
pub fn terms(&self) -> &[String] {
&self.terms
}
pub fn is_empty(&self) -> bool {
self.terms.is_empty()
}
pub fn matches(&self, candidate: &str) -> bool {
if self.is_empty() {
return true;
}
let normalized_candidate = normalize_query(candidate);
self.terms
.iter()
.all(|term| normalized_candidate.contains(term))
}
}
pub fn normalize_query(input: &str) -> String {
input
.split_whitespace()
.map(str::to_lowercase)
.collect::<Vec<_>>()
.join(" ")
}
pub fn split_terms(input: &str) -> Vec<String> {
parse_terms(&normalize_query(input))
}
pub fn matches_query(query: &str, candidate: &str) -> bool {
Query::new(query).matches(candidate)
}
fn parse_terms(normalized: &str) -> Vec<String> {
if normalized.is_empty() {
return Vec::new();
}
normalized.split(' ').map(str::to_owned).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normalize_query_trims_and_folds_whitespace() {
assert_eq!(normalize_query(" Foo\tbar\nBAZ "), "foo bar baz");
}
#[test]
fn split_terms_uses_normalized_tokens() {
assert_eq!(split_terms(" zg Search "), ["zg", "search"]);
}
#[test]
fn empty_query_matches_any_candidate() {
let query = Query::new(" ");
assert!(query.matches("anything"));
}
#[test]
fn query_matches_all_terms_case_insensitively() {
let query = Query::new("zg rust");
assert!(query.matches("Building ZG tools with Rust nightly"));
assert!(!query.matches("Building zg tools with Python"));
}
#[test]
fn matches_query_supports_one_off_checks() {
assert!(matches_query("mini grep", "A tiny mini GREP-like utility"));
}
}