Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions java/src/SimpleDiff.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
(C) Paul Butler 2008-2012 <http://www.paulbutler.org/>
May be used and distributed under the zlib/libpng license
<http://www.opensource.org/licenses/zlib-license.php>

Java implementation by Philippe Fremy, Copyright 2015

*/
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class SimpleDiff {
public static void main(String[] args) {
System.out.println( diffString("tito", "toto") );
System.out.println( diff( Arrays.asList(
"line1: if (titi) {",
"line2: toto();",
"line3: }"
), Arrays.asList(
"line1: if (titi && tutu) {",
"line2: toto();",
"line3: }"
)));


}

static public enum DiffOp {
ADD('+'), DEL('-'), EQ('=');

private char c;

DiffOp( char c ) {
this.c = c;
}

static public DiffOp fromString( String s ) {
return fromChar( s.charAt(0));
}

static public DiffOp fromInt( int i ) {
switch( i ) {
case 0:
return ADD;
case 1:
return DEL;
case 2:
return EQ;
}

return EQ;
}



static public DiffOp fromChar( char c ) {
switch( c ) {
case '+':
return ADD;
case '-':
return DEL;
case '=':
return EQ;
}

return EQ;
}

public String toString() {
return String.valueOf(c);
}
}

/** Return the differences between two generic lists */
static public <E> List< Map.Entry<DiffOp, List<E> > >
diff( List<E> olds, List<E> news ) {
//System.out.printf("diff \"%s\" vs \"%s\"%n", olds, news );
// Create a map from old values to their indices
Map< E, List<Integer> > oldIndexMap = new HashMap<>();
for( int i=0; i<olds.size(); i++) {
List<Integer> indexList = oldIndexMap.getOrDefault(olds.get(i), new ArrayList<Integer>());
indexList.add(i);
oldIndexMap.put(olds.get(i), indexList);
}

//System.out.printf( "oldIndexMap: %s%n", oldIndexMap );

// Find the largest substring common to old and new.
// We use a dynamic programming approach here.
//
// We iterate over each value in the `new` list, calling the
// index `inew`. At each iteration, `overlap[iold]` is the
// length of the largest suffix of `old[:iold]` equal to a suffix
// of `new[:inew]` (or unset when `old[iold]` != `new[inew]`).
//
// At each stage of iteration, the new `overlap` (called
// `_overlap` until the original `overlap` is no longer needed)
// is built from the old one.
//
// If the length of overlap exceeds the largest substring
// seen so far (`sub_length`), we update the largest substring
// to the overlapping strings.

// `subStartOld` is the index of the beginning of the largest overlapping
// substring in the old list. `subStartNew` is the index of the beginning
// of the same substring in the new list. `subLength` is the length that
// overlaps in both.

// These track the largest overlapping substring seen so far, so naturally
// we start with a 0-length substring.
int subStartOld=0, subStartNew=0, subLength=0;
HashMap<Integer, Integer> overlap, _overlap;

overlap = new HashMap<>();

for( int inew=0; inew<news.size(); inew++) {
_overlap = new HashMap<>();
E val=news.get(inew);
for( Integer iold : oldIndexMap.getOrDefault(val, new ArrayList<Integer>()) ) {
// now we are considering all values of iold such that
// `old[iold] == new[inew]`.
if (iold == 0) {
_overlap.put(iold, 1);
} else {
_overlap.put(iold, overlap.getOrDefault(iold-1, 0)+1 );
}

if (_overlap.get(iold) > subLength) {
// this is the largest substring seen so far, so store its
// indices
subLength = _overlap.get(iold);
subStartOld = iold - subLength + 1;
subStartNew = inew - subLength + 1;
}
}
overlap = _overlap;
//System.out.printf("inew=%d %s%n", inew, overlap);
}

List< Map.Entry<DiffOp, List<E> > > ret = new ArrayList<>();
if (subLength==0) {
// no common substring found
if (olds.size() > 0) {
ret.add( new AbstractMap.SimpleEntry<> (DiffOp.DEL, olds ) );
}
if (news.size() > 0) {
ret.add( new AbstractMap.SimpleEntry<> (DiffOp.ADD, news ) );
}
} else {
ret.addAll( diff( olds.subList(0, subStartOld), news.subList(0, subStartNew)) );
ret.add( new AbstractMap.SimpleEntry<>(DiffOp.EQ, news.subList(subStartNew, subStartNew + subLength) ) );
ret.addAll( diff( olds.subList(subStartOld+subLength, olds.size()), news.subList(subStartNew+subLength, news.size())));
}
//System.out.println( ret );
return ret;
}

static String listCharToString( List<Character> l ) {
StringBuilder builder = new StringBuilder();
for( Character c : l ) {
builder.append( c );
}
return builder.toString();
}

/** Return the difference between two strings */
static public List< Map.Entry<DiffOp, String > > diffString( String olds, String news ) {
List<Character> oldList = new ArrayList<>();
List<Character> newList = new ArrayList<>();
for( int i=0; i<olds.length(); i++) {
oldList.add( olds.charAt(i));
}
for( int i=0; i<news.length(); i++) {
newList.add( news.charAt(i));
}

List< Map.Entry<DiffOp, List<Character> > > retList = diff( oldList, newList );

List< Map.Entry<DiffOp, String> > retString = retList.stream().map(
entry -> new AbstractMap.SimpleEntry<>(
entry.getKey(),
listCharToString( entry.getValue() ) ) ).collect( Collectors.toList());

return retString;
}

}

