difference_rs/
lib.rs

1//! Functions to find the difference between two texts (strings).
2//!
3//! Usage
4//! ----------
5//!
6//! Add the following to your `Cargo.toml`:
7//!
8//! ```toml
9//! [dependencies]
10//! difference_rs = "3.0"
11//! ```
12//!
13//! Now you can use the crate in your code
14//!
15//!
16//! ## Examples
17//!
18//! See [Examples.md](Examples.md) for more examples.
19//!
20//! ```rust
21//! use difference_rs::{Difference, Changeset};
22//!
23//! let changeset = Changeset::new("test", "tent", "");
24//!
25//! assert_eq!(changeset.diffs, vec![
26//!   Difference::Same("te".to_string()),
27//!   Difference::Rem("s".to_string()),
28//!   Difference::Add("n".to_string()),
29//!   Difference::Same("t".to_string())
30//! ]);
31//! ```
32
33#![crate_name = "difference_rs"]
34#![doc(html_root_url = "http://docs.rs/difference-rs")]
35#![deny(missing_docs)]
36#![deny(warnings)]
37
38mod display;
39mod lcs;
40mod merge;
41
42use crate::lcs::lcs;
43use crate::merge::merge;
44
45/// Defines the contents of a changeset
46/// Changesets will be delivered in order of appearance in the original string
47/// Sequences of the same kind will be grouped into one Difference
48#[derive(PartialEq, Clone, Debug)]
49pub enum Difference {
50    /// Sequences that are the same
51    Same(String),
52    /// Sequences that are an addition (don't appear in the first string)
53    Add(String),
54    /// Sequences that are a removal (don't appear in the second string)
55    Rem(String),
56}
57
58/// The information about a full changeset
59#[derive(Clone, Debug)]
60pub struct Changeset {
61    /// An ordered vector of `Difference` objects, corresponding
62    /// to the differences within the text
63    pub diffs: Vec<Difference>,
64    /// The split used when creating the `Changeset`
65    /// Common splits are `""` for char-level, `" "` for word-level and `"\n"` for line-level.
66    pub split: String,
67    /// The edit distance of the `Changeset`
68    pub distance: i32,
69}
70
71impl Changeset {
72    /// Calculates the edit distance and the changeset for two given strings.
73    /// The first string is assumed to be the "original", the second to be an
74    /// edited version of the first. The third parameter specifies how to split
75    /// the input strings, leading to a more or less exact comparison.
76    ///
77    /// Common splits are `""` for char-level, `" "` for word-level and `"\n"` for line-level.
78    ///
79    /// Outputs the edit distance (how much the two strings differ) and a "changeset", that is
80    /// a `Vec` containing `Difference`s.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use difference_rs::{Changeset, Difference};
86    ///
87    /// let changeset = Changeset::new("test", "tent", "");
88    ///
89    /// assert_eq!(changeset.diffs, vec![
90    ///     Difference::Same("te".to_string()),
91    ///     Difference::Rem("s".to_string()),
92    ///     Difference::Add("n".to_string()),
93    ///     Difference::Same("t".to_string())
94    /// ]);
95    /// ```
96    pub fn new(orig: &str, edit: &str, split: &str) -> Changeset {
97        let (dist, common) = lcs(orig, edit, split);
98        Changeset {
99            diffs: merge(orig, edit, &common, split),
100            split: split.to_string(),
101            distance: dist,
102        }
103    }
104}
105
106/// **This function is deprecated, please use `Changeset::new` instead**
107///
108/// Calculates the edit distance and the changeset for two given strings.
109/// The first string is assumed to be the "original", the second to be an
110/// edited version of the first. The third parameter specifies how to split
111/// the input strings, leading to a more or less exact comparison.
112///
113/// Common splits are `""` for char-level, `" "` for word-level and `"\n"` for line-level.
114///
115/// Outputs the edit distance (how much the two strings differ) and a "changeset", that is
116/// a `Vec` containing `Difference`s.
117///
118/// # Examples
119///
120/// ```
121/// use difference_rs::diff;
122/// use difference_rs::Difference;
123///
124/// let (dist, changeset) = diff("test", "tent", "");
125///
126/// assert_eq!(changeset, vec![
127///     Difference::Same("te".to_string()),
128///     Difference::Rem("s".to_string()),
129///     Difference::Add("n".to_string()),
130///     Difference::Same("t".to_string())
131/// ]);
132/// ```
133#[deprecated(since = "1.0.0", note = "please use `Changeset::new` instead")]
134pub fn diff(orig: &str, edit: &str, split: &str) -> (i32, Vec<Difference>) {
135    let ch = Changeset::new(orig, edit, split);
136    (ch.distance, ch.diffs)
137}
138
139/// Assert the difference between two strings. Works like diff, but takes
140/// a fourth parameter that is the expected edit distance (e.g. 0 if you want to
141/// test for equality).
142///
143/// To include this macro use:
144///
145/// ```
146/// #[macro_use(assert_diff)]
147/// # fn main() { }
148/// ```
149///
150/// Remember that edit distance might not be equal to your understanding of difference,
151/// for example the words "Rust" and "Dust" have an edit distance of 2 because two changes (a
152/// removal and an addition) are required to make them look the same.
153///
154/// Will print an error with a colorful diff in case of failure.
155#[macro_export]
156macro_rules! assert_diff {
157    ($orig:expr_2021 , $edit:expr_2021, $split: expr_2021, $expected: expr_2021) => {{
158        let orig = $orig;
159        let edit = $edit;
160
161        let changeset = $crate::Changeset::new(orig, edit, &($split));
162        if changeset.distance != $expected {
163            println!("{}", changeset);
164            panic!(
165                "assertion failed: edit distance between {:?} and {:?} is {} and not {}, see \
166                    diffset above",
167                orig,
168                edit,
169                changeset.distance,
170                &($expected)
171            )
172        }
173    }};
174}
175
176/// **This function is deprecated, `Changeset` now implements the `Display` trait instead**
177///
178/// Prints a colorful visual representation of the diff.
179/// This is just a convenience function for those who want quick results.
180///
181/// I recommend checking out the examples on how to build your
182/// own diff output.
183/// # Examples
184///
185/// ```
186/// use difference_rs::print_diff;
187/// print_diff("Diffs are awesome", "Diffs are cool", " ");
188/// ```
189#[deprecated(
190    since = "1.0.0",
191    note = "`Changeset` now implements the `Display` trait instead"
192)]
193pub fn print_diff(orig: &str, edit: &str, split: &str) {
194    let ch = Changeset::new(orig, edit, split);
195    println!("{}", ch);
196}
197
198#[test]
199fn test_diff() {
200    let text1 = "Roses are red, violets are blue,\n\
201                 I wrote this library,\n\
202                 just for you.\n\
203                 (It's true).";
204
205    let text2 = "Roses are red, violets are blue,\n\
206                 I wrote this documentation,\n\
207                 just for you.\n\
208                 (It's quite true).";
209
210    let changeset = Changeset::new(text1, text2, "\n");
211
212    assert_eq!(changeset.distance, 4);
213
214    assert_eq!(
215        changeset.diffs,
216        vec![
217            Difference::Same("Roses are red, violets are blue,".to_string()),
218            Difference::Rem("I wrote this library,".to_string()),
219            Difference::Add("I wrote this documentation,".to_string()),
220            Difference::Same("just for you.".to_string()),
221            Difference::Rem("(It's true).".to_string()),
222            Difference::Add("(It's quite true).".to_string()),
223        ]
224    );
225}
226
227#[test]
228fn test_diff_brief() {
229    let text1 = "Hello\nworld";
230    let text2 = "Ola\nmundo";
231
232    let changeset = Changeset::new(text1, text2, "\n");
233
234    assert_eq!(
235        changeset.diffs,
236        vec![
237            Difference::Rem("Hello\nworld".to_string()),
238            Difference::Add("Ola\nmundo".to_string()),
239        ]
240    );
241}
242
243#[test]
244fn test_diff_smaller_line_count_on_left() {
245    let text1 = "Hello\nworld";
246    let text2 = "Ola\nworld\nHow is it\ngoing?";
247
248    let changeset = Changeset::new(text1, text2, "\n");
249
250    assert_eq!(
251        changeset.diffs,
252        vec![
253            Difference::Rem("Hello".to_string()),
254            Difference::Add("Ola".to_string()),
255            Difference::Same("world".to_string()),
256            Difference::Add("How is it\ngoing?".to_string()),
257        ]
258    );
259}
260
261#[test]
262fn test_diff_smaller_line_count_on_right() {
263    let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
264    let text2 = "Ola\nworld";
265
266    let changeset = Changeset::new(text1, text2, "\n");
267
268    assert_eq!(
269        changeset.diffs,
270        vec![
271            Difference::Rem("Hello".to_string()),
272            Difference::Add("Ola".to_string()),
273            Difference::Same("world".to_string()),
274            Difference::Rem("What a \nbeautiful\nday!".to_string()),
275        ]
276    );
277}
278
279#[test]
280fn test_diff_similar_text_with_smaller_line_count_on_right() {
281    let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
282    let text2 = "Hello\nwoRLd";
283
284    let changeset = Changeset::new(text1, text2, "\n");
285
286    assert_eq!(
287        changeset.diffs,
288        vec![
289            Difference::Same("Hello".to_string()),
290            Difference::Rem("world\nWhat a \nbeautiful\nday!".to_string()),
291            Difference::Add("woRLd".to_string()),
292        ]
293    );
294}
295
296#[test]
297fn test_diff_similar_text_with_similar_line_count() {
298    let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
299    let text2 = "Hello\nwoRLd\nbeautiful";
300
301    let changeset = Changeset::new(text1, text2, "\n");
302
303    assert_eq!(
304        changeset.diffs,
305        vec![
306            Difference::Same("Hello".to_string()),
307            Difference::Rem("world\nWhat a ".to_string()),
308            Difference::Add("woRLd".to_string()),
309            Difference::Same("beautiful".to_string()),
310            Difference::Rem("day!".to_string()),
311        ]
312    );
313}
314
315#[test]
316#[should_panic]
317fn test_assert_diff_panic() {
318    let text1 = "Roses are red, violets are blue,\n\
319                 I wrote this library,\n\
320                 just for you.\n\
321                 (It's true).";
322
323    let text2 = "Roses are red, violets are blue,\n\
324                 I wrote this documentation,\n\
325                 just for you.\n\
326                 (It's quite true).";
327
328    assert_diff!(text1, text2, "\n'", 0);
329}
330
331#[test]
332fn test_assert_diff() {
333    let text1 = "Roses are red, violets are blue";
334
335    let text2 = "Roses are green, violets are blue";
336
337    assert_diff!(text1, text2, " ", 2);
338}