use kodama::Method;
use std::collections::HashMap;
use std::convert::TryInto;
use std::str::FromStr;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub enum Blocking {
Dense,
QGram {
tau: f64,
},
}
impl Default for Blocking {
fn default() -> Self {
Self::Dense
}
}
pub(super) struct CosineData {
pub(super) vocab: Arc<HashMap<String, u32>>,
pub(super) idf: Arc<Vec<f32>>,
pub(super) positional: bool,
}
pub struct Config<V> {
pub(super) threshold: Threshold,
pub(super) method: Method,
#[allow(clippy::type_complexity)]
pub(super) compare: Box<dyn Fn(&V, &V) -> crate::Distance + Send + Sync>,
pub(super) normalize: Box<dyn Fn(&str) -> String + Send + Sync>,
pub(super) blocking: Blocking,
pub(super) cosine: Option<CosineData>,
}
impl<V: AsRef<str>> Config<V> {
pub fn jaro_winkler(threshold: Threshold) -> Self {
Config {
threshold,
method: Method::Complete,
compare: Box::new(|a, b| {
crate::Distance::clamped(1.0 - jaro_winkler::jaro_winkler(a.as_ref(), b.as_ref()))
}),
normalize: Box::new(crate::normalize::identity),
blocking: Blocking::default(),
cosine: None,
}
}
pub fn with_normalizer<F>(mut self, normalize: F) -> Self
where
F: Fn(&str) -> String + Send + Sync + 'static,
{
self.normalize = Box::new(normalize);
self
}
pub fn with_compare<F>(mut self, compare: F) -> Self
where
F: Fn(&V, &V) -> crate::Distance + Send + Sync + 'static,
{
self.compare = Box::new(compare);
self.cosine = None;
self
}
pub fn with_blocking(mut self, tau: f64) -> Self {
self.blocking = Blocking::QGram { tau };
self
}
pub fn without_blocking(mut self) -> Self {
self.blocking = Blocking::Dense;
self
}
pub fn token_cosine(corpus: &[V], threshold: Threshold) -> Self {
let (vocab, idf) = crate::tokens::build_idf(corpus);
let vocab = Arc::new(vocab);
let idf = Arc::new(idf);
let compare = {
let vocab = Arc::clone(&vocab);
let idf = Arc::clone(&idf);
move |a: &V, b: &V| -> crate::Distance {
let va = crate::tokens::vectorize(a.as_ref(), &vocab, &idf);
let vb = crate::tokens::vectorize(b.as_ref(), &vocab, &idf);
crate::Distance::clamped(1.0 - crate::tokens::sparse_cosine(&va, &vb))
}
};
Config {
threshold,
method: Method::Complete,
compare: Box::new(compare),
normalize: Box::new(crate::normalize::identity),
blocking: Blocking::default(),
cosine: Some(CosineData {
vocab,
idf,
positional: false,
}),
}
}
pub fn token_cosine_positional(corpus: &[V], threshold: Threshold) -> Self {
let (vocab, idf) = crate::tokens::build_idf(corpus);
let vocab = Arc::new(vocab);
let idf = Arc::new(idf);
let compare = {
let vocab = Arc::clone(&vocab);
let idf = Arc::clone(&idf);
move |a: &V, b: &V| -> crate::Distance {
let va = crate::tokens::vectorize_positional(a.as_ref(), &vocab, &idf);
let vb = crate::tokens::vectorize_positional(b.as_ref(), &vocab, &idf);
crate::Distance::clamped(1.0 - crate::tokens::sparse_cosine(&va, &vb))
}
};
Config {
threshold,
method: Method::Complete,
compare: Box::new(compare),
normalize: Box::new(crate::normalize::identity),
blocking: Blocking::default(),
cosine: Some(CosineData {
vocab,
idf,
positional: true,
}),
}
}
}
#[derive(Debug, Clone)]
pub struct Threshold(f64);
impl Threshold {
pub(super) fn within(&self, dissimilarity: f32) -> bool {
let dissimilarity = dissimilarity as f64;
dissimilarity <= self.0
}
pub(super) fn value(&self) -> f64 {
self.0
}
}
impl std::fmt::Display for Threshold {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Threshold {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
f64::from_str(s)
.map_err(|e| format!("{}", e))
.and_then(|v| v.try_into())
}
}
impl Default for Threshold {
fn default() -> Self {
Threshold(0.25)
}
}
impl std::convert::TryFrom<f64> for Threshold {
type Error = String;
fn try_from(input: f64) -> Result<Self, Self::Error> {
if !(0.0..=1.0).contains(&input) {
Err("Threshold must be between 0 and 1".to_string())
} else {
Ok(Threshold(input))
}
}
}