#![crate_name = "difference_rs"]
#![doc(html_root_url = "http://docs.rs/difference-rs")]
#![deny(missing_docs)]
#![deny(warnings)]
mod display;
mod lcs;
mod merge;
mod multi;
use std::char::REPLACEMENT_CHARACTER;
use crate::lcs::lcs;
use crate::merge::merge;
#[derive(PartialEq, Eq, Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Difference {
Same(String),
Add(String),
Rem(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Changeset {
pub diffs: Vec<Difference>,
pub split: String,
pub distance: i128,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChangesetMulti {
pub diffs: Vec<Difference>,
pub splits: Vec<(usize, String)>,
pub edit_splits: Vec<(usize, String)>,
pub distance: i128,
}
impl Changeset {
#[must_use]
pub fn new(orig: &str, edit: &str, split: &str) -> Changeset {
let (dist, common) = lcs(orig, edit, split);
Changeset {
diffs: merge(orig, edit, &common, split),
split: split.to_string(),
distance: dist,
}
}
#[must_use]
pub fn new_multi(orig: &str, edit: &str, splits: &[&str]) -> ChangesetMulti {
let matched_splits = splits
.iter()
.flat_map(|split| orig.match_indices(*split))
.map(|(k, v)| (k, v.to_string()))
.collect::<Vec<(usize, String)>>();
let edit_splits = splits
.iter()
.flat_map(|split| edit.match_indices(*split))
.map(|(k, v)| (k, v.to_string()))
.collect::<Vec<(usize, String)>>();
let mut aux_orig = orig.to_string();
let mut aux_edit = edit.to_string();
let replacement = REPLACEMENT_CHARACTER.to_string();
for split in splits {
aux_orig = aux_orig.replace(split, &replacement);
aux_edit = aux_edit.replace(split, &replacement);
}
let changeset = Changeset::new(&aux_orig, &aux_edit, &replacement);
ChangesetMulti::from((changeset, matched_splits, edit_splits))
}
}
#[macro_export]
macro_rules! assert_diff {
($orig:expr_2021 , $edit:expr_2021, $split: expr_2021, $expected: expr_2021) => {{
let orig = $orig;
let edit = $edit;
let changeset = $crate::Changeset::new(orig, edit, &($split));
if changeset.distance != $expected {
println!("{}", changeset);
panic!(
"assertion failed: edit distance between {:?} and {:?} is {} and not {}, see \
diffset above",
orig,
edit,
changeset.distance,
&($expected)
)
}
}};
}
#[test]
fn test_diff() {
let text1 = "Roses are red, violets are blue,\n\
I wrote this library,\n\
just for you.\n\
(It's true).";
let text2 = "Roses are red, violets are blue,\n\
I wrote this documentation,\n\
just for you.\n\
(It's quite true).";
let changeset = Changeset::new(text1, text2, "\n");
assert_eq!(changeset.distance, 4);
assert_eq!(
changeset.diffs,
vec![
Difference::Same("Roses are red, violets are blue,".to_string()),
Difference::Rem("I wrote this library,".to_string()),
Difference::Add("I wrote this documentation,".to_string()),
Difference::Same("just for you.".to_string()),
Difference::Rem("(It's true).".to_string()),
Difference::Add("(It's quite true).".to_string()),
]
);
}
#[test]
fn test_diff_brief() {
let text1 = "Hello\nworld";
let text2 = "Ola\nmundo";
let changeset = Changeset::new(text1, text2, "\n");
assert_eq!(
changeset.diffs,
vec![
Difference::Rem("Hello\nworld".to_string()),
Difference::Add("Ola\nmundo".to_string()),
]
);
}
#[test]
#[cfg(feature = "serde")]
fn test_diff_smaller_line_count_on_left() {
let text1 = "Hello\nworld";
let text2 = "Ola\nworld\nHow is it\ngoing?";
let changeset = Changeset::new(text1, text2, "\n");
assert_eq!(
changeset.diffs,
vec![
Difference::Rem("Hello".to_string()),
Difference::Add("Ola".to_string()),
Difference::Same("world".to_string()),
Difference::Add("How is it\ngoing?".to_string()),
]
);
let json = serde_json::to_string(&changeset).unwrap();
assert_eq!(
json,
r#"{"diffs":[{"Rem":"Hello"},{"Add":"Ola"},{"Same":"world"},{"Add":"How is it\ngoing?"}],"split":"\n","distance":4}"#
);
}
#[test]
fn test_diff_smaller_line_count_on_right() {
let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
let text2 = "Ola\nworld";
let changeset = Changeset::new(text1, text2, "\n");
assert_eq!(
changeset.diffs,
vec![
Difference::Rem("Hello".to_string()),
Difference::Add("Ola".to_string()),
Difference::Same("world".to_string()),
Difference::Rem("What a \nbeautiful\nday!".to_string()),
]
);
}
#[test]
fn test_diff_similar_text_with_smaller_line_count_on_right() {
let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
let text2 = "Hello\nwoRLd";
let changeset = Changeset::new(text1, text2, "\n");
assert_eq!(
changeset.diffs,
vec![
Difference::Same("Hello".to_string()),
Difference::Rem("world\nWhat a \nbeautiful\nday!".to_string()),
Difference::Add("woRLd".to_string()),
]
);
}
#[test]
fn test_diff_similar_text_with_similar_line_count() {
let text1 = "Hello\nworld\nWhat a \nbeautiful\nday!";
let text2 = "Hello\nwoRLd\nbeautiful";
let changeset = Changeset::new(text1, text2, "\n");
assert_eq!(
changeset.diffs,
vec![
Difference::Same("Hello".to_string()),
Difference::Rem("world\nWhat a ".to_string()),
Difference::Add("woRLd".to_string()),
Difference::Same("beautiful".to_string()),
Difference::Rem("day!".to_string()),
]
);
}
#[test]
#[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"#]
fn test_assert_diff_panic() {
let text1 = "Roses are red, violets are blue,\n\
I wrote this library,\n\
just for you.\n\
(It's true).";
let text2 = "Roses are red, violets are blue,\n\
I wrote this documentation,\n\
just for you.\n\
(It's quite true).";
assert_diff!(text1, text2, "\n'", 0);
}
#[test]
fn test_assert_diff() {
let text1 = "Roses are red, violets are blue";
let text2 = "Roses are green, violets are blue";
assert_diff!(text1, text2, " ", 2);
}
#[test]
fn test_multi_pattern() {
let cg = Changeset::new_multi("hello,world now", "hellow,world later", &[",", " "]);
let expected = ChangesetMulti {
diffs: vec![
Difference::Rem("hello,".to_string()),
Difference::Add("hellow,".to_string()),
Difference::Same("world ".to_string()),
Difference::Rem("now".to_string()),
Difference::Add("later".to_string()),
],
splits: vec![(5, ",".to_string()), (11, " ".to_string())],
edit_splits: vec![(6, ",".to_string()), (12, " ".to_string())],
distance: 4,
};
assert_eq!(cg, expected);
}
#[test]
fn test_multi_uri_pattern() {
let cg = Changeset::new_multi(
"https://localhost:8080/path?query=value",
"https://myapi.com/api/path?query=asset",
&["://", "/", "?", "="],
);
let expected = ChangesetMulti {
diffs: vec![
Difference::Same("https://".to_string()),
Difference::Rem("localhost:8080/".to_string()),
Difference::Add("myapi.com/api/".to_string()),
Difference::Same("path?query=".to_string()),
Difference::Rem("value".to_string()),
Difference::Add("asset".to_string()),
],
splits: vec![
(5, "://".to_string()),
(6, "/".to_string()),
(7, "/".to_string()),
(22, "/".to_string()),
(27, "?".to_string()),
(33, "=".to_string()),
],
edit_splits: vec![
(5, "://".to_string()),
(6, "/".to_string()),
(7, "/".to_string()),
(17, "/".to_string()),
(21, "/".to_string()),
(26, "?".to_string()),
(32, "=".to_string()),
],
distance: 5,
};
assert_eq!(cg, expected);
}