use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
pub(crate) struct Tok {
pub start: usize,
pub end: usize,
word: usize,
seg: u32,
pub file: u32,
}
pub(crate) struct Tokens {
pub toks: Vec<Tok>,
buf: String,
}
impl Tokens {
pub(crate) fn new() -> Self {
Tokens {
toks: Vec::new(),
buf: String::new(),
}
}
fn word(&self, i: usize) -> &str {
let s = self.toks[i].word;
let e = self
.toks
.get(i + 1)
.map(|t| t.word)
.unwrap_or(self.buf.len());
&self.buf[s..e]
}
fn shingles_eq(&self, a: usize, b: usize, k: usize) -> bool {
(0..k).all(|d| self.word(a + d) == self.word(b + d))
}
}
pub(crate) fn tokenize_into(tokens: &mut Tokens, text: &str, file: u32, seg: u32) {
let mut in_word = false;
let mut start = 0usize;
let mut word = 0usize;
for (i, c) in text.char_indices() {
let c = if c == '\u{2019}' { '\'' } else { c };
if c.is_alphanumeric() || c == '\'' {
if !in_word {
start = i;
word = tokens.buf.len();
in_word = true;
}
for lc in c.to_lowercase() {
tokens.buf.push(lc);
}
} else if in_word {
tokens.toks.push(Tok {
start,
end: i,
word,
seg,
file,
});
in_word = false;
}
}
if in_word {
tokens.toks.push(Tok {
start,
end: text.len(),
word,
seg,
file,
});
}
}
fn shingle_hash(tokens: &Tokens, i: usize, k: usize) -> u64 {
let mut h = std::collections::hash_map::DefaultHasher::new();
for d in 0..k {
tokens.word(i + d).hash(&mut h);
}
h.finish()
}
pub(crate) struct Run {
pub earlier: usize,
pub later: usize,
pub len: usize,
}
pub(crate) fn find_runs(
tokens: &Tokens,
k: usize,
floor: usize,
cross_file_only: bool,
) -> Vec<Run> {
let toks = &tokens.toks;
let mut runs = Vec::new();
if k == 0 || toks.len() < k {
return runs;
}
const WALK_CAP: usize = 32;
const NIL: usize = usize::MAX;
let mut heads: HashMap<u64, usize> = HashMap::new();
let mut next: Vec<usize> = vec![NIL; toks.len()];
let mut i = 0usize;
while i + k <= toks.len() {
if toks[i + k - 1].seg != toks[i].seg {
i += 1; continue;
}
match heads.entry(shingle_hash(tokens, i, k)) {
Entry::Vacant(v) => {
v.insert(i);
i += 1;
}
Entry::Occupied(mut o) => {
let mut best: Option<(usize, usize, usize)> = None;
let mut e = *o.get();
let mut walked = 0usize;
loop {
let same_file = toks[e].file == toks[i].file;
let eligible = if cross_file_only {
!same_file
} else {
e + k <= i
};
if eligible && tokens.shingles_eq(e, i, k) {
let mut len = k;
while i + len < toks.len()
&& (!same_file || e + len < i)
&& tokens.word(e + len) == tokens.word(i + len)
&& toks[e + len].seg == toks[e].seg
&& toks[i + len].seg == toks[i].seg
{
len += 1;
}
let (mut es, mut s, mut len) = (e, i, len);
while es > 0
&& (!same_file || es + len < s)
&& tokens.word(es - 1) == tokens.word(s - 1)
&& toks[es - 1].seg == toks[es].seg
&& toks[s - 1].seg == toks[s].seg
{
es -= 1;
s -= 1;
len += 1;
}
if best.is_none_or(|(_, _, b)| len >= b) {
best = Some((es, s, len));
}
}
walked += 1;
if walked >= WALK_CAP || next[e] == NIL {
break;
}
e = next[e];
}
next[i] = *o.get();
o.insert(i);
match best {
Some((e, s, len)) if len >= floor => {
runs.push(Run {
earlier: e,
later: s,
len,
});
i = s + len;
}
_ => i += 1,
}
}
}
}
runs
}
pub(crate) fn cap_longest_first(runs: &mut Vec<Run>, cap: usize) {
runs.sort_by_key(|r| (std::cmp::Reverse(r.len), r.later, r.earlier));
runs.truncate(cap);
}