public void TestRemove() { List <int> list = new List <int> { 1, 2, 3 }; Window <int> window = new Window <int>(list, 1); ReadOnlyWindow <int> readOnlyWindow = new ReadOnlyWindow <int>(list, 1); int[] l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(2, window.Count); Assert.AreEqual(3, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(2, readOnlyWindow.Count); Assert.AreEqual(3, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Remove through window window.Remove(2); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(1, window.Count); Assert.AreEqual(2, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(1, readOnlyWindow.Count); Assert.AreEqual(2, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Remove from underlying list (note that the offset doesn't change, the window is effectively moved). list.Remove(1); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(0, window.Count); Assert.AreEqual(1, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(0, readOnlyWindow.Count); Assert.AreEqual(1, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Remove all items! list.Clear(); Assert.AreEqual(0, window.Count); Assert.AreEqual(0, window.Length); Assert.AreEqual(1, window.Offset); // Note the offset is beyond the end of the underlying data Assert.AreEqual(0, readOnlyWindow.Count); Assert.AreEqual(0, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Note the offset is beyond the end of the underlying data }
public void TestLengthImplicitZeroOffsetOne() { List<int> list = new List<int> { 1 }; Window<int> window = new Window<int>(list, 1); ReadOnlyWindow<int> readOnlyWindow = new ReadOnlyWindow<int>(list, 1); Assert.AreEqual(0, window.Count); Assert.AreEqual(1, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(0, readOnlyWindow.Count); Assert.AreEqual(1, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); }
public void TestLengthZero() { List <int> list = new List <int>(); Window <int> window = list; ReadOnlyWindow <int> readOnlyWindow = list; Assert.AreEqual(0, window.Count); Assert.AreEqual(0, window.Length); Assert.AreEqual(0, window.Offset); Assert.AreEqual(0, readOnlyWindow.Count); Assert.AreEqual(0, readOnlyWindow.Length); Assert.AreEqual(0, readOnlyWindow.Offset); }
/// <summary> /// Initializes a new instance of the <see cref="Chunk{T}" /> class. /// </summary> /// <param name="areEqual">if set to <see langword="true" /> <paramref name="a"/> and <paramref name="b"/> are considered equal.</param> /// <param name="a">The "A" list.</param> /// <param name="b">The "B" list.</param> /// <exception cref="System.ArgumentNullException"></exception> /// <exception cref="ArgumentNullException"></exception> internal Chunk( bool areEqual, [CanBeNull] ReadOnlyWindow <T> a, [CanBeNull] ReadOnlyWindow <T> b) { if (a == null && b == null) { throw new ArgumentNullException(nameof(a)); } AreEqual = areEqual; A = a; B = b; }
public void TestInsert() { List <int> list = new List <int> { 1, 2, 3 }; Window <int> window = new Window <int>(list, 1); ReadOnlyWindow <int> readOnlyWindow = new ReadOnlyWindow <int>(list, 1); int[] l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(2, window.Count); Assert.AreEqual(3, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(2, readOnlyWindow.Count); Assert.AreEqual(3, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Insert through window window.Insert(1, 4); Assert.AreEqual(4, list[2]); Assert.AreEqual(window[1], list[2]); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(3, window.Count); Assert.AreEqual(4, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(3, readOnlyWindow.Count); Assert.AreEqual(4, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Insert through underlying data list.Insert(3, 5); Assert.AreEqual(5, list[3]); Assert.AreEqual(window[2], list[3]); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(4, window.Count); Assert.AreEqual(5, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(4, readOnlyWindow.Count); Assert.AreEqual(5, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); }
public void TestLengthExplicitZeroOffsetOne() { List <int> list = new List <int> { 1, 2 }; Window <int> window = new Window <int>(list, 1, 0); ReadOnlyWindow <int> readOnlyWindow = new ReadOnlyWindow <int>(list, 1, 0); Assert.AreEqual(0, window.Count); Assert.AreEqual(2, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(0, readOnlyWindow.Count); Assert.AreEqual(2, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); }
public void TestImplicitCastPreservesDataLink() { List <int> list = new List <int> { 1, 2, 3, 4 }; Window <int> window = list; ReadOnlyWindow <int> readOnlyWindow = window; CollectionAssert.AreEqual(list, window); CollectionAssert.AreEqual(list, readOnlyWindow); // Check that modifications are reflected in the windows. window[1] = 5; Assert.AreEqual(5, list[1]); Assert.AreEqual(window[1], list[1]); CollectionAssert.AreEqual(list, window); CollectionAssert.AreEqual(list, readOnlyWindow); }
/// <summary> /// Initializes a new instance of the <see cref="Differences{T}" /> class. /// </summary> /// <param name="a">The "A" list.</param> /// <param name="offsetA">The offset to the start of a window in the first collection.</param> /// <param name="lengthA">The length of the window in the first collection.</param> /// <param name="b">The "B" list.</param> /// <param name="offsetB">The offset to the start of a window in the second collection.</param> /// <param name="lengthB">The length of the window in the second collection.</param> /// <param name="comparer">The comparer to compare items in each collection.</param> /// <exception cref="ArgumentNullException"><paramref name="a" /> is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException"><paramref name="b" /> is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException"><paramref name="comparer" /> is <see langword="null" />.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="offsetA" /> is out of range.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="lengthA" /> is out of range.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="offsetB" /> is out of range.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="lengthB" /> is out of range.</exception> /// <exception cref="Exception">The <paramref name="comparer"/> throws an exception.</exception> internal Differences( [NotNull] IReadOnlyList <T> a, int offsetA, int lengthA, [NotNull] IReadOnlyList <T> b, int offsetB, int lengthB, [NotNull] Func <T, T, bool> comparer) { if (a == null) { throw new ArgumentNullException(nameof(a)); } if (b == null) { throw new ArgumentNullException(nameof(b)); } if (comparer == null) { throw new ArgumentNullException(nameof(comparer)); } A = new ReadOnlyWindow <T>(a, offsetA, lengthA); B = new ReadOnlyWindow <T>(b, offsetB, lengthB); List <Chunk <T> > chunks = new List <Chunk <T> >(); bool[] modificationsA = new bool[A.Count]; bool[] modificationsB = new bool[B.Count]; int max = A.Count + B.Count + 1; int vectorSize = 2 * max + 2; // Vector for the (0,0) to (x,y) search int[] downVector = new int[vectorSize]; // Vector for the (u,v) to (N,M) search int[] upVector = new int[vectorSize]; Stack <int, int, int, int> stack = new Stack <int, int, int, int>(); stack.Push(0, A.Count, 0, B.Count); int lowerA; int upperA; int lowerB; int upperB; while (stack.TryPop(out lowerA, out upperA, out lowerB, out upperB)) { // Skip equal lines at start of chunk while (lowerA < upperA && lowerB < upperB && comparer(A[lowerA], B[lowerB])) { lowerA++; lowerB++; } // Skip equal lines at end of the chunk. while (lowerA < upperA && lowerB < upperB && comparer(A[upperA - 1], B[upperB - 1])) { --upperA; --upperB; } if (lowerA == upperA) { // Everything left in the second collection is not in the first collection. while (lowerB < upperB) { modificationsB[lowerB++] = true; } continue; } if (lowerB == upperB) { // Everything left in the first collection is not in the second collection. while (lowerA < upperA) { modificationsA[lowerA++] = true; } continue; } /* * Find where to cut A & B for shortest middle snake. */ // The k-line to start the forward search int downK = lowerA - lowerB; // The k-line to start the reverse search int upK = upperA - upperB; // Difference in collection sizes at this point. int delta = (upperA - lowerA) - (upperB - lowerB); // Whether the delta is an odd number. bool oddDelta = (delta & 1) != 0; // The vectors in the publication accepts negative indexes. the vectors implemented here are 0-based // and are access using a specific offset: upOffset for upVector and downOffset for downVector int downOffset = max - downK; int upOffset = max - upK; int maxD = ((upperA - lowerA + upperB - lowerB) / 2) + 1; // Initialize vectors downVector[downOffset + downK + 1] = lowerA; upVector[upOffset + upK - 1] = upperA; int d = 0; int cutA = -1; int cutB = -1; do { // Extend the forward path. int downStart = downK - d; int downEnd = downK + d; int upStart = upK - d; int upEnd = upK + d; int k = downStart; do { // Find the only or better starting point int x; int offsetK = downOffset + k; if (k == downStart) { // Step down x = downVector[offsetK + 1]; } else { // Step right x = downVector[offsetK - 1] + 1; if ((k < downEnd) && (downVector[offsetK + 1] >= x)) { // Step down x = downVector[offsetK + 1]; } } int y = x - k; // Find the end of the furthest reaching forward D-path in diagonal k. while ((x < upperA) && (y < upperB) && (comparer(A[x], B[y]))) { x++; y++; } downVector[offsetK] = x; // Check for overlap if (oddDelta && (upStart < k) && (k < upEnd) && upVector[upOffset + k] <= downVector[offsetK]) { cutA = downVector[offsetK]; cutB = downVector[offsetK] - k; break; } k += 2; } while (k <= downEnd); // Once we find the cut points we're done. if (cutA > -1) { break; } // Extend the reverse path. k = upStart; do { // Find the only or better starting point int x; int offsetK = upOffset + k; if (k == upEnd) { // Step up x = upVector[offsetK - 1]; } else { // Step left x = upVector[offsetK + 1] - 1; if ((k > upStart) && (upVector[offsetK - 1] < x)) { // Step up x = upVector[offsetK - 1]; } } int y = x - k; while ((x > lowerA) && (y > lowerB) && (comparer(A[x - 1], B[y - 1]))) { x--; y--; } upVector[offsetK] = x; // Check for overlap if (!oddDelta && (downStart <= k) && (k <= downEnd) && upVector[offsetK] <= downVector[downOffset + k]) { cutA = downVector[downOffset + k]; cutB = downVector[downOffset + k] - k; break; } k += 2; } while (k <= upEnd); d++; } while (d <= maxD && cutA < 0); // Now that we have the cut points, add to stack and try again. stack.Push(lowerA, cutA, lowerB, cutB); stack.Push(cutA, upperA, cutB, upperB); } /* * Build chunks */ int itemA = 0; int itemB = 0; int startA = 0; int startB = 0; ReadOnlyWindow <T> subSetA; ReadOnlyWindow <T> subSetB; while (itemA < A.Count || itemB < B.Count) { // Scan through unchanged items. if ((itemA < A.Count) && (!modificationsA[itemA]) && (itemB < B.Count) && (!modificationsB[itemB])) { itemA++; itemB++; continue; } // Output any equal data. if (itemA > startA) { // The length of A & B should be identical! Debug.Assert(itemB - startB == itemA - startA); subSetA = A.GetSubset(startA, itemA - startA); subSetB = B.GetSubset(startB, itemB - startB); chunks.Add(new Chunk <T>(true, subSetA, subSetB)); } startA = itemA; startB = itemB; while (itemA < A.Count && (itemB >= B.Count || modificationsA[itemA])) { itemA++; } while (itemB < B.Count && (itemA >= A.Count || modificationsB[itemB])) { itemB++; } subSetA = itemA > startA?A.GetSubset(startA, itemA - startA) : null; subSetB = itemB > startB?B.GetSubset(startB, itemB - startB) : null; if (subSetA != null || subSetB != null) { chunks.Add(new Chunk <T>(false, subSetA, subSetB)); } startA = itemA; startB = itemB; } // Output any remaining data if (itemA > startA) { // The length of A & B should be identical! Debug.Assert(itemB - startB == itemA - startA); subSetA = A.GetSubset(startA, itemA - startA); subSetB = B.GetSubset(startB, itemB - startB); chunks.Add(new Chunk <T>(true, subSetA, subSetB)); } _chunks = chunks; }
/// <summary> /// Initializes a new instance of the <see cref="StringDifferences" /> class. /// </summary> /// <param name="a">The 'A' string.</param> /// <param name="offsetA">The offset to the start of a window in the first string.</param> /// <param name="lengthA">The length of the window in the first string.</param> /// <param name="b">The 'B' string.</param> /// <param name="offsetB">The offset to the start of a window in the second string.</param> /// <param name="lengthB">The length of the window in the second string.</param> /// <param name="textOptions">The text options.</param> /// <param name="comparer">The character comparer.</param> /// <exception cref="ArgumentNullException"><paramref name="a" /> is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException"><paramref name="b" /> is <see langword="null" />.</exception> /// <exception cref="ArgumentNullException"><paramref name="comparer" /> is <see langword="null" />.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="offsetA" /> is out of range.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="lengthA" /> is out of range.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="offsetB" /> is out of range.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="lengthB" /> is out of range.</exception> /// <exception cref="Exception">The <paramref name="comparer" /> throws an exception.</exception> internal StringDifferences( [NotNull] string a, int offsetA, int lengthA, [NotNull] string b, int offsetB, int lengthB, TextOptions textOptions, [NotNull] Func <char, char, bool> comparer) { if (a == null) { throw new ArgumentNullException(nameof(a)); } if (b == null) { throw new ArgumentNullException(nameof(b)); } if (comparer == null) { throw new ArgumentNullException(nameof(comparer)); } A = a; B = b; if (textOptions != TextOptions.None) { // Wrap the comparer with an additional check to handle special characters. Func <char, char, bool> oc = comparer; if (textOptions.HasFlag(TextOptions.IgnoreWhiteSpace)) { // Ignore white space - treat all whitespace as the same (note this will handle line endings too). comparer = (x, y) => char.IsWhiteSpace(x) ? char.IsWhiteSpace(y) : oc(x, y); } else if (textOptions.HasFlag(TextOptions.NormalizeLineEndings)) { // Just normalize line endings - treat '\r' and '\n\ as the same comparer = (x, y) => x == '\r' || x == '\n' ? y == '\r' || y == '\n' : oc(x, y); } } // Map strings based on text options StringMap aMap = a.ToMapped(textOptions); StringMap bMap = b.ToMapped(textOptions); // Perform diff on mapped string Differences <char> chunks = aMap.Diff(bMap, comparer); // Special case simple equality if (chunks.Count < 2) { Chunk <char> chunk = chunks.Single(); // ReSharper disable once PossibleNullReferenceException _chunks = new[] { new StringChunk(chunk.AreEqual, a, 0, b, 0) }; return; } // To reverse the mapping we first calculate the split points in the original strings, and find // the last reference to the original strings in each chunk. int[] aEnds = new int[chunks.Count]; int[] bEnds = new int[chunks.Count]; int lastA = 0; int lastB = 0; for (int i = 0; i < chunks.Count; i++) { Chunk <char> chunk = chunks[i]; ReadOnlyWindow <char> chunkA = chunk.A; ReadOnlyWindow <char> chunkB = chunk.B; if (chunk.A != null) { aEnds[i] = aMap.GetOriginalIndex(chunkA.Offset + chunkA.Count - 1) + 1; lastA = i; } else { aEnds[i] = -1; } if (chunk.B != null) { bEnds[i] = bMap.GetOriginalIndex(chunkB.Offset + chunkB.Count - 1) + 1; lastB = i; } else { bEnds[i] = -1; } } // Now we're ready to build up a new chunk array based on the original strings StringChunk[] stringChunks = new StringChunk[chunks.Count]; int aStart = 0; int bStart = 0; for (int i = 0; i < chunks.Count; i++) { int aEnd = i == lastA ? aMap.OriginalCount : aEnds[i]; int bEnd = i == lastB ? bMap.OriginalCount : bEnds[i]; string ac = aEnd > -1 ? a.Substring(aStart, aEnd - aStart) : null; string bc = bEnd > -1 ? b.Substring(bStart, bEnd - bStart) : null; stringChunks[i] = new StringChunk(chunks[i].AreEqual, ac, aEnd > -1 ? aStart : -1, bc, bEnd > -1 ? bStart : -1); if (aEnd > -1) { aStart = aEnd; } if (bEnd > -1) { bStart = bEnd; } } _chunks = stringChunks; }
public void TestRemove() { List<int> list = new List<int> { 1, 2, 3 }; Window<int> window = new Window<int>(list, 1); ReadOnlyWindow<int> readOnlyWindow = new ReadOnlyWindow<int>(list, 1); int[] l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(2, window.Count); Assert.AreEqual(3, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(2, readOnlyWindow.Count); Assert.AreEqual(3, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Remove through window window.Remove(2); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(1, window.Count); Assert.AreEqual(2, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(1, readOnlyWindow.Count); Assert.AreEqual(2, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Remove from underlying list (note that the offset doesn't change, the window is effectively moved). list.Remove(1); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(0, window.Count); Assert.AreEqual(1, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(0, readOnlyWindow.Count); Assert.AreEqual(1, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Remove all items! list.Clear(); Assert.AreEqual(0, window.Count); Assert.AreEqual(0, window.Length); Assert.AreEqual(1, window.Offset); // Note the offset is beyond the end of the underlying data Assert.AreEqual(0, readOnlyWindow.Count); Assert.AreEqual(0, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Note the offset is beyond the end of the underlying data }
public void TestInsert() { List<int> list = new List<int> { 1, 2, 3 }; Window<int> window = new Window<int>(list, 1); ReadOnlyWindow<int> readOnlyWindow = new ReadOnlyWindow<int>(list, 1); int[] l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(2, window.Count); Assert.AreEqual(3, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(2, readOnlyWindow.Count); Assert.AreEqual(3, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Insert through window window.Insert(1, 4); Assert.AreEqual(4, list[2]); Assert.AreEqual(window[1], list[2]); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(3, window.Count); Assert.AreEqual(4, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(3, readOnlyWindow.Count); Assert.AreEqual(4, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); // Insert through underlying data list.Insert(3, 5); Assert.AreEqual(5, list[3]); Assert.AreEqual(window[2], list[3]); l = list.Skip(1).ToArray(); CollectionAssert.AreEqual(l, window); CollectionAssert.AreEqual(l, readOnlyWindow); Assert.AreEqual(4, window.Count); Assert.AreEqual(5, window.Length); Assert.AreEqual(1, window.Offset); Assert.AreEqual(4, readOnlyWindow.Count); Assert.AreEqual(5, readOnlyWindow.Length); Assert.AreEqual(1, readOnlyWindow.Offset); }