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};
14use std::collections::{HashMap, HashSet};
15use std::hash::Hash;
16
17pub const DEFAULT_MAX_CHAIN_LENGTH: usize = 64;
19
20pub struct HistogramDiff<T> {
22 max_chain_length: usize,
23 equalizer: Option<Box<dyn Fn(&T, &T) -> bool>>,
24}
25
26impl<T> Default for HistogramDiff<T> {
27 fn default() -> Self {
28 Self {
29 max_chain_length: DEFAULT_MAX_CHAIN_LENGTH,
30 equalizer: None,
31 }
32 }
33}
34
35impl<T> HistogramDiff<T> {
36 pub fn new() -> Self {
38 Self::default()
39 }
40
41 #[must_use]
43 pub fn with_max_chain_length(mut self, max_chain_length: usize) -> Self {
44 self.max_chain_length = max_chain_length;
45 self
46 }
47
48 #[must_use]
50 pub fn with_equalizer<F>(mut self, equalizer: F) -> Self
51 where
52 F: Fn(&T, &T) -> bool + 'static,
53 {
54 self.equalizer = Some(Box::new(equalizer));
55 self
56 }
57}
58
59impl<T: Eq + Hash> DiffAlgorithm<T> for HistogramDiff<T> {
60 fn diff_with_listener(
61 &self,
62 source: &[T],
63 target: &[T],
64 listener: &mut dyn DiffAlgorithmListener,
65 ) -> Vec<Change> {
66 let eq: &dyn Fn(&T, &T) -> bool = match &self.equalizer {
67 Some(f) => f.as_ref(),
68 None => &|a, b| a == b,
69 };
70 compute_diff_full(source, target, eq, self.max_chain_length, Some(listener))
71 }
72}
73
74pub fn compute_diff<T: Eq + Hash>(source: &[T], target: &[T]) -> Vec<Change> {
76 compute_diff_with(source, target, |a, b| a == b)
77}
78
79pub fn compute_diff_with<T, F>(source: &[T], target: &[T], equalizer: F) -> Vec<Change>
81where
82 T: Eq + Hash,
83 F: Fn(&T, &T) -> bool,
84{
85 compute_diff_full(
86 source,
87 target,
88 &equalizer,
89 DEFAULT_MAX_CHAIN_LENGTH,
90 Option::<&mut dyn DiffAlgorithmListener>::None,
91 )
92}
93
94pub fn compute_diff_full<T, F, L>(
96 source: &[T],
97 target: &[T],
98 equalizer: &F,
99 max_chain_length: usize,
100 mut listener: Option<&mut L>,
101) -> Vec<Change>
102where
103 T: Eq + Hash,
104 F: Fn(&T, &T) -> bool + ?Sized,
105 L: DiffAlgorithmListener + ?Sized,
106{
107 if source.is_empty() && target.is_empty() {
108 return Vec::new();
109 }
110
111 if let Some(l) = listener.as_deref_mut() {
112 l.diff_start();
113 }
114
115 let mut script = Vec::new();
116 let total_steps = source.len() + target.len();
117
118 histogram_rec(
119 source,
120 target,
121 0,
122 source.len(),
123 0,
124 target.len(),
125 equalizer,
126 max_chain_length,
127 &mut script,
128 listener.as_deref_mut(),
129 total_steps,
130 );
131
132 normalize_replacements(&mut script);
133
134 if let Some(l) = listener {
135 l.diff_end();
136 }
137
138 script
139}
140
141#[derive(Clone, Copy)]
142struct MatchAnchor {
143 src_idx: usize,
144 tgt_idx: usize,
145 len: usize,
146}
147
148#[allow(clippy::too_many_arguments)]
149fn histogram_rec<T, F, L>(
150 source: &[T],
151 target: &[T],
152 mut src_start: usize,
153 mut src_end: usize,
154 mut tgt_start: usize,
155 mut tgt_end: usize,
156 equalizer: &F,
157 max_chain_length: usize,
158 script: &mut Vec<Change>,
159 mut listener: Option<&mut L>,
160 total_steps: usize,
161) where
162 T: Eq + Hash,
163 F: Fn(&T, &T) -> bool + ?Sized,
164 L: DiffAlgorithmListener + ?Sized,
165{
166 while src_start < src_end
168 && tgt_start < tgt_end
169 && equalizer(&source[src_start], &target[tgt_start])
170 {
171 src_start += 1;
172 tgt_start += 1;
173 }
174
175 while src_end > src_start
177 && tgt_end > tgt_start
178 && equalizer(&source[src_end - 1], &target[tgt_end - 1])
179 {
180 src_end -= 1;
181 tgt_end -= 1;
182 }
183
184 let src_len = src_end - src_start;
185 let tgt_len = tgt_end - tgt_start;
186
187 if src_len == 0 && tgt_len == 0 {
188 return;
189 }
190
191 if let Some(l) = listener.as_deref_mut() {
192 l.diff_step(src_start + tgt_start, total_steps);
193 }
194
195 if src_len == 0 {
197 push_change(
198 script,
199 DeltaType::Insert,
200 src_start,
201 src_start,
202 tgt_start,
203 tgt_end,
204 );
205 return;
206 }
207 if tgt_len == 0 {
208 push_change(
209 script,
210 DeltaType::Delete,
211 src_start,
212 src_end,
213 tgt_start,
214 tgt_start,
215 );
216 return;
217 }
218
219 if src_len > 256
220 && tgt_len > 256
221 && ((looks_like_high_entropy(source, src_start, src_end)
222 && looks_like_high_entropy(target, tgt_start, tgt_end))
223 || (looks_like_low_entropy(source, src_start, src_end)
224 && looks_like_low_entropy(target, tgt_start, tgt_end)))
225 {
226 let fallback_algo = MyersDiffWithLinearSpace::new();
227 let sub_source = &source[src_start..src_end];
228 let sub_target = &target[tgt_start..tgt_end];
229 let sub_changes = fallback_algo.diff_with_listener(
230 sub_source,
231 sub_target,
232 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
233 );
234 for c in sub_changes {
235 push_change(
236 script,
237 c.delta_type,
238 src_start + c.start_original,
239 src_start + c.end_original,
240 tgt_start + c.start_revised,
241 tgt_start + c.end_revised,
242 );
243 }
244 return;
245 }
246
247 let (src_buckets, _) = build_buckets(source, src_start, src_end, equalizer);
249 let (tgt_buckets, _) = build_buckets(target, tgt_start, tgt_end, equalizer);
250 let src_distinct = src_buckets.len();
251 let tgt_distinct = tgt_buckets.len();
252
253 let src_has_high_freq = src_buckets
256 .iter()
257 .any(|b| b.positions.len() > max_chain_length);
258 let tgt_has_high_freq = tgt_buckets
259 .iter()
260 .any(|b| b.positions.len() > max_chain_length);
261
262 let src_max_frequency = src_buckets
266 .iter()
267 .map(|bucket| bucket.positions.len())
268 .max()
269 .unwrap_or(0);
270 let tgt_max_frequency = tgt_buckets
271 .iter()
272 .map(|bucket| bucket.positions.len())
273 .max()
274 .unwrap_or(0);
275 let low_repetition = (src_distinct > 32 || tgt_distinct > 32)
276 && src_max_frequency <= 2
277 && tgt_max_frequency <= 2;
278
279 if src_has_high_freq || tgt_has_high_freq || low_repetition {
280 let fallback_algo = MyersDiffWithLinearSpace::new();
281 let sub_source = &source[src_start..src_end];
282 let sub_target = &target[tgt_start..tgt_end];
283
284 let sub_changes = fallback_algo.diff_with_listener(
285 sub_source,
286 sub_target,
287 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
288 );
289
290 for c in sub_changes {
291 push_change(
292 script,
293 c.delta_type,
294 src_start + c.start_original,
295 src_start + c.end_original,
296 tgt_start + c.start_revised,
297 tgt_start + c.end_revised,
298 );
299 }
300 return;
301 }
302 drop(src_buckets);
306 drop(tgt_buckets);
307
308 if let Some(anchor) = find_best_anchor(
310 source,
311 target,
312 src_start,
313 src_end,
314 tgt_start,
315 tgt_end,
316 equalizer,
317 max_chain_length,
318 ) {
319 histogram_rec(
321 source,
322 target,
323 src_start,
324 anchor.src_idx,
325 tgt_start,
326 anchor.tgt_idx,
327 equalizer,
328 max_chain_length,
329 script,
330 listener.as_deref_mut(),
331 total_steps,
332 );
333
334 histogram_rec(
336 source,
337 target,
338 anchor.src_idx + anchor.len,
339 src_end,
340 anchor.tgt_idx + anchor.len,
341 tgt_end,
342 equalizer,
343 max_chain_length,
344 script,
345 listener,
346 total_steps,
347 );
348 } else {
349 let fallback_algo = MyersDiffWithLinearSpace::new();
351 let sub_source = &source[src_start..src_end];
352 let sub_target = &target[tgt_start..tgt_end];
353
354 let sub_changes = fallback_algo.diff_with_listener(
355 sub_source,
356 sub_target,
357 &mut crate::algorithm::diff_algorithm_listener::NoOpListener,
358 );
359
360 for c in sub_changes {
361 push_change(
362 script,
363 c.delta_type,
364 src_start + c.start_original,
365 src_start + c.end_original,
366 tgt_start + c.start_revised,
367 tgt_start + c.end_revised,
368 );
369 }
370 }
371}
372
373struct OccurrenceBucket<'a, T> {
374 value: &'a T,
375 positions: Vec<usize>,
376}
377
378fn build_buckets<'a, T, F>(
382 sequence: &'a [T],
383 start: usize,
384 end: usize,
385 equalizer: &F,
386) -> (Vec<OccurrenceBucket<'a, T>>, HashMap<&'a T, usize>)
387where
388 T: Eq + Hash,
389 F: Fn(&T, &T) -> bool + ?Sized,
390{
391 let mut buckets: Vec<OccurrenceBucket<'a, T>> = Vec::new();
392 let mut bucket_by_value: HashMap<&'a T, usize> = HashMap::new();
393
394 for i in start..end {
395 let value = &sequence[i];
396 if let Some(&bucket_index) = bucket_by_value.get(value) {
397 buckets[bucket_index].positions.push(i);
398 } else {
399 let bucket_index = buckets.len();
400 buckets.push(OccurrenceBucket {
401 value,
402 positions: vec![i],
403 });
404 bucket_by_value.insert(value, bucket_index);
405 }
406 }
407
408 let _ = equalizer;
409 (buckets, bucket_by_value)
410}
411
412fn looks_like_high_entropy<T: Eq + Hash>(sequence: &[T], start: usize, end: usize) -> bool {
413 let sample_end = (start + 64).min(end);
414 let mut sample = HashSet::with_capacity(sample_end - start);
415 for value in &sequence[start..sample_end] {
416 if !sample.insert(value) {
417 return false;
418 }
419 }
420 true
421}
422
423fn looks_like_low_entropy<T: Eq + Hash>(sequence: &[T], start: usize, end: usize) -> bool {
424 let sample_end = (start + 64).min(end);
425 let mut sample = HashSet::with_capacity(sample_end - start);
426 for value in &sequence[start..sample_end] {
427 sample.insert(value);
428 }
429 sample.len() <= 32
430}
431
432fn find_best_anchor<T, F>(
433 source: &[T],
434 target: &[T],
435 src_start: usize,
436 src_end: usize,
437 tgt_start: usize,
438 tgt_end: usize,
439 equalizer: &F,
440 max_chain_length: usize,
441) -> Option<MatchAnchor>
442where
443 T: Eq + Hash,
444 F: Fn(&T, &T) -> bool + ?Sized,
445{
446 let (mut source_buckets, _) = build_buckets(source, src_start, src_end, equalizer);
448 let (target_buckets, target_by_value) = build_buckets(target, tgt_start, tgt_end, equalizer);
449
450 source_buckets.sort_unstable_by_key(|b| b.positions.len());
453
454 let mut best_anchor: Option<MatchAnchor> = None;
455 let mut lowest_occurrence = max_chain_length + 1;
456 let mut best_len = 0usize;
457
458 for source_bucket in &source_buckets {
459 let target_bucket_index = target_by_value
460 .get(source_bucket.value)
461 .copied()
462 .or_else(|| {
463 target_buckets
464 .iter()
465 .position(|bucket| equalizer(source_bucket.value, bucket.value))
466 });
467 let Some(target_bucket_index) = target_bucket_index else {
468 continue;
469 };
470 let target_bucket = &target_buckets[target_bucket_index];
471
472 let occurrence_count = source_bucket.positions.len().min(target_bucket.positions.len());
473 if occurrence_count > max_chain_length {
477 continue;
478 }
479
480 for &src_idx in &source_bucket.positions {
481 for &tgt_idx in &target_bucket.positions {
482 if !equalizer(&source[src_idx], &target[tgt_idx]) {
483 continue;
484 }
485 let mut len = 1usize;
486 while (src_idx + len) < src_end
487 && (tgt_idx + len) < tgt_end
488 && equalizer(&source[src_idx + len], &target[tgt_idx + len])
489 {
490 len += 1;
491 }
492
493 if occurrence_count < lowest_occurrence
494 || (occurrence_count == lowest_occurrence && len > best_len)
495 {
496 lowest_occurrence = occurrence_count;
497 best_len = len;
498 best_anchor = Some(MatchAnchor { src_idx, tgt_idx, len });
499
500 if occurrence_count == 1 {
502 return best_anchor;
503 }
504 }
505 }
506 }
507 }
508
509 best_anchor
510}
511
512fn push_change(
513 script: &mut Vec<Change>,
514 delta_type: DeltaType,
515 src_start: usize,
516 src_end: usize,
517 tgt_start: usize,
518 tgt_end: usize,
519) {
520 if let Some(last) = script.last_mut() {
521 if last.delta_type == delta_type {
522 match delta_type {
523 DeltaType::Delete if last.end_original == src_start => {
524 last.end_original = src_end;
525 return;
526 }
527 DeltaType::Insert if last.end_revised == tgt_start => {
528 last.end_revised = tgt_end;
529 return;
530 }
531 _ => {}
532 }
533 }
534 }
535
536 script.push(Change {
537 delta_type,
538 start_original: src_start,
539 end_original: src_end,
540 start_revised: tgt_start,
541 end_revised: tgt_end,
542 });
543}
544
545fn normalize_replacements(script: &mut Vec<Change>) {
546 let mut normalized: Vec<Change> = Vec::with_capacity(script.len());
547
548 for change in script.drain(..) {
549 if let Some(previous) = normalized.last_mut() {
550 let replacement = match (previous.delta_type, change.delta_type) {
551 (DeltaType::Insert, DeltaType::Delete)
552 if previous.start_original == change.start_original
553 && previous.end_revised == change.start_revised => Some(Change {
554 delta_type: DeltaType::Change,
555 start_original: change.start_original,
556 end_original: change.end_original,
557 start_revised: previous.start_revised,
558 end_revised: change.end_revised,
559 }),
560 (DeltaType::Delete, DeltaType::Insert)
561 if previous.end_original == change.start_original
562 && previous.end_revised == change.start_revised => Some(Change {
563 delta_type: DeltaType::Change,
564 start_original: previous.start_original,
565 end_original: previous.end_original,
566 start_revised: previous.start_revised,
567 end_revised: change.end_revised,
568 }),
569 _ => None,
570 };
571
572 if let Some(replacement) = replacement {
573 *previous = replacement;
574 continue;
575 }
576 }
577
578 normalized.push(change);
579 }
580
581 *script = normalized;
582}
583
584#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
586pub struct HistogramDiffFactory {
587 pub max_chain_length: usize,
588}
589
590impl HistogramDiffFactory {
591 pub fn new() -> Self {
592 Self {
593 max_chain_length: DEFAULT_MAX_CHAIN_LENGTH,
594 }
595 }
596
597 pub fn with_max_chain_length(max_chain_length: usize) -> Self {
598 Self { max_chain_length }
599 }
600}
601
602impl<T: Eq + Hash + 'static> DiffAlgorithmFactory<T> for HistogramDiffFactory {
603 fn create(&self) -> Box<dyn DiffAlgorithm<T>>
604 where
605 T: Eq + Hash + 'static,
606 {
607 Box::new(HistogramDiff::new().with_max_chain_length(self.max_chain_length))
608 }
609
610 fn create_with_equalizer(
611 &self,
612 equalizer: Box<dyn Fn(&T, &T) -> bool + 'static>,
613 ) -> Box<dyn DiffAlgorithm<T>> {
614 Box::new(
615 HistogramDiff::new()
616 .with_max_chain_length(self.max_chain_length)
617 .with_equalizer(move |a: &T, b: &T| equalizer(a, b)),
618 )
619 }
620}