arrayDiff2.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // Myers' Diff Algorithm
  2. // Modified from https://github.com/kpdecker/jsdiff/blob/master/src/diff/base.js
  3. function Diff() {}
  4. Diff.prototype = {
  5. diff: function (oldArr, newArr, equals) {
  6. if (!equals) {
  7. equals = function (a, b) {
  8. return a === b;
  9. };
  10. }
  11. this.equals = equals;
  12. var self = this;
  13. oldArr = oldArr.slice();
  14. newArr = newArr.slice(); // Allow subclasses to massage the input prior to running
  15. var newLen = newArr.length;
  16. var oldLen = oldArr.length;
  17. var editLength = 1;
  18. var maxEditLength = newLen + oldLen;
  19. var bestPath = [{
  20. newPos: -1,
  21. components: []
  22. }]; // Seed editLength = 0, i.e. the content starts with the same values
  23. var oldPos = this.extractCommon(bestPath[0], newArr, oldArr, 0);
  24. if (bestPath[0].newPos + 1 >= newLen && oldPos + 1 >= oldLen) {
  25. var indices = [];
  26. for (var i = 0; i < newArr.length; i++) {
  27. indices.push(i);
  28. } // Identity per the equality and tokenizer
  29. return [{
  30. indices: indices,
  31. count: newArr.length
  32. }];
  33. } // Main worker method. checks all permutations of a given edit length for acceptance.
  34. function execEditLength() {
  35. for (var diagonalPath = -1 * editLength; diagonalPath <= editLength; diagonalPath += 2) {
  36. var basePath;
  37. var addPath = bestPath[diagonalPath - 1];
  38. var removePath = bestPath[diagonalPath + 1];
  39. var oldPos = (removePath ? removePath.newPos : 0) - diagonalPath;
  40. if (addPath) {
  41. // No one else is going to attempt to use this value, clear it
  42. bestPath[diagonalPath - 1] = undefined;
  43. }
  44. var canAdd = addPath && addPath.newPos + 1 < newLen;
  45. var canRemove = removePath && 0 <= oldPos && oldPos < oldLen;
  46. if (!canAdd && !canRemove) {
  47. // If this path is a terminal then prune
  48. bestPath[diagonalPath] = undefined;
  49. continue;
  50. } // Select the diagonal that we want to branch from. We select the prior
  51. // path whose position in the new string is the farthest from the origin
  52. // and does not pass the bounds of the diff graph
  53. if (!canAdd || canRemove && addPath.newPos < removePath.newPos) {
  54. basePath = clonePath(removePath);
  55. self.pushComponent(basePath.components, undefined, true);
  56. } else {
  57. basePath = addPath; // No need to clone, we've pulled it from the list
  58. basePath.newPos++;
  59. self.pushComponent(basePath.components, true, undefined);
  60. }
  61. oldPos = self.extractCommon(basePath, newArr, oldArr, diagonalPath); // If we have hit the end of both strings, then we are done
  62. if (basePath.newPos + 1 >= newLen && oldPos + 1 >= oldLen) {
  63. return buildValues(self, basePath.components, newArr, oldArr);
  64. } else {
  65. // Otherwise track this path as a potential candidate and continue.
  66. bestPath[diagonalPath] = basePath;
  67. }
  68. }
  69. editLength++;
  70. }
  71. while (editLength <= maxEditLength) {
  72. var ret = execEditLength();
  73. if (ret) {
  74. return ret;
  75. }
  76. }
  77. },
  78. pushComponent: function (components, added, removed) {
  79. var last = components[components.length - 1];
  80. if (last && last.added === added && last.removed === removed) {
  81. // We need to clone here as the component clone operation is just
  82. // as shallow array clone
  83. components[components.length - 1] = {
  84. count: last.count + 1,
  85. added: added,
  86. removed: removed
  87. };
  88. } else {
  89. components.push({
  90. count: 1,
  91. added: added,
  92. removed: removed
  93. });
  94. }
  95. },
  96. extractCommon: function (basePath, newArr, oldArr, diagonalPath) {
  97. var newLen = newArr.length;
  98. var oldLen = oldArr.length;
  99. var newPos = basePath.newPos;
  100. var oldPos = newPos - diagonalPath;
  101. var commonCount = 0;
  102. while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(newArr[newPos + 1], oldArr[oldPos + 1])) {
  103. newPos++;
  104. oldPos++;
  105. commonCount++;
  106. }
  107. if (commonCount) {
  108. basePath.components.push({
  109. count: commonCount
  110. });
  111. }
  112. basePath.newPos = newPos;
  113. return oldPos;
  114. },
  115. tokenize: function (value) {
  116. return value.slice();
  117. },
  118. join: function (value) {
  119. return value.slice();
  120. }
  121. };
  122. function buildValues(diff, components, newArr, oldArr) {
  123. var componentPos = 0;
  124. var componentLen = components.length;
  125. var newPos = 0;
  126. var oldPos = 0;
  127. for (; componentPos < componentLen; componentPos++) {
  128. var component = components[componentPos];
  129. if (!component.removed) {
  130. var indices = [];
  131. for (var i = newPos; i < newPos + component.count; i++) {
  132. indices.push(i);
  133. }
  134. component.indices = indices;
  135. newPos += component.count; // Common case
  136. if (!component.added) {
  137. oldPos += component.count;
  138. }
  139. } else {
  140. var indices = [];
  141. for (var i = oldPos; i < oldPos + component.count; i++) {
  142. indices.push(i);
  143. }
  144. component.indices = indices;
  145. oldPos += component.count;
  146. }
  147. }
  148. return components;
  149. }
  150. function clonePath(path) {
  151. return {
  152. newPos: path.newPos,
  153. components: path.components.slice(0)
  154. };
  155. }
  156. var arrayDiff = new Diff();
  157. function _default(oldArr, newArr, callback) {
  158. return arrayDiff.diff(oldArr, newArr, callback);
  159. }
  160. module.exports = _default;