Permalink
Browse files

Initial, incomplete

  • Loading branch information...
0 parents commit 526487d9ceae8243fa72cdfbfdbd356b6666e246 @flitbit committed Nov 15, 2012
Showing with 403 additions and 0 deletions.
  1. +1 −0 .gitignore
  2. +48 −0 Readme.md
  3. +26 −0 examples/example1.js
  4. +1 −0 index.js
  5. +187 −0 lib/diff.js
  6. +40 −0 package.json
  7. +4 −0 test/all.js
  8. +47 −0 test/diff-scenarios.js
  9. +49 −0 test/diff-test.js
@@ -0,0 +1 @@
+node_modules
@@ -0,0 +1,48 @@
+# diff
+
+Calculates the difference between two javascript objects.
+
+``` javascript
+var diff = require('diff').diff;
+
+var lhs = {
+ name: 'my object',
+ description: 'it\'s an object!',
+ details: {
+ it: 'has',
+ an: 'array',
+ with: ['a', 'few', 'elements']
+ }
+};
+
+var rhs = {
+ name: 'updated object',
+ description: 'it\'s an object!',
+ details: {
+ it: 'has',
+ an: 'array',
+ with: ['a', 'few', 'more', 'elements', { than: 'before' }]
+ }
+};
+
+var differences = diff(lhs, rhs);
+
+```
+
+### Differences
+
+Differences are reported as one or more change records. Change records have the following structure:
+
+* `kind` - indicates the kind of change; will be one of the following:
+** `N` - indicates a newly added property/element
+** `D` - indicates a property/element was deleted
+** `E` - indicates a property/element was edited
+** `A` - indicates a change occurred within an array
+* `path` - the property path (from the left-hand-side root)
+* `lhs` - the value on the left-hand-side of the comparison (undefined if kind === 'N')
+* `rhs` - the value on the right-hand-side of the comparison (undefined if kind === 'D')
+* `index` - when kind === 'A', indicates the array index where the change occurred
+* `item` - when kind === 'A', contains a nested change record indicating the change that occurred at the array index
+
+Change records are generated for all structural differences between each object's own properties and array elements.
+the prototype
@@ -0,0 +1,26 @@
+var util = require('util'),
+diff = require('../index').diff;
+
+var lhs = {
+ name: 'my object',
+ description: 'it\'s an object!',
+ details: {
+ it: 'has',
+ an: 'array',
+ with: ['a', 'few', 'elements']
+ }
+};
+
+var rhs = {
+ name: 'updated object',
+ description: 'it\'s an object!',
+ details: {
+ it: 'has',
+ an: 'array',
+ with: ['a', 'few', 'more', 'elements', { than: 'before' }]
+ }
+};
+
+var differences = diff(lhs, rhs);
+util.log(util.inspect(differences, false, 99));
+
@@ -0,0 +1 @@
+module.exports = require('./lib/diff');
@@ -0,0 +1,187 @@
+({ define: typeof define === "function"
+ ? define // browser
+ : function(F) { F(require,exports,module); } }). // Node.js
+ define(function (require, exports, module) {
+ "use strict";
+
+ function arrayRemove(arr, from, to) {
+ var rest = arr.slice((to || from) + 1 || arr.length);
+ arr.length = from < 0 ? arr.length + from : from;
+ arr.push.apply(arr, rest);
+ return arr;
+ }
+
+ var recordDifferences;
+
+ function deepDiff(lhs, rhs, changes, path, key, stack) {
+ path = path || [];
+ var currentPath = path.slice(0);
+ if (key) { currentPath.push(key); }
+ var ltype = typeof lhs;
+ var rtype = typeof rhs;
+ if (ltype === 'undefined') {
+ if (rtype !== 'undefined') {
+ changes({kind: 'N', path: currentPath, rhs: rhs });
+ }
+ } else if (rtype === 'undefined') {
+ changes({kind: 'D', path: currentPath, lhs: lhs});
+ } else if (ltype !== rtype) {
+ changes({kind: 'E', path: currentPath, lhs: lhs, rhs: rhs});
+ } else if (ltype === 'object') {
+ stack = stack || [];
+ if (stack.indexOf(lhs) < 0) {
+ stack.push(lhs);
+ if (Array.isArray(lhs)) {
+ var i, ea = function(d) {
+ changes({
+ kind: 'A',
+ path: currentPath,
+ index: i,
+ item: d
+ });
+ };
+ for(i = 0; i < lhs.length; i++) {
+ if (i >= rhs.length) {
+ changes({
+ kind: 'A',
+ path: currentPath,
+ index: i,
+ item: {
+ kind: 'D',
+ lhs: lhs[i] }
+ });
+ } else {
+ deepDiff(lhs[i], rhs[i], ea, [], null, stack);
+ }
+ }
+ while(i < rhs.length) {
+ changes({
+ kind: 'A',
+ path: currentPath,
+ index: i,
+ item: {
+ kind: 'N',
+ rhs: rhs[i++] }
+ });
+ }
+ } else {
+ var akeys = Object.keys(lhs);
+ var pkeys = Object.keys(rhs);
+ akeys.forEach(function(k) {
+ var i = pkeys.indexOf(k);
+ if (i >= 0) {
+ deepDiff(lhs[k], rhs[k], changes, currentPath, k, stack);
+ pkeys = arrayRemove(pkeys, i);
+ } else {
+ deepDiff(lhs[k], undefined, changes, currentPath, k, stack);
+ }
+ });
+ pkeys.forEach(function(k) {
+ deepDiff(undefined, rhs[k], changes, currentPath, k, stack);
+ });
+ }
+ stack.length = stack.length - 1;
+ }
+ } else if (lhs !== rhs) {
+ changes({kind: 'E', path: currentPath, lhs: lhs, rhs: rhs});
+ }
+ }
+
+ function accumulateDiff(lhs, rhs, accum) {
+ accum = accum || [];
+ deepDiff(lhs, rhs, function(diff) {
+ if (diff) {
+ accum.push(diff);
+ }
+ });
+ return (accum.length) ? accum : undefined;
+ }
+
+ function applyArrayChange(arr, index, change) {
+ if (change.path && change.path.length) {
+ // the structure of the object at the index has changed...
+ var it = arr[index], i, u = change.path.length - 1;
+ for(i = 0; i < u; i++){
+ it = it[change.path[i]];
+ }
+ switch(change.kind) {
+ case 'A':
+ // Array was modified...
+ // it will be an array...
+ applyArrayChange(it, change.index, change.item);
+ break;
+ case 'D':
+ // Item was deleted...
+ delete it[change.path[i]];
+ break;
+ case 'E':
+ case 'N':
+ // Item was edited or is new...
+ it[change.path[i]] = change.rhs;
+ break;
+ }
+ } else {
+ // the array item is different...
+ switch(change.kind) {
+ case 'A':
+ // Array was modified...
+ // it will be an array...
+ applyArrayChange(arr[index], change.index, change.item);
+ break;
+ case 'D':
+ // Item was deleted...
+ arr = arrayRemove(arr, index);
+ break;
+ case 'E':
+ case 'N':
+ // Item was edited or is new...
+ arr[index] = change.rhs;
+ break;
+ }
+ }
+ return arr;
+ }
+
+ function applyChange(target, source, change) {
+ if (target && source && change) {
+ var it = target, i, u;
+ u = change.path.length - 1;
+ for(i = 0; i < u; i++){
+ it = it[change.path[i]];
+ }
+ switch(change.kind) {
+ case 'A':
+ // Array was modified...
+ // it will be an array...
+ applyArrayChange(it[change.path[i]], change.index, change.item);
+ break;
+ case 'D':
+ // Item was deleted...
+ delete it[change.path[i]];
+ break;
+ case 'E':
+ case 'N':
+ // Item was edited or is new...
+ it[change.path[i]] = change.rhs;
+ break;
+ }
+ }
+ }
+
+ function applyDiff(target, source, filter) {
+ if (target && source) {
+ var onChange = function(change) {
+ if (!filter || filter(target, source, change)) {
+ applyChange(target, source, change);
+ }
+ };
+ deepDiff(target, source, onChange);
+ }
+ }
+
+ exports.diff = accumulateDiff;
+ exports.observableDiff = deepDiff;
+ exports.applyDiff = applyDiff;
+ exports.applyChange = applyChange;
+ });
+
@@ -0,0 +1,40 @@
+{
+ "name": "diff",
+ "description": "Javascript module that calculates the deep-difference between objects.",
+ "version": "0.1.0",
+ "keywords": [
+ "diff",
+ "difference",
+ "compare",
+ "change-tracking"
+ ],
+ "author": {
+ "name": "Phillip Clark",
+ "email": "phillip@flitbit.org"
+ },
+ "repository": { "type": "git", "url": "git://github.com/flitbit/diff.git" },
+ "main": "./index.js",
+ "directories": {
+ "lib": "./lib",
+ "test": "./test"
+ },
+ "devDependencies" : {
+ "should": {
+ "version": "1.2.0"
+ },
+ "vows": {
+ "version": "0.6.4",
+ "dependencies": {
+ "eyes": {
+ "version": "0.1.8"
+ },
+ "diff": {
+ "version": "1.0.4"
+ }
+ }
+ }
+ },
+ "scripts": {
+ "test": "node test/all.js"
+ }
+}
@@ -0,0 +1,4 @@
+var vows = require('vows');
+var options = { reporter: require('../node_modules/vows/lib/vows/reporters/spec') };
+
+require('./diff-test.js').batch.run(options);
@@ -0,0 +1,47 @@
+var util = require('util'),
+extend = require('extend'),
+should = require('should'),
+_ = require('lodash'),
+diff = require('../index').diff,
+apply = require('../index').applyDiff;
+
+function f0() {};
+function f1() {};
+
+var one = { it: 'be one', changed: false, with: { nested: 'data'}, f: f1};
+var two = { it: 'be two', updated: true, changed: true, with: {nested: 'data', and: 'other', plus: one} };
+var circ = {};
+var other = { it: 'be other', numero: 34.29, changed: [ { it: 'is the same' }, 13.3, 'get some' ], with: {nested: 'reference', plus: circ} };
+var circular = extend(circ, { it: 'be circ', updated: false, changed: [ { it: 'is not same' }, 13.3, 'get some!', {extra: 'stuff'}], with: { nested: 'reference', circular: other } });
+
+util.log(util.inspect(diff(one, two), false, 99));
+util.log(util.inspect(diff(two, one), false, 99));
+
+util.log(util.inspect(diff(other, circular), false, 99));
+
+var clone = extend({}, one);
+apply(clone, two);
+util.log(util.inspect(clone, false, 99));
+_.isEqual(clone, two).should.be.true;
+_.isEqual(clone, one).should.be.false;
+
+clone = extend({}, circular);
+apply(clone, other);
+util.log(util.inspect(clone, false, 99));
+_.isEqual(clone, other).should.be.true;
+_.isEqual(clone, circular).should.be.false;
+
+
+var array = { name: 'array two levels deep', item: { arr: ['it', { has: 'data' }]}};
+var arrayChange = { name: 'array change two levels deep', item: { arr: ['it', { changes: 'data' }]}};
+
+util.log(util.inspect(diff(array, arrayChange), false, 99));
+clone = extend({}, array);
+apply(clone, arrayChange);
+util.log(util.inspect(clone, false, 99));
+_.isEqual(clone, arrayChange).should.be.true;
+
+var one_prop = { one: 'property' };
+var d = diff(one_prop, {});
+d.length.should.be.eql(1);
+
Oops, something went wrong.

0 comments on commit 526487d

Please sign in to comment.