Skip to main content

java_diff_utils_rs/algorithm/myers/
myers_linear.rs

1//! Eugene Myers linear space diff algorithm with O(N) space complexity.
2use crate::algorithm::{
3    change::{Change, DeltaType},
4    diff_algorithm_listener::DiffAlgorithmListener,
5    DiffAlgorithm,
6};
7
8/// A Snake represents a diagonal run of identical elements between two sequences.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10struct Snake {
11    start: usize,
12    end: usize,
13    diag: isize,
14}
15
16pub struct MyersDiffWithLinearSpace<T> {
17    equalizer: Option<Box<dyn Fn(&T, &T) -> bool>>,
18}
19
20impl<T> Default for MyersDiffWithLinearSpace<T> {
21    fn default() -> Self {
22        Self { equalizer: None }
23    }
24}
25
26impl<T> MyersDiffWithLinearSpace<T> {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    pub fn with_equalizer<F>(equalizer: F) -> Self
32    where
33        F: Fn(&T, &T) -> bool + 'static,
34    {
35        Self {
36            equalizer: Some(Box::new(equalizer)),
37        }
38    }
39}
40
41impl<T: PartialEq> DiffAlgorithm<T> for MyersDiffWithLinearSpace<T> {
42    fn diff_with_listener(
43        &self,
44        source: &[T],
45        target: &[T],
46        listener: &mut dyn DiffAlgorithmListener,
47    ) -> Vec<Change> {
48        let mut ws = LinearWorkspace::new();
49        if let Some(ref eq) = self.equalizer {
50            compute_diff_full(source, target, eq, &mut ws, Some(listener))
51        } else {
52            compute_diff_full(source, target, |a, b| a == b, &mut ws, Some(listener))
53        }
54    }
55}
56
57/// Pre-allocated workspace to avoid dynamic vector re-allocations during recursive divide-and-conquer steps.
58#[derive(Default)]
59pub struct LinearWorkspace {
60    v_down: Vec<usize>,
61    v_up: Vec<usize>,
62}
63
64impl LinearWorkspace {
65    pub fn new() -> Self {
66        Self::default()
67    }
68
69    fn prepare_buffers(&mut self, required_len: usize) {
70        if self.v_down.len() < required_len {
71            self.v_down.resize(required_len, 0);
72            self.v_up.resize(required_len, 0);
73        } else {
74            self.v_down[..required_len].fill(0);
75            self.v_up[..required_len].fill(0);
76        }
77    }
78}
79
80/// No-op listener used as a default when no progress updates are requested.
81pub struct NoOpListener;
82impl DiffAlgorithmListener for NoOpListener {}
83
84pub fn compute_diff<T: PartialEq>(source: &[T], target: &[T]) -> Vec<Change> {
85    compute_diff_with(source, target, |a, b| a == b)
86}
87
88pub fn compute_diff_with<T, F>(source: &[T], target: &[T], equalizer: F) -> Vec<Change>
89where
90    F: Fn(&T, &T) -> bool,
91{
92    let mut workspace = LinearWorkspace::new();
93    compute_diff_full(
94        source,
95        target,
96        equalizer,
97        &mut workspace,
98        Option::<&mut NoOpListener>::None,
99    )
100}
101
102pub fn compute_diff_full<T, F, L>(
103    source: &[T],
104    target: &[T],
105    equalizer: F,
106    workspace: &mut LinearWorkspace,
107    mut listener: Option<&mut L>,
108) -> Vec<Change>
109where
110    F: Fn(&T, &T) -> bool,
111    L: DiffAlgorithmListener + ?Sized,
112{
113    if source.is_empty() && target.is_empty() {
114        return Vec::new();
115    }
116
117    if let Some(l) = listener.as_deref_mut() {
118        l.diff_start();
119    }
120
121    let buffer_size = source.len() + target.len() + 2;
122    workspace.prepare_buffers(buffer_size);
123
124    let mut script = Vec::new();
125    let max_steps = source.len() + target.len();
126
127    partition_and_build(
128        source,
129        target,
130        &equalizer,
131        SubRegion {
132            src_start: 0,
133            src_end: source.len(),
134            tgt_start: 0,
135            tgt_end: target.len(),
136        },
137        workspace,
138        &mut script,
139        listener.as_deref_mut(),
140        max_steps,
141    );
142
143    if let Some(l) = listener {
144        l.diff_end();
145    }
146
147    script
148}
149
150/// Represents the active slicing window during recursion.
151#[derive(Clone, Copy)]
152struct SubRegion {
153    src_start: usize,
154    src_end: usize,
155    tgt_start: usize,
156    tgt_end: usize,
157}
158
159fn push_change(
160    script: &mut Vec<Change>,
161    delta_type: DeltaType,
162    src_start: usize,
163    src_end: usize,
164    tgt_start: usize,
165    tgt_end: usize,
166) {
167    // Coalesce contiguous operations of the same delta type
168    if let Some(last) = script.last_mut() {
169        if last.delta_type == delta_type {
170            match delta_type {
171                DeltaType::Delete if last.end_original == src_start => {
172                    last.end_original = src_end;
173                    return;
174                }
175                DeltaType::Insert if last.end_revised == tgt_start => {
176                    last.end_revised = tgt_end;
177                    return;
178                }
179                _ => {}
180            }
181        }
182    }
183
184    script.push(Change {
185        delta_type,
186        start_original: src_start,
187        end_original: src_end,
188        start_revised: tgt_start,
189        end_revised: tgt_end,
190    });
191}
192
193fn partition_and_build<T, F, L>(
194    source: &[T],
195    target: &[T],
196    equalizer: &F,
197    region: SubRegion,
198    ws: &mut LinearWorkspace,
199    script: &mut Vec<Change>,
200    mut listener: Option<&mut L>,
201    max_steps: usize,
202) where
203    F: Fn(&T, &T) -> bool,
204    L: DiffAlgorithmListener + ?Sized,
205{
206    if let Some(l) = listener.as_deref_mut() {
207        let step =
208            (region.src_end - region.src_start) / 2 + (region.tgt_end - region.tgt_start) / 2;
209        l.diff_step(step, max_steps);
210    }
211
212    let middle_snake = find_middle_snake(source, target, equalizer, region, ws);
213
214    let reached_terminal = match middle_snake {
215        None => true,
216        Some(s) => {
217            let diag_offset = region.src_end as isize - region.tgt_end as isize;
218            let start_offset = region.src_start as isize - region.tgt_start as isize;
219
220            (s.start == region.src_end && s.diag == diag_offset)
221                || (s.end == region.src_start && s.diag == start_offset)
222        }
223    };
224
225    if reached_terminal {
226        let mut i = region.src_start;
227        let mut j = region.tgt_start;
228
229        while i < region.src_end || j < region.tgt_end {
230            if i < region.src_end && j < region.tgt_end && equalizer(&source[i], &target[j]) {
231                i += 1;
232                j += 1;
233            } else if (region.src_end - i) > (region.tgt_end - j) {
234                push_change(script, DeltaType::Delete, i, i + 1, j, j);
235                i += 1;
236            } else {
237                push_change(script, DeltaType::Insert, i, i, j, j + 1);
238                j += 1;
239            }
240        }
241    } else if let Some(snake) = middle_snake {
242        let mid_tgt_1 = (snake.start as isize - snake.diag) as usize;
243        let mid_tgt_2 = (snake.end as isize - snake.diag) as usize;
244
245        // Left split branch
246        partition_and_build(
247            source,
248            target,
249            equalizer,
250            SubRegion {
251                src_start: region.src_start,
252                src_end: snake.start,
253                tgt_start: region.tgt_start,
254                tgt_end: mid_tgt_1,
255            },
256            ws,
257            script,
258            listener.as_deref_mut(),
259            max_steps,
260        );
261
262        // Right split branch
263        partition_and_build(
264            source,
265            target,
266            equalizer,
267            SubRegion {
268                src_start: snake.end,
269                src_end: region.src_end,
270                tgt_start: mid_tgt_2,
271                tgt_end: region.tgt_end,
272            },
273            ws,
274            script,
275            listener,
276            max_steps,
277        );
278    }
279}
280
281fn find_middle_snake<T, F>(
282    source: &[T],
283    target: &[T],
284    equalizer: &F,
285    region: SubRegion,
286    ws: &mut LinearWorkspace,
287) -> Option<Snake>
288where
289    F: Fn(&T, &T) -> bool,
290{
291    let src_len = region.src_end - region.src_start;
292    let tgt_len = region.tgt_end - region.tgt_start;
293
294    if src_len == 0 || tgt_len == 0 {
295        return None;
296    }
297
298    let delta = src_len as isize - tgt_len as isize;
299    let total_len = tgt_len + src_len;
300    let offset = if total_len.is_multiple_of(2) {
301        total_len
302    } else {
303        total_len + 1
304    } / 2;
305
306    ws.v_down[1 + offset] = region.src_start;
307    ws.v_up[1 + offset] = region.src_end + 1;
308
309    for d in 0..=offset {
310        let d_step = d as isize;
311
312        // --- Downward Search ---
313        for k in (-d_step..=d_step).step_by(2) {
314            let idx = (k + offset as isize) as usize;
315
316            if k == -d_step || (k != d_step && ws.v_down[idx - 1] < ws.v_down[idx + 1]) {
317                ws.v_down[idx] = ws.v_down[idx + 1];
318            } else {
319                ws.v_down[idx] = ws.v_down[idx - 1] + 1;
320            }
321
322            let mut x = ws.v_down[idx];
323            let mut y =
324                (x as isize - region.src_start as isize + region.tgt_start as isize - k) as usize;
325
326            while x < region.src_end && y < region.tgt_end && equalizer(&source[x], &target[y]) {
327                x += 1;
328                ws.v_down[idx] = x;
329                y += 1;
330            }
331
332            if delta % 2 != 0 && (delta - d_step) <= k && k <= (delta + d_step) {
333                let up_idx = (idx as isize - delta) as usize;
334                if ws.v_up.get(up_idx).is_some_and(|&v| v <= ws.v_down[idx]) {
335                    return Some(expand_snake(
336                        source,
337                        target,
338                        equalizer,
339                        ws.v_up[up_idx],
340                        k + region.src_start as isize - region.tgt_start as isize,
341                        region.src_end,
342                        region.tgt_end,
343                    ));
344                }
345            }
346        }
347
348        // --- Upward Search ---
349        let k_min = delta - d_step;
350        let k_max = delta + d_step;
351        for k in (k_min..=k_max).step_by(2) {
352            let idx = (k + offset as isize - delta) as usize;
353
354            if k == k_min || (k != k_max && ws.v_up[idx + 1] <= ws.v_up[idx - 1]) {
355                ws.v_up[idx] = ws.v_up[idx + 1].saturating_sub(1);
356            } else {
357                ws.v_up[idx] = ws.v_up[idx - 1];
358            }
359
360            let mut x = ws.v_up[idx].saturating_sub(1);
361            let mut y =
362                (x as isize - region.src_start as isize + region.tgt_start as isize - k) as usize;
363
364            while x >= region.src_start
365                && y >= region.tgt_start
366                && x < region.src_end
367                && y < region.tgt_end
368                && equalizer(&source[x], &target[y])
369            {
370                ws.v_up[idx] = x;
371                if x == 0 || y == 0 {
372                    break;
373                }
374                x -= 1;
375                y -= 1;
376            }
377
378            if delta % 2 == 0 && -d_step <= k && k <= d_step {
379                let down_idx = (idx as isize + delta) as usize;
380                if ws.v_down.get(down_idx).is_some_and(|&v| ws.v_up[idx] <= v) {
381                    return Some(expand_snake(
382                        source,
383                        target,
384                        equalizer,
385                        ws.v_up[idx],
386                        k + region.src_start as isize - region.tgt_start as isize,
387                        region.src_end,
388                        region.tgt_end,
389                    ));
390                }
391            }
392        }
393    }
394
395    Some(Snake {
396        start: region.src_start,
397        end: region.src_end,
398        diag: region.src_start as isize - region.tgt_start as isize,
399    })
400}
401
402fn expand_snake<T, F>(
403    source: &[T],
404    target: &[T],
405    equalizer: &F,
406    start: usize,
407    diag: isize,
408    src_bound: usize,
409    tgt_bound: usize,
410) -> Snake
411where
412    F: Fn(&T, &T) -> bool,
413{
414    let mut end = start;
415    while (end as isize - diag) < tgt_bound as isize
416        && end < src_bound
417        && equalizer(&source[end], &target[(end as isize - diag) as usize])
418    {
419        end += 1;
420    }
421
422    Snake { start, end, diag }
423}