8

I have two large, nested javascript objects and I would like to compare them and create an object that represents only the differences. I intend to use this to create a PATCH request.

Given oldObj and newObj:

  • Properties that are only on newObj should be in the diff
  • Properties that are only on oldObj should be in the diff
  • Properties that are on both objects should use the value from newObj if the value is an array, string, or number
  • The objects should be recursively compared
  • No need to merge arrays fancily, complete replace is ok

This may look like a duplicate, but I don't think it is. This solution (1) is one level deep only (the answer below is non-recursive, blows up on arrays, and is not bi-directional). this solution (2) returns unchanged properties is not bi-directional.

Target input/output:

diff({a:1},{a:0}); // {a:0}

diff({a:1},{b:1}); // {a:1,b:1}

diff({
  a: { x: 1 },
  b: 1
},
{
  a: { x: 0 },
  b: 1
}) // {a:{x:0}}

diff({a:[1,3,5,7]},{a:[1,3,7]}); // {a:[1,3,7]} 

I am using the following method which is modified from solution 1. It meets all criteria except diff({a:1},{b:1}) // {a:1,b:1} because it only compares in one direction.

jsonDiff = function(oldObject, newObject) {
  var diff, i, innerDiff;
  diff = {};
  for (i in newObject) {
    innerDiff = {};
    if (_.isArray(newObject[i])) {
      if (!_.isEqual(newObject[i], oldObject[i])) {
        diff[i] = newObject[i];
      }
    } else if (typeof newObject[i] === 'object') {
      innerDiff = jsonDiff(oldObject[i], newObject[i]);
      if (!_.isEmpty(innerDiff)) {
        diff[i] = innerDiff;
      }
    } else if (!oldObject) {
      diff[i] = newObject[i];
    } else if (!oldObject.hasOwnProperty(i)) {
      diff[i] = newObject[i];
    } else if (oldObject[i] !== newObject[i]) {
      diff[i] = newObject[i];
    }
  }
  return diff;
};

I have seen the jsonDiffPatch library, but I do not need all the metadata it creates, just the raw diff object. Is there a mini library that just does this? Seems sort of necessary to implement PATCH nicely, but I can't find one. Anyone have a small gist for this?

Community
  • 1
  • 1
SimplGy
  • 20,079
  • 15
  • 107
  • 144

2 Answers2

1

Here is a function that should work for you, more comments than code:

// diffObjs: return differences between JavaScript values
//
// Function:
//
//    Compare two JavaScript values, and return a two-element
//    array that contains a minimal representation of the difference
//    between the two.
//
//    Values may be scalar (e.g., string, integer, boolean) or objects,
//    including arrays.  When the two values match exactly, that is,
//    if the '===' operator between the two would return 'true', we return NULL.
//    
//    When the result contains an object or array, only data, not references,
//    are copied from the arguments.  This makes for a large size result
//    but one whose manipulation will not affect the original arguments.
//
// Args:
//    v1, v2: values to compare
//
// Specific behaviors:
//
//    *Return NULL if v1 === v2*
//
//    This happens when two scalar (non-object) values match, or when the same
//    object or array is passed in both arguments.
//    e.g.,
//        
//        var my_obj = { member1: 0, member1: 'dog' };
//        var my_array = [ 1, 'cat' ];
//        var my_int = 7;
//        var no_val = null;
//
//        diffObjs(my_int, my_int)        ==> NULL
//        diffObjs(1, 1)                  ==> NULL
//        diffObjs(my_obj, my_obj)        ==> NULL
//        diffObjs({x:1,y:2}, {x:1,y:2})  ==> NULL
//        diffObjs(my_array, my_array)    ==> NULL
//        diffObjs([1,'a'], [1,'1'])      ==> NULL
//        diffObjs(null, null)            ==> NULL
//        diffObjs(no_val, null)          ==> NULL
//
//    *Return copies of v1 and v2 on type mismatch*:
//
//    When type of v1 and v2 are different or one is an array and the other
//    is an object, the result array will contain exect copies of both
//    v1 and v2.
//
//    *Return minimal representation of differences among non-array objects*:
//
//    Otherwise, when two objects are passed in, element 0
//    in the result array contains the members and their values
//    that exist in v1 but not v2, or members that exist in both
//    v1 and v2 that have different values.  Element 1 contains
//    the same but with respect to v2, that is members and their
//    values that exist in v2 but not v1, or members that exist in
//    both v1 and v2 that have different values.
//    
//    Note: The members are represented in the result objects only when
//    they are specific to the object of the corresponding value argument
//    or when the members exist in both and have different values.  The
//    caller therefore can tell whether the object mismatch exists 
//    because of specificity of a member to one object vs. a mismatch
//    in values where one is null and the other is not.
//
//    Examples:
//        diffObjs({a:10, b:"dog"}, {a:1, b:"dog"}    ==> [ {a:10}, {a:1} ]
//        diffObjs({a:10},          {a:10, b:"dog"}   ==> [ {}, {b:"dog"} ]
//        diffObjs({a:10, c:null},  {a:10, b:"dog"}   ==> [ {c:null}, {b:"dog"} ]
//        diffObjs({a:[1], b:"cat"},{a:1, b:"dog"}    ==> [ {a:[1], b:"cat"}, {a:1, b:"dog"} ]
//        diffObjs(
//            {a:{ m1:"x", m2:"y"}, b:3 },
//            {a:{ m1:"x", m2:"z", m3:1 }, b:3 } )    ==> [ {a:{m2:"y"}}, {a:{m2:"z",m3:1}} ]
//
//    *Return copies of compared arrays when differing by position or value*
//
//    If the two arguments arrays, the results in elements 0 and 1
//    will contain results in array form that do not match with respect
//    to both value and order.  If two positionally corresponding
//    elements in the array arguments have identical value (e.g., two
//    scalars with matching values or two references to the same object), 
//    the corresponding values in the array will be null.  The
//    cardinality of the arrays within the result array will therefore
//    always match that of the corresponding arguments.
//
//    Examples:
//        diffObjs([1,2],        [1,2])   ==> [ [null,null], [null,null] ]
//        diffObjs([1,2],        [2,1])   ==> [ [1,2], [2,1] ]
//        diffObjs([1,2],        [1,2,3]) ==> [ [1,2,null], [2,1,3] ]
//        diffObjs([1,1,2,3],    [1,2,3]) ==> [ [null,1,2,3], [null,2,3] ]
//

