use crate::units::{Operands, dispatch, utf16_len};
pub const INCOMPARABLE: i64 = -1;
pub fn hamming(s1: &str, s2: &str, ignore_case: bool) -> i64 {
if !ignore_case && s1.len() == s2.len() {
let a = s1.as_bytes();
let b = s2.as_bytes();
let n = a.len();
if n < 16 {
if s1.is_ascii() && s2.is_ascii() {
if n < 8 {
return a.iter().zip(b).filter(|(x, y)| x != y).count() as i64;
}
return swar_diffs(a, b) as i64;
}
} else if let Some(d) = fused_ascii_diffs(a, b) {
return d as i64;
}
return hamming_slow(s1, s2, ignore_case);
}
if s1.is_ascii() && s2.is_ascii() {
if s1.len() != s2.len() {
return INCOMPARABLE;
}
return s1
.as_bytes()
.iter()
.zip(s2.as_bytes())
.filter(|(x, y)| !x.eq_ignore_ascii_case(y))
.count() as i64;
}
hamming_slow(s1, s2, ignore_case)
}
fn hamming_slow(s1: &str, s2: &str, ignore_case: bool) -> i64 {
if utf16_len(s1) != utf16_len(s2) {
return INCOMPARABLE;
}
if ignore_case {
let a = s1.to_lowercase();
let b = s2.to_lowercase();
return count_diffs(&a, &b);
}
count_diffs(s1, s2)
}
fn swar_diffs(a: &[u8], b: &[u8]) -> u64 {
const HI: u64 = 0x8080_8080_8080_8080;
debug_assert_eq!(a.len(), b.len());
let words_end = a.len() / 8 * 8;
let mut total = 0u64;
let mut i = 0usize;
while i < words_end {
let end = (i + 2040).min(words_end);
let mut acc = 0u64;
for (ca, cb) in a[i..end].chunks_exact(8).zip(b[i..end].chunks_exact(8)) {
let x = u64::from_le_bytes(ca.try_into().unwrap())
^ u64::from_le_bytes(cb.try_into().unwrap());
acc += ((((x & !HI).wrapping_add(!HI)) | x) & HI) >> 7;
}
total += acc.to_le_bytes().iter().map(|&x| u64::from(x)).sum::<u64>();
i = end;
}
for (x, y) in a[words_end..].iter().zip(&b[words_end..]) {
total += u64::from(x != y);
}
total
}
fn fused_ascii_diffs(a: &[u8], b: &[u8]) -> Option<u64> {
debug_assert_eq!(a.len(), b.len());
let n = a.len();
let mut total = 0u64;
let mut seen = 0u8;
let mut i = 0usize;
while i < n {
let end = (i + 4080).min(n);
let mut acc = [0u8; 16];
let mut hi = [0u8; 16];
let mut ai = a[i..end].chunks_exact(16);
let mut bi = b[i..end].chunks_exact(16);
for (ca, cb) in ai.by_ref().zip(bi.by_ref()) {
for k in 0..16 {
acc[k] += u8::from(ca[k] != cb[k]);
hi[k] |= ca[k] | cb[k];
}
}
total += acc.iter().map(|&x| u64::from(x)).sum::<u64>();
seen |= hi.iter().fold(0u8, |m, &x| m | x);
for (x, y) in ai.remainder().iter().zip(bi.remainder()) {
total += u64::from(x != y);
seen |= x | y;
}
i = end;
}
(seen & 0x80 == 0).then_some(total)
}
pub fn hamming_checked(s1: &str, s2: &str, ignore_case: bool) -> Option<u64> {
match hamming(s1, s2, ignore_case) {
INCOMPARABLE => None,
d => Some(d as u64),
}
}
fn count_diffs(s1: &str, s2: &str) -> i64 {
dispatch(s1, s2, |ops| match ops {
Operands::Bytes(a, b) => diffs_generic(a, b),
Operands::Units(a, b) => diffs_generic(a, b),
})
}
fn diffs_generic<T: Copy + PartialEq>(a: &[T], b: &[T]) -> i64 {
let mut diffs = 0i64;
for (i, x) in a.iter().enumerate() {
if Some(x) != b.get(i) {
diffs += 1;
}
}
diffs
}
#[cfg(feature = "parallel")]
pub fn par_hamming_batch(pairs: &[(&str, &str)], ignore_case: bool) -> Vec<i64> {
use rayon::prelude::*;
pairs
.par_iter()
.map(|(a, b)| hamming(a, b, ignore_case))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_differing_positions() {
assert_eq!(hamming("karolin", "kathrin", false), 3);
assert_eq!(hamming("1011101", "1001001", false), 2);
assert_eq!(hamming("abc", "abc", false), 0);
assert_eq!(hamming("", "", false), 0);
}
#[test]
fn length_mismatch_returns_the_sentinel() {
assert_eq!(hamming("abc", "ab", false), INCOMPARABLE);
assert_eq!(hamming_checked("abc", "ab", false), None);
}
#[test]
fn ignore_case_folds_both_sides() {
assert_eq!(hamming("ABC", "abc", false), 3);
assert_eq!(hamming("ABC", "abc", true), 0);
}
#[test]
fn length_is_measured_in_utf16_units() {
assert_ne!(hamming("a😀b", "abcd", false), INCOMPARABLE);
assert_eq!(hamming("a😀b", "ab", false), INCOMPARABLE);
}
#[test]
fn bmp_non_ascii_compares_per_character() {
assert_eq!(hamming("café", "cafe", false), 1);
assert_eq!(hamming("Москва", "Москва", false), 0);
}
struct Xorshift64(u64);
impl Xorshift64 {
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn next_range(&mut self, bound: usize) -> usize {
(self.next_u64() % bound as u64) as usize
}
fn chance(&mut self, one_in: u64) -> bool {
self.next_u64() % one_in == 0
}
}
fn random_ascii(rng: &mut Xorshift64, len: usize, alphabet: usize) -> String {
(0..len)
.map(|_| (b'a' + rng.next_range(alphabet) as u8) as char)
.collect()
}
#[test]
fn fast_lane_agrees_with_the_slow_path_on_random_pairs() {
let mut rng = Xorshift64(0xC0FF_EE00_5EED_0001);
for _ in 0..6000 {
let alphabet = [2usize, 4, 26][rng.next_range(3)];
let l1 = rng.next_range(80);
let l2 = if rng.chance(5) {
rng.next_range(80)
} else {
l1
};
let mut s1 = random_ascii(&mut rng, l1, alphabet);
let mut s2 = random_ascii(&mut rng, l2, alphabet);
if rng.chance(3) {
s1 = s1.to_uppercase();
}
if rng.chance(10) {
s1.push('é');
}
if rng.chance(10) {
s2.push('😀');
}
for ignore_case in [false, true] {
assert_eq!(
hamming(&s1, &s2, ignore_case),
hamming_slow(&s1, &s2, ignore_case),
"fast lane diverged for {s1:?} vs {s2:?} ignore_case={ignore_case}"
);
}
}
}
#[test]
fn kernels_agree_with_a_scalar_count_across_block_boundaries() {
let mut rng = Xorshift64(0x5EED_0BAD_F00D_0002);
let lengths = [
0usize, 1, 7, 8, 9, 15, 16, 17, 31, 32, 63, 64, 65, 100, 2039, 2040, 2041, 4079, 4080,
4081, 5000, 8159, 8160, 8161,
];
for &len in &lengths {
for _ in 0..4 {
let a = random_ascii(&mut rng, len, 2);
let b = random_ascii(&mut rng, len, 2);
let (x, y) = (a.as_bytes(), b.as_bytes());
let scalar = x.iter().zip(y).filter(|(p, q)| p != q).count() as u64;
assert_eq!(swar_diffs(x, y), scalar, "swar_diffs at len {len}");
assert_eq!(
fused_ascii_diffs(x, y),
Some(scalar),
"fused_ascii_diffs at len {len}"
);
}
}
let a = "a".repeat(8161);
let b = "b".repeat(8161);
assert_eq!(swar_diffs(a.as_bytes(), b.as_bytes()), 8161);
assert_eq!(fused_ascii_diffs(a.as_bytes(), b.as_bytes()), Some(8161));
}
#[test]
fn fused_kernel_detects_non_ascii_at_every_position_class() {
let clean = vec![b'x'; 50];
for pos in [0usize, 15, 16, 31, 47, 48, 49] {
for flip_first in [true, false] {
let mut dirty = clean.clone();
dirty[pos] = 0xC3;
let (a, b) = if flip_first {
(&dirty[..], &clean[..])
} else {
(&clean[..], &dirty[..])
};
assert_eq!(
fused_ascii_diffs(a, b),
None,
"missed high bit at {pos} (flip_first={flip_first})"
);
}
}
assert_eq!(fused_ascii_diffs(&clean, &clean), Some(0));
}
#[test]
fn equal_byte_length_non_ascii_takes_the_contract_path() {
let s1 = "ééééééééé"; let s2 = "abcdefghijklmnopqr"; assert_eq!(s1.len(), s2.len());
assert_eq!(hamming(s1, s2, false), INCOMPARABLE);
let s3 = "ééééééééé";
let s4 = "ééééééééà";
assert_eq!(s3.len(), s4.len());
assert_eq!(hamming(s3, s4, false), 1);
assert_eq!(hamming("éé", "ab", false), hamming_slow("éé", "ab", false));
}
#[test]
fn ignore_case_ascii_path_is_allocation_free_fold_equivalent() {
let tricky = ["@AZ[`az{", "ABCdefGH", "zzzZZZzz", "@[`{@[`{"];
for a in tricky {
for b in tricky {
assert_eq!(
hamming(a, b, true),
hamming_slow(a, b, true),
"{a:?} vs {b:?}"
);
}
}
}
}