1use crate::algorithm::{
8 change::{Change, DeltaType},
9 diff_algorithm_factory::DiffAlgorithmFactory,
10 diff_algorithm_listener::DiffAlgorithmListener,
11 myers::myers_linear::MyersDiffWithLinearSpace,
12 DiffAlgorithm,
13};
14
15pub const DEFAULT_MAX_CHAIN_LENGTH: usize = 64;
17
18pub struct HistogramDiff<T> {
20 max_chain_length: usize,
21 equalizer: Option<Box<dyn Fn(&T, &T) -> bool>>,
22}
23
24impl<T> Default for HistogramDiff<T> {
25 fn default() -> Self {
26 Self {
27 max_chain_length: DEFAULT_MAX_CHAIN_LENGTH,
28 equalizer: None,
29 }
30 }
31}
32
33impl<T> HistogramDiff<T> {
34 pub fn new() -> Self {
36 Self::default()
37 }
38
39 #[must_use]
41 pub fn with_max_chain_length(mut self, max_chain_length: usize) -> Self {
42 self.max_chain_length = max_chain_length;
43 self
44 }
45
46 #[must_use]
48 pub fn with_equalizer<F>(mut self, equalizer: F) -> Self
49 where
50 F: Fn(&T, &T) -> bool + 'static,
51 {
52 self.equalizer = Some(Box::new(equalizer));
53 self
54 }
55}
56
57impl<T: PartialEq> DiffAlgorithm<T> for HistogramDiff<T> {
58 fn diff_with_listener(
59 &self,
60 source: &[T],
61 target: &[T],
62 listener: &mut dyn DiffAlgorithmListener,
63 ) -> Vec<Change> {
64 let eq: &dyn Fn(&T, &T) -> bool = match &self.equalizer {
65 Some(f) => f.as_ref(),
66 None => &|a, b| a == b,
67 };
68 compute_diff_full(source, target, eq, self.max_chain_length, Some(listener))
69 }
70}
71
72pub fn compute_diff<T: PartialEq>(source: &[T], target: &[T]) -> Vec<Change> {
74 compute_diff_with(source, target, |a, b| a == b)
75}
76
77pub fn compute_diff_with<T, F>(source: &[T], target: &[T], equalizer: F) -> Vec<Change>
79where
80 T: PartialEq,
81 F: Fn(&T, &T) -> bool,
82{
83 compute_diff_full(
84 source,
85 target,
86 &equalizer,
87 DEFAULT_MAX_CHAIN_LENGTH,
88 Option::<&mut dyn DiffAlgorithmListener>::None,
89 )
90}
91
92pub fn compute_diff_full<T, F, L>(
94 source: &[T],
95 target: &[T],
96 equalizer: &F,
97 max_chain_length: usize,
98 mut listener: Option<&mut L>,
99) -> Vec<Change>
100where
101 T: PartialEq,
102 F: Fn(&T, &T) -> bool + ?Sized,
103 L: DiffAlgorithmListener + ?Sized,
104{
105 if source.is_empty() && target.is_empty() {
106 return Vec::new();
107 }
108
109 if let Some(l) = listener.as_deref_mut() {
110 l.diff_start();
111 }
112
113 let mut script = Vec::new();
114 let total_steps = source.len() + target.len();
115
116 histogram_rec(
117 source,
118 target,
119 0,
120 source.len(),
121 0,
122 target.len(),
123 equalizer,
124 max_chain_length,
125 &mut script,
126 listener.as_deref_mut(),
127 total_steps,
128 );
129
130 if let Some(l) = listener {
131 l.diff_end();
132 }
133
134 script
135}
136
137#[derive(Clone, Copy)]
138struct MatchAnchor {
139 src_idx: usize,
140 tgt_idx: usize,
141 len: usize,
142}
143
144#[allow(clippy::too_many_arguments)]
145fn histogram_rec<T, F, L>(
146 source: &[T],
147 target: &[T],
148 mut src_start: usize,
149 mut src_end: usize,
150 mut tgt_start: usize,
151 mut tgt_end: usize,
152 equalizer: &F,
153 max_chain_length: usize,
154 script: &mut Vec<Change>,
155 mut listener: Option<&mut L>,
156 total_steps: usize,
157) where
158 T: PartialEq,
159 F: Fn(&T, &T) -> bool + ?Sized,
160 L: DiffAlgorithmListener + ?Sized,
161{
162 while src_start < src_end
164 && tgt_start < tgt_end
165 && equalizer(&source[src_start], &target[tgt_start])
166 {
167 src_start += 1;
168 tgt_start += 1;
169 }
170
171 while src_end > src_start
173 && tgt_end > tgt_start
174 && equalizer(&source[src_end - 1], &target[tgt_end - 1])
175 {
176 src_end -= 1;
177 tgt_end -= 1;
178 }
179
180 let src_len = src_end - src_start;
181 let tgt_len = tgt_end - tgt_start;
182
183 if src_len == 0 && tgt_len == 0 {
184 return;
185 }
186
187 if let Some(l) = listener.as_deref_mut() {
188 l.diff_step(src_start + tgt_start, total_steps);
189 }
190
191 if src_len == 0 {
193 push_change(
194 script,
195 DeltaType::Insert,
196 src_start,
197 src_start,
198 tgt_start,
199 tgt_end,
200 );
201 return;
202 }
203 if tgt_len == 0 {
204 push_change(
205 script,
206 DeltaType::Delete,
207 src_start,
208 src_end,
209 tgt_start,
210 tgt_start,
211 );
212 return;
213 }
214
215 if let Some(anchor) = find_best_anchor(
217 source,
218 target,
219 src_start,
220 src_end,
221 tgt_start,
222 tgt_end,
223 equalizer,
224 max_chain_length,
225 ) {
226 histogram_rec(
228 source,
229 target,
230 src_start,
231 anchor.src_idx,
232 tgt_start,
233 anchor.tgt_idx,
234 equalizer,
235 max_chain_length,
236 script,
237 listener.as_deref_mut(),
238 total_steps,
239 );
240
241 histogram_rec(
243 source,
244 target,
245 anchor.src_idx + anchor.len,
246 src_end,
247 anchor.tgt_idx + anchor.len,
248 tgt_end,
249 equalizer,
250 max_chain_length,
251 script,
252 listener,
253 total_steps,
254 );
255 } else {
256 let fallback_algo = MyersDiffWithLinearSpace::new();
258 let sub_source = &source[src_start..src_end];
259 let sub_target = &target[tgt_start..tgt_end];
260
261 let sub_changes = fallback_algo.diff_with_listener(
262 sub_source,
263 sub_target,
264 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
265 );
266
267 for c in sub_changes {
268 push_change(
269 script,
270 c.delta_type,
271 src_start + c.start_original,
272 src_start + c.end_original,
273 tgt_start + c.start_revised,
274 tgt_start + c.end_revised,
275 );
276 }
277 }
278}
279
280#[allow(clippy::too_many_arguments)]
281fn find_best_anchor<T, F>(
282 source: &[T],
283 target: &[T],
284 src_start: usize,
285 src_end: usize,
286 tgt_start: usize,
287 tgt_end: usize,
288 equalizer: &F,
289 max_chain_length: usize,
290) -> Option<MatchAnchor>
291where
292 T: PartialEq,
293 F: Fn(&T, &T) -> bool + ?Sized,
294{
295 let mut best_anchor: Option<MatchAnchor> = None;
296 let mut lowest_occurrence = max_chain_length + 1;
297
298 for i in src_start..src_end {
299 let mut count = 0;
300 let mut first_match_j = 0;
301
302 for j in tgt_start..tgt_end {
303 if equalizer(&source[i], &target[j]) {
304 count += 1;
305 if count == 1 {
306 first_match_j = j;
307 }
308 if count >= lowest_occurrence {
309 break;
310 }
311 }
312 }
313
314 if count > 0 && count < lowest_occurrence {
315 lowest_occurrence = count;
316
317 let mut len = 1;
319 while (i + len) < src_end
320 && (first_match_j + len) < tgt_end
321 && equalizer(&source[i + len], &target[first_match_j + len])
322 {
323 len += 1;
324 }
325
326 best_anchor = Some(MatchAnchor {
327 src_idx: i,
328 tgt_idx: first_match_j,
329 len,
330 });
331
332 if count == 1 {
334 break;
335 }
336 }
337 }
338
339 best_anchor
340}
341
342fn push_change(
343 script: &mut Vec<Change>,
344 delta_type: DeltaType,
345 src_start: usize,
346 src_end: usize,
347 tgt_start: usize,
348 tgt_end: usize,
349) {
350 if let Some(last) = script.last_mut() {
351 if last.delta_type == delta_type {
352 match delta_type {
353 DeltaType::Delete if last.end_original == src_start => {
354 last.end_original = src_end;
355 return;
356 }
357 DeltaType::Insert if last.end_revised == tgt_start => {
358 last.end_revised = tgt_end;
359 return;
360 }
361 _ => {}
362 }
363 }
364 }
365
366 script.push(Change {
367 delta_type,
368 start_original: src_start,
369 end_original: src_end,
370 start_revised: tgt_start,
371 end_revised: tgt_end,
372 });
373}
374
375#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
377pub struct HistogramDiffFactory {
378 pub max_chain_length: usize,
379}
380
381impl HistogramDiffFactory {
382 pub fn new() -> Self {
383 Self {
384 max_chain_length: DEFAULT_MAX_CHAIN_LENGTH,
385 }
386 }
387
388 pub fn with_max_chain_length(max_chain_length: usize) -> Self {
389 Self { max_chain_length }
390 }
391}
392
393impl<T: PartialEq + 'static> DiffAlgorithmFactory<T> for HistogramDiffFactory {
394 fn create(&self) -> Box<dyn DiffAlgorithm<T>>
395 where
396 T: PartialEq + 'static,
397 {
398 Box::new(HistogramDiff::new().with_max_chain_length(self.max_chain_length))
399 }
400
401 fn create_with_equalizer(
402 &self,
403 equalizer: Box<dyn Fn(&T, &T) -> bool + 'static>,
404 ) -> Box<dyn DiffAlgorithm<T>> {
405 Box::new(
406 HistogramDiff::new()
407 .with_max_chain_length(self.max_chain_length)
408 .with_equalizer(move |a: &T, b: &T| equalizer(a, b)),
409 )
410 }
411}