#![warn(missing_docs)]
pub mod ranking;
pub mod key;
pub mod no_keys;
pub mod options;
pub mod sort;
use std::borrow::Cow;
pub use key::{Key, RankingInfo, get_highest_ranking, get_item_values};
pub use no_keys::{AsMatchStr, rank_item};
pub use options::{MatchSorterOptions, RankedItem};
pub use ranking::{Ranking, get_match_ranking};
pub use sort::{default_base_sort, sort_ranked_values};
use key::get_highest_ranking_prepared as get_highest_ranking_prepared_impl;
use no_keys::AsMatchStr as AsMatchStrTrait;
use ranking::{PreparedQuery, get_match_ranking_prepared as get_match_ranking_prepared_impl};
use sort::{
default_base_sort as default_base_sort_impl, sort_ranked_values as sort_ranked_values_impl,
};
pub fn match_sorter<'a, T>(
items: &'a [T],
value: &str,
options: MatchSorterOptions<T>,
) -> Vec<&'a T>
where
T: AsMatchStrTrait,
{
let pq = PreparedQuery::new(value, options.keep_diacritics);
let finder = if pq.lower.is_empty() {
None
} else {
Some(memchr::memmem::Finder::new(pq.lower.as_bytes()))
};
let mut candidate_buf = String::new();
let mut ranked_items: Vec<RankedItem<'a, T>> = Vec::with_capacity(items.len());
for (index, item) in items.iter().enumerate() {
let (rank, ranked_value, key_index, key_threshold) = if options.keys.is_empty() {
let s = item.as_match_str();
let rank = get_match_ranking_prepared_impl(
s,
&pq,
options.keep_diacritics,
&mut candidate_buf,
finder.as_ref(),
);
(rank, Cow::Borrowed(s), 0_usize, None)
} else {
let info = get_highest_ranking_prepared_impl(
item,
&options.keys,
&pq,
&options,
&mut candidate_buf,
finder.as_ref(),
);
(
info.rank,
Cow::Owned(info.ranked_value),
info.key_index,
info.key_threshold,
)
};
let effective_threshold = key_threshold.as_ref().unwrap_or(&options.threshold);
if rank >= *effective_threshold {
ranked_items.push(RankedItem {
item,
index,
rank,
ranked_value,
key_index,
key_threshold,
});
}
}
if let Some(ref sorter) = options.sorter {
ranked_items = sorter(ranked_items);
} else {
ranked_items.sort_by(|a, b| {
if let Some(ref base_sort) = options.base_sort {
sort_ranked_values_impl(a, b, base_sort.as_ref())
} else {
sort_ranked_values_impl(a, b, &default_base_sort_impl)
}
});
}
ranked_items.iter().map(|ri| ri.item).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_keys_basic_str_slice() {
let items = ["apple", "banana", "grape"];
let results = match_sorter(&items, "ap", MatchSorterOptions::default());
assert_eq!(results[0], &"apple");
assert!(!results.is_empty());
}
#[test]
fn no_keys_exact_match_first() {
let items = ["banana", "apple", "pineapple"];
let results = match_sorter(&items, "apple", MatchSorterOptions::default());
assert_eq!(results[0], &"apple");
}
#[test]
fn no_keys_empty_query_returns_all_sorted() {
let items = ["banana", "apple", "cherry"];
let results = match_sorter(&items, "", MatchSorterOptions::default());
assert_eq!(results.len(), 3);
assert_eq!(results[0], &"apple");
assert_eq!(results[1], &"banana");
assert_eq!(results[2], &"cherry");
}
#[test]
fn no_keys_no_match_returns_empty() {
let items = ["apple", "banana", "grape"];
let results = match_sorter(&items, "xyz", MatchSorterOptions::default());
assert!(results.is_empty());
}
#[test]
fn no_keys_string_items() {
let items = vec!["hello".to_owned(), "help".to_owned(), "world".to_owned()];
let results = match_sorter(&items, "hel", MatchSorterOptions::default());
assert_eq!(results.len(), 2);
assert_eq!(results[0].as_str(), "hello");
assert_eq!(results[1].as_str(), "help");
}
#[test]
fn no_keys_empty_items() {
let items: [&str; 0] = [];
let results = match_sorter(&items, "test", MatchSorterOptions::default());
assert!(results.is_empty());
}
#[test]
fn threshold_filters_below() {
let items = ["apple", "banana", "grape"];
let opts = MatchSorterOptions {
threshold: Ranking::Contains,
..Default::default()
};
let results = match_sorter(&items, "ap", opts);
assert_eq!(results, vec![&"apple", &"grape"]);
}
#[test]
fn threshold_case_sensitive_equal_excludes_case_insensitive() {
let items = ["Apple", "apple", "APPLE"];
let opts = MatchSorterOptions {
threshold: Ranking::CaseSensitiveEqual,
..Default::default()
};
let results = match_sorter(&items, "apple", opts);
assert_eq!(results, vec![&"apple"]);
}
#[test]
fn key_threshold_overrides_global() {
let items = vec!["apple".to_owned(), "apricot".to_owned()];
let opts = MatchSorterOptions {
keys: vec![
Key::new(|s: &String| vec![s.clone()]).threshold(Ranking::CaseSensitiveEqual),
],
threshold: Ranking::Matches(1.0), ..Default::default()
};
let results = match_sorter(&items, "apple", opts);
assert_eq!(results.len(), 1);
assert_eq!(results[0].as_str(), "apple");
}
#[test]
fn custom_sorter_replaces_default_sort() {
let items = ["apple", "banana", "grape"];
let opts = MatchSorterOptions {
sorter: Some(Box::new(|mut items: Vec<RankedItem<&str>>| {
items.reverse();
items
})),
..Default::default()
};
let default_results = match_sorter(
&["apple", "banana", "grape"],
"a",
MatchSorterOptions::default(),
);
let custom_results = match_sorter(&items, "a", opts);
assert_eq!(custom_results.len(), default_results.len());
if custom_results.len() > 1 {
assert_eq!(custom_results.first(), default_results.last(),);
}
}
#[test]
fn custom_sorter_called_with_filtered_items() {
let items = ["apple", "xyz"];
let opts: MatchSorterOptions<&str> = MatchSorterOptions {
sorter: Some(Box::new(|items: Vec<RankedItem<&str>>| {
assert!(items.iter().all(|ri| *ri.item != "xyz"));
items
})),
..Default::default()
};
let _ = match_sorter(&items, "ap", opts);
}
#[test]
fn custom_base_sort_reverse_alphabetical() {
let items = ["alpha", "beta", "gamma"];
let opts = MatchSorterOptions {
base_sort: Some(Box::new(|a: &RankedItem<&str>, b: &RankedItem<&str>| {
b.ranked_value.cmp(&a.ranked_value)
})),
..Default::default()
};
let results = match_sorter(&items, "", opts);
assert_eq!(results[0], &"gamma");
assert_eq!(results[1], &"beta");
assert_eq!(results[2], &"alpha");
}
#[test]
fn keys_mode_single_key() {
#[derive(Debug)]
struct Item {
name: String,
}
impl AsMatchStr for Item {
fn as_match_str(&self) -> &str {
&self.name
}
}
let items = vec![
Item {
name: "Alice".to_owned(),
},
Item {
name: "Bob".to_owned(),
},
Item {
name: "Charlie".to_owned(),
},
];
let opts = MatchSorterOptions {
keys: vec![Key::new(|i: &Item| vec![i.name.clone()])],
..Default::default()
};
let results = match_sorter(&items, "ali", opts);
assert!(!results.is_empty());
assert_eq!(results[0].name, "Alice");
}
#[test]
fn keys_mode_multiple_keys_best_wins() {
#[derive(Debug)]
struct Person {
name: String,
email: String,
}
impl AsMatchStr for Person {
fn as_match_str(&self) -> &str {
&self.name
}
}
let items = vec![
Person {
name: "Alice".to_owned(),
email: "alice@example.com".to_owned(),
},
Person {
name: "Bob".to_owned(),
email: "bob@example.com".to_owned(),
},
];
let opts = MatchSorterOptions {
keys: vec![
Key::new(|p: &Person| vec![p.name.clone()]),
Key::new(|p: &Person| vec![p.email.clone()]),
],
..Default::default()
};
let results = match_sorter(&items, "alice", opts);
assert_eq!(results.len(), 1);
assert_eq!(results[0].name, "Alice");
}
#[test]
fn items_sorted_by_rank_descending() {
let items = ["pineapple", "apple"];
let results = match_sorter(&items, "app", MatchSorterOptions::default());
assert_eq!(results[0], &"apple"); assert_eq!(results[1], &"pineapple");
}
#[test]
fn diacritics_handling() {
let items = ["cafe", "caf\u{00e9}"];
let results = match_sorter(&items, "cafe", MatchSorterOptions::default());
assert_eq!(results.len(), 2);
}
#[test]
fn keep_diacritics_option() {
let items = ["cafe", "caf\u{00e9}"];
let opts = MatchSorterOptions {
keep_diacritics: true,
..Default::default()
};
let results = match_sorter(&items, "cafe", opts);
assert_eq!(results, vec![&"cafe"]);
}
}