这是 git diff 后的效果,感觉挺简单的,不就是 比较新旧版本,新增了就用 "+" 显示新加一行,删除了就用 "-" 显示删除一行,修改了一行就用 "-"、"+" 显示将旧版本中的该行干掉了并且新版本中增加了一行,即使用 "删除" + "新增" 操作代替 "修改" 操作。
然后我写的测试代码如下:
import org.apache.commons.text.similarity.LevenshteinDistance;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
public class MyDiffTest {
public static void main(String[] args) {
List<String> lines_old = loadTxtFile2List("\\xxx\\DemoClass1.java" );
List<String> lines_new = loadTxtFile2List("\\xxx\\DemoClass2.java");
// lines1的起始行和 lines2 的起始行做映射
// 扫描旧版本中的每一行
int size = lines_old.size();
for( int i=0;i<size;i++ ){
// 从新版本中找该行
String line_old = lines_old.get(i);
String line_new = lines_new.get(i);
// 如果发现版本中中该行的数据变了,那么提示删除了旧的行,添加了新的行
if( line_new.equals( line_old ) ){
System.out.println( line_old );
}else {
System.out.println( "- " + line_old );
System.out.println( "+ " + line_new );
}
}
// xxxx xxxx1 -xxxx
// yyyy yyyy +xxxx1
// xxxxxx xxxxxx xxxxxx
// zzzz zzzz zzzz
}
private static List<String> loadTxtFile2List(String filePath) {
BufferedReader reader = null;
List<String> lines = new ArrayList<>();
try {
reader = new BufferedReader(new FileReader(filePath));
String line = reader.readLine();
while (line != null) {
// System.out.println(line);
lines.add( line );
line = reader.readLine();
}
return lines;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
其中用到的2个新旧版本的文本如下:
DemoClass1.java:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.similarity.LevenshteinDistance;
@Slf4j
public class DemoClass1 {
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE = LevenshteinDistance.getDefaultInstance();
public static String null2emptyWithTrim( String str ){
if( str == null ){
str = "";
}
str = str.trim();
return str;
}
public static String requiredStringParamCheck(String param, String paramRemark) {
param = null2emptyWithTrim( param );
if( param.length() == 0 ){
String msg = "操作失败,请求参数 \"" + paramRemark + "\" 为空";
log.error( msg );
throw new BusinessLogicException( msg );
}
return param;
}
public static double calculateSimilarity( String str1,String str2 ){
int distance = LEVENSHTEIN_DISTANCE.apply(str1, str2);
double similarity = 1 - (double) distance / Math.max(str1.length(), str2.length());
System.out.println("相似度:" + similarity);
return similarity;
}
}
DemoClass2.java:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.similarity.LevenshteinDistance;
@Slf4j
public class DemoClass2 {
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE = LevenshteinDistance.getDefaultInstance();
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE1 = LevenshteinDistance.getDefaultInstance();
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE2 = LevenshteinDistance.getDefaultInstance();
public static String null2emptyWithTrim( String str ){
if( str == null ){
str = "";
}
str = str.trim();
return str;
}
public static String requiredStringParamCheck(String param, String paramRemark) {
param = null2emptyWithTrim( param );
if( param.length() == 0 ){
String msg = "操作失败,请求参数 \"" + paramRemark + "\" 为空";
log.error( msg );
throw new BusinessLogicException( msg );
}
return param;
}
public static double calculateSimilarity( String str1,String str2 ){
int distance = LEVENSHTEIN_DISTANCE.apply(str1, str2);
double similarity = 1 - (double) distance / Math.max(str1.length(), str2.length());
System.out.println("相似度:" + similarity);
return similarity;
}
}
DemoClass2.java 相较于 DemoClass1.java 的区别是 "public class" 后面的类名不同,"private static final LevenshteinDistance LEVENSHTEIN_DISTANC..." 多复制了2行并改了名称,然后运行后显示差别如下:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.similarity.LevenshteinDistance;
@Slf4j
- public class DemoClass1 {
+ public class DemoClass2 {
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE = LevenshteinDistance.getDefaultInstance();
-
+ private static final LevenshteinDistance LEVENSHTEIN_DISTANCE1 = LevenshteinDistance.getDefaultInstance();
- public static String null2emptyWithTrim( String str ){
+ private static final LevenshteinDistance LEVENSHTEIN_DISTANCE2 = LevenshteinDistance.getDefaultInstance();
- if( str == null ){
+
- str = "";
+ public static String null2emptyWithTrim( String str ){
- }
+ if( str == null ){
- str = str.trim();
+ str = "";
- return str;
+ }
- }
+ str = str.trim();
-
+ return str;
- public static String requiredStringParamCheck(String param, String paramRemark) {
+ }
- param = null2emptyWithTrim( param );
+
- if( param.length() == 0 ){
+ public static String requiredStringParamCheck(String param, String paramRemark) {
- String msg = "操作失败,请求参数 \"" + paramRemark + "\" 为空";
+ param = null2emptyWithTrim( param );
- log.error( msg );
+ if( param.length() == 0 ){
- throw new BusinessLogicException( msg );
+ String msg = "操作失败,请求参数 \"" + paramRemark + "\" 为空";
- }
+ log.error( msg );
- return param;
+ throw new BusinessLogicException( msg );
- }
+ }
-
+ return param;
- public static double calculateSimilarity( String str1,String str2 ){
+ }
- int distance = LEVENSHTEIN_DISTANCE.apply(str1, str2);
+
- double similarity = 1 - (double) distance / Math.max(str1.length(), str2.length());
+ public static double calculateSimilarity( String str1,String str2 ){
- System.out.println("相似度:" + similarity);
+ int distance = LEVENSHTEIN_DISTANCE.apply(str1, str2);
- return similarity;
+ double similarity = 1 - (double) distance / Math.max(str1.length(), str2.length());
- }
+ System.out.println("相似度:" + similarity);
- }
+ return similarity;
为啥???
如上两张图片,旧版本的第10行和新版本的第10行对应,从直观上看新版本的第11、12行是在旧版本的第10行和第11行之间插进去的,但是程序并不这么认为,它会认为将旧版本的第11行的空白行修改为了新版本的 “private static final LevenshteinDistance LEVENSHTEIN_DISTANCE1 = LevenshteinDistance.getDefaultInstance();” 为什么我们人眼会这么直观的感觉到 新版本的 第11、12行时插进去的,因为我们比较了新旧版本的第7、8、9、10行都差不多,旧版本的11~27行和新版本的 13~29行都差不多,所以自然而然的认为新版本的11、12行是直接插进去的,那么现在我们就来算法实现吧!( ps:前文中的 “差不多” 是差几多?是完全equals 还是很像?”其实这里可以设置一个阈值,使用求字符串的相似度算法求出相似度,网上有现成的类库求相似度,自己也可以使用动态规划写一个简单的字符串相似度算法 )
感觉恰当的算法的执行过程应该是这样的:
新旧版本各维持一个行号游标:index_old、index_new,最开始这两个游标相同,越往后可能不同,但是他们表示的行的内容应该是应该相似的,因为新版本的修改会导致内容越来越 “错位”。假设我们从上面2张图片的第7行开始描:
1. 最开始 index_old = index_new = 7,算法检测到新旧版本的第7行的内容相同( 后面我们就尽量用伪代码表示,就不说这么多啰里啰嗦的话了 ),即 lines_old[ 7] = lines_new[ 7]。
2. index_old++,index_new++,都变为8,算法检测到 lines_old[ 8 ] != lines_new[ index_new ],并且他们的相似度很高,所以算法判断新版本的第8行相较于旧版本的第8行是做了修改操作。
3. index_old++,index_new++,都变为9,算法检测到 lines_old[ 9 ] = lines_new[ 9 ]。
4. index_old++,index_new++,都变为10,算法检测到 lines_old[ 10 ] = lines_new[ 10 ]。
5. index_old++,index_new++,都变为11,算法检测到 lines_old[ 11 ] != lines_new[ 11 ],并且这两行的文本内容及不相似,所以判断新版本是在旧版本的第11行插入了一行 “private static final LevenshteinDistance LEVENSHTEIN_DISTANCE1 =LevenshteinDistance.getDefaultInstance();”,所以此时旧版本的第11行就和新版本的第11行对应不上了,而是和新版本的第12行做对应。
6. index_old 不变,index_new++,index_old 还是11,index_new 变为 12,即旧版本的第11行要和新版本的第12行做对应,就像找老婆一样,旧7和新7结为了夫妻,旧8和新8结为了夫妻( 虽然有一点点的裂痕,但不打紧 ),新9和新9结为了夫妻,...,所以旧11也要在新版本中找到一个新x作为自己的伴侣,本来已经找到了一个新11,但是发现新11和自己三观差别很大,根本合不来,所以pass掉,继续找,现在发现了下一个相亲对象 新12,发现lines_old[ 11 ] 和 lines_new[ 12 ] 相似度还是差别很大,所以算法判断新版本又插入了一个新行 “private static final LevenshteinDistance LEVENSHTEIN_DISTANCE2 = LevenshteinDistance.getDefaultInstance();”。
7. index_old 不变,index_new++,index_old 还是11,index_new 变为 13,因为 lines_old[ 11 ] = lines_new[ 13 ],所以 旧11 很幸运的找到了自己的伴侣 新13,。
8. index_old++,index_new++,index_old变为12,index_new变为14,因为 lines_old[ 12 ] = lines_new[ 14 ],所以此步未检测到变化。
...
改进后的测试代码如下:
import org.apache.commons.text.similarity.LevenshteinDistance;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
public class MyDiffTest {
public static void main(String[] args) {
List<String> lines_old = loadTxtFile2List("\\xxx\\DemoClass1.java" );
List<String> lines_new = loadTxtFile2List("\\xxx\\DemoClass2.java");
// lines1的起始行和 lines2 的起始行做映射
// 扫描旧版本中的每一行
// xxxx xxxx1 -xxxx
// yyyy yyyy +xxxx1
// xxxxxx xxxxxx xxxxxx
// zzzz zzzz zzzz
int index_old = 0;
int index_new = 0;
while ( true ){
if( index_old > ( lines_old.size() - 1 ) ){
break;
}
String line_old = lines_old.get(index_old);
String line_new = lines_new.get(index_new);
// 先检测是否相等
String line_old_trim = line_old.trim();
String line_new_trim = line_new.trim();
if( line_old_trim.equals( line_new_trim.trim() ) ){
// 相等,直接输出,表示此行没有修改
System.out.println( line_old );
index_old++;
index_new++;
}else {
// 不相等
// 检测相似度,
double similarity = MyStringUitls.calculateSimilarity(line_old_trim, line_new_trim);
if( similarity > 0.5d ){
// 相似,表示此行进行了修改
System.out.println( "-" + line_old );
System.out.println( "+" + line_new );
index_old++;
index_new++;
}else {
// 不相似,表示新版本进行了插入
System.out.println( "+" +line_new );
index_new++;
}
}
}
}
private static List<String> loadTxtFile2List(String filePath) {
BufferedReader reader = null;
List<String> lines = new ArrayList<>();
try {
reader = new BufferedReader(new FileReader(filePath));
String line = reader.readLine();
while (line != null) {
// System.out.println(line);
lines.add( line );
line = reader.readLine();
}
return lines;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
修改 DemoClass2.java 中方法 calculateSimilarity 的代码( 方法内容使用 try catch 包裹 )后比较差异后的的输出效果如下:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.similarity.LevenshteinDistance;
@Slf4j
-public class DemoClass1 {
+public class DemoClass2 {
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE = LevenshteinDistance.getDefaultInstance();
+ private static final LevenshteinDistance LEVENSHTEIN_DISTANCE1 = LevenshteinDistance.getDefaultInstance();
+ private static final LevenshteinDistance LEVENSHTEIN_DISTANCE2 = LevenshteinDistance.getDefaultInstance();
public static String null2emptyWithTrim( String str ){
if( str == null ){
str = "";
}
str = str.trim();
return str;
}
public static String requiredStringParamCheck(String param, String paramRemark) {
param = null2emptyWithTrim( param );
if( param.length() == 0 ){
String msg = "操作失败,请求参数 \"" + paramRemark + "\" 为空";
log.error( msg );
throw new BusinessLogicException( msg );
}
return param;
}
public static double calculateSimilarity( String str1,String str2 ){
+ try {
int distance = LEVENSHTEIN_DISTANCE.apply(str1, str2);
double similarity = 1 - (double) distance / Math.max(str1.length(), str2.length());
System.out.println("相似度:" + similarity);
return similarity;
+ }catch ( Exception e ){
+ e.printStackTrace();
+ return 0d;
}
}
感觉有点 "git diff" 的那味儿了,其实算法还右很多需要优化的部分,比如这里最开始 index_old、index_new 都初始化为0,这是因为本例中的旧版本文本 DemoClass1.java 和 新版本文本DemoClass2.java 头部的几行都相同。比如新版本删除了行 ...
如下,右侧的新版本删除了 "log.error( msg );" 行。
如下,右侧新版本在 "log.error( msg );" 上面新增了三行。
那么算法在 “String msg = "操作失败,请求参数 \"" + paramRemark + "\" 为空";” 这一行配对后,分别进去新旧版本各自的下一行后,如何配对呢?换句话说,旧版本取出 “log.error( msg );” 行,新版本无论是新增行还是删除行,都找不到与之配对的行,那么是怎么判断新版本是新增行还是删除行呢?
如上图,新版本在 "log.error( msg );" 行上边新增了三行 "// xxxx",导致 "log.error( msg );" 被挤到了下边,导致旧版本的行 "log.error( msg );" 未匹配到 新版本的行 "log.error( msg );"( 匹配到了 "// xxxx" )。这时候如果检测 lines_new 中 Index > index_new的部分是否存在line_old,如果存在则表示新版本做了新增行操作,否则表示新版本做了删除行操作。如果做了新增行操作,输出 "+" +line_new,index_new++;如果做了删除行操作,输出 "-" +line_old,index_old++。改进后的算法如下:
import org.apache.commons.text.similarity.LevenshteinDistance;
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
public class MyDiffTest {
public static void main(String[] args) {
List<String> lines_old = loadTxtFile2List("\\xxx\\DemoClass1.java" );
List<String> lines_new = loadTxtFile2List("\\xxx\\DemoClass2.java");
// lines1的起始行和 lines2 的起始行做映射
// 扫描旧版本中的每一行
// xxxx xxxx1 -xxxx
// yyyy yyyy +xxxx1
// xxxxxx xxxxxx xxxxxx
// zzzz zzzz zzzz
int index_old = 0;
int index_new = 0;
while ( true ){
if( index_old > ( lines_old.size() - 1 ) ){
break;
}
String line_old = lines_old.get(index_old);
String line_new = lines_new.get(index_new);
// 先检测是否相等
String line_old_trim = line_old.trim();
String line_new_trim = line_new.trim();
if( line_old_trim.equals( line_new_trim.trim() ) ){
// 相等,直接输出,表示此行没有修改
String prefix = getPrefix( index_old, index_new,true );
System.out.println( prefix + line_old );
index_old++;
index_new++;
}else {
// 不相等
// 检测相似度,
double similarity = MyStringUitls.calculateSimilarity(line_old_trim, line_new_trim);
if( similarity > 0.5d ){
// 相似,表示此行进行了修改
String prefix = getPrefix( index_old, index_new,true );
System.out.println( prefix + "-" + line_old );
System.out.println( prefix + "+" + line_new );
index_old++;
index_new++;
}else {
// 不相似,表示新版本进行了 插入 或 删除( 如何判断是做了插入操作还是删除操作呢? )
Boolean doInsert = false;
// 新版本的当前行和旧版本的当前行相似度低于50%,要么新版本在当前行做了删除行操作,要么新版本在当前行做了新增行操作
// 怎么检测呢?如果是新增操作,则新版本中和 旧版本的 line_old 内容相同的行会被挤到下( 下... )行,
// 即检测 lines_new 中 index> index_new 的部分是否存在内容和 旧版本 line_old 内容一样的行,如果存在,表示新版本做了新增行操作
// 否则,新版本做了删除行操作,
if( contains( lines_new,index_new+1,line_old ) ){
// 新版本做了插入操作
String prefix = getPrefix(index_old, index_new, false);
System.out.println( prefix + "+" +line_new );
index_new++;
}else {
// 新版本做了删除操作
String prefix = getPrefix(index_old, index_new, false);
System.out.println( prefix + "-" +line_old );
index_old++;
}
}
}
}
}
/**
* 检测集合 list 中是否包含 targetElement 元素( 从 index_beginCheck 角标处开始检查,包含该角标 )
* @param list
* @param index_beginCheck
* @return
*/
private static boolean contains(List<String> list, int index_beginCheck,String targetElement) {
int size = list.size();
targetElement = targetElement.trim();
for (int i = index_beginCheck; i < size; i++) {
if( targetElement.equals( list.get( i ).trim() ) ){
return true;
}
}
return false;
}
private static String getPrefix( int index_old,int index_new,boolean matched ){
index_old++;
index_new++;
return ( index_old < 10?" ":"" ) + index_old + "-->" + ( index_new < 10?" ":"" ) + index_new + " " + ( matched?"matched ":"not found " );
}
private static List<String> loadTxtFile2List(String filePath) {
BufferedReader reader = null;
List<String> lines = new ArrayList<>();
try {
reader = new BufferedReader(new FileReader(filePath));
String line = reader.readLine();
while (line != null) {
// System.out.println(line);
lines.add( line );
line = reader.readLine();
}
return lines;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
测试输出如下:
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.text.similarity.LevenshteinDistance;
@Slf4j
-public class DemoClass1 {
+public class DemoClass2 {
private static final LevenshteinDistance LEVENSHTEIN_DISTANCE = LevenshteinDistance.getDefaultInstance();
+ private static final LevenshteinDistance LEVENSHTEIN_DISTANCE1 = LevenshteinDistance.getDefaultInstance();
+ private static final LevenshteinDistance LEVENSHTEIN_DISTANCE2 = LevenshteinDistance.getDefaultInstance();
public static String null2emptyWithTrim( String str ){
if( str == null ){
str = "";
}
str = str.trim();
return str;
}
public static String requiredStringParamCheck(String param, String paramRemark) {
param = null2emptyWithTrim( param );
if( param.length() == 0 ){
String msg = "操作失败,请求参数 \"" + paramRemark + "\" 为空";
- log.error( msg );
throw new BusinessLogicException( msg );
}
return param;
}
public static double calculateSimilarity( String str1,String str2 ){
+ try {
int distance = LEVENSHTEIN_DISTANCE.apply(str1, str2);
double similarity = 1 - (double) distance / Math.max(str1.length(), str2.length());
System.out.println("相似度:" + similarity);
return similarity;
+ }catch ( Exception e ){
+ e.printStackTrace();
+ return 0d;
}
}
其实稍微使用一点现实工作中复杂点的两个版本的代码文件进行diff会发现达不到预期,虽然逻辑上对,但是不是人眼直观看到的,即从 旧版本到新版本的笔变化的步骤,新增一行(+)、删除一行(-)、修改一行(-、+)不是花费最小的,我们应该找出一条最小的编辑步骤,所以还是需要使用动态规划,和求2个字符串的最小编辑距离很类似,只不过这里换成了2个集合,集合中的每个元素是一行文本,...
MyDiffTest_v2.java:
import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;
public class MyDiffTest_v2 {
private static final List<String> lines_v1 = loadTxtFile2List("\\xxx\\DemoClass1.txt" );
private static final List<String> lines_v2 = loadTxtFile2List("\\xxx\\DemoClass2.txt");
public static void main(String[] args) {
// 一行代表一个元素
// 删除一个元素,新增一个元素,修改一个元素
// 使用动态规划求出 lines_old 转换为 lines_new 的最小步骤
MinimumTransferWayVO way = calculateMinimumTransferWay(lines_v1, lines_v2);
printOpeartions( way.getOperations() );
}
/**
* ReadOperationVO 不共享操作次数
* @param operations
*/
private static int getOpeartionCount(List<OperationVO> operations) {
int count = 0;
for( OperationVO operation:operations ){
if( operation instanceof ReadOperationVO ){
// ReadOperationVO 不贡献操作次数
}else {
count++;
}
}
return count;
}
private static void printOpeartions(List<OperationVO> operations) {
for( OperationVO operation:operations ){
if( operation instanceof ReadOperationVO ){
ReadOperationVO readOperation = (ReadOperationVO) operation;
System.out.println( " " + lines_v1.get( readOperation.getLineNum_v1() ) );
}else if( operation instanceof InsertOperationVO ){
InsertOperationVO insertOperation = (InsertOperationVO) operation;
System.out.println( "+ " + lines_v2.get( insertOperation.getLineNum_v2() ) );
}else if( operation instanceof DeleteOperationVO ){
DeleteOperationVO deleteOperation = (DeleteOperationVO) operation;
System.out.println( "- " + lines_v1.get( deleteOperation.getLineNum_v1() ) );
}else if( operation instanceof EditOperationVO ){
EditOperationVO editOperation = (EditOperationVO) operation;
System.out.println( "- " + lines_v1.get( editOperation.getLineNum_v1() ) );
System.out.println( "+ " + lines_v2.get( editOperation.getLineNum_v2() ) );
}
}
}
private static MinimumTransferWayVO calculateMinimumTransferWay( List<String> lines_v1,List<String> lines_v2 ){
// dp[i][j] 表示的是将 lines_v1 的前i个元素变换为 lines_new 中的前j个元素需要使用的最优( 即需要转换步骤最少 )的转换方式
MinimumTransferWayVO[][] dp = new MinimumTransferWayVO[lines_v1.size()][lines_v2.size()];
int size_v1 = lines_v1.size();
int size_v2 = lines_v2.size();
for (int lineNum_v1 = 0; lineNum_v1 < size_v1; lineNum_v1++) {
String line_v1 = lines_v1.get( lineNum_v1 );
String line_v1_trim = line_v1.trim();
for (int lineNum_v2 = 0; lineNum_v2 < size_v2; lineNum_v2++) {
String line_v2 = lines_v2.get( lineNum_v2 );
String line_v2_trim = line_v2.trim();
MinimumTransferWayVO minimumTransferWay = new MinimumTransferWayVO();
if( lineNum_v1 == 0 ){
if( lineNum_v2 == 0 ){
// v1 和 v2 都只有 1 行文本,此时最简单,只需要检测这 1 行文本是否相同?
/*
v1 v2
1111111111111111111111 ==》 1111111111111111111111
*/
List<OperationVO> steps = new ArrayList<>();
if( line_v1_trim.equals( line_v2_trim ) ){
// 相同,不需要任何变换
}else {
// 不相同,需要 1 步修改操作
EditOperationVO editOperation = new EditOperationVO();
editOperation.setLineNum_v1( lineNum_v1 );
editOperation.setLineNum_v2( lineNum_v2 );
steps.add( editOperation );
}
minimumTransferWay.setOperations( steps );
}else {
// v1只有1行文本,v2有多行文本,此时的变换也比较简单,
// 检测 line_v1 在 lines_v2 中是否存在
List<OperationVO> operations = new ArrayList<>();
if( contains( lines_v2,line_v1_trim,0,lineNum_v2 ) ){
// line_v1 在 lines_v2 中存在
/*
v1 v2
1111111111111111111111 ==》 2222222222222222222222
1111111111111111111111
444444444444444444444444
555555555555555555
如下一种情形,v1转换为v2需要在 "1111111111111111111111" 上面插入一行 "2222222222222222222222",
然后在 "1111111111111111111111" 下面插入 "444444444444444444444444"、"555555555555555555" 行
*/
// firstEqualIndex 介于 0 和 lineNum_v2 之间
int firstEqualIndex = getFirstEqualIndex(lines_v2, line_v1_trim, 0, lineNum_v2);
for (int lineNum = 0; lineNum <=lineNum_v2 ; lineNum++) {
if( lineNum == firstEqualIndex ){
ReadOperationVO readOperation = new ReadOperationVO();
readOperation.setLineNum_v1( lineNum_v1 );
readOperation.setLineNum_v1( lineNum );
operations.add( readOperation );
}else {
// 插入行
InsertOperationVO insertOperation = new InsertOperationVO();
insertOperation.setLineNum_v2( lineNum );
operations.add( insertOperation );
}
}
}else {
// line_v1 在 lines_v2 中不存在
/*
v1 v2
1111111111111111111111 ==》 2222222222222222222222
3333333333333333333
444444444444444444444444
555555555555555555
此时,v1 转换成 v2,需要先删除 "1111111111111111111111" 行,
再插入 "2222222222222222222222"、
"3333333333333333333"、
"444444444444444444444444"、
"555555555555555555" 行
*/
DeleteOperationVO deleteOperation = new DeleteOperationVO();
deleteOperation.setLineNum_v1( lineNum_v1 );
operations.add( deleteOperation );
for (int lineNum = 0; lineNum <= lineNum_v2; lineNum++) {
InsertOperationVO insertOperation = new InsertOperationVO();
insertOperation.setLineNum_v2( lineNum );
operations.add( insertOperation );
}
}
minimumTransferWay.setOperations( operations );
}
}else {
if( lineNum_v2 == 0 ){
// v1有多行文本,v1只有1行文本,
List<OperationVO> operations = new ArrayList<>();
// 检测 line_v2 是否在 lines_v1 中存在
if( contains(lines_v1, line_v2_trim, 0, lineNum_v1) ){
// line_v2 在 lines_v1 中存在
/*
v1 v2
11111111111111 333333333333
333333333333
444444444444444
5555555555
*/
// 此时,v1转换为 v2需要删除 "333333333333" 上面的 "11111111111111"行,删除 "333333333333" 下面的 "444444444444444"、"5555555555" 行
int firstEqualIndex = getFirstEqualIndex(lines_v1, line_v2_trim, 0, lineNum_v1);
for (int lineNum = 0; lineNum <=lineNum_v1 ; lineNum++) {
if( lineNum == firstEqualIndex){
ReadOperationVO readOperation= new ReadOperationVO();
readOperation.setLineNum_v1( lineNum );
readOperation.setLineNum_v2( lineNum_v2 );
operations.add( readOperation );
}else {
// 删除行
DeleteOperationVO deleteOperation = new DeleteOperationVO();
deleteOperation.setLineNum_v1( lineNum );
operations.add( deleteOperation );
}
}
}else {
// line_v2 在 lines_v1 中不存在
/*
v1 v2
11111111111111 22222222222222
333333333333
444444444444444
5555555555
*/
// 此时,v1 转换为 v2 需要删除 "11111111111111"、 "333333333333"、 "444444444444444"、 "5555555555",再新增 "22222222222222"
for (int lineNum = 0; lineNum <=lineNum_v1 ; lineNum++) {
// 删除行
DeleteOperationVO deleteOperation = new DeleteOperationVO();
deleteOperation.setLineNum_v1( lineNum );
operations.add( deleteOperation );
}
// 插入行
InsertOperationVO insertOperation = new InsertOperationVO();
insertOperation.setLineNum_v2( lineNum_v2 );
operations.add( insertOperation );
}
minimumTransferWay.setOperations( operations );
}else {
List<OperationVO> operations = new ArrayList<>();
// v1 和 v2 都有多行文本,最复杂。
// 检测 v1 的 v2 的当前行是否相同
if( line_v1_trim.equals( line_v2_trim ) ){
// line_v1 和 line_v2 相同
/*
v1 v2
11111111 1111111
22222222 33333
... ...
44444444 555555
66666666 66666666
*/
// 如上所示,v1的当前行 "66666666" 和 v2 的当前行 "66666666" 相同,只需要看 v1 点位前面部分 转换wield v2的前面部分的最小转换方式就行了,
MinimumTransferWayVO minimumTransferWay_prev = dp[lineNum_v1 - 1][lineNum_v2 - 1];
// todo 是否需要深拷贝
operations.addAll( minimumTransferWay_prev.getOperations() );
// 追加一个读操作,方便输出转换过程
ReadOperationVO readOperation = new ReadOperationVO();
readOperation.setLineNum_v1( lineNum_v1 );
readOperation.setLineNum_v2( lineNum_v2 );
operations.add( readOperation );
}else {
// line_v1 和 line_v2 不相同
/*
v1 v2
11111111 1111111
22222222 33333
... ...
44444444 555555
66666666 7777777
*/
// 如上所示,v1 的当前行 "66666666" 和 v2 的当前行 "7777777" 不相同,则有3个候选转换方式,需要从中选择最小的转换方式
// 1. v1 的前面部分转换为 v2的前面部分,v1的 当前行 "66666666" 修改为 v2 的当前行 "7777777"
MinimumTransferWayVO minimumTransferWay_prev1 = dp[lineNum_v1 - 1][lineNum_v2 - 1];
int opeartionCount_1 = getOpeartionCount(minimumTransferWay_prev1.getOperations());
// 2. v1 的前面部分 转换为 v2 的前面部分 + v2 的当前行部分,删除 v1 的当前行 "66666666"
MinimumTransferWayVO minimumTransferWay_prev2 = dp[lineNum_v1 - 1][lineNum_v2];
int opeartionCount_2 = getOpeartionCount(minimumTransferWay_prev2.getOperations());
// 3. v1 的前面部分 + v1 的当前行部分 转换为 v2 的前面部分,v2 新增当前行 "7777777"
MinimumTransferWayVO minimumTransferWay_prev3 = dp[lineNum_v1 ][lineNum_v2 - 1];
int opeartionCount_3 = getOpeartionCount(minimumTransferWay_prev3.getOperations());
boolean isOne = true;
boolean isTwo = false;
boolean isThree = false;
MinimumTransferWayVO minimumTransferWay_prev = minimumTransferWay_prev1;
int opeartionCount = opeartionCount_1;
if( opeartionCount_2 < opeartionCount ){
minimumTransferWay_prev = minimumTransferWay_prev2;
opeartionCount = opeartionCount_2;
isOne = false;
isTwo = true;
isThree = false;
}
if( opeartionCount_3 < opeartionCount ){
minimumTransferWay_prev = minimumTransferWay_prev3;
opeartionCount = opeartionCount_3;
isOne = false;
isTwo = false;
isThree = true;
}
operations.addAll( minimumTransferWay_prev.getOperations() );
if( isOne ){
// v1 的 当前行 "66666666" 修改为 v2 的当前行 "7777777"
EditOperationVO editOperation = new EditOperationVO();
editOperation.setLineNum_v1( lineNum_v1 );
editOperation.setLineNum_v2( lineNum_v2 );
operations.add( editOperation );
}else if( isTwo ){
// 删除 v1 的当前行 "66666666"
DeleteOperationVO deleteOperation = new DeleteOperationVO();
deleteOperation.setLineNum_v1( lineNum_v1 );
operations.add( deleteOperation );
}else if( isThree ){
// v2 新增当前行 "7777777"
InsertOperationVO insertOperation = new InsertOperationVO();
insertOperation.setLineNum_v2( lineNum_v2 );
operations.add( insertOperation );
}
}
minimumTransferWay.setOperations( operations );
}
}
dp[ lineNum_v1 ][ lineNum_v2 ] = minimumTransferWay;
}
}
return dp[ lines_v1.size() -1 ][ lines_v2.size() -1 ];
}
/**
* @param list
* @param targetElement
* @param beginIndex
* @param endIndex
* @return
*/
private static boolean contains(List<String> list, String targetElement, int beginIndex, int endIndex) {
for (int i = beginIndex; i <=endIndex ; i++) {
if( targetElement.equals( list.get( i ).trim() ) ){
return true;
}
}
return false;
}
private static int getFirstEqualIndex(List<String> list, String targetElement, int beginIndex, int endIndex) {
for (int i = beginIndex; i <=endIndex ; i++) {
if( targetElement.equals( list.get( i ).trim() ) ){
return i;
}
}
return -1;
}
private static List<String> loadTxtFile2List(String filePath) {
BufferedReader reader = null;
List<String> lines = new ArrayList<>();
try {
reader = new BufferedReader(new FileReader(filePath));
String line = reader.readLine();
while (line != null) {
// System.out.println(line);
lines.add( line );
line = reader.readLine();
}
return lines;
} catch (Exception e) {
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
MinimumTransferWayVO.java:
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.List;
@Getter
@Setter
public class MinimumTransferWayVO implements Serializable {
private List<OperationVO> operations;
}
OperationVO.java:
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class OperationVO implements Serializable {
}
ReadOperationVO.java:
import lombok.Getter;
import lombok.Setter;
/**
* ReadOperationVO 不贡献操作步数,只是为了方便输出v1到v2的整个变换过程
**/
@Getter
@Setter
public class ReadOperationVO extends OperationVO {
// 此时 lines_v1[ lineNum_v1 ] 和 lines_v2[ lineNum_v2 ] 相同
private int lineNum_v1;
private int lineNum_v2;
}
InsertOperationVO.java:
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class InsertOperationVO extends OperationVO {
private int lineNum_v2;
}
DeleteOperationVO.java:
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
@Getter
@Setter
public class DeleteOperationVO extends OperationVO {
private int lineNum_v1;
}
EditOperationVO.java:
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class EditOperationVO extends OperationVO {
private int lineNum_v1;
private int lineNum_v2;
}
DemoClass1.txt 和 DemoClass2.txt 的文本内容如下所示:
测试输出如下所示:文章来源:https://www.toymoban.com/news/detail-757242.html
文章来源地址https://www.toymoban.com/news/detail-757242.html
到了这里,关于手动实现 git 的 git diff 功能的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!