/// <summary> /// Tests the string mapping. /// </summary> /// <param name="failures">The number of failures.</param> /// <param name="successes">The number of successes.</param> /// <param name="expected">The expected output mapped string.</param> /// <param name="input">The input string.</param> /// <param name="options">The options.</param> private static void TestMap( ref int failures, ref int successes, string expected, string input, TextOptions options) { Trace.WriteLine(new string('=', 80)); Assert.IsNotNull(input); Assert.IsNotNull(expected); // Generate map StringMap map = input.ToMapped(options); string output = map.Mapped; Trace.WriteLine($"Options:'{options}' String:'{input.Escape()}' Map: '{output.Escape()}'"); bool failed = false; if (!string.Equals(expected, output, StringComparison.Ordinal)) { Trace.WriteLine($"FAILED - Expected a mapped string of '{expected.Escape()}'"); failed = true; } else { for (int i = 0; i < map.Count; i++) { char o = output[i]; int j = map.GetOriginalIndex(i); if (j < 0 || j >= input.Length) { Trace.WriteLine( $"FAILED - Mapped character '{o.ToString().Escape()}' at index '{i}' has invalid mapping to index '{j}'."); failed = true; continue; } char m = input[j]; if (o != m) { Trace.WriteLine( $"FAILED - Expected mapped character '{o.ToString().Escape()}' at index '{i}' to equal '{m.ToString().Escape()}' at index '{j}'."); failed = true; } } } Trace.WriteLine(string.Empty); if (failed) { failures++; } else { successes++; } }
/// <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; }