diff --git a/projects/GKCore/GKCore.csproj b/projects/GKCore/GKCore.csproj index aad5d8f3f..034fa9d5c 100644 --- a/projects/GKCore/GKCore.csproj +++ b/projects/GKCore/GKCore.csproj @@ -376,6 +376,7 @@ + diff --git a/projects/GKCore/GKCore/NetDiff/DiffUtil.cs b/projects/GKCore/GKCore/NetDiff/DiffUtil.cs new file mode 100644 index 000000000..804678743 --- /dev/null +++ b/projects/GKCore/GKCore/NetDiff/DiffUtil.cs @@ -0,0 +1,474 @@ +/* + * NetDiff (Diff4Net), v1.2.0.0 + * This is the C# implementation of the Diff algorithm. + * Copyright © 2017 by skanmera + * https://github.com/skanmera/NetDiff + * License: MIT License (http://opensource.org/licenses/MIT) + */ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GKCore.NetDiff +{ + public enum DiffStatus + { + Equal, + Deleted, + Inserted, + Modified, + } + + + public class DiffResult + { + public T Obj1 { get; private set; } + public T Obj2 { get; private set; } + public DiffStatus Status { get; private set; } + + public DiffResult(T obj1, T obj2, DiffStatus status) + { + Obj1 = obj1; + Obj2 = obj2; + Status = status; + } + + public override string ToString() + { + var obj = Status != DiffStatus.Inserted ? Obj1 : Obj2; + return string.Format("{0} {1}", DiffUtil.GetStatusChar(Status), obj); + } + } + + + public class DiffOption + { + /// + /// Specify IEqualityComparer to be used for comparing equality. + /// + public IEqualityComparer EqualityComparer { get; set; } + + /// + /// Specify the maximum number of nodes that can exist at once at the edit graph. + /// The lower the number, the better the performance, but the redundant differences increase. + /// The default is 1000. + /// + public int Limit { get; set; } + } + + + public class DiffUtil + { + public static IEnumerable> Diff(IEnumerable seq1, IEnumerable seq2) + { + return Diff(seq1, seq2, new DiffOption()); + } + + public static IEnumerable> Diff(IEnumerable seq1, IEnumerable seq2, DiffOption option) + { + if (seq1 == null || seq2 == null || (!seq1.Any() && !seq2.Any())) + return Enumerable.Empty>(); + + var editGrap = new EditGraph(seq1, seq2); + var waypoints = editGrap.CalculatePath(option); + + return MakeResults(waypoints, seq1, seq2); + } + + public static IEnumerable CreateSrc(IEnumerable> diffResults) + { + return diffResults.Where(r => r.Status != DiffStatus.Inserted).Select(r => r.Obj1); + } + + public static IEnumerable CreateDst(IEnumerable> diffResults) + { + return diffResults.Where(r => r.Status != DiffStatus.Deleted).Select(r => r.Obj2); + } + + public static IEnumerable> OptimizeCaseDeletedFirst(IEnumerable> diffResults) + { + return Optimize(diffResults, true); + } + + public static IEnumerable> OptimizeCaseInsertedFirst(IEnumerable> diffResults) + { + return Optimize(diffResults, false); + } + + private static IEnumerable> Optimize(IEnumerable> diffResults, bool deleteFirst = true) + { + var currentStatus = deleteFirst ? DiffStatus.Deleted : DiffStatus.Inserted; + var nextStatus = deleteFirst ? DiffStatus.Inserted : DiffStatus.Deleted; + + var queue = new Queue>(diffResults); + while (queue.Any()) + { + var result = queue.Dequeue(); + if (result.Status == currentStatus) + { + if (queue.Any() && queue.Peek().Status == nextStatus) + { + var obj1 = deleteFirst ? result.Obj1 : queue.Dequeue().Obj1; + var obj2 = deleteFirst ? queue.Dequeue().Obj2 : result.Obj2; + yield return new DiffResult(obj1, obj2, DiffStatus.Modified); + } + else + yield return result; + + continue; + } + + yield return result; + } + } + + private static IEnumerable> MakeResults(IEnumerable waypoints, IEnumerable seq1, IEnumerable seq2) + { + var array1 = seq1.ToArray(); + var array2 = seq2.ToArray(); + + foreach (var pair in MakePairsWithNext(waypoints)) + { + var status = GetStatus(pair.Item1, pair.Item2); + T obj1 = default(T); + T obj2 = default(T); + switch (status) + { + case DiffStatus.Equal: + obj1 = array1[pair.Item2.X - 1]; + obj2 = array2[pair.Item2.Y - 1]; + break; + case DiffStatus.Inserted: + obj2 = array2[pair.Item2.Y - 1]; + break; + case DiffStatus.Deleted: + obj1 = array1[pair.Item2.X - 1]; + break; + } + + yield return new DiffResult(obj1, obj2, status); + } + } + + private static IEnumerable> MakePairsWithNext(IEnumerable source) + { + using (var enumerator = source.GetEnumerator()) + { + if (enumerator.MoveNext()) + { + var previous = enumerator.Current; + while (enumerator.MoveNext()) + { + var current = enumerator.Current; + + yield return new Tuple(previous, current); + + previous = current; + } + } + } + } + + private static DiffStatus GetStatus(Point current, Point prev) + { + if (current.X != prev.X && current.Y != prev.Y) + return DiffStatus.Equal; + else if (current.X != prev.X) + return DiffStatus.Deleted; + else if (current.Y != prev.Y) + return DiffStatus.Inserted; + else + throw new Exception(); + } + + internal static char GetStatusChar(DiffStatus status) + { + switch (status) + { + case DiffStatus.Equal: return '='; + case DiffStatus.Deleted: return '-'; + case DiffStatus.Inserted: return '+'; + case DiffStatus.Modified: return 'M'; + } + + throw new System.Exception(); + } + + #region EditGraph + + internal enum Direction + { + Right, + Bottom, + Diagonal, + } + + internal struct Point : IEquatable + { + public int X { get; private set; } + public int Y { get; private set; } + + public Point(int x, int y) + { + X = x; + Y = y; + } + + public override bool Equals(object obj) + { + if (!(obj is Point)) + return false; + + return Equals((Point)obj); + } + + public override int GetHashCode() + { + var hash = 17; + hash = hash * 23 + X.GetHashCode(); + hash = hash * 23 + Y.GetHashCode(); + + return hash; + } + + public bool Equals(Point other) + { + return X == other.X && Y == other.Y; + } + + public override string ToString() + { + return string.Format("X:{0} Y:{1}", X, Y); + } + } + + internal class Node + { + public Point Point { get; set; } + public Node Parent { get; set; } + + public Node(Point point) + { + Point = point; + } + + public override string ToString() + { + return string.Format("X:{0} Y:{1}", Point.X, Point.Y); + } + } + + internal class EditGraph + { + private T[] seq1; + private T[] seq2; + private DiffOption option; + private List heads; + private Point endpoint; + private int[] farthestPoints; + private int offset; + private bool isEnd; + + public EditGraph( + IEnumerable seq1, IEnumerable seq2) + { + this.seq1 = seq1.ToArray(); + this.seq2 = seq2.ToArray(); + endpoint = new Point(this.seq1.Length, this.seq2.Length); + offset = this.seq2.Length; + } + + public List CalculatePath(DiffOption option) + { + if (!seq1.Any()) + return Enumerable.Range(0, seq2.Length + 1).Select(i => new Point(0, i)).ToList(); + + if (!seq2.Any()) + return Enumerable.Range(0, seq1.Length + 1).Select(i => new Point(i, 0)).ToList(); + + this.option = option; + + BeginCalculatePath(); + + while (Next()) { } + + return EndCalculatePath(); + } + + private void Initialize() + { + farthestPoints = new int[seq1.Length + seq2.Length + 1]; + heads = new List(); + } + + private void BeginCalculatePath() + { + Initialize(); + + heads.Add(new Node(new Point(0, 0))); + + Snake(); + } + + private List EndCalculatePath() + { + var wayponit = new List(); + + var current = heads.Where(h => h.Point.Equals(endpoint)).FirstOrDefault(); + while (current != null) + { + wayponit.Add(current.Point); + + current = current.Parent; + } + + wayponit.Reverse(); + + return wayponit; + } + + private bool Next() + { + if (isEnd) + return false; + + UpdateHeads(); + + return true; + } + + private void UpdateHeads() + { + if (option.Limit > 0 && heads.Count > option.Limit) + { + var tmp = heads.First(); + heads.Clear(); + + heads.Add(tmp); + } + + var updated = new List(); + + foreach (var head in heads) + { + Node rightHead; + if (TryCreateHead(head, Direction.Right, out rightHead)) + { + updated.Add(rightHead); + } + + Node bottomHead; + if (TryCreateHead(head, Direction.Bottom, out bottomHead)) + { + updated.Add(bottomHead); + } + } + + heads = updated; + + Snake(); + } + + private void Snake() + { + var tmp = new List(); + foreach (var h in heads) + { + var newHead = Snake(h); + + if (newHead != null) + tmp.Add(newHead); + else + tmp.Add(h); + } + + heads = tmp; + } + + private Node Snake(Node head) + { + Node newHead = null; + while (true) + { + Node tmp; + if (TryCreateHead(newHead ?? head, Direction.Diagonal, out tmp)) + newHead = tmp; + else + break; + } + + return newHead; + } + + private bool TryCreateHead(Node head, Direction direction, out Node newHead) + { + newHead = null; + var newPoint = GetPoint(head.Point, direction); + + if (!CanCreateHead(head.Point, direction, newPoint)) + return false; + + newHead = new Node(newPoint); + newHead.Parent = head; + + isEnd |= newHead.Point.Equals(endpoint); + + return true; + } + + private bool CanCreateHead(Point currentPoint, Direction direction, Point nextPoint) + { + if (!InRange(nextPoint)) + return false; + + if (direction == Direction.Diagonal) + { + var equal = option.EqualityComparer != null + ? option.EqualityComparer.Equals(seq1[nextPoint.X - 1], (seq2[nextPoint.Y - 1])) + : seq1[nextPoint.X - 1].Equals(seq2[nextPoint.Y - 1]); + + if (!equal) + return false; + } + + return UpdateFarthestPoint(nextPoint); + } + + private Point GetPoint(Point currentPoint, Direction direction) + { + switch (direction) + { + case Direction.Right: + return new Point(currentPoint.X + 1, currentPoint.Y); + case Direction.Bottom: + return new Point(currentPoint.X, currentPoint.Y + 1); + case Direction.Diagonal: + return new Point(currentPoint.X + 1, currentPoint.Y + 1); + } + + throw new ArgumentException(); + } + + private bool InRange(Point point) + { + return point.X >= 0 && point.Y >= 0 && point.X <= endpoint.X && point.Y <= endpoint.Y; + } + + private bool UpdateFarthestPoint(Point point) + { + var k = point.X - point.Y; + var y = farthestPoints[k + offset]; + + if (point.Y <= y) + return false; + + farthestPoints[k + offset] = point.Y; + + return true; + } + } + + #endregion + } +} diff --git a/projects/GKTests/GKCore/NetDiffTests.cs b/projects/GKTests/GKCore/NetDiffTests.cs new file mode 100644 index 000000000..ac05af536 --- /dev/null +++ b/projects/GKTests/GKCore/NetDiffTests.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; + +namespace GKCore.NetDiff +{ + [TestFixture] + public class NetDiffTests + { + [Test] + public void StdCase_General() + { + var str1 = "string"; + var str2 = "strength"; + + var results = DiffUtil.Diff(str1, str2).ToList(); + + Assert.AreEqual(9, results.Count()); + + Assert.AreEqual("= s", results[0].ToString()); + Assert.AreEqual("= t", results[1].ToString()); + Assert.AreEqual("= r", results[2].ToString()); + Assert.AreEqual("- i", results[3].ToString()); + Assert.AreEqual("+ e", results[4].ToString()); + Assert.AreEqual("= n", results[5].ToString()); + Assert.AreEqual("= g", results[6].ToString()); + Assert.AreEqual("+ t", results[7].ToString()); + Assert.AreEqual("+ h", results[8].ToString()); + } + + [Test] + public void StdCase_Equal() + { + var str1 = "abcde"; + var str2 = "abcde"; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(str1.Count(), results.Count()); + Assert.IsTrue(results.All(r => r.Status == DiffStatus.Equal)); + } + + /* + a b a b a b + - + - + - + + */ + [Test] + public void StdCase_DifferentAll() + { + var str1 = "aaa"; + var str2 = "bbb"; + + var results = DiffUtil.Diff(str1, str2).ToList(); + + Assert.AreEqual("+ b", results.ElementAt(0).ToString()); + Assert.AreEqual("- a", results.ElementAt(1).ToString()); + Assert.AreEqual("- a", results.ElementAt(2).ToString()); + Assert.AreEqual("- a", results.ElementAt(3).ToString()); + Assert.AreEqual("+ b", results.ElementAt(4).ToString()); + Assert.AreEqual("+ b", results.ElementAt(5).ToString()); + } + + /* + a b c d + = = = + + */ + [Test] + public void StdCase_Appended() + { + var str1 = "abc"; + var str2 = "abcd"; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(0).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(1).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(3).Status); + } + + /* + a b c d + + = = = + */ + [Test] + public void StdCase_Prepended() + { + var str1 = "bcd"; + var str2 = "abcd"; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(0).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(1).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(3).Status); + } + + [Test] + public void StdCase_CaseMultiSameScore() + { + var str1 = "cdhijkz"; + var str2 = "ldxhnokz"; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(0).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(1).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(3).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(4).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(5).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(6).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(7).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(8).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(9).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(10).Status); + } + + [Test] + public void StdCase_CaseRepeat() + { + string str1 = "abbbc"; + string str2 = "adbbc"; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(0).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(1).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(3).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(4).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(5).Status); + } + + + [Test] + public void StdCase_SpecifiedComparer() + { + var str1 = "abc"; + var str2 = "dBf"; + + var option = new DiffOption(); + option.EqualityComparer = new CaseInsensitiveComparer(); + + var results = DiffUtil.Diff(str1, str2, option).ToList(); + + Assert.AreEqual("+ d", results[0].ToString()); + Assert.AreEqual("- a", results[1].ToString()); + Assert.AreEqual("= b", results[2].ToString()); + Assert.AreEqual("- c", results[3].ToString()); + Assert.AreEqual("+ f", results[4].ToString()); + + Assert.AreEqual("abc", new String(DiffUtil.CreateSrc(results).ToArray())); + Assert.AreEqual("dBf", new String(DiffUtil.CreateDst(results).ToArray())); + } + + [Test] + public void StdCase_CaseReplace() + { + string str1 = "abbbc"; + string str2 = "adbbc"; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(0).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(1).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(2).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(3).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(4).Status); + Assert.AreEqual(DiffStatus.Equal, results.ElementAt(5).Status); + } + + [Test] + public void BadCase_Seq1Empty() + { + string str1 = ""; + string str2 = "abcde"; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(0).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(1).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(2).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(3).Status); + Assert.AreEqual(DiffStatus.Inserted, results.ElementAt(4).Status); + } + + [Test] + public void BadCase_Seq2Empty() + { + string str1 = "abced"; + string str2 = ""; + + var results = DiffUtil.Diff(str1, str2); + + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(0).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(1).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(2).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(3).Status); + Assert.AreEqual(DiffStatus.Deleted, results.ElementAt(4).Status); + } + + [Test] + public void BadCase_Empty() + { + var str1 = string.Empty; + var str2 = string.Empty; + + var results = DiffUtil.Diff(str1, str2); + + Assert.IsTrue(!results.Any()); + } + + [Test] + public void BadCase_Null() + { + string str1 = null; + var str2 = string.Empty; + + var results = DiffUtil.Diff(str1, str2); + + Assert.IsTrue(!results.Any()); + } + + internal class CaseInsensitiveComparer : IEqualityComparer + { + public bool Equals(char x, char y) + { + return x.ToString().ToLower().Equals(y.ToString().ToLower()); + } + + public int GetHashCode(char obj) + { + return obj.ToString().ToLower().GetHashCode(); + } + } + } +} diff --git a/projects/GKTests/GKTests.csproj b/projects/GKTests/GKTests.csproj index d28bc6717..a8a35ce9d 100644 --- a/projects/GKTests/GKTests.csproj +++ b/projects/GKTests/GKTests.csproj @@ -112,6 +112,7 @@ +