test
This commit is contained in:
commit
fbe0743d27
6 files changed
+554
No files matched your search
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "lda_levenshtein"
|
||||
version = "0.0.1"
|
||||
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "lda_levenshtein"
|
||||
version = "0.0.1"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,19 @@
|
||||
Source String: AGTACGCA
|
||||
Target String: TATGC
|
||||
Edit Distance: 4
|
||||
Edit Sequence:
|
||||
Step 1: Delete 'A'
|
||||
Step 2: Delete 'G'
|
||||
Step 3: Match 'T'
|
||||
Step 4: Match 'A'
|
||||
Step 5: Substitute 'C' with 'T'
|
||||
Step 6: Match 'G'
|
||||
Step 7: Match 'C'
|
||||
Step 8: Delete 'A'
|
||||
|
||||
Reconstructed:
|
||||
from: AGTACGCA,
|
||||
to: TATGC
|
||||
Alignment:
|
||||
"AGTACGCA"
|
||||
"--TATGC-"
|
||||
+484
@@ -0,0 +1,484 @@
|
||||
//! # Levenshtein Distance
|
||||
//! This is a simple implementation of the Levenshtein distance algorithm with operations.
|
||||
|
||||
#[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: usize,
|
||||
pub delete: usize,
|
||||
pub substitute: usize,
|
||||
}
|
||||
|
||||
impl Default for EditCost {
|
||||
fn default() -> Self {
|
||||
EditCost {
|
||||
insert: 1,
|
||||
delete: 1,
|
||||
substitute: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditCost {
|
||||
pub fn new(insert: usize, delete: usize, substitute: usize) -> Self {
|
||||
EditCost {
|
||||
insert,
|
||||
delete,
|
||||
substitute,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CustomCost {
|
||||
fn cost(&self, operation: &EditOperation) -> usize;
|
||||
}
|
||||
|
||||
impl CustomCost for EditCost {
|
||||
fn cost(&self, operation: &EditOperation) -> usize {
|
||||
match operation {
|
||||
EditOperation::Insert { .. } => self.insert,
|
||||
EditOperation::Delete { .. } => self.delete,
|
||||
EditOperation::Substitute { .. } => self.substitute,
|
||||
EditOperation::Match { .. } => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct CostOperation {
|
||||
cost: usize,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn levenshtein_with_operations(
|
||||
str1: &str,
|
||||
str2: &str,
|
||||
cost_calc: &impl CustomCost,
|
||||
) -> (usize, Vec<Vec<CostOperation>>) {
|
||||
let len1 = str1.len();
|
||||
let len2 = str2.len();
|
||||
let mut dp = vec![
|
||||
vec![
|
||||
CostOperation {
|
||||
cost: 0,
|
||||
operation: None
|
||||
};
|
||||
len2 + 1
|
||||
];
|
||||
len1 + 1
|
||||
];
|
||||
|
||||
let chars1: Vec<char> = str1.chars().collect();
|
||||
let chars2: Vec<char> = str2.chars().collect();
|
||||
// Fill the base cases
|
||||
for i in 0..=len1 {
|
||||
dp[i][0] = CostOperation {
|
||||
cost: i,
|
||||
operation: if i > 0 {
|
||||
Some(EditOperation::Delete {
|
||||
char: chars1[i - 1],
|
||||
})
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
}
|
||||
for j in 0..=len2 {
|
||||
dp[0][j] = CostOperation {
|
||||
cost: j,
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn edit_sequence(dp: &Vec<Vec<CostOperation>>, str1: &str, str2: &str) -> Vec<EditOperation> {
|
||||
let mut i = str1.len();
|
||||
let mut j = str2.len();
|
||||
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
|
||||
}
|
||||
pub fn generate_alignment(
|
||||
str1: &str,
|
||||
str2: &str,
|
||||
operations: &Vec<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();
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn levenstein_distance(str1: &str, str2: &str, cost: &EditCost) -> (usize, Vec<EditOperation>) {
|
||||
let (c, d) = levenshtein_with_operations(str1, str2, cost);
|
||||
(c, edit_sequence(&d, str1, str2))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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, str1, str2);
|
||||
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(str1, str2, &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, str1, str2);
|
||||
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("flaw", "lawn", &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, str1, str2);
|
||||
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("gumbo", "gambol", &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, str1, str2);
|
||||
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("test", "test", &operations);
|
||||
assert_eq!(aligned_str1, "test");
|
||||
assert_eq!(aligned_str2, "test");
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use lda_levenshtein::*;
|
||||
|
||||
struct EditOperationVec<'e>(&'e Vec<EditOperation>);
|
||||
|
||||
impl Display for EditOperationVec<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
for (i, op) in self.0.iter().enumerate() {
|
||||
writeln!(f, "Step {}: {}", i + 1, op)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let str1 = "AGTACGCA";
|
||||
let str2 = "TATGC";
|
||||
|
||||
let (distance, reconstructed) = levenstein_distance(str1, str2, &EditCost::default());
|
||||
|
||||
println!("Source String: {}", str1);
|
||||
println!("Target String: {}", str2);
|
||||
println!("Edit Distance: {}", distance);
|
||||
println!("Edit Sequence:\n{}", EditOperationVec(&reconstructed));
|
||||
println!(
|
||||
"Reconstructed:\nfrom: {},\nto: {}",
|
||||
reconstruct_from(&reconstructed),
|
||||
reconstruct_to(&reconstructed)
|
||||
);
|
||||
|
||||
|
||||
let (d2, dp) = levenshtein_with_operations(str1, str2, &EditCost::default());
|
||||
assert!(d2 == distance);
|
||||
let (al1, al2) = generate_alignment(str1, str2, &dp);
|
||||
println!("Alignment:\n{:#?}\n{:#?}", al1, al2);
|
||||
}
|
||||
Reference in new issue
Block a user