Skip to main content

java_diff_utils_rs/
diff_utils.rs

1//! High-level convenience functions for computing diffs, applying patches, and unpatching lists or text.
2
3use std::sync::RwLock;
4
5use crate::algorithm::myers::myers::MyersDiff;
6use crate::algorithm::{DiffAlgorithm, DiffAlgorithmFactory, DiffAlgorithmListener};
7use crate::patch::patch_failed_exception::PatchFailedException;
8use crate::patch::Patch;
9
10static DEFAULT_DIFF_FACTORY: RwLock<Option<Box<dyn DiffAlgorithmFactory<String> + Send + Sync>>> =
11    RwLock::new(None);
12
13pub struct DiffUtils;
14
15impl DiffUtils {
16    pub fn with_default_diff_algorithm_factory(
17        factory: Box<dyn DiffAlgorithmFactory<String> + Send + Sync>,
18    ) {
19        if let Ok(mut guard) = DEFAULT_DIFF_FACTORY.write() {
20            *guard = Some(factory);
21        }
22    }
23
24    /// Computes the diff between `original` and `revised` using the default algorithm.
25    ///
26    /// # Algorithm
27    ///
28    /// The default algorithm is [`HistogramDiff`](crate::algorithm::HistogramDiff).
29    /// It uses low-occurrence element anchors to split the sequence recursively,
30    /// and falls back to Myers' linear-space algorithm for high-entropy regions.
31    ///
32    /// # Performance
33    ///
34    /// Histogram is generally faster than Myers for files with repeated structure
35    /// (e.g. source code), but Myers can be faster for high-entropy inputs where
36    /// all elements are unique. For workloads where you know the input is high-entropy,
37    /// call [`diff_with_algorithm`](Self::diff_with_algorithm) and pass a
38    /// [`MyersDiff`](crate::algorithm::myers::myers::MyersDiff) instance explicitly.
39    pub fn diff<T>(
40        original: &[T],
41        revised: &[T],
42        progress: Option<&dyn DiffAlgorithmListener>,
43    ) -> Patch<T>
44    where
45        T: PartialEq + Clone + 'static,
46    {
47        let algo = Self::get_default_algorithm::<T>();
48        Self::diff_with_algorithm(original, revised, algo.as_ref(), progress, false)
49    }
50
51    pub fn diff_with_options<T>(
52        original: &[T],
53        revised: &[T],
54        include_equal_parts: bool,
55    ) -> Patch<T>
56    where
57        T: PartialEq + Clone + 'static,
58    {
59        let algo = Self::get_default_algorithm::<T>();
60        Self::diff_with_algorithm(original, revised, algo.as_ref(), None, include_equal_parts)
61    }
62
63    pub fn diff_text(
64        source_text: &str,
65        target_text: &str,
66        progress: Option<&dyn DiffAlgorithmListener>,
67    ) -> Patch<String> {
68        let original: Vec<String> = source_text.lines().map(|s| s.to_string()).collect();
69        let revised: Vec<String> = target_text.lines().map(|s| s.to_string()).collect();
70        Self::diff(&original, &revised, progress)
71    }
72
73    /// Computes the diff with an optional custom element equality predicate.
74    ///
75    /// # Note
76    ///
77    /// When `equalizer` is `Some`, this method uses [`MyersDiff`] rather than the
78    /// HistogramDiff default, because HistogramDiff's custom-equalizer path requires
79    /// `T: 'static` which is not always available. If you need HistogramDiff with a
80    /// custom equalizer, construct one directly:
81    ///
82    /// ```ignore
83    /// let algo = HistogramDiff::new().with_equalizer(|a, b| a.eq_ignore_ascii_case(b));
84    /// DiffUtils::diff_with_algorithm(&source, &target, &algo, None, false);
85    /// ```
86    pub fn diff_with_equalizer<T, F>(source: &[T], target: &[T], equalizer: Option<F>) -> Patch<T>
87    where
88        T: PartialEq + Clone + 'static,
89        F: Fn(&T, &T) -> bool + Send + Sync + 'static,
90    {
91        if let Some(eq) = equalizer {
92            let algo = MyersDiff::with_equalizer(eq);
93            Self::diff_with_algorithm(source, target, &algo, None, false)
94        } else {
95            let algo = MyersDiff::default();
96            Self::diff_with_algorithm(source, target, &algo, None, false)
97        }
98    }
99
100    pub fn diff_with_algorithm<T>(
101        original: &[T],
102        revised: &[T],
103        algorithm: &dyn DiffAlgorithm<T>,
104        _progress: Option<&dyn DiffAlgorithmListener>,
105        include_equal_parts: bool,
106    ) -> Patch<T>
107    where
108        T: Clone + 'static,
109    {
110        let deltas = algorithm.diff(original, revised);
111        Patch::generate(original, revised, &deltas, include_equal_parts)
112    }
113
114    pub fn diff_inline(original: &str, revised: &str) -> Patch<String> {
115        let orig_list: Vec<String> = original.chars().map(|c| c.to_string()).collect();
116        let rev_list: Vec<String> = revised.chars().map(|c| c.to_string()).collect();
117
118        let mut patch = Self::diff(&orig_list, &rev_list, None);
119
120        for delta in patch.deltas_mut() {
121            let source_lines = Self::compress_lines(delta.source_mut().lines(), "");
122            delta.source_mut().set_lines(source_lines);
123
124            let target_lines = Self::compress_lines(delta.target_mut().lines(), "");
125            delta.target_mut().set_lines(target_lines);
126        }
127
128        patch
129    }
130
131    pub fn patch<T>(original: &[T], patch: &Patch<T>) -> Result<Vec<T>, PatchFailedException>
132    where
133        T: PartialEq + Clone,
134    {
135        patch.apply_to(original).map_err(PatchFailedException::from)
136    }
137
138    pub fn unpatch<T>(revised: &[T], patch: &Patch<T>) -> Result<Vec<T>, PatchFailedException>
139    where
140        T: PartialEq + Clone,
141    {
142        patch.restore(revised).map_err(PatchFailedException::from)
143    }
144
145    fn compress_lines(lines: &[String], delimiter: &str) -> Vec<String> {
146        if lines.is_empty() {
147            Vec::new()
148        } else {
149            vec![lines.join(delimiter)]
150        }
151    }
152
153    fn get_default_algorithm<T: PartialEq + Clone + 'static>() -> Box<dyn DiffAlgorithm<T>> {
154        Box::new(crate::algorithm::HistogramDiff::default())
155    }
156}