use super::predicates::*;
use super::types::*;
use crate::common::Text;
const WM: f32 = 150.0;
const POSITION_BOOST: f32 = 100.0;
const POSITION_BONUS: f32 = 20.0;
const POSITION_MIN: f32 = 0.0;
const TAU_SIZE: f32 = 150.0;
pub fn score_quality(query_len: usize, subject_len: usize, quality: f32, position: f32) -> f32 {
2.0 * (query_len as f32)
* ((WM * quality) + score_position(position))
* score_size(query_len, subject_len)
}
pub fn score_position(position: f32) -> f32 {
if position < POSITION_BONUS {
let sc = POSITION_BONUS - position;
POSITION_BOOST + (sc * sc)
} else {
POSITION_MIN.max((POSITION_BOOST + POSITION_BONUS) - position)
}
}
pub fn score_size(query_len: usize, subject_len: usize) -> f32 {
let penalty = (subject_len as isize - query_len as isize).abs();
TAU_SIZE / (TAU_SIZE + penalty as f32)
}
pub fn score_acronyms(query: &Query, subject: &Text) -> Option<AcronymResult> {
if query.len() <= 1 || subject.len() <= 1 {
return None;
}
let mut matches = vec![];
let mut count = 0;
let mut sep_count = 0;
let mut sum_position = 0;
let mut same_case = 0;
let query_iter = query.lowercase_iter().enumerate();
let mut subject_iter = subject.lowercase_iter().enumerate();
let mut progress = 0;
'query_loop: for (qindex, query_grapheme) in query_iter {
if progress == subject.len() {
break 'query_loop;
}
'subject_loop: for (index, subject_grapheme) in &mut subject_iter {
progress += 1;
if query_grapheme == subject_grapheme {
if is_word_separator(query_grapheme) {
sep_count += 1;
break 'subject_loop;
} else if is_start_of_word(subject, index) {
sum_position += index;
count += 1;
matches.push(index);
if query.grapheme_at(qindex) == subject.grapheme_at(index) {
same_case += 1;
}
break 'subject_loop;
}
}
}
}
if count < 2 {
return None;
}
let full_world = if count == query.len() {
is_a_unique_acronym(subject, count)
} else {
false
};
let score = score_pattern(count, query.len(), same_case, true, full_world);
if score <= 0.0 {
return None;
}
let position = sum_position as f32 / count as f32;
Some(AcronymResult::new(
score,
position,
count + sep_count,
matches,
))
}
pub fn score_exact_match(query: &Query, subject: &Text) -> Option<ExactMatchResult> {
let (mut position, mut same_case) = sequence_position(query, subject, 0)?;
let mut is_start = is_start_of_word(subject, position);
if !is_start {
if let Some((sec_position, sec_same_case)) =
sequence_position(query, subject, position + query.len())
{
is_start = is_start_of_word(subject, sec_position);
if is_start {
position = sec_position;
same_case = sec_same_case;
}
}
}
let is_end = is_end_of_word(subject, (position + query.len()) - 1);
let score = score_quality(
query.len(),
subject.len(),
score_pattern(query.len(), query.len(), same_case, is_start, is_end),
position as f32,
);
let matches: Vec<usize> = (position..(position + query.len())).collect();
Some(ExactMatchResult::new(score, matches))
}
pub fn score_pattern(
count: usize,
len: usize,
same_case: usize,
is_start: bool,
is_end: bool,
) -> f32 {
let mut sc = count;
let mut bonus = 6;
if same_case == count {
bonus += 2;
}
if is_start {
bonus += 3;
}
if is_end {
bonus += 1;
}
if count == len {
if is_start {
if same_case == len {
sc += 2;
} else {
sc += 1;
}
}
if is_end {
bonus += 1;
}
}
(same_case + (sc * (sc + bonus))) as f32
}
pub fn score_consecutives(
query: &Query,
subject: &Text,
query_position: usize,
subject_position: usize,
is_start: bool,
) -> f32 {
let query_left = query.len() - query_position;
let subject_left = subject.len() - subject_position;
let left = if subject_left < query_left {
subject_left
} else {
query_left
};
let mut same_case = 0;
let mut sz = 1;
if query.grapheme_at(query_position) == subject.grapheme_at(subject_position) {
same_case += 1;
}
let query_iter = query.lowercase_iter().enumerate().skip(query_position + 1);
let mut subject_iter = subject
.lowercase_iter()
.enumerate()
.skip(subject_position + 1);
let mut subject_cursor = subject_position;
for (qindex, query_grapheme) in query_iter {
if let Some((index, subject_grapheme)) = subject_iter.next() {
if query_grapheme == subject_grapheme {
subject_cursor = index;
if query.grapheme_at(qindex) == subject.grapheme_at(index) {
same_case += 1;
}
} else {
break;
}
} else {
break;
}
if sz >= left {
break;
}
sz += 1;
}
if sz == 1 {
let score = 1 + 2 * same_case;
return score as f32;
}
let is_end = is_end_of_word(subject, subject_cursor);
score_pattern(sz, query.len(), same_case, is_start, is_end)
}
pub fn score_character(
position: usize,
is_start: bool,
acronym_score: f32,
consecutive_score: f32,
) -> f32 {
let position_score = score_position(position as f32);
let mut start_bonus = 0.0;
let mut score = consecutive_score;
if is_start {
start_bonus = 10.0;
if acronym_score > consecutive_score {
score = acronym_score;
}
}
position_score + (WM * (score + start_bonus))
}
fn sequence_position(query: &Query, subject: &Text, skip: usize) -> Option<(usize, usize)> {
let mut query_iter = query.lowercase_iter().enumerate();
let mut subject_iter = subject.lowercase_iter().enumerate().skip(skip);
let mut sequence = false;
let mut position = 0;
let mut same_case = 0;
while let Some((qindex, query_grapheme)) = query_iter.next() {
let (index, subject_grapheme) = subject_iter.next()?;
if query_grapheme == subject_grapheme {
if !sequence {
position = index;
}
sequence = true;
if query.grapheme_at(qindex) == subject.grapheme_at(index) {
same_case += 1
}
} else {
same_case = 0;
sequence = false;
query_iter = query.lowercase_iter().enumerate();
}
}
Some((position, same_case))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::TextBuilder;
#[test]
fn score_acronyms_with_no_results_test() {
let cases = vec![
("fft", "FirstFactoryTests"),
("ff/t", "FirstFactory/Tests"),
("y̆f公🍣", "Y̆/first/公/🍣.js"),
];
for (q, s) in cases {
let query = Query::from(q);
let subject = TextBuilder::build(s);
let result = score_acronyms(&query, &subject);
assert!(result.is_some(), "Expected {} be inside {}", query, subject);
assert!(
result.unwrap().score > 0.0,
"Expected {} be inside {}",
query,
subject
);
}
}
#[test]
fn score_acronyms_test() {
let cases = vec![
("f", "fact"),
("fft", "f"),
("iae", "FirstFactoryTests.html"),
];
for (q, s) in cases {
let query = Query::from(q);
let subject = TextBuilder::build(s);
let result = score_acronyms(&query, &subject);
assert!(
result.is_none(),
"Expected {} not to be inside {}",
query,
subject
);
}
}
#[test]
fn score_exact_match_with_no_results_test() {
let cases = vec![
("test", "t0e0s0t"),
("foo", "f oo"),
("y̆公🍣", "y̆x公x🍣.js"),
];
for (q, s) in cases {
let query = Query::from(q);
let subject = TextBuilder::build(s);
let result = score_exact_match(&query, &subject);
assert!(
result.is_none(),
"Expected {} not to be inside {}",
query,
subject
);
}
}
#[test]
fn score_exact_match_test() {
let cases = vec![
("test", "subject_test.rb"),
("foo", "foo/foo_test.rb"),
("foo", "xfoo/foo_test.rb"),
("foo", "xfooxfoo_test.rb"),
("foo", "FooTest.rb"),
("y̆公🍣", "first/y̆公🍣.js"),
("y̆公🍣", "First/Y̆公🍣.js"),
];
for (q, s) in cases {
let query = Query::from(q);
let subject = TextBuilder::build(s);
let result = score_exact_match(&query, &subject);
assert!(
result.is_some(),
"Expected {} to score inside {}",
query,
subject
);
assert!(
result.unwrap().score > 0.0,
"Expected {} to score inside {}",
query,
subject
);
}
}
#[test]
fn score_consecutives_test() {
let cases = vec![
("foo", "faa", 0, 0, true),
("foo", "foxo", 0, 0, true),
("qfoo", "qabfoxo", 1, 3, false),
("foo", "what/foo/bar", 0, 5, true),
("foo", "what/foobar", 0, 5, true),
("foo", "whatfoobar", 0, 4, false),
("foo", "what/fo", 0, 5, true),
("foo", "fxoox", 1, 2, true),
];
for (q, s, qp, sp, start) in cases {
let query = Query::from(q);
let subject = TextBuilder::build(s);
assert!(
score_consecutives(&query, &subject, qp, sp, start) > 0.0,
"Expected {} to score in {}",
query,
subject,
);
}
}
#[test]
fn sequence_position_test() {
let cases = vec![
("foo", "foo", 0, Some((0, 3))),
("foo", "FOO", 0, Some((0, 0))),
("Foo", "foo", 0, Some((0, 2))),
("foo", "fooxfoo", 0, Some((0, 3))),
("foo", "xfoo", 0, Some((1, 3))),
("y̆", "xfy̆oo", 0, Some((2, 1))),
("y̆", "xfY̆oo", 0, Some((2, 0))),
("公", "公", 0, Some((0, 1))),
("🍣", "y̆公🍣", 0, Some((2, 1))),
("foo", "fooxfoo", 2, Some((4, 3))),
("foo", "xfoo", 2, None),
("foo", "foxo", 0, None),
("foo", "nope", 0, None),
];
for (q, s, skip, expected) in cases {
let query = Query::from(q);
let subject = TextBuilder::build(s);
assert_eq!(
sequence_position(&query, &subject, skip),
expected,
"Expected query {} to be contained in {} at {:?}",
query,
subject,
expected
);
}
}
}