186 changes: 186 additions & 0 deletions java/src/SimpleDiffTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import static org.junit.Assert.*;

import org.junit.Test;

import java.util.AbstractMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


public class SimpleDiffTest {

static String diffDataCharOld = "The quick brown fox.";
static String diffDataCharNew = "The kuick brown fix.";
static String[][] diffDataCharExpected = {
{ "=", "The " },
{ "-", "q" },
{ "+", "k" },
{ "=", "uick brown f" },
{ "-", "o" },
{ "+", "i" },
{ "=", "x." },
};

@Test
public void testDiffString() {
List< Map.Entry<SimpleDiff.DiffOp, String > > ret;
List< Map.Entry<SimpleDiff.DiffOp, String > > expected;
ret = SimpleDiff.diffString(diffDataCharOld, diffDataCharNew);

expected = Arrays.asList( diffDataCharExpected ).stream()
.map(ar -> new AbstractMap.SimpleEntry<SimpleDiff.DiffOp, String>(
SimpleDiff.DiffOp.fromString(ar[0]), ar[1]) )
.collect(Collectors.toList());
assertEquals(expected, ret);;

}

static String[] diffDataWordOld1 = { "The", "quick", "brown", "fox" };
static String[] diffDataWordNew1 = { "The", "slow", "green", "turtle" };
static String[][] diffDataWordExpected1 = {
{"=", "The"},
{"-", "quick", "brown", "fox"},
{"+", "slow", "green", "turtle"},
};

static String[] diffDataWordOld2 = { "jumps", "over", "the", "lazy", "dog" };
static String[] diffDataWordNew2 = { "walks", "around", "the", "orange", "cat" };
static String[][] diffDataWordExpected2 = {
{"-", "jumps", "over" },
{"+", "walks", "around" },
{"=", "the" },
{"-", "lazy", "dog" },
{"+", "orange", "cat" },
};

@Test
public void testDiffWord1() {
subDiffWord(diffDataWordOld1, diffDataWordNew1, diffDataWordExpected1 );
}

@Test
public void testDiffWord2() {
subDiffWord(diffDataWordOld2, diffDataWordNew2, diffDataWordExpected2 );
}

public void subDiffWord( String[] oldWord, String[] newWord, String[][] expectedWord ) {
List< Map.Entry<SimpleDiff.DiffOp, List<String> > > ret;
List< Map.Entry<SimpleDiff.DiffOp, List<String> > > expected;
ret = SimpleDiff.diff(Arrays.asList(oldWord), Arrays.asList(newWord));

expected = Arrays.asList( expectedWord ).stream()
.map(ar -> new AbstractMap.SimpleEntry<SimpleDiff.DiffOp, List<String> >(
SimpleDiff.DiffOp.fromString(ar[0]),
Arrays.asList(ar).subList(1, ar.length) ) )
.collect(Collectors.toList());
assertEquals(expected, ret);
}

static int ADD=0;
static int DEL=1;
static int EQ=2;

static Integer[] diffDataIntOld1 = {1, 3, 4};
static Integer[] diffDataIntNew1 = {1, 2, 3, 4};
static Integer[][] diffDataIntExpected1 = {
{EQ ,1 },
{ADD, 2},
{EQ, 3, 4},
};

static Integer[] diffDataIntOld2 = {1, 2, 3, 8, 9, 12, 13 };
static Integer[] diffDataIntNew2 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};
static Integer[][] diffDataIntExpected2 = {
{ EQ, 1, 2, 3 },
{ ADD, 4, 5, 6, 7 },
{ EQ, 8, 9 },
{ ADD, 10, 11 },
{ EQ, 12, 13 },
{ ADD, 14, 15 },
};

