use crate::options::MatchSorterOptions;
use crate::ranking::{PreparedQuery, Ranking, get_match_ranking, get_match_ranking_prepared};
pub fn get_item_values<T>(item: &T, key: &Key<T>) -> Vec<String> {
key.extract(item)
}
pub fn get_highest_ranking<T>(
item: &T,
keys: &[Key<T>],
query: &str,
options: &MatchSorterOptions<T>,
) -> RankingInfo {
let mut best = RankingInfo {
rank: Ranking::NoMatch,
ranked_value: String::new(),
key_index: 0,
key_threshold: None,
};
let mut key_index: usize = 0;
for key in keys {
let values = key.extract(item);
let threshold = key.threshold;
let min = key.min_ranking_value();
let max = key.max_ranking_value();
for value in &values {
let mut rank = get_match_ranking(value, query, options.keep_diacritics);
if rank > *max {
rank = *max;
}
if rank < *min && rank != Ranking::NoMatch {
rank = *min;
}
if rank > best.rank {
best = RankingInfo {
rank,
ranked_value: value.clone(),
key_index,
key_threshold: threshold,
};
}
key_index += 1;
}
}
best
}
pub(crate) fn get_highest_ranking_prepared<T>(
item: &T,
keys: &[Key<T>],
pq: &PreparedQuery,
options: &MatchSorterOptions<T>,
candidate_buf: &mut String,
finder: Option<&memchr::memmem::Finder<'_>>,
) -> RankingInfo {
let mut best = RankingInfo {
rank: Ranking::NoMatch,
ranked_value: String::new(),
key_index: 0,
key_threshold: None,
};
let mut key_index: usize = 0;
for key in keys {
let values = key.extract(item);
let threshold = key.threshold;
let min = key.min_ranking_value();
let max = key.max_ranking_value();
for value in &values {
let mut rank = get_match_ranking_prepared(
value,
pq,
options.keep_diacritics,
candidate_buf,
finder,
);
if rank > *max {
rank = *max;
}
if rank < *min && rank != Ranking::NoMatch {
rank = *min;
}
if rank > best.rank {
best = RankingInfo {
rank,
ranked_value: value.clone(),
key_index,
key_threshold: threshold,
};
}
key_index += 1;
}
}
best
}
type Extractor<T> = Box<dyn Fn(&T) -> Vec<String>>;
pub struct Key<T> {
extractor: Extractor<T>,
pub(crate) threshold: Option<Ranking>,
pub(crate) max_ranking: Ranking,
pub(crate) min_ranking: Ranking,
}
impl<T> Key<T> {
pub fn new<F>(extractor: F) -> Self
where
F: Fn(&T) -> Vec<String> + 'static,
{
Self {
extractor: Box::new(extractor),
threshold: None,
min_ranking: Ranking::NoMatch,
max_ranking: Ranking::CaseSensitiveEqual,
}
}
pub fn from_fn<F>(f: F) -> Self
where
F: Fn(&T) -> &str + 'static,
{
Self {
extractor: Box::new(move |item| vec![f(item).to_owned()]),
threshold: None,
min_ranking: Ranking::NoMatch,
max_ranking: Ranking::CaseSensitiveEqual,
}
}
pub fn from_fn_multi<F>(f: F) -> Self
where
F: Fn(&T) -> Vec<&str> + 'static,
{
Self {
extractor: Box::new(move |item| f(item).into_iter().map(|s| s.to_owned()).collect()),
threshold: None,
min_ranking: Ranking::NoMatch,
max_ranking: Ranking::CaseSensitiveEqual,
}
}
#[must_use]
pub fn threshold(mut self, ranking: Ranking) -> Self {
self.threshold = Some(ranking);
self
}
#[must_use]
pub fn max_ranking(mut self, ranking: Ranking) -> Self {
self.max_ranking = ranking;
self
}
#[must_use]
pub fn min_ranking(mut self, ranking: Ranking) -> Self {
self.min_ranking = ranking;
self
}
pub fn extract(&self, item: &T) -> Vec<String> {
(self.extractor)(item)
}
pub fn threshold_value(&self) -> Option<&Ranking> {
self.threshold.as_ref()
}
pub fn max_ranking_value(&self) -> &Ranking {
&self.max_ranking
}
pub fn min_ranking_value(&self) -> &Ranking {
&self.min_ranking
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RankingInfo {
pub rank: Ranking,
pub ranked_value: String,
pub key_index: usize,
pub key_threshold: Option<Ranking>,
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct User {
name: String,
email: String,
tags: Vec<String>,
}
fn sample_user() -> User {
User {
name: "Alice".to_owned(),
email: "alice@example.com".to_owned(),
tags: vec!["admin".to_owned(), "staff".to_owned()],
}
}
#[test]
fn new_accepts_closure_returning_vec_string() {
let key = Key::new(|u: &User| vec![u.name.clone()]);
let values = key.extract(&sample_user());
assert_eq!(values, vec!["Alice"]);
}
#[test]
fn new_default_threshold_is_none() {
let key = Key::new(|_: &User| vec![]);
assert_eq!(key.threshold, None);
}
#[test]
fn new_default_min_ranking_is_no_match() {
let key = Key::new(|_: &User| vec![]);
assert_eq!(key.min_ranking, Ranking::NoMatch);
}
#[test]
fn new_default_max_ranking_is_case_sensitive_equal() {
let key = Key::new(|_: &User| vec![]);
assert_eq!(key.max_ranking, Ranking::CaseSensitiveEqual);
}
#[test]
fn from_fn_single_value_extraction() {
let key = Key::<User>::from_fn(|u| u.name.as_str());
let values = key.extract(&sample_user());
assert_eq!(values, vec!["Alice"]);
}
#[test]
fn from_fn_equivalent_to_new_with_vec() {
let user = sample_user();
let key_new = Key::new(|u: &User| vec![u.name.clone()]);
let key_fn = Key::<User>::from_fn(|u| u.name.as_str());
let values_new = key_new.extract(&user);
let values_fn = key_fn.extract(&user);
assert_eq!(values_new, values_fn);
}
#[test]
fn from_fn_default_attributes() {
let key = Key::<User>::from_fn(|u| u.name.as_str());
assert_eq!(key.threshold, None);
assert_eq!(key.min_ranking, Ranking::NoMatch);
assert_eq!(key.max_ranking, Ranking::CaseSensitiveEqual);
}
#[test]
fn from_fn_multi_extracts_multiple_values() {
let key = Key::<User>::from_fn_multi(|u| u.tags.iter().map(|t| t.as_str()).collect());
let values = key.extract(&sample_user());
assert_eq!(values, vec!["admin", "staff"]);
}
#[test]
fn from_fn_multi_default_attributes() {
let key = Key::<User>::from_fn_multi(|u| u.tags.iter().map(|t| t.as_str()).collect());
assert_eq!(key.threshold, None);
assert_eq!(key.min_ranking, Ranking::NoMatch);
assert_eq!(key.max_ranking, Ranking::CaseSensitiveEqual);
}
#[test]
fn from_fn_multi_empty_vec() {
let key = Key::<User>::from_fn_multi(|_| vec![]);
let values = key.extract(&sample_user());
assert!(values.is_empty());
}
#[test]
fn threshold_sets_value() {
let key = Key::new(|_: &User| vec![]).threshold(Ranking::StartsWith);
assert_eq!(key.threshold, Some(Ranking::StartsWith));
}
#[test]
fn max_ranking_sets_value() {
let key = Key::new(|_: &User| vec![]).max_ranking(Ranking::Contains);
assert_eq!(key.max_ranking, Ranking::Contains);
}
#[test]
fn min_ranking_sets_value() {
let key = Key::new(|_: &User| vec![]).min_ranking(Ranking::Contains);
assert_eq!(key.min_ranking, Ranking::Contains);
}
#[test]
fn builder_chain_all_three() {
let key = Key::new(|u: &User| vec![u.email.clone()])
.threshold(Ranking::Acronym)
.max_ranking(Ranking::Equal)
.min_ranking(Ranking::Contains);
assert_eq!(key.threshold, Some(Ranking::Acronym));
assert_eq!(key.max_ranking, Ranking::Equal);
assert_eq!(key.min_ranking, Ranking::Contains);
}
#[test]
fn builder_chain_preserves_extractor() {
let key = Key::new(|u: &User| vec![u.name.clone()])
.threshold(Ranking::StartsWith)
.max_ranking(Ranking::Contains)
.min_ranking(Ranking::Acronym);
let values = key.extract(&sample_user());
assert_eq!(values, vec!["Alice"]);
}
#[test]
fn builder_from_fn_with_chain() {
let key = Key::<User>::from_fn(|u| u.email.as_str())
.threshold(Ranking::WordStartsWith)
.max_ranking(Ranking::StartsWith);
assert_eq!(key.threshold, Some(Ranking::WordStartsWith));
assert_eq!(key.max_ranking, Ranking::StartsWith);
assert_eq!(key.min_ranking, Ranking::NoMatch);
let values = key.extract(&sample_user());
assert_eq!(values, vec!["alice@example.com"]);
}
#[test]
fn builder_from_fn_multi_with_chain() {
let key = Key::<User>::from_fn_multi(|u| u.tags.iter().map(|t| t.as_str()).collect())
.min_ranking(Ranking::Contains);
assert_eq!(key.min_ranking, Ranking::Contains);
assert_eq!(key.threshold, None);
assert_eq!(key.max_ranking, Ranking::CaseSensitiveEqual);
let values = key.extract(&sample_user());
assert_eq!(values, vec!["admin", "staff"]);
}
#[test]
fn builder_last_call_wins_for_same_method() {
let key = Key::new(|_: &User| vec![])
.threshold(Ranking::Contains)
.threshold(Ranking::StartsWith);
assert_eq!(key.threshold, Some(Ranking::StartsWith));
}
#[test]
fn builder_matches_variant_in_threshold() {
let key = Key::new(|_: &User| vec![])
.threshold(Ranking::Matches(1.5))
.min_ranking(Ranking::Matches(1.2))
.max_ranking(Ranking::Matches(1.8));
assert_eq!(key.threshold, Some(Ranking::Matches(1.5)));
assert_eq!(key.min_ranking, Ranking::Matches(1.2));
assert_eq!(key.max_ranking, Ranking::Matches(1.8));
}
#[test]
fn ranking_info_construction() {
let info = RankingInfo {
rank: Ranking::Contains,
ranked_value: "hello".to_owned(),
key_index: 2,
key_threshold: Some(Ranking::StartsWith),
};
assert_eq!(info.rank, Ranking::Contains);
assert_eq!(info.ranked_value, "hello");
assert_eq!(info.key_index, 2);
assert_eq!(info.key_threshold, Some(Ranking::StartsWith));
}
#[test]
fn ranking_info_with_no_threshold() {
let info = RankingInfo {
rank: Ranking::Equal,
ranked_value: "world".to_owned(),
key_index: 0,
key_threshold: None,
};
assert_eq!(info.key_threshold, None);
}
#[test]
fn ranking_info_debug_formatting() {
let info = RankingInfo {
rank: Ranking::Acronym,
ranked_value: "test".to_owned(),
key_index: 1,
key_threshold: None,
};
let debug_str = format!("{info:?}");
assert!(debug_str.contains("Acronym"));
assert!(debug_str.contains("test"));
}
#[test]
fn ranking_info_clone() {
let info = RankingInfo {
rank: Ranking::StartsWith,
ranked_value: "cloned".to_owned(),
key_index: 3,
key_threshold: Some(Ranking::Contains),
};
let cloned = info.clone();
assert_eq!(info, cloned);
}
#[test]
fn ranking_info_partial_eq() {
let a = RankingInfo {
rank: Ranking::Contains,
ranked_value: "val".to_owned(),
key_index: 0,
key_threshold: None,
};
let b = RankingInfo {
rank: Ranking::Contains,
ranked_value: "val".to_owned(),
key_index: 0,
key_threshold: None,
};
assert_eq!(a, b);
}
#[test]
fn ranking_info_partial_eq_different_rank() {
let a = RankingInfo {
rank: Ranking::Contains,
ranked_value: "val".to_owned(),
key_index: 0,
key_threshold: None,
};
let b = RankingInfo {
rank: Ranking::Equal,
ranked_value: "val".to_owned(),
key_index: 0,
key_threshold: None,
};
assert_ne!(a, b);
}
#[test]
fn key_with_string_type() {
let key = Key::new(|s: &String| vec![s.clone()]);
let values = key.extract(&"hello world".to_owned());
assert_eq!(values, vec!["hello world"]);
}
#[test]
fn from_fn_with_string_type() {
let key = Key::<String>::from_fn(|s| s.as_str());
let values = key.extract(&"test".to_owned());
assert_eq!(values, vec!["test"]);
}
#[test]
fn get_item_values_single_value() {
let key = Key::<User>::from_fn(|u| u.name.as_str());
let values = get_item_values(&sample_user(), &key);
assert_eq!(values, vec!["Alice"]);
}
#[test]
fn get_item_values_multi_value() {
let key = Key::<User>::from_fn_multi(|u| u.tags.iter().map(|t| t.as_str()).collect());
let values = get_item_values(&sample_user(), &key);
assert_eq!(values, vec!["admin", "staff"]);
}
#[test]
fn get_item_values_empty() {
let key = Key::new(|_: &User| vec![]);
let values = get_item_values(&sample_user(), &key);
assert!(values.is_empty());
}
fn default_opts<T>() -> MatchSorterOptions<T> {
MatchSorterOptions::default()
}
#[test]
fn highest_ranking_single_key_exact_match() {
let keys = vec![Key::new(|u: &User| vec![u.name.clone()])];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.ranked_value, "Alice");
assert_eq!(info.key_index, 0);
assert_eq!(info.key_threshold, None);
}
#[test]
fn highest_ranking_picks_best_across_multiple_keys() {
let keys: Vec<Key<User>> = vec![
Key::new(|u: &User| vec![u.email.clone()]),
Key::new(|u: &User| vec![u.name.clone()]),
];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.ranked_value, "Alice");
assert_eq!(info.key_index, 1);
}
#[test]
fn highest_ranking_max_ranking_clamps_down() {
let keys = vec![Key::new(|u: &User| vec![u.name.clone()]).max_ranking(Ranking::Contains)];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
}
#[test]
fn highest_ranking_max_ranking_clamps_starts_with_to_contains() {
let keys = vec![Key::new(|u: &User| vec![u.name.clone()]).max_ranking(Ranking::Contains)];
let info = get_highest_ranking(&sample_user(), &keys, "ali", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
}
#[test]
fn highest_ranking_min_ranking_promotes_matches_to_contains() {
let item = "playground".to_owned();
let keys = vec![Key::new(|s: &String| vec![s.clone()]).min_ranking(Ranking::Contains)];
let info = get_highest_ranking(&item, &keys, "plgnd", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
}
#[test]
fn highest_ranking_min_ranking_does_not_promote_no_match() {
let item = "abc".to_owned();
let keys = vec![Key::new(|s: &String| vec![s.clone()]).min_ranking(Ranking::Contains)];
let info = get_highest_ranking(&item, &keys, "xyz", &default_opts());
assert_eq!(info.rank, Ranking::NoMatch);
}
#[test]
fn highest_ranking_tie_break_lower_key_index_wins() {
let keys: Vec<Key<User>> = vec![
Key::new(|u: &User| vec![u.name.clone()]),
Key::new(|u: &User| vec![u.name.clone()]),
];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.key_index, 0);
}
#[test]
fn highest_ranking_tie_break_with_clamping() {
let keys: Vec<Key<User>> = vec![
Key::new(|u: &User| vec![u.name.clone()]).max_ranking(Ranking::Contains),
Key::new(|u: &User| vec![u.email.clone()]).max_ranking(Ranking::Contains),
];
let info = get_highest_ranking(&sample_user(), &keys, "alice", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
assert_eq!(info.key_index, 0);
assert_eq!(info.ranked_value, "Alice");
}
#[test]
fn highest_ranking_key_threshold_reflected() {
let keys = vec![Key::new(|u: &User| vec![u.name.clone()]).threshold(Ranking::StartsWith)];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.key_threshold, Some(Ranking::StartsWith));
}
#[test]
fn highest_ranking_key_threshold_none_when_not_set() {
let keys = vec![Key::new(|u: &User| vec![u.name.clone()])];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.key_threshold, None);
}
#[test]
fn highest_ranking_multi_value_key_best_value_wins() {
let keys = vec![Key::new(|u: &User| u.tags.clone())];
let info = get_highest_ranking(&sample_user(), &keys, "admin", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.ranked_value, "admin");
assert_eq!(info.key_index, 0);
}
#[test]
fn highest_ranking_flattened_index_across_keys() {
let keys: Vec<Key<User>> = vec![
Key::new(|u: &User| u.tags.clone()),
Key::new(|u: &User| vec![u.name.clone()]),
];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.key_index, 2);
}
#[test]
fn highest_ranking_no_keys_returns_no_match() {
let keys: Vec<Key<User>> = vec![];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::NoMatch);
}
#[test]
fn highest_ranking_empty_extractor_returns_no_match() {
let keys = vec![Key::new(|_: &User| vec![])];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::NoMatch);
}
#[test]
fn highest_ranking_max_ranking_does_not_affect_lower_ranks() {
let item = "xxadminxx".to_owned();
let keys = vec![Key::new(|s: &String| vec![s.clone()]).max_ranking(Ranking::StartsWith)];
let info = get_highest_ranking(&item, &keys, "admin", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
}
#[test]
fn highest_ranking_min_ranking_does_not_affect_higher_ranks() {
let keys = vec![Key::new(|u: &User| vec![u.name.clone()]).min_ranking(Ranking::Contains)];
let info = get_highest_ranking(&sample_user(), &keys, "ali", &default_opts());
assert_eq!(info.rank, Ranking::StartsWith);
}
#[test]
fn highest_ranking_both_clamps_applied() {
let keys = vec![
Key::new(|u: &User| vec![u.name.clone()])
.min_ranking(Ranking::Contains)
.max_ranking(Ranking::Contains),
];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::Contains);
}
#[test]
fn highest_ranking_winning_key_threshold_from_correct_key() {
let keys: Vec<Key<User>> = vec![
Key::new(|u: &User| vec![u.email.clone()]).threshold(Ranking::StartsWith),
Key::new(|u: &User| vec![u.name.clone()]).threshold(Ranking::Acronym),
];
let info = get_highest_ranking(&sample_user(), &keys, "Alice", &default_opts());
assert_eq!(info.rank, Ranking::CaseSensitiveEqual);
assert_eq!(info.key_threshold, Some(Ranking::Acronym));
}
#[test]
fn highest_ranking_keep_diacritics_option_passed() {
let item = "caf\u{e9}".to_owned();
let keys = vec![Key::new(|s: &String| vec![s.clone()])];
let opts_strip = MatchSorterOptions {
keep_diacritics: false,
..Default::default()
};
let info_strip = get_highest_ranking(&item, &keys, "cafe", &opts_strip);
assert_eq!(info_strip.rank, Ranking::CaseSensitiveEqual);
let opts_keep = MatchSorterOptions {
keep_diacritics: true,
..Default::default()
};
let info_keep = get_highest_ranking(&item, &keys, "cafe", &opts_keep);
assert_eq!(info_keep.rank, Ranking::NoMatch);
}
}