Diferencia profunda genérica entre dos objetos.
Tengo dos objetos: oldObj
y newObj
.
Los datos oldObj
se utilizaron para completar un formulario y newObj
son el resultado de que el usuario cambie los datos en este formulario y los envíe.
Ambos objetos son profundos, es decir. tienen propiedades que son objetos o conjuntos de objetos, etc.; pueden tener n niveles de profundidad, por lo que el algoritmo de diferenciación debe ser recursivo.
Ahora necesito no solo descubrir qué se cambió (como en agregado/actualizado/eliminado) de oldObj
a newObj
, sino también cómo representarlo mejor.
Hasta ahora, mi idea era simplemente crear un genericDeepDiffBetweenObjects
método que devolviera un objeto en el formulario {add:{...},upd:{...},del:{...}}
, pero luego pensé: alguien más debe haber necesitado esto antes.
Entonces... ¿alguien conoce una biblioteca o un fragmento de código que haga esto y tal vez tenga una manera aún mejor de representar la diferencia (de una manera que aún sea serializable en JSON)?
Actualizar:
He pensado en una mejor manera de representar los datos actualizados, usando la misma estructura de objetos que newObj
, pero convirtiendo todos los valores de propiedad en objetos en el formulario:
{type: '<update|create|delete>', data: <propertyValue>}
Entonces si newObj.prop1 = 'new value'
y oldObj.prop1 = 'old value'
se estableceríareturnObj.prop1 = {type: 'update', data: 'new value'}
Actualización 2:
Se vuelve realmente complicado cuando llegamos a propiedades que son matrices, ya que la matriz [1,2,3]
debe contarse como igual a [2,3,1]
, lo cual es bastante simple para matrices de tipos basados en valores como string, int y bool, pero se vuelve realmente difícil de manejar cuando se trata de matrices de tipos de referencia como objetos y matrices.
Ejemplos de matrices que deberían ser iguales:
[1,[{c: 1},2,3],{a:'hey'}] and [{a:'hey'},1,[3,{c: 1},2]]
No sólo es bastante complejo comprobar este tipo de igualdad profunda de valores, sino también encontrar una buena manera de representar los cambios que podrían producirse.
Escribí una pequeña clase que hace lo que quieres, puedes probarla aquí .
Lo único que difiere de tu propuesta es que no considero
[1,[{c: 1},2,3],{a:'hey'}]
y
[{a:'hey'},1,[3,{c: 1},2]]
ser igual, porque creo que las matrices no son iguales si el orden de sus elementos no es el mismo. Por supuesto, esto se puede cambiar si es necesario. Además, este código se puede mejorar aún más para que tome la función como argumento que se usará para formatear el objeto diff de forma arbitraria en función de los valores primitivos pasados (ahora este trabajo se realiza mediante el método "compareValues").
var deepDiffMapper = function () {
return {
VALUE_CREATED: 'created',
VALUE_UPDATED: 'updated',
VALUE_DELETED: 'deleted',
VALUE_UNCHANGED: 'unchanged',
map: function(obj1, obj2) {
if (this.isFunction(obj1) || this.isFunction(obj2)) {
throw 'Invalid argument. Function given, object expected.';
}
if (this.isValue(obj1) || this.isValue(obj2)) {
return {
type: this.compareValues(obj1, obj2),
data: obj1 === undefined ? obj2 : obj1
};
}
var diff = {};
for (var key in obj1) {
if (this.isFunction(obj1[key])) {
continue;
}
var value2 = undefined;
if (obj2[key] !== undefined) {
value2 = obj2[key];
}
diff[key] = this.map(obj1[key], value2);
}
for (var key in obj2) {
if (this.isFunction(obj2[key]) || diff[key] !== undefined) {
continue;
}
diff[key] = this.map(undefined, obj2[key]);
}
return diff;
},
compareValues: function (value1, value2) {
if (value1 === value2) {
return this.VALUE_UNCHANGED;
}
if (this.isDate(value1) && this.isDate(value2) && value1.getTime() === value2.getTime()) {
return this.VALUE_UNCHANGED;
}
if (value1 === undefined) {
return this.VALUE_CREATED;
}
if (value2 === undefined) {
return this.VALUE_DELETED;
}
return this.VALUE_UPDATED;
},
isFunction: function (x) {
return Object.prototype.toString.call(x) === '[object Function]';
},
isArray: function (x) {
return Object.prototype.toString.call(x) === '[object Array]';
},
isDate: function (x) {
return Object.prototype.toString.call(x) === '[object Date]';
},
isObject: function (x) {
return Object.prototype.toString.call(x) === '[object Object]';
},
isValue: function (x) {
return !this.isObject(x) && !this.isArray(x);
}
}
}();
var result = deepDiffMapper.map({
a: 'i am unchanged',
b: 'i am deleted',
e: {
a: 1,
b: false,
c: null
},
f: [1, {
a: 'same',
b: [{
a: 'same'
}, {
d: 'delete'
}]
}],
g: new Date('2017.11.25')
}, {
a: 'i am unchanged',
c: 'i am created',
e: {
a: '1',
b: '',
d: 'created'
},
f: [{
a: 'same',
b: [{
a: 'same'
}, {
c: 'create'
}]
}, 1],
g: new Date('2017.11.25')
});
console.log(result);
Usando subrayado, una diferencia simple:
var o1 = {a: 1, b: 2, c: 2},
o2 = {a: 2, b: 1, c: 2};
_.omit(o1, function(v,k) { return o2[k] === v; })
Resultados en las partes o1
que corresponden pero con valores diferentes en o2
:
{a: 1, b: 2}
Sería diferente para una diferencia profunda:
function diff(a,b) {
var r = {};
_.each(a, function(v,k) {
if(b[k] === v) return;
// but what if it returns an empty object? still attach?
r[k] = _.isObject(v)
? _.diff(v, b[k])
: v
;
});
return r;
}
Como señaló @Juhana en los comentarios, lo anterior es solo una diferencia a-->b y no es reversible (lo que significa que las propiedades adicionales en b se ignorarían). Utilice en su lugar a-->b-->a:
(function(_) {
function deepDiff(a, b, r) {
_.each(a, function(v, k) {
// already checked this or equal...
if (r.hasOwnProperty(k) || b[k] === v) return;
// but what if it returns an empty object? still attach?
r[k] = _.isObject(v) ? _.diff(v, b[k]) : v;
});
}
/* the function */
_.mixin({
diff: function(a, b) {
var r = {};
deepDiff(a, b, r);
deepDiff(b, a, r);
return r;
}
});
})(_.noConflict());
Consulte http://jsfiddle.net/drzaus/9g5qoxwj/ para ver un ejemplo completo+pruebas+mixins