508e35c73f1b8a06872ca640cedd72f55f9e5710402eadb70699c91c024bba34d36b1035bda8a55c0f60e8634e97b1c5644489bf8ea3b0fc436c376f060de3 6.2 KB

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