Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get the delta of two javascript objects

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?

like image 296
SimplGy Avatar asked Mar 01 '14 01:03

SimplGy


People also ask

How to compare two objects in JavaScript and get differences?

You can JavaScript compare two objects and get differences by iterate object over the loop and checking for equality in both objects, if the values at any point don't match we will update a flag, exit out of the loop and return the specific key.

How do you compare objects in JavaScript?

Comparing objects is easy, use === or Object.is(). This function returns true if they have the same reference and false if they do not. Again, let me stress, it is comparing the references to the objects, not the keys and values of the objects. So, from Example 3, Object.is(obj1,obj2); would return false.


1 Answers

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];
};
like image 102
blackcatweb Avatar answered Nov 09 '22 21:11

blackcatweb