var diffObjs = function(v1, v2) {

    // return NULL when passed references to
    // the same objects or matching scalar values
    if (v1 === v2) {
        return null;
    }
    var cloneIt = function(v) {
        if (v == null || typeof v != 'object') {
            return v;
        }

        var isArray = Array.isArray(v);

        var obj = isArray ? [] : {};
        if (!isArray) {
            // handles function, etc
            Object.assign({}, v);
        }

        for (var i in v) {
            obj[i] = cloneIt(v[i]);
        }

        return obj;
    }

    // different types or array compared to non-array
    if (typeof v1 != typeof v2 || Array.isArray(v1) != Array.isArray(v2)) {
        return [cloneIt(v1), cloneIt(v2)];
    }

    // different scalars (no cloning needed)
    if (typeof v1 != 'object' && v1 !== v2) {
        return [v1, v2];
    }

    // one is null, the other isn't
    // (if they were both null, the '===' comparison
    // above would not have allowed us here)
    if (v1 == null || v2 == null) {
        return [cloneIt(v1), cloneIt(v2)]; 
    }

    // We have two objects or two arrays to compare.
    var isArray = Array.isArray(v1);

    var left = isArray ? [] : {};
    var right = isArray ? [] : {};

    for (var i in v1) {
        if (!v2.hasOwnProperty(i)) {
            left[i] = cloneIt(v1[i]);
        } else {
            var sub_diff = diffObjs(v1[i], v2[i]);
            // copy the differences between the 
            // two objects into the results.
            // - If the object is array, use 'null'
            //   to indicate the two corresponding elements
            //   match.
            //
            // - If the object is not an array, copy only
            //   the members that point to an unmatched
            //   object.
            if (isArray || sub_diff) { 
                left[i] = sub_diff ? cloneIt(sub_diff[0]) : null;
                right[i] = sub_diff ? cloneIt(sub_diff[1]) : null;
            }
        }
    }

    for (var i in v2) {
        if (!v1.hasOwnProperty(i)) {
            right[i] = cloneIt(v2[i]);
        }
    }

    return [ left, right];
};
blackcatweb
  • 1,003
  • 1
  • 10
  • 11
0

this is a bit long for an answer, but i don't have it published yet.

