pub(crate) const MAX_DIFF_CHARS: usize = 200_000;
const MAX_DIFF_STEPS: usize = 2_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct CharDiff {
pub(crate) chars: usize,
pub(crate) exact: bool,
}
pub(crate) fn char_diff(a: &str, b: &str) -> CharDiff {
let av: Vec<char> = a.chars().collect();
let bv: Vec<char> = b.chars().collect();
let mut head = 0usize;
while head < av.len() && head < bv.len() && av[head] == bv[head] {
head += 1;
}
let (a_rest, b_rest) = (av.len() - head, bv.len() - head);
let mut tail = 0usize;
while tail < a_rest && tail < b_rest && av[av.len() - 1 - tail] == bv[bv.len() - 1 - tail] {
tail += 1;
}
myers(&av[head..av.len() - tail], &bv[head..bv.len() - tail])
}
fn myers(a: &[char], b: &[char]) -> CharDiff {
let (n, m) = (a.len(), b.len());
if n == 0 || m == 0 {
return CharDiff {
chars: n + m,
exact: true,
};
}
let lb = n.abs_diff(m);
if is_subsequence(a, b) || is_subsequence(b, a) {
return CharDiff {
chars: lb,
exact: true,
};
}
if lb > MAX_DIFF_CHARS || lb.saturating_mul(lb) / 2 > MAX_DIFF_STEPS {
return CharDiff {
chars: lb,
exact: false,
};
}
let max_d = (n + m).min(MAX_DIFF_CHARS);
let off = max_d + 1;
let mut v = vec![0isize; 2 * max_d + 3];
let mut steps = 0usize;
for d in 0..=max_d {
let di = d as isize;
for k in (-di..=di).step_by(2) {
let (x, y, slid) = step(a, b, &v, off, di, k);
steps += slid;
let idx = (off as isize + k) as usize;
v[idx] = x;
if x >= n as isize && y >= m as isize {
return CharDiff {
chars: d,
exact: true,
};
}
}
steps += d + 1;
if steps > MAX_DIFF_STEPS {
return CharDiff {
chars: d.max(lb),
exact: false,
};
}
}
CharDiff {
chars: max_d,
exact: false,
}
}
fn is_subsequence(small: &[char], large: &[char]) -> bool {
if small.len() > large.len() {
return false;
}
let mut it = large.iter();
small.iter().all(|c| it.any(|l| l == c))
}
fn step(
a: &[char],
b: &[char],
v: &[isize],
off: usize,
d: isize,
k: isize,
) -> (isize, isize, usize) {
let idx = (off as isize + k) as usize;
let mut x = if k == -d || (k != d && v[idx - 1] < v[idx + 1]) {
v[idx + 1]
} else {
v[idx - 1] + 1
};
let mut y = x - k;
let mut slid = 0usize;
while x >= 0
&& y >= 0
&& x < a.len() as isize
&& y < b.len() as isize
&& a[x as usize] == b[y as usize]
{
x += 1;
y += 1;
slid += 1;
}
(x, y, slid)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn identical_texts_have_no_distance() {
assert_eq!(
char_diff("chart the reef", "chart the reef"),
CharDiff {
chars: 0,
exact: true
}
);
assert_eq!(
char_diff("", ""),
CharDiff {
chars: 0,
exact: true
}
);
}
#[test]
fn a_pure_insertion_counts_the_inserted_characters() {
let d = char_diff("chart the reef", "chart the whole reef");
assert_eq!(
d,
CharDiff {
chars: 6,
exact: true
}
);
assert_eq!(char_diff("look", "take a closer look").chars, 14);
}
#[test]
fn a_pure_deletion_counts_the_deleted_characters() {
let d = char_diff("chart the whole reef", "chart the reef");
assert_eq!(
d,
CharDiff {
chars: 6,
exact: true
}
);
assert_eq!(char_diff("abcd", "").chars, 4);
assert_eq!(char_diff("", "abcd").chars, 4);
}
#[test]
fn a_replacement_costs_a_deletion_plus_an_insertion() {
assert_eq!(char_diff("reef", "reeX").chars, 2);
assert_eq!(char_diff("ab-XYZ-cd", "ab-QP-cd").chars, 5);
}
#[test]
fn the_metric_is_not_the_length_difference() {
assert_eq!(char_diff("abcd", "wxyz").chars, 8);
}
#[test]
fn multibyte_characters_count_as_one_each() {
assert_eq!(char_diff("caf\u{e9} au lait", "caf\u{e8} au lait").chars, 2);
assert_eq!(
char_diff("ok \u{1f600}", "ok "),
CharDiff {
chars: 1,
exact: true
}
);
assert_eq!(
char_diff("\u{6d77}\u{56fe}", "\u{6d77}\u{6d0b}\u{5730}\u{56fe}").chars,
2
);
}
#[test]
fn the_common_head_and_tail_are_stripped_before_the_walk() {
let head = "x".repeat(400_000);
let tail = "y".repeat(400_000);
let a = format!("{head}ALPHA{tail}");
let b = format!("{head}BETA{tail}");
let d = char_diff(&a, &b);
assert!(
d.exact,
"a small edit inside a huge shared body stays exact"
);
assert_eq!(d.chars, 7, "the residual pair is what gets measured");
}
#[test]
fn a_distance_past_the_cap_reports_the_length_difference_not_the_cap() {
let a = "a".repeat(MAX_DIFF_CHARS + 10);
let b = "b".repeat(5);
let d = char_diff(&a, &b);
assert!(!d.exact, "past the cap the result is a floor: {d:?}");
assert_eq!(d.chars, MAX_DIFF_CHARS + 5, "the length difference: {d:?}");
assert!(
d.chars > MAX_DIFF_CHARS,
"never a floor weaker than the length difference: {d:?}"
);
}
#[test]
fn a_draft_the_resend_still_contains_is_exact_for_free() {
let a = "chart the reef";
let b = format!("please {} and the harbor before the tide turns", a);
let d = char_diff(a, &b);
assert!(d.exact, "a contained draft is exact: {d:?}");
assert_eq!(d.chars, b.chars().count() - a.chars().count());
}
#[test]
fn a_long_replacement_reports_the_length_difference_as_a_floor() {
let a = format!("{}Q", "a".repeat(10));
let b = "a".repeat(5_000);
let d = char_diff(&a, &b);
assert!(!d.exact, "{d:?}");
assert_eq!(d.chars, 4_989, "the residual length difference: {d:?}");
}
#[test]
fn a_wide_walk_stops_on_the_step_budget_with_a_floor() {
let a = "a".repeat(60_000);
let b = "b".repeat(60_000);
let d = char_diff(&a, &b);
assert!(!d.exact, "the budget stops the walk: {d:?}");
assert_eq!(
d.chars, 1999,
"the floor is the last fully explored depth: {d:?}"
);
}
fn reference_distance(a: &str, b: &str) -> usize {
let av: Vec<char> = a.chars().collect();
let bv: Vec<char> = b.chars().collect();
let mut lcs = vec![vec![0usize; bv.len() + 1]; av.len() + 1];
for i in 1..=av.len() {
for j in 1..=bv.len() {
lcs[i][j] = if av[i - 1] == bv[j - 1] {
lcs[i - 1][j - 1] + 1
} else {
lcs[i - 1][j].max(lcs[i][j - 1])
};
}
}
av.len() + bv.len() - 2 * lcs[av.len()][bv.len()]
}
#[test]
fn the_walk_is_exact_against_the_reference_on_every_small_pair() {
let mut state = 0x2545_f491_4f6c_dd1du64;
let mut next = move || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
state
};
let alphabet = ['a', 'b', 'c'];
for _ in 0..600 {
let mut pair = [String::new(), String::new()];
for side in &mut pair {
let len = next() % 11;
for _ in 0..len {
side.push(alphabet[(next() % 3) as usize]);
}
}
let (a, b) = (&pair[0], &pair[1]);
let got = char_diff(a, b);
assert!(got.exact, "a tiny pair never hits a bound: {a:?} {b:?}");
assert_eq!(
got.chars,
reference_distance(a, b),
"walked {a:?} against {b:?}"
);
}
}
#[test]
fn a_huge_common_head_is_stripped_before_the_budget_can_see_it() {
let head = "x".repeat(MAX_DIFF_STEPS + 100_000);
let a = format!("{head}ALPHA");
let b = format!("{head}BETA");
let d = char_diff(&a, &b);
assert_eq!(
d,
CharDiff {
chars: 7,
exact: true
},
"the residual pair (ALPH against BET) is what gets measured: {d:?}"
);
}
#[test]
fn a_huge_common_tail_is_stripped_before_the_budget_can_see_it() {
let tail = "y".repeat(MAX_DIFF_STEPS + 100_000);
let a = format!("y{tail}");
let b = format!("CD{tail}");
let d = char_diff(&a, &b);
assert_eq!(
d,
CharDiff {
chars: 3,
exact: true
},
"{d:?}"
);
}
#[test]
fn a_contained_draft_stays_exact_however_far_past_the_cap_it_sits() {
let b = format!("A{}B{}", "c".repeat(300_000), "d".repeat(400_000));
let d = char_diff("AB", &b);
assert_eq!(
d,
CharDiff {
chars: 700_000,
exact: true
},
"{d:?}"
);
assert!(d.chars > MAX_DIFF_CHARS, "well past the cap: {d:?}");
}
#[test]
fn the_walk_runs_when_its_first_affordable_depth_fits_the_budget() {
let a = format!("Q{}", "a".repeat(10));
let b = format!("R{}", "a".repeat(1510));
let d = char_diff(&a, &b);
assert_eq!(
d,
CharDiff {
chars: 1502,
exact: true
},
"{d:?}"
);
}
#[test]
fn the_step_budget_counts_the_characters_slid_not_only_the_diagonals() {
let mid = "m".repeat(MAX_DIFF_STEPS + 100_000);
let a = format!("P{mid}Q");
let b = format!("R{mid}S");
let d = char_diff(&a, &b);
assert_eq!(
d,
CharDiff {
chars: 2,
exact: false
},
"{d:?}"
);
}
#[test]
fn the_budget_gives_up_past_the_limit_not_on_it() {
let mid = "m".repeat(MAX_DIFF_STEPS - 6);
let a = format!("P{mid}Q");
let b = format!("R{mid}S");
let d = char_diff(&a, &b);
assert_eq!(
d,
CharDiff {
chars: 3,
exact: false
},
"{d:?}"
);
}
}