use std::collections::HashMap;
use std::hash::{Hash, Hasher};
struct Fnv1aHasher {
state: u64,
}
impl Fnv1aHasher {
const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x00000100000001B3;
fn new() -> Self {
Fnv1aHasher { state: Self::FNV_OFFSET_BASIS }
}
}
impl Hasher for Fnv1aHasher {
fn write(&mut self, bytes: &[u8]) {
for &byte in bytes {
self.state ^= byte as u64;
self.state = self.state.wrapping_mul(Self::FNV_PRIME);
}
}
fn finish(&self) -> u64 {
self.state
}
}
pub fn compute_view_hash(view_bytes: &[u8]) -> String {
let mut hasher = Fnv1aHasher::new();
view_bytes.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn compute_all_view_hashes(views: &HashMap<String, Vec<u8>>) -> HashMap<String, String> {
views.iter().map(|(key, bytes)| (key.clone(), compute_view_hash(bytes))).collect()
}
pub struct ViewDiff {
pub updated_keys: Vec<String>,
pub removed_keys: Vec<String>,
pub added_keys: Vec<String>,
pub total_new_views: usize,
}
impl ViewDiff {
pub fn is_incremental(&self) -> bool {
self.is_incremental_with_threshold(0.5)
}
pub fn is_incremental_with_threshold(&self, threshold: f64) -> bool {
if self.total_new_views == 0 {
return false;
}
let changed_count =
self.updated_keys.len() + self.removed_keys.len() + self.added_keys.len();
(changed_count as f64 / self.total_new_views as f64) <= threshold
}
pub fn change_count(&self) -> usize {
self.updated_keys.len() + self.removed_keys.len() + self.added_keys.len()
}
}
pub fn diff_view_hashes(
old_hashes: &HashMap<String, String>,
new_hashes: &HashMap<String, String>,
) -> ViewDiff {
let mut updated_keys = Vec::new();
let mut removed_keys = Vec::new();
let mut added_keys = Vec::new();
for (key, old_hash) in old_hashes {
match new_hashes.get(key) {
Some(new_hash) if new_hash != old_hash => {
updated_keys.push(key.clone());
}
None => {
removed_keys.push(key.clone());
}
_ => {} }
}
for key in new_hashes.keys() {
if !old_hashes.contains_key(key) {
added_keys.push(key.clone());
}
}
ViewDiff { updated_keys, removed_keys, added_keys, total_new_views: new_hashes.len() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_same_bytes_same_hash() {
let bytes = b"hello world";
assert_eq!(compute_view_hash(bytes), compute_view_hash(bytes));
}
#[test]
fn test_different_bytes_different_hash() {
let a = compute_view_hash(b"hello");
let b = compute_view_hash(b"world");
assert_ne!(a, b);
}
#[test]
fn test_hash_format() {
let hash = compute_view_hash(b"test");
assert_eq!(hash.len(), 16); assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn test_diff_no_changes() {
let mut old = HashMap::new();
old.insert("a".to_string(), "hash1".to_string());
old.insert("b".to_string(), "hash2".to_string());
let diff = diff_view_hashes(&old, &old);
assert_eq!(diff.change_count(), 0);
assert!(diff.is_incremental());
}
#[test]
fn test_diff_one_updated() {
let mut old = HashMap::new();
old.insert("a".to_string(), "hash1".to_string());
old.insert("b".to_string(), "hash2".to_string());
let mut new = HashMap::new();
new.insert("a".to_string(), "hash1_changed".to_string());
new.insert("b".to_string(), "hash2".to_string());
let diff = diff_view_hashes(&old, &new);
assert_eq!(diff.updated_keys, vec!["a"]);
assert!(diff.removed_keys.is_empty());
assert!(diff.added_keys.is_empty());
assert!(diff.is_incremental());
}
#[test]
fn test_diff_added_and_removed() {
let mut old = HashMap::new();
old.insert("a".to_string(), "hash1".to_string());
let mut new = HashMap::new();
new.insert("b".to_string(), "hash2".to_string());
let diff = diff_view_hashes(&old, &new);
assert_eq!(diff.removed_keys, vec!["a"]);
assert_eq!(diff.added_keys, vec!["b"]);
assert!(!diff.is_incremental()); }
#[test]
fn test_is_incremental_threshold() {
let mut old = HashMap::new();
old.insert("a".to_string(), "hash1".to_string());
old.insert("b".to_string(), "hash2".to_string());
old.insert("c".to_string(), "hash3".to_string());
let mut new = old.clone();
new.insert("a".to_string(), "hash1_changed".to_string());
let diff = diff_view_hashes(&old, &new);
assert!(diff.is_incremental());
new.insert("b".to_string(), "hash2_changed".to_string());
let diff2 = diff_view_hashes(&old, &new);
assert!(!diff2.is_incremental());
}
#[test]
fn test_is_incremental_with_custom_threshold() {
let mut old = HashMap::new();
old.insert("a".to_string(), "hash1".to_string());
old.insert("b".to_string(), "hash2".to_string());
old.insert("c".to_string(), "hash3".to_string());
old.insert("d".to_string(), "hash4".to_string());
let mut new = old.clone();
new.insert("a".to_string(), "changed1".to_string());
new.insert("b".to_string(), "changed2".to_string());
new.insert("c".to_string(), "changed3".to_string());
let diff = diff_view_hashes(&old, &new);
assert_eq!(diff.change_count(), 3);
assert!(!diff.is_incremental());
assert!(!diff.is_incremental_with_threshold(0.5));
assert!(diff.is_incremental_with_threshold(0.8));
assert!(!diff.is_incremental_with_threshold(0.7));
assert!(diff.is_incremental_with_threshold(0.75));
}
#[test]
fn test_threshold_zero_always_full() {
let mut old = HashMap::new();
old.insert("a".to_string(), "hash1".to_string());
old.insert("b".to_string(), "hash2".to_string());
let new = old.clone(); let diff = diff_view_hashes(&old, &new);
assert!(diff.is_incremental_with_threshold(0.0));
let mut new_changed = old.clone();
new_changed.insert("a".to_string(), "changed".to_string());
let diff2 = diff_view_hashes(&old, &new_changed);
assert!(!diff2.is_incremental_with_threshold(0.0));
}
#[test]
fn test_threshold_one_always_incremental() {
let mut old = HashMap::new();
old.insert("a".to_string(), "hash1".to_string());
let mut new = HashMap::new();
new.insert("a".to_string(), "changed".to_string());
let diff = diff_view_hashes(&old, &new);
assert!(diff.is_incremental_with_threshold(1.0));
let mut old2 = HashMap::new();
old2.insert("a".to_string(), "hash1".to_string());
let mut new2 = HashMap::new();
new2.insert("b".to_string(), "hash2".to_string());
let diff2 = diff_view_hashes(&old2, &new2);
assert!(!diff2.is_incremental_with_threshold(1.0));
}
#[test]
fn test_empty_views_not_incremental() {
let old = HashMap::new();
let new = HashMap::new();
let diff = diff_view_hashes(&old, &new);
assert!(!diff.is_incremental()); assert!(!diff.is_incremental_with_threshold(1.0));
}
}