function monitor(obj, callBack){


var api={
        patch: patchObjectWithDiff, 
        init: init, 
        resolve: resolve, 
        snapshot: snapshot, 
        diff: diff, 
        update: changeMonitor 
   };



function merge2(o, ob) {
     for (var z in ob) {
        if (ob.hasOwnProperty(z)) {
           if(typeof ob[z]=="object"){ 
                if(ob[z]==null){
                    delete o[z];
                }else{
                    merge2( o[z] || {},  ob[z]);
                }

            }else{
                 o[z] = ob[z];
            }

        }
    }
    return o;
}






function snapshot(obj) { 
    var out = [];
    function merge3(ob, path) {
        path = path || [];
                var tp;
        for(var z in ob) {
            if(ob.hasOwnProperty(z)) {
                if(ob[z] && typeof ob[z] == "object" && [Date, RegExp].indexOf(ob[z].constructor) == -1) {

                                        tp=path.concat(z);
                    out.push({
                                                path:  tp.join("`"),
                                                path2:  tp,
                                                dt:  "set",
                                                date:  +new Date,
                                                v: Array.isArray(ob[z]) ? "[]" : "{}"
                                        });

                    merge3(ob[z], path.concat(z));
                } else {
                                        tp=path.concat(z);
                    out.push({
                                                path:  tp.join("`"),
                                                path2:  tp,
                                                type:  "set",
                                                dt:  +new Date,
                                                v: JSON.stringify(ob[z]) 
                                        });
                }
            }
        }
    }

    merge3(obj);
    return out;
};



function diff(d1, d2){

  var out=d2.filter(function(a,b,c){
     var ov=JSON.stringify(a.v);
     return d1.some(function(aa,bb){ return aa.path==a.path && JSON.stringify(aa.v) != ov;  });
  }),

  // find deletions
  dels=d1.filter(function(a,b,c){
     return !d2.some(function(aa,bb){ if(aa.path==a.path ){  return true; };  });
  }),

  allPaths=dels.map(function(a){return a.path}).sort(),

  dels2=dels.filter(function eliminateUnneededSubBranches(a){

        var pos=allPaths.indexOf( a.path2.slice(0,-1).join("`") );

        return pos==-1 || pos >= allPaths.indexOf(a.path);

  }).map(function(a){a.type="del"; delete a.v; return a;});


  [].push.apply(out, dels2);


 //find inserts


var outNew=d2.filter(function(a,b,c){
     var ov=JSON.stringify(a.v);
     return !d1.some(function(aa,bb){ return aa.path==a.path  });
  });

 [].push.apply(out, outNew);



  return out.map(function(a){
       var x= {
         dt: a.dt,
         k: a.path2
       };

       if(a.hasOwnProperty("v")){ x.v=a.v; }

       return x;

            a.k=a.path2; 
            delete a.path; 
            delete a.path2; 
            delete a.type;
      return a;
  });
}



function resolve(path, object){
  var tob=object;
  path.map(function(a){ return (tob=tob[a])||tob; })
 return tob;
}








function patchObjectWithDiff(diff, object){

  diff.forEach(function(a,b,c){
       var p= resolve(a.k.slice(0,-1), object), 
           k= a.k.slice(-1)[0];

       if(a.hasOwnProperty("v")){ //set:
              p[k]=JSON.parse(a.v);
             if(String(p[k]).match(/Z$/)){ p[k]=new Date(''+p[k]) || p[k]; }
        }else{ // del:
           if(Array.isArray(p)){ p.splice(k,1); }else{ delete p[k]; }
       }
  });

   return object;
}











    var init=snapshot(JSON.parse(JSON.stringify(obj))),
          id=Math.random()+ Number(new Date());


    var init=snapshot(obj);

    function changeMonitor(){
        var thisTime=snapshot(obj),
               diffs=diff(init, thisTime);
        if(diffs.length){  
            api.diffs=diffs;
            (callBack||console.log.bind(console))("objectUpdate", diffs );
            init=thisTime;
        }//end if change?
    }

    setInterval(changeMonitor, 2500);

 return api;

}

demo / example usage:

var obj={a:1, b:[1,2,3], c: false}; // a model object
var dupe=JSON.parse(JSON.stringify(obj)); // a cheap clone of the data for demo use

//subscribe this object to updates    
var mon=monitor(obj, function(type, changes){console.log(type,  changes); });

// make some changes to the object:
obj.e="cool!";
obj.b.push(5);
obj.a=7;

// manually call update instead of waiting for the bundler:
mon.update();

// now apply stored changes to the clone of the orig data:
var updatedDupe= mon.patch(mon.diffs, dupe);

// use a cheap and easy but not production-reliable to compare the objects:
JSON.stringify(updatedDupe)==JSON.stringify(obj); // should be true

tested in chrome and firefox.

be aware that this particular demo's use of JSON depends on some luck, and consistent key ordering, which is not guaranteed by the JS spec. Key order doesn't really matter, but it might cause the JSON.stringify() == comparison to fail, even though the object's properties are indeed sync'd. This is just for demonstration's sake to get a true/false answer if it works, don't beat me up...

All diffs in the change list come with three keys:

  {"dt":1392348959730,"k":["b","3"],"v":"5"}
dt: a timestamp of when the change was discovered
k: the key path where the change was detected
v: what the discovered changed value is as of dt

This script is hot off the press and i haven't had time to write proper documentation, but i figure it might help or at least inspire a solution that works for you.

dandavis
  • 16,370
  • 5
  • 40
  • 36