use std::collections::{HashSet, VecDeque};
use super::helpers::normalize_loop_text;
pub const TEXT_LOOP_RING_CAP: usize = 8;
pub const TEXT_LOOP_TRIP_AT: usize = 3;
const NEAR_DUPLICATE_JACCARD: f64 = 0.8;
const NEAR_DUPLICATE_OVERLAP: f64 = 0.8;
const NEAR_DUPLICATE_MIN_WORDS: usize = 3;
pub fn near_duplicate(a: &str, b: &str) -> bool {
let na = normalize_loop_text(a);
let nb = normalize_loop_text(b);
if na.is_empty() || nb.is_empty() {
return false;
}
if na == nb {
return true;
}
let wa: HashSet<&str> = na.split(' ').collect();
let wb: HashSet<&str> = nb.split(' ').collect();
if wa.len() < NEAR_DUPLICATE_MIN_WORDS || wb.len() < NEAR_DUPLICATE_MIN_WORDS {
return false;
}
let inter = wa.intersection(&wb).count() as f64;
let union = wa.union(&wb).count() as f64;
let min_len = wa.len().min(wb.len()) as f64;
(union > 0.0 && inter / union >= NEAR_DUPLICATE_JACCARD)
|| (min_len > 0.0 && inter / min_len >= NEAR_DUPLICATE_OVERLAP)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextLoopAction {
Continue,
Nudge,
Abort,
}
#[derive(Debug, Default)]
pub struct OutgoingTextRing {
ring: VecDeque<String>,
nudged: bool,
}
impl OutgoingTextRing {
pub fn record_and_check(&mut self, text: &str) -> TextLoopAction {
self.ring.push_back(text.to_string());
while self.ring.len() > TEXT_LOOP_RING_CAP {
self.ring.pop_front();
}
let hits = self.ring.iter().filter(|t| near_duplicate(text, t)).count();
if hits >= TEXT_LOOP_TRIP_AT {
if self.nudged {
TextLoopAction::Abort
} else {
self.nudged = true;
TextLoopAction::Nudge
}
} else {
TextLoopAction::Continue
}
}
pub fn last_recorded(&self) -> Option<&str> {
self.ring.back().map(String::as_str)
}
}