Compare commits
1
Commits
ddd3b7bbb9
...
de5d5cc62c
No files matched your search
@@ -1,13 +1,12 @@
|
|||||||
//! # Generalized Levenshtein Distance
|
//! # Generalized Levenshtein Distance
|
||||||
//! This is a simple generalized Levenshtein Distance implementation.
|
//! This is a simple generalized Levenshtein Distance implementation.
|
||||||
|
|
||||||
|
|
||||||
/// costs for different edit operations
|
/// costs for different edit operations
|
||||||
///
|
///
|
||||||
/// each with the letters involved: so you can further tweak the cost in ur own [`CustomCost`]s impl
|
/// each with the letters involved: so you can further tweak the cost in ur own [`CustomCost`]s impl
|
||||||
/// position? idk
|
/// position? idk
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum EditOperation<T>{
|
pub enum EditOperation<T> {
|
||||||
Insert { elem: T }, // Insert a character from the target string
|
Insert { elem: T }, // Insert a character from the target string
|
||||||
Delete { elem: T }, // Delete a character from the source string
|
Delete { elem: T }, // Delete a character from the source string
|
||||||
Substitute { from: T, to: T }, // Replace one character with another
|
Substitute { from: T, to: T }, // Replace one character with another
|
||||||
@@ -59,7 +58,7 @@ impl<T> CustomCost<T> for EditCost {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CostOperation<T> {
|
pub struct CostOperation<T> {
|
||||||
pub cost: i32,
|
pub cost: i32,
|
||||||
pub operation: Option<EditOperation<T>>,
|
pub operation: EditOperation<T>,
|
||||||
}
|
}
|
||||||
// absolutely phenomenal naming
|
// absolutely phenomenal naming
|
||||||
use std::fmt::{Debug, Display};
|
use std::fmt::{Debug, Display};
|
||||||
@@ -89,7 +88,7 @@ pub fn generalized_levenshtein<T, F>(
|
|||||||
seq1: &[T],
|
seq1: &[T],
|
||||||
seq2: &[T],
|
seq2: &[T],
|
||||||
cost_calc: &F,
|
cost_calc: &F,
|
||||||
) -> (i32, Vec<Vec<CostOperation<T>>>)
|
) -> (i32, Vec<Vec<Option<CostOperation<T>>>>)
|
||||||
where
|
where
|
||||||
T: PartialEq + Clone + Debug,
|
T: PartialEq + Clone + Debug,
|
||||||
F: CustomCost<T>,
|
F: CustomCost<T>,
|
||||||
@@ -97,40 +96,32 @@ where
|
|||||||
let len1 = seq1.len();
|
let len1 = seq1.len();
|
||||||
let len2 = seq2.len();
|
let len2 = seq2.len();
|
||||||
|
|
||||||
let mut dp = vec![
|
let mut dp = vec![vec![None; len2 + 1]; len1 + 1];
|
||||||
vec![
|
|
||||||
CostOperation {
|
|
||||||
cost: 0,
|
|
||||||
operation: None
|
|
||||||
};
|
|
||||||
len2 + 1
|
|
||||||
];
|
|
||||||
len1 + 1
|
|
||||||
];
|
|
||||||
|
|
||||||
// Fill the base cases
|
// Fill the base cases
|
||||||
for i in 0..=len1 {
|
for i in 1..=len1 {
|
||||||
dp[i][0] = CostOperation {
|
dp[i][0] = if i > 0 {
|
||||||
|
Some(CostOperation {
|
||||||
cost: i as i32,
|
cost: i as i32,
|
||||||
operation: if i > 0 {
|
operation: EditOperation::Delete {
|
||||||
Some(EditOperation::Delete {
|
|
||||||
elem: seq1[i - 1].clone(),
|
elem: seq1[i - 1].clone(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for j in 0..=len2 {
|
for j in 0..=len2 {
|
||||||
dp[0][j] = CostOperation {
|
dp[0][j] = if j > 0 {
|
||||||
|
Some(CostOperation {
|
||||||
cost: j as i32,
|
cost: j as i32,
|
||||||
operation: if j > 0 {
|
operation: EditOperation::Insert {
|
||||||
Some(EditOperation::Insert {
|
|
||||||
elem: seq2[j - 1].clone(),
|
elem: seq2[j - 1].clone(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,9 +132,16 @@ where
|
|||||||
let delete_op = EditOperation::Delete {
|
let delete_op = EditOperation::Delete {
|
||||||
elem: seq1[i - 1].clone(),
|
elem: seq1[i - 1].clone(),
|
||||||
};
|
};
|
||||||
|
let delete_cost = dp[i - 1][j]
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| Some(s.cost + cost_calc.cost(&delete_op)));
|
||||||
|
|
||||||
let insert_op = EditOperation::Insert {
|
let insert_op = EditOperation::Insert {
|
||||||
elem: seq2[j - 1].clone(),
|
elem: seq2[j - 1].clone(),
|
||||||
};
|
};
|
||||||
|
let insert_cost = dp[i][j - 1]
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|c| Some(c.cost + cost_calc.cost(&insert_op)));
|
||||||
let sub_op = if cost {
|
let sub_op = if cost {
|
||||||
EditOperation::Match {
|
EditOperation::Match {
|
||||||
elem: seq1[i - 1].clone(),
|
elem: seq1[i - 1].clone(),
|
||||||
@@ -155,65 +153,58 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let delete = CostOperation {
|
let substitute_cost = dp[i - 1][j - 1]
|
||||||
cost: dp[i - 1][j].cost + cost_calc.cost(&delete_op),
|
.as_ref()
|
||||||
operation: Some(delete_op),
|
.and_then(|c| Some(c.cost + cost_calc.cost(&sub_op)));
|
||||||
};
|
|
||||||
let insert = CostOperation {
|
|
||||||
cost: dp[i][j - 1].cost + cost_calc.cost(&insert_op),
|
|
||||||
operation: Some(insert_op),
|
|
||||||
};
|
|
||||||
let substitute = CostOperation {
|
|
||||||
cost: dp[i - 1][j - 1].cost + cost_calc.cost(&sub_op),
|
|
||||||
operation: Some(sub_op),
|
|
||||||
};
|
|
||||||
|
|
||||||
dp[i][j] = [delete, insert, substitute]
|
dp[i][j] = [
|
||||||
|
(delete_cost, delete_op),
|
||||||
|
(insert_cost, insert_op),
|
||||||
|
(substitute_cost, sub_op),
|
||||||
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.min_by_key(|x| x.cost)
|
.filter_map(|(c, o)| c.and_then(|c| Some((c, o))))
|
||||||
.unwrap();
|
.min_by_key(|c| c.0)
|
||||||
|
.and_then(|(c, o)| {
|
||||||
|
Some(CostOperation {
|
||||||
|
cost: c,
|
||||||
|
operation: o,
|
||||||
|
})
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(dp[len1][len2].cost, dp)
|
(dp[len1][len2].as_ref().unwrap().cost, dp)
|
||||||
}
|
}
|
||||||
|
|
||||||
///extract the optimized edit sequence from the dp table
|
///extract the optimized edit sequence from the dp table
|
||||||
///
|
///
|
||||||
/// strs there just for the len() lmfao
|
/// strs there just for the len() lmfao
|
||||||
pub fn edit_sequence<T: Clone>(dp: &[Vec<CostOperation<T>>]) -> Vec<EditOperation<T>> {
|
pub fn edit_sequence<T: Clone>(dp: &Vec<Vec<Option<CostOperation<T>>>>) -> Vec<EditOperation<T>> {
|
||||||
let mut i = dp.len() - 1;
|
let mut i = dp.len() - 1;
|
||||||
let mut j = dp[0].len() - 1;
|
let mut j = dp[0].len() - 1;
|
||||||
let mut funny = vec![];
|
let mut funny = Vec::new();
|
||||||
|
while let Some(op) = dp
|
||||||
while i > 0 || j > 0 {
|
.get(i)
|
||||||
if let Some(operation) = &dp[i][j].operation {
|
.and_then(|row| row.get(j))
|
||||||
match operation {
|
.and_then(|op| Some(op.as_ref().unwrap().operation.clone()))
|
||||||
EditOperation::Insert { .. } => {
|
{
|
||||||
funny.push(operation.clone());
|
match op {
|
||||||
j -= 1;
|
EditOperation::Insert { .. } => j -= 1,
|
||||||
}
|
EditOperation::Delete { .. } => i -= 1,
|
||||||
EditOperation::Delete { .. } => {
|
|
||||||
funny.push(operation.clone());
|
|
||||||
i -= 1;
|
|
||||||
}
|
|
||||||
EditOperation::Substitute { .. } | EditOperation::Match { .. } => {
|
EditOperation::Substitute { .. } | EditOperation::Match { .. } => {
|
||||||
funny.push(operation.clone());
|
|
||||||
i -= 1;
|
i -= 1;
|
||||||
j -= 1;
|
j -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
funny.push(op);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
funny.reverse();
|
funny.reverse();
|
||||||
funny
|
funny
|
||||||
}
|
}
|
||||||
|
|
||||||
/// alignment
|
|
||||||
/// is ass dont use
|
|
||||||
pub fn generate_alignment<T: Clone + Debug>(
|
pub fn generate_alignment<T: Clone + Debug>(
|
||||||
operations: &[Vec<CostOperation<T>>],
|
operations: &Vec<Vec<Option<CostOperation<T>>>>,
|
||||||
) -> (Vec<T>, Vec<T>) {
|
) -> (Vec<T>, Vec<T>) {
|
||||||
let mut aligned_seq1 = Vec::new();
|
let mut aligned_seq1 = Vec::new();
|
||||||
let mut aligned_seq2 = Vec::new();
|
let mut aligned_seq2 = Vec::new();
|
||||||
@@ -221,39 +212,35 @@ pub fn generate_alignment<T: Clone + Debug>(
|
|||||||
let mut i = operations.len() - 1;
|
let mut i = operations.len() - 1;
|
||||||
let mut j = operations[0].len() - 1;
|
let mut j = operations[0].len() - 1;
|
||||||
|
|
||||||
while i > 0 || j > 0 {
|
while let Some(op) = &operations[i][j] {
|
||||||
let op = &operations[i][j];
|
|
||||||
match op.operation {
|
match op.operation {
|
||||||
Some(EditOperation::Insert { ref elem }) => {
|
EditOperation::Insert { ref elem } => {
|
||||||
aligned_seq1.push(None);
|
aligned_seq2.push(elem.clone());
|
||||||
aligned_seq2.push(Some(elem.clone()));
|
|
||||||
j -= 1;
|
j -= 1;
|
||||||
}
|
}
|
||||||
Some(EditOperation::Delete { ref elem }) => {
|
EditOperation::Delete { ref elem } => {
|
||||||
aligned_seq1.push(Some(elem.clone()));
|
aligned_seq1.push(elem.clone());
|
||||||
aligned_seq2.push(None);
|
|
||||||
i -= 1;
|
i -= 1;
|
||||||
}
|
}
|
||||||
Some(EditOperation::Substitute { ref from, ref to }) => {
|
EditOperation::Substitute { ref from, ref to } => {
|
||||||
aligned_seq1.push(Some(from.clone()));
|
aligned_seq1.push(from.clone());
|
||||||
aligned_seq2.push(Some(to.clone()));
|
aligned_seq2.push(to.clone());
|
||||||
i -= 1;
|
i -= 1;
|
||||||
j -= 1;
|
j -= 1;
|
||||||
}
|
}
|
||||||
Some(EditOperation::Match { ref elem }) => {
|
EditOperation::Match { ref elem } => {
|
||||||
aligned_seq1.push(Some(elem.clone()));
|
aligned_seq1.push(elem.clone());
|
||||||
aligned_seq2.push(Some(elem.clone()));
|
aligned_seq2.push(elem.clone());
|
||||||
i -= 1;
|
i -= 1;
|
||||||
j -= 1;
|
j -= 1;
|
||||||
}
|
}
|
||||||
None => break,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
aligned_seq1.reverse();
|
aligned_seq1.reverse();
|
||||||
aligned_seq2.reverse();
|
aligned_seq2.reverse();
|
||||||
|
|
||||||
(aligned_seq1.into_iter().filter_map(|x| x).collect(), aligned_seq2.into_iter().filter_map(|x| x).collect())
|
(aligned_seq1, aligned_seq2)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// calculate distance, also get u the [`edit_sequence`]
|
/// calculate distance, also get u the [`edit_sequence`]
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
//! # Levenshtein Distance
|
||||||
|
//! This is a simple implementation of the Levenshtein distance algorithm with operations.
|
||||||
|
|
||||||
|
pub mod general;
|
||||||
|
|
||||||
|
/// costs for different edit operations
|
||||||
|
///
|
||||||
|
/// each with the letters involved: so you can further tweak the cost in ur own [`CustomCost`]s impl
|
||||||
|
/// position? idk
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum EditOperation {
|
||||||
|
Insert { char: char }, // Insert a character from the target string
|
||||||
|
Delete { char: char }, // Delete a character from the source string
|
||||||
|
Substitute { from: char, to: char }, // Replace one character with another
|
||||||
|
Match { char: char }, // Characters match, no operation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EditCost {
|
||||||
|
pub insert: i32,
|
||||||
|
pub delete: i32,
|
||||||
|
pub substitute: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EditCost {
|
||||||
|
fn default() -> Self {
|
||||||
|
EditCost {
|
||||||
|
insert: 1,
|
||||||
|
delete: 1,
|
||||||
|
substitute: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EditCost {
|
||||||
|
pub fn new(insert: i32, delete: i32, substitute: i32) -> Self {
|
||||||
|
EditCost {
|
||||||
|
insert,
|
||||||
|
delete,
|
||||||
|
substitute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// custom cost for each [`EditOperation`]s operation
|
||||||
|
pub trait CustomCost {
|
||||||
|
fn cost(&self, operation: &EditOperation) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomCost for EditCost {
|
||||||
|
fn cost(&self, operation: &EditOperation) -> i32 {
|
||||||
|
match operation {
|
||||||
|
EditOperation::Insert { .. } => self.insert,
|
||||||
|
EditOperation::Delete { .. } => self.delete,
|
||||||
|
EditOperation::Substitute { .. } => self.substitute,
|
||||||
|
EditOperation::Match { .. } => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// is this the main struct
|
||||||
|
///
|
||||||
|
/// cost of operation and the operation itself
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub struct CostOperation {
|
||||||
|
pub cost: i32,
|
||||||
|
pub operation: Option<EditOperation>,
|
||||||
|
}
|
||||||
|
// absolutely phenomenal naming
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
impl Display for EditOperation {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
EditOperation::Insert { char } => write!(f, "Insert {:#?}", char),
|
||||||
|
EditOperation::Delete { char } => write!(f, "Delete {:#?}", char),
|
||||||
|
EditOperation::Substitute { from, to } => {
|
||||||
|
write!(f, "Substitute {:#?} with {:#?}", from, to)
|
||||||
|
}
|
||||||
|
EditOperation::Match { char } => write!(f, "Match {:#?}", char),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CostOperation {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "Cost: {}, Operation: {:#?}", self.cost, self.operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// returns the distance + the dp table for who needs it
|
||||||
|
///
|
||||||
|
/// this the slow version yes
|
||||||
|
pub fn levenshtein_with_operations(
|
||||||
|
str1: &str,
|
||||||
|
str2: &str,
|
||||||
|
cost_calc: &impl CustomCost,
|
||||||
|
) -> (i32, Vec<Vec<CostOperation>>) {
|
||||||
|
let chars1: Vec<char> = str1.chars().collect();
|
||||||
|
let chars2: Vec<char> = str2.chars().collect();
|
||||||
|
let len1: usize = chars1.len();
|
||||||
|
let len2 = chars2.len();
|
||||||
|
let mut dp = vec![
|
||||||
|
vec![
|
||||||
|
CostOperation {
|
||||||
|
cost: 0,
|
||||||
|
operation: None
|
||||||
|
};
|
||||||
|
len2 + 1
|
||||||
|
];
|
||||||
|
len1 + 1
|
||||||
|
];
|
||||||
|
// println!("len1: {}, len2: {}", str1.len(), str2.len());
|
||||||
|
// println!("len char1: {}, len char2: {}", chars1.len(), chars2.len());
|
||||||
|
// Fill the base cases
|
||||||
|
for i in 0..=len1 {
|
||||||
|
// if i > 3068 && i < 3075 {
|
||||||
|
// println!("len1: {}, len2: {}", str1.len(), str2.len());
|
||||||
|
// println!("len char1: {}, len char2: {}", chars1.len(), chars2.len());
|
||||||
|
// println!("i: {}, char: {:#?}", i, chars1[i - 1]);
|
||||||
|
// }
|
||||||
|
dp[i][0] = CostOperation {
|
||||||
|
cost: i as i32,
|
||||||
|
operation: if i > 0 {
|
||||||
|
Some(EditOperation::Delete {
|
||||||
|
char: chars1[i - 1],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
for j in 0..=len2 {
|
||||||
|
dp[0][j] = CostOperation {
|
||||||
|
cost: j as i32,
|
||||||
|
operation: if j > 0 {
|
||||||
|
Some(EditOperation::Insert {
|
||||||
|
char: chars2[j - 1],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill the DP table
|
||||||
|
for i in 1..=len1 {
|
||||||
|
for j in 1..=len2 {
|
||||||
|
let cost = chars1[i - 1] == chars2[j - 1];
|
||||||
|
let delete_op = EditOperation::Delete {
|
||||||
|
char: chars1[i - 1],
|
||||||
|
};
|
||||||
|
let insert_op = EditOperation::Insert {
|
||||||
|
char: chars2[j - 1],
|
||||||
|
};
|
||||||
|
let sub_op = if cost {
|
||||||
|
EditOperation::Match {
|
||||||
|
char: chars1[i - 1],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EditOperation::Substitute {
|
||||||
|
from: chars1[i - 1],
|
||||||
|
to: chars2[j - 1],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let delete = CostOperation {
|
||||||
|
cost: dp[i - 1][j].cost + cost_calc.cost(&delete_op),
|
||||||
|
operation: Some(delete_op),
|
||||||
|
};
|
||||||
|
let insert = CostOperation {
|
||||||
|
cost: dp[i][j - 1].cost + cost_calc.cost(&insert_op),
|
||||||
|
operation: Some(insert_op),
|
||||||
|
};
|
||||||
|
let substitute = CostOperation {
|
||||||
|
cost: dp[i - 1][j - 1].cost + cost_calc.cost(&sub_op),
|
||||||
|
operation: Some(sub_op),
|
||||||
|
};
|
||||||
|
|
||||||
|
dp[i][j] = [delete, insert, substitute]
|
||||||
|
.into_iter()
|
||||||
|
.min_by_key(|x| x.cost)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(dp[len1][len2].cost, dp)
|
||||||
|
}
|
||||||
|
|
||||||
|
///extract the optimized edit sequence from the dp table
|
||||||
|
///
|
||||||
|
/// strs there just for the len() lmfao
|
||||||
|
pub fn edit_sequence(dp: &[Vec<CostOperation>]) -> Vec<EditOperation> {
|
||||||
|
let mut i = dp.len() - 1;
|
||||||
|
let mut j = dp[0].len() - 1; // guaranteed
|
||||||
|
let mut funny = vec![];
|
||||||
|
|
||||||
|
while i > 0 || j > 0 {
|
||||||
|
if let Some(operation) = &dp[i][j].operation {
|
||||||
|
match operation {
|
||||||
|
EditOperation::Insert { .. } => {
|
||||||
|
funny.push(*operation);
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
EditOperation::Delete { .. } => {
|
||||||
|
funny.push(*operation);
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
EditOperation::Substitute { .. } => {
|
||||||
|
funny.push(*operation);
|
||||||
|
i -= 1;
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
EditOperation::Match { .. } => {
|
||||||
|
funny.push(*operation);
|
||||||
|
i -= 1;
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funny.reverse();
|
||||||
|
funny
|
||||||
|
}
|
||||||
|
|
||||||
|
/// alignment
|
||||||
|
/// is ass dont use
|
||||||
|
pub fn generate_alignment(
|
||||||
|
operations: &[Vec<CostOperation>],
|
||||||
|
) -> (String, String) {
|
||||||
|
let mut aligned_str1 = String::new();
|
||||||
|
let mut aligned_str2 = String::new();
|
||||||
|
|
||||||
|
let mut i = operations.len() - 1;
|
||||||
|
let mut j = operations[0].len() - 1;
|
||||||
|
|
||||||
|
while i > 0 || j > 0 {
|
||||||
|
let op = &operations[i][j];
|
||||||
|
match op.operation {
|
||||||
|
Some(EditOperation::Insert { char }) => {
|
||||||
|
aligned_str1.push('-');
|
||||||
|
aligned_str2.push(char);
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
Some(EditOperation::Delete { char }) => {
|
||||||
|
aligned_str1.push(char);
|
||||||
|
aligned_str2.push('-');
|
||||||
|
i -= 1;
|
||||||
|
}
|
||||||
|
Some(EditOperation::Substitute { from, to }) => {
|
||||||
|
aligned_str1.push(from);
|
||||||
|
aligned_str2.push(to);
|
||||||
|
i -= 1;
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
Some(EditOperation::Match { char }) => {
|
||||||
|
aligned_str1.push(char);
|
||||||
|
aligned_str2.push(char);
|
||||||
|
i -= 1;
|
||||||
|
j -= 1;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse the strings as we built them backwards
|
||||||
|
aligned_str1 = aligned_str1.chars().rev().collect();
|
||||||
|
aligned_str2 = aligned_str2.chars().rev().collect();
|
||||||
|
|
||||||
|
(aligned_str1, aligned_str2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// calculate distance, also get u the [`edit_sequence`]
|
||||||
|
/// use this one
|
||||||
|
///
|
||||||
|
/// what this does is call this ass [`levenshtein_with_operations`] and then the [`edit_sequence`] from the generated dp table
|
||||||
|
pub fn levenstein_distance(str1: &str, str2: &str, cost: &EditCost) -> (i32, Vec<EditOperation>) {
|
||||||
|
let (c, d) = levenshtein_with_operations(str1, str2, cost);
|
||||||
|
(c, edit_sequence(&d))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get source str from [`edit_sequence`]
|
||||||
|
pub fn reconstruct_from(vector: &Vec<EditOperation>) -> String {
|
||||||
|
let mut funny = String::new();
|
||||||
|
for operation in vector {
|
||||||
|
match operation {
|
||||||
|
EditOperation::Insert { .. } => {}
|
||||||
|
EditOperation::Delete { char } => {
|
||||||
|
funny.push(*char);
|
||||||
|
}
|
||||||
|
EditOperation::Substitute { from, .. } => {
|
||||||
|
funny.push(*from);
|
||||||
|
}
|
||||||
|
EditOperation::Match { char } => {
|
||||||
|
funny.push(*char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funny
|
||||||
|
}
|
||||||
|
|
||||||
|
/// get changed str from [`edit_sequence`]
|
||||||
|
pub fn reconstruct_to(vector: &Vec<EditOperation>) -> String {
|
||||||
|
let mut funny = String::new();
|
||||||
|
for operation in vector {
|
||||||
|
match operation {
|
||||||
|
EditOperation::Insert { char } => {
|
||||||
|
funny.push(*char);
|
||||||
|
}
|
||||||
|
EditOperation::Delete { .. } => {}
|
||||||
|
EditOperation::Substitute { to, .. } => {
|
||||||
|
funny.push(*to);
|
||||||
|
}
|
||||||
|
EditOperation::Match { char } => {
|
||||||
|
funny.push(*char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
funny
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_insert() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let (distance, operations) = levenstein_distance("kitten", "sitting", &cost);
|
||||||
|
assert_eq!(distance, 3);
|
||||||
|
assert_eq!(
|
||||||
|
operations,
|
||||||
|
vec![
|
||||||
|
EditOperation::Substitute { from: 'k', to: 's' },
|
||||||
|
EditOperation::Match { char: 'i' },
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
EditOperation::Substitute { from: 'e', to: 'i' },
|
||||||
|
EditOperation::Match { char: 'n' },
|
||||||
|
EditOperation::Insert { char: 'g' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_delete() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let (distance, operations) = levenstein_distance("flaw", "lawn", &cost);
|
||||||
|
assert_eq!(distance, 2);
|
||||||
|
assert_eq!(
|
||||||
|
operations,
|
||||||
|
vec![
|
||||||
|
EditOperation::Delete { char: 'f' },
|
||||||
|
EditOperation::Match { char: 'l' },
|
||||||
|
EditOperation::Match { char: 'a' },
|
||||||
|
EditOperation::Match { char: 'w' },
|
||||||
|
EditOperation::Insert { char: 'n' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_substitute() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let (distance, operations) = levenstein_distance("gumbo", "gambol", &cost);
|
||||||
|
assert_eq!(distance, 2);
|
||||||
|
assert_eq!(
|
||||||
|
operations,
|
||||||
|
vec![
|
||||||
|
EditOperation::Match { char: 'g' },
|
||||||
|
EditOperation::Substitute { from: 'u', to: 'a' },
|
||||||
|
EditOperation::Match { char: 'm' },
|
||||||
|
EditOperation::Match { char: 'b' },
|
||||||
|
EditOperation::Match { char: 'o' },
|
||||||
|
EditOperation::Insert { char: 'l' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_match() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let (distance, operations) = levenstein_distance("test", "test", &cost);
|
||||||
|
assert_eq!(distance, 0);
|
||||||
|
assert_eq!(
|
||||||
|
operations,
|
||||||
|
vec![
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
EditOperation::Match { char: 'e' },
|
||||||
|
EditOperation::Match { char: 's' },
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_funny_from() {
|
||||||
|
let operations = vec![
|
||||||
|
EditOperation::Insert { char: 'a' },
|
||||||
|
EditOperation::Delete { char: 'b' },
|
||||||
|
EditOperation::Substitute { from: 'c', to: 'd' },
|
||||||
|
EditOperation::Match { char: 'e' },
|
||||||
|
];
|
||||||
|
let result = reconstruct_from(&operations);
|
||||||
|
assert_eq!(result, "bce");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_funny_to() {
|
||||||
|
let operations = vec![
|
||||||
|
EditOperation::Insert { char: 'a' },
|
||||||
|
EditOperation::Delete { char: 'b' },
|
||||||
|
EditOperation::Substitute { from: 'c', to: 'd' },
|
||||||
|
EditOperation::Match { char: 'e' },
|
||||||
|
];
|
||||||
|
let result = reconstruct_to(&operations);
|
||||||
|
assert_eq!(result, "ade");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_insert_with_dashes() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let str1 = "kitten";
|
||||||
|
let str2 = "sitting";
|
||||||
|
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||||
|
let operation = edit_sequence(&operations);
|
||||||
|
assert_eq!(distance, 3);
|
||||||
|
assert_eq!(
|
||||||
|
operation,
|
||||||
|
vec![
|
||||||
|
EditOperation::Substitute { from: 'k', to: 's' },
|
||||||
|
EditOperation::Match { char: 'i' },
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
EditOperation::Substitute { from: 'e', to: 'i' },
|
||||||
|
EditOperation::Match { char: 'n' },
|
||||||
|
EditOperation::Insert { char: 'g' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||||
|
assert_eq!(aligned_str1, "kitten-");
|
||||||
|
assert_eq!(aligned_str2, "sitting");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_delete_with_dashes() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let (str1, str2) = ("flaw", "lawn");
|
||||||
|
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||||
|
let operation = edit_sequence(&operations);
|
||||||
|
assert_eq!(distance, 2);
|
||||||
|
assert_eq!(
|
||||||
|
operation,
|
||||||
|
vec![
|
||||||
|
EditOperation::Delete { char: 'f' },
|
||||||
|
EditOperation::Match { char: 'l' },
|
||||||
|
EditOperation::Match { char: 'a' },
|
||||||
|
EditOperation::Match { char: 'w' },
|
||||||
|
EditOperation::Insert { char: 'n' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||||
|
assert_eq!(aligned_str1, "flaw-");
|
||||||
|
assert_eq!(aligned_str2, "-lawn");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_substitute_with_dashes() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let (str1, str2) = ("gumbo", "gambol");
|
||||||
|
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||||
|
let operation = edit_sequence(&operations);
|
||||||
|
assert_eq!(distance, 2);
|
||||||
|
assert_eq!(
|
||||||
|
operation,
|
||||||
|
vec![
|
||||||
|
EditOperation::Match { char: 'g' },
|
||||||
|
EditOperation::Substitute { from: 'u', to: 'a' },
|
||||||
|
EditOperation::Match { char: 'm' },
|
||||||
|
EditOperation::Match { char: 'b' },
|
||||||
|
EditOperation::Match { char: 'o' },
|
||||||
|
EditOperation::Insert { char: 'l' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||||
|
assert_eq!(aligned_str1, "gumbo-");
|
||||||
|
assert_eq!(aligned_str2, "gambol");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_levenshtein_distance_match_with_dashes() {
|
||||||
|
let cost = EditCost::default();
|
||||||
|
let (str1, str2) = ("test", "test");
|
||||||
|
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||||
|
let operation = edit_sequence(&operations);
|
||||||
|
assert_eq!(distance, 0);
|
||||||
|
assert_eq!(
|
||||||
|
operation,
|
||||||
|
vec![
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
EditOperation::Match { char: 'e' },
|
||||||
|
EditOperation::Match { char: 's' },
|
||||||
|
EditOperation::Match { char: 't' },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||||
|
assert_eq!(aligned_str1, "test");
|
||||||
|
assert_eq!(aligned_str2, "test");
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-510
@@ -1,511 +1,2 @@
|
|||||||
//! # Levenshtein Distance
|
pub mod levenshtein;
|
||||||
//! This is a simple implementation of the Levenshtein distance algorithm with operations.
|
|
||||||
//!
|
|
||||||
pub mod general;
|
|
||||||
|
|
||||||
/// costs for different edit operations
|
|
||||||
///
|
|
||||||
/// each with the letters involved: so you can further tweak the cost in ur own [`CustomCost`]s impl
|
|
||||||
/// position? idk
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum EditOperation {
|
|
||||||
Insert { char: char }, // Insert a character from the target string
|
|
||||||
Delete { char: char }, // Delete a character from the source string
|
|
||||||
Substitute { from: char, to: char }, // Replace one character with another
|
|
||||||
Match { char: char }, // Characters match, no operation
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EditCost {
|
|
||||||
pub insert: i32,
|
|
||||||
pub delete: i32,
|
|
||||||
pub substitute: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for EditCost {
|
|
||||||
fn default() -> Self {
|
|
||||||
EditCost {
|
|
||||||
insert: 1,
|
|
||||||
delete: 1,
|
|
||||||
substitute: 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EditCost {
|
|
||||||
pub fn new(insert: i32, delete: i32, substitute: i32) -> Self {
|
|
||||||
EditCost {
|
|
||||||
insert,
|
|
||||||
delete,
|
|
||||||
substitute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// custom cost for each [`EditOperation`]s operation
|
|
||||||
pub trait CustomCost {
|
|
||||||
fn cost(&self, operation: &EditOperation) -> i32;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CustomCost for EditCost {
|
|
||||||
fn cost(&self, operation: &EditOperation) -> i32 {
|
|
||||||
match operation {
|
|
||||||
EditOperation::Insert { .. } => self.insert,
|
|
||||||
EditOperation::Delete { .. } => self.delete,
|
|
||||||
EditOperation::Substitute { .. } => self.substitute,
|
|
||||||
EditOperation::Match { .. } => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// is this the main struct
|
|
||||||
///
|
|
||||||
/// cost of operation and the operation itself
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub struct CostOperation {
|
|
||||||
pub cost: i32,
|
|
||||||
pub operation: Option<EditOperation>,
|
|
||||||
}
|
|
||||||
// absolutely phenomenal naming
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
impl Display for EditOperation {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
EditOperation::Insert { char } => write!(f, "Insert {:#?}", char),
|
|
||||||
EditOperation::Delete { char } => write!(f, "Delete {:#?}", char),
|
|
||||||
EditOperation::Substitute { from, to } => {
|
|
||||||
write!(f, "Substitute {:#?} with {:#?}", from, to)
|
|
||||||
}
|
|
||||||
EditOperation::Match { char } => write!(f, "Match {:#?}", char),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CostOperation {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "Cost: {}, Operation: {:#?}", self.cost, self.operation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// returns the distance + the dp table for who needs it
|
|
||||||
///
|
|
||||||
/// this the slow version yes
|
|
||||||
pub fn levenshtein_with_operations(
|
|
||||||
str1: &str,
|
|
||||||
str2: &str,
|
|
||||||
cost_calc: &impl CustomCost,
|
|
||||||
) -> (i32, Vec<Vec<CostOperation>>) {
|
|
||||||
let chars1: Vec<char> = str1.chars().collect();
|
|
||||||
let chars2: Vec<char> = str2.chars().collect();
|
|
||||||
let len1: usize = chars1.len();
|
|
||||||
let len2 = chars2.len();
|
|
||||||
let mut dp = vec![
|
|
||||||
vec![
|
|
||||||
CostOperation {
|
|
||||||
cost: 0,
|
|
||||||
operation: None
|
|
||||||
};
|
|
||||||
len2 + 1
|
|
||||||
];
|
|
||||||
len1 + 1
|
|
||||||
];
|
|
||||||
// println!("len1: {}, len2: {}", str1.len(), str2.len());
|
|
||||||
// println!("len char1: {}, len char2: {}", chars1.len(), chars2.len());
|
|
||||||
// Fill the base cases
|
|
||||||
for i in 0..=len1 {
|
|
||||||
// if i > 3068 && i < 3075 {
|
|
||||||
// println!("len1: {}, len2: {}", str1.len(), str2.len());
|
|
||||||
// println!("len char1: {}, len char2: {}", chars1.len(), chars2.len());
|
|
||||||
// println!("i: {}, char: {:#?}", i, chars1[i - 1]);
|
|
||||||
// }
|
|
||||||
dp[i][0] = CostOperation {
|
|
||||||
cost: i as i32,
|
|
||||||
operation: if i > 0 {
|
|
||||||
Some(EditOperation::Delete {
|
|
||||||
char: chars1[i - 1],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
for j in 0..=len2 {
|
|
||||||
dp[0][j] = CostOperation {
|
|
||||||
cost: j as i32,
|
|
||||||
operation: if j > 0 {
|
|
||||||
Some(EditOperation::Insert {
|
|
||||||
char: chars2[j - 1],
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill the DP table
|
|
||||||
for i in 1..=len1 {
|
|
||||||
for j in 1..=len2 {
|
|
||||||
let cost = chars1[i - 1] == chars2[j - 1];
|
|
||||||
let delete_op = EditOperation::Delete {
|
|
||||||
char: chars1[i - 1],
|
|
||||||
};
|
|
||||||
let insert_op = EditOperation::Insert {
|
|
||||||
char: chars2[j - 1],
|
|
||||||
};
|
|
||||||
let sub_op = if cost {
|
|
||||||
EditOperation::Match {
|
|
||||||
char: chars1[i - 1],
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
EditOperation::Substitute {
|
|
||||||
from: chars1[i - 1],
|
|
||||||
to: chars2[j - 1],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let delete = CostOperation {
|
|
||||||
cost: dp[i - 1][j].cost + cost_calc.cost(&delete_op),
|
|
||||||
operation: Some(delete_op),
|
|
||||||
};
|
|
||||||
let insert = CostOperation {
|
|
||||||
cost: dp[i][j - 1].cost + cost_calc.cost(&insert_op),
|
|
||||||
operation: Some(insert_op),
|
|
||||||
};
|
|
||||||
let substitute = CostOperation {
|
|
||||||
cost: dp[i - 1][j - 1].cost + cost_calc.cost(&sub_op),
|
|
||||||
operation: Some(sub_op),
|
|
||||||
};
|
|
||||||
|
|
||||||
dp[i][j] = [delete, insert, substitute]
|
|
||||||
.into_iter()
|
|
||||||
.min_by_key(|x| x.cost)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(dp[len1][len2].cost, dp)
|
|
||||||
}
|
|
||||||
|
|
||||||
///extract the optimized edit sequence from the dp table
|
|
||||||
///
|
|
||||||
/// strs there just for the len() lmfao
|
|
||||||
pub fn edit_sequence(dp: &[Vec<CostOperation>]) -> Vec<EditOperation> {
|
|
||||||
let mut i = dp.len() - 1;
|
|
||||||
let mut j = dp[0].len() - 1; // guaranteed
|
|
||||||
let mut funny = vec![];
|
|
||||||
|
|
||||||
while i > 0 || j > 0 {
|
|
||||||
if let Some(operation) = &dp[i][j].operation {
|
|
||||||
match operation {
|
|
||||||
EditOperation::Insert { .. } => {
|
|
||||||
funny.push(*operation);
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
EditOperation::Delete { .. } => {
|
|
||||||
funny.push(*operation);
|
|
||||||
i -= 1;
|
|
||||||
}
|
|
||||||
EditOperation::Substitute { .. } => {
|
|
||||||
funny.push(*operation);
|
|
||||||
i -= 1;
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
EditOperation::Match { .. } => {
|
|
||||||
funny.push(*operation);
|
|
||||||
i -= 1;
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
funny.reverse();
|
|
||||||
funny
|
|
||||||
}
|
|
||||||
|
|
||||||
/// alignment
|
|
||||||
/// is ass dont use
|
|
||||||
pub fn generate_alignment(
|
|
||||||
operations: &[Vec<CostOperation>],
|
|
||||||
) -> (String, String) {
|
|
||||||
let mut aligned_str1 = String::new();
|
|
||||||
let mut aligned_str2 = String::new();
|
|
||||||
|
|
||||||
let mut i = operations.len() - 1;
|
|
||||||
let mut j = operations[0].len() - 1;
|
|
||||||
|
|
||||||
while i > 0 || j > 0 {
|
|
||||||
let op = &operations[i][j];
|
|
||||||
match op.operation {
|
|
||||||
Some(EditOperation::Insert { char }) => {
|
|
||||||
aligned_str1.push('-');
|
|
||||||
aligned_str2.push(char);
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
Some(EditOperation::Delete { char }) => {
|
|
||||||
aligned_str1.push(char);
|
|
||||||
aligned_str2.push('-');
|
|
||||||
i -= 1;
|
|
||||||
}
|
|
||||||
Some(EditOperation::Substitute { from, to }) => {
|
|
||||||
aligned_str1.push(from);
|
|
||||||
aligned_str2.push(to);
|
|
||||||
i -= 1;
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
Some(EditOperation::Match { char }) => {
|
|
||||||
aligned_str1.push(char);
|
|
||||||
aligned_str2.push(char);
|
|
||||||
i -= 1;
|
|
||||||
j -= 1;
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse the strings as we built them backwards
|
|
||||||
aligned_str1 = aligned_str1.chars().rev().collect();
|
|
||||||
aligned_str2 = aligned_str2.chars().rev().collect();
|
|
||||||
|
|
||||||
(aligned_str1, aligned_str2)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// calculate distance, also get u the [`edit_sequence`]
|
|
||||||
/// use this one
|
|
||||||
///
|
|
||||||
/// what this does is call this ass [`levenshtein_with_operations`] and then the [`edit_sequence`] from the generated dp table
|
|
||||||
pub fn levenstein_distance(str1: &str, str2: &str, cost: &EditCost) -> (i32, Vec<EditOperation>) {
|
|
||||||
let (c, d) = levenshtein_with_operations(str1, str2, cost);
|
|
||||||
(c, edit_sequence(&d))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get source str from [`edit_sequence`]
|
|
||||||
pub fn reconstruct_from(vector: &Vec<EditOperation>) -> String {
|
|
||||||
let mut funny = String::new();
|
|
||||||
for operation in vector {
|
|
||||||
match operation {
|
|
||||||
EditOperation::Insert { .. } => {}
|
|
||||||
EditOperation::Delete { char } => {
|
|
||||||
funny.push(*char);
|
|
||||||
}
|
|
||||||
EditOperation::Substitute { from, .. } => {
|
|
||||||
funny.push(*from);
|
|
||||||
}
|
|
||||||
EditOperation::Match { char } => {
|
|
||||||
funny.push(*char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
funny
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get changed str from [`edit_sequence`]
|
|
||||||
pub fn reconstruct_to(vector: &Vec<EditOperation>) -> String {
|
|
||||||
let mut funny = String::new();
|
|
||||||
for operation in vector {
|
|
||||||
match operation {
|
|
||||||
EditOperation::Insert { char } => {
|
|
||||||
funny.push(*char);
|
|
||||||
}
|
|
||||||
EditOperation::Delete { .. } => {}
|
|
||||||
EditOperation::Substitute { to, .. } => {
|
|
||||||
funny.push(*to);
|
|
||||||
}
|
|
||||||
EditOperation::Match { char } => {
|
|
||||||
funny.push(*char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
funny
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_insert() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let (distance, operations) = levenstein_distance("kitten", "sitting", &cost);
|
|
||||||
assert_eq!(distance, 3);
|
|
||||||
assert_eq!(
|
|
||||||
operations,
|
|
||||||
vec![
|
|
||||||
EditOperation::Substitute { from: 'k', to: 's' },
|
|
||||||
EditOperation::Match { char: 'i' },
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
EditOperation::Substitute { from: 'e', to: 'i' },
|
|
||||||
EditOperation::Match { char: 'n' },
|
|
||||||
EditOperation::Insert { char: 'g' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_delete() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let (distance, operations) = levenstein_distance("flaw", "lawn", &cost);
|
|
||||||
assert_eq!(distance, 2);
|
|
||||||
assert_eq!(
|
|
||||||
operations,
|
|
||||||
vec![
|
|
||||||
EditOperation::Delete { char: 'f' },
|
|
||||||
EditOperation::Match { char: 'l' },
|
|
||||||
EditOperation::Match { char: 'a' },
|
|
||||||
EditOperation::Match { char: 'w' },
|
|
||||||
EditOperation::Insert { char: 'n' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_substitute() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let (distance, operations) = levenstein_distance("gumbo", "gambol", &cost);
|
|
||||||
assert_eq!(distance, 2);
|
|
||||||
assert_eq!(
|
|
||||||
operations,
|
|
||||||
vec![
|
|
||||||
EditOperation::Match { char: 'g' },
|
|
||||||
EditOperation::Substitute { from: 'u', to: 'a' },
|
|
||||||
EditOperation::Match { char: 'm' },
|
|
||||||
EditOperation::Match { char: 'b' },
|
|
||||||
EditOperation::Match { char: 'o' },
|
|
||||||
EditOperation::Insert { char: 'l' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_match() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let (distance, operations) = levenstein_distance("test", "test", &cost);
|
|
||||||
assert_eq!(distance, 0);
|
|
||||||
assert_eq!(
|
|
||||||
operations,
|
|
||||||
vec![
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
EditOperation::Match { char: 'e' },
|
|
||||||
EditOperation::Match { char: 's' },
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_funny_from() {
|
|
||||||
let operations = vec![
|
|
||||||
EditOperation::Insert { char: 'a' },
|
|
||||||
EditOperation::Delete { char: 'b' },
|
|
||||||
EditOperation::Substitute { from: 'c', to: 'd' },
|
|
||||||
EditOperation::Match { char: 'e' },
|
|
||||||
];
|
|
||||||
let result = reconstruct_from(&operations);
|
|
||||||
assert_eq!(result, "bce");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_funny_to() {
|
|
||||||
let operations = vec![
|
|
||||||
EditOperation::Insert { char: 'a' },
|
|
||||||
EditOperation::Delete { char: 'b' },
|
|
||||||
EditOperation::Substitute { from: 'c', to: 'd' },
|
|
||||||
EditOperation::Match { char: 'e' },
|
|
||||||
];
|
|
||||||
let result = reconstruct_to(&operations);
|
|
||||||
assert_eq!(result, "ade");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_insert_with_dashes() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let str1 = "kitten";
|
|
||||||
let str2 = "sitting";
|
|
||||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
|
||||||
let operation = edit_sequence(&operations);
|
|
||||||
assert_eq!(distance, 3);
|
|
||||||
assert_eq!(
|
|
||||||
operation,
|
|
||||||
vec![
|
|
||||||
EditOperation::Substitute { from: 'k', to: 's' },
|
|
||||||
EditOperation::Match { char: 'i' },
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
EditOperation::Substitute { from: 'e', to: 'i' },
|
|
||||||
EditOperation::Match { char: 'n' },
|
|
||||||
EditOperation::Insert { char: 'g' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
|
||||||
assert_eq!(aligned_str1, "kitten-");
|
|
||||||
assert_eq!(aligned_str2, "sitting");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_delete_with_dashes() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let (str1, str2) = ("flaw", "lawn");
|
|
||||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
|
||||||
let operation = edit_sequence(&operations);
|
|
||||||
assert_eq!(distance, 2);
|
|
||||||
assert_eq!(
|
|
||||||
operation,
|
|
||||||
vec![
|
|
||||||
EditOperation::Delete { char: 'f' },
|
|
||||||
EditOperation::Match { char: 'l' },
|
|
||||||
EditOperation::Match { char: 'a' },
|
|
||||||
EditOperation::Match { char: 'w' },
|
|
||||||
EditOperation::Insert { char: 'n' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
|
||||||
assert_eq!(aligned_str1, "flaw-");
|
|
||||||
assert_eq!(aligned_str2, "-lawn");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_substitute_with_dashes() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let (str1, str2) = ("gumbo", "gambol");
|
|
||||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
|
||||||
let operation = edit_sequence(&operations);
|
|
||||||
assert_eq!(distance, 2);
|
|
||||||
assert_eq!(
|
|
||||||
operation,
|
|
||||||
vec![
|
|
||||||
EditOperation::Match { char: 'g' },
|
|
||||||
EditOperation::Substitute { from: 'u', to: 'a' },
|
|
||||||
EditOperation::Match { char: 'm' },
|
|
||||||
EditOperation::Match { char: 'b' },
|
|
||||||
EditOperation::Match { char: 'o' },
|
|
||||||
EditOperation::Insert { char: 'l' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
|
||||||
assert_eq!(aligned_str1, "gumbo-");
|
|
||||||
assert_eq!(aligned_str2, "gambol");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_levenshtein_distance_match_with_dashes() {
|
|
||||||
let cost = EditCost::default();
|
|
||||||
let (str1, str2) = ("test", "test");
|
|
||||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
|
||||||
let operation = edit_sequence(&operations);
|
|
||||||
assert_eq!(distance, 0);
|
|
||||||
assert_eq!(
|
|
||||||
operation,
|
|
||||||
vec![
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
EditOperation::Match { char: 'e' },
|
|
||||||
EditOperation::Match { char: 's' },
|
|
||||||
EditOperation::Match { char: 't' },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
|
||||||
assert_eq!(aligned_str1, "test");
|
|
||||||
assert_eq!(aligned_str2, "test");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub mod wrapper_;
|
pub mod wrapper_;
|
||||||
+8
-4
@@ -1,6 +1,10 @@
|
|||||||
use lda_strdist::*;
|
use lda_strdist::{
|
||||||
|
levenshtein::{
|
||||||
use wrapper_::EditOperationVec;
|
generate_alignment, levenshtein_with_operations, levenstein_distance, reconstruct_from,
|
||||||
|
reconstruct_to, EditCost,
|
||||||
|
},
|
||||||
|
wrapper_::EditOperationVec,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let str1 = "AGTACGCA";
|
let str1 = "AGTACGCA";
|
||||||
@@ -20,6 +24,6 @@ fn main() {
|
|||||||
|
|
||||||
let (d2, dp) = levenshtein_with_operations(str1, str2, &EditCost::default());
|
let (d2, dp) = levenshtein_with_operations(str1, str2, &EditCost::default());
|
||||||
assert!(d2 == distance);
|
assert!(d2 == distance);
|
||||||
let (al1, al2) = generate_alignment( &dp);
|
let (al1, al2) = generate_alignment(&dp);
|
||||||
println!("Alignment:\n{:#?}\n{:#?}", al1, al2);
|
println!("Alignment:\n{:#?}\n{:#?}", al1, al2);
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
use crate::EditOperation;
|
use crate::levenshtein::EditOperation;
|
||||||
|
|
||||||
pub struct EditOperationVec<'e>(pub &'e Vec<EditOperation>);
|
pub struct EditOperationVec<'e>(pub &'e Vec<EditOperation>);
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user