what the hell
This commit is contained in:
1 parent
827f1e4b75
commit
ddd3b7bbb9
3 files changed
+338
-23
No files matched your search
+309
@@ -0,0 +1,309 @@
|
||||
//! # Generalized Levenshtein Distance
|
||||
//! This is a simple generalized Levenshtein Distance implementation.
|
||||
|
||||
|
||||
/// 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, PartialEq)]
|
||||
pub enum EditOperation<T>{
|
||||
Insert { elem: T }, // Insert a character from the target string
|
||||
Delete { elem: T }, // Delete a character from the source string
|
||||
Substitute { from: T, to: T }, // Replace one character with another
|
||||
Match { elem: T }, // 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<T> {
|
||||
fn cost(&self, operation: &EditOperation<T>) -> i32;
|
||||
}
|
||||
|
||||
impl<T> CustomCost<T> for EditCost {
|
||||
fn cost(&self, operation: &EditOperation<T>) -> i32 {
|
||||
match operation {
|
||||
EditOperation::Insert { .. } => self.insert,
|
||||
EditOperation::Delete { .. } => self.delete,
|
||||
EditOperation::Substitute { .. } => self.substitute,
|
||||
EditOperation::Match { .. } => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// cost of operation and the operation itself
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CostOperation<T> {
|
||||
pub cost: i32,
|
||||
pub operation: Option<EditOperation<T>>,
|
||||
}
|
||||
// absolutely phenomenal naming
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
impl<T: Debug> Display for EditOperation<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
EditOperation::Insert { elem } => write!(f, "Insert {:#?}", elem),
|
||||
EditOperation::Delete { elem } => write!(f, "Delete {:#?}", elem),
|
||||
EditOperation::Substitute { from, to } => {
|
||||
write!(f, "Substitute {:#?} with {:#?}", from, to)
|
||||
}
|
||||
EditOperation::Match { elem } => write!(f, "Match {:#?}", elem),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Debug> Display for CostOperation<T> {
|
||||
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 generalized_levenshtein<T, F>(
|
||||
seq1: &[T],
|
||||
seq2: &[T],
|
||||
cost_calc: &F,
|
||||
) -> (i32, Vec<Vec<CostOperation<T>>>)
|
||||
where
|
||||
T: PartialEq + Clone + Debug,
|
||||
F: CustomCost<T>,
|
||||
{
|
||||
let len1 = seq1.len();
|
||||
let len2 = seq2.len();
|
||||
|
||||
let mut dp = vec![
|
||||
vec![
|
||||
CostOperation {
|
||||
cost: 0,
|
||||
operation: None
|
||||
};
|
||||
len2 + 1
|
||||
];
|
||||
len1 + 1
|
||||
];
|
||||
|
||||
// Fill the base cases
|
||||
for i in 0..=len1 {
|
||||
dp[i][0] = CostOperation {
|
||||
cost: i as i32,
|
||||
operation: if i > 0 {
|
||||
Some(EditOperation::Delete {
|
||||
elem: seq1[i - 1].clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
}
|
||||
for j in 0..=len2 {
|
||||
dp[0][j] = CostOperation {
|
||||
cost: j as i32,
|
||||
operation: if j > 0 {
|
||||
Some(EditOperation::Insert {
|
||||
elem: seq2[j - 1].clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Fill the DP table
|
||||
for i in 1..=len1 {
|
||||
for j in 1..=len2 {
|
||||
let cost = seq1[i - 1] == seq2[j - 1];
|
||||
let delete_op = EditOperation::Delete {
|
||||
elem: seq1[i - 1].clone(),
|
||||
};
|
||||
let insert_op = EditOperation::Insert {
|
||||
elem: seq2[j - 1].clone(),
|
||||
};
|
||||
let sub_op = if cost {
|
||||
EditOperation::Match {
|
||||
elem: seq1[i - 1].clone(),
|
||||
}
|
||||
} else {
|
||||
EditOperation::Substitute {
|
||||
from: seq1[i - 1].clone(),
|
||||
to: seq2[j - 1].clone(),
|
||||
}
|
||||
};
|
||||
|
||||
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<T: Clone>(dp: &[Vec<CostOperation<T>>]) -> Vec<EditOperation<T>> {
|
||||
let mut i = dp.len() - 1;
|
||||
let mut j = dp[0].len() - 1;
|
||||
let mut funny = vec![];
|
||||
|
||||
while i > 0 || j > 0 {
|
||||
if let Some(operation) = &dp[i][j].operation {
|
||||
match operation {
|
||||
EditOperation::Insert { .. } => {
|
||||
funny.push(operation.clone());
|
||||
j -= 1;
|
||||
}
|
||||
EditOperation::Delete { .. } => {
|
||||
funny.push(operation.clone());
|
||||
i -= 1;
|
||||
}
|
||||
EditOperation::Substitute { .. } | EditOperation::Match { .. } => {
|
||||
funny.push(operation.clone());
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
funny.reverse();
|
||||
funny
|
||||
}
|
||||
|
||||
/// alignment
|
||||
/// is ass dont use
|
||||
pub fn generate_alignment<T: Clone + Debug>(
|
||||
operations: &[Vec<CostOperation<T>>],
|
||||
) -> (Vec<T>, Vec<T>) {
|
||||
let mut aligned_seq1 = Vec::new();
|
||||
let mut aligned_seq2 = Vec::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 { ref elem }) => {
|
||||
aligned_seq1.push(None);
|
||||
aligned_seq2.push(Some(elem.clone()));
|
||||
j -= 1;
|
||||
}
|
||||
Some(EditOperation::Delete { ref elem }) => {
|
||||
aligned_seq1.push(Some(elem.clone()));
|
||||
aligned_seq2.push(None);
|
||||
i -= 1;
|
||||
}
|
||||
Some(EditOperation::Substitute { ref from, ref to }) => {
|
||||
aligned_seq1.push(Some(from.clone()));
|
||||
aligned_seq2.push(Some(to.clone()));
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
}
|
||||
Some(EditOperation::Match { ref elem }) => {
|
||||
aligned_seq1.push(Some(elem.clone()));
|
||||
aligned_seq2.push(Some(elem.clone()));
|
||||
i -= 1;
|
||||
j -= 1;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
aligned_seq1.reverse();
|
||||
aligned_seq2.reverse();
|
||||
|
||||
(aligned_seq1.into_iter().filter_map(|x| x).collect(), aligned_seq2.into_iter().filter_map(|x| x).collect())
|
||||
}
|
||||
|
||||
/// 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<T: PartialEq + Clone + Debug>(
|
||||
seq1: &[T],
|
||||
seq2: &[T],
|
||||
cost: &impl CustomCost<T>,
|
||||
) -> (i32, Vec<EditOperation<T>>) {
|
||||
let (c, d) = generalized_levenshtein(seq1, seq2, cost);
|
||||
(c, edit_sequence(&d))
|
||||
}
|
||||
|
||||
/// get source str from [`edit_sequence`]
|
||||
pub fn reconstruct_from<T: Clone>(vector: &Vec<EditOperation<T>>) -> Vec<T> {
|
||||
let mut result = Vec::new();
|
||||
for operation in vector {
|
||||
match operation {
|
||||
EditOperation::Insert { .. } => {}
|
||||
EditOperation::Delete { elem } => {
|
||||
result.push(elem.clone());
|
||||
}
|
||||
EditOperation::Substitute { from, .. } => {
|
||||
result.push(from.clone());
|
||||
}
|
||||
EditOperation::Match { elem } => {
|
||||
result.push(elem.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
/// get changed str from [`edit_sequence`]
|
||||
pub fn reconstruct_to<T: Clone>(vector: &Vec<EditOperation<T>>) -> Vec<T> {
|
||||
let mut result = Vec::new();
|
||||
for operation in vector {
|
||||
match operation {
|
||||
EditOperation::Insert { elem } => {
|
||||
result.push(elem.clone());
|
||||
}
|
||||
EditOperation::Delete { .. } => {}
|
||||
EditOperation::Substitute { to, .. } => {
|
||||
result.push(to.clone());
|
||||
}
|
||||
EditOperation::Match { elem } => {
|
||||
result.push(elem.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
+28
-22
@@ -1,5 +1,7 @@
|
||||
//! # Levenshtein Distance
|
||||
//! This is a simple implementation of the Levenshtein distance algorithm with operations.
|
||||
//!
|
||||
pub mod general;
|
||||
|
||||
/// costs for different edit operations
|
||||
///
|
||||
@@ -90,8 +92,10 @@ pub fn levenshtein_with_operations(
|
||||
str2: &str,
|
||||
cost_calc: &impl CustomCost,
|
||||
) -> (i32, Vec<Vec<CostOperation>>) {
|
||||
let len1 = str1.len();
|
||||
let len2 = str2.len();
|
||||
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 {
|
||||
@@ -102,11 +106,15 @@ pub fn levenshtein_with_operations(
|
||||
];
|
||||
len1 + 1
|
||||
];
|
||||
|
||||
let chars1: Vec<char> = str1.chars().collect();
|
||||
let chars2: Vec<char> = str2.chars().collect();
|
||||
// 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 {
|
||||
@@ -178,9 +186,9 @@ pub fn levenshtein_with_operations(
|
||||
///extract the optimized edit sequence from the dp table
|
||||
///
|
||||
/// strs there just for the len() lmfao
|
||||
pub fn edit_sequence(dp: &[Vec<CostOperation>], str1: &str, str2: &str) -> Vec<EditOperation> {
|
||||
let mut i = str1.len();
|
||||
let mut j = str2.len();
|
||||
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 {
|
||||
@@ -214,15 +222,13 @@ pub fn edit_sequence(dp: &[Vec<CostOperation>], str1: &str, str2: &str) -> Vec<E
|
||||
/// alignment
|
||||
/// is ass dont use
|
||||
pub fn generate_alignment(
|
||||
str1: &str,
|
||||
str2: &str,
|
||||
operations: &[Vec<CostOperation>],
|
||||
) -> (String, String) {
|
||||
let mut aligned_str1 = String::new();
|
||||
let mut aligned_str2 = String::new();
|
||||
|
||||
let mut i = str1.len();
|
||||
let mut j = str2.len();
|
||||
let mut i = operations.len() - 1;
|
||||
let mut j = operations[0].len() - 1;
|
||||
|
||||
while i > 0 || j > 0 {
|
||||
let op = &operations[i][j];
|
||||
@@ -266,7 +272,7 @@ pub fn generate_alignment(
|
||||
/// 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, str1, str2))
|
||||
(c, edit_sequence(&d))
|
||||
}
|
||||
|
||||
/// get source str from [`edit_sequence`]
|
||||
@@ -413,7 +419,7 @@ mod tests {
|
||||
let str1 = "kitten";
|
||||
let str2 = "sitting";
|
||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||
let operation = edit_sequence(&operations, str1, str2);
|
||||
let operation = edit_sequence(&operations);
|
||||
assert_eq!(distance, 3);
|
||||
assert_eq!(
|
||||
operation,
|
||||
@@ -428,7 +434,7 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
let (aligned_str1, aligned_str2) = generate_alignment(str1, str2, &operations);
|
||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||
assert_eq!(aligned_str1, "kitten-");
|
||||
assert_eq!(aligned_str2, "sitting");
|
||||
}
|
||||
@@ -438,7 +444,7 @@ mod tests {
|
||||
let cost = EditCost::default();
|
||||
let (str1, str2) = ("flaw", "lawn");
|
||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||
let operation = edit_sequence(&operations, str1, str2);
|
||||
let operation = edit_sequence(&operations);
|
||||
assert_eq!(distance, 2);
|
||||
assert_eq!(
|
||||
operation,
|
||||
@@ -451,7 +457,7 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
let (aligned_str1, aligned_str2) = generate_alignment("flaw", "lawn", &operations);
|
||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||
assert_eq!(aligned_str1, "flaw-");
|
||||
assert_eq!(aligned_str2, "-lawn");
|
||||
}
|
||||
@@ -461,7 +467,7 @@ mod tests {
|
||||
let cost = EditCost::default();
|
||||
let (str1, str2) = ("gumbo", "gambol");
|
||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||
let operation = edit_sequence(&operations, str1, str2);
|
||||
let operation = edit_sequence(&operations);
|
||||
assert_eq!(distance, 2);
|
||||
assert_eq!(
|
||||
operation,
|
||||
@@ -475,7 +481,7 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
let (aligned_str1, aligned_str2) = generate_alignment("gumbo", "gambol", &operations);
|
||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||
assert_eq!(aligned_str1, "gumbo-");
|
||||
assert_eq!(aligned_str2, "gambol");
|
||||
}
|
||||
@@ -485,7 +491,7 @@ mod tests {
|
||||
let cost = EditCost::default();
|
||||
let (str1, str2) = ("test", "test");
|
||||
let (distance, operations) = levenshtein_with_operations(str1, str2, &cost);
|
||||
let operation = edit_sequence(&operations, str1, str2);
|
||||
let operation = edit_sequence(&operations);
|
||||
assert_eq!(distance, 0);
|
||||
assert_eq!(
|
||||
operation,
|
||||
@@ -497,9 +503,9 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
let (aligned_str1, aligned_str2) = generate_alignment("test", "test", &operations);
|
||||
let (aligned_str1, aligned_str2) = generate_alignment(&operations);
|
||||
assert_eq!(aligned_str1, "test");
|
||||
assert_eq!(aligned_str2, "test");
|
||||
}
|
||||
}
|
||||
pub mod wrapper_;
|
||||
pub mod wrapper_;
|
||||
+1
-1
@@ -20,6 +20,6 @@ fn main() {
|
||||
|
||||
let (d2, dp) = levenshtein_with_operations(str1, str2, &EditCost::default());
|
||||
assert!(d2 == distance);
|
||||
let (al1, al2) = generate_alignment(str1, str2, &dp);
|
||||
let (al1, al2) = generate_alignment( &dp);
|
||||
println!("Alignment:\n{:#?}\n{:#?}", al1, al2);
|
||||
}
|
||||
Reference in new issue
Block a user