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