extern crate alloc;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
use core::marker::PhantomData;
pub trait Similarity {
type Value: ?Sized;
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64;
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EditDistanceError {
InputTooLong {
maximum: usize,
observed: usize,
},
}
impl From<EditDistanceError> for EvidenceError {
fn from(value: EditDistanceError) -> Self {
match value {
EditDistanceError::InputTooLong { maximum, observed } => {
Self::InputTooLong { maximum, observed }
}
}
}
}
impl fmt::Display for EditDistanceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::InputTooLong { maximum, observed } => write!(
formatter,
"similarity input length {observed} exceeds maximum {maximum}"
),
}
}
}
impl core::error::Error for EditDistanceError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct EditDistance {
maximum_length: usize,
}
impl EditDistance {
#[must_use]
pub const fn new(maximum_length: usize) -> Self {
Self { maximum_length }
}
#[must_use]
pub const fn maximum_length(&self) -> usize {
self.maximum_length
}
#[must_use]
pub fn normalized_length(&self, input: &str) -> usize {
normalize_text(input, usize::MAX).map_or(0, |normalized| normalized.len())
}
pub fn try_score(&self, left: &str, right: &str) -> Result<f64, EditDistanceError> {
let left = normalize_text(left, self.maximum_length)?;
let right = normalize_text(right, self.maximum_length)?;
Ok(sequence_similarity(&left, &right))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvidenceError {
DimensionMismatch {
left: usize,
right: usize,
},
ZeroMagnitude,
NonFinite,
InputTooLong {
maximum: usize,
observed: usize,
},
CollectionTooLong {
maximum: usize,
observed: usize,
},
InsufficientEvidence {
total_weight: u64,
},
Composition(WeightedError),
}
impl fmt::Display for EvidenceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::DimensionMismatch { left, right } => write!(
formatter,
"cosine vectors differ in length: {left} and {right}"
),
Self::ZeroMagnitude => formatter.write_str("cosine vector has zero magnitude"),
Self::NonFinite => formatter.write_str("cosine vector is not finite"),
Self::InputTooLong { maximum, observed } => write!(
formatter,
"similarity input length {observed} exceeds maximum {maximum}"
),
Self::CollectionTooLong { maximum, observed } => write!(
formatter,
"similarity collection length {observed} exceeds maximum {maximum}"
),
Self::InsufficientEvidence { total_weight } => write!(
formatter,
"weighted similarity has no effective evidence (total weight {total_weight})"
),
Self::Composition(inner) => {
write!(formatter, "weighted similarity composition: {inner}")
}
}
}
}
impl core::error::Error for EvidenceError {}
impl From<WeightedError> for EvidenceError {
fn from(value: WeightedError) -> Self {
Self::Composition(value)
}
}
impl From<CosineError> for EvidenceError {
fn from(value: CosineError) -> Self {
match value {
CosineError::DimensionMismatch { left, right } => {
Self::DimensionMismatch { left, right }
}
CosineError::ZeroMagnitude => Self::ZeroMagnitude,
CosineError::NonFinite => Self::NonFinite,
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ComponentOutcome {
index: usize,
weight: f64,
score: Option<f64>,
reason: Option<EvidenceError>,
}
impl ComponentOutcome {
#[must_use]
pub const fn index(&self) -> usize {
self.index
}
#[must_use]
pub const fn weight(&self) -> f64 {
self.weight
}
#[must_use]
pub const fn score(&self) -> Option<f64> {
self.score
}
#[must_use]
pub const fn reason(&self) -> Option<&EvidenceError> {
self.reason.as_ref()
}
#[must_use]
pub const fn is_applicable(&self) -> bool {
self.score.is_some() && self.reason.is_none()
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Evidence {
Measured {
value: f64,
},
Refused {
reason: EvidenceError,
},
}
impl Evidence {
#[must_use]
pub const fn measured(value: f64) -> Self {
Self::Measured { value }
}
#[must_use]
pub const fn refused(reason: EvidenceError) -> Self {
Self::Refused { reason }
}
#[must_use]
pub const fn is_applicable(&self) -> bool {
matches!(self, Self::Measured { .. })
}
#[must_use]
pub const fn value(&self) -> Option<f64> {
match *self {
Self::Measured { value } => Some(value),
Self::Refused { .. } => None,
}
}
}
impl Similarity for EditDistance {
type Value = str;
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64 {
self.try_score(left, right).unwrap_or(0.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Jaccard<T = String> {
marker: PhantomData<fn() -> T>,
}
impl<T> Jaccard<T> {
#[must_use]
pub const fn new() -> Self {
Self {
marker: PhantomData,
}
}
}
impl<T> Default for Jaccard<T> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct BoundedJaccard<T = String> {
maximum_length: usize,
marker: PhantomData<fn() -> T>,
}
impl<T> BoundedJaccard<T> {
#[must_use]
pub const fn new(maximum_length: usize) -> Self {
Self {
maximum_length,
marker: PhantomData,
}
}
#[must_use]
pub const fn maximum_length(&self) -> usize {
self.maximum_length
}
}
impl<T> Default for BoundedJaccard<T> {
fn default() -> Self {
Self::new(256)
}
}
fn bounded_jaccard_score<T: PartialEq>(
maximum_length: usize,
left: &[T],
right: &[T],
) -> Result<f64, EvidenceError> {
let observed = left.len().max(right.len());
if observed > maximum_length {
let refusal = Err(EvidenceError::CollectionTooLong {
maximum: maximum_length,
observed,
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "bounded_jaccard_score: returning an error to the caller");
return refusal;
}
Ok(jaccard_unit(left, right))
}
impl<T: PartialEq + Sync> CheckedSimilarity for BoundedJaccard<T> {
type Value = [T];
fn try_score(&self, left: &Self::Value, right: &Self::Value) -> Result<f64, EvidenceError> {
bounded_jaccard_score(self.maximum_length, left, right)
}
}
impl<T: PartialEq> Similarity for BoundedJaccard<T> {
type Value = [T];
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64 {
bounded_jaccard_score(self.maximum_length, left, right).unwrap_or(0.0)
}
}
impl<T: PartialEq> Similarity for Jaccard<T> {
type Value = [T];
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64 {
jaccard_unit(left, right)
}
}
fn jaccard_unit<T: PartialEq>(left: &[T], right: &[T]) -> f64 {
if left.is_empty() && right.is_empty() {
return 1.0;
}
if left.is_empty() || right.is_empty() {
return 0.0;
}
let left_unique = unique_count(left);
let right_unique = unique_count(right);
let intersection = left
.iter()
.enumerate()
.filter(|item| {
let index = item.0;
let value = item.1;
!left[..index].iter().any(|previous| previous == value)
&& right.iter().any(|candidate| candidate == value)
})
.fold(0.0, |count, _| count + 1.0);
let union = left_unique + right_unique - intersection;
unit_score(intersection / union)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct PathSimilarity;
impl PathSimilarity {
#[must_use]
pub const fn new() -> Self {
Self
}
}
impl Similarity for PathSimilarity {
type Value = str;
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64 {
let left = normalized_path(left);
let right = normalized_path(right);
sequence_similarity(&left, &right)
}
}
#[must_use]
pub fn is_exact_path_match(left: &str, right: &str) -> bool {
left == right
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct BoundingBox {
x: f64,
y: f64,
width: f64,
height: f64,
}
impl BoundingBox {
#[must_use]
pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
Self {
x,
y,
width,
height,
}
}
#[must_use]
pub const fn x(&self) -> f64 {
self.x
}
#[must_use]
pub const fn y(&self) -> f64 {
self.y
}
#[must_use]
pub const fn width(&self) -> f64 {
self.width
}
#[must_use]
pub const fn height(&self) -> f64 {
self.height
}
#[must_use]
pub const fn to_array(&self) -> [f64; 4] {
[self.x, self.y, self.width, self.height]
}
}
impl From<[f64; 4]> for BoundingBox {
fn from(values: [f64; 4]) -> Self {
Self::new(values[0], values[1], values[2], values[3])
}
}
impl From<BoundingBox> for [f64; 4] {
fn from(value: BoundingBox) -> Self {
value.to_array()
}
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct Geometry {
maximum_distance: f64,
}
impl Geometry {
#[must_use]
pub const fn new(maximum_distance: f64) -> Self {
Self { maximum_distance }
}
#[must_use]
pub const fn maximum_distance(&self) -> f64 {
self.maximum_distance
}
#[must_use]
pub fn score<Value>(&self, left: &Value, right: &Value) -> f64
where
Value: Copy + Into<[f64; 4]>,
{
let left = (*left).into();
let right = (*right).into();
geometry_score(self.maximum_distance, &left, &right)
}
}
impl Similarity for Geometry {
type Value = [f64; 4];
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64 {
geometry_score(self.maximum_distance, left, right)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum CosineError {
DimensionMismatch {
left: usize,
right: usize,
},
ZeroMagnitude,
NonFinite,
}
impl fmt::Display for CosineError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::DimensionMismatch { left, right } => write!(
formatter,
"cosine vectors differ in length: {left} and {right}"
),
Self::ZeroMagnitude => formatter.write_str("cosine vector has zero magnitude"),
Self::NonFinite => formatter.write_str("cosine vector is not finite"),
}
}
}
impl core::error::Error for CosineError {}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Cosine;
impl Cosine {
#[must_use]
pub const fn new() -> Self {
Self
}
pub fn try_score(&self, left: &[f32], right: &[f32]) -> Result<f64, CosineError> {
if left.len() != right.len() {
let refusal = Err(CosineError::DimensionMismatch {
left: left.len(),
right: right.len(),
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "try_score: returning an error to the caller");
return refusal;
}
let mut dot = 0.0_f64;
let mut left_squared = 0.0_f64;
let mut right_squared = 0.0_f64;
for (left_value, right_value) in left.iter().zip(right) {
let left_value = f64::from(*left_value);
let right_value = f64::from(*right_value);
if !left_value.is_finite() || !right_value.is_finite() {
let refusal = Err(CosineError::NonFinite);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "try_score: returning an error to the caller");
return refusal;
}
dot += left_value * right_value;
left_squared += left_value * left_value;
right_squared += right_value * right_value;
}
if !dot.is_finite() || !left_squared.is_finite() || !right_squared.is_finite() {
let refusal = Err(CosineError::NonFinite);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "try_score: returning an error to the caller");
return refusal;
}
let magnitudes = left_squared.sqrt() * right_squared.sqrt();
if magnitudes <= 0.0 {
let refusal = Err(CosineError::ZeroMagnitude);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "try_score: returning an error to the caller");
return refusal;
}
Ok((dot / magnitudes).clamp(-1.0, 1.0))
}
pub fn normalized_score(&self, left: &[f32], right: &[f32]) -> Result<f64, EvidenceError> {
let raw = self.try_score(left, right)?;
Ok(unit_score((raw + 1.0) / 2.0))
}
}
impl Similarity for Cosine {
type Value = [f32];
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64 {
self.normalized_score(left, right).unwrap_or(0.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum WeightedError {
Empty,
InvalidWeight {
index: usize,
},
WeightSumExceedsOne,
InvalidThreshold,
WeightCountMismatch {
scorers: usize,
weights: usize,
},
}
impl fmt::Display for WeightedError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Self::Empty => formatter.write_str("weighted similarity needs a component"),
Self::InvalidWeight { index } => {
write!(
formatter,
"weighted similarity has invalid weight at {index}"
)
}
Self::WeightSumExceedsOne => {
formatter.write_str("weighted similarity weights must sum to at most 1.0")
}
Self::InvalidThreshold => {
formatter.write_str("weighted similarity threshold must be in [0.0, 1.0]")
}
Self::WeightCountMismatch { scorers, weights } => write!(
formatter,
"weighted similarity has {scorers} scorers and {weights} weights"
),
}
}
}
impl core::error::Error for WeightedError {}
pub struct Weighted<Value: ?Sized> {
components: Vec<(f64, Box<dyn Similarity<Value = Value>>)>,
threshold: f64,
}
fn require_present(count: usize) -> Result<(), WeightedError> {
if count == 0 {
let refusal = Err(WeightedError::Empty);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "weighted scorer: there are no components");
return refusal;
}
Ok(())
}
fn validate_policy(
threshold: f64,
weights: impl Iterator<Item = f64>,
) -> Result<(), WeightedError> {
if !threshold.is_finite() || !(0.0..=1.0).contains(&threshold) {
let refusal = Err(WeightedError::InvalidThreshold);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), threshold, "weighted scorer: the threshold is outside [0, 1]");
return refusal;
}
let mut total = 0.0;
for (index, weight) in weights.enumerate() {
if !weight.is_finite() || weight < 0.0 {
let refusal = Err(WeightedError::InvalidWeight { index });
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), index, weight, "weighted scorer: the weight is not finite and non-negative");
return refusal;
}
total += weight;
}
if total > 1.0 {
let refusal = Err(WeightedError::WeightSumExceedsOne);
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), total, "weighted scorer: the weights sum past one");
return refusal;
}
Ok(())
}
impl<Value: ?Sized> Weighted<Value> {
pub fn new(
components: Vec<(f64, Box<dyn Similarity<Value = Value>>)>,
threshold: f64,
) -> Result<Self, WeightedError> {
require_present(components.len())?;
validate_policy(threshold, components.iter().map(|component| component.0))?;
Ok(Self {
components,
threshold,
})
}
#[must_use]
pub const fn threshold(&self) -> f64 {
self.threshold
}
#[must_use]
pub fn is_accepted(&self, left: &Value, right: &Value) -> bool {
self.score(left, right) >= self.threshold
}
#[must_use]
pub fn total_weight(&self) -> f64 {
self.components
.iter()
.fold(0.0, |sum, component| sum + component.0)
}
}
impl<Value: ?Sized> core::fmt::Debug for Weighted<Value> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Weighted")
.field("components", &self.components.len())
.field("weights", &WeightList(self))
.field("threshold", &self.threshold)
.finish_non_exhaustive()
}
}
struct WeightList<'a, Value: ?Sized>(&'a Weighted<Value>);
impl<Value: ?Sized> core::fmt::Debug for WeightList<'_, Value> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_list()
.entries(self.0.components.iter().map(|component| component.0))
.finish()
}
}
impl<Value: ?Sized> Similarity for Weighted<Value> {
type Value = Value;
fn score(&self, left: &Self::Value, right: &Self::Value) -> f64 {
let total = self.components.iter().fold(0.0, |sum, component| {
sum + (component.0 * unit_score(component.1.score(left, right)))
});
unit_score(total)
}
}
pub trait CheckedSimilarity {
type Value: ?Sized + Sync;
fn try_score(&self, left: &Self::Value, right: &Self::Value) -> Result<f64, EvidenceError>;
}
impl CheckedSimilarity for EditDistance {
type Value = str;
fn try_score(&self, left: &Self::Value, right: &Self::Value) -> Result<f64, EvidenceError> {
EditDistance::try_score(self, left, right).map_err(EvidenceError::from)
}
}
impl CheckedSimilarity for Cosine {
type Value = [f32];
fn try_score(&self, left: &Self::Value, right: &Self::Value) -> Result<f64, EvidenceError> {
Cosine::try_score(self, left, right).map_err(EvidenceError::from)
}
}
impl CheckedSimilarity for Geometry {
type Value = [f64; 4];
fn try_score(&self, left: &Self::Value, right: &Self::Value) -> Result<f64, EvidenceError> {
Ok(Similarity::score(self, left, right))
}
}
pub struct CheckedEvidence<Value: ?Sized> {
scorers: Vec<Box<dyn CheckedSimilarity<Value = Value> + Send + Sync>>,
weights: Vec<f64>,
threshold: f64,
}
impl<Value: ?Sized + Sync> CheckedEvidence<Value> {
pub fn new(
scorers: Vec<Box<dyn CheckedSimilarity<Value = Value> + Send + Sync>>,
weights: Vec<f64>,
threshold: f64,
) -> Result<Self, WeightedError> {
require_present(scorers.len())?;
if scorers.len() != weights.len() {
let refusal = Err(WeightedError::WeightCountMismatch {
scorers: scorers.len(),
weights: weights.len(),
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), scorers = scorers.len(), weights = weights.len(), "checked weighted scorer: every scorer needs exactly one weight");
return refusal;
}
validate_policy(threshold, weights.iter().copied())?;
Ok(Self {
scorers,
weights,
threshold,
})
}
#[must_use]
pub const fn threshold(&self) -> f64 {
self.threshold
}
#[must_use]
pub fn total_weight(&self) -> f64 {
self.weights.iter().fold(0.0, |sum, weight| sum + weight)
}
pub fn verdict(&self, left: &Value, right: &Value) -> Result<EvidenceVerdict, EvidenceError> {
let total_weight = self.total_weight();
if total_weight <= 0.0 {
let refusal = Err(EvidenceError::InsufficientEvidence {
total_weight: total_weight.to_bits(),
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "verdict: returning an error to the caller");
return refusal;
}
let mut outcomes = Vec::with_capacity(self.scorers.len());
let mut measured_total = 0.0;
let mut refusals = 0_usize;
for (index, scorer) in self.scorers.iter().enumerate() {
let weight = self.weights[index];
match scorer.try_score(left, right) {
Ok(score) => {
measured_total += weight * score;
outcomes.push(ComponentOutcome {
index,
weight,
score: Some(score),
reason: None,
});
}
Err(reason) => {
refusals = refusals.saturating_add(1);
outcomes.push(ComponentOutcome {
index,
weight,
score: None,
reason: Some(reason),
});
}
}
}
if refusals > 0 {
return Ok(EvidenceVerdict {
score: None,
is_accepted: false,
outcomes,
});
}
let score = unit_score(measured_total);
Ok(EvidenceVerdict {
score: Some(score),
is_accepted: score >= self.threshold,
outcomes,
})
}
}
impl<Value: ?Sized + Sync> core::fmt::Debug for CheckedEvidence<Value> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("CheckedEvidence")
.field("components", &self.scorers.len())
.field("weights", &self.weights)
.field("threshold", &self.threshold)
.finish()
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct EvidenceVerdict {
score: Option<f64>,
is_accepted: bool,
outcomes: Vec<ComponentOutcome>,
}
impl EvidenceVerdict {
#[must_use]
pub const fn score(&self) -> Option<f64> {
self.score
}
#[must_use]
pub const fn is_accepted(&self) -> bool {
self.is_accepted
}
#[must_use]
pub fn outcomes(&self) -> &[ComponentOutcome] {
&self.outcomes
}
#[must_use]
pub fn refusals(&self) -> Vec<&ComponentOutcome> {
self.outcomes
.iter()
.filter(|outcome| outcome.reason.is_some())
.collect()
}
}
fn unit_score(candidate: f64) -> f64 {
if candidate.is_finite() {
candidate.clamp(0.0, 1.0)
} else {
0.0
}
}
fn normalize_text(input: &str, maximum: usize) -> Result<Vec<char>, EditDistanceError> {
let mut observed = 0usize;
for _ in input.chars() {
observed = observed.saturating_add(1);
if observed > maximum {
let refusal = Err(EditDistanceError::InputTooLong { maximum, observed });
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "normalize_text: returning an error to the caller");
return refusal;
}
}
let mut normalized = Vec::with_capacity(observed);
let mut pending_space = false;
for character in input.chars() {
if character.is_whitespace() {
if !normalized.is_empty() {
pending_space = true;
}
continue;
}
if pending_space {
normalized.push(' ');
pending_space = false;
}
for lower in character.to_lowercase() {
normalized.push(lower);
}
}
if normalized.len() > maximum {
let refusal = Err(EditDistanceError::InputTooLong {
maximum,
observed: normalized.len(),
});
#[cfg(feature = "trace")]
crate::trace::debug!(error = ?refusal.as_ref().err(), "normalize_text: returning an error to the caller");
return refusal;
}
Ok(normalized)
}
fn sequence_similarity<T: PartialEq>(left: &[T], right: &[T]) -> f64 {
if left.is_empty() && right.is_empty() {
return 1.0;
}
if left.is_empty() || right.is_empty() {
return 0.0;
}
let (rows, columns) = if left.len() < right.len() {
(left, right)
} else {
(right, left)
};
let mut previous: Vec<f64> = Vec::with_capacity(columns.len().saturating_add(1));
let mut denominator = 0.0f64;
previous.push(0.0);
for _ in columns {
denominator += 1.0;
previous.push(denominator);
}
let mut current: Vec<f64> = vec![0.0; columns.len().saturating_add(1)];
let mut row_distance = 1.0;
for row_value in rows {
current[0] = row_distance;
for (column_index, column_value) in columns.iter().enumerate() {
let cost = if row_value == column_value { 0.0 } else { 1.0 };
let index = column_index.saturating_add(1);
let deletion = previous[index] + 1.0;
let insertion = current[column_index] + 1.0;
let substitution = previous[column_index] + cost;
current[index] = deletion.min(insertion).min(substitution);
}
core::mem::swap(&mut previous, &mut current);
row_distance += 1.0;
}
unit_score(1.0 - (previous[columns.len()] / denominator))
}
fn unique_count<T: PartialEq>(values: &[T]) -> f64 {
values
.iter()
.enumerate()
.filter(|item| {
let index = item.0;
let value = item.1;
!values[..index].iter().any(|previous| previous == value)
})
.fold(0.0, |count, _| count + 1.0)
}
fn normalized_path(path: &str) -> Vec<&str> {
let mut segments = Vec::new();
for part in path.split(|character: char| {
character == '/' || character == '\\' || character == '>' || character.is_whitespace()
}) {
for component in part.split(['#', '.']) {
let component = component.trim();
if !component.is_empty() && !is_volatile_segment(component) {
segments.push(component);
}
}
}
segments
}
fn is_volatile_segment(segment: &str) -> bool {
let bytes = segment.as_bytes();
if bytes.is_empty() || bytes.iter().all(|byte| byte.is_ascii_digit()) {
return true;
}
if bytes.len() >= 6 && bytes.iter().all(|byte| byte.is_ascii_hexdigit()) {
return true;
}
let suffix_is_numeric =
|suffix: &str| !suffix.is_empty() && suffix.bytes().all(|byte| byte.is_ascii_digit());
let suffix_is_hash = |suffix: &str| {
suffix.len() >= 6
&& suffix.bytes().all(|byte| byte.is_ascii_alphanumeric())
&& suffix.bytes().any(|byte| byte.is_ascii_digit())
};
if let Some((_, suffix)) = segment.rsplit_once(['-', '_'])
&& (suffix_is_numeric(suffix) || suffix_is_hash(suffix))
{
return true;
}
["css-", "sc-", "jsx-", "emotion-", "hash-"]
.iter()
.any(|prefix| {
segment.strip_prefix(prefix).is_some_and(|suffix| {
suffix.len() >= 4 && suffix.bytes().all(|byte| byte.is_ascii_alphanumeric())
})
})
}
fn geometry_score(maximum_distance: f64, left: &[f64; 4], right: &[f64; 4]) -> f64 {
let mut squared_distance = 0.0;
for (left_value, right_value) in left.iter().zip(right.iter()) {
let difference = *left_value - *right_value;
if !difference.is_finite() {
return 0.0;
}
squared_distance = difference.mul_add(difference, squared_distance);
if !squared_distance.is_finite() {
return 0.0;
}
}
let distance = squared_distance.sqrt();
if !distance.is_finite() {
return 0.0;
}
if maximum_distance.is_finite() && maximum_distance > 0.0 {
unit_score(1.0 - (distance / maximum_distance))
} else if distance <= f64::EPSILON {
1.0
} else {
0.0
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_unit(value: f64) {
assert!(value.is_finite(), "similarity must be finite: {value}");
assert!(
(0.0..=1.0).contains(&value),
"similarity outside [0, 1]: {value}"
);
}
fn assert_close(actual: f64, expected: f64) {
assert!(
(actual - expected).abs() < 1e-12,
"expected {expected}, got {actual}"
);
}
fn boxed_edit_distance(maximum_length: usize) -> Box<dyn Similarity<Value = str>> {
Box::new(EditDistance::new(maximum_length))
}
#[test]
fn edit_distance_handles_empty_and_identical_inputs() -> Result<(), EditDistanceError> {
let scorer = EditDistance::new(16);
assert_close(scorer.try_score("", "")?, 1.0);
assert_close(scorer.try_score("same", "same")?, 1.0);
assert_close(scorer.try_score("", "text")?, 0.0);
assert_close(scorer.try_score("text", "")?, 0.0);
assert_close(scorer.try_score(" SAME ", "same")?, 1.0);
Ok(())
}
#[test]
fn edit_distance_normalizes_known_distance() -> Result<(), EditDistanceError> {
let scorer = EditDistance::new(16);
let score = scorer.try_score("kitten", "sitting")?;
assert!((score - (4.0 / 7.0)).abs() < 1e-12, "score was {score}");
Ok(())
}
#[test]
fn edit_distance_refuses_over_limit_inputs() -> Result<(), EditDistanceError> {
let scorer = EditDistance::new(3);
assert_close(scorer.try_score("abc", "abc")?, 1.0);
assert!(matches!(
scorer.try_score("four", "four"),
Err(EditDistanceError::InputTooLong {
maximum: 3,
observed: 4
})
));
assert_close(scorer.score("four", "four"), 0.0);
assert!(
scorer.score("four", "four") <= 0.0,
"the infallible trait method must map refusal to no similarity"
);
Ok(())
}
#[test]
fn every_evidence_error_variant_is_exercised_by_a_test() {
let produced: Vec<EvidenceError> = vec![
EvidenceError::from(EditDistanceError::InputTooLong {
maximum: 1,
observed: 2,
}),
EvidenceError::from(CosineError::DimensionMismatch { left: 1, right: 2 }),
EvidenceError::from(CosineError::ZeroMagnitude),
EvidenceError::from(CosineError::NonFinite),
EvidenceError::CollectionTooLong {
maximum: 1,
observed: 2,
},
EvidenceError::InsufficientEvidence {
total_weight: 0.0_f64.to_bits(),
},
EvidenceError::from(WeightedError::Empty),
EvidenceError::from(WeightedError::InvalidWeight { index: 0 }),
EvidenceError::from(WeightedError::WeightSumExceedsOne),
EvidenceError::from(WeightedError::InvalidThreshold),
EvidenceError::from(WeightedError::WeightCountMismatch {
scorers: 2,
weights: 1,
}),
];
assert_eq!(
produced.len(),
11,
"every EvidenceError variant is produced exactly once"
);
for reason in &produced {
assert!(
!reason.to_string().is_empty(),
"{reason:?} must render a message"
);
}
assert_eq!(
EvidenceError::from(CosineError::ZeroMagnitude),
EvidenceError::ZeroMagnitude,
"a zero magnitude stays its own refusal through conversion"
);
assert_ne!(
EvidenceError::ZeroMagnitude.to_string(),
EvidenceError::NonFinite.to_string(),
"the two vector refusals must not render identically"
);
assert_ne!(
EvidenceError::InputTooLong {
maximum: 1,
observed: 2
}
.to_string(),
EvidenceError::CollectionTooLong {
maximum: 1,
observed: 2
}
.to_string(),
"the text and set budgets must name themselves apart"
);
}
#[test]
fn bounded_jaccard_refuses_before_the_quadratic_scan() -> Result<(), EvidenceError> {
let scorer = BoundedJaccard::<u32>::new(2);
assert_close(
CheckedSimilarity::try_score(&scorer, &[1, 2], &[2, 1])?,
1.0,
);
assert_eq!(
CheckedSimilarity::try_score(&scorer, &[1, 2, 3], &[1, 2]),
Err(EvidenceError::CollectionTooLong {
maximum: 2,
observed: 3
}),
"an over-budget set is refused with its limit, before dedup"
);
assert_close(Similarity::score(&scorer, &[], &[]), 1.0);
Ok(())
}
#[test]
fn the_normalized_budget_unit_is_the_expanded_scalar_count() -> Result<(), EditDistanceError> {
let tight = EditDistance::new(1);
assert_eq!(
tight.normalized_length("i"),
1,
"an ASCII scalar normalizes to itself"
);
assert_eq!(
tight.normalized_length("İ"),
2,
"`İ` lower-cases to two scalars, and that is the charged unit"
);
assert_eq!(
tight.try_score("İ", "i"),
Err(EditDistanceError::InputTooLong {
maximum: 1,
observed: 2
}),
"a budget of one refuses the expanded scalar, not the raw count"
);
let roomy = EditDistance::new(2);
assert_close(roomy.try_score("İ", "i")?, 0.5);
Ok(())
}
#[test]
fn jaccard_handles_boundary_sets() {
let scorer = Jaccard::<&str>::new();
assert_close(scorer.score(&[], &[]), 1.0);
assert_close(scorer.score(&["a", "b"], &["a", "b"]), 1.0);
assert_close(scorer.score(&["a"], &["b"]), 0.0);
assert_close(scorer.score(&["a", "a"], &["a"]), 1.0);
}
#[test]
fn cosine_scores_known_angles() -> Result<(), CosineError> {
let scorer = Cosine::new();
assert_close(scorer.try_score(&[1.0, 2.0, 3.0], &[2.0, 4.0, 6.0])?, 1.0);
assert_close(scorer.try_score(&[1.0, 0.0], &[0.0, 1.0])?, 0.0);
assert_close(scorer.try_score(&[1.0, 0.0], &[-1.0, 0.0])?, -1.0);
let diagonal = scorer.try_score(&[1.0, 0.0], &[1.0, 1.0])?;
assert!((diagonal - core::f64::consts::FRAC_1_SQRT_2).abs() < 1e-12);
Ok(())
}
#[test]
fn cosine_refuses_a_dimension_mismatch_rather_than_scoring_zero() {
let scorer = Cosine::new();
assert!(matches!(
scorer.try_score(&[1.0, 0.0], &[1.0, 0.0, 0.0]),
Err(CosineError::DimensionMismatch { left: 2, right: 3 })
));
assert_close(scorer.score(&[1.0, 0.0], &[1.0, 0.0, 0.0]), 0.0);
}
#[test]
fn cosine_refuses_a_zero_magnitude_vector() {
let scorer = Cosine::new();
assert!(matches!(
scorer.try_score(&[0.0, 0.0], &[1.0, 0.0]),
Err(CosineError::ZeroMagnitude)
));
assert!(matches!(
scorer.try_score(&[], &[]),
Err(CosineError::ZeroMagnitude)
));
}
#[test]
fn cosine_names_a_non_finite_vector_apart_from_a_zero_magnitude_one() {
let scorer = Cosine::new();
for (left, right) in [
(&[f32::NAN, 1.0][..], &[1.0, 1.0][..]),
(&[1.0, 1.0][..], &[f32::NAN, 1.0][..]),
(&[f32::INFINITY, 1.0][..], &[1.0, 1.0][..]),
(&[1.0, 1.0][..], &[1.0, f32::NEG_INFINITY][..]),
] {
assert!(
matches!(scorer.try_score(left, right), Err(CosineError::NonFinite)),
"a non-finite vector must be refused as NonFinite: {left:?} {right:?}"
);
}
assert_ne!(
CosineError::NonFinite.to_string(),
CosineError::ZeroMagnitude.to_string(),
"the two refusals must not render identically"
);
}
#[test]
fn cosine_never_returns_a_non_finite_score_from_the_infallible_method() {
let scorer = Cosine::new();
let score = scorer.score(&[f32::NAN, 1.0], &[1.0, 1.0]);
assert!(score.is_finite(), "the infallible method returned {score}");
assert_close(score, 0.0);
assert_close(scorer.score(&[f32::INFINITY, 1.0], &[1.0, 1.0]), 0.0);
}
#[test]
fn cosine_stays_inside_the_contract_interval() -> Result<(), CosineError> {
let scorer = Cosine::new();
let wide: Vec<f32> = (0_u16..64).map(f32::from).collect();
let narrow: Vec<f32> = wide.iter().map(|value| value * 0.5).collect();
for (left, right) in [(&wide, &narrow), (&narrow, &wide)] {
let score = scorer.try_score(left, right)?;
assert!(
(-1.0..=1.0).contains(&score),
"score {score} left the contract interval"
);
}
assert_close(scorer.try_score(&wide, &narrow)?, 1.0);
Ok(())
}
#[test]
fn path_similarity_discards_volatile_segments() {
let scorer = PathSimilarity::new();
assert_close(
scorer.score(
"html/body/div/123/button.css-a1b2c3",
"html/body/div/987/button.css-z9y8x7",
),
1.0,
);
assert_close(scorer.score("root/left", "root/right"), 0.5);
assert_close(scorer.score("", ""), 1.0);
assert_close(scorer.score("left", "right"), 0.0);
}
#[test]
fn geometry_maps_zero_and_maximum_distance() {
let scorer = Geometry::new(2.0);
let origin = [0.0, 0.0, 0.0, 0.0];
let far = [2.0, 0.0, 0.0, 0.0];
assert_close(scorer.score(&origin, &origin), 1.0);
assert_close(scorer.score(&origin, &far), 0.0);
assert_close(scorer.score(&origin, &[1.0, 0.0, 0.0, 0.0]), 0.5);
}
#[test]
fn bounding_box_accessors_preserve_coordinates() {
let box_value = BoundingBox::new(0.1, 0.2, 0.3, 0.4);
assert_close(box_value.x(), 0.1);
assert_close(box_value.y(), 0.2);
assert_close(box_value.width(), 0.3);
assert_close(box_value.height(), 0.4);
for (actual, expected) in box_value.to_array().iter().zip([0.1, 0.2, 0.3, 0.4]) {
assert_close(*actual, expected);
}
}
#[test]
fn weighted_rejects_empty_components() {
let result = Weighted::<str>::new(Vec::new(), 0.5);
assert!(matches!(result, Err(WeightedError::Empty)));
}
#[test]
fn weighted_single_vector_returns_that_vectors_score() -> Result<(), WeightedError> {
let direct = EditDistance::new(16);
let weighted = Weighted::<str>::new(vec![(1.0, boxed_edit_distance(16))], 0.8)?;
assert_close(
weighted.score("kitten", "sitting"),
direct.score("kitten", "sitting"),
);
assert!(!weighted.is_accepted("kitten", "sitting"));
Ok(())
}
#[test]
fn every_metric_stays_in_unit_interval_for_fixed_inputs() -> Result<(), WeightedError> {
let edit = EditDistance::new(32);
for (left, right) in [("", ""), ("a", "b"), ("short", "longer"), ("A B", "a b")] {
assert_unit(edit.score(left, right));
}
let sets = Jaccard::<&str>::new();
for (left, right) in [
(&[][..], &[][..]),
(&["a"][..], &["b"][..]),
(&["a", "b"][..], &["b", "c"][..]),
] {
assert_unit(sets.score(left, right));
}
let paths = PathSimilarity::new();
for (left, right) in [("", ""), ("root/1", "root/2"), ("a", "b/c")] {
assert_unit(paths.score(left, right));
}
let geometry = Geometry::new(1.0);
for (left, right) in [
([0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0]),
([0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0]),
([0.5, 0.5, 0.25, 0.25], [0.25, 0.25, 0.5, 0.5]),
] {
assert_unit(geometry.score(&left, &right));
}
let weighted = Weighted::<str>::new(vec![(1.0, boxed_edit_distance(32))], 0.5)?;
for (left, right) in [
("", ""),
("same", "same"),
("a", "b"),
("too long", "too long"),
] {
assert_unit(weighted.score(left, right));
}
Ok(())
}
}