1#![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;
41mod multi;
42
43use std::char::REPLACEMENT_CHARACTER;
44
45use crate::lcs::lcs;
46use crate::merge::merge;
47
48#[derive(PartialEq, Eq, Clone, Debug)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub enum Difference {
54 Same(String),
56 Add(String),
58 Rem(String),
60}
61
62#[derive(Clone, Debug, PartialEq, Eq)]
64#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
65pub struct Changeset {
66 pub diffs: Vec<Difference>,
69 pub split: String,
72 pub distance: i128,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79pub struct ChangesetMulti {
80 pub diffs: Vec<Difference>,
83 pub splits: Vec<(usize, String)>,
85 pub edit_splits: Vec<(usize, String)>,
87 pub distance: i128,
89}
90
91impl Changeset {
92 #[must_use]
117 pub fn new(orig: &str, edit: &str, split: &str) -> Changeset {
118 let (dist, common) = lcs(orig, edit, split);
119 Changeset {
120 diffs: merge(orig, edit, &common, split),
121 split: split.to_string(),
122 distance: dist,
123 }
124 }
125
126 #[must_use]
158 pub fn new_multi(orig: &str, edit: &str, splits: &[&str]) -> ChangesetMulti {
159 let matched_splits = splits
160 .iter()
161 .flat_map(|split| orig.match_indices(*split))
162 .map(|(k, v)| (k, v.to_string()))
163 .collect::<Vec<(usize, String)>>();
164 let edit_splits = splits
165 .iter()
166 .flat_map(|split| edit.match_indices(*split))
167 .map(|(k, v)| (k, v.to_string()))
168 .collect::<Vec<(usize, String)>>();
169
170 let mut aux_orig = orig.to_string();
171 let mut aux_edit = edit.to_string();
172 let replacement = REPLACEMENT_CHARACTER.to_string();
173 for split in splits {
174 aux_orig = aux_orig.replace(split, &replacement);
175 aux_edit = aux_edit.replace(split, &replacement);
176 }
177
178 let changeset = Changeset::new(&aux_orig, &aux_edit, &replacement);
179 ChangesetMulti::from((changeset, matched_splits, edit_splits))
180 }
181}
182
183#[macro_export]
193macro_rules! assert_diff {
194 ($orig:expr_2021 , $edit:expr_2021, $split: expr_2021, $expected: expr_2021) => {{
195 let orig = $orig;
196 let edit = $edit;
197
198 let changeset = $crate::Changeset::new(orig, edit, &($split));
199 if changeset.distance != $expected {
200 println!("{}", changeset);
201 panic!(
202 "assertion failed: edit distance between {:?} and {:?} is {} and not {}, see \
203 diffset above",
204 orig,
205 edit,
206 changeset.distance,
207 &($expected)
208 )
209 }
210 }};
211}
212
213#[test]
214fn test_diff() {
215 let text1 = "Roses are red, violets are blue,\n\
216 I wrote this library,\n\
217 just for you.\n\
218 (It's true).";
219
220 let text2 = "Roses are red, violets are blue,\n\
221 I wrote this documentation,\n\
222 just for you.\n\
223 (It's quite true).";
224
225 let changeset = Changeset::new(text1, text2, "\n");
226
227 assert_eq!(changeset.distance, 4);
228
229 assert_eq!(
230 changeset.diffs,
231 vec![
232 Difference::Same("Roses are red, violets are blue,".to_string()),
233 Difference::Rem("I wrote this library,".to_string()),
234 Difference::Add("I wrote this documentation,".to_string()),
235 Difference::Same("just for you.".to_string()),
236 Difference::Rem("(It's true).".to_string()),
237 Difference::Add("(It's quite true).".to_string()),
238 ]
239 );
240}
241
242#[test]
243fn test_diff_brief() {
244 let text1 = "Hello\nworld";
245 let text2 = "Ola\nmundo";
246
247 let changeset = Changeset::new(text1, text2, "\n");
248
249 assert_eq!(
250 changeset.diffs,
251 vec![
252 Difference::Rem("Hello\nworld".to_string()),
253 Difference::Add("Ola\nmundo".to_string()),
254 ]
255 );
256}
257
258#[test]
259#[cfg(feature = "serde")]
260fn test_diff_smaller_line_count_on_left() {
261 let text1 = "Hello\nworld";
262 let text2 = "Ola\nworld\nHow is it\ngoing?";
263
264 let changeset = Changeset::new(text1, text2, "\n");
265
266 assert_eq!(
267 changeset.diffs,
268 vec![
269 Difference::Rem("Hello".to_string()),
270 Difference::Add("Ola".to_string()),
271 Difference::Same("world".to_string()),
272 Difference::Add("How is it\ngoing?".to_string()),
273 ]
274 );
275
276 let json = serde_json::to_string(&changeset).unwrap();
277
278 assert_eq!(
279 json,
280 r#"{"diffs":[{"Rem":"Hello"},{"Add":"Ola"},{"Same":"world"},{"Add":"How is it\ngoing?"}],"split":"\n","distance":4}"#
281 );
282}
283
284#[test]
285fn test_diff_smaller_line_count_on_right() {
286 let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
287 let text2 = "Ola\nworld";
288
289 let changeset = Changeset::new(text1, text2, "\n");
290
291 assert_eq!(
292 changeset.diffs,
293 vec![
294 Difference::Rem("Hello".to_string()),
295 Difference::Add("Ola".to_string()),
296 Difference::Same("world".to_string()),
297 Difference::Rem("What a \nbeautiful\nday!".to_string()),
298 ]
299 );
300}
301
302#[test]
303fn test_diff_similar_text_with_smaller_line_count_on_right() {
304 let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
305 let text2 = "Hello\nwoRLd";
306
307 let changeset = Changeset::new(text1, text2, "\n");
308
309 assert_eq!(
310 changeset.diffs,
311 vec![
312 Difference::Same("Hello".to_string()),
313 Difference::Rem("world\nWhat a \nbeautiful\nday!".to_string()),
314 Difference::Add("woRLd".to_string()),
315 ]
316 );
317}
318
319#[test]
320fn test_diff_similar_text_with_similar_line_count() {
321 let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
322 let text2 = "Hello\nwoRLd\nbeautiful";
323
324 let changeset = Changeset::new(text1, text2, "\n");
325
326 assert_eq!(
327 changeset.diffs,
328 vec![
329 Difference::Same("Hello".to_string()),
330 Difference::Rem("world\nWhat a ".to_string()),
331 Difference::Add("woRLd".to_string()),
332 Difference::Same("beautiful".to_string()),
333 Difference::Rem("day!".to_string()),
334 ]
335 );
336}
337
338#[test]
339#[should_panic = r#"assertion failed: edit distance between "Roses are red, violets are blue,\nI wrote this library,\njust for you.\n(It's true)." and "Roses are red, violets are blue,\nI wrote this documentation,\njust for you.\n(It's quite true)." is 2 and not 0, see diffset above"#]
340fn test_assert_diff_panic() {
341 let text1 = "Roses are red, violets are blue,\n\
342 I wrote this library,\n\
343 just for you.\n\
344 (It's true).";
345
346 let text2 = "Roses are red, violets are blue,\n\
347 I wrote this documentation,\n\
348 just for you.\n\
349 (It's quite true).";
350
351 assert_diff!(text1, text2, "\n'", 0);
352}
353
354#[test]
355fn test_assert_diff() {
356 let text1 = "Roses are red, violets are blue";
357
358 let text2 = "Roses are green, violets are blue";
359
360 assert_diff!(text1, text2, " ", 2);
361}
362
363#[test]
364fn test_multi_pattern() {
365 let cg = Changeset::new_multi("hello,world now", "hellow,world later", &[",", " "]);
366 let expected = ChangesetMulti {
367 diffs: vec![
368 Difference::Rem("hello,".to_string()),
369 Difference::Add("hellow,".to_string()),
370 Difference::Same("world ".to_string()),
371 Difference::Rem("now".to_string()),
372 Difference::Add("later".to_string()),
373 ],
374 splits: vec![(5, ",".to_string()), (11, " ".to_string())],
375 edit_splits: vec![(6, ",".to_string()), (12, " ".to_string())],
376 distance: 4,
377 };
378
379 assert_eq!(cg, expected);
380}
381
382#[test]
383fn test_multi_uri_pattern() {
384 let cg = Changeset::new_multi(
385 "https://localhost:8080/path?query=value",
386 "https://myapi.com/api/path?query=asset",
387 &["://", "/", "?", "="],
388 );
389 let expected = ChangesetMulti {
390 diffs: vec![
391 Difference::Same("https://".to_string()),
392 Difference::Rem("localhost:8080/".to_string()),
393 Difference::Add("myapi.com/api/".to_string()),
394 Difference::Same("path?query=".to_string()),
395 Difference::Rem("value".to_string()),
396 Difference::Add("asset".to_string()),
397 ],
398 splits: vec![
399 (5, "://".to_string()),
400 (6, "/".to_string()),
401 (7, "/".to_string()),
402 (22, "/".to_string()),
403 (27, "?".to_string()),
404 (33, "=".to_string()),
405 ],
406 edit_splits: vec![
407 (5, "://".to_string()),
408 (6, "/".to_string()),
409 (7, "/".to_string()),
410 (17, "/".to_string()),
411 (21, "/".to_string()),
412 (26, "?".to_string()),
413 (32, "=".to_string()),
414 ],
415 distance: 5,
416 };
417
418 assert_eq!(cg, expected);
419}