#! [allow(
clippy::incompatible_msrv,
// https://github.com/rust-lang/rust-clippy/issues/12257
)]
use super ::*;
use std::sync::OnceLock;
macro_rules! range {
($text:expr) => {{
static CHARS: OnceLock<Vec<char>> = OnceLock::new();
let chars = CHARS.get_or_init(|| $text.chars().collect());
Range::new(chars, ..)
}};
}
macro_rules! diff_list {
() => {
Solution {
text1: Range::empty(),
text2: Range::empty(),
diffs: Vec::new(),
}
};
($($kind:ident($text:literal)),+ $(,)?) => {{
#[ allow(unused_macro_rules)]
macro_rules! text1 {
(Insert, $s:literal) => {
"" };
(Delete, $s:literal) => { $s };
(Equal, $s:literal) => { $s };
}
#[ allow(unused_macro_rules)]
macro_rules! text2 {
(Insert, $s:literal) => { $s };
(Delete, $s:literal) => {
"" };
(Equal, $s:literal) => { $s };
}
let text1 = range!(concat!($(text1!($kind, $text)),*));
let text2 = range!(concat!($(text2!($kind, $text)),*));
let (_i, _j) = (&
mut 0 , &
mut 0 );
#[ allow(unused_macro_rules)]
macro_rules! range {
(Insert, $s:literal) => {
Diff::Insert(range(text2.doc, _j, $s))
};
(Delete, $s:literal) => {
Diff::Delete(range(text1.doc, _i, $s))
};
(Equal, $s:literal) => {
Diff::Equal(range(text1.doc, _i, $s), range(text2.doc, _j, $s))
};
}
Solution {
text1,
text2,
diffs: vec![$(range!($kind, $text)),*],
}
}};
}
fn range<
'a>(doc: &' a [char], offset: &
mut usize, text: &str) -> Ra
nge<'a> {
let len = text.chars().count();
let range = Range {
doc,
offset: *offset,
len,
};
*offset += len;
range
}
macro_rules! assert_diffs {
([$($kind:ident($text:literal)),* $(,)?], $solution:ident, $msg:expr $(,)?) => {
let expected = &[$(Chunk::$kind($text)),*];
assert!(
same_diffs(expected, &$solution.diffs),
concat!($msg, "\nexpected={:#?}\nactual={:#?}" ),
expected, $solution.diffs,
);
};
}
fn same_diffs(expected: &[Chunk], actual: &[Diff]) -> bool {
fn eq(expected: &str, actual: &Range) -> bool {
expected.chars().eq(slice(*actual).iter().copied())
}
expected.len() == actual.len()
&& expected.iter().zip(actual).all(|pair| match pair {
(Chunk::Insert(expected), Diff::Insert(actual)) => eq(expected, actual),
(Chunk::Delete(expected), Diff::Delete(actual)) => eq(expected, actual),
(Chunk::Equal(expected), Diff::Equal(actual1, actual2)) => {
eq(expected, actual1) && eq(expected, actual2)
}
(_, _) => false ,
})
}
#[ test]
fn test_common_prefix() {
let text1 = range!("abc" );
let text2 = range!("xyz" );
assert_eq!(0 , common_prefix(text1, text2), "Null case" );
let text1 = range!("1234abcdef" );
let text2 = range!("1234xyz" );
assert_eq!(4 , common_prefix(text1, text2), "Non-null case" );
let text1 = range!("1234" );
let text2 = range!("1234xyz" );
assert_eq!(4 , common_prefix(text1, text2), "Whole case" );
}
#[ test]
fn test_common_suffix() {
let text1 = range!("abc" );
let text2 = range!("xyz" );
assert_eq!(0 , common_suffix(text1, text2), "Null case" );
let text1 = range!("abcdef1234" );
let text2 = range!("xyz1234" );
assert_eq!(4 , common_suffix(text1, text2), "Non-null case" );
let text1 = range!("1234" );
let text2 = range!("xyz1234" );
assert_eq!(4 , common_suffix(text1, text2), "Whole case" );
}
#[ test]
fn test_common_overlap() {
let text1 = Range::empty();
let text2 = range!("abcd" );
assert_eq!(0 , common_overlap(text1, text2), "Null case" );
let text1 = range!("abc" );
let text2 = range!("abcd" );
assert_eq!(3 , common_overlap(text1, text2), "Whole case" );
let text1 = range!("123456" );
let text2 = range!("abcd" );
assert_eq!(0 , common_overlap(text1, text2), "No overlap" );
let text1 = range!("123456xxx" );
let text2 = range!("xxxabcd" );
assert_eq!(3 , common_overlap(text1, text2), "Overlap" );
// Some overly clever languages (C#) may treat ligatures as equal to their
// component letters. E.g. U+FB01 == 'fi'
let text1 = range!("fi" );
let text2 = range!("\u{fb01}i" );
assert_eq!(0 , common_overlap(text1, text2), "Unicode" );
}
#[ test]
fn test_cleanup_merge() {
let mut solution = diff_list![];
cleanup_merge(&mut solution);
assert_diffs!([], solution, "Null case" );
let mut solution = diff_list![Equal("a" ), Delete("b" ), Insert("c" )];
cleanup_merge(&mut solution);
assert_diffs!(
[Equal("a" ), Delete("b" ), Insert("c" )],
solution,
"No change case" ,
);
let mut solution = diff_list![Equal("a" ), Equal("b" ), Equal("c" )];
cleanup_merge(&mut solution);
assert_diffs!([Equal("abc" )], solution, "Merge equalities" );
let mut solution = diff_list![Delete("a" ), Delete("b" ), Delete("c" )];
cleanup_merge(&mut solution);
assert_diffs!([Delete("abc" )], solution, "Merge deletions" );
let mut solution = diff_list![Insert("a" ), Insert("b" ), Insert("c" )];
cleanup_merge(&mut solution);
assert_diffs!([Insert("abc" )], solution, "Merge insertions" );
let mut solution = diff_list![
Delete("a" ),
Insert("b" ),
Delete("c" ),
Insert("d" ),
Equal("e" ),
Equal("f" ),
];
cleanup_merge(&mut solution);
assert_diffs!(
[Delete("ac" ), Insert("bd" ), Equal("ef" )],
solution,
"Merge interweave" ,
);
let mut solution = diff_list![Delete("a" ), Insert("abc" ), Delete("dc" )];
cleanup_merge(&mut solution);
assert_diffs!(
[Equal("a" ), Delete("d" ), Insert("b" ), Equal("c" )],
solution,
"Prefix and suffix detection" ,
);
let mut solution = diff_list![
Equal("x" ),
Delete("a" ),
Insert("abc" ),
Delete("dc" ),
Equal("y" ),
];
cleanup_merge(&mut solution);
assert_diffs!(
[Equal("xa" ), Delete("d" ), Insert("b" ), Equal("cy" )],
solution,
"Prefix and suffix detection with equalities" ,
);
let mut solution = diff_list![Equal("a" ), Insert("ba" ), Equal("c" )];
cleanup_merge(&mut solution);
assert_diffs!([Insert("ab" ), Equal("ac" )], solution, "Slide edit left" );
let mut solution = diff_list![Equal("c" ), Insert("ab" ), Equal("a" )];
cleanup_merge(&mut solution);
assert_diffs!([Equal("ca" ), Insert("ba" )], solution, "Slide edit right" );
let mut solution = diff_list![
Equal("a" ),
Delete("b" ),
Equal("c" ),
Delete("ac" ),
Equal("x" ),
];
cleanup_merge(&mut solution);
assert_diffs!(
[Delete("abc" ), Equal("acx" )],
solution,
"Slide edit left recursive" ,
);
let mut solution = diff_list![
Equal("x" ),
Delete("ca" ),
Equal("c" ),
Delete("b" ),
Equal("a" ),
];
cleanup_merge(&mut solution);
assert_diffs!(
[Equal("xca" ), Delete("cba" )],
solution,
"Slide edit right recursive" ,
);
let mut solution = diff_list![Delete("b" ), Insert("ab" ), Equal("c" )];
cleanup_merge(&mut solution);
assert_diffs!([Insert("a" ), Equal("bc" )], solution, "Empty range" );
let mut solution = diff_list![Equal("" ), Insert("a" ), Equal("b" )];
cleanup_merge(&mut solution);
assert_diffs!([Insert("a" ), Equal("b" )], solution, "Empty equality" );
}
#[ test]
fn test_cleanup_semantic_lossless() {
let mut solution = diff_list![];
cleanup_semantic_lossless(&mut solution);
assert_diffs!([], solution, "Null case" );
let mut solution = diff_list![
Equal("AAA\r\n\r\nBBB" ),
Insert("\r\nDDD\r\n\r\nBBB" ),
Equal("\r\nEEE" ),
];
cleanup_semantic_lossless(&mut solution);
assert_diffs!(
[
Equal("AAA\r\n\r\n" ),
Insert("BBB\r\nDDD\r\n\r\n" ),
Equal("BBB\r\nEEE" ),
],
solution,
"Blank lines" ,
);
let mut solution = diff_list![Equal("AAA\r\nBBB" ), Insert(" DDD\r\nBBB" ), Equal(" EEE" )];
cleanup_semantic_lossless(&mut solution);
assert_diffs!(
[Equal("AAA\r\n" ), Insert("BBB DDD\r\n" ), Equal("BBB EEE" )],
solution,
"Line boundaries" ,
);
let mut solution = diff_list![Equal("The c" ), Insert("ow and the c" ), Equal("at." )];
cleanup_semantic_lossless(&mut solution);
assert_diffs!(
[Equal("The " ), Insert("cow and the " ), Equal("cat." )],
solution,
"Word boundaries" ,
);
let mut solution = diff_list![Equal("The-c" ), Insert("ow-and-the-c" ), Equal("at." )];
cleanup_semantic_lossless(&mut solution);
assert_diffs!(
[Equal("The-" ), Insert("cow-and-the-" ), Equal("cat." )],
solution,
"Alphanumeric boundaries" ,
);
let mut solution = diff_list![Equal("a" ), Delete("a" ), Equal("ax" )];
cleanup_semantic_lossless(&mut solution);
assert_diffs!([Delete("a" ), Equal("aax" )], solution, "Hitting the start" );
let mut solution = diff_list![Equal("xa" ), Delete("a" ), Equal("a" )];
cleanup_semantic_lossless(&mut solution);
assert_diffs!([Equal("xaa" ), Delete("a" )], solution, "Hitting the end" );
let mut solution = diff_list![Equal("The xxx. The " ), Insert("zzz. The " ), Equal("yyy." )];
cleanup_semantic_lossless(&mut solution);
assert_diffs!(
[Equal("The xxx." ), Insert(" The zzz." ), Equal(" The yyy." )],
solution,
"Sentence boundaries" ,
);
}
#[ test]
fn test_cleanup_semantic() {
let mut solution = diff_list![];
cleanup_semantic(&mut solution);
assert_diffs!([], solution, "Null case" );
let mut solution = diff_list![Delete("ab" ), Insert("cd" ), Equal("12" ), Delete("e" )];
cleanup_semantic(&mut solution);
assert_diffs!(
[Delete("ab" ), Insert("cd" ), Equal("12" ), Delete("e" )],
solution,
"No elimination #1" ,
);
let mut solution = diff_list![Delete("abc" ), Insert("ABC" ), Equal("1234" ), Delete("wxyz" )];
cleanup_semantic(&mut solution);
assert_diffs!(
[Delete("abc" ), Insert("ABC" ), Equal("1234" ), Delete("wxyz" )],
solution,
"No elimination #2" ,
);
let mut solution = diff_list![Delete("a" ), Equal("b" ), Delete("c" )];
cleanup_semantic(&mut solution);
assert_diffs!([Delete("abc" ), Insert("b" )], solution, "Simple elimination" ,);
let mut solution = diff_list![
Delete("ab" ),
Equal("cd" ),
Delete("e" ),
Equal("f" ),
Insert("g" ),
];
cleanup_semantic(&mut solution);
assert_diffs!(
[Delete("abcdef" ), Insert("cdfg" )],
solution,
"Backpass elimination" ,
);
let mut solution = diff_list![
Insert("1" ),
Equal("A" ),
Delete("B" ),
Insert("2" ),
Equal("_" ),
Insert("1" ),
Equal("A" ),
Delete("B" ),
Insert("2" ),
];
cleanup_semantic(&mut solution);
assert_diffs!(
[Delete("AB_AB" ), Insert("1A2_1A2" )],
solution,
"Multiple elimination" ,
);
let mut solution = diff_list![Equal("The c" ), Delete("ow and the c" ), Equal("at." )];
cleanup_semantic(&mut solution);
assert_diffs!(
[Equal("The " ), Delete("cow and the " ), Equal("cat." )],
solution,
"Word boundaries" ,
);
let mut solution = diff_list![Delete("abcxx" ), Insert("xxdef" )];
cleanup_semantic(&mut solution);
assert_diffs!(
[Delete("abcxx" ), Insert("xxdef" )],
solution,
"No overlap elimination" ,
);
let mut solution = diff_list![Delete("abcxxx" ), Insert("xxxdef" )];
cleanup_semantic(&mut solution);
assert_diffs!(
[Delete("abc" ), Equal("xxx" ), Insert("def" )],
solution,
"Overlap elimination" ,
);
let mut solution = diff_list![Delete("xxxabc" ), Insert("defxxx" )];
cleanup_semantic(&mut solution);
assert_diffs!(
[Insert("def" ), Equal("xxx" ), Delete("abc" )],
solution,
"Reverse overlap elimination" ,
);
let mut solution = diff_list![
Delete("abcd1212" ),
Insert("1212efghi" ),
Equal("----" ),
Delete("A3" ),
Insert("3BC" ),
];
cleanup_semantic(&mut solution);
assert_diffs!(
[
Delete("abcd" ),
Equal("1212" ),
Insert("efghi" ),
Equal("----" ),
Delete("A" ),
Equal("3" ),
Insert("BC" ),
],
solution,
"Two overlap eliminations" ,
);
}
#[ test]
fn test_bisect() {
let text1 = range!("cat" );
let text2 = range!("map" );
let solution = Solution {
text1,
text2,
diffs: bisect(text1, text2),
};
assert_diffs!(
[
Delete("c" ),
Insert("m" ),
Equal("a" ),
Delete("t" ),
Insert("p" ),
],
solution,
"Normal" ,
);
}
#[ test]
fn test_main() {
let solution = main(Range::empty(), Range::empty());
assert_diffs!([], solution, "Null case" );
let solution = main(range!("abc" ), range!("abc" ));
assert_diffs!([Equal("abc" )], solution, "Equality" );
let solution = main(range!("abc" ), range!("ab123c" ));
assert_diffs!(
[Equal("ab" ), Insert("123" ), Equal("c" )],
solution,
"Simple insertion" ,
);
let solution = main(range!("a123bc" ), range!("abc" ));
assert_diffs!(
[Equal("a" ), Delete("123" ), Equal("bc" )],
solution,
"Simple deletion" ,
);
let solution = main(range!("abc" ), range!("a123b456c" ));
assert_diffs!(
[
Equal("a" ),
Insert("123" ),
Equal("b" ),
Insert("456" ),
Equal("c" ),
],
solution,
"Two insertions" ,
);
let solution = main(range!("a123b456c" ), range!("abc" ));
assert_diffs!(
[
Equal("a" ),
Delete("123" ),
Equal("b" ),
Delete("456" ),
Equal("c" ),
],
solution,
"Two deletions" ,
);
let solution = main(range!("a" ), range!("b" ));
assert_diffs!([Delete("a" ), Insert("b" )], solution, "Simple case #1" );
let solution = main(
range!("Apples are a fruit." ),
range!("Bananas are also fruit." ),
);
assert_diffs!(
[
Delete("Apple" ),
Insert("Banana" ),
Equal("s are a" ),
Insert("lso" ),
Equal(" fruit." ),
],
solution,
"Simple case #2" ,
);
let solution = main(range!("ax\t" ), range!("\u{0680}x\000" ));
assert_diffs!(
[
Delete("a" ),
Insert("\u{0680}" ),
Equal("x" ),
Delete("\t" ),
Insert("\000" ),
],
solution,
"Simple case #3" ,
);
let solution = main(range!("1ayb2" ), range!("abxab" ));
assert_diffs!(
[
Delete("1" ),
Equal("a" ),
Delete("y" ),
Equal("b" ),
Delete("2" ),
Insert("xab" ),
],
solution,
"Overlap #1" ,
);
let solution = main(range!("abcy" ), range!("xaxcxabc" ));
assert_diffs!(
[Insert("xaxcx" ), Equal("abc" ), Delete("y" )],
solution,
"Overlap #2" ,
);
let solution = main(
range!("ABCDa=bcd=efghijklmnopqrsEFGHIJKLMNOefg" ),
range!("a-bcd-efghijklmnopqrs" ),
);
assert_diffs!(
[
Delete("ABCD" ),
Equal("a" ),
Delete("=" ),
Insert("-" ),
Equal("bcd" ),
Delete("=" ),
Insert("-" ),
Equal("efghijklmnopqrs" ),
Delete("EFGHIJKLMNOefg" ),
],
solution,
"Overlap #3" ,
);
let solution = main(
range!("a [[Pennsylvania]] and [[New" ),
range!(" and [[Pennsylvania]]" ),
);
assert_diffs!(
[
Insert(" " ),
Equal("a" ),
Insert("nd" ),
Equal(" [[Pennsylvania]]" ),
Delete(" and [[New" ),
],
solution,
"Large equality" ,
);
}
Messung V0.5 in Prozent C=90 H=97 G=93
¤ Dauer der Verarbeitung: 0.17 Sekunden
(vorverarbeitet am 2026-08-25)
¤
*© Formatika GbR, Deutschland