use sha2::{Sha256, Digest};
pub fn longest_common_substring(a: &str, b: &str) -> usize {
let a_chars: Vec<char> = a.chars().collect();
let b_chars: Vec<char> = b.chars().collect();
let m = a_chars.len();
let n = b_chars.len();
if m == 0 || n == 0 {
return 0;
}
let mut prev = vec![0u32; n + 1];
let mut curr = vec![0u32; n + 1];
let mut max_len: u32 = 0;
for i in 1..=m {
for j in 1..=n {
if a_chars[i - 1] == b_chars[j - 1] {
curr[j] = prev[j - 1] + 1;
max_len = max_len.max(curr[j]);
} else {
curr[j] = 0;
}
}
std::mem::swap(&mut prev, &mut curr);
}
max_len as usize
}
pub fn lcs_similarity(a: &str, b: &str) -> f64 {
let max_len = a.chars().count().max(b.chars().count());
if max_len == 0 {
return 0.0;
}
longest_common_substring(a, b) as f64 / max_len as f64
}
#[derive(Debug, PartialEq, Eq)]
pub enum ConflictCategory {
Exact,
Similar,
Related,
Unrelated,
}
impl ConflictCategory {
pub fn from_similarity(sim: f64) -> Self {
if sim >= 0.90 {
ConflictCategory::Exact
} else if sim >= 0.70 {
ConflictCategory::Similar
} else if sim >= 0.40 {
ConflictCategory::Related
} else {
ConflictCategory::Unrelated
}
}
}
pub fn canonical_hash(content: &str) -> String {
let normalized = content.trim().to_lowercase();
let mut hasher = Sha256::new();
hasher.update(normalized.as_bytes());
let result = hasher.finalize();
hex::encode(&result[..8]) }
mod hex {
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lcs_identical() {
assert_eq!(lcs_similarity("Hello World", "Hello World"), 1.0);
}
#[test]
fn test_lcs_completely_different() {
assert_eq!(lcs_similarity("abc", "xyz"), 0.0);
}
#[test]
fn test_lcs_partial_overlap() {
let sim = lcs_similarity("使用 pnpm 作为包管理器", "建议使用 pnpm 管理依赖");
assert!(sim > 0.3 && sim < 0.9, "similarity = {}", sim);
}
#[test]
fn test_lcs_empty_strings() {
assert_eq!(lcs_similarity("", "Hello"), 0.0);
assert_eq!(lcs_similarity("Hello", ""), 0.0);
assert_eq!(lcs_similarity("", ""), 0.0);
}
#[test]
fn test_conflict_category() {
assert_eq!(ConflictCategory::from_similarity(0.95), ConflictCategory::Exact);
assert_eq!(ConflictCategory::from_similarity(0.80), ConflictCategory::Similar);
assert_eq!(ConflictCategory::from_similarity(0.50), ConflictCategory::Related);
assert_eq!(ConflictCategory::from_similarity(0.10), ConflictCategory::Unrelated);
}
#[test]
fn test_canonical_hash_deterministic() {
let h1 = canonical_hash("Hello World");
let h2 = canonical_hash("Hello World");
assert_eq!(h1, h2);
}
#[test]
fn test_canonical_hash_case_insensitive() {
let h1 = canonical_hash("HELLO WORLD");
let h2 = canonical_hash("hello world");
assert_eq!(h1, h2);
}
#[test]
fn test_canonical_hash_trim() {
let h1 = canonical_hash(" test ");
let h2 = canonical_hash("test");
assert_eq!(h1, h2);
}
#[test]
fn test_canonical_hash_length() {
let h = canonical_hash("test");
assert_eq!(h.len(), 16);
}
#[test]
fn test_canonical_hash_different() {
let h1 = canonical_hash("内容A");
let h2 = canonical_hash("内容B");
assert_ne!(h1, h2);
}
}