json-diff.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /*
  2. * json-diff.c
  3. *
  4. * Copyright (C) 2014 Andreas Gruenbacher <andreas.gruenbacher@gmail.com>
  5. *
  6. * Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3,
  7. * http://www.fsf.org/licensing/licenses/agpl-3.0.html, or any later version.
  8. */
  9. /*
  10. * Function json_diff(a, b)
  11. *
  12. * Create an RFC 6902 style patch between two JSON objects.
  13. * For example, json_diff({a: [9, 7]}, {a: [9, 8, 7]}) results in:
  14. * [{op: 'add', path: '/a/1', value: 8}].
  15. *
  16. * No cyclic references are allowed in a or b. Because the algorithm computes
  17. * differences between arrays in a way similar to the diff utility, it will
  18. * become slow for large arrays of complex objects.
  19. */
  20. var json_diff = (function() {
  21. function equal(a, b) {
  22. if (a === b)
  23. return true;
  24. if (typeof a !== 'object' || typeof b !== 'object')
  25. return false;
  26. if (a === null || b === null)
  27. return false;
  28. if (a instanceof Array !== b instanceof Array)
  29. return false;
  30. if (a instanceof Array) {
  31. if (a.length != b.length)
  32. return false;
  33. for (var n = 0; n < a.length; n++)
  34. if (!equal(a[n], b[n]))
  35. return false;
  36. } else {
  37. for (var key in a) {
  38. if (!(key in b) || !equal(a[key], b[key]))
  39. return false;
  40. }
  41. for (var key in b) {
  42. if (!(key in a))
  43. return false;
  44. }
  45. }
  46. return true;
  47. }
  48. function type(v) {
  49. if (typeof v === 'object') {
  50. if (v instanceof Array)
  51. return 'array';
  52. if (v === null)
  53. return 'null';
  54. }
  55. return typeof v // 'undefined', 'string', 'number', 'boolean'
  56. }
  57. // The actual json_diff function
  58. return function(a, b) {
  59. var path = [], operations = [];
  60. function json_pointer(tail) {
  61. if (tail === undefined)
  62. return '/';
  63. else {
  64. path.push(tail);
  65. var pointer = '';
  66. for (var n = 0; n < path.length; n++)
  67. pointer = pointer + '/' + path[n].toString().replace(/~/g, '~0').replace(/\//g, '~1');
  68. path.pop();
  69. return pointer;
  70. }
  71. }
  72. function result(op, tail, value) {
  73. var pointer = json_pointer(tail);
  74. if (op === 'remove')
  75. operations.push({op: op, path: pointer});
  76. else
  77. operations.push({op: op, path: pointer,
  78. value: value === undefined ? null : value});
  79. }
  80. // Based on the Dynamic Programming Longest Common Subsequence algorithm at
  81. // http://rosettacode.org/wiki/Longest_common_subsequence#JavaScript
  82. function diffseq(a, b) {
  83. var i, j, m, n,
  84. row = [], c = [],
  85. left, diag, latch;
  86. var skip, edit = [];
  87. m = a.length;
  88. n = b.length;
  89. // ignore equal elements at both ends
  90. for (; m && n; m--, n--)
  91. if (!equal(a[m - 1], b[n - 1]))
  92. break;
  93. for (skip = 0; m && n; skip++, m--, n--)
  94. if (!equal(a[skip], b[skip]))
  95. break;
  96. if (m && n) {
  97. //build the c-table
  98. for (j = 0; j < n; row[j++] = 0);
  99. for (i = 0 ; i < m; i++) {
  100. c[i] = row = row.slice();
  101. for (diag = 0, j = 0; j < n; j++, diag = latch) {
  102. latch = row[j];
  103. if (equal(a[i + skip], b[j + skip]))
  104. row[j] = diag + 1;
  105. else {
  106. left = row[j - 1] || 0;
  107. if (left > row[j])
  108. row[j] = left;
  109. }
  110. }
  111. }
  112. i--, j--;
  113. //row[j] now contains the length of the lcs
  114. //compute the edit sequence from the table
  115. while (i > -1 && j > -1) {
  116. switch (c[i][j]) {
  117. default:
  118. edit.unshift('=');
  119. i--; j--;
  120. break;
  121. case j && c[i][j - 1]:
  122. edit.unshift('+');
  123. j--;
  124. break;
  125. case i && c[i - 1][j]:
  126. edit.unshift('-');
  127. i--;
  128. break;
  129. }
  130. }
  131. } else {
  132. i = m - 1;
  133. j = n - 1;
  134. }
  135. for (; j > -1; j--)
  136. edit.unshift('+');
  137. for (; i > -1; i--)
  138. edit.unshift('-');
  139. var edit_replace = [];
  140. for (n = 0; n < edit.length; n++) {
  141. if (edit[n] === '-') {
  142. for (i = n + 1; i < edit.length && edit[i] === '-'; i++);
  143. for (j = i; j < edit.length && edit[j] === '+'; j++);
  144. if (i - n == j - i) {
  145. while (i++ < j)
  146. edit_replace.push('*');
  147. n = j - 1;
  148. } else {
  149. for (; n < j; n++)
  150. edit_replace.push(edit[n]);
  151. n--;
  152. }
  153. } else
  154. edit_replace.push(edit[n]);
  155. }
  156. // console.log('>>> ' + skip + ' ' + edit.join('') + ' ' + edit_replace.join(''));
  157. edit = edit_replace;
  158. for (i = 0, j = 0, n = 0; n < edit.length; n++) {
  159. switch(edit[n]) {
  160. case '*':
  161. diff_recursive(a[i + skip], b[j + skip], j + skip);
  162. case '=':
  163. i++;
  164. j++;
  165. break;
  166. case '-':
  167. result('remove', j + skip);
  168. i++;
  169. break;
  170. case '+':
  171. result('add', j + skip, b[j + skip]);
  172. j++;
  173. break;
  174. }
  175. }
  176. }
  177. function diff_arrays(a, b, tail) {
  178. if (tail !== undefined)
  179. path.push(tail);
  180. diffseq(a, b);
  181. if (tail !== undefined)
  182. path.pop();
  183. }
  184. function diff_objects(a, b, tail) {
  185. if (tail !== undefined)
  186. path.push(tail);
  187. for (var key in a) {
  188. if (key in b)
  189. diff_recursive(a[key], b[key], key);
  190. else
  191. result('remove', key);
  192. }
  193. for (var key in b) {
  194. if (!(key in a))
  195. result('add', key, b[key]);
  196. }
  197. if (tail !== undefined)
  198. path.pop();
  199. }
  200. function diff_recursive(a, b, tail) {
  201. if (a === b)
  202. return;
  203. var ta = type(a), tb = type(b);
  204. if (ta !== tb)
  205. result('replace', tail, b);
  206. else {
  207. switch(ta) {
  208. case 'object':
  209. diff_objects(a, b, tail);
  210. break;
  211. case 'array':
  212. diff_arrays(a, b, tail);
  213. break;
  214. default:
  215. result('replace', tail, b);
  216. }
  217. }
  218. }
  219. diff_recursive(a, b, undefined);
  220. return operations;
  221. };
  222. })();
  223. if (typeof exports !== 'undefined')
  224. exports.diff = json_diff;