/* * json-diff.c * * Copyright (C) 2014 Andreas Gruenbacher * * Licensed under the GNU AFFERO GENERAL PUBLIC LICENSE Version 3, * http://www.fsf.org/licensing/licenses/agpl-3.0.html, or any later version. */ /* * Function json_diff(a, b) * * Create an RFC 6902 style patch between two JSON objects. * For example, json_diff({a: [9, 7]}, {a: [9, 8, 7]}) results in: * [{op: 'add', path: '/a/1', value: 8}]. * * No cyclic references are allowed in a or b. Because the algorithm computes * differences between arrays in a way similar to the diff utility, it will * become slow for large arrays of complex objects. */ var json_diff = (function() { function equal(a, b) { if (a === b) return true; if (typeof a !== 'object' || typeof b !== 'object') return false; if (a === null || b === null) return false; if (a instanceof Array !== b instanceof Array) return false; if (a instanceof Array) { if (a.length != b.length) return false; for (var n = 0; n < a.length; n++) if (!equal(a[n], b[n])) return false; } else { for (var key in a) { if (!(key in b) || !equal(a[key], b[key])) return false; } for (var key in b) { if (!(key in a)) return false; } } return true; } function type(v) { if (typeof v === 'object') { if (v instanceof Array) return 'array'; if (v === null) return 'null'; } return typeof v // 'undefined', 'string', 'number', 'boolean' } // The actual json_diff function return function(a, b) { var path = [], operations = []; function json_pointer(tail) { if (tail === undefined) return '/'; else { path.push(tail); var pointer = ''; for (var n = 0; n < path.length; n++) pointer = pointer + '/' + path[n].toString().replace(/~/g, '~0').replace(/\//g, '~1'); path.pop(); return pointer; } } function result(op, tail, value) { var pointer = json_pointer(tail); if (op === 'remove') operations.push({op: op, path: pointer}); else operations.push({op: op, path: pointer, value: value === undefined ? null : value}); } // Based on the Dynamic Programming Longest Common Subsequence algorithm at // http://rosettacode.org/wiki/Longest_common_subsequence#JavaScript function diffseq(a, b) { var i, j, m, n, row = [], c = [], left, diag, latch; var skip, edit = []; m = a.length; n = b.length; // ignore equal elements at both ends for (; m && n; m--, n--) if (!equal(a[m - 1], b[n - 1])) break; for (skip = 0; m && n; skip++, m--, n--) if (!equal(a[skip], b[skip])) break; if (m && n) { //build the c-table for (j = 0; j < n; row[j++] = 0); for (i = 0 ; i < m; i++) { c[i] = row = row.slice(); for (diag = 0, j = 0; j < n; j++, diag = latch) { latch = row[j]; if (equal(a[i + skip], b[j + skip])) row[j] = diag + 1; else { left = row[j - 1] || 0; if (left > row[j]) row[j] = left; } } } i--, j--; //row[j] now contains the length of the lcs //compute the edit sequence from the table while (i > -1 && j > -1) { switch (c[i][j]) { default: edit.unshift('='); i--; j--; break; case j && c[i][j - 1]: edit.unshift('+'); j--; break; case i && c[i - 1][j]: edit.unshift('-'); i--; break; } } } else { i = m - 1; j = n - 1; } for (; j > -1; j--) edit.unshift('+'); for (; i > -1; i--) edit.unshift('-'); var edit_replace = []; for (n = 0; n < edit.length; n++) { if (edit[n] === '-') { for (i = n + 1; i < edit.length && edit[i] === '-'; i++); for (j = i; j < edit.length && edit[j] === '+'; j++); if (i - n == j - i) { while (i++ < j) edit_replace.push('*'); n = j - 1; } else { for (; n < j; n++) edit_replace.push(edit[n]); n--; } } else edit_replace.push(edit[n]); } // console.log('>>> ' + skip + ' ' + edit.join('') + ' ' + edit_replace.join('')); edit = edit_replace; for (i = 0, j = 0, n = 0; n < edit.length; n++) { switch(edit[n]) { case '*': diff_recursive(a[i + skip], b[j + skip], j + skip); case '=': i++; j++; break; case '-': result('remove', j + skip); i++; break; case '+': result('add', j + skip, b[j + skip]); j++; break; } } } function diff_arrays(a, b, tail) { if (tail !== undefined) path.push(tail); diffseq(a, b); if (tail !== undefined) path.pop(); } function diff_objects(a, b, tail) { if (tail !== undefined) path.push(tail); for (var key in a) { if (key in b) diff_recursive(a[key], b[key], key); else result('remove', key); } for (var key in b) { if (!(key in a)) result('add', key, b[key]); } if (tail !== undefined) path.pop(); } function diff_recursive(a, b, tail) { if (a === b) return; var ta = type(a), tb = type(b); if (ta !== tb) result('replace', tail, b); else { switch(ta) { case 'object': diff_objects(a, b, tail); break; case 'array': diff_arrays(a, b, tail); break; default: result('replace', tail, b); } } } diff_recursive(a, b, undefined); return operations; }; })(); if (typeof exports !== 'undefined') exports.diff = json_diff;