diff --git a/java/src/SimpleDiff.java b/java/src/SimpleDiff.java new file mode 100644 index 0000000..1124950 --- /dev/null +++ b/java/src/SimpleDiff.java @@ -0,0 +1,193 @@ +/* +(C) Paul Butler 2008-2012 +May be used and distributed under the zlib/libpng license + + +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 List< Map.Entry > > + diff( List olds, List news ) { + //System.out.printf("diff \"%s\" vs \"%s\"%n", olds, news ); + // Create a map from old values to their indices + Map< E, List > oldIndexMap = new HashMap<>(); + for( int i=0; i indexList = oldIndexMap.getOrDefault(olds.get(i), new ArrayList()); + 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 overlap, _overlap; + + overlap = new HashMap<>(); + + for( int inew=0; inew(); + E val=news.get(inew); + for( Integer iold : oldIndexMap.getOrDefault(val, new ArrayList()) ) { + // 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 > > 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 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 > diffString( String olds, String news ) { + List oldList = new ArrayList<>(); + List newList = new ArrayList<>(); + for( int i=0; i > > retList = diff( oldList, newList ); + + List< Map.Entry > retString = retList.stream().map( + entry -> new AbstractMap.SimpleEntry<>( + entry.getKey(), + listCharToString( entry.getValue() ) ) ).collect( Collectors.toList()); + + return retString; + } + +} + diff --git a/java/src/SimpleDiffTest.java b/java/src/SimpleDiffTest.java new file mode 100644 index 0000000..d1ed02c --- /dev/null +++ b/java/src/SimpleDiffTest.java @@ -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 > ret; + List< Map.Entry > expected; + ret = SimpleDiff.diffString(diffDataCharOld, diffDataCharNew); + + expected = Arrays.asList( diffDataCharExpected ).stream() + .map(ar -> new AbstractMap.SimpleEntry( + 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 > > ret; + List< Map.Entry > > expected; + ret = SimpleDiff.diff(Arrays.asList(oldWord), Arrays.asList(newWord)); + + expected = Arrays.asList( expectedWord ).stream() + .map(ar -> new AbstractMap.SimpleEntry >( + 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 oldIntList, List newIntList, List> expectedIntList ) { + List< Map.Entry > > ret; + List< Map.Entry > > expected; + ret = SimpleDiff.diff(oldIntList, newIntList); + + expected = expectedIntList.stream() + .map(li -> new AbstractMap.SimpleEntry >( + 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" ); + } + + + +} +