File Compare Output

diff: Diff.cs.v2 >> Diff.cs.v3

1namespace my.utils {
2  using System;
3  using System.Collections;
4  using System.Text;
5  using System.Text.RegularExpressions;
6
7  /// <summary>
8  /// This Class implements the Difference Algorithm published in
9  /// "An O(ND) Difference Algorithm and its Variations" by Eugene Myers
10  /// Algorithmica Vol. 1 No. 2, 1986, p 251.  
11  /// 
12  /// There are many C, Java, Lisp implementations public available but they all seem to come
13  /// from the same source (diffutils) that is under the (unfree) GNU public License
14  /// and cannot be reused as a sourcecode for a commercial application.
15  /// There are very old C implementations that use other (worse) algorithms.
16  /// Microsoft also published sourcecode of a diff-tool (windiff) that uses some tree data.
17  /// Also, a direct transfer from a C source to C# is not easy because there is a lot of pointer
18  /// arithmetic in the typical C solutions and i need a managed solution.
19  /// These are the reasons why I implemented the original published algorithm from the scratch and
20  /// make it avaliable without the GNU license limitations.
21  /// I do not need a high performance diff tool because it is used only sometimes.
22  /// I will do some performace tweaking when needed.
23  /// 
24  /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents
25  /// each line is converted into a (hash) number. See DiffText(). 
26  /// 
27  /// Some chages to the original algorithm:
28  /// The original algorithm was described using a recursive approach and comparing zero indexed arrays.
29  /// Extracting sub-arrays and rejoining them is very performance and memory intensive so the same
30  /// (readonly) data arrays are passed arround together with their lower and upper bounds.
31  /// This circumstance makes the LCS and SMS functions more complicate.
32  /// I added some code to the LCS function to get a fast response on sub-arrays that are identical,
33  /// completely deleted or inserted.
34  /// 
35  /// The result from a comparisation is stored in 2 arrays that flag for modified (deleted or inserted)
36  /// lines in the 2 data arrays. These bits are then analysed to produce a array of Item objects.
37  /// 
38  /// Further possible optimizations:
39  /// (first rule: don't do it; second: don't do it yet)
40  /// The arrays DataA and DataB are passed as parameters, but are never changed after the creation
41  /// so they can be members of the class to avoid the paramter overhead.
42  /// In SMS is a lot of boundary arithmetic in the for-D and for-k loops that can be done by increment
43  /// and decrement of local variables.
44  /// The DownVector and UpVector arrays are alywas created and destroyed each time the SMS gets called.
45  /// It is possible to reuse tehm when transfering them to members of the class.
46  /// See TODO: hints.
47  /// 
48  /// Changes:
49  /// 2002.09.20 There was a "hang" in some situations.
50  /// Now I undestand a little bit more of the SMS algorithm. 
51  /// There have been overlapping boxes; that where analyzed partial differently.
52  /// One return-point is enough.
53  /// A assertion was added in CreateDiffs when in debug-mode, that counts the number of equal (no modified) lines in both arrays.
54  /// They must be identical.
55  /// 
56  /// Changes:
57  /// 2003.02.07 Out of bounds error in the Up/Down vector arrays in some situations.
58  /// The two vetors are now accessed using different offsets that are adjusted using the start k-Line. 
59  /// A test case is added. 
60  /// </summary>
61
62  public class Diff {
63
64    /// <summary>details of one difference.</summary>
65    public struct Item {
66      /// <summary>Start Line number in Data A.</summary>
67      public int StartA;
68      /// <summary>Start Line number in Data B.</summary>
69      public int StartB;
70
71      /// <summary>Number of changes in Data A.</summary>
72      public int deletedA;
73      /// <summary>Number of changes in Data A.</summary>
74      public int insertedB;
75    } // Item
76
77    /// <summary>
78    /// Shortest Middle Snake Return Data
79    /// </summary>
80    private struct SMSRD {
81      internal int x, y; 
82      // internal int u, v;  // 2002.09.20: no need for 2 points 
83    } 
84
85    /// <summary>The A-Version of the data (original data) to be compared.</summary>
86    private DiffData DataA;
87
88    /// <summary>The B-Version of the data (modified data) to be compared.</summary>
89    private DiffData DataB;
90
91    #region self-Test
92
 #if (false)
93#if (SELFTEST)
94    /// <summary>
95    /// start a self- / box-test for some diff cases and report to the debug output.
96    /// </summary>
97    /// <param name="args">not used</param>
98    /// <returns>always 0</returns>
99    public static int Main(string[] args) {
100      Diff d = new Diff();
101      StringBuilder ret = new StringBuilder();
102      string a, b;
103
104      Debug.setDebug(true);
105      Debug.setDebugLevel(0);
106
107      // test all changes
108      a = "a,b,c,d,e,f,g,h,i,j,k,l".Replace(',', '\n');
109      b = "0,1,2,3,4,5,6,7,8,9".Replace(',', '\n');
110      Debug.Assert(TestHelper(d.DiffText(a, b, false, false, false))
111        == "12.10.0.0*", 
112        "Diff-Selftest", "all-changes test failed.");
113      Debug.Write(Debug.LEVEL_INFO, "Selftest", "all-changes test passed.");
114
115      // test all same
116      a = "a,b,c,d,e,f,g,h,i,j,k,l".Replace(',', '\n');
117      b = a;
118      Debug.Assert(TestHelper(d.DiffText(a, b, false, false, false))
119        == "", 
120        "Diff-Selftest", "all-same test failed.");
121      Debug.Write(Debug.LEVEL_INFO, "Selftest", "all-same test passed.");
122
123      // test snake
124      a = "a,b,c,d,e,f".Replace(',', '\n');
125      b = "b,c,d,e,f,x".Replace(',', '\n');
126      Debug.Assert(TestHelper(d.DiffText(a, b, false, false, false))
127        == "1.0.0.0*0.1.6.5*", 
128        "Diff-Selftest", "snake test failed.");
129      Debug.Write(Debug.LEVEL_INFO, "Selftest", "snake test passed.");
130
131      // 2002.09.20 - repro
132      a = "c1,a,c2,b,c,d,e,g,h,i,j,c3,k,l".Replace(',', '\n');
133      b = "C1,a,C2,b,c,d,e,I1,e,g,h,i,j,C3,k,I2,l".Replace(',', '\n');
134      Debug.Assert(TestHelper(d.DiffText(a, b, false, false, false))
135        == "1.1.0.0*1.1.2.2*0.2.7.7*1.1.11.13*0.1.13.15*", 
         "Diff-Selftest", "snake test failed.");
136        "Diff-Selftest", "repro20020920 test failed.");
137      Debug.Write(Debug.LEVEL_INFO, "Selftest", "repro20020920 test passed.");
138      
139      // 2003.02.07 - repro
140      a = "F".Replace(',', '\n');
141      b = "0,F,1,2,3,4,5,6,7".Replace(',', '\n');
142      Debug.Assert(TestHelper(d.DiffText(a, b, false, false, false))
143        == "0.1.0.0*0.7.1.2*", 
144        "Diff-Selftest", "repro20030207 test failed.");
145      Debug.Write(Debug.LEVEL_INFO, "Selftest", "repro20030207 test passed.");
146      
147      // Muegel - repro
148      a = "HELLO\nWORLD";
149      b = "\n\nhello\n\n\n\nworld\n";
150      Debug.Assert(TestHelper(d.DiffText(a, b, false, false, false))
151        == "2.8.0.0*", 
152        "Diff-Selftest", "repro20030409 test failed.");
153      Debug.Write(Debug.LEVEL_INFO, "Selftest", "repro20030409 test passed.");
154
155    // test some differences
156      a = "a,b,-,c,d,e,f,f".Replace(',', '\n');
157      b = "a,b,x,c,e,f".Replace(',', '\n');
158      Debug.Assert(TestHelper(d.DiffText(a, b, false, false, false))
159        == "1.1.2.2*1.0.4.4*1.0.6.5*", 
160        "Diff-Selftest", "some-changes test failed.");
161
162      Debug.Write(Debug.LEVEL_INFO, "Selftest", "some-changes test passed.");
163
164      Debug.Write(Debug.LEVEL_INFO, "Selftest", "End.");
165      return (0);
166    }
167
168
169    public static string TestHelper(Item []f) {
170      StringBuilder ret = new StringBuilder();
171      for (int n = 0; n < f.Length; n++) {
172        ret.Append(f[n].deletedA.ToString() + "." + f[n].insertedB.ToString() + "." + f[n].StartA.ToString() + "." + f[n].StartB.ToString() + "*");
173      }
174      // Debug.Write(5, "TestHelper", ret.ToString());
175      return (ret.ToString());
176    }
177#endif
178    #endregion
179
180    /// <summary>
181    /// Find the difference in 2 texts, comparing by textlines.
182    /// </summary>
183    /// <param name="TextA">A-version of the text (usualy the old one)</param>
184    /// <param name="TextB">B-version of the text (usualy the new one)</param>
185    public Item [] DiffText(string TextA, string TextB) {
186      return(DiffText(TextA, TextB, false, false, false));
187    } // DiffText
188
189      
190    /// <summary>
191    /// Find the difference in 2 text documents, comparing by textlines.
192    /// The algorithm itself is comparing 2 arrays of numbers so when comparing 2 text documents
193    /// each line is converted into a (hash) number. This hash-value is computed by storing all
194    /// textlines into a common hashtable so i can find dublicates in there, and generating a 
195    /// new number each time a new textline is inserted.
196    /// </summary>
197    /// <param name="TextA">A-version of the text (usualy the old one)</param>
198    /// <param name="TextB">B-version of the text (usualy the new one)</param>
199    /// <param name="trimSpace">When set to true, all leading and trailing whitespace characters are stripped out before the comparation is done.</param>
200    /// <param name="ignoreSpace">When set to true, all whitespace characters are converted to a single space character before the comparation is done.</param>
201    /// <param name="ignoreCase">When set to true, all characters are converted to their lowercase equivivalence before the comparation is done.</param>
202    /// <returns></returns>
203    public Item [] DiffText(string TextA, string TextB, bool trimSpace, bool ignoreSpace, bool ignoreCase) {
204      // prepare the input-text and convert to comparable numbers.
205      Hashtable h = new Hashtable(TextA.Length + TextB.Length);
206      DataA = new DiffData(DiffCodes(TextA, h, trimSpace, ignoreSpace, ignoreCase));
207      DataB = new DiffData(DiffCodes(TextB, h, trimSpace, ignoreSpace, ignoreCase));
208      h = null; // free up hashtable memory (maybe)
209
210      LCS(DataA, 0, DataA.Length, DataB, 0, DataB.Length);
211      return CreateDiffs();
212    } // DiffText
213
214
215    /// <summary>
216    /// This function converts all textlines of the text into unique numbers for every unique textline
217    /// so further work can work only with simple numbers.
218    /// </summary>
219    /// <param name="aText">the input text</param>
220    /// <param name="h">This extern initialized hashtable is used for storing all ever used textlines.</param>
221    /// <param name="trimSpace">ignore leading and trailing space characters</param>
222    /// <returns>a array of integers.</returns>
223    private int[] DiffCodes(string aText, Hashtable h, bool trimSpace, bool ignoreSpace, bool ignoreCase) {
224      // get all codes of the text
225      string []Lines;
226      int []Codes;
227      int lastUsedCode = h.Count;
228      object aCode;
229      string s;
230
231      // strip off all cr, only use lf as textline separator.
232      aText = aText.Replace("\r", "");
233      Lines = aText.Split('\n');
234
235      Codes = new int[Lines.Length];
236
237      for (int i = 0; i < Lines.Length; ++i) {
238        s = Lines[i];
239        if (trimSpace)
240          s = s.Trim();
241
242        if (ignoreSpace) {
243          s = Regex.Replace(s, "\\s+", " ");            // TODO: optimization: faster blank removal.
244        }
245
246        if (ignoreCase)
247          s = s.ToLower();
248        
249        aCode = h[s];
250        if (aCode == null) {
251          lastUsedCode++;
252          h[s] = lastUsedCode;
253          Codes[i] = lastUsedCode;
254        } else {
255          Codes[i] = (int)aCode;
256        } // if
257      } // for
258      return(Codes);
259    } // DiffCodes
260
261
262    /// <summary>
263    /// This is the algorithm to find the Shortest Middle Snake (SMS).
264    /// </summary>
265    /// <param name="DataA">sequence A</param>
266    /// <param name="LowerA">lower bound of the actual range in DataA</param>
267    /// <param name="UpperA">upper bound of the actual range in DataA (exclusive)</param>
268    /// <param name="DataB">sequence B</param>
269    /// <param name="LowerB">lower bound of the actual range in DataB</param>
270    /// <param name="UpperB">upper bound of the actual range in DataB (exclusive)</param>
271    /// <returns>a MiddleSnakeData record containing x,y and u,v</returns>
272    private SMSRD SMS(DiffData DataA, int LowerA, int UpperA, DiffData DataB, int LowerB, int UpperB) {
273      SMSRD ret;
       int MAX = DataA.Length + DataB.Length;
274      int MAX = DataA.Length + DataB.Length + 1;
275
276      int DownK = LowerA - LowerB; // the k-line to start the forward search
277      int UpK = UpperA - UpperB; // the k-line to start the reverse search
278
279      int Delta = (UpperA - LowerA) - (UpperB - LowerB);
280      bool oddDelta = (Delta & 1) != 0;
281
282      /// vector for the (0,0) to (x,y) search
283      int[] DownVector = new int[2* MAX + 2];
284
285      /// vector for the (u,v) to (N,M) search
286      int[] UpVector = new int[2 * MAX + 2];
287      
288      // The vectors in the publication accepts negative indexes. the vectors implemented here are 0-based
       // and are access using a specific offset: vOffset
       int vOffset = MAX;
289      // and are access using a specific offset: UpOffset UpVector and DownOffset for DownVektor
290      int DownOffset = MAX - DownK;
291      int UpOffset = MAX - UpK;
292
       int DownK = LowerA - LowerB; // the k-line to start the forward search
       int UpK = UpperA - UpperB; // the k-line to start the reverse search
 
       int Delta = (UpperA - LowerA) - (UpperB - LowerB);
       bool oddDelta = (Delta & 1) != 0;
 
293      int  MaxD = ((UpperA - LowerA + UpperB - LowerB) / 2) + 1;
294
295      // Debug.Write(2, "SMS", String.Format("Search the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB));
296
297      // init vectors
       DownVector[vOffset + DownK + 1] = LowerA;
       UpVector[vOffset + UpK - 1] = UpperA;
298      DownVector[DownOffset + DownK + 1] = LowerA;
299      UpVector[UpOffset + UpK - 1] = UpperA;
300
301      for (int D = 0; D <= MaxD; D++) {
302
303        // Extend the forward path.
304        for (int k = DownK - D; k <= DownK + D; k += 2) {
305          // Debug.Write(0, "SMS", "extend forward path " + k.ToString());
306
307          // find the only or better starting point
308          int x, y;
309          if (k == DownK - D) {
             x = DownVector[vOffset + k+1];
310            x = DownVector[DownOffset + k+1]; // down
311          } else {
             x = DownVector[vOffset + k-1] + 1;
             if ((k < DownK + D) && (DownVector[vOffset + k+1] >= x))
               x = DownVector[vOffset + k+1];
312            x = DownVector[DownOffset + k-1] + 1; // a step to the right
313            if ((k < DownK + D) && (DownVector[DownOffset + k+1] >= x))
314              x = DownVector[DownOffset + k+1]; // down
315          }
316          y = x - k;
317
318          // find the end of the furthest reaching forward D-path in diagonal k.
319          while ((x < UpperA) && (y < UpperB) && (DataA.data[x] == DataB.data[y])) {
320            x++; y++;
321          }
           DownVector[vOffset + k] = x;
322          DownVector[DownOffset + k] = x;
323
324          // overlap ?
325          if (oddDelta && (UpK-D < k) && (k < UpK+D)) {
             if (UpVector[vOffset + k] <= DownVector[vOffset + k]) {
               ret.x = DownVector[vOffset + k];
               ret.y = DownVector[vOffset + k] - k;
               // ret.u = UpVector[vOffset + k];      // 2002.09.20: no need for 2 points 
               // ret.v = UpVector[vOffset + k] - k;
326            if (UpVector[UpOffset + k] <= DownVector[DownOffset + k]) {
327              ret.x = DownVector[DownOffset + k];
328              ret.y = DownVector[DownOffset + k] - k;
329              // ret.u = UpVector[UpOffset + k];      // 2002.09.20: no need for 2 points 
330              // ret.v = UpVector[UpOffset + k] - k;
331              return (ret);
332            } // if
333          } // if
334
335        } // for k
336
337        // Extend the reverse path.
338        for (int k = UpK - D; k <= UpK + D; k += 2) {
339          // Debug.Write(0, "SMS", "extend reverse path " + k.ToString());
340
341          // find the only or better starting point
342          int x, y;
343          if (k == UpK + D) {
             x = UpVector[vOffset + k-1]; // up
344            x = UpVector[UpOffset + k-1]; // up
345          } else {
             x = UpVector[vOffset + k+1] - 1; // left
             if ((k > UpK - D) && (UpVector[vOffset + k-1] < x))
               x = UpVector[vOffset + k-1]; // up
346            x = UpVector[UpOffset + k+1] - 1; // left
347            if ((k > UpK - D) && (UpVector[UpOffset + k-1] < x))
348              x = UpVector[UpOffset + k-1]; // up
349          } // if
350          y = x - k;
351
352          while ((x > LowerA) && (y > LowerB) && (DataA.data[x-1] == DataB.data[y-1])) {
353            x--; y--; // diagonal
354          }
           UpVector[vOffset + k] = x;
355          UpVector[UpOffset + k] = x;
356
357          // overlap ?
358          if (! oddDelta && (DownK-D <= k) && (k <= DownK+D)) {
             if (UpVector[vOffset + k] <= DownVector[vOffset + k]) {
               ret.x = DownVector[vOffset + k];
               ret.y = DownVector[vOffset + k] - k;
               // ret.u = UpVector[vOffset + k];     // 2002.09.20: no need for 2 points 
               // ret.v = UpVector[vOffset + k] - k;
359            if (UpVector[UpOffset + k] <= DownVector[DownOffset + k]) {
360              ret.x = DownVector[DownOffset + k];
361              ret.y = DownVector[DownOffset + k] - k;
362              // ret.u = UpVector[UpOffset + k];     // 2002.09.20: no need for 2 points 
363              // ret.v = UpVector[UpOffset + k] - k;
364              return (ret);
365            } // if
366          } // if
367
368        } // for k
369
370      } // for D
371
372      throw new ApplicationException("the algorithm should never come here.");
373    } // SMS
374
375
376    /// <summary>
377    /// This is the divide-and-conquer implementation of the longes common-subsequence (LCS) 
378    /// algorithm.
379    /// The published algorithm passes recursively parts of the A and B sequences.
380    /// To avoid copying these arrays the lower and upper bounds are passed while the sequences stay constant.
381    /// </summary>
382    /// <param name="DataA">sequence A</param>
383    /// <param name="LowerA">lower bound of the actual range in DataA</param>
384    /// <param name="UpperA">upper bound of the actual range in DataA (exclusive)</param>
385    /// <param name="DataB">sequence B</param>
386    /// <param name="LowerB">lower bound of the actual range in DataB</param>
387    /// <param name="UpperB">upper bound of the actual range in DataB (exclusive)</param>
388    private void LCS(DiffData DataA, int LowerA, int UpperA, DiffData DataB, int LowerB, int UpperB) {
389      // Debug.Write(2, "LCS", String.Format("Analyse the box: A[{0}-{1}] to B[{2}-{3}]", LowerA, UpperA, LowerB, UpperB));
390
391      // Fast walkthrough equal lines at the start
392      while (LowerA < UpperA && LowerB < UpperB && DataA.data[LowerA] == DataB.data[LowerB]) {
393        LowerA++; LowerB++;
394      }
395
396      // Fast walkthrough equal lines at the end
397      while (LowerA < UpperA && LowerB < UpperB && DataA.data[UpperA-1] == DataB.data[UpperB-1]) {
398        --UpperA; --UpperB;
399      }
400
401      if (LowerA == UpperA) {
402        // mark as inserted lines.
403        while (LowerB < UpperB)
404          DataB.modified[LowerB++] = true;
405
406      } else if (LowerB == UpperB) {
407        // mark as deleted lines.
408        while (LowerA < UpperA)
409          DataA.modified[LowerA++] = true;
410
411      } else {
412        // Find the middle snakea and length of an optimal path for A and B
413        SMSRD smsrd = SMS(DataA, LowerA, UpperA, DataB, LowerB, UpperB);
414        // Debug.Write(2, "MiddleSnakeData", String.Format("{0},{1}", smsrd.x, smsrd.y));
415
416        // The path is from LowerX to (x,y) and (x,y) ot UpperX
417        LCS(DataA, LowerA, smsrd.x, DataB, LowerB, smsrd.y);
418        LCS(DataA, smsrd.x, UpperA, DataB, smsrd.y, UpperB);  // 2002.09.20: no need for 2 points 
419      }
420    } // LCS()
421
422
423    /// <summary>Scan the tables of which lines are inserted and deleted,
424    /// producing an edit script in forward order.  
425    /// </summary>
426    /// dynamic array
427    private Item [] CreateDiffs() {
428      ArrayList a = new ArrayList();
429      Item aItem;
430      Item []result;
431
432      int StartA, StartB;
433      int LineA, LineB;
434
435      LineA = 0;
436      LineB = 0;
437      while (LineA < DataA.Length || LineB < DataB.Length) {
438        if ((LineA < DataA.Length) && (! DataA.modified[LineA])
439          && (LineB < DataB.Length) && (! DataB.modified[LineB])) {
440          // equal lines
441          LineA++; 
442          LineB++;
443
444        } else {
445          // maybe deleted and/or inserted lines
446          StartA = LineA;
447          StartB = LineB;
448  
449          while (LineA < DataA.Length && (LineB >= DataB.Length || DataA.modified[LineA]))
450            // while (LineA < DataA.Length && DataA.modified[LineA])
451            LineA++;
452
453          while (LineB < DataB.Length && (LineA >= DataA.Length || DataB.modified[LineB]))
454            // while (LineB < DataB.Length && DataB.modified[LineB])
455            LineB++;
456
457          if ((StartA < LineA) || (StartB < LineB)) {
458            // store a new difference-item
459            aItem = new Item();
460            aItem.StartA = StartA;
461            aItem.StartB = StartB;
462            aItem.deletedA = LineA - StartA;
463            aItem.insertedB = LineB - StartB;
464            a.Add(aItem);
465          } // if
466        } // if
467      } // while
468
469      result = new Item[a.Count];
470      a.CopyTo(result);
471
472      return (result);
473    }
474
475  } // class Diff
476
477  /// <summary>Data on one input file being compared.  
478  /// </summary>
479  internal class DiffData {
480
481    /// <summary>Number of elements (lines).</summary>
482    internal int Length;
483
484    /// <summary>Buffer of numbers that will be compared.</summary>
485    internal int[] data;
486
487    /// <summary>
488    /// Array of booleans that flag for modified data.
489    /// This is the result of the diff.
490    /// This means deletedA in the first Data or inserted in the second Data.
491    /// </summary>
492    internal bool[] modified;
493
494    /// <summary>
495    /// Initialize the Diff-Data buffer.
496    /// </summary>
497    /// <param name="data">reference to the buffer</param>
498    internal DiffData(int[] initData) {
499      data = initData;
500      Length = initData.Length;
501      modified = new bool[Length + 2];
502    } // DiffData
503
504  } // class DiffData
505
506} // namespace

This page is part of the http://www.mathertel.de/ web site.