static Integer[] diffDataIntOld34 = {1, 2, 3, 4, 5 };
static Integer[] diffDataIntNew34 = {1, 2, 2, 3, 4, 5};
static Integer[][] diffDataIntExpected34 = {
{ EQ, 1 },
{ ADD, 2 },
{ EQ, 2, 3, 4, 5 },
};

static Integer[] diffDataIntOld4 = {1, 2, 3, 4, 5 };
static Integer[] diffDataIntNew4 = {1, 2, 2, 3, 4, 4, 5};
static Integer[][] diffDataIntExpected4 = {
{ EQ, 1 },
{ ADD, 2 },
{ EQ, 2, 3, 4 },
{ ADD, 4 },
{ EQ, 5 },
};

static Integer[] diffDataIntOld5 = {1, 2, 3, 4, 5 };
static Integer[] diffDataIntNew5 = {1, 2, 1, 2, 3, 3, 2, 1, 4, 5};
static Integer[][] diffDataIntExpected5 = {
{ ADD, 1, 2 },
{ EQ, 1, 2, 3 },
{ ADD, 3, 2, 1 },
{ EQ, 4, 5 },
};

static Integer[] diffDataIntOld6 = {1, 2, 3, 4, 5};
static Integer[] diffDataIntNew6 = {1, 2, 5};
static Integer[][] diffDataIntExpected6 = {
{ EQ, 1, 2 },
{ DEL, 3, 4 },
{ EQ, 5 },
};

static Integer[] diffDataIntOld7 = {1, 2, 3, 4, 5, 6, 7, 8};
static Integer[] diffDataIntNew7 = {3, 6, 7};
static Integer[][] diffDataIntExpected7 = {
{ DEL, 1, 2 },
{ EQ, 3 },
{ DEL, 4, 5 },
{ EQ, 6, 7 },
{ DEL, 8 },
};

static Integer[] diffDataIntOld8 = {1, 2, 3, 4, 5, 1, 2, 3, 4, 5};
static Integer[] diffDataIntNew8 = {1, 2, 3, 4, 5};
static Integer[][] diffDataIntExpected8 = {
{EQ, 1, 2, 3, 4, 5},
{DEL, 1, 2, 3, 4, 5},
};

@Test
public void testDiffInt1() {
subDiffInt(Arrays.asList(diffDataIntOld2),
Arrays.asList(diffDataIntNew2),
Arrays.asList(diffDataIntExpected2).stream()
.map(ar -> Arrays.asList(ar)).collect(Collectors.toList()));
}

public void subDiffInt( List<Integer> oldIntList, List<Integer> newIntList, List<List<Integer>> expectedIntList ) {
List< Map.Entry<SimpleDiff.DiffOp, List<Integer> > > ret;
List< Map.Entry<SimpleDiff.DiffOp, List<Integer> > > expected;
ret = SimpleDiff.diff(oldIntList, newIntList);

expected = expectedIntList.stream()
.map(li -> new AbstractMap.SimpleEntry<SimpleDiff.DiffOp, List<Integer> >(
SimpleDiff.DiffOp.fromInt(li.get(0)),
li.subList(1, li.size()) ) )
.collect(Collectors.toList());
assertEquals(expected, ret);
}

@Test
public void testListCharToString() {
Character[] listChar = { 't', 'o', 't', 'o' };
assertEquals( SimpleDiff.listCharToString( Arrays.asList(listChar) ), "toto" );
}



}