use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OperationCostsF64 {
pub match_cost: f64,
pub substitution: f64,
pub insertion: f64,
pub deletion: f64,
pub transposition: f64,
pub split: f64,
pub merge: f64,
}
impl OperationCostsF64 {
#[inline]
pub const fn standard() -> Self {
Self {
match_cost: 0.0,
substitution: 1.0,
insertion: 1.0,
deletion: 1.0,
transposition: 1.0,
split: 1.0,
merge: 1.0,
}
}
#[inline]
pub const fn typo_friendly() -> Self {
Self {
match_cost: 0.0,
substitution: 1.2,
insertion: 1.0,
deletion: 1.0,
transposition: 0.5, split: 1.5,
merge: 1.5,
}
}
#[inline]
pub const fn ocr_friendly() -> Self {
Self {
match_cost: 0.0,
substitution: 0.8, insertion: 1.0,
deletion: 1.0,
transposition: 1.2, split: 1.5,
merge: 1.5,
}
}
pub fn custom(
substitution: f64,
insertion: f64,
deletion: f64,
transposition: f64,
split: f64,
merge: f64,
) -> Self {
assert!(
substitution >= 0.0,
"Substitution cost must be non-negative"
);
assert!(insertion >= 0.0, "Insertion cost must be non-negative");
assert!(deletion >= 0.0, "Deletion cost must be non-negative");
assert!(
transposition >= 0.0,
"Transposition cost must be non-negative"
);
assert!(split >= 0.0, "Split cost must be non-negative");
assert!(merge >= 0.0, "Merge cost must be non-negative");
Self {
match_cost: 0.0, substitution,
insertion,
deletion,
transposition,
split,
merge,
}
}
pub fn is_valid(&self) -> bool {
self.match_cost == 0.0
&& self.substitution >= 0.0
&& self.insertion >= 0.0
&& self.deletion >= 0.0
&& self.transposition >= 0.0
&& self.split >= 0.0
&& self.merge >= 0.0
}
pub fn min_nonzero_cost(&self) -> f64 {
let costs = [
self.substitution,
self.insertion,
self.deletion,
self.transposition,
self.split,
self.merge,
];
costs
.iter()
.copied()
.filter(|&c| c > 0.0)
.min_by(|a, b| {
a.partial_cmp(b)
.expect("OperationCostsF64: costs are finite (filtered > 0.0)")
})
.unwrap_or(1.0)
}
pub fn is_standard(&self) -> bool {
const EPSILON: f64 = 1e-10;
(self.match_cost - 0.0).abs() < EPSILON
&& (self.substitution - 1.0).abs() < EPSILON
&& (self.insertion - 1.0).abs() < EPSILON
&& (self.deletion - 1.0).abs() < EPSILON
&& (self.transposition - 1.0).abs() < EPSILON
&& (self.split - 1.0).abs() < EPSILON
&& (self.merge - 1.0).abs() < EPSILON
}
}
impl Default for OperationCostsF64 {
fn default() -> Self {
Self::standard()
}
}
impl fmt::Display for OperationCostsF64 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"OperationCosts(sub={:.2}, ins={:.2}, del={:.2}, trans={:.2}, split={:.2}, merge={:.2})",
self.substitution,
self.insertion,
self.deletion,
self.transposition,
self.split,
self.merge
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-10;
#[test]
fn test_standard_costs() {
let costs = OperationCostsF64::standard();
assert!((costs.match_cost - 0.0).abs() < EPSILON);
assert!((costs.substitution - 1.0).abs() < EPSILON);
assert!((costs.insertion - 1.0).abs() < EPSILON);
assert!((costs.deletion - 1.0).abs() < EPSILON);
assert!((costs.transposition - 1.0).abs() < EPSILON);
assert!((costs.split - 1.0).abs() < EPSILON);
assert!((costs.merge - 1.0).abs() < EPSILON);
assert!(costs.is_valid());
assert!(costs.is_standard());
}
#[test]
fn test_typo_friendly() {
let costs = OperationCostsF64::typo_friendly();
assert!(costs.transposition < costs.substitution);
assert!(costs.is_valid());
assert!(!costs.is_standard());
}
#[test]
fn test_custom_costs() {
let costs = OperationCostsF64::custom(1.5, 1.0, 0.8, 0.5, 2.0, 2.0);
assert!((costs.substitution - 1.5).abs() < EPSILON);
assert!((costs.deletion - 0.8).abs() < EPSILON);
assert!((costs.transposition - 0.5).abs() < EPSILON);
assert!(costs.is_valid());
}
#[test]
#[should_panic(expected = "Substitution cost must be non-negative")]
fn test_negative_substitution_panics() {
OperationCostsF64::custom(-0.5, 1.0, 1.0, 1.0, 1.0, 1.0);
}
#[test]
fn test_min_nonzero_cost() {
let costs = OperationCostsF64::custom(1.5, 1.0, 0.8, 0.3, 2.0, 2.0);
assert!((costs.min_nonzero_cost() - 0.3).abs() < EPSILON);
}
#[test]
fn test_display() {
let costs = OperationCostsF64::standard();
let s = format!("{}", costs);
assert!(s.contains("sub=1.00"));
assert!(s.contains("ins=1.00"));
}
}