use crate::word::Word;
struct Peq<W: Word> {
table: [W; 256],
m: u32,
}
impl<W: Word> Peq<W> {
fn new(pattern: &[u8]) -> Option<Self> {
let m = u32::try_from(pattern.len())
.ok()
.filter(|&m| m <= W::BITS)?;
Some(Self::fill(pattern.iter().copied(), m))
}
fn reversed(&self, pattern: &[u8]) -> Self {
debug_assert_eq!(pattern.len(), self.m as usize, "not this table's pattern");
Self::fill(pattern.iter().rev().copied(), self.m)
}
fn fill(pattern: impl Iterator<Item = u8>, m: u32) -> Self {
let mut table = [W::ZERO; 256];
for (i, c) in pattern.enumerate() {
#[allow(clippy::cast_possible_truncation)]
let bit = W::ONE.shl(i as u32);
table[usize::from(c)] = table[usize::from(c)].or(bit);
}
Self { table, m }
}
}
#[derive(Clone, Copy)]
struct Column<W: Word> {
pv: W,
mv: W,
top: W,
}
impl<W: Word> Column<W> {
fn new(m: u32) -> Self {
Self {
pv: W::ONES,
mv: W::ZERO,
top: W::ONE.shl(m - 1),
}
}
#[inline]
fn step(&mut self, eq: W, charge_start: bool) -> i32 {
let xv = eq.or(self.mv);
let xh = eq.and(self.pv).wrapping_add(self.pv).xor(self.pv).or(eq);
let ph = self.mv.or(xh.or(self.pv).not());
let mh = self.pv.and(xh);
let delta = if !ph.and(self.top).is_zero() {
1
} else if !mh.and(self.top).is_zero() {
-1
} else {
0
};
let ph = if charge_start {
ph.shl(1).or(W::ONE)
} else {
ph.shl(1)
};
let mh = mh.shl(1);
self.pv = mh.or(xv.or(ph).not());
self.mv = ph.and(xv);
delta
}
}
#[must_use]
#[allow(clippy::missing_inline_in_public_items)]
pub fn distance_in<W: Word>(pattern: &[u8], text: &[u8]) -> Option<u32> {
let peq = Peq::<W>::new(pattern)?;
if peq.m == 0 {
return u32::try_from(text.len()).ok();
}
let mut col = Column::new(peq.m);
let mut score = i64::from(peq.m);
for &c in text {
score += i64::from(col.step(peq.table[usize::from(c)], true));
}
u32::try_from(score).ok()
}
#[must_use]
#[allow(clippy::missing_inline_in_public_items)]
pub fn search<'t, W: Word>(
pattern: &'t [u8],
text: &'t [u8],
max_dist: u32,
) -> Option<Search<'t, W>> {
let peq = Peq::<W>::new(pattern)?;
if peq.m == 0 {
return None;
}
let m = i64::from(peq.m);
let col = Column::new(peq.m);
Some(Search {
pattern,
peq,
col,
score: m,
text,
pos: 0,
max_dist: i64::from(max_dist),
})
}
#[must_use]
#[allow(clippy::missing_inline_in_public_items)]
pub fn substring_distance<W: Word>(pattern: &[u8], text: &[u8]) -> Option<u32> {
let peq = Peq::<W>::new(pattern)?;
if peq.m == 0 {
return Some(0);
}
let mut col = Column::new(peq.m);
let mut score = i64::from(peq.m);
let mut best = score;
for &c in text {
score += i64::from(col.step(peq.table[usize::from(c)], false));
best = best.min(score);
}
u32::try_from(best).ok()
}
#[must_use = "iterators are lazy; this one has not read a byte"]
pub struct Search<'t, W: Word> {
pattern: &'t [u8],
peq: Peq<W>,
col: Column<W>,
score: i64,
text: &'t [u8],
pos: usize,
max_dist: i64,
}
impl<W: Word> core::fmt::Debug for Search<'_, W> {
#[allow(clippy::missing_inline_in_public_items)]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Search")
.field("pos", &self.pos)
.field("text_len", &self.text.len())
.field("score", &self.score)
.field("max_dist", &self.max_dist)
.finish_non_exhaustive()
}
}
impl<W: Word> Iterator for Search<'_, W> {
type Item = (usize, u32);
#[inline]
fn next(&mut self) -> Option<(usize, u32)> {
while let Some(&c) = self.text.get(self.pos) {
self.pos += 1;
self.score += i64::from(self.col.step(self.peq.table[usize::from(c)], false));
if self.score <= self.max_dist {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
return Some((self.pos, self.score as u32));
}
}
None
}
}
impl<W: Word> core::iter::FusedIterator for Search<'_, W> {}
impl<'t, W: Word> Search<'t, W> {
#[inline]
pub fn occurrences(self) -> Occurrences<'t, W> {
let reversed = self.peq.reversed(self.pattern);
Occurrences {
search: self,
reversed,
best: None,
run_end: 0,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Occurrence {
start: usize,
end: usize,
distance: u32,
}
impl Occurrence {
#[must_use]
#[inline]
pub const fn start(&self) -> usize {
self.start
}
#[must_use]
#[inline]
pub const fn end(&self) -> usize {
self.end
}
#[must_use]
#[inline]
pub const fn distance(&self) -> u32 {
self.distance
}
#[must_use]
#[inline]
pub const fn range(&self) -> core::ops::Range<usize> {
self.start..self.end
}
}
#[must_use = "iterators are lazy; this one has not read a byte"]
pub struct Occurrences<'t, W: Word> {
search: Search<'t, W>,
reversed: Peq<W>,
best: Option<(usize, u32)>,
run_end: usize,
}
impl<W: Word> Occurrences<'_, W> {
fn finish(&self, (end, distance): (usize, u32)) -> Occurrence {
let m = self.reversed.m as usize;
let lo = end.saturating_sub(m + distance as usize);
let mut col = Column::new(self.reversed.m);
let mut score = i64::from(self.reversed.m);
let (mut best, mut len) = (score, 0usize);
for (j, &c) in self.search.text[lo..end].iter().rev().enumerate() {
score += i64::from(col.step(self.reversed.table[usize::from(c)], true));
let j = j + 1;
if score < best || (score == best && j.abs_diff(m) < len.abs_diff(m)) {
(best, len) = (score, j);
}
}
debug_assert_eq!(
best,
i64::from(distance),
"the walk back disagrees with the walk forward"
);
Occurrence {
start: end - len,
end,
distance,
}
}
}
impl<W: Word> Iterator for Occurrences<'_, W> {
type Item = Occurrence;
#[inline]
fn next(&mut self) -> Option<Occurrence> {
for (end, distance) in self.search.by_ref() {
match self.best {
Some((_, d)) if end == self.run_end + 1 => {
if distance < d {
self.best = Some((end, distance));
}
self.run_end = end;
}
_ => {
let done = self.best.replace((end, distance));
self.run_end = end;
if let Some(done) = done {
return Some(self.finish(done));
}
}
}
}
self.best.take().map(|done| self.finish(done))
}
}
impl<W: Word> core::iter::FusedIterator for Occurrences<'_, W> {}
impl<W: Word> core::fmt::Debug for Occurrences<'_, W> {
#[allow(clippy::missing_inline_in_public_items)]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Occurrences")
.field("search", &self.search)
.field("best", &self.best)
.finish_non_exhaustive()
}
}
#[must_use]
#[doc(alias("levenshtein", "edit distance"))]
#[allow(clippy::missing_inline_in_public_items)]
pub fn distance(pattern: &[u8], text: &[u8]) -> Option<u32> {
match pattern.len() {
0..=64 => distance_in::<u64>(pattern, text),
65..=128 => distance_in::<u128>(pattern, text),
129..=256 => distance_in::<crate::Wide<4>>(pattern, text),
257..=512 => distance_in::<crate::Wide<8>>(pattern, text),
_ => None,
}
}