1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
// SPDX-License-Identifier: Apache-2.0
#![allow(dead_code)]
//! Myers diff algorithm for text lines.
//!
//! Implements the O(ND) algorithm from:
//! Myers, E.W. (1986). "An O(ND) Difference Algorithm and Its Variations".
//! *Algorithmica* 1(1–4): 251–266.
/// One edit operation in a diff.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditOp {
Equal(String),
Insert(String),
Delete(String),
}
/// Myers diff state.
pub struct MyersDiff {
ops: Vec<EditOp>,
}
impl MyersDiff {
pub fn new() -> Self {
MyersDiff { ops: Vec::new() }
}
pub fn ops(&self) -> &[EditOp] {
&self.ops
}
}
impl Default for MyersDiff {
fn default() -> Self {
Self::new()
}
}
/// Run the Myers O(ND) forward phase.
///
/// Returns `(d, trace)` where `d` is the edit distance and `trace[d]` is the
/// V-array snapshot taken *before* the d-th round of extensions (so that the
/// backtracker can recover which diagonal was active at each depth).
///
/// V is indexed by `k + max` to avoid negative array indices (k = x − y,
/// −max ≤ k ≤ max). Only odd/even parities matching the current D are
/// ever written, matching the standard Myers presentation.
fn myers_forward(old: &[&str], new: &[&str]) -> (usize, Vec<Vec<i64>>) {
let n = old.len();
let m = new.len();
let max = n + m;
// V[k + max] = furthest x reached on diagonal k.
// Allocate 2*max+1 slots; initialise to 0, but the sentinel at k=1
// (index max+1) needs to be 0 so the first step lands at (0,0) via
// "go down from k=1".
let mut v: Vec<i64> = vec![0i64; 2 * max + 1];
let mut trace: Vec<Vec<i64>> = Vec::with_capacity(max + 1);
for d in 0..=max {
// Snapshot V *before* this round so backtracking can use it.
trace.push(v.clone());
for k_raw in (-(d as i64)..=(d as i64)).step_by(2) {
let ki = (k_raw + max as i64) as usize;
// Choose the starting x: either extend the snake from k+1 (move
// down → y increases, insert from new) or from k-1 (move right →
// x increases, delete from old).
let go_down = k_raw == -(d as i64)
|| (k_raw != d as i64
&& v.get(ki.wrapping_sub(1)).copied().unwrap_or(i64::MIN)
< v.get(ki + 1).copied().unwrap_or(i64::MIN));
let mut x: usize = if go_down {
v[ki + 1] as usize // come from k+1: same x (insert)
} else {
v[ki - 1] as usize + 1 // come from k-1: advance x (delete)
};
let mut y = (x as i64 - k_raw) as usize;
// Extend the snake along equal elements.
while x < n && y < m && old[x] == new[y] {
x += 1;
y += 1;
}
v[ki] = x as i64;
if x >= n && y >= m {
// We have found a shortest edit path of length d.
// Push the final V so the backtracker has it at index d+1
// (it reads trace[d] for the state *entering* round d).
// Actually trace already has d+1 entries (indices 0..=d);
// the backtracker iterates d downward and reads trace[d].
return (d, trace);
}
}
}
(max, trace)
}
/// Backtrack through the recorded trace to reconstruct the sequence of
/// `EditOp`s. The algorithm works backwards from (n, m) to (0, 0),
/// peeling off one edit (plus its preceding snake) per depth level.
fn reconstruct(old: &[&str], new: &[&str], trace: &[Vec<i64>], max: usize) -> Vec<EditOp> {
let n = old.len();
let m = new.len();
let mut ops: Vec<EditOp> = Vec::new();
let mut x = n as i64;
let mut y = m as i64;
for d in (1..trace.len()).rev() {
let v = &trace[d];
let k = x - y;
let ki = (k + max as i64) as usize;
// Mirror the choice made during the forward phase at depth d.
let go_down = k == -(d as i64)
|| (k != d as i64
&& v.get(ki.wrapping_sub(1)).copied().unwrap_or(i64::MIN)
< v.get(ki + 1).copied().unwrap_or(i64::MIN));
let prev_k = if go_down { k + 1 } else { k - 1 };
let prev_ki = (prev_k + max as i64) as usize;
let prev_x = v[prev_ki];
let prev_y = prev_x - prev_k;
// Snake: equal elements from the end of the previous edit to the start
// of the current position. After the edit we are at
// (prev_x + delta_x, prev_y + delta_y) and the snake extends from
// there up to (x, y).
let snake_x0 = if go_down { prev_x } else { prev_x + 1 };
let snake_y0 = snake_x0 - k;
// Emit equal ops in reverse (we prepend or push-then-reverse).
let mut cx = x;
let mut cy = y;
while cx > snake_x0 && cy > snake_y0 && cx > 0 && cy > 0 {
cx -= 1;
cy -= 1;
ops.push(EditOp::Equal(old[cx as usize].to_string()));
}
// Emit the single edit that preceded the snake.
if go_down {
// Came down from diagonal k+1: insert new[prev_y].
ops.push(EditOp::Insert(new[prev_y as usize].to_string()));
} else {
// Came right from diagonal k-1: delete old[prev_x].
ops.push(EditOp::Delete(old[prev_x as usize].to_string()));
}
x = prev_x;
y = prev_y;
}
// Any remaining (x, y) > (0, 0) after consuming all edits are equal ops
// along the initial snake (when d=0 the whole content may be identical).
while x > 0 && y > 0 {
x -= 1;
y -= 1;
ops.push(EditOp::Equal(old[x as usize].to_string()));
}
// We built operations backwards; reverse for chronological order.
ops.reverse();
ops
}
/// Compute the edit distance between two line slices (O(ND) Myers).
pub fn edit_distance(a: &[&str], b: &[&str]) -> usize {
if a == b {
return 0;
}
let (d, _trace) = myers_forward(a, b);
d
}
/// Compute Myers diff between two lists of lines.
pub fn diff_lines<'a>(old: &[&'a str], new: &[&'a str]) -> Vec<EditOp> {
if old.is_empty() && new.is_empty() {
return Vec::new();
}
let n = old.len();
let m = new.len();
let max = n + m;
let (d, trace) = myers_forward(old, new);
if d == 0 {
// Sequences are identical.
return old.iter().map(|s| EditOp::Equal(s.to_string())).collect();
}
reconstruct(old, new, &trace, max)
}
/// Count insertions and deletions in a diff result.
pub fn diff_stats(ops: &[EditOp]) -> (usize, usize) {
let mut ins = 0usize;
let mut del = 0usize;
for op in ops {
match op {
EditOp::Insert(_) => ins += 1,
EditOp::Delete(_) => del += 1,
EditOp::Equal(_) => {}
}
}
(ins, del)
}
/// Return true if a and b are identical (zero diff).
pub fn is_same(a: &[&str], b: &[&str]) -> bool {
a == b
}
/// Reconstruct the "new" file from a diff.
pub fn apply_diff(ops: &[EditOp]) -> Vec<String> {
let mut result = Vec::new();
for op in ops {
match op {
EditOp::Equal(s) | EditOp::Insert(s) => result.push(s.clone()),
EditOp::Delete(_) => {}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_same_lines() {
let a = vec!["hello", "world"];
assert!(is_same(&a, &a));
}
#[test]
fn test_diff_empty() {
let ops = diff_lines(&[], &[]);
assert!(ops.is_empty());
}
#[test]
fn test_diff_insert_only() {
let ops = diff_lines(&[], &["new"]);
let (ins, del) = diff_stats(&ops);
assert_eq!(ins, 1);
assert_eq!(del, 0);
}
#[test]
fn test_diff_delete_only() {
let ops = diff_lines(&["old"], &[]);
let (ins, del) = diff_stats(&ops);
assert_eq!(ins, 0);
assert_eq!(del, 1);
}
#[test]
fn test_diff_common_prefix() {
let ops = diff_lines(&["a", "b", "c"], &["a", "b", "d"]);
// First two lines are equal.
let eq_count = ops.iter().filter(|o| matches!(o, EditOp::Equal(_))).count();
assert_eq!(eq_count, 2);
}
#[test]
fn test_apply_diff_roundtrip() {
let ops = diff_lines(&["x"], &["x", "y"]);
let result = apply_diff(&ops);
assert!(result.contains(&"x".to_string()));
assert!(result.contains(&"y".to_string()));
}
#[test]
fn test_edit_distance_zero() {
assert_eq!(edit_distance(&["a"], &["a"]), 0);
}
#[test]
fn test_edit_distance_non_zero() {
let d = edit_distance(&["a", "b"], &["c"]);
assert!(d > 0);
}
#[test]
fn test_myers_diff_new() {
let md = MyersDiff::new();
assert!(md.ops().is_empty());
}
#[test]
fn test_diff_stats_equal_only() {
let ops = diff_lines(&["a"], &["a"]);
let (ins, del) = diff_stats(&ops);
assert_eq!(ins, 0);
assert_eq!(del, 0);
}
/// Myers must find the optimal (shortest) edit path, not a naive
/// prefix-truncation. old=["a","b","c","d"], new=["a","c","d","e"]:
/// the shortest edit is Delete("b") + Insert("e"), keeping a, c, d equal.
#[test]
fn test_diff_interleaved() {
let old = ["a", "b", "c", "d"];
let new = ["a", "c", "d", "e"];
let ops = diff_lines(&old, &new);
// Verify the reconstructed new sequence is correct.
let applied = apply_diff(&ops);
assert_eq!(applied, vec!["a", "c", "d", "e"]);
let (ins, del) = diff_stats(&ops);
// Optimal edit: 1 delete ("b") + 1 insert ("e").
assert_eq!(del, 1, "expected exactly 1 delete, got {del}");
assert_eq!(ins, 1, "expected exactly 1 insert, got {ins}");
// The three elements a, c, d must appear as Equal ops.
let eq_lines: Vec<&str> = ops
.iter()
.filter_map(|o| {
if let EditOp::Equal(s) = o {
Some(s.as_str())
} else {
None
}
})
.collect();
assert_eq!(eq_lines, vec!["a", "c", "d"]);
}
/// Deleting a single middle element: ["a","b","c"] → ["a","c"] has
/// edit distance 1 (just delete "b").
#[test]
fn test_edit_distance_abc_to_ac() {
assert_eq!(edit_distance(&["a", "b", "c"], &["a", "c"]), 1);
}
/// Verify that apply_diff(diff_lines(old, new)) always reconstructs `new`
/// for a richer example.
#[test]
fn test_apply_diff_reconstructs_new() {
let old = ["line1", "line2", "line3", "line4", "line5"];
let new = ["line1", "lineX", "line3", "lineY", "line5"];
let ops = diff_lines(&old, &new);
let result = apply_diff(&ops);
assert_eq!(result, new.to_vec());
}
/// Completely disjoint sequences: all old lines deleted, all new inserted.
#[test]
fn test_diff_completely_different() {
let old = ["a", "b"];
let new = ["c", "d"];
let ops = diff_lines(&old, &new);
let applied = apply_diff(&ops);
assert_eq!(applied, vec!["c", "d"]);
let (ins, del) = diff_stats(&ops);
assert_eq!(ins, 2);
assert_eq!(del, 2);
}
}