使用 C# 產生資料修改差異報告
4 |
最近寫到內容維護 UI,想留下資料修改軌跡。除了保留修改前修改後的完整內容,我還想出一份簡單的差異報告,指出哪幾行有更改,增加了哪幾行、哪幾行被刪除... 等,方便一眼看出變更重點;甚至不必保留舊版,用最新版加差異報告便能一路反推完整修改歷程,大量節省儲存空間。思考這個需求時,我想到 Git diff,它的比對報告就很接近我要的結果,但我的需求又更簡單一點,只要像這樣就好:
上圖最下方為 JSON 修改前後的增刪異動報告,第一欄為行號,如為舊版顯示於左,新版顯示在右方,接著是異動符號,< 表舊版內容,> 指新版內容,+ 表新增,- 表刪除,最後為文字內容。當有連續數行異動時,希望先一次顯示完舊版的多行資料,再顯示新版。(遇到 XML 元素拆成多行,這種排版比單方穿插可讀性高)
為實現這個功能,我找到一個老牌程式庫 - DiffPlex,用 NuGet 即可安裝:
DiffPlex 的程式說明及範例有點簡略,不過有原始碼在手,沒什麼可以難倒我們。Open Source 萬歲!
完整測試程式如下:
using DiffPlex;
using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApp1
{
class Program
{
public class Post
{
public int Id { get; set; }
public string Author { get; set; }
public string Title { get; set; }
public string Tags { get; set; }
public string Content { get; set; }
public string Remark { get; set; }
}
static void Main(string[] args)
{
var post = new Post()
{
Id = 1,
Author = "Jeffrey",
Title = "Hello, World!",
Tags = "Fun,Test",
Content = @"To be or not to be, that is the question.",
Remark = ""
};
var jsonB4 = JsonConvert.SerializeObject(post, Formatting.Indented);
var jo = JsonConvert.DeserializeObject<JObject>(jsonB4);
jo["Id"] = 2;
jo["Author"] = "Darkthread";
jo.Property("Tags").Remove();
jo.Add(new JProperty("Url", "http://blog.darkthread.net"));
var jsonAft = JsonConvert.SerializeObject(jo, Formatting.Indented);
var d = new Differ();
var sbsDiffBuilder = new SideBySideDiffBuilder(new Differ());
var diffResult = sbsDiffBuilder.BuildDiffModel(jsonB4, jsonAft);
var diffLeft = new Dictionary<int, string>();
var diffRight = new Dictionary<int, string>();
var maxLineNo = 0;
foreach (var line in diffResult.OldText.Lines)
{
if (line.Position == null || line.Type == ChangeType.Unchanged) continue;
var symbol = "<";
if (line.Type == ChangeType.Inserted) symbol = "+";
else if (line.Type == ChangeType.Deleted) symbol = "-";
diffLeft.Add(line.Position.Value, $"{symbol} {line.Text}");
maxLineNo = Math.Max(line.Position.Value, maxLineNo);
}
foreach (var line in diffResult.NewText.Lines)
{
if (line.Position == null || line.Type == ChangeType.Unchanged) continue;
var symbol = ">";
if (line.Type == ChangeType.Inserted) symbol = "+";
else if (line.Type == ChangeType.Deleted) symbol = "-";
diffRight.Add(line.Position.Value, $"{symbol} {line.Text}");
maxLineNo = Math.Max(line.Position.Value, maxLineNo);
}
var sb = new StringBuilder();
List<string> buffLeft = new List<string>();
List<string> buffRight = new List<string>();
Action flushBuffer = () =>
{
buffLeft.ForEach(o => sb.AppendLine(o));
buffLeft.Clear();
buffRight.ForEach(o => sb.AppendLine(o));
buffRight.Clear();
};
//以最大行數位數決定寬度
var lineNoWidth = maxLineNo.ToString().Length + 1;
var format = $"{{0,{lineNoWidth}}} {{1,{lineNoWidth}}}\t{{2}}";
Func<int, bool, string, string> genLine = (lineNo, isLeft, text) =>
{
string n1 = isLeft ? lineNo.ToString() : " ";
string n2 = isLeft ? " " : lineNo.ToString();
return string.Format(format, n1, n2, text);
};
for (var i = 1; i<=maxLineNo; i++)
{
if (diffLeft.ContainsKey(i))
buffLeft.Add(genLine(i, true, diffLeft[i]));
if (diffRight.ContainsKey(i))
buffRight.Add(genLine(i, false, diffRight[i]));
if (diffLeft.ContainsKey(i) && diffRight.ContainsKey(i))
continue; //連續行數兩邊都有內容,暫不送出
//否則送出內容
flushBuffer();
}
flushBuffer();
Console.WriteLine($"Orig\n====\n{jsonB4}");
Console.WriteLine($"\nNew \n====\n{jsonAft}");
Console.WriteLine("\nDiff\n====");
Console.WriteLine(sb.ToString());
//Console.ReadLine();
}
}
}
實測結果:
Orig
====
{
"Id": 1,
"Author": "Jeffrey",
"Title": "Hello, World!",
"Tags": "Fun,Test",
"Content": "To be or not to be, that is the question.",
"Remark": ""
}
New
====
{
"Id": 2,
"Author": "Darkthread",
"Title": "Hello, World!",
"Content": "To be or not to be, that is the question.",
"Remark": "",
"Url": "http://blog.darkthread.net"
}
Diff
====
2 < "Id": 1,
3 < "Author": "Jeffrey",
2 > "Id": 2,
3 > "Author": "Darkthread",
5 - "Tags": "Fun,Test",
6 > "Remark": "",
7 < "Remark": ""
7 + "Url": "http://blog.darkthread.net"
最後試一下 XML,以下範例展示是連續多行修改時,先印舊版 3-5 行再印新版 3-5 行的效果,可讀性較佳。
工具箱再添實用工具一件。
The example showing how to use DiffPlex to generate a 'git diff'-like report with C#.
Comments
# by Alan
黑大,小弟最近也有一樣的需求,感謝您無私的分享。
# by abc0922001
這不就是 git format-patch 輸出的結果嗎
# by ct
不曉得您需要的用途,不過使用git format-patch有類似的效果: git format-patch --no-prefix --no-binary <sha1>...<sha1> -o "d:\temp" ps. <sha1>為兩個commit的點,建議為相鄰的2個commit。
# by Jeffrey
to abc0922001, ct, 謝謝你們的回饋。 這個功能的點子來自git沒錯,一開始也有考慮直接引用 git.exe,後來基於緊密整合(外部 EXE vs .NET 程式庫)與當成練功題材的考量,決定自己寫。ct 補充的提示很棒,已筆記留待日後派上用場。