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_distinct = distinct_count(source, src_start, src_end, equalizer);
242 let tgt_distinct = distinct_count(target, tgt_start, tgt_end, equalizer);
243
244 if (src_distinct > 32 || tgt_distinct > 32)
245 && (has_high_frequency_value(source, src_start, src_end, equalizer, max_chain_length)
246 || has_high_frequency_value(target, tgt_start, tgt_end, equalizer, max_chain_length))
247 {
248 let fallback_algo = MyersDiffWithLinearSpace::new();
249 let sub_source = &source[src_start..src_end];
250 let sub_target = &target[tgt_start..tgt_end];
251
252 let sub_changes = fallback_algo.diff_with_listener(
253 sub_source,
254 sub_target,
255 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
256 );
257
258 for c in sub_changes {
259 push_change(
260 script,
261 c.delta_type,
262 src_start + c.start_original,
263 src_start + c.end_original,
264 tgt_start + c.start_revised,
265 tgt_start + c.end_revised,
266 );
267 }
268 return;
269 }
270
271 if let Some(anchor) = find_best_anchor(
273 source,
274 target,
275 src_start,
276 src_end,
277 tgt_start,
278 tgt_end,
279 equalizer,
280 max_chain_length,
281 ) {
282 histogram_rec(
284 source,
285 target,
286 src_start,
287 anchor.src_idx,
288 tgt_start,
289 anchor.tgt_idx,
290 equalizer,
291 max_chain_length,
292 script,
293 listener.as_deref_mut(),
294 total_steps,
295 );
296
297 histogram_rec(
299 source,
300 target,
301 anchor.src_idx + anchor.len,
302 src_end,
303 anchor.tgt_idx + anchor.len,
304 tgt_end,
305 equalizer,
306 max_chain_length,
307 script,
308 listener,
309 total_steps,
310 );
311 } else {
312 let fallback_algo = MyersDiffWithLinearSpace::new();
314 let sub_source = &source[src_start..src_end];
315 let sub_target = &target[tgt_start..tgt_end];
316
317 let sub_changes = fallback_algo.diff_with_listener(
318 sub_source,
319 sub_target,
320 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
321 );
322
323 for c in sub_changes {
324 push_change(
325 script,
326 c.delta_type,
327 src_start + c.start_original,
328 src_start + c.end_original,
329 tgt_start + c.start_revised,
330 tgt_start + c.end_revised,
331 );
332 }
333 }
334}
335
336struct OccurrenceBucket<'a, T> {
337 value: &'a T,
338 count: usize,
339 first_index: usize,
340}
341
342#[allow(clippy::too_many_arguments)]
343fn should_fallback_to_myers<T, F>(
344 source: &[T],
345 src_start: usize,
346 src_end: usize,
347 target: &[T],
348 tgt_start: usize,
349 tgt_end: usize,
350 equalizer: &F,
351) -> bool
352where
353 T: PartialEq,
354 F: Fn(&T, &T) -> bool + ?Sized,
355{
356 let src_len = src_end - src_start;
357 let tgt_len = tgt_end - tgt_start;
358 if src_len == 0 || tgt_len == 0 {
359 return false;
360 }
361
362 let distinct_source = distinct_count(source, src_start, src_end, equalizer);
363 let distinct_target = distinct_count(target, tgt_start, tgt_end, equalizer);
364
365 (distinct_source > 32 || distinct_target > 32)
366 && (src_len > 0 && tgt_len > 0)
367 && (distinct_source >= 64 || distinct_target >= 64)
368}
369
370fn distinct_count<T, F>(sequence: &[T], start: usize, end: usize, equalizer: &F) -> usize
371where
372 T: PartialEq,
373 F: Fn(&T, &T) -> bool + ?Sized,
374{
375 let mut seen = Vec::new();
376 for i in start..end {
377 let value = &sequence[i];
378 let mut already_seen = false;
379 for prev in &seen {
380 if equalizer(value, *prev) {
381 already_seen = true;
382 break;
383 }
384 }
385 if !already_seen {
386 seen.push(value);
387 }
388 }
389 seen.len()
390}
391
392fn has_high_frequency_value<T, F>(
393 sequence: &[T],
394 start: usize,
395 end: usize,
396 equalizer: &F,
397 max_chain_length: usize,
398) -> bool
399where
400 T: PartialEq,
401 F: Fn(&T, &T) -> bool + ?Sized,
402{
403 let distinct = distinct_count(sequence, start, end, equalizer);
404 if distinct <= 32 {
405 return false;
406 }
407
408 let mut buckets: Vec<OccurrenceBucket<'_, T>> = Vec::new();
409
410 for i in start..end {
411 let value = &sequence[i];
412 let mut found = false;
413
414 for bucket in &mut buckets {
415 if equalizer(value, bucket.value) {
416 bucket.count += 1;
417 found = true;
418 break;
419 }
420 }
421
422 if !found {
423 buckets.push(OccurrenceBucket {
424 value,
425 count: 1,
426 first_index: i,
427 });
428 }
429 }
430
431 buckets.iter().any(|bucket| bucket.count > max_chain_length)
432}
433
434fn find_best_anchor<T, F>(
435 source: &[T],
436 target: &[T],
437 src_start: usize,
438 src_end: usize,
439 tgt_start: usize,
440 tgt_end: usize,
441 equalizer: &F,
442 max_chain_length: usize,
443) -> Option<MatchAnchor>
444where
445 T: PartialEq,
446 F: Fn(&T, &T) -> bool + ?Sized,
447{
448 let mut source_buckets: Vec<OccurrenceBucket<'_, T>> = Vec::new();
449 for i in src_start..src_end {
450 let value = &source[i];
451 let mut bucket_idx = None;
452 for (idx, bucket) in source_buckets.iter_mut().enumerate() {
453 if equalizer(value, bucket.value) {
454 bucket.count += 1;
455 bucket_idx = Some(idx);
456 break;
457 }
458 }
459
460 if bucket_idx.is_none() {
461 source_buckets.push(OccurrenceBucket {
462 value,
463 count: 1,
464 first_index: i,
465 });
466 }
467 }
468
469 let mut target_buckets: Vec<OccurrenceBucket<'_, T>> = Vec::new();
470 for j in tgt_start..tgt_end {
471 let value = &target[j];
472 let mut bucket_idx = None;
473 for (idx, bucket) in target_buckets.iter_mut().enumerate() {
474 if equalizer(value, bucket.value) {
475 bucket.count += 1;
476 bucket_idx = Some(idx);
477 break;
478 }
479 }
480
481 if bucket_idx.is_none() {
482 target_buckets.push(OccurrenceBucket {
483 value,
484 count: 1,
485 first_index: j,
486 });
487 }
488 }
489
490 let mut best_anchor: Option<MatchAnchor> = None;
491 let mut lowest_occurrence = max_chain_length + 1;
492 let mut best_len = 0usize;
493
494 for source_bucket in &source_buckets {
495 let target_bucket = target_buckets
496 .iter()
497 .find(|bucket| equalizer(source_bucket.value, bucket.value));
498
499 let Some(target_bucket) = target_bucket else {
500 continue;
501 };
502
503 let occurrence_count = source_bucket.count.min(target_bucket.count);
504 if occurrence_count == 0 || occurrence_count > max_chain_length {
505 if occurrence_count == 0 {
510 continue;
511 }
512 }
513
514 if occurrence_count == 0 {
515 continue;
516 }
517
518 let src_idx = source_bucket.first_index;
519 let tgt_idx = target_bucket.first_index;
520 let mut len = 1usize;
521 while (src_idx + len) < src_end
522 && (tgt_idx + len) < tgt_end
523 && equalizer(&source[src_idx + len], &target[tgt_idx + len])
524 {
525 len += 1;
526 }
527
528 if occurrence_count < lowest_occurrence
529 || (occurrence_count == lowest_occurrence && len > best_len)
530 {
531 lowest_occurrence = occurrence_count;
532 best_len = len;
533 best_anchor = Some(MatchAnchor {
534 src_idx,
535 tgt_idx,
536 len,
537 });
538
539 if occurrence_count == 1 {
540 return best_anchor;
541 }
542 }
543 }
544
545 best_anchor
546}
547
548fn push_change(
549 script: &mut Vec<Change>,
550 delta_type: DeltaType,
551 src_start: usize,
552 src_end: usize,
553 tgt_start: usize,
554 tgt_end: usize,
555) {
556 if let Some(last) = script.last_mut() {
557 if last.delta_type == delta_type {
558 match delta_type {
559 DeltaType::Delete if last.end_original == src_start => {
560 last.end_original = src_end;
561 return;
562 }
563 DeltaType::Insert if last.end_revised == tgt_start => {
564 last.end_revised = tgt_end;
565 return;
566 }
567 _ => {}
568 }
569 }
570 }
571
572 script.push(Change {
573 delta_type,
574 start_original: src_start,
575 end_original: src_end,
576 start_revised: tgt_start,
577 end_revised: tgt_end,
578 });
579}
580
581fn normalize_replacements(script: &mut Vec<Change>) {
582 let mut normalized: Vec<Change> = Vec::with_capacity(script.len());
583
584 for change in script.drain(..) {
585 if let Some(previous) = normalized.last_mut() {
586 let replacement = match (previous.delta_type, change.delta_type) {
587 (DeltaType::Insert, DeltaType::Delete)
588 if previous.start_original == change.start_original
589 && previous.end_revised == change.start_revised => Some(Change {
590 delta_type: DeltaType::Change,
591 start_original: change.start_original,
592 end_original: change.end_original,
593 start_revised: previous.start_revised,
594 end_revised: change.end_revised,
595 }),
596 (DeltaType::Delete, DeltaType::Insert)
597 if previous.end_original == change.start_original
598 && previous.end_revised == change.start_revised => Some(Change {
599 delta_type: DeltaType::Change,
600 start_original: previous.start_original,
601 end_original: previous.end_original,
602 start_revised: previous.start_revised,
603 end_revised: change.end_revised,
604 }),
605 _ => None,
606 };
607
608 if let Some(replacement) = replacement {
609 *previous = replacement;
610 continue;
611 }
612 }
613
614 normalized.push(change);
615 }
616
617 *script = normalized;
618}
619
620#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
622pub struct HistogramDiffFactory {
623 pub max_chain_length: usize,
624}
625
626impl HistogramDiffFactory {
627 pub fn new() -> Self {
628 Self {
629 max_chain_length: DEFAULT_MAX_CHAIN_LENGTH,
630 }
631 }
632
633 pub fn with_max_chain_length(max_chain_length: usize) -> Self {
634 Self { max_chain_length }
635 }
636}
637
638impl<T: PartialEq + 'static> DiffAlgorithmFactory<T> for HistogramDiffFactory {
639 fn create(&self) -> Box<dyn DiffAlgorithm<T>>
640 where
641 T: PartialEq + 'static,
642 {
643 Box::new(HistogramDiff::new().with_max_chain_length(self.max_chain_length))
644 }
645
646 fn create_with_equalizer(
647 &self,
648 equalizer: Box<dyn Fn(&T, &T) -> bool + 'static>,
649 ) -> Box<dyn DiffAlgorithm<T>> {
650 Box::new(
651 HistogramDiff::new()
652 .with_max_chain_length(self.max_chain_length)
653 .with_equalizer(move |a: &T, b: &T| equalizer(a, b)),
654 )
655 }
656}