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 normalize_replacements(&mut script);
131
132 if let Some(l) = listener {
133 l.diff_end();
134 }
135
136 script
137}
138
139#[derive(Clone, Copy)]
140struct MatchAnchor {
141 src_idx: usize,
142 tgt_idx: usize,
143 len: usize,
144}
145
146#[allow(clippy::too_many_arguments)]
147fn histogram_rec<T, F, L>(
148 source: &[T],
149 target: &[T],
150 mut src_start: usize,
151 mut src_end: usize,
152 mut tgt_start: usize,
153 mut tgt_end: usize,
154 equalizer: &F,
155 max_chain_length: usize,
156 script: &mut Vec<Change>,
157 mut listener: Option<&mut L>,
158 total_steps: usize,
159) where
160 T: PartialEq,
161 F: Fn(&T, &T) -> bool + ?Sized,
162 L: DiffAlgorithmListener + ?Sized,
163{
164 while src_start < src_end
166 && tgt_start < tgt_end
167 && equalizer(&source[src_start], &target[tgt_start])
168 {
169 src_start += 1;
170 tgt_start += 1;
171 }
172
173 while src_end > src_start
175 && tgt_end > tgt_start
176 && equalizer(&source[src_end - 1], &target[tgt_end - 1])
177 {
178 src_end -= 1;
179 tgt_end -= 1;
180 }
181
182 let src_len = src_end - src_start;
183 let tgt_len = tgt_end - tgt_start;
184
185 if src_len == 0 && tgt_len == 0 {
186 return;
187 }
188
189 if let Some(l) = listener.as_deref_mut() {
190 l.diff_step(src_start + tgt_start, total_steps);
191 }
192
193 if should_fallback_to_myers(source, src_start, src_end, target, tgt_start, tgt_end, equalizer) {
194 let fallback_algo = MyersDiffWithLinearSpace::new();
195 let sub_source = &source[src_start..src_end];
196 let sub_target = &target[tgt_start..tgt_end];
197
198 let sub_changes = fallback_algo.diff_with_listener(
199 sub_source,
200 sub_target,
201 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
202 );
203
204 for c in sub_changes {
205 push_change(
206 script,
207 c.delta_type,
208 src_start + c.start_original,
209 src_start + c.end_original,
210 tgt_start + c.start_revised,
211 tgt_start + c.end_revised,
212 );
213 }
214 return;
215 }
216
217 if src_len == 0 {
219 push_change(
220 script,
221 DeltaType::Insert,
222 src_start,
223 src_start,
224 tgt_start,
225 tgt_end,
226 );
227 return;
228 }
229 if tgt_len == 0 {
230 push_change(
231 script,
232 DeltaType::Delete,
233 src_start,
234 src_end,
235 tgt_start,
236 tgt_start,
237 );
238 return;
239 }
240
241 let (src_buckets, src_distinct) = build_buckets(source, src_start, src_end, equalizer);
243 let (tgt_buckets, tgt_distinct) = build_buckets(target, tgt_start, tgt_end, equalizer);
244
245 let src_has_high_freq = (src_distinct > 32)
248 && src_buckets.iter().any(|b| b.count > max_chain_length);
249 let tgt_has_high_freq = (tgt_distinct > 32)
250 && tgt_buckets.iter().any(|b| b.count > max_chain_length);
251
252 if (src_distinct > 32 || tgt_distinct > 32) && (src_has_high_freq || tgt_has_high_freq) {
253 let fallback_algo = MyersDiffWithLinearSpace::new();
254 let sub_source = &source[src_start..src_end];
255 let sub_target = &target[tgt_start..tgt_end];
256
257 let sub_changes = fallback_algo.diff_with_listener(
258 sub_source,
259 sub_target,
260 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
261 );
262
263 for c in sub_changes {
264 push_change(
265 script,
266 c.delta_type,
267 src_start + c.start_original,
268 src_start + c.end_original,
269 tgt_start + c.start_revised,
270 tgt_start + c.end_revised,
271 );
272 }
273 return;
274 }
275 drop(src_buckets);
279 drop(tgt_buckets);
280
281 if let Some(anchor) = find_best_anchor(
283 source,
284 target,
285 src_start,
286 src_end,
287 tgt_start,
288 tgt_end,
289 equalizer,
290 max_chain_length,
291 ) {
292 histogram_rec(
294 source,
295 target,
296 src_start,
297 anchor.src_idx,
298 tgt_start,
299 anchor.tgt_idx,
300 equalizer,
301 max_chain_length,
302 script,
303 listener.as_deref_mut(),
304 total_steps,
305 );
306
307 histogram_rec(
309 source,
310 target,
311 anchor.src_idx + anchor.len,
312 src_end,
313 anchor.tgt_idx + anchor.len,
314 tgt_end,
315 equalizer,
316 max_chain_length,
317 script,
318 listener,
319 total_steps,
320 );
321 } else {
322 let fallback_algo = MyersDiffWithLinearSpace::new();
324 let sub_source = &source[src_start..src_end];
325 let sub_target = &target[tgt_start..tgt_end];
326
327 let sub_changes = fallback_algo.diff_with_listener(
328 sub_source,
329 sub_target,
330 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
331 );
332
333 for c in sub_changes {
334 push_change(
335 script,
336 c.delta_type,
337 src_start + c.start_original,
338 src_start + c.end_original,
339 tgt_start + c.start_revised,
340 tgt_start + c.end_revised,
341 );
342 }
343 }
344}
345
346struct OccurrenceBucket<'a, T> {
347 value: &'a T,
348 count: usize,
349 first_index: usize,
350}
351
352fn build_buckets<'a, T, F>(
358 sequence: &'a [T],
359 start: usize,
360 end: usize,
361 equalizer: &F,
362) -> (Vec<OccurrenceBucket<'a, T>>, usize)
363where
364 T: PartialEq,
365 F: Fn(&T, &T) -> bool + ?Sized,
366{
367 let mut buckets: Vec<OccurrenceBucket<'a, T>> = Vec::new();
368
369 for i in start..end {
370 let value = &sequence[i];
371 let mut found = false;
372
373 for bucket in &mut buckets {
374 if equalizer(value, bucket.value) {
375 bucket.count += 1;
376 found = true;
377 break;
378 }
379 }
380
381 if !found {
382 buckets.push(OccurrenceBucket {
383 value,
384 count: 1,
385 first_index: i,
386 });
387 }
388 }
389
390 let distinct = buckets.len();
391 (buckets, distinct)
392}
393
394#[allow(clippy::too_many_arguments)]
395fn should_fallback_to_myers<T, F>(
396 source: &[T],
397 src_start: usize,
398 src_end: usize,
399 target: &[T],
400 tgt_start: usize,
401 tgt_end: usize,
402 equalizer: &F,
403) -> bool
404where
405 T: PartialEq,
406 F: Fn(&T, &T) -> bool + ?Sized,
407{
408 let src_len = src_end - src_start;
409 let tgt_len = tgt_end - tgt_start;
410 if src_len == 0 || tgt_len == 0 {
411 return false;
412 }
413
414 let (_, distinct_source) = build_buckets(source, src_start, src_end, equalizer);
416 let (_, distinct_target) = build_buckets(target, tgt_start, tgt_end, equalizer);
417
418 (distinct_source > 32 || distinct_target > 32)
419 && (distinct_source >= 64 || distinct_target >= 64)
420}
421
422fn find_best_anchor<T, F>(
423 source: &[T],
424 target: &[T],
425 src_start: usize,
426 src_end: usize,
427 tgt_start: usize,
428 tgt_end: usize,
429 equalizer: &F,
430 max_chain_length: usize,
431) -> Option<MatchAnchor>
432where
433 T: PartialEq,
434 F: Fn(&T, &T) -> bool + ?Sized,
435{
436 let (mut source_buckets, _) = build_buckets(source, src_start, src_end, equalizer);
438 let (target_buckets, _) = build_buckets(target, tgt_start, tgt_end, equalizer);
439
440 source_buckets.sort_unstable_by_key(|b| b.count);
443
444 let mut best_anchor: Option<MatchAnchor> = None;
445 let mut lowest_occurrence = max_chain_length + 1;
446 let mut best_len = 0usize;
447
448 for source_bucket in &source_buckets {
449 let target_bucket = target_buckets
450 .iter()
451 .find(|bucket| equalizer(source_bucket.value, bucket.value));
452
453 let Some(target_bucket) = target_bucket else {
454 continue;
455 };
456
457 let occurrence_count = source_bucket.count.min(target_bucket.count);
458 if occurrence_count > max_chain_length {
462 continue;
463 }
464
465 let src_idx = source_bucket.first_index;
466 let tgt_idx = target_bucket.first_index;
467 let mut len = 1usize;
468 while (src_idx + len) < src_end
469 && (tgt_idx + len) < tgt_end
470 && equalizer(&source[src_idx + len], &target[tgt_idx + len])
471 {
472 len += 1;
473 }
474
475 if occurrence_count < lowest_occurrence
476 || (occurrence_count == lowest_occurrence && len > best_len)
477 {
478 lowest_occurrence = occurrence_count;
479 best_len = len;
480 best_anchor = Some(MatchAnchor {
481 src_idx,
482 tgt_idx,
483 len,
484 });
485
486 if occurrence_count == 1 {
488 return best_anchor;
489 }
490 }
491 }
492
493 best_anchor
494}
495
496fn push_change(
497 script: &mut Vec<Change>,
498 delta_type: DeltaType,
499 src_start: usize,
500 src_end: usize,
501 tgt_start: usize,
502 tgt_end: usize,
503) {
504 if let Some(last) = script.last_mut() {
505 if last.delta_type == delta_type {
506 match delta_type {
507 DeltaType::Delete if last.end_original == src_start => {
508 last.end_original = src_end;
509 return;
510 }
511 DeltaType::Insert if last.end_revised == tgt_start => {
512 last.end_revised = tgt_end;
513 return;
514 }
515 _ => {}
516 }
517 }
518 }
519
520 script.push(Change {
521 delta_type,
522 start_original: src_start,
523 end_original: src_end,
524 start_revised: tgt_start,
525 end_revised: tgt_end,
526 });
527}
528
529fn normalize_replacements(script: &mut Vec<Change>) {
530 let mut normalized: Vec<Change> = Vec::with_capacity(script.len());
531
532 for change in script.drain(..) {
533 if let Some(previous) = normalized.last_mut() {
534 let replacement = match (previous.delta_type, change.delta_type) {
535 (DeltaType::Insert, DeltaType::Delete)
536 if previous.start_original == change.start_original
537 && previous.end_revised == change.start_revised => Some(Change {
538 delta_type: DeltaType::Change,
539 start_original: change.start_original,
540 end_original: change.end_original,
541 start_revised: previous.start_revised,
542 end_revised: change.end_revised,
543 }),
544 (DeltaType::Delete, DeltaType::Insert)
545 if previous.end_original == change.start_original
546 && previous.end_revised == change.start_revised => Some(Change {
547 delta_type: DeltaType::Change,
548 start_original: previous.start_original,
549 end_original: previous.end_original,
550 start_revised: previous.start_revised,
551 end_revised: change.end_revised,
552 }),
553 _ => None,
554 };
555
556 if let Some(replacement) = replacement {
557 *previous = replacement;
558 continue;
559 }
560 }
561
562 normalized.push(change);
563 }
564
565 *script = normalized;
566}
567
568#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
570pub struct HistogramDiffFactory {
571 pub max_chain_length: usize,
572}
573
574impl HistogramDiffFactory {
575 pub fn new() -> Self {
576 Self {
577 max_chain_length: DEFAULT_MAX_CHAIN_LENGTH,
578 }
579 }
580
581 pub fn with_max_chain_length(max_chain_length: usize) -> Self {
582 Self { max_chain_length }
583 }
584}
585
586impl<T: PartialEq + 'static> DiffAlgorithmFactory<T> for HistogramDiffFactory {
587 fn create(&self) -> Box<dyn DiffAlgorithm<T>>
588 where
589 T: PartialEq + 'static,
590 {
591 Box::new(HistogramDiff::new().with_max_chain_length(self.max_chain_length))
592 }
593
594 fn create_with_equalizer(
595 &self,
596 equalizer: Box<dyn Fn(&T, &T) -> bool + 'static>,
597 ) -> Box<dyn DiffAlgorithm<T>> {
598 Box::new(
599 HistogramDiff::new()
600 .with_max_chain_length(self.max_chain_length)
601 .with_equalizer(move |a: &T, b: &T| equalizer(a, b)),
602 )
603 }
604}