use std::cmp::max;
use crate::chars::{Char, CharClass};
use crate::matrix::{MatcherDataView, MatrixCell, ScoreCell};
use crate::score::{
BONUS_BOUNDARY, BONUS_CONSECUTIVE, BONUS_FIRST_CHAR_MULTIPLIER, MAX_PREFIX_BONUS,
PENALTY_GAP_EXTENSION, PENALTY_GAP_START, PREFIX_BONUS_SCALE, SCORE_MATCH,
};
use crate::{Config, Matcher};
impl Matcher {
pub(crate) fn fuzzy_match_optimal<const INDICES: bool, H: Char + PartialEq<N>, N: Char>(
&mut self,
haystack: &[H],
needle: &[N],
start: usize,
greedy_end: usize,
end: usize,
indices: &mut Vec<u32>,
) -> Option<u16> {
let Some(mut matrix) = self.slab.alloc(&haystack[start..end], needle.len()) else {
return self.fuzzy_match_greedy_::<INDICES, H, N>(
haystack, needle, start, greedy_end, indices,
);
};
let prev_class = start
.checked_sub(1)
.map(|i| haystack[i].char_class(&self.config))
.unwrap_or(self.config.initial_char_class);
let matched = matrix.setup::<INDICES, _>(needle, prev_class, &self.config, start as u32);
if !matched {
assert!(
!N::ASCII || !H::ASCII,
"should have been caught by prefilter"
);
return None;
}
let matrix_len = matrix.populate_matrix::<INDICES, _>(needle);
let last_row_off = matrix.row_offs[needle.len() - 1];
let relative_last_row_off = last_row_off as usize + 1 - needle.len();
let (match_end, match_score_cell) = matrix.current_row[relative_last_row_off..]
.iter()
.enumerate()
.max_by_key(|(_, cell)| cell.score)
.expect("there must be atleast one match");
if INDICES {
matrix.reconstruct_optimal_path(match_end as u16, indices, matrix_len, start as u32);
}
Some(match_score_cell.score)
}
}
const UNMATCHED: ScoreCell = ScoreCell {
score: 0,
consecutive_bonus: 0,
matched: true,
};
fn next_m_cell(p_score: u16, bonus: u16, m_cell: ScoreCell) -> ScoreCell {
if m_cell == UNMATCHED {
return ScoreCell {
score: p_score + bonus + SCORE_MATCH,
matched: false,
consecutive_bonus: bonus as u8,
};
}
let mut consecutive_bonus = max(m_cell.consecutive_bonus as u16, BONUS_CONSECUTIVE);
if bonus >= BONUS_BOUNDARY && bonus > consecutive_bonus {
consecutive_bonus = bonus
}
let score_match = m_cell.score + max(consecutive_bonus, bonus);
let score_skip = p_score + bonus;
if score_match > score_skip {
ScoreCell {
score: score_match + SCORE_MATCH,
matched: true,
consecutive_bonus: consecutive_bonus as u8,
}
} else {
ScoreCell {
score: score_skip + SCORE_MATCH,
matched: false,
consecutive_bonus: bonus as u8,
}
}
}
fn p_score(prev_p_score: u16, prev_m_score: u16) -> (u16, bool) {
let score_match = prev_m_score.saturating_sub(PENALTY_GAP_START);
let score_skip = prev_p_score.saturating_sub(PENALTY_GAP_EXTENSION);
if score_match > score_skip {
(score_match, true)
} else {
(score_skip, false)
}
}
impl<H: Char> MatcherDataView<'_, H> {
fn setup<const INDICES: bool, N: Char>(
&mut self,
needle: &[N],
mut prev_class: CharClass,
config: &Config,
start: u32,
) -> bool
where
H: PartialEq<N>,
{
let mut row_iter = needle.iter().copied().zip(self.row_offs.iter_mut());
let (mut needle_char, mut row_start) = row_iter.next().unwrap();
let col_iter = self
.haystack
.iter_mut()
.zip(self.bonus.iter_mut())
.enumerate();
let mut matched = false;
for (i, (c_, bonus_)) in col_iter {
let (c, class) = c_.char_class_and_normalize(config);
*c_ = c;
let bonus = config.bonus_for(prev_class, class);
*bonus_ = bonus as u8;
prev_class = class;
let i = i as u16;
if c == needle_char {
if let Some(next) = row_iter.next() {
*row_start = i;
(needle_char, row_start) = next;
} else if !matched {
*row_start = i;
matched = true;
}
}
}
if !matched {
return false;
}
debug_assert_eq!(self.row_offs[0], 0);
Self::score_row::<true, INDICES, _>(
self.current_row,
self.matrix_cells,
self.haystack,
self.bonus,
0,
self.row_offs[1],
0,
needle[0],
needle[1],
if config.prefer_prefix {
if start == 0 {
MAX_PREFIX_BONUS * PREFIX_BONUS_SCALE
} else {
(MAX_PREFIX_BONUS * PREFIX_BONUS_SCALE - PENALTY_GAP_START).saturating_sub(
(start - 1).min(u16::MAX as u32) as u16 * PENALTY_GAP_EXTENSION,
)
}
} else {
0
},
);
true
}
#[allow(clippy::too_many_arguments)]
fn score_row<const FIRST_ROW: bool, const INDICES: bool, N: Char>(
current_row: &mut [ScoreCell],
matrix_cells: &mut [MatrixCell],
haystack: &[H],
bonus: &[u8],
row_off: u16,
mut next_row_off: u16,
needle_idx: u16,
needle_char: N,
next_needle_char: N,
mut prefix_bonus: u16,
) where
H: PartialEq<N>,
{
next_row_off -= 1;
let relative_row_off = row_off - needle_idx;
let next_relative_row_off = next_row_off - needle_idx;
let skipped_col_iter = haystack[row_off as usize..next_row_off as usize]
.iter()
.zip(bonus[row_off as usize..next_row_off as usize].iter())
.zip(current_row[relative_row_off as usize..next_relative_row_off as usize].iter_mut())
.zip(matrix_cells.iter_mut());
let mut prev_p_score = 0;
let mut prev_m_score = 0;
for (((&c, bonus), score_cell), matrix_cell) in skipped_col_iter {
let (p_score, p_matched) = p_score(prev_p_score, prev_m_score);
let m_cell = if FIRST_ROW {
let cell = if c == needle_char {
ScoreCell {
score: *bonus as u16 * BONUS_FIRST_CHAR_MULTIPLIER
+ SCORE_MATCH
+ prefix_bonus / PREFIX_BONUS_SCALE,
matched: false,
consecutive_bonus: *bonus,
}
} else {
UNMATCHED
};
prefix_bonus = prefix_bonus.saturating_sub(PENALTY_GAP_EXTENSION);
cell
} else {
*score_cell
};
if INDICES {
matrix_cell.set(p_matched, m_cell.matched);
}
prev_p_score = p_score;
prev_m_score = m_cell.score;
}
let col_iter = haystack[next_row_off as usize..]
.windows(2)
.zip(bonus[next_row_off as usize..].windows(2))
.zip(current_row[next_relative_row_off as usize..].iter_mut())
.zip(matrix_cells[(next_relative_row_off - relative_row_off) as usize..].iter_mut());
for (((c, bonus), score_cell), matrix_cell) in col_iter {
let (p_score, p_matched) = p_score(prev_p_score, prev_m_score);
let m_cell = if FIRST_ROW {
let cell = if c[0] == needle_char {
ScoreCell {
score: bonus[0] as u16 * BONUS_FIRST_CHAR_MULTIPLIER
+ SCORE_MATCH
+ prefix_bonus / PREFIX_BONUS_SCALE,
matched: false,
consecutive_bonus: bonus[0],
}
} else {
UNMATCHED
};
prefix_bonus = prefix_bonus.saturating_sub(PENALTY_GAP_EXTENSION);
cell
} else {
*score_cell
};
*score_cell = if c[1] == next_needle_char {
next_m_cell(p_score, bonus[1] as u16, m_cell)
} else {
UNMATCHED
};
if INDICES {
matrix_cell.set(p_matched, m_cell.matched);
}
prev_p_score = p_score;
prev_m_score = m_cell.score;
}
}
fn populate_matrix<const INDICES: bool, N: Char>(&mut self, needle: &[N]) -> usize
where
H: PartialEq<N>,
{
let mut matrix_cells = &mut self.matrix_cells[self.current_row.len()..];
let mut row_iter = needle[1..]
.iter()
.copied()
.zip(self.row_offs[1..].iter().copied())
.enumerate();
let (mut needle_idx, (mut needle_char, mut row_off)) = row_iter.next().unwrap();
for (next_needle_idx, (next_needle_char, next_row_off)) in row_iter {
Self::score_row::<false, INDICES, _>(
self.current_row,
matrix_cells,
self.haystack,
self.bonus,
row_off,
next_row_off,
needle_idx as u16 + 1,
needle_char,
next_needle_char,
0,
);
let len = self.current_row.len() + needle_idx + 1 - row_off as usize;
matrix_cells = &mut matrix_cells[len..];
(needle_idx, needle_char, row_off) = (next_needle_idx, next_needle_char, next_row_off);
}
matrix_cells.as_ptr() as usize - self.matrix_cells.as_ptr() as usize
}
fn reconstruct_optimal_path(
&self,
max_score_end: u16,
indices: &mut Vec<u32>,
matrix_len: usize,
start: u32,
) {
let indices_start = indices.len();
indices.resize(indices_start + self.row_offs.len(), 0);
let indices = &mut indices[indices_start..];
let last_row_off = *self.row_offs.last().unwrap();
indices[self.row_offs.len() - 1] = start + max_score_end as u32 + last_row_off as u32;
let mut matrix_cells = &self.matrix_cells[..matrix_len];
let width = self.current_row.len();
let mut row_iter = self.row_offs[..self.row_offs.len() - 1]
.iter()
.copied()
.enumerate()
.rev()
.map(|(i, off)| {
let relative_off = off as usize - i;
let row;
(matrix_cells, row) =
matrix_cells.split_at(matrix_cells.len() - (width - relative_off));
(i, off, row)
});
let (mut row_idx, mut row_off, mut row) = row_iter.next().unwrap();
let mut col = max_score_end;
let relative_last_row_off = last_row_off as usize + 1 - self.row_offs.len();
let mut matched = self.current_row[col as usize + relative_last_row_off].matched;
col += last_row_off - row_off - 1;
loop {
if matched {
indices[row_idx] = start + col as u32 + row_off as u32;
}
let next_matched = row[col as usize].get(matched);
if matched {
let Some((next_row_idx, next_row_off, next_row)) = row_iter.next() else {
break;
};
col += row_off - next_row_off;
(row_idx, row_off, row) = (next_row_idx, next_row_off, next_row)
}
col -= 1;
matched = next_matched;
}
}
}